lanka 1.2.0 → 2.0.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.
Files changed (90) hide show
  1. package/README.md +3 -1
  2. package/dist/{ALankaGateway-CkW1LbKE.d.ts → ALankaGateway-BrVPaZN5.d.ts} +1 -1
  3. package/dist/ILankaFieldError-D5931-vT.d.ts +34 -0
  4. package/dist/ILankaReadableVM-BoPzHEPV.d.ts +72 -0
  5. package/dist/{ILankaScenarioMetadata-GoWWNEQL.d.ts → ILankaScenarioMetadata-Dj4GCqmX.d.ts} +1 -1
  6. package/dist/{ILankaScenarioVM-DUsI-fSc.d.ts → ILankaScenarioVM-DpKFL3iE.d.ts} +5 -4
  7. package/dist/{LankaError-xpI-qj35.d.ts → LankaError-D6RWNyLM.d.ts} +18 -0
  8. package/dist/{LankaScenarioLocator-D86TIwiu.d.ts → LankaScenarioLocator-Dki2Es3O.d.ts} +5 -5
  9. package/dist/{LankaSharedStoreLocator-zS2kLu-S.d.ts → LankaSharedStoreLocator-jsz_gnUm.d.ts} +1 -1
  10. package/dist/_extend/index.d.ts +233 -9
  11. package/dist/_extend/index.js +137 -8
  12. package/dist/_extend/index.js.map +1 -1
  13. package/dist/_internal/index.d.ts +62 -9
  14. package/dist/_internal/index.js +34 -3
  15. package/dist/_internal/index.js.map +1 -1
  16. package/dist/{activeRuntime-B336NU5I.d.ts → activeRuntime-BO-qY0yY.d.ts} +51 -4
  17. package/dist/bootstrap/index.d.ts +11 -10
  18. package/dist/bootstrap/index.js +7 -8
  19. package/dist/cache/index.d.ts +99 -0
  20. package/dist/cache/index.js +1 -0
  21. package/dist/cache/index.js.map +1 -0
  22. package/dist/{chunk-D27MREPB.js → chunk-7DQUF2QR.js} +2 -2
  23. package/dist/{chunk-UJEC7H6K.js → chunk-BHQ2SQ7P.js} +2 -2
  24. package/dist/chunk-CRIRTOLB.js +27 -0
  25. package/dist/chunk-CRIRTOLB.js.map +1 -0
  26. package/dist/chunk-EYDMBI5T.js +550 -0
  27. package/dist/chunk-EYDMBI5T.js.map +1 -0
  28. package/dist/{chunk-C2HP7CRD.js → chunk-FII3PW2G.js} +2 -2
  29. package/dist/{chunk-Q7QESSYF.js → chunk-G32H73QY.js} +10 -1
  30. package/dist/chunk-G32H73QY.js.map +1 -0
  31. package/dist/{chunk-DTO27QFR.js → chunk-H5TIUKRK.js} +2 -2
  32. package/dist/chunk-HVSQOMRE.js +69 -0
  33. package/dist/chunk-HVSQOMRE.js.map +1 -0
  34. package/dist/{chunk-73IVH3C6.js → chunk-O5EUTNR6.js} +3 -3
  35. package/dist/{chunk-RKYKK6MN.js → chunk-RUMUFHSL.js} +2 -2
  36. package/dist/{chunk-G3I7QIZR.js → chunk-SZVNPK24.js} +21 -14
  37. package/dist/chunk-SZVNPK24.js.map +1 -0
  38. package/dist/chunk-UDP6IXDS.js +17 -0
  39. package/dist/chunk-UDP6IXDS.js.map +1 -0
  40. package/dist/{chunk-5MAQVBI2.js → chunk-UMPSR4US.js} +3 -3
  41. package/dist/{chunk-BGVDPDX4.js → chunk-XGMXT4XZ.js} +20 -3
  42. package/dist/chunk-XGMXT4XZ.js.map +1 -0
  43. package/dist/config/index.js +3 -3
  44. package/dist/{createLanka-DI1CSy2Q.d.ts → createLanka-x4oSMs7P.d.ts} +2 -2
  45. package/dist/{createLankaScope-BiFxNQgl.d.ts → createLankaScope-DomNbEvb.d.ts} +1 -1
  46. package/dist/errors/index.d.ts +22 -3
  47. package/dist/errors/index.js +10 -5
  48. package/dist/errors/index.js.map +1 -1
  49. package/dist/gateway/index.d.ts +4 -3
  50. package/dist/gateway/index.js +9 -8
  51. package/dist/gateway/index.js.map +1 -1
  52. package/dist/index.d.ts +11 -10
  53. package/dist/index.js +9 -10
  54. package/dist/{lankaStandardValidator-CL-r-zEV.d.ts → lankaStandardValidator-BUFnysK0.d.ts} +10 -0
  55. package/dist/locator/index.d.ts +5 -4
  56. package/dist/locator/index.js +2 -2
  57. package/dist/logger/index.js +3 -3
  58. package/dist/mock/index.js +3 -3
  59. package/dist/scenario/index.d.ts +81 -6
  60. package/dist/scenario/index.js +6 -8
  61. package/dist/scenario/index.js.map +1 -1
  62. package/dist/storage/index.d.ts +120 -0
  63. package/dist/storage/index.js +1 -0
  64. package/dist/storage/index.js.map +1 -0
  65. package/dist/stream/index.d.ts +10 -9
  66. package/dist/validation/index.d.ts +7 -4
  67. package/dist/validation/index.js +5 -3
  68. package/dist/viewmodel/index.d.ts +188 -32
  69. package/dist/viewmodel/index.js +167 -171
  70. package/dist/viewmodel/index.js.map +1 -1
  71. package/package.json +11 -4
  72. package/skills/lanka-core/SKILL.md +70 -10
  73. package/skills/lanka-core/reference.md +417 -34
  74. package/skills/lanka-packages/SKILL.md +94 -22
  75. package/dist/chunk-BGVDPDX4.js.map +0 -1
  76. package/dist/chunk-G3I7QIZR.js.map +0 -1
  77. package/dist/chunk-GV5DUYST.js +0 -64
  78. package/dist/chunk-GV5DUYST.js.map +0 -1
  79. package/dist/chunk-O5ROO7QF.js +0 -315
  80. package/dist/chunk-O5ROO7QF.js.map +0 -1
  81. package/dist/chunk-Q7QESSYF.js.map +0 -1
  82. package/dist/chunk-UGXSGQPW.js +0 -180
  83. package/dist/chunk-UGXSGQPW.js.map +0 -1
  84. /package/dist/{chunk-D27MREPB.js.map → chunk-7DQUF2QR.js.map} +0 -0
  85. /package/dist/{chunk-UJEC7H6K.js.map → chunk-BHQ2SQ7P.js.map} +0 -0
  86. /package/dist/{chunk-C2HP7CRD.js.map → chunk-FII3PW2G.js.map} +0 -0
  87. /package/dist/{chunk-DTO27QFR.js.map → chunk-H5TIUKRK.js.map} +0 -0
  88. /package/dist/{chunk-73IVH3C6.js.map → chunk-O5EUTNR6.js.map} +0 -0
  89. /package/dist/{chunk-RKYKK6MN.js.map → chunk-RUMUFHSL.js.map} +0 -0
  90. /package/dist/{chunk-5MAQVBI2.js.map → chunk-UMPSR4US.js.map} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **◆ core** · Core
