@xmachines/play-actor 2.0.0-alpha.1 → 2.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.
@@ -1,47 +1,52 @@
1
1
  /**
2
- * AbstractActor base class for Play Architecture
2
+ * The AbstractActor base class of the Play Architecture
3
3
  *
4
- * Extends XState Actor to maintain ecosystem compatibility (inspection, devtools)
5
- * while enforcing signal protocol for Actor Infrastructure communication.
4
+ * It extends the XState Actor class, which keeps the compatibility with the
5
+ * ecosystem, such as the inspection and the devtools. It also enforces the signal
6
+ * protocol of the communication between the Actor and the infrastructure.
6
7
  *
7
- * Per RFC section 5.3, the Actor exposes a minimal protocol:
8
- * - state: Current machine state snapshot
9
- * - send: Event dispatch method
8
+ * RFC section 5.3 gives the minimal protocol of the Actor:
9
+ * - state: the snapshot of the current machine state
10
+ * - send: the method that sends an event
10
11
  *
11
- * Optional capabilities are provided via interfaces:
12
- * - Routable: For actors that support routing
13
- * - Viewable: For actors that support view rendering
12
+ * Two interfaces give the optional capabilities:
13
+ * - Routable: for an actor with a routing support
14
+ * - Viewable: for an actor with a view rendering
14
15
  *
15
- * Concrete implementations are created by adapters (e.g., @xmachines/play-xstate).
16
+ * An adapter, such as @xmachines/play-xstate, makes the concrete implementations.
16
17
  *
17
18
  * @packageDocumentation
18
19
  */
19
20
  import { Actor } from "xstate";
20
21
  /**
21
- * Identity helper that constrains a `PlaySpec` object's `contextProps` to keys
22
- * of a specific machine context type, giving compile-time validation and IDE
23
- * autocomplete at the definition site.
22
+ * The identity helper gives a view spec literal the type `PlaySpec` at the
23
+ * definition site. The compiler therefore checks the spec, and the IDE completes it.
24
24
  *
25
- * XState's `meta` field is typed as `Record<string, unknown>`, so TypeScript
26
- * cannot infer the constraint from context. `typedSpec<MyCtx>(...)` is the
27
- * opt-in mechanism that activates enforcement where the spec is written.
25
+ * The XState `meta` field has the type `Record<string, unknown>`. TypeScript
26
+ * therefore infers no spec shape from the context. `typedSpec(...)` is the
27
+ * mechanism that starts the check where you write the spec. The parameter holds no
28
+ * `viewKey`: the derivation stamps that field, and it overwrites a value from an
29
+ * author without a notice. Therefore the compiler refuses a `viewKey` here.
28
30
  *
29
- * At runtime this is a no-op the spec object is returned unchanged.
31
+ * The check of an excess property works on an inline object literal only. For a
32
+ * spec in a variable, or for a spec from a spread, write `satisfies PlaySpec` at
33
+ * the literal instead.
34
+ *
35
+ * At run time this function does nothing: it returns the spec object without a
36
+ * change.
30
37
  *
31
38
  * @example
32
39
  * ```ts
33
- * interface DashboardCtx {
34
- * username: string;
35
- * params: Record<string, string>;
36
- * query: Record<string, string>;
37
- * }
38
- *
39
40
  * meta: {
40
- * view: typedSpec<DashboardCtx>({
41
+ * view: typedSpec({
41
42
  * root: "root",
42
- * contextProps: ["username"], // ✓ key of DashboardCtx
43
- * // contextProps: ["usernaem"], // ✗ compile error
44
- * elements: { root: { type: "Dashboard", props: {}, children: [] } },
43
+ * elements: {
44
+ * root: {
45
+ * type: "Dashboard",
46
+ * props: { username: { $state: "/context/username" } },
47
+ * children: [],
48
+ * },
49
+ * },
45
50
  * }),
46
51
  * }
47
52
  * ```
@@ -50,14 +55,21 @@ export function typedSpec(spec) {
50
55
  return spec;
51
56
  }
52
57
  /**
53
- * Abstract base class for Play Architecture actors.
58
+ * The abstract base class of an actor of the Play Architecture.
59
+ *
60
+ * It observes the state through the signals, and it works with the tools of the
61
+ * XState ecosystem, such as the devtools and the inspection. It also exposes the
62
+ * reactive signals of the communication with the infrastructure layer.
54
63
  *
55
- * Provides signal-driven state observation that integrates with XState ecosystem
56
- * tooling (devtools, inspection) while exposing reactive signals for
57
- * Infrastructure layer communication.
64
+ * **A subclass IS the actor.** Give the logic *and* its options to
65
+ * `super(logic, options)`, then observe `this`. A separate actor beside this
66
+ * instance leaves this instance as an empty second actor. Every inherited member
67
+ * then answers from that empty actor, until you forward each one: `system`,
68
+ * `sessionId`, `clock`, the internal `_send` that receives the traffic of
69
+ * `sendTo()`, and each member that a later XState version adds.
58
70
  *
59
- * @typeParam TLogic - XState actor logic type
60
- * @typeParam TEvent - Event type constraint (defaults to EventObject)
71
+ * @typeParam TLogic - The type of the XState actor logic
72
+ * @typeParam TEvent - The constraint of the event type. The default is EventObject
61
73
  */
