Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
dvtng committed Jun 19, 2024
0 parents commit 72e434c
Show file tree
Hide file tree
Showing 13 changed files with 470 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# cosmos
Binary file added bun.lockb
Binary file not shown.
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "cosmos",
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^18.3.3"
},
"peerDependencies": {
"react": "^18.3.1",
"typescript": "^5.0.0"
},
"dependencies": {
"safe-stable-stringify": "^2.4.3",
"valtio": "^1.13.2"
}
}
51 changes: 51 additions & 0 deletions src/core-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* A model describes how data is retrieved and updated over time.
*
* Models can have params. Each unique set of params initializes a new
* model instance.
*/
export type Model<T, P extends object | void> = {
// Globally unique type name for this model.
type: string;
// All logic for how this model's data is retrieved and updated over time
// is contained in this init function.
// It's called once for each unique set of params.
init: (args: { params: P; atom: Atom<T> }) => void | (() => void);
};

/**
* An atom contains the state needed to manage a single model instance.
*/
export type Atom<T = unknown> = {
value: T | undefined;
initialized: boolean;
subscribers: Set<number>;
cleanup?: () => void;
promise?: Promise<T>;
};

/**
* A query for a model instance.
*/
export type Query<T, P extends object | void> = {
$key: string; // Serialized model type and params.
model: Model<T, P>;
params: P;
};

/**
* useModel enables synchronous access to a model instance's current value.
*/
export type UseModel = {
// When wait: true, we can assume that the value is available.
<T, P extends object | void = void>(
query: Query<T, P>,
options: { wait: true }
): T;

// Otherwise, value can be undefined.
<T, P extends object | void = void>(
query: Query<T, P> | null | undefined,
options?: { wait?: boolean }
): T | undefined;
};
86 changes: 86 additions & 0 deletions src/derived-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { watch } from "valtio/utils";
import type { Model, Query, UseModel } from "./core-types";
import { getNextSubscriberId, subscribe, unsubscribe } from "./state";

/**
* A DerivedModel synchronously computes its value based on other models.
* To do this, it is provided a getModel function that behaves similarly to
* useModel.
*/
export type DerivedModel<T, P extends object | void> = {
type?: string;
derive: (getModel: UseModel, params: P) => T | undefined;
};

export function isDerivedModel<T, P extends object | void>(
model: any
): model is DerivedModel<T, P> {
return "derive" in model;
}

let nextDerivedModelId = 0;

const Suspend = {};

export function fromDerivedModel<T, P extends object | void>(
model: DerivedModel<T, P>
): Model<T, P> {
const type = model.type ?? `$Derived-${nextDerivedModelId++}`;

return {
type,
init({ params, atom }) {
const subscriberId = getNextSubscriberId();

let queries: Record<string, Query<any, any>> = {};

const unwatch = watch((get) => {
const nextQueries: Record<string, Query<any, any>> = {};

const getModel: UseModel = function (query, options = {}) {
if (!query) {
return;
}

const atom = subscribe(query, subscriberId);
nextQueries[query.$key] = query;

// Tell valtio to track the dependency
get(atom);

// We mimic React's suspense in our derived model. Throwing the
// Suspend object simply tells us to wait to re-run the derivation.
if (options.wait && atom.value === undefined) {
throw Suspend;
}

return atom.value;
};

try {
const value = model.derive(getModel, params);
atom.value = value;
} catch (e) {
if (e !== Suspend) {
throw e;
}
}

// Unsubscribe from old queries
for (const key in queries) {
if (!nextQueries[key]) {
unsubscribe(queries[key], subscriberId);
}
}
queries = nextQueries;
});

return function cleanup() {
unwatch();
for (const key in queries) {
unsubscribe(queries[key], subscriberId);
}
};
},
};
}
31 changes: 31 additions & 0 deletions src/get-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ref } from "valtio";
import type { Query } from "./core-types";
import { getAtom, getNextSubscriberId, subscribe, unsubscribe } from "./state";
import { subscribeKey } from "valtio/utils";

/**
* Resolve a model query as a promise.
*/
export function getPromise<T, P extends object | void>(query: Query<T, P>) {
const atom = getAtom(query);

if (!atom.promise) {
atom.promise = ref(
atom.value !== undefined
? Promise.resolve(atom.value)
: new Promise<T>((resolve) => {
const subscriberId = getNextSubscriberId();

const unsubscribeKey = subscribeKey(atom, "value", (value) => {
resolve(value as T);
unsubscribeKey();
unsubscribe(query, subscriberId);
});

subscribe(query, subscriberId);
})
);
}

return atom.promise;
}
80 changes: 80 additions & 0 deletions src/getter-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Model } from "./core-types";

/**
* A GetterModel is an asynchronous request/response style model.
* A get() function returns a promise with the value.
*/
export type GetterModel<T, P extends object | void> = {
type: string;
get: (params: P, tools: Tools) => Promise<T>;
refresh?: number;
};

type Tools = {
refreshIn: (ms: number) => void;
};

export function isGetterModel<T, P extends object | void>(
model: any
): model is GetterModel<T, P> {
return "get" in model;
}

export function fromGetterModel<T, P extends object | void>(
model: GetterModel<T, P>
): Model<T, P> {
return {
type: model.type,
init({ params, atom }) {
let cleanedUp = false;
let scheduledRefreshTime: number | null = null;
let timerId: number | null = null;

function get() {
if (cleanedUp) {
return;
}

scheduledRefreshTime = null;

model.get(params, { refreshIn }).then(
(value) => {
atom.value = value;
},
() => {
// TODO handle errors
}
);
}

function refreshIn(ms: number) {
if (cleanedUp) {
return;
}

const targetTime = Date.now() + ms;
if (scheduledRefreshTime == null || targetTime < scheduledRefreshTime) {
if (timerId) {
window.clearTimeout(timerId);
}

scheduledRefreshTime = targetTime;
timerId = window.setTimeout(get, ms);
}
}

get();

if (model.refresh) {
refreshIn(model.refresh);
}

return function cleanup() {
cleanedUp = true;
if (timerId) {
window.clearTimeout(timerId);
}
};
},
};
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./model";
export * from "./use-model";
export * from "./get-promise";
32 changes: 32 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import stableStringify from "safe-stable-stringify";
import type { Query } from "./core-types";
import type { GetterModel } from "./getter-model";
import { fromGetterModel, isGetterModel } from "./getter-model";
import type { DerivedModel } from "./derived-model";
import { fromDerivedModel, isDerivedModel } from "./derived-model";

export function model<T, P extends object | void = void>(
m: GetterModel<T, P> | DerivedModel<T, P>
) {
// TODO make it easier to add new model templates
const genericModel = isGetterModel<T, P>(m)
? fromGetterModel(m)
: isDerivedModel<T, P>(m)
? fromDerivedModel(m)
: (null as never);

return function buildQuery(params: P): Query<T, P> {
return {
$key: `${genericModel.type}(${serializeParams(params)})`,
model: genericModel,
params,
};
};
}

function serializeParams<P extends object | void>(params: P) {
if (params == null) {
return "";
}
return stableStringify(params);
}
Loading

0 comments on commit 72e434c

Please sign in to comment.