4
4
 
5
- > Twelve subsystems, two peer dependencies, five extension points.
5
+ > Fourteen subsystems, one peer dependency, six extension points.
6
6
 
7
7
  One, unscoped. Everything depends on it; it depends on nothing.
8
8
 
@@ -44,6 +44,8 @@ the seal on `internal`, and catches a subsystem added as a folder and forgotten
44
44
  - `src/locator/`
45
45
  - `src/gateway/`
46
46
  - `src/validation/`
47
+ - `src/cache/`
48
+ - `src/storage/`
47
49
  - `src/mock/`
48
50
  - `src/errors/`
49
51
  - `src/scenario/`
@@ -1,4 +1,4 @@
1
- import { I as ILankaValidator } from './lankaStandardValidator-CL-r-zEV.js';
1
+ import { I as ILankaValidator } from './lankaStandardValidator-BUFnysK0.js';
2
2
 
3
3
  /**
4
4
  * `RequestInit`, with the body a JSON API actually sends.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A failure with an ADDRESS: which input it belongs to, and what to say there.
3
+ *
4
+ * `issues` on `LankaError` flattens the address into the text — `items.1.qty:
5
+ * only 2 left` — which is right for a banner and useless for a form, which has
6
+ * a place per input and must find it. The path here stays in segments because
7
+ * the two form libraries this was designed against spell the same address
8
+ * differently (`items.1.qty` and `items[1].qty`) and neither can be parsed back
9
+ * out of a string safely: a message may contain a colon, a key may contain a
10
+ * dot.
11
+ *
12
+ * Produced by the validation port from a schema's issues, and by an
13
+ * application's request policy from a server body. Consumed by a ViewModel,
14
+ * which hands it to whatever holds the fields — its own state, or a form.
15
+ */
16
+ interface ILankaFieldError {
17
+ /**
18
+ * Segments, never a joined string: `["items", 1, "qty"]`.
19
+ *
20
+ * A segment is a name or an index and nothing else — a producer holding a
21
+ * symbol stringifies it, because no form can address one. EMPTY means the
22
+ * value as a whole: a cross-field refusal ("the dates are in the wrong
23
+ * order"), or a body that was not the expected shape at all. That is the
24
+ * form's ROOT, and an adapter routes it there rather than to an input named
25
+ * `""`.
26
+ */
27
+ readonly path: readonly (string | number)[];
28
+ /** What to show at that address when the application does not translate. */
29
+ readonly message: string;
30
+ /** The machine-readable reason, when the producer had one — what i18n keys on. */
31
+ readonly code?: string;
32
+ }
33
+
34
+ export type { ILankaFieldError as I };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * What a view binding needs of a ViewModel, and nothing else.
3
+ *
4
+ * The reading half: a name, the current state, a way to hear about the next one,
5
+ * and whether this ViewModel wants access tracking. Writing is deliberately
6
+ * absent — a binding renders, and a binding that could write would be a second
7
+ * place actions live.
8
+ *
9
+ * ## Why this is the port and not `StoreApi`
10
+ *
11
+ * All three ViewModel shapes answer it, and they are built out of different
12
+ * things: the stateful one over a store of its own, the shared-store one over a
13
+ * slice of somebody else's, the stateless one over a plain object with no store
14
+ * at all. A binding written against a store type would work for one of the three.
15
+ *
16
+ * It is also what keeps zustand inside core. `@lankajs/react`, `@lankajs/vue`
17
+ * and every later member of the shelf see this interface and never `StoreApi`,
18
+ * so the store underneath can change without a binding hearing about it — and if
19
+ * a binding ever needs something zustand-shaped, the abstraction leaked and the
20
+ * fix belongs here rather than there. Canon: `skills/hosts/SKILL.md` §1a.
21
+ *
22
+ * @see ILankaVM for the half that also writes, which `hydrateLankaVM` needs.
23
+ */
24
+ interface ILankaReadableVM<TState extends object> {
25
+ /**
26
+ * Names the ViewModel in logs, the scenario registry and the blind-spot
27
+ * warning — and in whatever a binding's devtools shows.
28
+ */
29
+ readonly name: string;
30
+ /**
31
+ * The current state, in full — and the SAME object until something changes.
32
+ *
33
+ * The identity is part of the contract, not an accident of the six factories
34
+ * that keep it. Every binding on the shelf holds something against it: the
35
+ * access tracker caches its recording proxy by the identity of the state it
36
+ * wrapped, and `@lankajs/react` holds a selector's answer the same way. An
37
+ * implementation that composes a fresh object on every call hands React a
38
+ * snapshot that never agrees with itself between the render read and the
39
+ * post-commit one, and the component renders until React stops it — a crash
40
+ * whose stack names React and not the ViewModel.
41
+ *
42
+ * It costs an implementer nothing to keep: answer a held object, and build a
43
+ * new one when you write. A ViewModel that composes its state from somewhere
44
+ * else memoises the composition against what it composed from —
45
+ * `createSharedStoreLankaVM` is the worked example.
46
+ */
47
+ getState(): TState;
48
+ /**
49
+ * Hears about every change, with both states in FULL shape.
50
+ *
51
+ * A shared-store ViewModel composes its full state from a store slice, and
52
+ * does so HERE rather than in the binding: a binding compares two states, it
53
+ * does not know how one is assembled.
54
+ *
55
+ * A stateless ViewModel has nothing that changes, so its implementation
56
+ * returns an unsubscribe and never calls the listener. That is not a gap —
57
+ * it is what makes the same binding work for all three without asking which
58
+ * it was handed.
59
+ */
60
+ subscribe(listener: (next: TState, prev: TState) => void): () => void;
61
+ /**
62
+ * Whether this ViewModel wants a reader to track which keys it read.
63
+ *
64
+ * `enableAccessTrackingOptimization`, as a binding sees it. A binding that
65
+ * ignores it is not merely slower: the blind spot documented on `ALankaVM`
66
+ * becomes a frozen screen, because the ViewModel turned tracking OFF for
67
+ * exactly the reason that it derives what the view shows.
68
+ */
69
+ readonly isAccessTracked: boolean;
70
+ }
71
+
72
+ export type { ILankaReadableVM as I };
@@ -1,4 +1,4 @@
1
- import { a as ILankaScenario } from './ILankaScenarioVM-DUsI-fSc.js';
1
+ import { a as ILankaScenario } from './ILankaScenarioVM-DpKFL3iE.js';
2
2
 
