@xmachines/play-view 0.0.0-bootstrap.0 → 4.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.
package/README.md CHANGED
@@ -1,17 +1,158 @@
1
1
  # @xmachines/play-view
2
2
 
3
- The shared view half of every XMachines renderer: the Viewable capability, the PlaySpec, and the view store lifecycle.
3
+ The shared view half of every renderer of the XMachines Play Architecture.
4
4
 
5
- ## This version is a placeholder
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Version](https://img.shields.io/badge/version-4.0.0-blue)](https://www.npmjs.com/package/@xmachines/play-view)
6
6
 
7
- Version `0.0.0-bootstrap.0` contains no code. It holds the name on the registry, so that
8
- the maintainers can configure trusted publishing before the first real release.
9
-
10
- The first release of this package is **4.0.0**. Install it with the other packages of the
11
- XMachines Play Architecture:
7
+ ## Installation
12
8
 
13
9
  ```bash
14
10
  pnpm add @xmachines/play-view
15
11
  ```
16
12
 
17
- Read the documentation at <https://gitlab.com/xmachin-es/xmachines-js>.
13
+ **Peer dependencies.** Install them with the package:
14
+
15
+ ```bash
16
+ pnpm add @xmachines/json-render-core
17
+ ```
18
+
19
+ A renderer of a framework depends on this package already. Install it directly when you write a renderer of your own.
20
+
21
+ ## Overview
22
+
23
+ A renderer of a framework is two things: the view lifecycle, and the code that binds that lifecycle to the effects of the framework. This package holds the first one. [`@xmachines/play-react`](../play-react/README.md), [`@xmachines/play-vue`](../play-vue/README.md), [`@xmachines/play-solid`](../play-solid/README.md), [`@xmachines/play-svelte`](../play-svelte/README.md) and [`@xmachines/play-dom`](../play-dom/README.md) hold the second one.
24
+
25
+ It also holds `Viewable` and `PlaySpec`. Every type here names a type of `@xmachines/json-render-core`, and that is the reason they are here and not in [`@xmachines/play-actor`](../play-actor/README.md): an application that routes and renders no view installs this package never, and json-render never.
26
+
27
+ | Export | Description |
28
+ | ------------------------------------------------ | --------------------------------------------------------------------- |
29
+ | `Viewable` | The capability of an actor that publishes a `currentView` signal |
30
+ | `PlaySpec`, `typedSpec` | The spec that an actor publishes, and its identity helper |
31
+ | `BaseActorProviderProps`, `BaseViewContextValue` | The framework-free half of the props and of the context of a provider |
32
+ | `createViewStoreLifecycle` | The lifecycle of the state store of one view |
33
+ | `createFailureLatch`, `createReportGuard` | The report of a render failure, one time for each view |
34
+ | `toAtomState`, `attachRenderErrorHandler` | The guards that a provider puts around a registry |
35
+ | `composePlayState`, `refreshContextSubtree`, … | The projection of the machine context into the store |
36
+
37
+ ## API Summary
38
+
39
+ ### `typedSpec(spec)`
40
+
41
+ This identity helper gives a view-spec literal the type `PlaySpec` at the definition site. The
42
+ XState `meta` field has the type `Record<string, unknown>`. Therefore this helper is the place
43
+ where the spec shape receives the compile-time check and the IDE autocomplete. The helper has no
44
+ cost at run time.
45
+
46
+ ```ts
47
+ import { typedSpec } from "@xmachines/play-view";
48
+
49
+ // In an XState machine meta block:
50
+ meta: {
51
+ view: typedSpec({
52
+ root: "root",
53
+ elements: {
54
+ root: {
55
+ type: "Dashboard",
56
+ props: { username: { $state: "/context/username" } },
57
+ children: [],
58
+ },
59
+ },
60
+ }),
61
+ }
62
+ ```
63
+
64
+ ### `PlaySpec`
65
+
66
+ This type extends the `Spec` type of `@xmachines/json-render-core`. Each derived view receives
67
+ the complete machine context in its state store, under the read-only **`/context` subtree**. A
68
+ spec therefore reads the context through the ordinary `{ $state: "/context/…" }` grammar: in a
69
+ prop, in a `visible` condition, and in `repeat.statePath`.
70
+
71
+ `/context` is read-only by design. Nothing can write to it. The machine context changes through
72
+ an event only. A `$bindState` write or a `setState` write under `/context` throws an error, and
73
+ the error names the event to send. This is the model: the bindable ephemeral state is at the
74
+ root of the store, from `spec.state`; the domain state is in the machine, and it changes through
75
+ events that are meaningful and easy to inspect.
76
+
77
+ The path shows the origin of each value. `/context/params/username` comes from the URL.
78
+ `/context/username` belongs to the machine. One value can never hide the other.
79
+
80
+ The model projects everything, and this has two consequences. The first consequence is
81
+ **exposure**. The complete context is visible to the client in the view store, which includes a
82
+ debug panel, an inspector, and a validator. The context is a client-side value in each case, so
83
+ keep a secret out of it.
84
+
85
+ The second consequence is **emission granularity**. The emit gate compares the context field by
86
+ field, at the top level only. Therefore an event that changes any field emits the view again
87
+ with the same `viewKey`. A provider refreshes `/context` in the live store, and it does not seed
88
+ the store again. The component does not remount, and the ephemeral view state and the focus
89
+ stay. However, a new emission is still a render pass in the framework layer. Keep
90
+ high-frequency ephemeral data, such as a draft for each keystroke or a timer, in the view store
91
+ (`spec.state` with `$bindState`) or in a child actor. The domain state belongs in the context. A
92
+ keystroke does not.
93
+
94
+ ```ts
95
+ import type { PlaySpec } from "@xmachines/play-view";
96
+
97
+ const spec: PlaySpec = {
98
+ root: "root",
99
+ elements: {
100
+ root: {
101
+ type: "Profile",
102
+ props: { username: { $state: "/context/username" } },
103
+ children: [],
104
+ },
105
+ },
106
+ };
107
+ ```
108
+
109
+ > Historical note: an earlier version had a `contextProps` field. At first the field drove an
110
+ > implicit prop-enrichment pass. That pass merged the allowlisted context fields and the URL
111
+ > params into the props of every element. We removed it, because it put values into components
112
+ > that never asked for them, and it let URL data from the user hide machine-owned state. The
113
+ > field was then a projection filter for a short time. We removed that filter too, because a
114
+ > limit on what a view can read added machinery without a real problem to solve. Always
115
+ > validate the **derived** view (`actor.currentView.get()`), not the raw `meta.view`. The
116
+ > `state` of the derived spec carries the projection, so a tool such as `validateSpec` sees a
117
+ > spec that is consistent with itself.
118
+
119
+ ### `Viewable`
120
+
121
+ Interface for actors that expose a renderable view signal.
122
+
123
+ ```ts
124
+ import type { Viewable } from "@xmachines/play-view";
125
+ import type { PlaySpec } from "@xmachines/play-view";
126
+ import { Signal } from "@xmachines/play-signals";
127
+
128
+ // currentView carries PlaySpec | null
129
+ const signal = new Signal.State<PlaySpec | null>(null);
130
+ const viewable: Viewable = { currentView: signal };
131
+ ```
132
+
133
+ ### `BaseActorProviderProps<TRegistry>`
134
+
135
+ The framework-agnostic base props. Every `ActorProvider` implementation shares them: React, Vue, Solid, and Svelte. Each framework renderer package extends this interface.
136
+
137
+ ```ts
138
+ import type { BaseActorProviderProps } from "@xmachines/play-view";
139
+ import type { DefineRegistryResult } from "@xmachines/json-render-react";
140
+
141
+ interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
142
+ fallback?: React.ReactNode;
143
+ children: React.ReactNode;
144
+ }
145
+ ```
146
+
147
+ ### `BaseViewContextValue<TRegistry>`
148
+
149
+ The framework-agnostic base of the `ViewContextValue` type in each framework. It holds the `spec`, `handlers`, `registry`, and `store` fields. These fields are identical in React, Vue, Solid, and Svelte.
150
+
151
+ ## Documentation
152
+
153
+ - [Architecture](../docs/contributing/architecture.md)
154
+ - [Play RFC](../docs/rfc/play.md)
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,106 @@
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 "./view-types.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
+ * Reuses the composed `state` of the previous emission, or the complete previous
32
+ * spec, when the value of the projection did not change.
33
+ *
34
+ * `deriveCurrentView` composes `state: { ...meta.view.state, context: slice }` new
35
+ * on each call, and the XState function `assign` makes a new context object on
36
+ * every event. Without this reuse, every event therefore presents a new `state`
37
+ * reference, and the `Object.is` test of each field in the emit gate emits the view
38
+ * again and remounts it, without an end. The function compares the slices field by
39
+ * field ({@link shallowEqualExcept}), because the XState function `assign` makes a
40
+ * new context object for each event, and a test of the identity of the whole object
41
+ * therefore reports a change every time. The authored fields come from the static
42
+ * `meta.view.state` through a spread. Therefore their references are stable for an
43
+ * unchanged `viewKey`, and a plain `Object.is` test is correct for them.
44
+ *
45
+ * The function returns one of three values, in this order of preference:
46
+ * - `prev` itself, when nothing observable changed. The emit gate then stops at the
47
+ * identity of the reference, and it walks no element;
48
+ * - `{ ...next, state: prev.state }`, when the value of the state did not change
49
+ * but another top-level field is different;
50
+ * - `next`, when something changed.
51
+ */
52
+ export declare function reuseComposedState(prev: PlaySpec | null, next: PlaySpec | null): PlaySpec | null;
53
+ /**
54
+ * Wraps a StateStore, and the wrapper refuses each write under `/context`.
55
+ *
56
+ * A provider applies it to the store that it gives to the bindings and to the
57
+ * action handlers (`$bindState`, `setState`, and a chained `set`). The provider
58
+ * keeps the store without the wrapper, and it refreshes the projection through that
59
+ * store. The subtree is therefore read-only to the spec, and not to the machinery.
60
+ *
61
+ * A write with a value that is **identical** to the current value passes in
62
+ * silence. A handler in the style of `setState` reads the complete snapshot,
63
+ * changes it, and writes the whole object back. The `context` key that goes through
64
+ * that round trip without a change is no attempt of a mutation. Only a write that
65
+ * changes the subtree throws.
66
+ *
67
+ * Each read passes through without a change. The wrapper delegates every member
68
+ * explicitly, and it does not use a spread: a store from a consumer can be an
69
+ * instance of a class, and the methods of that instance are on the prototype. Those
70
+ * methods do not survive `{ ...store }`, and the first render then fails with
71
+ * `store.getSnapshot is not a function`. The code builds the wrapper one time for
72
+ * each resolved store. Therefore the identity of the delegating `getSnapshot` and
73
+ * of the delegating `subscribe` stays stable for a consumer in the style of
74
+ * `useSyncExternalStore`.
75
+ *
76
+ * @param store - The store below the wrapper.
77
+ * @returns A store with a guard on `set` and on `update`.
78
+ */
79
+ export declare function guardContextWrites(store: StateStore): StateStore;
80
+ /**
81
+ * Refreshes the `/context` subtree of a live store from the composed state of a
82
+ * derived view. An emission with the same `viewKey` means that only the projection
83
+ * changed. The function therefore replaces the complete subtree, and it never
84
+ * merges it field by field, because `update` cannot delete a key. Every ephemeral
85
+ * value at the root level stays. The function does nothing when the view carries no
86
+ * slice, or when the store holds the slice already.
87
+ *
88
+ * @param store - The store WITHOUT the guard. A provider refreshes through it.
89
+ * @param view - The derived view. Its `state.context` field carries the slice.
90
+ */
91
+ export declare function refreshContextSubtree(store: StateStore, view: {
92
+ state?: unknown;
93
+ }): void;
94
+ /**
95
+ * Composes the effective state of a view: the authored `spec.state` and the
96
+ * `/context` slice.
97
+ *
98
+ * When the authored state declares the reserved key already, the function skips the
99
+ * projection, the authored state wins, and the code writes a warning in
100
+ * development. No spec that exists now therefore fails.
101
+ *
102
+ * @param authoredState - The raw `meta.view.state` value. It can be anything, and the caller or toAtomState cleans it.
103
+ * @param slice - The context of the machine, as one complete value.
104
+ */
105
+ export declare function composePlayState(authoredState: Record<string, unknown> | undefined, slice: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
106
+ //# 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;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAiB3C;;;;;;;;;;;;;;;;;;;;;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,CA2ChE;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,215 @@
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 { ReadOnlyContextError } from "./errors.js";
21
+ import { shallowEqualExcept } from "@xmachines/play";
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 const CONTEXT_STATE_KEY = "context";
30
+ /**
31
+ * The record that reports each diagnostic one time for each spec. The condition
32
+ * here is a static property of the authored meta.view, because its `state` object
33
+ * never changes between two events. `deriveCurrentView` runs on every machine
34
+ * snapshot, and a report for each derivation therefore floods the console at the
35
+ * rate of the events. The key is the identity of the static object. Each spec
36
+ * therefore reports one time, and a test with a new literal stays separate.
37
+ */
38
+ const reportedDiagnostics = new WeakSet();
39
+ function warnOnce(anchor, message) {
40
+ if (reportedDiagnostics.has(anchor))
41
+ return;
42
+ reportedDiagnostics.add(anchor);
43
+ console.warn(message);
44
+ }
45
+ /**
46
+ * Reuses the composed `state` of the previous emission, or the complete previous
47
+ * spec, when the value of the projection did not change.
48
+ *
49
+ * `deriveCurrentView` composes `state: { ...meta.view.state, context: slice }` new
50
+ * on each call, and the XState function `assign` makes a new context object on
51
+ * every event. Without this reuse, every event therefore presents a new `state`
52
+ * reference, and the `Object.is` test of each field in the emit gate emits the view
53
+ * again and remounts it, without an end. The function compares the slices field by
54
+ * field ({@link shallowEqualExcept}), because the XState function `assign` makes a
55
+ * new context object for each event, and a test of the identity of the whole object
56
+ * therefore reports a change every time. The authored fields come from the static
57
+ * `meta.view.state` through a spread. Therefore their references are stable for an
58
+ * unchanged `viewKey`, and a plain `Object.is` test is correct for them.
59
+ *
60
+ * The function returns one of three values, in this order of preference:
61
+ * - `prev` itself, when nothing observable changed. The emit gate then stops at the
62
+ * identity of the reference, and it walks no element;
63
+ * - `{ ...next, state: prev.state }`, when the value of the state did not change
64
+ * but another top-level field is different;
65
+ * - `next`, when something changed.
66
+ */
67
+ export function reuseComposedState(prev, next) {
68
+ if (!prev || !next || prev.viewKey !== next.viewKey)
69
+ return next;
70
+ const prevState = prev.state;
71
+ const nextState = next.state;
72
+ if (prevState !== nextState) {
73
+ if (prevState === undefined || nextState === undefined)
74
+ return next;
75
+ if (!shallowEqualExcept(prevState, nextState, CONTEXT_STATE_KEY))
76
+ return next;
77
+ const prevSlice = prevState[CONTEXT_STATE_KEY];
78
+ const nextSlice = nextState[CONTEXT_STATE_KEY];
79
+ if (prevSlice !== nextSlice) {
80
+ if (prevSlice === undefined || nextSlice === undefined)
81
+ return next;
82
+ if (!shallowEqualExcept(prevSlice, nextSlice))
83
+ return next;
84
+ }
85
+ if (shallowEqualExcept(prev, next, "state"))
86
+ return prev;
87
+ return { ...next, state: prevState };
88
+ }
89
+ return shallowEqualExcept(prev, next, "state") ? prev : next;
90
+ }
91
+ /**
92
+ * The paths of the reserved subtree: `"/context"` itself, and every path below it.
93
+ * An update map of the store can hold a bare top-level key ("context") and also a
94
+ * JSON Pointer ("/context/…"). Both forms address the same subtree.
95
+ */
96
+ function toPointer(path) {
97
+ return path.startsWith("/") ? path : `/${path}`;
98
+ }
99
+ /** This function expects a NORMALIZED pointer from {@link toPointer}. */
100
+ function isContextPointer(pointer) {
101
+ return pointer === `/${CONTEXT_STATE_KEY}` || pointer.startsWith(`/${CONTEXT_STATE_KEY}/`);
102
+ }
103
+ /**
104
+ * Wraps a StateStore, and the wrapper refuses each write under `/context`.
105
+ *
106
+ * A provider applies it to the store that it gives to the bindings and to the
107
+ * action handlers (`$bindState`, `setState`, and a chained `set`). The provider
108
+ * keeps the store without the wrapper, and it refreshes the projection through that
109
+ * store. The subtree is therefore read-only to the spec, and not to the machinery.
110
+ *
111
+ * A write with a value that is **identical** to the current value passes in
112
+ * silence. A handler in the style of `setState` reads the complete snapshot,
113
+ * changes it, and writes the whole object back. The `context` key that goes through
114
+ * that round trip without a change is no attempt of a mutation. Only a write that
115
+ * changes the subtree throws.
116
+ *
117
+ * Each read passes through without a change. The wrapper delegates every member
118
+ * explicitly, and it does not use a spread: a store from a consumer can be an
119
+ * instance of a class, and the methods of that instance are on the prototype. Those
120
+ * methods do not survive `{ ...store }`, and the first render then fails with
121
+ * `store.getSnapshot is not a function`. The code builds the wrapper one time for
122
+ * each resolved store. Therefore the identity of the delegating `getSnapshot` and
123
+ * of the delegating `subscribe` stays stable for a consumer in the style of
124
+ * `useSyncExternalStore`.
125
+ *
126
+ * @param store - The store below the wrapper.
127
+ * @returns A store with a guard on `set` and on `update`.
128
+ */
129
+ export function guardContextWrites(store) {
130
+ const reject = (path) => {
131
+ throw new ReadOnlyContextError(path, CONTEXT_STATE_KEY);
132
+ };
133
+ const checkWrite = (path, value) => {
134
+ const pointer = toPointer(path);
135
+ if (!isContextPointer(pointer))
136
+ return true;
137
+ // A round trip without a change, which is a read-modify-write of the complete
138
+ // snapshot, does nothing to this subtree, and it is no mutation. Drop the key in
139
+ // silence.
140
+ if (Object.is(store.get(pointer), value))
141
+ return false;
142
+ return reject(path);
143
+ };
144
+ const guarded = {
145
+ get: (path) => store.get(path),
146
+ getSnapshot: () => store.getSnapshot(),
147
+ subscribe: (listener) => store.subscribe(listener),
148
+ set: (path, value) => {
149
+ if (checkWrite(path, value))
150
+ store.set(path, value);
151
+ },
152
+ update: (updates) => {
153
+ const allowed = {};
154
+ let dropped = false;
155
+ for (const [path, value] of Object.entries(updates)) {
156
+ if (checkWrite(path, value)) {
157
+ allowed[path] = value; // nosemgrep: gitlab.eslint.detect-object-injection
158
+ }
159
+ else {
160
+ dropped = true;
161
+ }
162
+ }
163
+ store.update(dropped ? allowed : updates);
164
+ },
165
+ };
166
+ // The contract makes this member optional, and its absence has a meaning:
167
+ // json-render-core uses `getSnapshot` when the key is absent. Therefore the wrapper
168
+ // must build no member that returns `undefined`.
169
+ const { getServerSnapshot } = store;
170
+ if (getServerSnapshot) {
171
+ guarded.getServerSnapshot = () => getServerSnapshot.call(store);
172
+ }
173
+ return guarded;
174
+ }
175
+ /**
176
+ * Refreshes the `/context` subtree of a live store from the composed state of a
177
+ * derived view. An emission with the same `viewKey` means that only the projection
178
+ * changed. The function therefore replaces the complete subtree, and it never
179
+ * merges it field by field, because `update` cannot delete a key. Every ephemeral
180
+ * value at the root level stays. The function does nothing when the view carries no
181
+ * slice, or when the store holds the slice already.
182
+ *
183
+ * @param store - The store WITHOUT the guard. A provider refreshes through it.
184
+ * @param view - The derived view. Its `state.context` field carries the slice.
185
+ */
186
+ export function refreshContextSubtree(store, view) {
187
+ const slice = view.state?.[CONTEXT_STATE_KEY];
188
+ if (slice !== undefined && !Object.is(store.get(`/${CONTEXT_STATE_KEY}`), slice)) {
189
+ store.update({ [`/${CONTEXT_STATE_KEY}`]: slice });
190
+ }
191
+ }
192
+ /**
193
+ * Composes the effective state of a view: the authored `spec.state` and the
194
+ * `/context` slice.
195
+ *
196
+ * When the authored state declares the reserved key already, the function skips the
197
+ * projection, the authored state wins, and the code writes a warning in
198
+ * development. No spec that exists now therefore fails.
199
+ *
200
+ * @param authoredState - The raw `meta.view.state` value. It can be anything, and the caller or toAtomState cleans it.
201
+ * @param slice - The context of the machine, as one complete value.
202
+ */
203
+ export function composePlayState(authoredState, slice) {
204
+ if (authoredState !== undefined && Object.hasOwn(authoredState, CONTEXT_STATE_KEY)) {
205
+ warnOnce(authoredState, `[play-view] spec.state declares a top-level "${CONTEXT_STATE_KEY}" key; ` +
206
+ `skipping the machine-context projection for this view. ` +
207
+ `Rename the state field to project context at /${CONTEXT_STATE_KEY}.`);
208
+ return authoredState;
209
+ }
210
+ if (slice === undefined) {
211
+ return authoredState;
212
+ }
213
+ return { ...authoredState, [CONTEXT_STATE_KEY]: slice };
214
+ }
215
+ //# 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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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The errors of the view half.
3
+ *
4
+ * Every error of this workspace extends `PlayError` of `@xmachines/play`, and it carries
5
+ * a `scope` and a `code` that stay the same across a patch release and a minor release.
6
+ * Match on the `code`, or on the class. Match on the `message` never.
7
+ *
8
+ * @packageDocumentation
9
+ * @see [Architecture](../../docs/contributing/architecture.md)
10
+ */
11
+ import { PlayError } from "@xmachines/play/errors";
12
+ /**
13
+ * A write reached the context projection of the view store.
14
+ *
15
+ * The `/context` subtree of a view mirrors the context of the machine, and it is a
16
+ * READ of that context. A write there would hold a value that the next snapshot of the
17
+ * machine throws away, so the store refuses it. Send the actor an event instead: the
18
+ * context changes in the machine, and the projection follows.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { ReadOnlyContextError } from "@xmachines/play-view/errors";
23
+ *
24
+ * try {
25
+ * store.set("/context/user/name", "Alice");
26
+ * } catch (error) {
27
+ * if (error instanceof ReadOnlyContextError) {
28
+ * actor.send({ type: "user.rename", name: "Alice" });
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export declare class ReadOnlyContextError extends PlayError {
34
+ /** The JSON pointer of the write that the store refused, as the caller gave it. */
35
+ readonly path: string;
36
+ /**
37
+ * @param path - The path of the refused write.
38
+ * @param stateKey - The key of the context subtree, for the message.
39
+ */
40
+ constructor(path: string, stateKey: string);
41
+ }
42
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;IAClD,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;gBACS,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAU1C"}
package/dist/errors.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The errors of the view half.
3
+ *
4
+ * Every error of this workspace extends `PlayError` of `@xmachines/play`, and it carries
5
+ * a `scope` and a `code` that stay the same across a patch release and a minor release.
6
+ * Match on the `code`, or on the class. Match on the `message` never.
7
+ *
8
+ * @packageDocumentation
9
+ * @see [Architecture](../../docs/contributing/architecture.md)
10
+ */
11
+ import { PlayError } from "@xmachines/play/errors";
12
+ /**
13
+ * A write reached the context projection of the view store.
14
+ *
15
+ * The `/context` subtree of a view mirrors the context of the machine, and it is a
16
+ * READ of that context. A write there would hold a value that the next snapshot of the
17
+ * machine throws away, so the store refuses it. Send the actor an event instead: the
18
+ * context changes in the machine, and the projection follows.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { ReadOnlyContextError } from "@xmachines/play-view/errors";
23
+ *
24
+ * try {
25
+ * store.set("/context/user/name", "Alice");
26
+ * } catch (error) {
27
+ * if (error instanceof ReadOnlyContextError) {
28
+ * actor.send({ type: "user.rename", name: "Alice" });
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+ export class ReadOnlyContextError extends PlayError {
34
+ /** The JSON pointer of the write that the store refused, as the caller gave it. */
35
+ path;
36
+ /**
37
+ * @param path - The path of the refused write.
38
+ * @param stateKey - The key of the context subtree, for the message.
39
+ */
40
+ constructor(path, stateKey) {
41
+ super("guardContextWrites", "PLAY_VIEW_READ_ONLY_CONTEXT", `"${path}" is read-only: /${stateKey} mirrors the context of the machine. ` +
42
+ `Send the actor an event to change it, because the context changes through the view store never.`);
43
+ this.name = "ReadOnlyContextError";
44
+ this.path = path;
45
+ }
46
+ }
47
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAClD,mFAAmF;IAC1E,IAAI,CAAS;IAEtB;;;OAGG;IACH,YAAY,IAAY,EAAE,QAAgB;QACzC,KAAK,CACJ,oBAAoB,EACpB,6BAA6B,EAC7B,IAAI,IAAI,oBAAoB,QAAQ,uCAAuC;YAC1E,iGAAiG,CAClG,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;CACD"}