@gyral/core 0.0.0 → 0.2.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 +32 -0
- package/LICENSE +1 -1
- 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 +338 -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 +68 -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 +39 -0
- package/dist/focus.js +42 -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 +43 -0
- package/dist/index.js +37 -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 +166 -0
- package/dist/internal/interpreter.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/lit-html-check.d.ts +6 -0
- package/dist/lit-html-check.js +48 -0
- package/dist/lit-html-check.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/template-whitespace.d.ts +7 -0
- package/dist/template-whitespace.js +192 -0
- package/dist/template-whitespace.js.map +1 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.js +15 -0
- package/dist/templates.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 -13
package/dist/props.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PropertyDeclaration } from 'lit';
|
|
2
|
+
type Bag = Readonly<Record<string, unknown>>;
|
|
3
|
+
/** Gyral's view of a declaration: Lit's options plus `required` and `default`. */
|
|
4
|
+
export interface PropInfo extends PropertyDeclaration {
|
|
5
|
+
readonly required?: boolean;
|
|
6
|
+
readonly default?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type PropTable = Readonly<Record<string, PropInfo>>;
|
|
9
|
+
/** The element's declared props as set on it (no defaults): what a seed must carry. */
|
|
10
|
+
export declare function readRawProps(el: object, table: PropTable): Bag;
|
|
11
|
+
/** The element's declared props as components see them: `default` fills `undefined`. */
|
|
12
|
+
export declare function readProps(el: object, table: PropTable): Bag;
|
|
13
|
+
/** Required props that are still `undefined` (ADR 0007 addendum). */
|
|
14
|
+
export declare function missingRequired(el: object, table: PropTable): string[];
|
|
15
|
+
/** True when every declared prop is identical (`Object.is`) in both snapshots. */
|
|
16
|
+
export declare function sameProps<P>(names: readonly string[], a: P, b: P): boolean;
|
|
17
|
+
/** Hydration: sets props that only travelled in the seed, unless the page already set them. */
|
|
18
|
+
export declare function restoreProps(el: object, names: readonly string[], seeded: Bag): void;
|
|
19
|
+
/**
|
|
20
|
+
* Declared props that collide with a built-in element property (`hidden`, `title`, `id`, …).
|
|
21
|
+
* Lit's reactive accessor replaces the built-in, so setting the prop silently changes what the
|
|
22
|
+
* platform does with it: a prop named `hidden` hides the element (gyral-czi.33). Browser only:
|
|
23
|
+
* the server's DOM shim doesn't model the full prototype chain.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shadowedBuiltins(names: readonly string[]): string[];
|
|
26
|
+
export {};
|
package/dist/props.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The element's declared props as set on it (no defaults): what a seed must carry. */
|
|
2
|
+
export function readRawProps(el, table) {
|
|
3
|
+
const self = el;
|
|
4
|
+
return Object.fromEntries(Object.keys(table).map((name) => [name, self[name]]));
|
|
5
|
+
}
|
|
6
|
+
/** The element's declared props as components see them: `default` fills `undefined`. */
|
|
7
|
+
export function readProps(el, table) {
|
|
8
|
+
const self = el;
|
|
9
|
+
return Object.fromEntries(Object.entries(table).map(([name, info]) => {
|
|
10
|
+
const value = self[name];
|
|
11
|
+
return [name, value === undefined && 'default' in info ? info.default : value];
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
/** Required props that are still `undefined` (ADR 0007 addendum). */
|
|
15
|
+
export function missingRequired(el, table) {
|
|
16
|
+
const self = el;
|
|
17
|
+
return Object.entries(table)
|
|
18
|
+
.filter(([name, info]) => info.required === true && self[name] === undefined)
|
|
19
|
+
.map(([name]) => name);
|
|
20
|
+
}
|
|
21
|
+
/** True when every declared prop is identical (`Object.is`) in both snapshots. */
|
|
22
|
+
export function sameProps(names, a, b) {
|
|
23
|
+
return names.every((name) => Object.is(a[name], b[name]));
|
|
24
|
+
}
|
|
25
|
+
/** Hydration: sets props that only travelled in the seed, unless the page already set them. */
|
|
26
|
+
export function restoreProps(el, names, seeded) {
|
|
27
|
+
const self = el;
|
|
28
|
+
for (const [name, value] of Object.entries(seeded)) {
|
|
29
|
+
if (names.includes(name) && self[name] === undefined)
|
|
30
|
+
self[name] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Declared props that collide with a built-in element property (`hidden`, `title`, `id`, …).
|
|
35
|
+
* Lit's reactive accessor replaces the built-in, so setting the prop silently changes what the
|
|
36
|
+
* platform does with it: a prop named `hidden` hides the element (gyral-czi.33). Browser only:
|
|
37
|
+
* the server's DOM shim doesn't model the full prototype chain.
|
|
38
|
+
*/
|
|
39
|
+
export function shadowedBuiltins(names) {
|
|
40
|
+
if (typeof HTMLElement === 'undefined')
|
|
41
|
+
return [];
|
|
42
|
+
return names.filter((name) => name in HTMLElement.prototype);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=props.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"props.js","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAaA,uFAAuF;AACvF,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAgB;IACvD,MAAM,IAAI,GAAG,EAAS,CAAC;IACvB,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,SAAS,CAAC,EAAU,EAAE,KAAgB;IACpD,MAAM,IAAI,GAAG,EAAS,CAAC;IACvB,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,CAAC,IAAI,EAAE,KAAK,KAAK,SAAS,IAAI,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACjF,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,eAAe,CAAC,EAAU,EAAE,KAAgB;IAC1D,MAAM,IAAI,GAAG,EAAS,CAAC;IACvB,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;SAC5E,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,SAAS,CAAI,KAAwB,EAAE,CAAI,EAAE,CAAI;IAC/D,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAE,CAAS,CAAC,IAAI,CAAC,EAAG,CAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAwB,EAAE,MAAW;IAC5E,MAAM,IAAI,GAAG,EAA6B,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IAC3E,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAwB;IACvD,IAAI,OAAO,WAAW,KAAK,WAAW;QAAE,OAAO,EAAE,CAAC;IAClD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,WAAW,CAAC,SAAS,CAAC,CAAC;AAC/D,CAAC","sourcesContent":["// Prop bookkeeping for define() (ADR 0007): snapshots, defaults, change detection, seed restore.\nimport type { PropertyDeclaration } from 'lit';\n\ntype Bag = Readonly<Record<string, unknown>>;\n\n/** Gyral's view of a declaration: Lit's options plus `required` and `default`. */\nexport interface PropInfo extends PropertyDeclaration {\n readonly required?: boolean;\n readonly default?: unknown;\n}\n\nexport type PropTable = Readonly<Record<string, PropInfo>>;\n\n/** The element's declared props as set on it (no defaults): what a seed must carry. */\nexport function readRawProps(el: object, table: PropTable): Bag {\n const self = el as Bag;\n return Object.fromEntries(Object.keys(table).map((name) => [name, self[name]]));\n}\n\n/** The element's declared props as components see them: `default` fills `undefined`. */\nexport function readProps(el: object, table: PropTable): Bag {\n const self = el as Bag;\n return Object.fromEntries(\n Object.entries(table).map(([name, info]) => {\n const value = self[name];\n return [name, value === undefined && 'default' in info ? info.default : value];\n }),\n );\n}\n\n/** Required props that are still `undefined` (ADR 0007 addendum). */\nexport function missingRequired(el: object, table: PropTable): string[] {\n const self = el as Bag;\n return Object.entries(table)\n .filter(([name, info]) => info.required === true && self[name] === undefined)\n .map(([name]) => name);\n}\n\n/** True when every declared prop is identical (`Object.is`) in both snapshots. */\nexport function sameProps<P>(names: readonly string[], a: P, b: P): boolean {\n return names.every((name) => Object.is((a as Bag)[name], (b as Bag)[name]));\n}\n\n/** Hydration: sets props that only travelled in the seed, unless the page already set them. */\nexport function restoreProps(el: object, names: readonly string[], seeded: Bag): void {\n const self = el as Record<string, unknown>;\n for (const [name, value] of Object.entries(seeded)) {\n if (names.includes(name) && self[name] === undefined) self[name] = value;\n }\n}\n\n/**\n * Declared props that collide with a built-in element property (`hidden`, `title`, `id`, …).\n * Lit's reactive accessor replaces the built-in, so setting the prop silently changes what the\n * platform does with it: a prop named `hidden` hides the element (gyral-czi.33). Browser only:\n * the server's DOM shim doesn't model the full prototype chain.\n */\nexport function shadowedBuiltins(names: readonly string[]): string[] {\n if (typeof HTMLElement === 'undefined') return [];\n return names.filter((name) => name in HTMLElement.prototype);\n}\n"]}
|
package/dist/random.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Command, type Driver } from './command.js';
|
|
2
|
+
export interface RandomInput {
|
|
3
|
+
/** How many numbers to draw. */
|
|
4
|
+
readonly count: number;
|
|
5
|
+
}
|
|
6
|
+
/** Uniform numbers in [0, 1). Substitute it in tests: `el.drivers = { random: fake }`. */
|
|
7
|
+
export declare const randomDriver: Driver<RandomInput, readonly number[]>;
|
|
8
|
+
/** Draws `count` uniform numbers in [0, 1) and turns them into a message. */
|
|
9
|
+
export declare function random<M>(count: number, toMsg: (values: readonly number[]) => M | undefined): Command<M>;
|
|
10
|
+
/** Maps a uniform number in [0, 1) to an integer in `min..max` (inclusive). Pure. */
|
|
11
|
+
export declare const toInt: (u: number, min: number, max: number) => number;
|
|
12
|
+
/** Draws one integer in `min..max` (inclusive). */
|
|
13
|
+
export declare function randomInt<M>(min: number, max: number, toMsg: (value: number) => M | undefined): Command<M>;
|
package/dist/random.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Randomness as an effect (gyral-czi.10): reducers ask for random numbers with a command,
|
|
2
|
+
// so models stay pure and tests substitute a deterministic driver by name.
|
|
3
|
+
import { command, defineDriver } from './command.js';
|
|
4
|
+
/** Uniform numbers in [0, 1). Substitute it in tests: `el.drivers = { random: fake }`. */
|
|
5
|
+
export const randomDriver = defineDriver({
|
|
6
|
+
name: 'random',
|
|
7
|
+
run: ({ count }) => Array.from({ length: count }, () => Math.random()),
|
|
8
|
+
});
|
|
9
|
+
/** Draws `count` uniform numbers in [0, 1) and turns them into a message. */
|
|
10
|
+
export function random(count, toMsg) {
|
|
11
|
+
return command(randomDriver, { count }, { onSuccess: toMsg });
|
|
12
|
+
}
|
|
13
|
+
/** Maps a uniform number in [0, 1) to an integer in `min..max` (inclusive). Pure. */
|
|
14
|
+
export const toInt = (u, min, max) => min + Math.min(max - min, Math.floor(u * (max - min + 1)));
|
|
15
|
+
/** Draws one integer in `min..max` (inclusive). */
|
|
16
|
+
export function randomInt(min, max, toMsg) {
|
|
17
|
+
return random(1, ([u = 0]) => toMsg(toInt(u, min, max)));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=random.js.map
|
|
@@ -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 {};
|