@gyral/core 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/NOTICE +11 -0
- package/README.md +45 -5
- package/dist/accessible-name.d.ts +19 -0
- package/dist/accessible-name.js +56 -0
- package/dist/accessible-name.js.map +1 -0
- package/dist/children.d.ts +37 -0
- package/dist/children.js +49 -0
- package/dist/children.js.map +1 -0
- package/dist/command.d.ts +63 -0
- package/dist/command.js +26 -0
- package/dist/command.js.map +1 -0
- package/dist/define.d.ts +9 -0
- package/dist/define.js +345 -0
- package/dist/define.js.map +1 -0
- package/dist/devtools-events.d.ts +51 -0
- package/dist/devtools-events.js +3 -0
- package/dist/devtools-events.js.map +1 -0
- package/dist/devtools-off.d.ts +9 -0
- package/dist/devtools-off.js +10 -0
- package/dist/devtools-off.js.map +1 -0
- package/dist/devtools.d.ts +13 -0
- package/dist/devtools.js +66 -0
- package/dist/devtools.js.map +1 -0
- package/dist/drivers-scope.d.ts +15 -0
- package/dist/drivers-scope.js +48 -0
- package/dist/drivers-scope.js.map +1 -0
- package/dist/element-directive.d.ts +20 -0
- package/dist/element-directive.js +31 -0
- package/dist/element-directive.js.map +1 -0
- package/dist/element-types.d.ts +34 -0
- package/dist/element-types.js +2 -0
- package/dist/element-types.js.map +1 -0
- package/dist/focus.d.ts +27 -0
- package/dist/focus.js +30 -0
- package/dist/focus.js.map +1 -0
- package/dist/forms.d.ts +59 -0
- package/dist/forms.js +138 -0
- package/dist/forms.js.map +1 -0
- package/dist/hydration.d.ts +36 -0
- package/dist/hydration.js +72 -0
- package/dist/hydration.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +7 -0
- package/dist/init.js +10 -0
- package/dist/init.js.map +1 -0
- package/dist/intent.d.ts +25 -0
- package/dist/intent.js +157 -0
- package/dist/intent.js.map +1 -0
- package/dist/internal/interpreter.d.ts +8 -0
- package/dist/internal/interpreter.js +128 -0
- package/dist/internal/interpreter.js.map +1 -0
- package/dist/internal/runtime.d.ts +7 -0
- package/dist/internal/runtime.js +8 -0
- package/dist/internal/runtime.js.map +1 -0
- package/dist/invalid.d.ts +21 -0
- package/dist/invalid.js +70 -0
- package/dist/invalid.js.map +1 -0
- package/dist/invokers.d.ts +15 -0
- package/dist/invokers.js +64 -0
- package/dist/invokers.js.map +1 -0
- package/dist/islands.d.ts +11 -0
- package/dist/islands.js +63 -0
- package/dist/islands.js.map +1 -0
- package/dist/json-safety.d.ts +8 -0
- package/dist/json-safety.js +68 -0
- package/dist/json-safety.js.map +1 -0
- package/dist/light-dom.d.ts +61 -0
- package/dist/light-dom.js +109 -0
- package/dist/light-dom.js.map +1 -0
- package/dist/live-boolean.d.ts +22 -0
- package/dist/live-boolean.js +34 -0
- package/dist/live-boolean.js.map +1 -0
- package/dist/props.d.ts +26 -0
- package/dist/props.js +44 -0
- package/dist/props.js.map +1 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +19 -0
- package/dist/random.js.map +1 -0
- package/dist/states.d.ts +7 -0
- package/dist/states.js +23 -0
- package/dist/states.js.map +1 -0
- package/dist/store-binding.d.ts +14 -0
- package/dist/store-binding.js +63 -0
- package/dist/store-binding.js.map +1 -0
- package/dist/store-scope.d.ts +40 -0
- package/dist/store-scope.js +146 -0
- package/dist/store-scope.js.map +1 -0
- package/dist/store.d.ts +114 -0
- package/dist/store.js +129 -0
- package/dist/store.js.map +1 -0
- package/dist/stores-provider.d.ts +6 -0
- package/dist/stores-provider.js +57 -0
- package/dist/stores-provider.js.map +1 -0
- package/dist/styles.d.ts +12 -0
- package/dist/styles.js +11 -0
- package/dist/styles.js.map +1 -0
- package/dist/textarea.d.ts +26 -0
- package/dist/textarea.js +85 -0
- package/dist/textarea.js.map +1 -0
- package/dist/transitions.d.ts +9 -0
- package/dist/transitions.js +26 -0
- package/dist/transitions.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +25 -0
- package/dist/vite.js +37 -0
- package/dist/vite.js.map +1 -0
- package/package.json +41 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random.js","sourceRoot":"","sources":["../src/random.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,2EAA2E;AAC3E,OAAO,EAAE,OAAO,EAAE,YAAY,EAA6B,MAAM,cAAc,CAAC;AAOhF,0FAA0F;AAC1F,MAAM,CAAC,MAAM,YAAY,GAA2C,YAAY,CAAC;IAC/E,IAAI,EAAE,QAAQ;IACd,GAAG,EAAE,CAAC,EAAE,KAAK,EAAe,EAAqB,EAAE,CACjD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;CACrD,CAAC,CAAC;AAEH,6EAA6E;AAC7E,MAAM,UAAU,MAAM,CACpB,KAAa,EACb,KAAmD;IAEnD,OAAO,OAAO,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,GAAW,EAAE,GAAW,EAAU,EAAE,CACnE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7D,mDAAmD;AACnD,MAAM,UAAU,SAAS,CACvB,GAAW,EACX,GAAW,EACX,KAAuC;IAEvC,OAAO,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["// Randomness as an effect (gyral-czi.10): reducers ask for random numbers with a command,\n// so models stay pure and tests substitute a deterministic driver by name.\nimport { command, defineDriver, type Command, type Driver } from './command.js';\n\nexport interface RandomInput {\n /** How many numbers to draw. */\n readonly count: number;\n}\n\n/** Uniform numbers in [0, 1). Substitute it in tests: `el.drivers = { random: fake }`. */\nexport const randomDriver: Driver<RandomInput, readonly number[]> = defineDriver({\n name: 'random',\n run: ({ count }: RandomInput): readonly number[] =>\n Array.from({ length: count }, () => Math.random()),\n});\n\n/** Draws `count` uniform numbers in [0, 1) and turns them into a message. */\nexport function random<M>(\n count: number,\n toMsg: (values: readonly number[]) => M | undefined,\n): Command<M> {\n return command(randomDriver, { count }, { onSuccess: toMsg });\n}\n\n/** Maps a uniform number in [0, 1) to an integer in `min..max` (inclusive). Pure. */\nexport const toInt = (u: number, min: number, max: number): number =>\n min + Math.min(max - min, Math.floor(u * (max - min + 1)));\n\n/** Draws one integer in `min..max` (inclusive). */\nexport function randomInt<M>(\n min: number,\n max: number,\n toMsg: (value: number) => M | undefined,\n): Command<M> {\n return random(1, ([u = 0]) => toMsg(toInt(u, min, max)));\n}\n"]}
|
package/dist/states.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Writes a component's boolean states to its CustomStateSet. */
|
|
2
|
+
export type StateSync = (states: Readonly<Record<string, boolean>>) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Attaches ElementInternals to `host` and returns a function that mirrors boolean states onto
|
|
5
|
+
* `internals.states`, or `undefined` when the platform lacks custom states.
|
|
6
|
+
*/
|
|
7
|
+
export declare function attachStates(host: HTMLElement): StateSync | undefined;
|
package/dist/states.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Model state → CSS custom states (`:state(loading)`), via ElementInternals (ADR 0001 addendum).
|
|
2
|
+
// An ADR 0003 enhancement: CustomStateSet is feature-detected; without it nothing is set.
|
|
3
|
+
/**
|
|
4
|
+
* Attaches ElementInternals to `host` and returns a function that mirrors boolean states onto
|
|
5
|
+
* `internals.states`, or `undefined` when the platform lacks custom states.
|
|
6
|
+
*/
|
|
7
|
+
export function attachStates(host) {
|
|
8
|
+
if (typeof host.attachInternals !== 'function')
|
|
9
|
+
return undefined;
|
|
10
|
+
const internals = host.attachInternals();
|
|
11
|
+
if (!('states' in internals))
|
|
12
|
+
return undefined;
|
|
13
|
+
const set = internals.states;
|
|
14
|
+
return (states) => {
|
|
15
|
+
for (const [name, on] of Object.entries(states)) {
|
|
16
|
+
if (on)
|
|
17
|
+
set.add(name);
|
|
18
|
+
else
|
|
19
|
+
set.delete(name);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=states.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"states.js","sourceRoot":"","sources":["../src/states.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,0FAA0F;AAU1F;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAiB;IAC5C,IAAI,OAAO,IAAI,CAAC,eAAe,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IACjE,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;IACzC,IAAI,CAAC,CAAC,QAAQ,IAAI,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/C,MAAM,GAAG,GAAG,SAAS,CAAC,MAAsB,CAAC;IAC7C,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAChD,IAAI,EAAE;gBAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;;gBACjB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Model state → CSS custom states (`:state(loading)`), via ElementInternals (ADR 0001 addendum).\n// An ADR 0003 enhancement: CustomStateSet is feature-detected; without it nothing is set.\n\ninterface StateSetLike {\n add(name: string): void;\n delete(name: string): boolean;\n}\n\n/** Writes a component's boolean states to its CustomStateSet. */\nexport type StateSync = (states: Readonly<Record<string, boolean>>) => void;\n\n/**\n * Attaches ElementInternals to `host` and returns a function that mirrors boolean states onto\n * `internals.states`, or `undefined` when the platform lacks custom states.\n */\nexport function attachStates(host: HTMLElement): StateSync | undefined {\n if (typeof host.attachInternals !== 'function') return undefined;\n const internals = host.attachInternals();\n if (!('states' in internals)) return undefined;\n const set = internals.states as StateSetLike;\n return (states) => {\n for (const [name, on] of Object.entries(states)) {\n if (on) set.add(name);\n else set.delete(name);\n }\n };\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyStore, StoreOverrides, StoreRef, StoreSendInput } from './store.js';
|
|
2
|
+
type OnChange = (store: AnyStore, state: unknown, prev: unknown) => void;
|
|
3
|
+
export declare class StoreBinding {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(host: Element, tag: string, declared: readonly AnyStore[], overrides: () => StoreOverrides, onChange: OnChange);
|
|
6
|
+
/** `ctx.read(store)`: the current state of this component's instance of `store`. */
|
|
7
|
+
read<S>(ref: StoreRef<S>): S;
|
|
8
|
+
/** Delivers a `send(store, msg)` command to this component's instance of the store. */
|
|
9
|
+
send(input: StoreSendInput): void;
|
|
10
|
+
/** Resolves and subscribes every declared store. Returns true if any instance changed. */
|
|
11
|
+
connect(): boolean;
|
|
12
|
+
disconnect(): void;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// A component's connection to the stores it declares (ADR 0013): resolve, read, subscribe, send.
|
|
2
|
+
import { scopeFor } from './store-scope.js';
|
|
3
|
+
export class StoreBinding {
|
|
4
|
+
#host;
|
|
5
|
+
#tag;
|
|
6
|
+
#declared;
|
|
7
|
+
#overrides;
|
|
8
|
+
#onChange;
|
|
9
|
+
/** Resolved instances while connected (the scope can't change until it moves). */
|
|
10
|
+
#bound = new Map();
|
|
11
|
+
#unsubscribe = [];
|
|
12
|
+
constructor(host, tag, declared, overrides, onChange) {
|
|
13
|
+
this.#host = host;
|
|
14
|
+
this.#tag = tag;
|
|
15
|
+
this.#declared = new Map(declared.map((s) => [s.name, s]));
|
|
16
|
+
this.#overrides = overrides;
|
|
17
|
+
this.#onChange = onChange;
|
|
18
|
+
}
|
|
19
|
+
#instance(store) {
|
|
20
|
+
return (this.#bound.get(store.name) ??
|
|
21
|
+
this.#overrides()[store.name] ??
|
|
22
|
+
scopeFor(this.#host, this.#tag, store).get(store));
|
|
23
|
+
}
|
|
24
|
+
#declaredStore(name) {
|
|
25
|
+
const store = this.#declared.get(name);
|
|
26
|
+
if (store === undefined) {
|
|
27
|
+
throw new Error(`<${this.#tag}> uses store "${name}" without declaring it. Add it to the spec: ` +
|
|
28
|
+
`stores: [${name}] (ADR 0013), so the component subscribes to its changes.`);
|
|
29
|
+
}
|
|
30
|
+
return store;
|
|
31
|
+
}
|
|
32
|
+
/** `ctx.read(store)`: the current state of this component's instance of `store`. */
|
|
33
|
+
read(ref) {
|
|
34
|
+
// Sound: instances are keyed by store name, and the ref carries that store's state type.
|
|
35
|
+
return this.#instance(this.#declaredStore(ref.name)).state;
|
|
36
|
+
}
|
|
37
|
+
/** Delivers a `send(store, msg)` command to this component's instance of the store. */
|
|
38
|
+
send(input) {
|
|
39
|
+
const store = this.#declaredStore(input.store.name);
|
|
40
|
+
this.#instance(store).send(input.msg);
|
|
41
|
+
}
|
|
42
|
+
/** Resolves and subscribes every declared store. Returns true if any instance changed. */
|
|
43
|
+
connect() {
|
|
44
|
+
this.disconnect();
|
|
45
|
+
let rebound = false;
|
|
46
|
+
for (const store of this.#declared.values()) {
|
|
47
|
+
const instance = this.#instance(store);
|
|
48
|
+
this.#bound.set(store.name, instance);
|
|
49
|
+
this.#unsubscribe.push(instance.subscribe((state, prev) => {
|
|
50
|
+
this.#onChange(store, state, prev);
|
|
51
|
+
}));
|
|
52
|
+
rebound = true;
|
|
53
|
+
}
|
|
54
|
+
return rebound;
|
|
55
|
+
}
|
|
56
|
+
disconnect() {
|
|
57
|
+
for (const unsubscribe of this.#unsubscribe)
|
|
58
|
+
unsubscribe();
|
|
59
|
+
this.#unsubscribe = [];
|
|
60
|
+
this.#bound.clear();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=store-binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-binding.js","sourceRoot":"","sources":["../src/store-binding.ts"],"names":[],"mappings":"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","sourcesContent":["// A component's connection to the stores it declares (ADR 0013): resolve, read, subscribe, send.\nimport { scopeFor } from './store-scope.js';\nimport type {\n AnyStore,\n AnyStoreInstance,\n StoreOverrides,\n StoreRef,\n StoreSendInput,\n} from './store.js';\n\ntype OnChange = (store: AnyStore, state: unknown, prev: unknown) => void;\n\nexport class StoreBinding {\n readonly #host: Element;\n readonly #tag: string;\n readonly #declared: ReadonlyMap<string, AnyStore>;\n readonly #overrides: () => StoreOverrides;\n readonly #onChange: OnChange;\n /** Resolved instances while connected (the scope can't change until it moves). */\n readonly #bound = new Map<string, AnyStoreInstance>();\n #unsubscribe: (() => void)[] = [];\n\n constructor(\n host: Element,\n tag: string,\n declared: readonly AnyStore[],\n overrides: () => StoreOverrides,\n onChange: OnChange,\n ) {\n this.#host = host;\n this.#tag = tag;\n this.#declared = new Map(declared.map((s) => [s.name, s]));\n this.#overrides = overrides;\n this.#onChange = onChange;\n }\n\n #instance(store: AnyStore): AnyStoreInstance {\n return (\n this.#bound.get(store.name) ??\n this.#overrides()[store.name] ??\n scopeFor(this.#host, this.#tag, store).get(store)\n );\n }\n\n #declaredStore(name: string): AnyStore {\n const store = this.#declared.get(name);\n if (store === undefined) {\n throw new Error(\n `<${this.#tag}> uses store \"${name}\" without declaring it. Add it to the spec: ` +\n `stores: [${name}] (ADR 0013), so the component subscribes to its changes.`,\n );\n }\n return store;\n }\n\n /** `ctx.read(store)`: the current state of this component's instance of `store`. */\n read<S>(ref: StoreRef<S>): S {\n // Sound: instances are keyed by store name, and the ref carries that store's state type.\n return this.#instance(this.#declaredStore(ref.name)).state as S;\n }\n\n /** Delivers a `send(store, msg)` command to this component's instance of the store. */\n send(input: StoreSendInput): void {\n const store = this.#declaredStore(input.store.name);\n (this.#instance(store) as { send(msg: unknown): void }).send(input.msg);\n }\n\n /** Resolves and subscribes every declared store. Returns true if any instance changed. */\n connect(): boolean {\n this.disconnect();\n let rebound = false;\n for (const store of this.#declared.values()) {\n const instance = this.#instance(store);\n this.#bound.set(store.name, instance);\n this.#unsubscribe.push(\n instance.subscribe((state, prev) => {\n this.#onChange(store, state, prev);\n }),\n );\n rebound = true;\n }\n return rebound;\n }\n\n disconnect(): void {\n for (const unsubscribe of this.#unsubscribe) unsubscribe();\n this.#unsubscribe = [];\n this.#bound.clear();\n }\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AnyStore, AnyStoreInstance } from './store.js';
|
|
2
|
+
/** Page-level seed written by @gyral/ssr: `<script type="application/json" data-gyral-stores>`. */
|
|
3
|
+
export declare const STORE_SEED_ATTRIBUTE = "data-gyral-stores";
|
|
4
|
+
/** Provider element name. Set `.instances=${[cart.instance()]}`, or let it create its own. */
|
|
5
|
+
export declare const STORES_ELEMENT = "gyral-stores";
|
|
6
|
+
/** A set of store instances by store name, created on demand from optional seeds. */
|
|
7
|
+
export declare class StoreRegistry {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(instances?: readonly AnyStoreInstance[], seeds?: Readonly<Record<string, unknown>>);
|
|
10
|
+
/** The instance for `store`, creating it (from its seed, if any) on first use. */
|
|
11
|
+
get(store: AnyStore): AnyStoreInstance;
|
|
12
|
+
/** Every instance's state by store name (what the SSR seed carries). */
|
|
13
|
+
snapshot(): Readonly<Record<string, unknown>>;
|
|
14
|
+
/** Disposes every instance (stops their commands). */
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs `fn` with `registry` as the server's store scope. Server renders are synchronous per
|
|
19
|
+
* render step, so @gyral/ssr wraps every step: interleaved requests never see each other's
|
|
20
|
+
* stores, without AsyncLocalStorage (works on any runtime).
|
|
21
|
+
*/
|
|
22
|
+
export declare function withStoreScope<T>(registry: StoreRegistry, fn: () => T): T;
|
|
23
|
+
/** JSON for an inline `<script type="application/json">`: `</script>` and friends escaped. */
|
|
24
|
+
export declare function scriptSafeJson(value: unknown): string;
|
|
25
|
+
/**
|
|
26
|
+
* A `<gyral-stores>` provider's scope, created on first use. On the client a server-rendered
|
|
27
|
+
* provider carries its instances' states in `data-gyral-stores` (gyral-czi.20): seeded stores
|
|
28
|
+
* are restored from it (so hydration matches the server), and `.instances` supplies the rest.
|
|
29
|
+
*/
|
|
30
|
+
export declare function providerScope(el: Element): StoreRegistry;
|
|
31
|
+
/** Event a component dispatches on the server to find its nearest `<gyral-stores>` provider. */
|
|
32
|
+
export declare const STORES_REQUEST = "gyral-stores-request";
|
|
33
|
+
/** The detail of a STORES_REQUEST event; the provider fills in `registry`. */
|
|
34
|
+
export interface StoresRequest {
|
|
35
|
+
registry?: StoreRegistry;
|
|
36
|
+
}
|
|
37
|
+
/** The scope for a component: its nearest provider (across shadow roots) or the default. */
|
|
38
|
+
export declare function scopeFor(host: Element, tag: string, store: AnyStore): StoreRegistry;
|
|
39
|
+
/** Tests only: forget the document default so each test starts clean. */
|
|
40
|
+
export declare function resetDocumentStores(): void;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Which store instance a component uses (docs/design-docs/0013-shared-state.md):
|
|
2
|
+
// el.stores override → nearest <gyral-stores> ancestor → the document default (client), or the
|
|
3
|
+
// request scope set by @gyral/ssr around each render step (server).
|
|
4
|
+
import { isServer } from 'lit';
|
|
5
|
+
const onServer = isServer;
|
|
6
|
+
/** Page-level seed written by @gyral/ssr: `<script type="application/json" data-gyral-stores>`. */
|
|
7
|
+
export const STORE_SEED_ATTRIBUTE = 'data-gyral-stores';
|
|
8
|
+
/** Provider element name. Set `.instances=${[cart.instance()]}`, or let it create its own. */
|
|
9
|
+
export const STORES_ELEMENT = 'gyral-stores';
|
|
10
|
+
/** A set of store instances by store name, created on demand from optional seeds. */
|
|
11
|
+
export class StoreRegistry {
|
|
12
|
+
#instances = new Map();
|
|
13
|
+
#seeds;
|
|
14
|
+
constructor(instances = [], seeds = {}) {
|
|
15
|
+
for (const instance of instances)
|
|
16
|
+
this.#hold(instance);
|
|
17
|
+
this.#seeds = seeds;
|
|
18
|
+
}
|
|
19
|
+
/** The instance for `store`, creating it (from its seed, if any) on first use. */
|
|
20
|
+
get(store) {
|
|
21
|
+
return this.#instances.get(store.name) ?? this.#hold(this.#create(store));
|
|
22
|
+
}
|
|
23
|
+
/** Keeps an instance and lets its `send(other, msg)` commands reach this scope. */
|
|
24
|
+
#hold(instance) {
|
|
25
|
+
this.#instances.set(instance.store.name, instance);
|
|
26
|
+
instance.bindScope?.((store) => this.get(store));
|
|
27
|
+
return instance;
|
|
28
|
+
}
|
|
29
|
+
/** A new instance from its seed (checked by the store's schema), or from `init`. */
|
|
30
|
+
#create(store) {
|
|
31
|
+
if (!(store.name in this.#seeds))
|
|
32
|
+
return store.instance();
|
|
33
|
+
const check = store.checkSeed(this.#seeds[store.name]);
|
|
34
|
+
if (check.ok)
|
|
35
|
+
return store.instance(check.state);
|
|
36
|
+
console.error(`store "${store.name}": the server's seed failed its schema, so it starts from init. ` +
|
|
37
|
+
`Issues: ${check.issues.join('; ')}`);
|
|
38
|
+
return store.instance();
|
|
39
|
+
}
|
|
40
|
+
/** Every instance's state by store name (what the SSR seed carries). */
|
|
41
|
+
snapshot() {
|
|
42
|
+
return Object.fromEntries([...this.#instances].map(([name, i]) => [name, i.state]));
|
|
43
|
+
}
|
|
44
|
+
/** Disposes every instance (stops their commands). */
|
|
45
|
+
dispose() {
|
|
46
|
+
for (const instance of this.#instances.values())
|
|
47
|
+
instance.dispose();
|
|
48
|
+
this.#instances.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let serverScope;
|
|
52
|
+
/**
|
|
53
|
+
* Runs `fn` with `registry` as the server's store scope. Server renders are synchronous per
|
|
54
|
+
* render step, so @gyral/ssr wraps every step: interleaved requests never see each other's
|
|
55
|
+
* stores, without AsyncLocalStorage (works on any runtime).
|
|
56
|
+
*/
|
|
57
|
+
export function withStoreScope(registry, fn) {
|
|
58
|
+
const previous = serverScope;
|
|
59
|
+
serverScope = registry;
|
|
60
|
+
try {
|
|
61
|
+
return fn();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
serverScope = previous;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** JSON for an inline `<script type="application/json">`: `</script>` and friends escaped. */
|
|
68
|
+
export function scriptSafeJson(value) {
|
|
69
|
+
return JSON.stringify(value)
|
|
70
|
+
.replaceAll('<', '\\u003c')
|
|
71
|
+
.replaceAll('>', '\\u003e')
|
|
72
|
+
.replaceAll('&', '\\u0026')
|
|
73
|
+
.replaceAll('
', '\\u2028')
|
|
74
|
+
.replaceAll('
', '\\u2029');
|
|
75
|
+
}
|
|
76
|
+
let documentRegistry;
|
|
77
|
+
function parseSeeds(raw, where) {
|
|
78
|
+
if (raw == null)
|
|
79
|
+
return {};
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : {};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error(`unreadable ${STORE_SEED_ATTRIBUTE} seed on ${where}`, error);
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The client's default scope, restored from the server's page seed on first use. */
|
|
90
|
+
function documentScope() {
|
|
91
|
+
const script = document.querySelector(`script[${STORE_SEED_ATTRIBUTE}]`);
|
|
92
|
+
documentRegistry ??= new StoreRegistry([], parseSeeds(script?.textContent, 'the page'));
|
|
93
|
+
return documentRegistry;
|
|
94
|
+
}
|
|
95
|
+
const providers = new WeakMap();
|
|
96
|
+
/**
|
|
97
|
+
* A `<gyral-stores>` provider's scope, created on first use. On the client a server-rendered
|
|
98
|
+
* provider carries its instances' states in `data-gyral-stores` (gyral-czi.20): seeded stores
|
|
99
|
+
* are restored from it (so hydration matches the server), and `.instances` supplies the rest.
|
|
100
|
+
*/
|
|
101
|
+
export function providerScope(el) {
|
|
102
|
+
let registry = providers.get(el);
|
|
103
|
+
if (registry === undefined) {
|
|
104
|
+
const { instances = [] } = el;
|
|
105
|
+
const seeds = onServer
|
|
106
|
+
? {}
|
|
107
|
+
: parseSeeds(el.getAttribute(STORE_SEED_ATTRIBUTE), `<${el.localName}>`);
|
|
108
|
+
const unseeded = instances.filter((i) => !(i.store.name in seeds));
|
|
109
|
+
registry = new StoreRegistry(unseeded, seeds);
|
|
110
|
+
providers.set(el, registry);
|
|
111
|
+
}
|
|
112
|
+
return registry;
|
|
113
|
+
}
|
|
114
|
+
/** Event a component dispatches on the server to find its nearest `<gyral-stores>` provider. */
|
|
115
|
+
export const STORES_REQUEST = 'gyral-stores-request';
|
|
116
|
+
// The server has no DOM ancestry, but Lit's SSR DOM shim bubbles events through the custom
|
|
117
|
+
// elements being rendered (the mechanism @lit/context uses), so a registered provider can answer.
|
|
118
|
+
function serverProvider(host) {
|
|
119
|
+
const detail = {};
|
|
120
|
+
host.dispatchEvent(new CustomEvent(STORES_REQUEST, { bubbles: true, composed: true, detail }));
|
|
121
|
+
return detail.registry;
|
|
122
|
+
}
|
|
123
|
+
/** The scope for a component: its nearest provider (across shadow roots) or the default. */
|
|
124
|
+
export function scopeFor(host, tag, store) {
|
|
125
|
+
if (onServer) {
|
|
126
|
+
const provided = serverProvider(host);
|
|
127
|
+
if (provided !== undefined)
|
|
128
|
+
return provided;
|
|
129
|
+
if (serverScope !== undefined)
|
|
130
|
+
return serverScope;
|
|
131
|
+
throw new Error(`<${tag}> reads store "${store.name}" during a server render without a store scope. ` +
|
|
132
|
+
"Pass the request's store instances to page({ stores }) or renderToString(value, { stores }).");
|
|
133
|
+
}
|
|
134
|
+
for (let node = host.parentNode; node !== null;) {
|
|
135
|
+
if (node instanceof Element && node.localName === STORES_ELEMENT)
|
|
136
|
+
return providerScope(node);
|
|
137
|
+
node = node instanceof ShadowRoot ? node.host : node.parentNode;
|
|
138
|
+
}
|
|
139
|
+
return documentScope();
|
|
140
|
+
}
|
|
141
|
+
/** Tests only: forget the document default so each test starts clean. */
|
|
142
|
+
export function resetDocumentStores() {
|
|
143
|
+
documentRegistry?.dispose();
|
|
144
|
+
documentRegistry = undefined;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=store-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-scope.js","sourceRoot":"","sources":["../src/store-scope.ts"],"names":[],"mappings":"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","sourcesContent":["// Which store instance a component uses (docs/design-docs/0013-shared-state.md):\n// el.stores override → nearest <gyral-stores> ancestor → the document default (client), or the\n// request scope set by @gyral/ssr around each render step (server).\nimport { isServer } from 'lit';\nimport type { AnyStore, AnyStoreInstance } from './store.js';\n\nconst onServer: boolean = isServer;\n\n/** Page-level seed written by @gyral/ssr: `<script type=\"application/json\" data-gyral-stores>`. */\nexport const STORE_SEED_ATTRIBUTE = 'data-gyral-stores';\n\n/** Provider element name. Set `.instances=${[cart.instance()]}`, or let it create its own. */\nexport const STORES_ELEMENT = 'gyral-stores';\n\n/** A set of store instances by store name, created on demand from optional seeds. */\nexport class StoreRegistry {\n readonly #instances = new Map<string, AnyStoreInstance>();\n readonly #seeds: Readonly<Record<string, unknown>>;\n\n constructor(\n instances: readonly AnyStoreInstance[] = [],\n seeds: Readonly<Record<string, unknown>> = {},\n ) {\n for (const instance of instances) this.#hold(instance);\n this.#seeds = seeds;\n }\n\n /** The instance for `store`, creating it (from its seed, if any) on first use. */\n get(store: AnyStore): AnyStoreInstance {\n return this.#instances.get(store.name) ?? this.#hold(this.#create(store));\n }\n\n /** Keeps an instance and lets its `send(other, msg)` commands reach this scope. */\n #hold(instance: AnyStoreInstance): AnyStoreInstance {\n this.#instances.set(instance.store.name, instance);\n instance.bindScope?.((store) => this.get(store));\n return instance;\n }\n\n /** A new instance from its seed (checked by the store's schema), or from `init`. */\n #create(store: AnyStore): AnyStoreInstance {\n if (!(store.name in this.#seeds)) return store.instance();\n const check = store.checkSeed(this.#seeds[store.name]);\n if (check.ok) return store.instance(check.state as never);\n console.error(\n `store \"${store.name}\": the server's seed failed its schema, so it starts from init. ` +\n `Issues: ${check.issues.join('; ')}`,\n );\n return store.instance();\n }\n\n /** Every instance's state by store name (what the SSR seed carries). */\n snapshot(): Readonly<Record<string, unknown>> {\n return Object.fromEntries([...this.#instances].map(([name, i]) => [name, i.state]));\n }\n\n /** Disposes every instance (stops their commands). */\n dispose(): void {\n for (const instance of this.#instances.values()) instance.dispose();\n this.#instances.clear();\n }\n}\n\nlet serverScope: StoreRegistry | undefined;\n\n/**\n * Runs `fn` with `registry` as the server's store scope. Server renders are synchronous per\n * render step, so @gyral/ssr wraps every step: interleaved requests never see each other's\n * stores, without AsyncLocalStorage (works on any runtime).\n */\nexport function withStoreScope<T>(registry: StoreRegistry, fn: () => T): T {\n const previous = serverScope;\n serverScope = registry;\n try {\n return fn();\n } finally {\n serverScope = previous;\n }\n}\n\n/** JSON for an inline `<script type=\"application/json\">`: `</script>` and friends escaped. */\nexport function scriptSafeJson(value: unknown): string {\n return JSON.stringify(value)\n .replaceAll('<', '\\\\u003c')\n .replaceAll('>', '\\\\u003e')\n .replaceAll('&', '\\\\u0026')\n .replaceAll('
', '\\\\u2028')\n .replaceAll('
', '\\\\u2029');\n}\n\nlet documentRegistry: StoreRegistry | undefined;\n\nfunction parseSeeds(\n raw: string | null | undefined,\n where: string,\n): Readonly<Record<string, unknown>> {\n if (raw == null) return {};\n try {\n const parsed = JSON.parse(raw) as unknown;\n return parsed !== null && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : {};\n } catch (error) {\n console.error(`unreadable ${STORE_SEED_ATTRIBUTE} seed on ${where}`, error);\n return {};\n }\n}\n\n/** The client's default scope, restored from the server's page seed on first use. */\nfunction documentScope(): StoreRegistry {\n const script = document.querySelector(`script[${STORE_SEED_ATTRIBUTE}]`);\n documentRegistry ??= new StoreRegistry([], parseSeeds(script?.textContent, 'the page'));\n return documentRegistry;\n}\n\nconst providers = new WeakMap<Element, StoreRegistry>();\n\n/**\n * A `<gyral-stores>` provider's scope, created on first use. On the client a server-rendered\n * provider carries its instances' states in `data-gyral-stores` (gyral-czi.20): seeded stores\n * are restored from it (so hydration matches the server), and `.instances` supplies the rest.\n */\nexport function providerScope(el: Element): StoreRegistry {\n let registry = providers.get(el);\n if (registry === undefined) {\n const { instances = [] } = el as { instances?: readonly AnyStoreInstance[] };\n const seeds = onServer\n ? {}\n : parseSeeds(el.getAttribute(STORE_SEED_ATTRIBUTE), `<${el.localName}>`);\n const unseeded = instances.filter((i) => !(i.store.name in seeds));\n registry = new StoreRegistry(unseeded, seeds);\n providers.set(el, registry);\n }\n return registry;\n}\n\n/** Event a component dispatches on the server to find its nearest `<gyral-stores>` provider. */\nexport const STORES_REQUEST = 'gyral-stores-request';\n\n/** The detail of a STORES_REQUEST event; the provider fills in `registry`. */\nexport interface StoresRequest {\n registry?: StoreRegistry;\n}\n\n// The server has no DOM ancestry, but Lit's SSR DOM shim bubbles events through the custom\n// elements being rendered (the mechanism @lit/context uses), so a registered provider can answer.\nfunction serverProvider(host: Element): StoreRegistry | undefined {\n const detail: StoresRequest = {};\n host.dispatchEvent(new CustomEvent(STORES_REQUEST, { bubbles: true, composed: true, detail }));\n return detail.registry;\n}\n\n/** The scope for a component: its nearest provider (across shadow roots) or the default. */\nexport function scopeFor(host: Element, tag: string, store: AnyStore): StoreRegistry {\n if (onServer) {\n const provided = serverProvider(host);\n if (provided !== undefined) return provided;\n if (serverScope !== undefined) return serverScope;\n throw new Error(\n `<${tag}> reads store \"${store.name}\" during a server render without a store scope. ` +\n \"Pass the request's store instances to page({ stores }) or renderToString(value, { stores }).\",\n );\n }\n for (let node: Node | null = host.parentNode; node !== null;) {\n if (node instanceof Element && node.localName === STORES_ELEMENT) return providerScope(node);\n node = node instanceof ShadowRoot ? node.host : node.parentNode;\n }\n return documentScope();\n}\n\n/** Tests only: forget the document default so each test starts clean. */\nexport function resetDocumentStores(): void {\n documentRegistry?.dispose();\n documentRegistry = undefined;\n}\n"]}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { type Command, type DriverOverrides, type Next } from './command.js';
|
|
3
|
+
import type { Tagged } from './types.js';
|
|
4
|
+
type Variant<M extends Tagged, K extends M['_tag']> = Extract<M, {
|
|
5
|
+
readonly _tag: K;
|
|
6
|
+
}>;
|
|
7
|
+
/** One pure reducer per message tag, like a component's `update` but without context. */
|
|
8
|
+
export type StoreUpdate<S, M extends Tagged> = {
|
|
9
|
+
readonly [K in M['_tag']]: (state: S, msg: Variant<M, K>) => Next<S, M>;
|
|
10
|
+
};
|
|
11
|
+
export interface StoreSpec<S, M extends Tagged> {
|
|
12
|
+
/** Initial state (and optional commands, e.g. load a saved cart). */
|
|
13
|
+
readonly init: () => Next<S, M>;
|
|
14
|
+
readonly update: StoreUpdate<S, M>;
|
|
15
|
+
/** Driver substitutions by name for this store's commands. */
|
|
16
|
+
readonly drivers?: DriverOverrides;
|
|
17
|
+
/**
|
|
18
|
+
* Validates the server's page seed before the client uses it (any synchronous Standard
|
|
19
|
+
* Schema). An invalid seed is reported with console.error and the store starts from `init`.
|
|
20
|
+
*/
|
|
21
|
+
readonly schema?: StandardSchemaV1<unknown, S>;
|
|
22
|
+
}
|
|
23
|
+
/** The result of checking a seed against a store's schema. */
|
|
24
|
+
export type SeedCheck<S> = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
readonly state: S;
|
|
27
|
+
} | {
|
|
28
|
+
readonly ok: false;
|
|
29
|
+
readonly issues: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
/** What `ctx.read()` accepts: anything that names a store and carries its state type. */
|
|
32
|
+
export interface StoreRef<S> {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/** Type-only marker for the state type. Never set at runtime. */
|
|
35
|
+
readonly stateType?: (state: S) => S;
|
|
36
|
+
}
|
|
37
|
+
/** A live store: one per page on the client, one per request on the server, one per test. */
|
|
38
|
+
export interface StoreInstance<S, M extends Tagged> {
|
|
39
|
+
readonly store: Store<S, M>;
|
|
40
|
+
readonly state: S;
|
|
41
|
+
/** Feeds a message through the store's `update` and notifies subscribers on change. */
|
|
42
|
+
send(msg: M): void;
|
|
43
|
+
/** Called after every state change. Returns an unsubscribe function. */
|
|
44
|
+
subscribe(listener: (state: S, prev: S) => void): () => void;
|
|
45
|
+
/** Per-instance driver substitutions (test fakes). Checked before the spec's. */
|
|
46
|
+
drivers: DriverOverrides;
|
|
47
|
+
/** Stops running commands and drops subscribers. */
|
|
48
|
+
dispose(): void;
|
|
49
|
+
/** Set by the registry that holds the instance (store-to-store `send`). */
|
|
50
|
+
bindScope(resolve: StoreResolver): void;
|
|
51
|
+
}
|
|
52
|
+
export interface Store<S, M extends Tagged> extends StoreRef<S> {
|
|
53
|
+
readonly spec: StoreSpec<S, M>;
|
|
54
|
+
/**
|
|
55
|
+
* A fresh, independent instance. `initial` skips `init`'s state (a server seed, a test
|
|
56
|
+
* fixture); `init`'s commands still run on the client.
|
|
57
|
+
*/
|
|
58
|
+
instance(initial?: S): StoreInstance<S, M>;
|
|
59
|
+
/** Checks a seed with `spec.schema` (ok as-is without one). */
|
|
60
|
+
checkSeed(seed: unknown): SeedCheck<S>;
|
|
61
|
+
/** Type-only marker for the message type. Never set at runtime. */
|
|
62
|
+
readonly messageType?: (msg: M) => M;
|
|
63
|
+
}
|
|
64
|
+
/** Any store, with its types erased (for declarations and registries). */
|
|
65
|
+
export interface AnyStore {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
instance(initial?: never): AnyStoreInstance;
|
|
68
|
+
checkSeed(seed: unknown): SeedCheck<unknown>;
|
|
69
|
+
}
|
|
70
|
+
/** Any store instance, with its types erased. */
|
|
71
|
+
export interface AnyStoreInstance {
|
|
72
|
+
readonly store: AnyStore;
|
|
73
|
+
readonly state: unknown;
|
|
74
|
+
send(msg: never): void;
|
|
75
|
+
subscribe(listener: (state: unknown, prev: unknown) => void): () => void;
|
|
76
|
+
dispose(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Called by the registry that holds this instance, so the store's `send(other, msg)`
|
|
79
|
+
* commands reach `other`'s instance in the same scope (gyral-czi.20).
|
|
80
|
+
*/
|
|
81
|
+
bindScope?(resolve: StoreResolver): void;
|
|
82
|
+
}
|
|
83
|
+
/** Finds a store's instance in a scope (a registry). */
|
|
84
|
+
export type StoreResolver = (store: AnyStore) => AnyStoreInstance;
|
|
85
|
+
/** Store instances substituted by store name (tests, islands). */
|
|
86
|
+
export type StoreOverrides = Readonly<Record<string, AnyStoreInstance>>;
|
|
87
|
+
/** Defines a store. `name` keys it in registries, seeds and overrides, so keep it unique. */
|
|
88
|
+
export declare function defineStore<S, M extends Tagged>(name: string, spec: StoreSpec<S, M>): Store<S, M>;
|
|
89
|
+
/** Marker driver for `send()`: `define()` delivers these to the resolved store instance. */
|
|
90
|
+
export declare const STORE_SEND: {
|
|
91
|
+
readonly name: "@gyral/store-send";
|
|
92
|
+
readonly run: () => undefined;
|
|
93
|
+
};
|
|
94
|
+
/** The input of a `send()` command. */
|
|
95
|
+
export interface StoreSendInput {
|
|
96
|
+
readonly store: AnyStore;
|
|
97
|
+
readonly msg: Tagged;
|
|
98
|
+
}
|
|
99
|
+
/** A command that sends `msg` to the component's instance of `store`. Writes are commands. */
|
|
100
|
+
export declare function send<S, M extends Tagged>(store: Store<S, M>, msg: NoInfer<M>): Command<never>;
|
|
101
|
+
/** Framework message: a store this component reads changed (ADR 0013). Optional reducer. */
|
|
102
|
+
export interface StoreChanged {
|
|
103
|
+
readonly _tag: 'StoreChanged';
|
|
104
|
+
/** The store's name. Narrow with `changed(store, msg)`. */
|
|
105
|
+
readonly store: string;
|
|
106
|
+
readonly state: unknown;
|
|
107
|
+
readonly prev: unknown;
|
|
108
|
+
}
|
|
109
|
+
/** Narrows a `StoreChanged` message to one store's typed state, or `undefined`. */
|
|
110
|
+
export declare function changed<S>(store: StoreRef<S>, msg: StoreChanged): {
|
|
111
|
+
readonly state: S;
|
|
112
|
+
readonly prev: S;
|
|
113
|
+
} | undefined;
|
|
114
|
+
export {};
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isServer } from 'lit';
|
|
2
|
+
import { DEVTOOLS_ENABLED, devCommands, devStore } from '#devtools';
|
|
3
|
+
import { splitNext, } from './command.js';
|
|
4
|
+
import { makeInterpreter } from './internal/interpreter.js';
|
|
5
|
+
const onServer = isServer;
|
|
6
|
+
/** Defines a store. `name` keys it in registries, seeds and overrides, so keep it unique. */
|
|
7
|
+
export function defineStore(name, spec) {
|
|
8
|
+
const store = {
|
|
9
|
+
name,
|
|
10
|
+
spec,
|
|
11
|
+
instance: (initial) => createInstance(store, initial),
|
|
12
|
+
checkSeed: (seed) => checkSeed(spec.schema, seed),
|
|
13
|
+
};
|
|
14
|
+
return store;
|
|
15
|
+
}
|
|
16
|
+
const issuePath = (issue) => (issue.path ?? []).map((s) => String(typeof s === 'object' ? s.key : s)).join('.');
|
|
17
|
+
function checkSeed(schema, seed) {
|
|
18
|
+
// Sound without a schema: the seed is this store's state, serialized by the server.
|
|
19
|
+
if (schema === undefined)
|
|
20
|
+
return { ok: true, state: seed };
|
|
21
|
+
const result = schema['~standard'].validate(seed);
|
|
22
|
+
if (result instanceof Promise) {
|
|
23
|
+
return { ok: false, issues: ['the store schema is async; seeds need a synchronous schema'] };
|
|
24
|
+
}
|
|
25
|
+
if (result.issues === undefined)
|
|
26
|
+
return { ok: true, state: result.value };
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
issues: result.issues.map((i) => `${issuePath(i) || '(root)'}: ${i.message}`),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createInstance(store, initial) {
|
|
33
|
+
// Sound: send() only calls the reducer whose key equals msg._tag.
|
|
34
|
+
const reducers = store.spec.update;
|
|
35
|
+
const listeners = new Set();
|
|
36
|
+
let interpreter;
|
|
37
|
+
let disposed = false;
|
|
38
|
+
const [initState, initCommands] = splitNext(store.spec.init());
|
|
39
|
+
let state = initial === undefined ? initState : initial;
|
|
40
|
+
const resolve = (driver) => instance.drivers[driver.name] ?? store.spec.drivers?.[driver.name] ?? driver;
|
|
41
|
+
let scope;
|
|
42
|
+
// Store-to-store writes: delivered synchronously to the other store's instance in scope.
|
|
43
|
+
const deliver = ({ store: target, msg }) => {
|
|
44
|
+
if (scope === undefined) {
|
|
45
|
+
console.warn(`store "${store.name}" sends to "${target.name}" but is not held by a store scope ` +
|
|
46
|
+
'(a registry, <gyral-stores> or the page default), so the message is dropped.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
scope(target).send(msg);
|
|
50
|
+
};
|
|
51
|
+
const run = (commands) => {
|
|
52
|
+
if (onServer || disposed || commands.length === 0)
|
|
53
|
+
return; // never on the server (ADR 0012)
|
|
54
|
+
for (const cmd of commands) {
|
|
55
|
+
if (cmd.driver === STORE_SEND) {
|
|
56
|
+
deliver(cmd.input);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
interpreter ??= makeInterpreter(resolve, (msg) => {
|
|
60
|
+
// Stores have no IntentRejected reducer; only their own messages apply.
|
|
61
|
+
if (msg._tag !== 'IntentRejected')
|
|
62
|
+
instance.send(msg);
|
|
63
|
+
}, DEVTOOLS_ENABLED ? devCommands(() => `store:${store.name}`) : undefined);
|
|
64
|
+
interpreter.run(cmd);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const instance = {
|
|
68
|
+
store,
|
|
69
|
+
get state() {
|
|
70
|
+
return state;
|
|
71
|
+
},
|
|
72
|
+
drivers: {},
|
|
73
|
+
send(msg) {
|
|
74
|
+
const reducer = reducers[msg._tag];
|
|
75
|
+
if (reducer === undefined) {
|
|
76
|
+
console.warn(`store "${store.name}" has no update for message "${msg._tag}".`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const prev = state;
|
|
80
|
+
const [next, commands] = splitNext(reducer(prev, msg));
|
|
81
|
+
state = next;
|
|
82
|
+
if (DEVTOOLS_ENABLED)
|
|
83
|
+
devStore(store.name, msg, prev, next);
|
|
84
|
+
if (!Object.is(next, prev))
|
|
85
|
+
for (const listener of [...listeners])
|
|
86
|
+
listener(next, prev);
|
|
87
|
+
run(commands);
|
|
88
|
+
},
|
|
89
|
+
subscribe(listener) {
|
|
90
|
+
listeners.add(listener);
|
|
91
|
+
return () => listeners.delete(listener);
|
|
92
|
+
},
|
|
93
|
+
dispose() {
|
|
94
|
+
disposed = true;
|
|
95
|
+
interpreter?.dispose();
|
|
96
|
+
listeners.clear();
|
|
97
|
+
},
|
|
98
|
+
bindScope(resolver) {
|
|
99
|
+
scope = resolver;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
// A seeded instance (hydration) starts init's commands after the hydrating renders flushed,
|
|
103
|
+
// so a fast driver can't change state under them (same reason as ADR 0012 for components).
|
|
104
|
+
if (initial === undefined)
|
|
105
|
+
run(initCommands);
|
|
106
|
+
else if (initCommands.length > 0)
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
run(initCommands);
|
|
109
|
+
}, 0);
|
|
110
|
+
return instance;
|
|
111
|
+
}
|
|
112
|
+
/** Marker driver for `send()`: `define()` delivers these to the resolved store instance. */
|
|
113
|
+
export const STORE_SEND = {
|
|
114
|
+
name: '@gyral/store-send',
|
|
115
|
+
run: () => undefined,
|
|
116
|
+
};
|
|
117
|
+
/** A command that sends `msg` to the component's instance of `store`. Writes are commands. */
|
|
118
|
+
// NoInfer: the store decides the message type, so a literal (`{ _tag: 'Track', … }`) is checked
|
|
119
|
+
// against it instead of widening `_tag` to string (store-to-store sends have no other context).
|
|
120
|
+
export function send(store, msg) {
|
|
121
|
+
const input = { store, msg };
|
|
122
|
+
return { driver: STORE_SEND, input, onSuccess: () => undefined };
|
|
123
|
+
}
|
|
124
|
+
/** Narrows a `StoreChanged` message to one store's typed state, or `undefined`. */
|
|
125
|
+
export function changed(store, msg) {
|
|
126
|
+
// Sound: the message was built from this store's instance (matched by unique name).
|
|
127
|
+
return msg.store === store.name ? { state: msg.state, prev: msg.prev } : undefined;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared state: stores as \"props from the side\" (docs/design-docs/0013-shared-state.md).\n// A store is MVI without a view: init + pure update, commands run by its own interpreter.\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { isServer } from 'lit';\nimport { DEVTOOLS_ENABLED, devCommands, devStore } from '#devtools';\nimport {\n splitNext,\n type AnyDriver,\n type Command,\n type DriverOverrides,\n type Next,\n} from './command.js';\nimport { makeInterpreter, type Interpreter } from './internal/interpreter.js';\nimport type { IntentRejected, Tagged } from './types.js';\n\nconst onServer: boolean = isServer;\n\ntype Variant<M extends Tagged, K extends M['_tag']> = Extract<M, { readonly _tag: K }>;\n\n/** One pure reducer per message tag, like a component's `update` but without context. */\nexport type StoreUpdate<S, M extends Tagged> = {\n readonly [K in M['_tag']]: (state: S, msg: Variant<M, K>) => Next<S, M>;\n};\n\nexport interface StoreSpec<S, M extends Tagged> {\n /** Initial state (and optional commands, e.g. load a saved cart). */\n readonly init: () => Next<S, M>;\n readonly update: StoreUpdate<S, M>;\n /** Driver substitutions by name for this store's commands. */\n readonly drivers?: DriverOverrides;\n /**\n * Validates the server's page seed before the client uses it (any synchronous Standard\n * Schema). An invalid seed is reported with console.error and the store starts from `init`.\n */\n readonly schema?: StandardSchemaV1<unknown, S>;\n}\n\n/** The result of checking a seed against a store's schema. */\nexport type SeedCheck<S> =\n | { readonly ok: true; readonly state: S }\n | { readonly ok: false; readonly issues: readonly string[] };\n\n/** What `ctx.read()` accepts: anything that names a store and carries its state type. */\nexport interface StoreRef<S> {\n readonly name: string;\n /** Type-only marker for the state type. Never set at runtime. */\n readonly stateType?: (state: S) => S;\n}\n\n/** A live store: one per page on the client, one per request on the server, one per test. */\nexport interface StoreInstance<S, M extends Tagged> {\n readonly store: Store<S, M>;\n readonly state: S;\n /** Feeds a message through the store's `update` and notifies subscribers on change. */\n send(msg: M): void;\n /** Called after every state change. Returns an unsubscribe function. */\n subscribe(listener: (state: S, prev: S) => void): () => void;\n /** Per-instance driver substitutions (test fakes). Checked before the spec's. */\n drivers: DriverOverrides;\n /** Stops running commands and drops subscribers. */\n dispose(): void;\n /** Set by the registry that holds the instance (store-to-store `send`). */\n bindScope(resolve: StoreResolver): void;\n}\n\nexport interface Store<S, M extends Tagged> extends StoreRef<S> {\n readonly spec: StoreSpec<S, M>;\n /**\n * A fresh, independent instance. `initial` skips `init`'s state (a server seed, a test\n * fixture); `init`'s commands still run on the client.\n */\n instance(initial?: S): StoreInstance<S, M>;\n /** Checks a seed with `spec.schema` (ok as-is without one). */\n checkSeed(seed: unknown): SeedCheck<S>;\n /** Type-only marker for the message type. Never set at runtime. */\n readonly messageType?: (msg: M) => M;\n}\n\n/** Any store, with its types erased (for declarations and registries). */\nexport interface AnyStore {\n readonly name: string;\n instance(initial?: never): AnyStoreInstance;\n checkSeed(seed: unknown): SeedCheck<unknown>;\n}\n\n/** Any store instance, with its types erased. */\nexport interface AnyStoreInstance {\n readonly store: AnyStore;\n readonly state: unknown;\n send(msg: never): void;\n subscribe(listener: (state: unknown, prev: unknown) => void): () => void;\n dispose(): void;\n /**\n * Called by the registry that holds this instance, so the store's `send(other, msg)`\n * commands reach `other`'s instance in the same scope (gyral-czi.20).\n */\n bindScope?(resolve: StoreResolver): void;\n}\n\n/** Finds a store's instance in a scope (a registry). */\nexport type StoreResolver = (store: AnyStore) => AnyStoreInstance;\n\n/** Store instances substituted by store name (tests, islands). */\nexport type StoreOverrides = Readonly<Record<string, AnyStoreInstance>>;\n\n/** Defines a store. `name` keys it in registries, seeds and overrides, so keep it unique. */\nexport function defineStore<S, M extends Tagged>(name: string, spec: StoreSpec<S, M>): Store<S, M> {\n const store: Store<S, M> = {\n name,\n spec,\n instance: (initial?: S) => createInstance(store, initial),\n checkSeed: (seed) => checkSeed(spec.schema, seed),\n };\n return store;\n}\n\nconst issuePath = (issue: StandardSchemaV1.Issue): string =>\n (issue.path ?? []).map((s) => String(typeof s === 'object' ? s.key : s)).join('.');\n\nfunction checkSeed<S>(\n schema: StandardSchemaV1<unknown, S> | undefined,\n seed: unknown,\n): SeedCheck<S> {\n // Sound without a schema: the seed is this store's state, serialized by the server.\n if (schema === undefined) return { ok: true, state: seed as S };\n const result = schema['~standard'].validate(seed);\n if (result instanceof Promise) {\n return { ok: false, issues: ['the store schema is async; seeds need a synchronous schema'] };\n }\n if (result.issues === undefined) return { ok: true, state: result.value };\n return {\n ok: false,\n issues: result.issues.map((i) => `${issuePath(i) || '(root)'}: ${i.message}`),\n };\n}\n\nfunction createInstance<S, M extends Tagged>(\n store: Store<S, M>,\n initial: S | undefined,\n): StoreInstance<S, M> {\n // Sound: send() only calls the reducer whose key equals msg._tag.\n const reducers = store.spec.update as unknown as Readonly<\n Record<string, ((state: S, msg: M) => Next<S, M>) | undefined>\n >;\n const listeners = new Set<(state: S, prev: S) => void>();\n let interpreter: Interpreter<M | IntentRejected> | undefined;\n let disposed = false;\n\n const [initState, initCommands] = splitNext(store.spec.init());\n let state = initial === undefined ? initState : initial;\n\n const resolve = (driver: AnyDriver): AnyDriver =>\n instance.drivers[driver.name] ?? store.spec.drivers?.[driver.name] ?? driver;\n\n let scope: StoreResolver | undefined;\n\n // Store-to-store writes: delivered synchronously to the other store's instance in scope.\n const deliver = ({ store: target, msg }: StoreSendInput): void => {\n if (scope === undefined) {\n console.warn(\n `store \"${store.name}\" sends to \"${target.name}\" but is not held by a store scope ` +\n '(a registry, <gyral-stores> or the page default), so the message is dropped.',\n );\n return;\n }\n (scope(target) as { send(msg: unknown): void }).send(msg);\n };\n\n const run = (commands: ReadonlyArray<Command<M | IntentRejected>>): void => {\n if (onServer || disposed || commands.length === 0) return; // never on the server (ADR 0012)\n for (const cmd of commands) {\n if (cmd.driver === STORE_SEND) {\n deliver(cmd.input as StoreSendInput);\n continue;\n }\n interpreter ??= makeInterpreter<M | IntentRejected>(\n resolve,\n (msg) => {\n // Stores have no IntentRejected reducer; only their own messages apply.\n if (msg._tag !== 'IntentRejected') instance.send(msg as M);\n },\n DEVTOOLS_ENABLED ? devCommands(() => `store:${store.name}`) : undefined,\n );\n interpreter.run(cmd);\n }\n };\n\n const instance: StoreInstance<S, M> = {\n store,\n get state() {\n return state;\n },\n drivers: {},\n send(msg) {\n const reducer = reducers[msg._tag];\n if (reducer === undefined) {\n console.warn(`store \"${store.name}\" has no update for message \"${msg._tag}\".`);\n return;\n }\n const prev = state;\n const [next, commands] = splitNext(reducer(prev, msg));\n state = next;\n if (DEVTOOLS_ENABLED) devStore(store.name, msg, prev, next);\n if (!Object.is(next, prev)) for (const listener of [...listeners]) listener(next, prev);\n run(commands);\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n disposed = true;\n interpreter?.dispose();\n listeners.clear();\n },\n bindScope(resolver) {\n scope = resolver;\n },\n };\n\n // A seeded instance (hydration) starts init's commands after the hydrating renders flushed,\n // so a fast driver can't change state under them (same reason as ADR 0012 for components).\n if (initial === undefined) run(initCommands);\n else if (initCommands.length > 0)\n setTimeout(() => {\n run(initCommands);\n }, 0);\n return instance;\n}\n\n/** Marker driver for `send()`: `define()` delivers these to the resolved store instance. */\nexport const STORE_SEND = {\n name: '@gyral/store-send',\n run: () => undefined,\n} as const;\n\n/** The input of a `send()` command. */\nexport interface StoreSendInput {\n readonly store: AnyStore;\n readonly msg: Tagged;\n}\n\n/** A command that sends `msg` to the component's instance of `store`. Writes are commands. */\n// NoInfer: the store decides the message type, so a literal (`{ _tag: 'Track', … }`) is checked\n// against it instead of widening `_tag` to string (store-to-store sends have no other context).\nexport function send<S, M extends Tagged>(store: Store<S, M>, msg: NoInfer<M>): Command<never> {\n const input: StoreSendInput = { store, msg };\n return { driver: STORE_SEND, input, onSuccess: () => undefined };\n}\n\n/** Framework message: a store this component reads changed (ADR 0013). Optional reducer. */\nexport interface StoreChanged {\n readonly _tag: 'StoreChanged';\n /** The store's name. Narrow with `changed(store, msg)`. */\n readonly store: string;\n readonly state: unknown;\n readonly prev: unknown;\n}\n\n/** Narrows a `StoreChanged` message to one store's typed state, or `undefined`. */\nexport function changed<S>(\n store: StoreRef<S>,\n msg: StoreChanged,\n): { readonly state: S; readonly prev: S } | undefined {\n // Sound: the message was built from this store's instance (matched by unique name).\n return msg.store === store.name ? { state: msg.state as S, prev: msg.prev as S } : undefined;\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers `<gyral-stores>` for server renders; `@gyral/ssr` calls it. A no-op in the browser,
|
|
3
|
+
* where a plain `<gyral-stores>` element already scopes stores and the server's shadow root
|
|
4
|
+
* (a `<slot>`) needs no JavaScript. Idempotent.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defineStoresProvider(): void;
|