3
3
  /**
4
4
  * Metadata about a registered scenario
@@ -345,10 +345,11 @@ interface ILankaScenario<TData = void> {
345
345
  /**
346
346
  * A ViewModel that participates in scenarios.
347
347
  *
348
- * Not every ViewModel does: one with no `scenarioHandlers` never subscribes to
349
- * anything and is never registered. This is the pair of methods
350
- * `LankaScenarioBootstrap` calls on those that do, which is why the name says
351
- * scenario rather than ViewModel.
348
+ * Not every ViewModel does: one with no `scenarioHandlers` and neither
349
+ * lifecycle hook has nothing for bootstrap to do and is never registered. This
350
+ * is the pair of methods `LankaScenarioBootstrap` calls on those that are, which
351
+ * is why the name says scenario rather than ViewModel — `onInit` and `onReset`
352
+ * ride on the same two calls.
352
353
  *
353
354
  * Both are idempotent by contract: bootstrap may run after a ViewModel has
354
355
  * already initialised itself, and a reset may arrive for one that never did.
@@ -1,3 +1,5 @@
1
+ import { I as ILankaFieldError } from './ILankaFieldError-D5931-vT.js';
2
+
1
3
  /**
2
4
  * Structural API-error shape read by consumer code.
3
5
  *
@@ -42,6 +44,14 @@ interface ILankaErrorInit {
42
44
  code?: string;
43
45
  /** Details: server messages, or paths to the fields that failed validation. */
