From dd7b995c99c1a84a448c232c78cc6e8b97db8bb9 Mon Sep 17 00:00:00 2001 From: David Tang Date: Mon, 3 Feb 2025 16:42:51 -0800 Subject: [PATCH] Remove v1 --- cosmos/src/__tests__/derived-model.test.ts | 25 ---- cosmos/src/__tests__/get-model.test.ts | 59 +++------ cosmos/src/{v2 => }/combine-behavior.ts | 0 cosmos/src/{v2 => }/compute.ts | 0 cosmos/src/core-types.ts | 49 ------- cosmos/src/{v2 => }/core.ts | 2 +- cosmos/src/{v2 => }/cosmos.ts | 4 +- cosmos/src/{v2 => }/dev/dev-state.ts | 0 cosmos/src/{v2 => }/dev/dev.tsx | 0 cosmos/src/{v2 => }/dev/index.ts | 0 cosmos/src/{v2 => }/dev/model-view.tsx | 0 cosmos/src/{v2 => }/dev/query-view.tsx | 0 cosmos/src/{v2 => }/dom.ts | 0 cosmos/src/get-model.ts | 37 ++++-- cosmos/src/{v2 => }/get-next-subscriber-id.ts | 0 cosmos/src/index.ts | 11 +- cosmos/src/{v2 => }/later-map.ts | 0 cosmos/src/{v2 => }/later.ts | 0 cosmos/src/model.ts | 79 +++++++----- cosmos/src/null.ts | 8 -- cosmos/src/{v2 => }/persist.ts | 0 cosmos/src/{v2 => }/request.ts | 2 +- cosmos/src/{v2 => }/serialize-args.ts | 0 cosmos/src/{v2 => }/set-model.ts | 0 cosmos/src/{v2 => }/snapshot.ts | 0 cosmos/src/state.ts | 121 ------------------ cosmos/src/templates/derived-model.ts | 77 ----------- cosmos/src/templates/emitter-model.ts | 30 ----- cosmos/src/templates/getter-model.ts | 98 -------------- cosmos/src/{v2 => }/timer.ts | 4 +- cosmos/src/use-model.ts | 57 +++++---- cosmos/src/v2/example.md | 61 --------- cosmos/src/v2/get-model.ts | 30 ----- cosmos/src/v2/index.ts | 11 -- cosmos/src/v2/model.ts | 54 -------- cosmos/src/v2/use-model.ts | 45 ------- cosmos/src/{v2 => }/value.ts | 0 cosmos/src/wait-for.ts | 12 -- example/src/app-state.ts | 2 +- example/src/app.tsx | 2 +- example/src/clock.tsx | 2 +- example/src/coin-price.tsx | 2 +- example/src/counter-on-focus.tsx | 2 +- example/src/counter.tsx | 2 +- example/src/counters.tsx | 2 +- 45 files changed, 145 insertions(+), 745 deletions(-) delete mode 100644 cosmos/src/__tests__/derived-model.test.ts rename cosmos/src/{v2 => }/combine-behavior.ts (100%) rename cosmos/src/{v2 => }/compute.ts (100%) delete mode 100644 cosmos/src/core-types.ts rename cosmos/src/{v2 => }/core.ts (96%) rename cosmos/src/{v2 => }/cosmos.ts (98%) rename cosmos/src/{v2 => }/dev/dev-state.ts (100%) rename cosmos/src/{v2 => }/dev/dev.tsx (100%) rename cosmos/src/{v2 => }/dev/index.ts (100%) rename cosmos/src/{v2 => }/dev/model-view.tsx (100%) rename cosmos/src/{v2 => }/dev/query-view.tsx (100%) rename cosmos/src/{v2 => }/dom.ts (100%) rename cosmos/src/{v2 => }/get-next-subscriber-id.ts (100%) rename cosmos/src/{v2 => }/later-map.ts (100%) rename cosmos/src/{v2 => }/later.ts (100%) delete mode 100644 cosmos/src/null.ts rename cosmos/src/{v2 => }/persist.ts (100%) rename cosmos/src/{v2 => }/request.ts (97%) rename cosmos/src/{v2 => }/serialize-args.ts (100%) rename cosmos/src/{v2 => }/set-model.ts (100%) rename cosmos/src/{v2 => }/snapshot.ts (100%) delete mode 100644 cosmos/src/state.ts delete mode 100644 cosmos/src/templates/derived-model.ts delete mode 100644 cosmos/src/templates/emitter-model.ts delete mode 100644 cosmos/src/templates/getter-model.ts rename cosmos/src/{v2 => }/timer.ts (92%) delete mode 100644 cosmos/src/v2/example.md delete mode 100644 cosmos/src/v2/get-model.ts delete mode 100644 cosmos/src/v2/index.ts delete mode 100644 cosmos/src/v2/model.ts delete mode 100644 cosmos/src/v2/use-model.ts rename cosmos/src/{v2 => }/value.ts (100%) delete mode 100644 cosmos/src/wait-for.ts diff --git a/cosmos/src/__tests__/derived-model.test.ts b/cosmos/src/__tests__/derived-model.test.ts deleted file mode 100644 index 7f3cecc..0000000 --- a/cosmos/src/__tests__/derived-model.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, test } from "bun:test"; -import { model } from "../model"; -import { getModel } from "../get-model"; -import { delay } from "./util/delay"; -import { waitFor } from "../wait-for"; - -const Echo = model({ - type: "Echo", - async get({ value }: { value: string }) { - await delay(100); - return value; - }, -}); - -const UppercaseEcho = model({ - derive(useModel, { value }: { value: string }) { - const [_value] = waitFor(useModel(Echo({ value }))); - return _value.toUpperCase(); - }, -}); - -test("derive", async () => { - const [value] = await getModel(UppercaseEcho({ value: "test" })); - expect(value).toBe("TEST"); -}); diff --git a/cosmos/src/__tests__/get-model.test.ts b/cosmos/src/__tests__/get-model.test.ts index 3b8be19..a5abab4 100644 --- a/cosmos/src/__tests__/get-model.test.ts +++ b/cosmos/src/__tests__/get-model.test.ts @@ -1,50 +1,21 @@ -import { expect, test, beforeEach } from "bun:test"; -import { model } from "../model"; -import { getModel } from "../get-model"; -import { reset } from "../state"; -import { Null } from "../null"; - -let count = 0; - -const Counter = model({ - type: "Counter", - refresh: { seconds: 60 }, - async get() { - return ++count; - }, +import { expect, test } from "bun:test"; +import { model, getModel, request, compute } from "../index"; +import { delay } from "./util/delay"; + +const Echo = model((value: string) => { + return request(async () => { + await delay(100); + return value; + }); }); -beforeEach(() => { - count = 0; - reset(Counter()); +const UppercaseEcho = model((value: string) => { + return compute((get) => { + return get(Echo(value)).value.toUpperCase(); + }); }); test("getModel", async () => { - const [count] = await getModel(Counter()); - expect(count).toBe(1); -}); - -test("getModel uses cached value", async () => { - await getModel(Counter()); - const [count] = await getModel(Counter()); - expect(count).toBe(1); -}); - -test("force model to update", async () => { - await getModel(Counter()); - reset(Counter()); - const [count] = await getModel(Counter()); - expect(count).toBe(2); -}); - -test("get null model", async () => { - const enabled = false; - const [value] = await getModel(enabled ? Counter() : Null()); - expect(value).toBe(null); -}); - -test("get expiry", async () => { - const now = Date.now(); - const [_, { expiry }] = await getModel(Counter()); - expect(expiry).toBe(now + 60 * 1000); + const uppercaseEcho = await getModel(UppercaseEcho("test")); + expect(uppercaseEcho.value).toBe("TEST"); }); diff --git a/cosmos/src/v2/combine-behavior.ts b/cosmos/src/combine-behavior.ts similarity index 100% rename from cosmos/src/v2/combine-behavior.ts rename to cosmos/src/combine-behavior.ts diff --git a/cosmos/src/v2/compute.ts b/cosmos/src/compute.ts similarity index 100% rename from cosmos/src/v2/compute.ts rename to cosmos/src/compute.ts diff --git a/cosmos/src/core-types.ts b/cosmos/src/core-types.ts deleted file mode 100644 index 7a8c491..0000000 --- a/cosmos/src/core-types.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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 = { - // 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 }) => void | (() => void); -}; - -/** - * An atom contains the state needed to manage a single model instance. - */ -export type Atom = { - value: T | undefined; - initialized: boolean; - subscribers: Set; - cleanup?: () => void; - promise?: Promise>; - expiry?: number; -}; - -/** - * A query for a model instance. - */ -export type Query = { - $key: string; // Serialized model type and params. - model: Model; - params: P; -}; - -/** - * Infer a query's model type - */ -export type QueryType = Q extends Query ? T : never; - -/** - * useModel enables synchronous access to a model instance's current value. - */ -export type UseModel = ( - query: Q -) => ModelResult | undefined>; - -export type ModelResult = [T, Atom, Query]; diff --git a/cosmos/src/v2/core.ts b/cosmos/src/core.ts similarity index 96% rename from cosmos/src/v2/core.ts rename to cosmos/src/core.ts index 10abc21..8ebb6a8 100644 --- a/cosmos/src/v2/core.ts +++ b/cosmos/src/core.ts @@ -1,4 +1,4 @@ -import type { Duration } from "../duration"; +import type { Duration } from "./duration"; import type { Ready } from "./later"; import type { Mapper } from "./later-map"; diff --git a/cosmos/src/v2/cosmos.ts b/cosmos/src/cosmos.ts similarity index 98% rename from cosmos/src/v2/cosmos.ts rename to cosmos/src/cosmos.ts index 55a9b6e..c604de3 100644 --- a/cosmos/src/v2/cosmos.ts +++ b/cosmos/src/cosmos.ts @@ -2,10 +2,10 @@ import { proxy, subscribe, ref } from "valtio"; import { type Space, type Meta, type Spec, type State } from "./core"; import { serializeArgs } from "./serialize-args"; import { getNextSubscriberId } from "./get-next-subscriber-id"; -import { toMs } from "../duration"; +import { toMs } from "./duration"; import { type Ready } from "./later"; import { createMapper } from "./later-map"; -import { setSmartTimeout } from "../set-smart-timeout"; +import { setSmartTimeout } from "./set-smart-timeout"; const KEEP_ALIVE_MS = 1000; diff --git a/cosmos/src/v2/dev/dev-state.ts b/cosmos/src/dev/dev-state.ts similarity index 100% rename from cosmos/src/v2/dev/dev-state.ts rename to cosmos/src/dev/dev-state.ts diff --git a/cosmos/src/v2/dev/dev.tsx b/cosmos/src/dev/dev.tsx similarity index 100% rename from cosmos/src/v2/dev/dev.tsx rename to cosmos/src/dev/dev.tsx diff --git a/cosmos/src/v2/dev/index.ts b/cosmos/src/dev/index.ts similarity index 100% rename from cosmos/src/v2/dev/index.ts rename to cosmos/src/dev/index.ts diff --git a/cosmos/src/v2/dev/model-view.tsx b/cosmos/src/dev/model-view.tsx similarity index 100% rename from cosmos/src/v2/dev/model-view.tsx rename to cosmos/src/dev/model-view.tsx diff --git a/cosmos/src/v2/dev/query-view.tsx b/cosmos/src/dev/query-view.tsx similarity index 100% rename from cosmos/src/v2/dev/query-view.tsx rename to cosmos/src/dev/query-view.tsx diff --git a/cosmos/src/v2/dom.ts b/cosmos/src/dom.ts similarity index 100% rename from cosmos/src/v2/dom.ts rename to cosmos/src/dom.ts diff --git a/cosmos/src/get-model.ts b/cosmos/src/get-model.ts index 7d497de..1bf8506 100644 --- a/cosmos/src/get-model.ts +++ b/cosmos/src/get-model.ts @@ -1,11 +1,30 @@ -import type { ModelResult, Query, QueryType } from "./core-types"; -import { getAtom, getPromise } from "./state"; +import type { Spec, State } from "./core"; +import { getPromise, initSpace } from "./cosmos"; +import type { Ready } from "./later"; -/** - * Resolve a model query as a promise. - */ -export function getModel(query: Q) { - return getPromise(query).then((atom) => { - return [atom.value, atom, query] as ModelResult>; - }); +export type GetModelResult = State & PromiseShape>>; + +type PromiseShape = { + then: Promise["then"]; + catch: Promise["catch"]; + finally: Promise["finally"]; +}; + +export function getModel(spec: Spec): GetModelResult { + const space = initSpace(spec); + return { + ...space.state, + get then() { + const promise = getPromise(spec); + return promise.then.bind(promise); + }, + get catch() { + const promise = getPromise(spec); + return promise.catch.bind(promise); + }, + get finally() { + const promise = getPromise(spec); + return promise.finally.bind(promise); + }, + }; } diff --git a/cosmos/src/v2/get-next-subscriber-id.ts b/cosmos/src/get-next-subscriber-id.ts similarity index 100% rename from cosmos/src/v2/get-next-subscriber-id.ts rename to cosmos/src/get-next-subscriber-id.ts diff --git a/cosmos/src/index.ts b/cosmos/src/index.ts index f25614a..ab8b3d0 100644 --- a/cosmos/src/index.ts +++ b/cosmos/src/index.ts @@ -1,6 +1,11 @@ +export * from "./later"; +export * from "./core"; export * from "./model"; export * from "./use-model"; +export * from "./set-model"; export * from "./get-model"; -export * from "./null"; -export * from "./wait-for"; -export { getAtom, checkAtom } from "./state"; +export * from "./snapshot"; +export * from "./value"; +export * from "./compute"; +export * from "./request"; +export * from "./persist"; diff --git a/cosmos/src/v2/later-map.ts b/cosmos/src/later-map.ts similarity index 100% rename from cosmos/src/v2/later-map.ts rename to cosmos/src/later-map.ts diff --git a/cosmos/src/v2/later.ts b/cosmos/src/later.ts similarity index 100% rename from cosmos/src/v2/later.ts rename to cosmos/src/later.ts diff --git a/cosmos/src/model.ts b/cosmos/src/model.ts index b71dd6d..dc80cf2 100644 --- a/cosmos/src/model.ts +++ b/cosmos/src/model.ts @@ -1,39 +1,54 @@ -import stableStringify from "safe-stable-stringify"; -import type { Query } from "./core-types"; -import type { GetterModel } from "./templates/getter-model"; -import { fromGetterModel, isGetterModel } from "./templates/getter-model"; -import type { DerivedModel } from "./templates/derived-model"; -import { fromDerivedModel, isDerivedModel } from "./templates/derived-model"; -import { - fromEmitterModel, - isEmitterModel, - type EmitterModel, -} from "./templates/emitter-model"; - -export function model( - m: GetterModel | EmitterModel | DerivedModel -) { - // TODO make it easier to add new model templates - const genericModel = isGetterModel(m) - ? fromGetterModel(m) - : isEmitterModel(m) - ? fromEmitterModel(m) - : isDerivedModel(m) - ? fromDerivedModel(m) - : (null as never); - - return function buildQuery(params: P): Query { +import type { Behavior, Model } from "./core"; +import { combineBehavior, type Traits } from "./combine-behavior"; + +export function model(resolve: Resolve): Model; + +export function model( + identity: string | Identity, + resolve: Resolve +): Model; + +export function model( + identity: string | Identity | Resolve, + resolve?: Resolve +): Model { + // Create a default identity if one is not provided + if (typeof identity === "function") { + return model(toIdentity({}), identity); + } + + const _identity = toIdentity(identity); + const _resolve = resolve!; + + return (...args) => { return { - $key: `${genericModel.type}(${serializeParams(params)})`, - model: genericModel, - params, + name: _identity.name, + args: _identity.args(...args), + resolve: () => combineBehavior(_resolve(...args)), }; }; } -function serializeParams

(params: P) { - if (params == null) { - return ""; +type Identity = { + name: string; + args: (...args: A) => unknown[]; +}; + +type Resolve = (...args: A) => Behavior | Traits; + +let nextModelId = 0; + +function toIdentity( + identity: string | Partial> +): Identity { + if (typeof identity === "string") { + return { + name: identity, + args: (...args) => args, + }; } - return stableStringify(params); + return { + name: identity.name ?? `MODEL-${nextModelId++}`, + args: identity.args ?? ((...args) => args), + }; } diff --git a/cosmos/src/null.ts b/cosmos/src/null.ts deleted file mode 100644 index a0d48ac..0000000 --- a/cosmos/src/null.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { model } from "./model"; - -export const Null = model({ - type: "$NULL", - async get() { - return null; - }, -}); diff --git a/cosmos/src/v2/persist.ts b/cosmos/src/persist.ts similarity index 100% rename from cosmos/src/v2/persist.ts rename to cosmos/src/persist.ts diff --git a/cosmos/src/v2/request.ts b/cosmos/src/request.ts similarity index 97% rename from cosmos/src/v2/request.ts rename to cosmos/src/request.ts index 95bce3f..623f5c3 100644 --- a/cosmos/src/v2/request.ts +++ b/cosmos/src/request.ts @@ -1,4 +1,4 @@ -import { type Duration } from "../duration"; +import { type Duration } from "./duration"; import { asError, loading, type Later } from "./later"; import type { Behavior } from "./core"; import { Timer } from "./timer"; diff --git a/cosmos/src/v2/serialize-args.ts b/cosmos/src/serialize-args.ts similarity index 100% rename from cosmos/src/v2/serialize-args.ts rename to cosmos/src/serialize-args.ts diff --git a/cosmos/src/v2/set-model.ts b/cosmos/src/set-model.ts similarity index 100% rename from cosmos/src/v2/set-model.ts rename to cosmos/src/set-model.ts diff --git a/cosmos/src/v2/snapshot.ts b/cosmos/src/snapshot.ts similarity index 100% rename from cosmos/src/v2/snapshot.ts rename to cosmos/src/snapshot.ts diff --git a/cosmos/src/state.ts b/cosmos/src/state.ts deleted file mode 100644 index 208312c..0000000 --- a/cosmos/src/state.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { proxy, ref } from "valtio"; -import type { Atom, Query, QueryType } from "./core-types"; -import { subscribeKey } from "valtio/utils"; - -export type State = { - nextSubscriberId: number; - atoms: Record>; -}; - -export const state: State = proxy({ - nextSubscriberId: 0, - atoms: {}, -}); - -export function getNextSubscriberId() { - return state.nextSubscriberId++; -} - -export function getAtom>(query: Q) { - if (!state.atoms[query.model.type]) { - state.atoms[query.model.type] = proxy({}); - } - - const namespace = state.atoms[query.model.type]; - - if (!namespace[query.$key]) { - namespace[query.$key] = proxy({ - value: undefined, - initialized: false, - subscribers: ref(new Set()), - }); - } - - return namespace[query.$key] as Atom>; -} - -export function checkAtom( - query: Q -): Atom> | undefined { - return state.atoms[query.model.type]?.[query.$key] as - | Atom> - | undefined; -} - -export function reset

(query: Query) { - const atom = state.atoms[query.model.type]?.[query.$key]; - if (!atom) { - return; - } - - atom.cleanup?.(); - atom.value = undefined; - atom.promise = undefined; - atom.initialized = false; - atom.expiry = undefined; - if (atom.subscribers.size > 0) { - initialize(query); - } -} - -export function initialize(query: Query) { - const atom = getAtom(query); - if (!atom.initialized) { - atom.initialized = true; - - const cleanup = query.model.init({ params: query.params, atom }); - atom.cleanup = () => { - cleanup?.(); - atom.initialized = false; - }; - } -} - -export function getPromise(query: Q) { - const atom = getAtom(query); - - if (!atom.promise) { - atom.promise = new Promise>>((resolve) => { - const subscriberId = getNextSubscriberId(); - const unsubscribeKey = subscribeKey(atom, "value", () => { - unsubscribeKey(); - unsubscribe(query, subscriberId); - resolve(atom as Atom>); - }); - subscribe(query, subscriberId); - }); - } - - return atom.promise; -} - -export function subscribe(query: Q, subscriberId: number) { - const atom = getAtom(query); - atom.subscribers.add(subscriberId); - initialize(query); -} - -export function unsubscribe(query: Query, subscriberId: number) { - const atom = state.atoms[query.model.type]?.[query.$key]; - if (!atom) { - return; - } - - atom.subscribers.delete(subscriberId); - - // Cleanup if this is the last subscriber - if (atom.subscribers.size === 0 && atom.cleanup) { - const cleanup = atom.cleanup; - delete atom.cleanup; - - // Schedule cleanup - in case there's a new subscriber soon - const KEEP_ALIVE_MS = 1000; - setTimeout(() => { - if (atom.subscribers.size === 0) { - cleanup(); - } else { - atom.cleanup = cleanup; - } - }, KEEP_ALIVE_MS); - } -} diff --git a/cosmos/src/templates/derived-model.ts b/cosmos/src/templates/derived-model.ts deleted file mode 100644 index df14165..0000000 --- a/cosmos/src/templates/derived-model.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { watch } from "valtio/utils"; -import type { Model, Query, UseModel } from "../core-types"; -import { getAtom, 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 = { - type?: string; - derive: (getModel: UseModel, params: P) => T | undefined; -}; - -export function isDerivedModel( - model: any -): model is DerivedModel { - return "derive" in model; -} - -let nextDerivedModelId = 0; - -export function fromDerivedModel( - model: DerivedModel -): Model { - const type = model.type ?? `$Derived-${nextDerivedModelId++}`; - - return { - type, - init({ params, atom }) { - const subscriberId = getNextSubscriberId(); - - let queries: Record> = {}; - - const unwatch = watch((get) => { - const nextQueries: Record> = {}; - - const getModel: UseModel = function (query) { - const atom = getAtom(query); - subscribe(query, subscriberId); - nextQueries[query.$key] = query; - - // Tell valtio to track the dependency - get(atom); - - return [atom.value, atom, query]; - }; - - try { - const value = model.derive(getModel, params); - atom.value = value; - } catch (e) { - if (e instanceof Promise) { - // Ignore, since this model will re-run when there is a value - } else { - 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); - } - }; - }, - }; -} diff --git a/cosmos/src/templates/emitter-model.ts b/cosmos/src/templates/emitter-model.ts deleted file mode 100644 index c17507f..0000000 --- a/cosmos/src/templates/emitter-model.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Model } from "../core-types"; - -/** - * An EmitterModel represents data sources that emit values over time, such as - * websockets or event listeners. - */ -export type EmitterModel = { - type: string; - emitter: (emit: (value: T) => void, params: P) => void | (() => void); -}; - -export function isEmitterModel( - model: any -): model is EmitterModel { - return "emitter" in model; -} - -export function fromEmitterModel( - model: EmitterModel -): Model { - return { - type: model.type, - init({ params, atom }) { - // Emitter models are trivially simple because generic models are - // essentially the same thing. - const emit = (value: T) => (atom.value = value); - return model.emitter(emit, params); - }, - }; -} diff --git a/cosmos/src/templates/getter-model.ts b/cosmos/src/templates/getter-model.ts deleted file mode 100644 index a60040a..0000000 --- a/cosmos/src/templates/getter-model.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Model } from "../core-types"; -import { toMs, type Duration } from "../duration"; -import { setSmartTimeout } from "../set-smart-timeout"; - -/** - * A GetterModel is an asynchronous request/response style model. - * A get() function returns a promise with the value. - */ -export type GetterModel = { - type: string; - get: (params: P, tools: Tools) => Promise; - refresh?: RefreshInterval; -}; - -export type RefreshInterval = Duration & { - onFocus?: boolean; -}; - -type Tools = { - refreshIn: (duration: Duration) => void; -}; - -export function isGetterModel( - model: any -): model is GetterModel { - return "get" in model; -} - -export function fromGetterModel( - model: GetterModel -): Model { - return { - type: model.type, - init({ params, atom }) { - let cleanedUp = false; - let scheduledRefreshTime: number | null = null; - let clearTimer: (() => void) | undefined; - - function get() { - if (cleanedUp) { - return; - } - - scheduledRefreshTime = null; - - model - .get(params, { refreshIn }) - .then( - (value) => { - atom.value = value; - }, - (e) => { - // TODO handle errors - throw e; - } - ) - .finally(() => { - if (model.refresh) { - refreshIn(model.refresh); - } - }); - } - - function refreshIn(duration: Duration) { - if (cleanedUp) { - return; - } - - const ms = toMs(duration); - if (!ms) { - return; - } - - const expiry = Date.now() + ms; - atom.expiry = expiry; - if (scheduledRefreshTime == null || expiry < scheduledRefreshTime) { - clearTimer?.(); - scheduledRefreshTime = expiry; - clearTimer = setSmartTimeout(get, ms); - } - } - - if (model.refresh?.onFocus) { - window.addEventListener("focus", get); - } - - get(); - - return function cleanup() { - cleanedUp = true; - clearTimer?.(); - if (model.refresh?.onFocus) { - window.removeEventListener("focus", get); - } - }; - }, - }; -} diff --git a/cosmos/src/v2/timer.ts b/cosmos/src/timer.ts similarity index 92% rename from cosmos/src/v2/timer.ts rename to cosmos/src/timer.ts index a207865..c5aae6e 100644 --- a/cosmos/src/v2/timer.ts +++ b/cosmos/src/timer.ts @@ -1,5 +1,5 @@ -import { toMs, type Duration } from "../duration"; -import { setSmartTimeout } from "../set-smart-timeout"; +import { toMs, type Duration } from "./duration"; +import { setSmartTimeout } from "./set-smart-timeout"; type ScheduledEvent = { time: number; diff --git a/cosmos/src/use-model.ts b/cosmos/src/use-model.ts index 872174d..248b224 100644 --- a/cosmos/src/use-model.ts +++ b/cosmos/src/use-model.ts @@ -1,34 +1,45 @@ -import { useLayoutEffect } from "react"; -import { - getNextSubscriberId, - getAtom, - subscribe, - unsubscribe, - initialize, -} from "./state"; import { useSnapshot } from "valtio"; -import type { Atom, Query, QueryType, UseModel } from "./core-types"; - -export const useModel: UseModel = function (query: Q) { - const atom = getAtom(query); +import { type Snapshot, type Spec } from "./core"; +import { + removeSubscriber, + addSubscriber, + getPromise, + initSpace, +} from "./cosmos"; +import { serializeArgs } from "./serialize-args"; +import { useLayoutEffect } from "react"; +import { getNextSubscriberId } from "./get-next-subscriber-id"; +import { createMapper } from "./later-map"; - // Initialize in the render phase to ensure sync models (e.g. derived models - // with already cached data) are ready on first render. - initialize(query); +export function useModel(spec: Spec): Snapshot { + const $state = useSnapshot(initSpace(spec).state); useLayoutEffect(() => { const subscriberId = getNextSubscriberId(); - subscribe(query, subscriberId); + addSubscriber(spec, subscriberId); return () => { - unsubscribe(query, subscriberId); + removeSubscriber(spec, subscriberId); }; - // Since query.$key is a seralized representation of the query, - // we only need this as the dependency. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [query?.$key]); + }, [spec.name, serializeArgs(spec.args)]); - const _atom = useSnapshot(atom) as Atom>; + const map = createMapper(() => $state.value as T, { + value: (value) => { + return value; + }, + loading: () => { + throw getPromise(spec); + }, + error: (error) => { + throw error; + }, + }); - return [_atom.value, atom, query]; -}; + return { + map, + get value() { + return map({}); + }, + }; +} diff --git a/cosmos/src/v2/example.md b/cosmos/src/v2/example.md deleted file mode 100644 index c70b46e..0000000 --- a/cosmos/src/v2/example.md +++ /dev/null @@ -1,61 +0,0 @@ -```ts -const LocationName = model( - args((coord: Coord) => [coord.lat.toFixed(4), coord.lon.toFixed(4)]), - request(fetchLocationName), - refresh(fetchLocationName, { minutes: 10 }), - persist("LocationName"), - { forget: { days: 30 } } -); - -const LocationName = model( - { - name: "LocationName", - args: (coord: Coord) => [coord.lat.toFixed(4), coord.lon.toFixed(4)], - }, - (coord: Coord) => { - return [ - request(() => fetchLocationName(coord)), - refresh(() => fetchLocationName(coord), { minutes: 10 }), - persist("LocationName"), - { forget: { days: 30 } }, - ]; - } -); - -const LocationName = model("LocationName", (coord: Coord) => { - return [ - request(() => fetchLocationName(coord)), - refresh(() => fetchLocationName(coord), { minutes: 10 }), - persist("LocationName"), - { forget: { days: 30 } }, - ]; -}); - -useModel({ - name: "LocationName", - args: [{ lat: 2, lon: 3 }], - resolve: () => { - return [request(() => fetchLocationName({ lat: 2, lon: 3 }))]; - }, -}); - -const LocationName = model({ - args: (coord: Coord) => [coord.lat.toFixed(4), coord.lon.toFixed(4)], -}); - -const CounterRounded = model( - computed((id: number) => { - return get(Counter(id)).value; - }) -); - -const Counter = model( - value(() => 0), - interval( - (state) => { - state.value++; - }, - { seconds: 1 } - ) -); -``` diff --git a/cosmos/src/v2/get-model.ts b/cosmos/src/v2/get-model.ts deleted file mode 100644 index 1bf8506..0000000 --- a/cosmos/src/v2/get-model.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Spec, State } from "./core"; -import { getPromise, initSpace } from "./cosmos"; -import type { Ready } from "./later"; - -export type GetModelResult = State & PromiseShape>>; - -type PromiseShape = { - then: Promise["then"]; - catch: Promise["catch"]; - finally: Promise["finally"]; -}; - -export function getModel(spec: Spec): GetModelResult { - const space = initSpace(spec); - return { - ...space.state, - get then() { - const promise = getPromise(spec); - return promise.then.bind(promise); - }, - get catch() { - const promise = getPromise(spec); - return promise.catch.bind(promise); - }, - get finally() { - const promise = getPromise(spec); - return promise.finally.bind(promise); - }, - }; -} diff --git a/cosmos/src/v2/index.ts b/cosmos/src/v2/index.ts deleted file mode 100644 index ab8b3d0..0000000 --- a/cosmos/src/v2/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./later"; -export * from "./core"; -export * from "./model"; -export * from "./use-model"; -export * from "./set-model"; -export * from "./get-model"; -export * from "./snapshot"; -export * from "./value"; -export * from "./compute"; -export * from "./request"; -export * from "./persist"; diff --git a/cosmos/src/v2/model.ts b/cosmos/src/v2/model.ts deleted file mode 100644 index dc80cf2..0000000 --- a/cosmos/src/v2/model.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Behavior, Model } from "./core"; -import { combineBehavior, type Traits } from "./combine-behavior"; - -export function model(resolve: Resolve): Model; - -export function model( - identity: string | Identity, - resolve: Resolve -): Model; - -export function model( - identity: string | Identity | Resolve, - resolve?: Resolve -): Model { - // Create a default identity if one is not provided - if (typeof identity === "function") { - return model(toIdentity({}), identity); - } - - const _identity = toIdentity(identity); - const _resolve = resolve!; - - return (...args) => { - return { - name: _identity.name, - args: _identity.args(...args), - resolve: () => combineBehavior(_resolve(...args)), - }; - }; -} - -type Identity = { - name: string; - args: (...args: A) => unknown[]; -}; - -type Resolve = (...args: A) => Behavior | Traits; - -let nextModelId = 0; - -function toIdentity( - identity: string | Partial> -): Identity { - if (typeof identity === "string") { - return { - name: identity, - args: (...args) => args, - }; - } - return { - name: identity.name ?? `MODEL-${nextModelId++}`, - args: identity.args ?? ((...args) => args), - }; -} diff --git a/cosmos/src/v2/use-model.ts b/cosmos/src/v2/use-model.ts deleted file mode 100644 index 248b224..0000000 --- a/cosmos/src/v2/use-model.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useSnapshot } from "valtio"; -import { type Snapshot, type Spec } from "./core"; -import { - removeSubscriber, - addSubscriber, - getPromise, - initSpace, -} from "./cosmos"; -import { serializeArgs } from "./serialize-args"; -import { useLayoutEffect } from "react"; -import { getNextSubscriberId } from "./get-next-subscriber-id"; -import { createMapper } from "./later-map"; - -export function useModel(spec: Spec): Snapshot { - const $state = useSnapshot(initSpace(spec).state); - - useLayoutEffect(() => { - const subscriberId = getNextSubscriberId(); - addSubscriber(spec, subscriberId); - - return () => { - removeSubscriber(spec, subscriberId); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [spec.name, serializeArgs(spec.args)]); - - const map = createMapper(() => $state.value as T, { - value: (value) => { - return value; - }, - loading: () => { - throw getPromise(spec); - }, - error: (error) => { - throw error; - }, - }); - - return { - map, - get value() { - return map({}); - }, - }; -} diff --git a/cosmos/src/v2/value.ts b/cosmos/src/value.ts similarity index 100% rename from cosmos/src/v2/value.ts rename to cosmos/src/value.ts diff --git a/cosmos/src/wait-for.ts b/cosmos/src/wait-for.ts deleted file mode 100644 index 753b02f..0000000 --- a/cosmos/src/wait-for.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ModelResult } from "./core-types"; -import { getPromise } from "./state"; - -export function waitFor( - modelResult: ModelResult -): ModelResult { - if (modelResult[0] === undefined) { - throw getPromise(modelResult[2]); - } - - return modelResult as ModelResult; -} diff --git a/example/src/app-state.ts b/example/src/app-state.ts index 6f2e25e..0a9a2e8 100644 --- a/example/src/app-state.ts +++ b/example/src/app-state.ts @@ -1,4 +1,4 @@ -import { model, persist, value } from "@dvtng/cosmos/src/v2"; +import { model, persist, value } from "@dvtng/cosmos"; export const AppState = model("AppState", () => { return [ diff --git a/example/src/app.tsx b/example/src/app.tsx index 510fc1d..e788611 100644 --- a/example/src/app.tsx +++ b/example/src/app.tsx @@ -4,7 +4,7 @@ import { CounterOnFocusView } from "./counter-on-focus"; import { Counters } from "./counters"; import { CoinPriceView } from "./coin-price"; import { ErrorBoundary } from "./error-boundary"; -import { CosmosDev } from "@dvtng/cosmos/src/v2/dev"; +import { CosmosDev } from "@dvtng/cosmos/dist/dev"; export function App() { return ( diff --git a/example/src/clock.tsx b/example/src/clock.tsx index 44fd1ff..dab2740 100644 --- a/example/src/clock.tsx +++ b/example/src/clock.tsx @@ -1,4 +1,4 @@ -import { model, useModel } from "@dvtng/cosmos/src/v2"; +import { model, useModel } from "@dvtng/cosmos"; export const Time = model("Time", () => { return [ diff --git a/example/src/coin-price.tsx b/example/src/coin-price.tsx index f71b1ba..783587e 100644 --- a/example/src/coin-price.tsx +++ b/example/src/coin-price.tsx @@ -1,4 +1,4 @@ -import { model, persist, request, useModel } from "@dvtng/cosmos/src/v2"; +import { model, persist, request, useModel } from "@dvtng/cosmos"; import NumberFlow from "@number-flow/react"; export const CoinPrice = model("CoinPrice", (coinId: string) => { diff --git a/example/src/counter-on-focus.tsx b/example/src/counter-on-focus.tsx index acc5a9d..9db82f6 100644 --- a/example/src/counter-on-focus.tsx +++ b/example/src/counter-on-focus.tsx @@ -1,4 +1,4 @@ -import { model, request, useModel } from "@dvtng/cosmos/src/v2"; +import { model, request, useModel } from "@dvtng/cosmos"; import NumberFlow from "@number-flow/react"; let count = 0; diff --git a/example/src/counter.tsx b/example/src/counter.tsx index 94777f5..f533ced 100644 --- a/example/src/counter.tsx +++ b/example/src/counter.tsx @@ -1,4 +1,4 @@ -import { model, useModel } from "@dvtng/cosmos/src/v2"; +import { model, useModel } from "@dvtng/cosmos"; import NumberFlow from "@number-flow/react"; export const Counter = model( diff --git a/example/src/counters.tsx b/example/src/counters.tsx index ce55d2b..4ba4307 100644 --- a/example/src/counters.tsx +++ b/example/src/counters.tsx @@ -1,4 +1,4 @@ -import { compute, getModel, model, useModel } from "@dvtng/cosmos/src/v2"; +import { compute, getModel, model, useModel } from "@dvtng/cosmos"; import { AppState } from "./app-state"; import { Counter } from "./counter"; import { CounterView } from "./counter";