-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 72e434c
Showing
13 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# cosmos |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.