44
46
  issues?: readonly string[];
47
+ /**
48
+ * The failures that have an input to be shown at, with the path in segments.
49
+ *
50
+ * Beside `issues` rather than instead of it: `issues` is the banner's list and
51
+ * is promised; this is the form's, and a failure may have either, both or
52
+ * neither. Absent means "nothing here has an address", not "unknown".
53
+ */
54
+ fields?: readonly ILankaFieldError[];
45
55
  /**
46
56
  * The parsed ERROR response body, when there was one and it was JSON.
47
57
  *
@@ -72,6 +82,14 @@ declare class LankaError extends Error implements ILankaApiError {
72
82
  readonly status?: number;
73
83
  readonly code?: string;
74
84
  readonly issues?: readonly string[];
85
+ /**
86
+ * The failures with an input to be shown at. See `ILankaErrorInit.fields`.
87
+ *
88
+ * The array given is the array kept — unlike `errors`, which copies `issues`
89
+ * on every read. `readonly` carries the promise, and this is read on the way
90
+ * to a form, once, where a copy would buy nothing.
91
+ */
92
+ readonly fields?: readonly ILankaFieldError[];
75
93
  /** The parsed error response body; core carries it without reading it. */
76
94
  readonly body?: unknown;
77
95
  constructor(init: ILankaErrorInit);
@@ -1,7 +1,7 @@
1
- import { a as ILankaScenario, I as ILankaScenarioVM } from './ILankaScenarioVM-DUsI-fSc.js';
2
- import { I as ILankaScenarioMetadata } from './ILankaScenarioMetadata-GoWWNEQL.js';
3
- import { A as ALankaGateway } from './ALankaGateway-CkW1LbKE.js';
4
- import { A as ALankaLocator } from './LankaSharedStoreLocator-zS2kLu-S.js';
1
+ import { a as ILankaScenario, I as ILankaScenarioVM } from './ILankaScenarioVM-DpKFL3iE.js';
2
+ import { I as ILankaScenarioMetadata } from './ILankaScenarioMetadata-Dj4GCqmX.js';
3
+ import { A as ALankaGateway } from './ALankaGateway-BrVPaZN5.js';
4
+ import { A as ALankaLocator } from './LankaSharedStoreLocator-jsz_gnUm.js';
5
5
 
6
6
  /**
7
7
  * The scenario registry: both those that registered themselves and those
@@ -146,4 +146,4 @@ declare class LankaScenarioLocator extends ALankaLocator<ILankaScenario<unknown>
146
146
  constructor();
147
147
  }
148
148
 
149
- export { LankaGatewayLocator as L, LankaScenarioLocator as a, LankaScenarioVMRegistry as b, LankaScenariosRegistry as c };
149
+ export { LankaScenariosRegistry as L, LankaScenarioVMRegistry as a, LankaGatewayLocator as b, LankaScenarioLocator as c };
@@ -144,4 +144,4 @@ declare class LankaSharedStoreLocator extends ALankaLocator<ALankaSharedStore<TS
144
144
  constructor(config?: ILankaSharedStoreLocatorConfig);
145
145
  }
146
146
 
147
- export { ALankaLocator as A, type ILankaLocatorConfig as I, LankaSharedStoreLocator as L, type ILankaLocator as a, LankaSingletonLocator as b, type ILankaSharedStoreLocatorConfig as c, type ILankaSingletonLocatorConfig as d };
147
+ export { ALankaLocator as A, type ILankaLocatorConfig as I, LankaSingletonLocator as L, LankaSharedStoreLocator as a, type ILankaLocator as b, type ILankaSharedStoreLocatorConfig as c, type ILankaSingletonLocatorConfig as d };
@@ -1,13 +1,15 @@
1
- export { L as LankaGatewayLocator, a as LankaScenarioLocator, b as LankaScenarioVMRegistry, c as LankaScenariosRegistry } from '../LankaScenarioLocator-D86TIwiu.js';
2
- import { a as ILankaLocator } from '../LankaSharedStoreLocator-zS2kLu-S.js';
3
- export { A as ALankaLocator, L as LankaSharedStoreLocator, b as LankaSingletonLocator } from '../LankaSharedStoreLocator-zS2kLu-S.js';
4
- export { c as createLankaScope } from '../createLankaScope-BiFxNQgl.js';
1
+ export { b as LankaGatewayLocator, c as LankaScenarioLocator, a as LankaScenarioVMRegistry, L as LankaScenariosRegistry } from '../LankaScenarioLocator-Dki2Es3O.js';
2
+ import { b as ILankaLocator } from '../LankaSharedStoreLocator-jsz_gnUm.js';
3
+ export { A as ALankaLocator, a as LankaSharedStoreLocator, L as LankaSingletonLocator } from '../LankaSharedStoreLocator-jsz_gnUm.js';
4
+ export { c as createLankaScope } from '../createLankaScope-DomNbEvb.js';
5
5
  export { c as composeLankaRequestMiddleware } from '../lankaRequestMiddleware-DAC5kCb7.js';
6
- import '../ILankaScenarioVM-DUsI-fSc.js';
7
- import '../ILankaScenarioMetadata-GoWWNEQL.js';
8
- import '../ALankaGateway-CkW1LbKE.js';
9
- import '../lankaStandardValidator-CL-r-zEV.js';
6
+ import { I as ILankaReadableVM } from '../ILankaReadableVM-BoPzHEPV.js';
7
+ import '../ILankaScenarioVM-DpKFL3iE.js';
8
+ import '../ILankaScenarioMetadata-Dj4GCqmX.js';
9
+ import '../ALankaGateway-BrVPaZN5.js';
10
+ import '../lankaStandardValidator-BUFnysK0.js';
10
11
  import '@standard-schema/spec';
12
+ import '../ILankaFieldError-D5931-vT.js';
11
13
  import '../ALankaSharedStore-B7uepuuk.js';
12
14
  import 'zustand/vanilla';
13
15
 
@@ -31,4 +33,226 @@ interface ILankaLocatorProxyConfig<TInstance> {
31
33
  */