62
74
  export class AbstractActor extends Actor {
63
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"abstract-actor.js","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAwC,MAAM,QAAQ,CAAC;AAuCrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,SAAS,CACxB,IAEC;IAED,OAAO,IAAgB,CAAC;AACzB,CAAC;AAiFD;;;;;;;;;GASG;AACH,MAAM,OAAgB,aAGpB,SAAQ,KAAa;CAetB"}
1
+ {"version":3,"file":"abstract-actor.js","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,KAAK,EAAwC,MAAM,QAAQ,CAAC;AAqCrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,SAAS,CAAC,IAA+B;IACxD,OAAO,IAAI,CAAC;AACb,CAAC;AAuFD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAgB,aAGpB,SAAQ,KAAa;CAwBtB"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The context projection — the machine context as the read-only `/context`
3
+ * subtree of the view state store.
4
+ *
5
+ * `deriveCurrentView` composes the `state` field of each emitted view as
6
+ * `{ ...meta.view.state, context: <slice> }`. The spec is therefore consistent with
7
+ * itself: its `state` field describes the contents of the store correctly, and
8
+ * `repeat.statePath`, `visible.$state`, a `$state` prop, and a validator all read
9
+ * the machine context through the ordinary `{ $state: "/context/…" }` grammar.
10
+ *
11
+ * The subtree is read-only **to the spec, and not to the machinery**: a provider
12
+ * gives the bindings and the action handlers a store with the wrapper of
13
+ * {@link guardContextWrites}, and it refreshes the projection through the store
14
+ * below that wrapper, which has no guard. The context changes through a machine
15
+ * event only. Therefore a write under `/context` is always a fault of the spec, and
16
+ * the guard says so.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ import type { StateStore } from "@xmachines/json-render-core";
21
+ import type { PlaySpec } from "./abstract-actor.js";
22
+ /**
23
+ * The reserved top-level state key of the projection.
24
+ *
25
+ * The `spec.state` object of a view must not declare this key. When it does, the
26
+ * derivation skips the projection for that view, the authored state wins, and the
27
+ * code writes a warning in development. See {@link composePlayState}.
28
+ */
29
+ export declare const CONTEXT_STATE_KEY = "context";
30
+ /**
31
+ * The shallow equality of the own keys, with `Object.is`. It can ignore one key on
32
+ * both sides.
33
+ *
34
+ * This is the one comparison rule behind every decision of the emission dedup: the
35
+ * equality of a slice, and also the emit gate of the player, which compares the
36
+ * spec fields in `viewSpecsEquivalent` and reuses the composed state in
37
+ * {@link reuseComposedState}.
38
+ */
39
+ export declare function shallowEqualExcept(a: object, b: object, except?: string): boolean;
40
+ /**
41
+ * Reuses the composed `state` of the previous emission, or the complete previous
42
+ * spec, when the value of the projection did not change.
43
+ *
44
+ * `deriveCurrentView` composes `state: { ...meta.view.state, context: slice }` new
45
+ * on each call, and the XState function `assign` makes a new context object on
46
+ * every event. Without this reuse, every event therefore presents a new `state`
47
+ * reference, and the `Object.is` test of each field in the emit gate emits the view
48
+ * again and remounts it, without an end. The function compares the slices field by
49
+ * field ({@link shallowEqualExcept}), because the XState function `assign` makes a
50
+ * new context object for each event, and a test of the identity of the whole object
51
+ * therefore reports a change every time. The authored fields come from the static
52
+ * `meta.view.state` through a spread. Therefore their references are stable for an
53
+ * unchanged `viewKey`, and a plain `Object.is` test is correct for them.
54
+ *
55
+ * The function returns one of three values, in this order of preference:
56
+ * - `prev` itself, when nothing observable changed. The emit gate then stops at the
57
+ * identity of the reference, and it walks no element;
58
+ * - `{ ...next, state: prev.state }`, when the value of the state did not change
59
+ * but another top-level field is different;
60
+ * - `next`, when something changed.
61
+ */
62
+ export declare function reuseComposedState(prev: PlaySpec | null, next: PlaySpec | null): PlaySpec | null;
63
+ /**
64
+ * Wraps a StateStore, and the wrapper refuses each write under `/context`.
65
+ *
66
+ * A provider applies it to the store that it gives to the bindings and to the
67
+ * action handlers (`$bindState`, `setState`, and a chained `set`). The provider
68
+ * keeps the store without the wrapper, and it refreshes the projection through that
69
+ * store. The subtree is therefore read-only to the spec, and not to the machinery.
70
+ *
71
+ * A write with a value that is **identical** to the current value passes in
72
+ * silence. A handler in the style of `setState` reads the complete snapshot,
73
+ * changes it, and writes the whole object back. The `context` key that goes through
74
+ * that round trip without a change is no attempt of a mutation. Only a write that
75
+ * changes the subtree throws.
76
+ *
77
+ * Each read passes through without a change. The wrapper delegates every member
78
+ * explicitly, and it does not use a spread: a store from a consumer can be an
79
+ * instance of a class, and the methods of that instance are on the prototype. Those
80
+ * methods do not survive `{ ...store }`, and the first render then fails with
81
+ * `store.getSnapshot is not a function`. The code builds the wrapper one time for
82
+ * each resolved store. Therefore the identity of the delegating `getSnapshot` and
83
+ * of the delegating `subscribe` stays stable for a consumer in the style of
84
+ * `useSyncExternalStore`.
85
+ *
86
+ * @param store - The store below the wrapper.
87
+ * @returns A store with a guard on `set` and on `update`.
88
+ */
89
+ export declare function guardContextWrites(store: StateStore): StateStore;
90
+ /**
91
+ * Refreshes the `/context` subtree of a live store from the composed state of a
92
+ * derived view. An emission with the same `viewKey` means that only the projection
93
+ * changed. The function therefore replaces the complete subtree, and it never
94
+ * merges it field by field, because `update` cannot delete a key. Every ephemeral
95
+ * value at the root level stays. The function does nothing when the view carries no
96
+ * slice, or when the store holds the slice already.
97
+ *
98
+ * @param store - The store WITHOUT the guard. A provider refreshes through it.
99
+ * @param view - The derived view. Its `state.context` field carries the slice.
100
+ */
101
+ export declare function refreshContextSubtree(store: StateStore, view: {
102
+ state?: unknown;
103
+ }): void;
104
+ /**
105
+ * Composes the effective state of a view: the authored `spec.state` and the
106
+ * `/context` slice.
107
+ *
108
+ * When the authored state declares the reserved key already, the function skips the
109
+ * projection, the authored state wins, and the code writes a warning in
110
+ * development. No spec that exists now therefore fails.
111
+ *
112
+ * @param authoredState - The raw `meta.view.state` value. It can be anything, and the caller or toAtomState cleans it.
113
+ * @param slice - The context of the machine, as one complete value.
114
+ */
115
+ export declare function composePlayState(authoredState: Record<string, unknown> | undefined, slice: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
116
+ //# sourceMappingURL=context-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-projection.d.ts","sourceRoot":"","sources":["../src/context-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAiB3C;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAUjF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,CAiBhG;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CA8ChE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAKxF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC/B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAClD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACxC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAcrC"}
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The context projection — the machine context as the read-only `/context`
3
+ * subtree of the view state store.
4
+ *
5
+ * `deriveCurrentView` composes the `state` field of each emitted view as
6
+ * `{ ...meta.view.state, context: <slice> }`. The spec is therefore consistent with
7
+ * itself: its `state` field describes the contents of the store correctly, and
8
+ * `repeat.statePath`, `visible.$state`, a `$state` prop, and a validator all read
9
+ * the machine context through the ordinary `{ $state: "/context/…" }` grammar.
10
+ *
11
+ * The subtree is read-only **to the spec, and not to the machinery**: a provider
12
+ * gives the bindings and the action handlers a store with the wrapper of
13
+ * {@link guardContextWrites}, and it refreshes the projection through the store
14
+ * below that wrapper, which has no guard. The context changes through a machine
15
+ * event only. Therefore a write under `/context` is always a fault of the spec, and
16
+ * the guard says so.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ /**
21
+ * The reserved top-level state key of the projection.
22
+ *
23
+ * The `spec.state` object of a view must not declare this key. When it does, the
24
+ * derivation skips the projection for that view, the authored state wins, and the
25
+ * code writes a warning in development. See {@link composePlayState}.
26
+ */
27
+ export const CONTEXT_STATE_KEY = "context";
28
+ /**
29
+ * The record that reports each diagnostic one time for each spec. The condition
30
+ * here is a static property of the authored meta.view, because its `state` object
31
+ * never changes between two events. `deriveCurrentView` runs on every machine
32
+ * snapshot, and a report for each derivation therefore floods the console at the
33
+ * rate of the events. The key is the identity of the static object. Each spec
34
+ * therefore reports one time, and a test with a new literal stays separate.
35
+ */
36
+ const reportedDiagnostics = new WeakSet();
37
+ function warnOnce(anchor, message) {
38
+ if (reportedDiagnostics.has(anchor))
39
+ return;
40
+ reportedDiagnostics.add(anchor);
41
+ console.warn(message);
42
+ }
43
+ /**
44
+ * The shallow equality of the own keys, with `Object.is`. It can ignore one key on
45
+ * both sides.
46
+ *
47
+ * This is the one comparison rule behind every decision of the emission dedup: the
48
+ * equality of a slice, and also the emit gate of the player, which compares the
49
+ * spec fields in `viewSpecsEquivalent` and reuses the composed state in
50
+ * {@link reuseComposedState}.
51
+ */
52
+ export function shallowEqualExcept(a, b, except) {
53
+ const aEntries = Object.entries(a).filter(([key]) => key !== except);
54
+ const bKeyCount = Object.keys(b).filter((key) => key !== except).length;
55
+ if (aEntries.length !== bKeyCount)
56
+ return false;
57
+ const bRecord = b;
58
+ for (const [key, value] of aEntries) {
59
+ // nosemgrep: gitlab.eslint.detect-object-injection
60
+ if (!Object.hasOwn(b, key) || !Object.is(value, bRecord[key]))
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ /**
66
+ * Reuses the composed `state` of the previous emission, or the complete previous
67
+ * spec, when the value of the projection did not change.
68
+ *
69
+ * `deriveCurrentView` composes `state: { ...meta.view.state, context: slice }` new
70
+ * on each call, and the XState function `assign` makes a new context object on
71
+ * every event. Without this reuse, every event therefore presents a new `state`
72
+ * reference, and the `Object.is` test of each field in the emit gate emits the view
73
+ * again and remounts it, without an end. The function compares the slices field by
74
+ * field ({@link shallowEqualExcept}), because the XState function `assign` makes a
75
+ * new context object for each event, and a test of the identity of the whole object
76
+ * therefore reports a change every time. The authored fields come from the static
77
+ * `meta.view.state` through a spread. Therefore their references are stable for an
78
+ * unchanged `viewKey`, and a plain `Object.is` test is correct for them.
79
+ *
80
+ * The function returns one of three values, in this order of preference:
81
+ * - `prev` itself, when nothing observable changed. The emit gate then stops at the
82
+ * identity of the reference, and it walks no element;
83
+ * - `{ ...next, state: prev.state }`, when the value of the state did not change
84
+ * but another top-level field is different;
85
+ * - `next`, when something changed.
86
+ */
87
+ export function reuseComposedState(prev, next) {
88
+ if (!prev || !next || prev.viewKey !== next.viewKey)
89
+ return next;
90
+ const prevState = prev.state;
91
+ const nextState = next.state;
92
+ if (prevState !== nextState) {
93
+ if (prevState === undefined || nextState === undefined)
94
+ return next;
95
+ if (!shallowEqualExcept(prevState, nextState, CONTEXT_STATE_KEY))
96
+ return next;
97
+ const prevSlice = prevState[CONTEXT_STATE_KEY];
98
+ const nextSlice = nextState[CONTEXT_STATE_KEY];
99
+ if (prevSlice !== nextSlice) {
100
+ if (prevSlice === undefined || nextSlice === undefined)
101
+ return next;
102
+ if (!shallowEqualExcept(prevSlice, nextSlice))
103
+ return next;
104
+ }
105
+ if (shallowEqualExcept(prev, next, "state"))
106
+ return prev;
107
+ return { ...next, state: prevState };
108
+ }
109
+ return shallowEqualExcept(prev, next, "state") ? prev : next;
110
+ }
111
+ /**
112
+ * The paths of the reserved subtree: `"/context"` itself, and every path below it.
113
+ * An update map of the store can hold a bare top-level key ("context") and also a
114
+ * JSON Pointer ("/context/…"). Both forms address the same subtree.
115
+ */
116
+ function toPointer(path) {
117
+ return path.startsWith("/") ? path : `/${path}`;
118
+ }
119
+ /** This function expects a NORMALIZED pointer from {@link toPointer}. */
120
+ function isContextPointer(pointer) {
121
+ return pointer === `/${CONTEXT_STATE_KEY}` || pointer.startsWith(`/${CONTEXT_STATE_KEY}/`);
122
+ }
123
+ /**
124
+ * Wraps a StateStore, and the wrapper refuses each write under `/context`.
125
+ *
126
+ * A provider applies it to the store that it gives to the bindings and to the
127
+ * action handlers (`$bindState`, `setState`, and a chained `set`). The provider
128
+ * keeps the store without the wrapper, and it refreshes the projection through that
129
+ * store. The subtree is therefore read-only to the spec, and not to the machinery.
130
+ *
131
+ * A write with a value that is **identical** to the current value passes in
132
+ * silence. A handler in the style of `setState` reads the complete snapshot,
133
+ * changes it, and writes the whole object back. The `context` key that goes through
134
+ * that round trip without a change is no attempt of a mutation. Only a write that
135
+ * changes the subtree throws.
136
+ *
137
+ * Each read passes through without a change. The wrapper delegates every member
138
+ * explicitly, and it does not use a spread: a store from a consumer can be an
139
+ * instance of a class, and the methods of that instance are on the prototype. Those
140
+ * methods do not survive `{ ...store }`, and the first render then fails with
141
+ * `store.getSnapshot is not a function`. The code builds the wrapper one time for
142
+ * each resolved store. Therefore the identity of the delegating `getSnapshot` and
143
+ * of the delegating `subscribe` stays stable for a consumer in the style of
144
+ * `useSyncExternalStore`.
145
+ *
146
+ * @param store - The store below the wrapper.
147
+ * @returns A store with a guard on `set` and on `update`.
148
+ */
149
+ export function guardContextWrites(store) {
150
+ const reject = (path) => {
151
+ throw new Error(`[play-actor] "${path}" is read-only: /${CONTEXT_STATE_KEY} mirrors the machine's context. ` +
152
+ `Send the actor an event to change it — context never changes through the view store.`);
153
+ };
154
+ const checkWrite = (path, value) => {
155
+ const pointer = toPointer(path);
156
+ if (!isContextPointer(pointer))
157
+ return true;
158
+ // A round trip without a change, which is a read-modify-write of the complete
159
+ // snapshot, does nothing to this subtree, and it is no mutation. Drop the key in
160
+ // silence.
161
+ if (Object.is(store.get(pointer), value))
162
+ return false;
163
+ return reject(path);
164
+ };
165
+ const guarded = {
166
+ get: (path) => store.get(path),
167
+ getSnapshot: () => store.getSnapshot(),
168
+ subscribe: (listener) => store.subscribe(listener),
169
+ set: (path, value) => {
170
+ if (checkWrite(path, value))
171
+ store.set(path, value);
172
+ },
173
+ update: (updates) => {
174
+ const allowed = {};
175
+ let dropped = false;
176
+ for (const [path, value] of Object.entries(updates)) {
177
+ if (checkWrite(path, value)) {
178
+ allowed[path] = value; // nosemgrep: gitlab.eslint.detect-object-injection
179
+ }
180
+ else {
181
+ dropped = true;
182
+ }
183
+ }
184
+ store.update(dropped ? allowed : updates);
185
+ },
186
+ };
187
+ // The contract makes this member optional, and its absence has a meaning:
188
+ // json-render-core uses `getSnapshot` when the key is absent. Therefore the wrapper
189
+ // must build no member that returns `undefined`.
190
+ const { getServerSnapshot } = store;
191
+ if (getServerSnapshot) {
192
+ guarded.getServerSnapshot = () => getServerSnapshot.call(store);
193
+ }
194
+ return guarded;
195
+ }
196
+ /**
197
+ * Refreshes the `/context` subtree of a live store from the composed state of a
198
+ * derived view. An emission with the same `viewKey` means that only the projection
199
+ * changed. The function therefore replaces the complete subtree, and it never
200
+ * merges it field by field, because `update` cannot delete a key. Every ephemeral
201
+ * value at the root level stays. The function does nothing when the view carries no
202
+ * slice, or when the store holds the slice already.
203
+ *
204
+ * @param store - The store WITHOUT the guard. A provider refreshes through it.
205
+ * @param view - The derived view. Its `state.context` field carries the slice.
206
+ */
207
+ export function refreshContextSubtree(store, view) {
208
+ const slice = view.state?.[CONTEXT_STATE_KEY];
209
+ if (slice !== undefined && !Object.is(store.get(`/${CONTEXT_STATE_KEY}`), slice)) {
210
+ store.update({ [`/${CONTEXT_STATE_KEY}`]: slice });
211
+ }
212
+ }
213
+ /**
214
+ * Composes the effective state of a view: the authored `spec.state` and the
215
+ * `/context` slice.
216
+ *
217
+ * When the authored state declares the reserved key already, the function skips the
218
+ * projection, the authored state wins, and the code writes a warning in
219
+ * development. No spec that exists now therefore fails.
220
+ *
221
+ * @param authoredState - The raw `meta.view.state` value. It can be anything, and the caller or toAtomState cleans it.
222
+ * @param slice - The context of the machine, as one complete value.
223
+ */
224
+ export function composePlayState(authoredState, slice) {
225
+ if (authoredState !== undefined && Object.hasOwn(authoredState, CONTEXT_STATE_KEY)) {
226
+ warnOnce(authoredState, `[play-actor] spec.state declares a top-level "${CONTEXT_STATE_KEY}" key; ` +
227
+ `skipping the machine-context projection for this view. ` +
228
+ `Rename the state field to project context at /${CONTEXT_STATE_KEY}.`);
229
+ return authoredState;
230
+ }
231
+ if (slice === undefined) {
232
+ return authoredState;
233
+ }
234
+ return { ...authoredState, [CONTEXT_STATE_KEY]: slice };
235
+ }
236
+ //# sourceMappingURL=context-projection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-projection.js","sourceRoot":"","sources":["../src/context-projection.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,20 +1,25 @@
1
1
  /**
2
- * @xmachines/play-actor - Abstract Actor base class for Play Architecture
2
+ * @xmachines/play-actor - the abstract Actor base class of the Play Architecture
3
3
  *
4
- * This package provides AbstractActor, a minimal base class that extends XState Actor
5
- * while enforcing the Play Architecture's signal protocol (RFC section 5.3).
4
+ * This package gives you AbstractActor, a minimal base class. It extends the XState
5
+ * Actor class, and it enforces the signal protocol of the Play Architecture (RFC
6
+ * section 5.3).
6
7
  *
7
- * The core protocol is minimal (state + send). Optional capabilities are provided
8
- * via interfaces:
9
- * - Routable: For actors that support routing
10
- * - Viewable: For actors that support view rendering
8
+ * The core protocol is minimal: state and send. Two interfaces give the optional
9
+ * capabilities:
10
+ * - Routable: for an actor with a routing support
11
+ * - Viewable: for an actor with a view rendering
11
12
  *
12
- * Maintains XState ecosystem compatibility (inspection, devtools) while exposing
13
- * reactive signals for Infrastructure layer communication.
13
+ * The class keeps the compatibility with the XState ecosystem, such as the
14
+ * inspection and the devtools. It also exposes the reactive signals of the
15
+ * communication with the infrastructure layer.
14
16
  *
15
17
  * @packageDocumentation
16
18
  * @see [Play RFC](../../docs/rfc/play.md)
17
19
  */
18
20
  export { AbstractActor, typedSpec, type Routable, type Viewable, type PlaySpec, type BaseActorProviderProps, type BaseViewContextValue, } from "./abstract-actor.js";
19
21
  export { toAtomState, attachRenderErrorHandler } from "./provider-guards.js";
22
+ export { CONTEXT_STATE_KEY, guardContextWrites, refreshContextSubtree, composePlayState, shallowEqualExcept, reuseComposedState, } from "./context-projection.js";
23
+ export { createViewStoreLifecycle } from "./view-store-lifecycle.js";
24
+ export type { ViewStoreLifecycle, ViewStoreResolution, ResolveViewStoreOptions, } from "./view-store-lifecycle.js";
20
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACN,aAAa,EACb,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GACzB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACN,aAAa,EACb,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GACzB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,EACN,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EACX,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,GACvB,MAAM,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -1,20 +1,24 @@
1
1
  /**
2
- * @xmachines/play-actor - Abstract Actor base class for Play Architecture
2
+ * @xmachines/play-actor - the abstract Actor base class of the Play Architecture
3
3
  *
4
- * This package provides AbstractActor, a minimal base class that extends XState Actor
5
- * while enforcing the Play Architecture's signal protocol (RFC section 5.3).
4
+ * This package gives you AbstractActor, a minimal base class. It extends the XState
5
+ * Actor class, and it enforces the signal protocol of the Play Architecture (RFC
6
+ * section 5.3).
6
7
  *
7
- * The core protocol is minimal (state + send). Optional capabilities are provided
8
- * via interfaces:
9
- * - Routable: For actors that support routing
10
- * - Viewable: For actors that support view rendering
8
+ * The core protocol is minimal: state and send. Two interfaces give the optional
9
+ * capabilities:
10
+ * - Routable: for an actor with a routing support
11
+ * - Viewable: for an actor with a view rendering
11
12
  *
12
- * Maintains XState ecosystem compatibility (inspection, devtools) while exposing
13
- * reactive signals for Infrastructure layer communication.
13
+ * The class keeps the compatibility with the XState ecosystem, such as the
14
+ * inspection and the devtools. It also exposes the reactive signals of the
15
+ * communication with the infrastructure layer.
14
16
  *
15
17
  * @packageDocumentation
16
18
  * @see [Play RFC](../../docs/rfc/play.md)
17
19
  */
18
20
  export { AbstractActor, typedSpec, } from "./abstract-actor.js";
19
21
  export { toAtomState, attachRenderErrorHandler } from "./provider-guards.js";
22
+ export { CONTEXT_STATE_KEY, guardContextWrites, refreshContextSubtree, composePlayState, shallowEqualExcept, reuseComposedState, } from "./context-projection.js";
23
+ export { createViewStoreLifecycle } from "./view-store-lifecycle.js";
20
24
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACN,aAAa,EACb,SAAS,GAMT,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACN,aAAa,EACb,SAAS,GAMT,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,EACN,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC"}