32
34
  declare function createLankaLocatorProxy<TInstance, TType extends Record<string, TInstance>>(config: ILankaLocatorProxyConfig<TInstance>): TType;
33
35
 
34
- export { type ILankaLocatorProxyConfig, createLankaLocatorProxy };
36
+ interface ILankaAccessTracker<TState extends object> {
37
+ /**
38
+ * The state as a recording Proxy: every key read off it is remembered.
39
+ *
40
+ * Cached by the IDENTITY of the state it wrapped, so a second read while
41
+ * nothing has changed hands back the same proxy — and therefore the same
42
+ * recorded keys — rather than starting the recording over.
43
+ *
44
+ * A ViewModel that turned tracking off gets the state itself, and every
45
+ * change then notifies. That is its decision, not a fallback: it turned
46
+ * tracking off because it DERIVES what the screen shows, and a recording that
47
+ * cannot see those reads would skip renders the screen needs.
48
+ */
49
+ read(): TState;
50
+ /**
51
+ * Whether a change touches anything this reader actually looked at.
52
+ *
53
+ * A reader that has looked at NOTHING yet is notified of everything: it has
54
+ * not had the chance to record a key, and staying silent would mean its first
55
+ * render never arrives.
56
+ */
57
+ shouldNotify(next: TState, prev: TState): boolean;
58
+ /**
59
+ * Says, in development, that a change was skipped — so the framework can warn
60
+ * if the screen reads the changed key through a getter.
61
+ *
62
+ * Called by a binding exactly when `shouldNotify` answered `false`. In
63
+ * production, and for a ViewModel with no trap, it does nothing.
64
+ */
65
+ reportSkipped(next: TState, prev: TState): void;
66
+ /**
67
+ * The state itself, never the proxy.
68
+ *
69
+ * For a render that happens once and is thrown away — a server snapshot. There
70
+ * is nothing to skip on a second render that will not happen, so recording
71
+ * reads would be work whose result nothing consults.
72
+ */
73
+ readPlain(): TState;
74
+ /** The keys read so far. Handed to the blind-spot diagnostic, which names them. */
75
+ readonly trackedKeys: ReadonlySet<string>;
76
+ }
77
+ /**
78
+ * Which state keys one reader looked at, and whether a change touched them.
79
+ *
80
+ * This is the whole of access tracking, and it is deliberately ignorant of how
81
+ * anybody subscribes. A component re-renders only for keys it READ off the proxy
82
+ * this returns; every framework asks that question the same way and answers it
83
+ * with a different mechanism — `useSyncExternalStore`, a `shallowRef`, a signal —
84
+ * so the question lives here and the mechanism lives in the binding.
85
+ *
86
+ * **This is the one piece of core a binding author needs.** It is published
87
+ * through `lanka/extend` for exactly that: a binding is then a subscription, a
88
+ * render trigger and these four calls, and the behaviour a consumer sees is the
89
+ * framework's rather than each binding's re-reading of it. Canon:
90
+ * `skills/parity/SKILL.md`.
91
+ *
92
+ * ## One tracker per reader, not per store
93
+ *
94
+ * The recorded keys are the property of whoever did the reading. Two components
95
+ * over one ViewModel read different keys and must re-render for different
96
+ * changes, so each holds its own tracker — which is also why this is a factory
97
+ * with closed-over state rather than a set of pure functions over a shared map:
98
+ * the lifetime of the recording is exactly the lifetime of the reader.
99
+ *
100
+ * ## The blind spot this cannot see, and reports instead
101
+ *
102
+ * Tracking sees reads made DIRECTLY off the proxy. A key reached only inside a
103
+ * derived getter — an action calling `get()` — is invisible here, so a change to
104
+ * it answers `shouldNotify` with `false` and the screen does not move. That is
105
+ * what `reportSkipped` is for: core kept a trap for this ViewModel, and in
106
+ * development it names the ViewModel and the key rather than leaving a frozen
107
+ * screen with no error anywhere.
108
+ */
109
+ declare const createLankaAccessTracker: <TState extends object>(viewModel: ILankaReadableVM<TState>) => ILankaAccessTracker<TState>;
110
+
111
+ /** One reader's live view of a ViewModel. */
112
+ interface ILankaViewSubscription<TState extends object> {
113
+ /**
114
+ * The current state, RECORDED.
115
+ *
116
+ * Every key read off it is remembered, which is what lets the next change be
117
+ * skipped when it touched none of them. Call it again on every read: it asks
118
+ * the ViewModel for its state each time, so it is never a snapshot.
119
+ */
120
+ read: () => TState;
121
+ /** Releases the subscription. */
122
+ stop: () => void;
123
+ }
124
+ /**
125
+ * The TRACKED half of a view binding, minus the framework.
126
+ *
127
+ * The tracked half, and not the whole: all five shipped members also publish a
128
+ * selector arm, which this deliberately does not serve — the reason is below,
129
+ * under "Why it does not take a selector".
130
+ *
131
+ * ```ts
132
+ * // a binding for a framework this repository has never heard of
133
+ * export const useMyFrameworkVM = (viewModel) => {
134
+ * const view = createLankaViewSubscription(viewModel, () => invalidate());
135
+ * onTeardown(view.stop);
136
+ *
137
+ * return view.read;
138
+ * };
139
+ * ```
140
+ *
141
+ * Subscribe, ask whether the change touched anything this reader looked at,
142
+ * report the skip so the blind-spot diagnostic can fire, and hand back a read
143
+ * that records. Five packages wrote those four steps out by hand, identically —
144
+ * and five copies of a decision diverge on the day one of them gains a line.
145
+ *
146
+ * ## What a caller still owns
147
+ *
148
+ * `onChange` and the teardown, which are the only framework-shaped things left.
149
+ * That is the seam: a binding says how its framework is WOKEN and when a reader
150
+ * has gone, and everything about which changes are worth waking for is here.
151
+ *
152
+ * ## Why it does not take a selector
153
+ *
154
+ * A selector BYPASSES tracking — the selector decides, and there is nothing to
155
+ * record — so a subscription that took one would be two mechanisms behind one
156
+ * name, each right half the time. A binding with a selector arm calls
157
+ * `createLankaAccessTracker` directly, which is what the shelf's members do, and
158
+ * the four steps are worth writing out where they genuinely differ.
159
+ *
160
+ * ## What it is not
161
+ *
162
+ * Not a store, not a cache, not a second place state lives. It holds a tracker
163
+ * and an unsubscribe, and everything it answers comes from the ViewModel on the
164
+ * call.
165
+ */
166
+ declare const createLankaViewSubscription: <TState extends object>(viewModel: ILankaReadableVM<TState>, onChange: () => void) => ILankaViewSubscription<TState>;
167
+
168
+ /**
169
+ * The brand that makes a definition impossible to write by hand.
170
+ *
171
+ * Not exported, and that is the whole mechanism. A structural interface with
172
+ * `name` and `build` on it would let a consumer author one, and the moment one
173
+ * does, `ILankaVMDefinition` stops being a type the framework implements and
174
+ * becomes a PORT — after which every member added to it is a compile error in
175
+ * code nobody touched. Publishing an opaque type costs one line and keeps the
176
+ * option space: a `dispose`, a `scope` field, even a call signature so that
177
+ * `missionsVM()` could mean `resolveLankaVM(missionsVM)`, all stay additive.
178
+ */
179
+ declare const LANKA_VM_DEFINITION: unique symbol;
180
+ /**
181
+ * A ViewModel that has not been built yet.
182
+ *
183
+ * Hold it, pass it, type against it. There is nothing on it to call, because
184
+ * calling it is `resolveLankaVM`'s job and a definition built by hand would be
185
+ * an instance outside every scope's map.
186
+ */
187
+ interface ILankaVMDefinition<TViewModel extends ILankaReadableVM<object>> {
188
+ readonly [LANKA_VM_DEFINITION]: TViewModel;
189
+ }
190
+ /**
191
+ * Declares a ViewModel WITHOUT building it.
192
+ *
193
+ * ## When NOT to use this
194
+ *
195
+ * A browser-only application needs none of it. One module is one instance per
196
+ * TAB there, so a module-level `createLankaVM` is still the shape, and wrapping
197
+ * the nine factories in this one buys nothing but a lookup per read. Reach for
198
+ * it when the same ViewModel has to exist on a SERVER, where one module is one
199
+ * instance per PROCESS — shared by every user connected to it, so the first
200
+ * request to write a draft into it serves that draft to the next stranger.
201
+ *
202
+ * ```ts
203
+ * export const missionsVM = defineLankaVM({
204
+ * name: "MissionsVM",
205
+ * build: () => createLankaVM({ … }),
206
+ * });
207
+ * ```
208
+ *
209
+ * ## What it is not
210
+ *
211
+ * Not a tenth way to write a ViewModel. `build` returns whatever the nine
212
+ * existing factories return and nothing here reaches inside it — a definition
213
+ * adds a lifetime and takes nothing away.
214
+ *
215
+ * Not lazy in the sense `createLazyLankaVM` is. That one defers the STORE until
216
+ * first read and still has one per module; this defers WHICH INSTANCE, and the
217
+ * two compose.
218
+ */
219
+ declare const defineLankaVM: <TViewModel extends ILankaReadableVM<object>>(config: {
220
+ name: string;
221
+ build: () => TViewModel;
222
+ }) => ILankaVMDefinition<TViewModel>;
223
+
224
+ /**
225
+ * The instance of a definition that belongs to the CURRENT scope.
226
+ *
227
+ * Called twice in one scope it answers the same instance; called in two scopes
228
+ * it answers two, and neither can see the other's state. In a browser there is
229
+ * one scope for the life of the tab, so this is the module-level ViewModel a
230
+ * consumer already knows — written once and correct on a server as well.
231
+ *
232
+ * ## It throws outside a scope, and that is the feature
233
+ *
234
+ * On a server the answer comes from the SCOPE seam rather than from the active
235
+ * runtime, and the difference is not academic. `runInLankaServerScope` creates
236
+ * its instance inside the scope and `createLanka` activates every instance it
237
+ * builds, so during a request the process pointer and the scope's runtime are
238
+ * the same object — keying on the runtime would make "inside a request" and
239
+ * "after one ended" indistinguishable, and a call made after would be handed the
240
+ * last stranger's ViewModel.
241
+ *
242
+ * So: a scope resolver installed and answering `null` means this ran outside
243
+ * every request, and that fails loudly. No fallback, for the same reason
244
+ * `requireActiveRuntime` has none — a wrong answer here is one user's data in
245
+ * another user's page, and it would surface three layers from the call.
246
+ *
247
+ * ## Why the build is wrapped
248
+ *
249
+ * `ALankaVM.build()` declares a ViewModel that has scenario handlers into a
250
+ * PROCESS-wide list, so that every instance ever created adopts it. That is
251
+ * right for a module-level ViewModel and catastrophic for a scoped one: the list
252
+ * would grow per request forever, and request N+1 would adopt request N's
253
+ * ViewModel and run N's handlers against N's gateways. `buildScoped` is the seam
254
+ * that keeps a scoped declaration out of it.
255
+ */
256
+ declare const resolveLankaVM: <TViewModel extends ILankaReadableVM<object>>(definition: ILankaVMDefinition<TViewModel>) => TViewModel;
257
+
258
+ export { type ILankaAccessTracker, type ILankaLocatorProxyConfig, type ILankaVMDefinition, type ILankaViewSubscription, createLankaAccessTracker, createLankaLocatorProxy, createLankaViewSubscription, defineLankaVM, resolveLankaVM };
@@ -1,26 +1,151 @@
1
+ import {
2
+ lankaBlindSpotRegistry
3
+ } from "../chunk-UDP6IXDS.js";
1
4
  import {
2
5
  LankaGatewayLocator,
3
6
  LankaScenarioLocator,
4
7
  createLankaScope
5
- } from "../chunk-5MAQVBI2.js";
8
+ } from "../chunk-UMPSR4US.js";
6
9
  import {
7
10
  LankaScenarioVMRegistry,
8
- LankaScenariosRegistry
9
- } from "../chunk-O5ROO7QF.js";
11
+ LankaScenariosRegistry,
12
+ lankaScenarioBootstrap
13
+ } from "../chunk-EYDMBI5T.js";
10
14
  import {
11
15
  composeLankaRequestMiddleware
12
16
  } from "../chunk-YR4MZXMU.js";
13
17
  import {
14
18
  createLankaLocatorProxy
15
- } from "../chunk-DTO27QFR.js";
19
+ } from "../chunk-H5TIUKRK.js";
16
20
  import {
17
21
  ALankaLocator,
18
22
  LankaSharedStoreLocator,
19
23
  LankaSingletonLocator
20
24
  } from "../chunk-UE2C76OR.js";
21
- import "../chunk-C2HP7CRD.js";
22
- import "../chunk-D27MREPB.js";
23
- import "../chunk-BGVDPDX4.js";
25
+ import "../chunk-FII3PW2G.js";
26
+ import "../chunk-7DQUF2QR.js";
27
+ import {
28
+ getActiveLankaScope,
29
+ hasLankaScopeResolver,
30
+ requireActiveRuntime
31
+ } from "../chunk-XGMXT4XZ.js";
32
+
33
+ // src/viewmodel/_internal/create-lanka-access-tracker/createLankaAccessTracker.ts
34
+ var asRecord = (state) => state;
35
+ var recordReadsInto = (state, keys) => new Proxy(state, {
36
+ get(target, prop, receiver) {
37
+ const value = Reflect.get(target, prop, receiver);
38
+ if (typeof prop === "string" && typeof value !== "function" && Object.hasOwn(target, prop)) {
39
+ keys.add(prop);
40
+ }
41
+ return value;
42
+ }
43
+ });
44
+ var trackedReadOf = (read) => {
45
+ let keys = /* @__PURE__ */ new Set();
46
+ let state = null;
47
+ let proxy = null;
48
+ return {
49
+ keys: () => keys,
50
+ current: () => {
51
+ const next = read();
52
+ if (state === next && proxy) return proxy;
53
+ keys = /* @__PURE__ */ new Set();
54
+ state = next;
55
+ proxy = recordReadsInto(next, keys);
56
+ return proxy;
57
+ }
58
+ };
59
+ };
60
+ var anyKeyMoved = (keys, next, prev) => {
61
+ const nextRecord = asRecord(next);
62
+ const prevRecord = asRecord(prev);
63
+ for (const key of keys) {
64
+ if (!Object.is(nextRecord[key], prevRecord[key])) return true;
65
+ }
66
+ return false;
67
+ };
68
+ var createLankaAccessTracker = (viewModel) => {
69
+ const trap = lankaBlindSpotRegistry.of(viewModel);
70
+ const isTracked = viewModel.isAccessTracked;
71
+ const tracked = trackedReadOf(() => viewModel.getState());
72
+ return {
73
+ get trackedKeys() {
74
+ return isTracked ? tracked.keys() : /* @__PURE__ */ new Set();
75
+ },
76
+ read() {
77
+ return isTracked ? tracked.current() : viewModel.getState();
78
+ },
79
+ shouldNotify(next, prev) {
80
+ if (!isTracked) return true;
81
+ const keys = tracked.keys();
82
+ return keys.size === 0 || anyKeyMoved(keys, next, prev);
83
+ },
84
+ reportSkipped(next, prev) {
85
+ trap?.report(new Set(tracked.keys()), asRecord(next), asRecord(prev));
86
+ },
87
+ readPlain() {
88
+ return viewModel.getState();
89
+ }
90
+ };
91
+ };
92
+
93
+ // src/viewmodel/_factories/create-lanka-view-subscription/createLankaViewSubscription.ts
94
+ var createLankaViewSubscription = (viewModel, onChange) => {
95
+ const tracker = createLankaAccessTracker(viewModel);
96
+ const stop = viewModel.subscribe((next, prev) => {
97
+ if (!tracker.shouldNotify(next, prev)) {
98
+ tracker.reportSkipped(next, prev);
99
+ return;
100
+ }
101
+ onChange();
102
+ });
103
+ return { read: () => tracker.read(), stop };
104
+ };
105
+
106
+ // src/viewmodel/_internal/lanka-vm-recipes/lankaVMRecipes.ts
107
+ var lankaVMRecipes = {
108
+ recipes: /* @__PURE__ */ new WeakMap(),
109
+ instances: /* @__PURE__ */ new WeakMap(),
110
+ /** The instances belonging to one scope, created on first use. */
111
+ forScope(scope) {
112
+ const existing = this.instances.get(scope);
113
+ if (existing) return existing;
114
+ const created = /* @__PURE__ */ new WeakMap();
115
+ this.instances.set(scope, created);
116
+ return created;
117
+ }
118
+ };
119
+
120
+ // src/viewmodel/_factories/define-lanka-vm/defineLankaVM.ts
121
+ var defineLankaVM = (config) => {
122
+ const definition = Object.freeze({});
123
+ lankaVMRecipes.recipes.set(definition, config);
124
+ return definition;
125
+ };
126
+
127
+ // src/viewmodel/_factories/resolve-lanka-vm/resolveLankaVM.ts
128
+ var resolveLankaVM = (definition) => {
129
+ const recipe = lankaVMRecipes.recipes.get(definition);
130
+ if (!recipe) {
131
+ throw new Error(
132
+ "resolveLankaVM was given something defineLankaVM did not make. A definition is opaque on purpose: build it with defineLankaVM rather than by hand."
133
+ );
134
+ }
135
+ const runtime = requireActiveRuntime();
136
+ const scope = getActiveLankaScope();
137
+ if (hasLankaScopeResolver() && !scope) {
138
+ throw new Error(
139
+ `resolveLankaVM("${recipe.name}") ran outside every scope. A scope resolver is installed, which on a server means this code ran outside a request \u2014 and a ViewModel resolved there would be the previous request's. Do this work inside the scope, or start one.`
140
+ );
141
+ }
142
+ const instances = lankaVMRecipes.forScope(scope ?? runtime);
143
+ const held = instances.get(definition);
144
+ if (held) return held;
145
+ const built = lankaScenarioBootstrap.buildScoped(() => recipe.build());
146
+ instances.set(definition, built);
147
+ return built;
148
+ };
24
149
  export {
25
150
  ALankaLocator,
26
151
  LankaGatewayLocator,
@@ -30,7 +155,11 @@ export {
30
155
  LankaSharedStoreLocator,
31
156
  LankaSingletonLocator,
32
157
  composeLankaRequestMiddleware,
158
+ createLankaAccessTracker,
33
159
  createLankaLocatorProxy,
34
- createLankaScope
160
+ createLankaScope,
161
+ createLankaViewSubscription,
162
+ defineLankaVM,
163
+ resolveLankaVM
35
164
  };
36
165
  //# sourceMappingURL=index.js.map