@xmachines/play-actor 2.0.0-alpha.1 → 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.
- package/README.md +68 -26
- package/dist/abstract-actor.d.ts +46 -31
- package/dist/abstract-actor.d.ts.map +1 -1
- package/dist/abstract-actor.js +26 -15
- package/dist/abstract-actor.js.map +1 -1
- package/dist/context-projection.d.ts +109 -0
- package/dist/context-projection.d.ts.map +1 -0
- package/dist/context-projection.js +228 -0
- package/dist/context-projection.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/view-store-lifecycle.d.ts +77 -0
- package/dist/view-store-lifecycle.d.ts.map +1 -0
- package/dist/view-store-lifecycle.js +94 -0
- package/dist/view-store-lifecycle.js.map +1 -0
- package/package.json +22 -21
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<!-- generated-by: gsd-doc-writer -->
|
|
2
|
-
|
|
3
1
|
# @xmachines/play-actor
|
|
4
2
|
|
|
5
3
|
Abstract Actor base class for XMachines Play Architecture.
|
|
6
4
|
|
|
5
|
+
[](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/@xmachines/play-actor)
|
|
6
|
+
|
|
7
7
|
Part of the [xmachines-js monorepo](../../README.md).
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
@@ -15,7 +15,7 @@ pnpm add @xmachines/play-actor
|
|
|
15
15
|
**Peer dependencies** — install alongside the package:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pnpm add xstate @xmachines/play @xmachines/play-signals
|
|
18
|
+
pnpm add xstate @xmachines/play @xmachines/play-signals @xmachines/json-render-core
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Overview
|
|
@@ -45,19 +45,30 @@ Concrete implementations are created by adapters such as [`@xmachines/play-xstat
|
|
|
45
45
|
|
|
46
46
|
Abstract base class extending XState `Actor<TLogic>`.
|
|
47
47
|
|
|
48
|
+
A subclass **is** the actor: hand the logic and its options to `super()` so one
|
|
49
|
+
instance holds the running machine, and reach XState's own `send` through the
|
|
50
|
+
prototype — `send` is declared abstract here only to narrow the event type, and
|
|
51
|
+
TypeScript forbids `super` calls to an abstract member.
|
|
52
|
+
|
|
48
53
|
```ts
|
|
49
54
|
import { AbstractActor } from "@xmachines/play-actor";
|
|
50
55
|
import { Signal } from "@xmachines/play-signals";
|
|
51
|
-
import type
|
|
56
|
+
import { Actor, type ActorOptions, type AnyActorLogic } from "xstate";
|
|
52
57
|
|
|
53
58
|
class MyActor extends AbstractActor<AnyActorLogic> {
|
|
54
59
|
// Required: reactive state signal
|
|
55
|
-
state
|
|
60
|
+
state: Signal.State<unknown>;
|
|
61
|
+
|
|
62
|
+
constructor(logic: AnyActorLogic, options?: ActorOptions<AnyActorLogic>) {
|
|
63
|
+
super(logic, options);
|
|
64
|
+
this.state = new Signal.State(this.getSnapshot());
|
|
65
|
+
super.subscribe((snapshot) => this.state.set(snapshot));
|
|
66
|
+
}
|
|
56
67
|
|
|
57
68
|
// Required: typed event dispatch
|
|
58
|
-
send
|
|
59
|
-
|
|
60
|
-
}
|
|
69
|
+
override send(event: { type: string }): void {
|
|
70
|
+
Actor.prototype.send.call(this, event);
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
```
|
|
63
74
|
|
|
@@ -69,33 +80,31 @@ type AuthEvent = { type: "auth.login"; username: string } | { type: "auth.logout
|
|
|
69
80
|
class AuthActor extends AbstractActor<AnyActorLogic, AuthEvent> {
|
|
70
81
|
state = new Signal.State({ isAuthenticated: false, username: null });
|
|
71
82
|
|
|
72
|
-
send
|
|
73
|
-
|
|
74
|
-
}
|
|
83
|
+
override send(event: AuthEvent): void {
|
|
84
|
+
Actor.prototype.send.call(this, event);
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
87
|
```
|
|
77
88
|
|
|
78
|
-
### `typedSpec
|
|
89
|
+
### `typedSpec(spec)`
|
|
79
90
|
|
|
80
|
-
Identity helper that
|
|
91
|
+
Identity helper that types a view-spec literal as `PlaySpec` at the definition site. XState's
|
|
92
|
+
`meta` field is `Record<string, unknown>`, so this is where the spec shape gets compile-time
|
|
93
|
+
validation and IDE autocomplete — without any runtime cost.
|
|
81
94
|
|
|
82
95
|
```ts
|
|
83
96
|
import { typedSpec } from "@xmachines/play-actor";
|
|
84
97
|
|
|
85
|
-
interface DashboardCtx {
|
|
86
|
-
username: string;
|
|
87
|
-
params: Record<string, string>;
|
|
88
|
-
query: Record<string, string>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
98
|
// In an XState machine meta block:
|
|
92
99
|
meta: {
|
|
93
|
-
view: typedSpec
|
|
100
|
+
view: typedSpec({
|
|
94
101
|
root: "root",
|
|
95
|
-
contextProps: ["username"], // ✓ key of DashboardCtx
|
|
96
|
-
// contextProps: ["usernaem"], // ✗ compile error
|
|
97
102
|
elements: {
|
|
98
|
-
root: {
|
|
103
|
+
root: {
|
|
104
|
+
type: "Dashboard",
|
|
105
|
+
props: { username: { $state: "/context/username" } },
|
|
106
|
+
children: [],
|
|
107
|
+
},
|
|
99
108
|
},
|
|
100
109
|
}),
|
|
101
110
|
}
|
|
@@ -103,20 +112,53 @@ meta: {
|
|
|
103
112
|
|
|
104
113
|
### `PlaySpec`
|
|
105
114
|
|
|
106
|
-
Extends `@xmachines/json-render-core`'s `Spec
|
|
115
|
+
Extends `@xmachines/json-render-core`'s `Spec`. The machine's whole context is projected into
|
|
116
|
+
every derived view's state store under the read-only **`/context` subtree**, so specs read it
|
|
117
|
+
through the ordinary `{ $state: "/context/…" }` grammar — in props, `visible` conditions, and
|
|
118
|
+
`repeat.statePath` alike.
|
|
119
|
+
|
|
120
|
+
`/context` is read-only by design — never writable. Machine context changes only through events;
|
|
121
|
+
a `$bindState` or `setState` write under `/context` throws with an error naming the event to
|
|
122
|
+
send instead. This is the model: bindable ephemeral state lives at the store root (seeded from
|
|
123
|
+
`spec.state`), domain state lives in the machine and changes via meaningful, inspectable events.
|
|
124
|
+
|
|
125
|
+
Provenance is legible in the path: `/context/params/username` is visibly URL-derived, while
|
|
126
|
+
`/context/username` is machine-owned — one can never shadow the other.
|
|
127
|
+
|
|
128
|
+
Two consequences of the everything-is-projected model are worth knowing. First, **exposure**:
|
|
129
|
+
the whole context is client-visible in the view store (debug panels, inspectors, validators) —
|
|
130
|
+
context is a client-side value either way, so keep secrets out of it. Second, **emission
|
|
131
|
+
granularity**: the emit gate compares context per top-level field, so an event that changes any
|
|
132
|
+
field re-emits the view with the same `viewKey`. Providers refresh `/context` in the live store
|
|
133
|
+
rather than reseeding it — no remount, ephemeral view state and focus survive — but a
|
|
134
|
+
re-emission is still a render pass in the framework layer. Keep high-frequency ephemeral data
|
|
135
|
+
(per-keystroke drafts, timers) in the view store (`spec.state` + `$bindState`) or a child actor
|
|
136
|
+
rather than in machine context; domain state belongs in context, keystrokes do not.
|
|
107
137
|
|
|
108
138
|
```ts
|
|
109
139
|
import type { PlaySpec } from "@xmachines/play-actor";
|
|
110
140
|
|
|
111
141
|
const spec: PlaySpec = {
|
|
112
142
|
root: "root",
|
|
113
|
-
contextProps: ["username"], // only these keys are exposed to components
|
|
114
143
|
elements: {
|
|
115
|
-
root: {
|
|
144
|
+
root: {
|
|
145
|
+
type: "Profile",
|
|
146
|
+
props: { username: { $state: "/context/username" } },
|
|
147
|
+
children: [],
|
|
148
|
+
},
|
|
116
149
|
},
|
|
117
150
|
};
|
|
118
151
|
```
|
|
119
152
|
|
|
153
|
+
> Historical note: earlier versions had a `contextProps` field. It first drove an implicit
|
|
154
|
+
> prop-enrichment pass that merged allowlisted context fields and URL params into every
|
|
155
|
+
> element's props (removed — it injected values into components that never asked for them and
|
|
156
|
+
> let user-manipulable URL data silently shadow machine-owned state), and was then briefly a
|
|
157
|
+
> projection filter (removed — filtering what a view may read added machinery without a real
|
|
158
|
+
> problem to solve). Validate the **derived** view (`actor.currentView.get()`), not the raw
|
|
159
|
+
> `meta.view`: the derived spec's `state` carries the projection, so tools like `validateSpec`
|
|
160
|
+
> see a self-consistent spec.
|
|
161
|
+
|
|
120
162
|
### `Routable`
|
|
121
163
|
|
|
122
164
|
Interface for actors that support routing.
|
package/dist/abstract-actor.d.ts
CHANGED
|
@@ -29,56 +29,55 @@ export interface Routable {
|
|
|
29
29
|
/**
|
|
30
30
|
* XMachines extension of `@xmachines/json-render-core` `Spec`.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* Use `typedSpec<TContext>(...)` at the definition site to validate `contextProps`
|
|
38
|
-
* entries against your machine's context type at compile time.
|
|
32
|
+
* The machine's context is projected into every derived view's state store
|
|
33
|
+
* under the read-only `/context` subtree, so specs read it through the
|
|
34
|
+
* ordinary `{ $state: "/context/…" }` grammar — in props, `visible`
|
|
35
|
+
* conditions, and `repeat.statePath` alike. The whole context is always
|
|
36
|
+
* projected; a spec simply reads the paths it needs.
|
|
39
37
|
*/
|
|
40
38
|
export interface PlaySpec extends Spec {
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
40
|
+
* Identity of the view this derived spec came from — set by
|
|
41
|
+
* `deriveCurrentView` from the meta entry the derivation actually selected.
|
|
42
|
+
* Providers key their store lifecycle on it: a changed `viewKey` reseeds the
|
|
43
|
+
* store; an unchanged one refreshes `/context` in place, preserving
|
|
44
|
+
* ephemeral view state. Never author this field in `meta.view`.
|
|
47
45
|
*/
|
|
48
|
-
readonly
|
|
46
|
+
readonly viewKey?: string;
|
|
49
47
|
}
|
|
50
48
|
/**
|
|
51
|
-
* Identity helper that
|
|
52
|
-
*
|
|
53
|
-
* autocomplete at the definition site.
|
|
49
|
+
* Identity helper that types a view spec literal as `PlaySpec` at the
|
|
50
|
+
* definition site, giving compile-time validation and IDE autocomplete.
|
|
54
51
|
*
|
|
55
52
|
* XState's `meta` field is typed as `Record<string, unknown>`, so TypeScript
|
|
56
|
-
* cannot infer the
|
|
57
|
-
*
|
|
53
|
+
* cannot infer the spec shape from context. `typedSpec(...)` is the opt-in
|
|
54
|
+
* mechanism that activates checking where the spec is written. `viewKey` is
|
|
55
|
+
* excluded from the parameter — derivation stamps it and would silently
|
|
56
|
+
* overwrite an authored value, so authoring one is rejected at compile time.
|
|
57
|
+
*
|
|
58
|
+
* Excess-property checking only applies to an inline object literal; for a
|
|
59
|
+
* spec built in a variable or through spreads, use `satisfies PlaySpec` at
|
|
60
|
+
* the literal instead.
|
|
58
61
|
*
|
|
59
62
|
* At runtime this is a no-op — the spec object is returned unchanged.
|
|
60
63
|
*
|
|
61
64
|
* @example
|
|
62
65
|
* ```ts
|
|
63
|
-
* interface DashboardCtx {
|
|
64
|
-
* username: string;
|
|
65
|
-
* params: Record<string, string>;
|
|
66
|
-
* query: Record<string, string>;
|
|
67
|
-
* }
|
|
68
|
-
*
|
|
69
66
|
* meta: {
|
|
70
|
-
* view: typedSpec
|
|
67
|
+
* view: typedSpec({
|
|
71
68
|
* root: "root",
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
69
|
+
* elements: {
|
|
70
|
+
* root: {
|
|
71
|
+
* type: "Dashboard",
|
|
72
|
+
* props: { username: { $state: "/context/username" } },
|
|
73
|
+
* children: [],
|
|
74
|
+
* },
|
|
75
|
+
* },
|
|
75
76
|
* }),
|
|
76
77
|
* }
|
|
77
78
|
* ```
|
|
78
79
|
*/
|
|
79
|
-
export declare function typedSpec
|
|
80
|
-
readonly contextProps?: readonly (keyof TContext & string)[];
|
|
81
|
-
}): PlaySpec;
|
|
80
|
+
export declare function typedSpec(spec: Omit<PlaySpec, "viewKey">): PlaySpec;
|
|
82
81
|
/**
|
|
83
82
|
* Actor capability for exposing renderable view state.
|
|
84
83
|
*
|
|
@@ -163,6 +162,14 @@ export interface BaseActorProviderProps<TRegistry extends {
|
|
|
163
162
|
* tooling (devtools, inspection) while exposing reactive signals for
|
|
164
163
|
* Infrastructure layer communication.
|
|
165
164
|
*
|
|
165
|
+
* **A subclass IS the actor.** Forward the logic *and* its options to
|
|
166
|
+
* `super(logic, options)`, then observe `this`. Holding a separately
|
|
167
|
+
* constructed actor alongside leaves this instance running as an empty second
|
|
168
|
+
* actor, and every inherited member — `system`, `sessionId`, `clock`, the
|
|
169
|
+
* internal `_send` that receives `sendTo()` traffic, and anything a future
|
|
170
|
+
* XState version adds — answers from that empty one until it is individually
|
|
171
|
+
* forwarded.
|
|
172
|
+
*
|
|
166
173
|
* @typeParam TLogic - XState actor logic type
|
|
167
174
|
* @typeParam TEvent - Event type constraint (defaults to EventObject)
|
|
168
175
|
*/
|
|
@@ -178,6 +185,14 @@ export declare abstract class AbstractActor<TLogic extends AnyActorLogic, TEvent
|
|
|
178
185
|
* Send event to Actor.
|
|
179
186
|
*
|
|
180
187
|
* Constrained to TEvent for type safety in concrete implementations.
|
|
188
|
+
*
|
|
189
|
+
* Note for implementations that wrap `send` (validating the event, or
|
|
190
|
+
* notifying hooks around it): this declaration is abstract purely to narrow
|
|
191
|
+
* the event type, and TypeScript forbids `super` calls to an abstract
|
|
192
|
+
* member. Reach XState's own implementation with
|
|
193
|
+
* `Actor.prototype.send.call(this, event)` instead. Making this concrete
|
|
194
|
+
* would allow `super.send()` but would force every existing subclass to add
|
|
195
|
+
* an `override` modifier — a breaking change for adapters outside this repo.
|
|
181
196
|
*/
|
|
182
197
|
abstract send(event: TEvent): void;
|
|
183
198
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abstract-actor.d.ts","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EACX,IAAI,EACJ,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,MAAM,6BAA6B,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED
|
|
1
|
+
{"version":3,"file":"abstract-actor.d.ts","sourceRoot":"","sources":["../src/abstract-actor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EACX,IAAI,EACJ,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,MAAM,6BAA6B,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAS,SAAQ,IAAI;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,QAAQ,CAEnE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;CACpD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB,CAAC,SAAS,SAAS,MAAM;IAC7D,sCAAsC;IACtC,IAAI,EAAE,QAAQ,CAAC;IACf,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxC,uDAAuD;IACvD,QAAQ,EAAE,SAAS,CAAC;IACpB,gHAAgH;IAChH,KAAK,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,sBAAsB,CACtC,SAAS,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAA;CAAE;IAE/E,6EAA6E;IAC7E,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,QAAQ,CAAC;IAC/C,uGAAuG;IACvG,cAAc,EAAE,SAAS,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,8BAAsB,aAAa,CAClC,MAAM,SAAS,aAAa,EAC5B,MAAM,SAAS,WAAW,GAAG,WAAW,CACvC,SAAQ,KAAK,CAAC,MAAM,CAAC;IACtB;;;;;OAKG;IACH,SAAgB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE7C;;;;;;;;;;;;OAYG;aACsB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAClD"}
|
package/dist/abstract-actor.js
CHANGED
|
@@ -18,30 +18,33 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { Actor } from "xstate";
|
|
20
20
|
/**
|
|
21
|
-
* Identity helper that
|
|
22
|
-
*
|
|
23
|
-
* autocomplete at the definition site.
|
|
21
|
+
* Identity helper that types a view spec literal as `PlaySpec` at the
|
|
22
|
+
* definition site, giving compile-time validation and IDE autocomplete.
|
|
24
23
|
*
|
|
25
24
|
* XState's `meta` field is typed as `Record<string, unknown>`, so TypeScript
|
|
26
|
-
* cannot infer the
|
|
27
|
-
*
|
|
25
|
+
* cannot infer the spec shape from context. `typedSpec(...)` is the opt-in
|
|
26
|
+
* mechanism that activates checking where the spec is written. `viewKey` is
|
|
27
|
+
* excluded from the parameter — derivation stamps it and would silently
|
|
28
|
+
* overwrite an authored value, so authoring one is rejected at compile time.
|
|
29
|
+
*
|
|
30
|
+
* Excess-property checking only applies to an inline object literal; for a
|
|
31
|
+
* spec built in a variable or through spreads, use `satisfies PlaySpec` at
|
|
32
|
+
* the literal instead.
|
|
28
33
|
*
|
|
29
34
|
* At runtime this is a no-op — the spec object is returned unchanged.
|
|
30
35
|
*
|
|
31
36
|
* @example
|
|
32
37
|
* ```ts
|
|
33
|
-
* interface DashboardCtx {
|
|
34
|
-
* username: string;
|
|
35
|
-
* params: Record<string, string>;
|
|
36
|
-
* query: Record<string, string>;
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
38
|
* meta: {
|
|
40
|
-
* view: typedSpec
|
|
39
|
+
* view: typedSpec({
|
|
41
40
|
* root: "root",
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* elements: {
|
|
42
|
+
* root: {
|
|
43
|
+
* type: "Dashboard",
|
|
44
|
+
* props: { username: { $state: "/context/username" } },
|
|
45
|
+
* children: [],
|
|
46
|
+
* },
|
|
47
|
+
* },
|
|
45
48
|
* }),
|
|
46
49
|
* }
|
|
47
50
|
* ```
|
|
@@ -56,6 +59,14 @@ export function typedSpec(spec) {
|
|
|
56
59
|
* tooling (devtools, inspection) while exposing reactive signals for
|
|
57
60
|
* Infrastructure layer communication.
|
|
58
61
|
*
|
|
62
|
+
* **A subclass IS the actor.** Forward the logic *and* its options to
|
|
63
|
+
* `super(logic, options)`, then observe `this`. Holding a separately
|
|
64
|
+
* constructed actor alongside leaves this instance running as an empty second
|
|
65
|
+
* actor, and every inherited member — `system`, `sessionId`, `clock`, the
|
|
66
|
+
* internal `_send` that receives `sendTo()` traffic, and anything a future
|
|
67
|
+
* XState version adds — answers from that empty one until it is individually
|
|
68
|
+
* forwarded.
|
|
69
|
+
*
|
|
59
70
|
* @typeParam TLogic - XState actor logic type
|
|
60
71
|
* @typeParam TEvent - Event type constraint (defaults to EventObject)
|
|
61
72
|
*/
|
|
@@ -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;
|
|
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;AAqCrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,SAAS,CAAC,IAA+B;IACxD,OAAO,IAAI,CAAC;AACb,CAAC;AAiFD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAgB,aAGpB,SAAQ,KAAa;CAuBtB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context projection — machine context as a read-only `/context` subtree of the
|
|
3
|
+
* view state store.
|
|
4
|
+
*
|
|
5
|
+
* `deriveCurrentView` composes each emitted view's `state` as
|
|
6
|
+
* `{ ...meta.view.state, context: <slice> }`, so the spec is self-consistent:
|
|
7
|
+
* its `state` honestly describes the store contents, and `repeat.statePath`,
|
|
8
|
+
* `visible.$state`, `$state` props and validators can all read machine context
|
|
9
|
+
* through the ordinary `{ $state: "/context/…" }` grammar.
|
|
10
|
+
*
|
|
11
|
+
* The subtree is read-only **to the spec, not to the machinery**: providers
|
|
12
|
+
* hand bindings and action handlers a store wrapped with
|
|
13
|
+
* {@link guardContextWrites}, while refreshing the projection through the
|
|
14
|
+
* unguarded store underneath. Context changes flow exclusively through machine
|
|
15
|
+
* events — a write under `/context` is always a spec bug, and the guard says so.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
import type { StateStore } from "@xmachines/json-render-core";
|
|
20
|
+
import type { PlaySpec } from "./abstract-actor.js";
|
|
21
|
+
/**
|
|
22
|
+
* The reserved top-level state key the projection materializes under.
|
|
23
|
+
*
|
|
24
|
+
* A view's authored `spec.state` must not declare this key — when it does, the
|
|
25
|
+
* projection is skipped for that view (the authored state wins) and a dev
|
|
26
|
+
* warning is emitted. See {@link composePlayState}.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CONTEXT_STATE_KEY = "context";
|
|
29
|
+
/**
|
|
30
|
+
* Own-key shallow equality with `Object.is`, optionally ignoring one key on
|
|
31
|
+
* both sides.
|
|
32
|
+
*
|
|
33
|
+
* The single comparison rule behind every emission-dedup decision: slice
|
|
34
|
+
* equality and the player's emit gate alike (spec-field comparison in
|
|
35
|
+
* `viewSpecsEquivalent`, composed-state reuse in {@link reuseComposedState}).
|
|
36
|
+
*/
|
|
37
|
+
export declare function shallowEqualExcept(a: object, b: object, except?: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Reuse the previous emission's composed `state` — or the whole previous spec
|
|
40
|
+
* — when the projection is value-unchanged.
|
|
41
|
+
*
|
|
42
|
+
* `deriveCurrentView` composes `state: { ...meta.view.state, context: slice }`
|
|
43
|
+
* fresh per call, and XState's `assign` produces a fresh context object on
|
|
44
|
+
* every event — so without reuse, every event would present a new `state`
|
|
45
|
+
* reference and the emit gate's per-field `Object.is` would re-emit (and
|
|
46
|
+
* remount) constantly. Slices are compared per field ({@link shallowEqualExcept} —
|
|
47
|
+
* XState's `assign` produces a fresh context object per event, so whole-object
|
|
48
|
+
* identity would report a change every time); the authored fields are spread
|
|
49
|
+
* from the static `meta.view.state`, so for an unchanged `viewKey` their
|
|
50
|
+
* references are stable and plain `Object.is` holds.
|
|
51
|
+
*
|
|
52
|
+
* Returns, in order of preference:
|
|
53
|
+
* - `prev` itself when nothing observable changed — the emit gate then
|
|
54
|
+
* short-circuits on reference identity with no element walk;
|
|
55
|
+
* - `{ ...next, state: prev.state }` when the state is value-unchanged but
|
|
56
|
+
* some other top-level field differs;
|
|
57
|
+
* - `next` when something actually changed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function reuseComposedState(prev: PlaySpec | null, next: PlaySpec | null): PlaySpec | null;
|
|
60
|
+
/**
|
|
61
|
+
* Wrap a StateStore so writes under `/context` are rejected.
|
|
62
|
+
*
|
|
63
|
+
* Applied by providers to the store they hand to bindings and action handlers
|
|
64
|
+
* (`$bindState`, `setState`, chained `set`). The provider keeps the unwrapped
|
|
65
|
+
* store and refreshes the projection through it — read-only to the spec, not
|
|
66
|
+
* to the machinery.
|
|
67
|
+
*
|
|
68
|
+
* A write whose value is **identical** to the current one passes silently:
|
|
69
|
+
* `setState`-style handlers read the full snapshot, transform it, and write
|
|
70
|
+
* the whole object back — the untouched `context` key flowing through that
|
|
71
|
+
* round-trip is not a mutation attempt. Only a write that would actually
|
|
72
|
+
* change the subtree throws.
|
|
73
|
+
*
|
|
74
|
+
* Reads pass through untouched, and every member is delegated explicitly
|
|
75
|
+
* rather than spread: a consumer-supplied store may be a class instance, whose
|
|
76
|
+
* methods live on the prototype and would not survive `{ ...store }` — the
|
|
77
|
+
* first render would die on `store.getSnapshot is not a function`. The wrapper
|
|
78
|
+
* is built once per resolved store, so the delegating `getSnapshot`/`subscribe`
|
|
79
|
+
* identities stay stable for `useSyncExternalStore`-style consumers.
|
|
80
|
+
*
|
|
81
|
+
* @param store - The underlying store.
|
|
82
|
+
* @returns A store with guarded `set`/`update`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function guardContextWrites(store: StateStore): StateStore;
|
|
85
|
+
/**
|
|
86
|
+
* Refresh a live store's `/context` subtree from a derived view's composed
|
|
87
|
+
* state. A same-`viewKey` emission means only the projection changed — the
|
|
88
|
+
* subtree is replaced wholesale (never merged per-field: `update` cannot
|
|
89
|
+
* delete keys) and every ephemeral root-level value is left untouched.
|
|
90
|
+
* No-op when the view carries no slice or the store already holds it.
|
|
91
|
+
*
|
|
92
|
+
* @param store - The UNGUARDED store (providers refresh through it).
|
|
93
|
+
* @param view - The derived view whose `state.context` carries the slice.
|
|
94
|
+
*/
|
|
95
|
+
export declare function refreshContextSubtree(store: StateStore, view: {
|
|
96
|
+
state?: unknown;
|
|
97
|
+
}): void;
|
|
98
|
+
/**
|
|
99
|
+
* Compose a view's effective state: the authored `spec.state` plus the
|
|
100
|
+
* `/context` slice.
|
|
101
|
+
*
|
|
102
|
+
* When the authored state already declares the reserved key, the projection is
|
|
103
|
+
* skipped (authored state wins) with a dev warning — no existing spec breaks.
|
|
104
|
+
*
|
|
105
|
+
* @param authoredState - The raw `meta.view.state` (may be anything; sanitized by the caller/toAtomState).
|
|
106
|
+
* @param slice - The machine's context, projected wholesale.
|
|
107
|
+
*/
|
|
108
|
+
export declare function composePlayState(authoredState: Record<string, unknown> | undefined, slice: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
109
|
+
//# 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;;;;;;;;;;;;;;;;;GAiBG;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;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAUjF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,CAiBhG;AAgBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CA6ChE;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAKxF;AAED;;;;;;;;;GASG;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,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context projection — machine context as a read-only `/context` subtree of the
|
|
3
|
+
* view state store.
|
|
4
|
+
*
|
|
5
|
+
* `deriveCurrentView` composes each emitted view's `state` as
|
|
6
|
+
* `{ ...meta.view.state, context: <slice> }`, so the spec is self-consistent:
|
|
7
|
+
* its `state` honestly describes the store contents, and `repeat.statePath`,
|
|
8
|
+
* `visible.$state`, `$state` props and validators can all read machine context
|
|
9
|
+
* through the ordinary `{ $state: "/context/…" }` grammar.
|
|
10
|
+
*
|
|
11
|
+
* The subtree is read-only **to the spec, not to the machinery**: providers
|
|
12
|
+
* hand bindings and action handlers a store wrapped with
|
|
13
|
+
* {@link guardContextWrites}, while refreshing the projection through the
|
|
14
|
+
* unguarded store underneath. Context changes flow exclusively through machine
|
|
15
|
+
* events — a write under `/context` is always a spec bug, and the guard says so.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The reserved top-level state key the projection materializes under.
|
|
21
|
+
*
|
|
22
|
+
* A view's authored `spec.state` must not declare this key — when it does, the
|
|
23
|
+
* projection is skipped for that view (the authored state wins) and a dev
|
|
24
|
+
* warning is emitted. See {@link composePlayState}.
|
|
25
|
+
*/
|
|
26
|
+
export const CONTEXT_STATE_KEY = "context";
|
|
27
|
+
/**
|
|
28
|
+
* Once-per-spec diagnostic dedup. The condition reported here is a static
|
|
29
|
+
* property of the authored meta.view (its `state` object never changes
|
|
30
|
+
* between events), but `deriveCurrentView` runs on every machine snapshot —
|
|
31
|
+
* reporting per derivation would flood the console at event rate. Keyed by
|
|
32
|
+
* the static object's identity, so each spec reports exactly once and tests
|
|
33
|
+
* with fresh literals stay isolated.
|
|
34
|
+
*/
|
|
35
|
+
const reportedDiagnostics = new WeakSet();
|
|
36
|
+
function warnOnce(anchor, message) {
|
|
37
|
+
if (reportedDiagnostics.has(anchor))
|
|
38
|
+
return;
|
|
39
|
+
reportedDiagnostics.add(anchor);
|
|
40
|
+
console.warn(message);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Own-key shallow equality with `Object.is`, optionally ignoring one key on
|
|
44
|
+
* both sides.
|
|
45
|
+
*
|
|
46
|
+
* The single comparison rule behind every emission-dedup decision: slice
|
|
47
|
+
* equality and the player's emit gate alike (spec-field comparison in
|
|
48
|
+
* `viewSpecsEquivalent`, composed-state reuse in {@link reuseComposedState}).
|
|
49
|
+
*/
|
|
50
|
+
export function shallowEqualExcept(a, b, except) {
|
|
51
|
+
const aEntries = Object.entries(a).filter(([key]) => key !== except);
|
|
52
|
+
const bKeyCount = Object.keys(b).filter((key) => key !== except).length;
|
|
53
|
+
if (aEntries.length !== bKeyCount)
|
|
54
|
+
return false;
|
|
55
|
+
const bRecord = b;
|
|
56
|
+
for (const [key, value] of aEntries) {
|
|
57
|
+
// nosemgrep: gitlab.eslint.detect-object-injection
|
|
58
|
+
if (!Object.hasOwn(b, key) || !Object.is(value, bRecord[key]))
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reuse the previous emission's composed `state` — or the whole previous spec
|
|
65
|
+
* — when the projection is value-unchanged.
|
|
66
|
+
*
|
|
67
|
+
* `deriveCurrentView` composes `state: { ...meta.view.state, context: slice }`
|
|
68
|
+
* fresh per call, and XState's `assign` produces a fresh context object on
|
|
69
|
+
* every event — so without reuse, every event would present a new `state`
|
|
70
|
+
* reference and the emit gate's per-field `Object.is` would re-emit (and
|
|
71
|
+
* remount) constantly. Slices are compared per field ({@link shallowEqualExcept} —
|
|
72
|
+
* XState's `assign` produces a fresh context object per event, so whole-object
|
|
73
|
+
* identity would report a change every time); the authored fields are spread
|
|
74
|
+
* from the static `meta.view.state`, so for an unchanged `viewKey` their
|
|
75
|
+
* references are stable and plain `Object.is` holds.
|
|
76
|
+
*
|
|
77
|
+
* Returns, in order of preference:
|
|
78
|
+
* - `prev` itself when nothing observable changed — the emit gate then
|
|
79
|
+
* short-circuits on reference identity with no element walk;
|
|
80
|
+
* - `{ ...next, state: prev.state }` when the state is value-unchanged but
|
|
81
|
+
* some other top-level field differs;
|
|
82
|
+
* - `next` when something actually changed.
|
|
83
|
+
*/
|
|
84
|
+
export function reuseComposedState(prev, next) {
|
|
85
|
+
if (!prev || !next || prev.viewKey !== next.viewKey)
|
|
86
|
+
return next;
|
|
87
|
+
const prevState = prev.state;
|
|
88
|
+
const nextState = next.state;
|
|
89
|
+
if (prevState !== nextState) {
|
|
90
|
+
if (prevState === undefined || nextState === undefined)
|
|
91
|
+
return next;
|
|
92
|
+
if (!shallowEqualExcept(prevState, nextState, CONTEXT_STATE_KEY))
|
|
93
|
+
return next;
|
|
94
|
+
const prevSlice = prevState[CONTEXT_STATE_KEY];
|
|
95
|
+
const nextSlice = nextState[CONTEXT_STATE_KEY];
|
|
96
|
+
if (prevSlice !== nextSlice) {
|
|
97
|
+
if (prevSlice === undefined || nextSlice === undefined)
|
|
98
|
+
return next;
|
|
99
|
+
if (!shallowEqualExcept(prevSlice, nextSlice))
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
if (shallowEqualExcept(prev, next, "state"))
|
|
103
|
+
return prev;
|
|
104
|
+
return { ...next, state: prevState };
|
|
105
|
+
}
|
|
106
|
+
return shallowEqualExcept(prev, next, "state") ? prev : next;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Paths that address the reserved subtree: "/context" itself or below it.
|
|
110
|
+
* Store update maps may carry bare top-level keys ("context") as well as
|
|
111
|
+
* JSON Pointers ("/context/…") — both forms address the same subtree.
|
|
112
|
+
*/
|
|
113
|
+
function toPointer(path) {
|
|
114
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
115
|
+
}
|
|
116
|
+
/** Expects a NORMALIZED pointer from {@link toPointer}. */
|
|
117
|
+
function isContextPointer(pointer) {
|
|
118
|
+
return pointer === `/${CONTEXT_STATE_KEY}` || pointer.startsWith(`/${CONTEXT_STATE_KEY}/`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Wrap a StateStore so writes under `/context` are rejected.
|
|
122
|
+
*
|
|
123
|
+
* Applied by providers to the store they hand to bindings and action handlers
|
|
124
|
+
* (`$bindState`, `setState`, chained `set`). The provider keeps the unwrapped
|
|
125
|
+
* store and refreshes the projection through it — read-only to the spec, not
|
|
126
|
+
* to the machinery.
|
|
127
|
+
*
|
|
128
|
+
* A write whose value is **identical** to the current one passes silently:
|
|
129
|
+
* `setState`-style handlers read the full snapshot, transform it, and write
|
|
130
|
+
* the whole object back — the untouched `context` key flowing through that
|
|
131
|
+
* round-trip is not a mutation attempt. Only a write that would actually
|
|
132
|
+
* change the subtree throws.
|
|
133
|
+
*
|
|
134
|
+
* Reads pass through untouched, and every member is delegated explicitly
|
|
135
|
+
* rather than spread: a consumer-supplied store may be a class instance, whose
|
|
136
|
+
* methods live on the prototype and would not survive `{ ...store }` — the
|
|
137
|
+
* first render would die on `store.getSnapshot is not a function`. The wrapper
|
|
138
|
+
* is built once per resolved store, so the delegating `getSnapshot`/`subscribe`
|
|
139
|
+
* identities stay stable for `useSyncExternalStore`-style consumers.
|
|
140
|
+
*
|
|
141
|
+
* @param store - The underlying store.
|
|
142
|
+
* @returns A store with guarded `set`/`update`.
|
|
143
|
+
*/
|
|
144
|
+
export function guardContextWrites(store) {
|
|
145
|
+
const reject = (path) => {
|
|
146
|
+
throw new Error(`[play-actor] "${path}" is read-only: /${CONTEXT_STATE_KEY} mirrors the machine's context. ` +
|
|
147
|
+
`Send the actor an event to change it — context never changes through the view store.`);
|
|
148
|
+
};
|
|
149
|
+
const checkWrite = (path, value) => {
|
|
150
|
+
const pointer = toPointer(path);
|
|
151
|
+
if (!isContextPointer(pointer))
|
|
152
|
+
return true;
|
|
153
|
+
// Unchanged round-trip (read-modify-write of the whole snapshot) is a
|
|
154
|
+
// no-op for this subtree, not a mutation — drop the key silently.
|
|
155
|
+
if (Object.is(store.get(pointer), value))
|
|
156
|
+
return false;
|
|
157
|
+
return reject(path);
|
|
158
|
+
};
|
|
159
|
+
const guarded = {
|
|
160
|
+
get: (path) => store.get(path),
|
|
161
|
+
getSnapshot: () => store.getSnapshot(),
|
|
162
|
+
subscribe: (listener) => store.subscribe(listener),
|
|
163
|
+
set: (path, value) => {
|
|
164
|
+
if (checkWrite(path, value))
|
|
165
|
+
store.set(path, value);
|
|
166
|
+
},
|
|
167
|
+
update: (updates) => {
|
|
168
|
+
const allowed = {};
|
|
169
|
+
let dropped = false;
|
|
170
|
+
for (const [path, value] of Object.entries(updates)) {
|
|
171
|
+
if (checkWrite(path, value)) {
|
|
172
|
+
allowed[path] = value; // nosemgrep: gitlab.eslint.detect-object-injection
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
dropped = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
store.update(dropped ? allowed : updates);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
// Optional in the contract, and its absence is meaningful: json-render-core
|
|
182
|
+
// falls back to `getSnapshot` when the key is missing, so the wrapper must
|
|
183
|
+
// not manufacture one that returns `undefined`.
|
|
184
|
+
const { getServerSnapshot } = store;
|
|
185
|
+
if (getServerSnapshot) {
|
|
186
|
+
guarded.getServerSnapshot = () => getServerSnapshot.call(store);
|
|
187
|
+
}
|
|
188
|
+
return guarded;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Refresh a live store's `/context` subtree from a derived view's composed
|
|
192
|
+
* state. A same-`viewKey` emission means only the projection changed — the
|
|
193
|
+
* subtree is replaced wholesale (never merged per-field: `update` cannot
|
|
194
|
+
* delete keys) and every ephemeral root-level value is left untouched.
|
|
195
|
+
* No-op when the view carries no slice or the store already holds it.
|
|
196
|
+
*
|
|
197
|
+
* @param store - The UNGUARDED store (providers refresh through it).
|
|
198
|
+
* @param view - The derived view whose `state.context` carries the slice.
|
|
199
|
+
*/
|
|
200
|
+
export function refreshContextSubtree(store, view) {
|
|
201
|
+
const slice = view.state?.[CONTEXT_STATE_KEY];
|
|
202
|
+
if (slice !== undefined && !Object.is(store.get(`/${CONTEXT_STATE_KEY}`), slice)) {
|
|
203
|
+
store.update({ [`/${CONTEXT_STATE_KEY}`]: slice });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Compose a view's effective state: the authored `spec.state` plus the
|
|
208
|
+
* `/context` slice.
|
|
209
|
+
*
|
|
210
|
+
* When the authored state already declares the reserved key, the projection is
|
|
211
|
+
* skipped (authored state wins) with a dev warning — no existing spec breaks.
|
|
212
|
+
*
|
|
213
|
+
* @param authoredState - The raw `meta.view.state` (may be anything; sanitized by the caller/toAtomState).
|
|
214
|
+
* @param slice - The machine's context, projected wholesale.
|
|
215
|
+
*/
|
|
216
|
+
export function composePlayState(authoredState, slice) {
|
|
217
|
+
if (authoredState !== undefined && Object.hasOwn(authoredState, CONTEXT_STATE_KEY)) {
|
|
218
|
+
warnOnce(authoredState, `[play-actor] spec.state declares a top-level "${CONTEXT_STATE_KEY}" key; ` +
|
|
219
|
+
`skipping the machine-context projection for this view. ` +
|
|
220
|
+
`Rename the state field to project context at /${CONTEXT_STATE_KEY}.`);
|
|
221
|
+
return authoredState;
|
|
222
|
+
}
|
|
223
|
+
if (slice === undefined) {
|
|
224
|
+
return authoredState;
|
|
225
|
+
}
|
|
226
|
+
return { ...authoredState, [CONTEXT_STATE_KEY]: slice };
|
|
227
|
+
}
|
|
228
|
+
//# 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
|
@@ -17,4 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export { AbstractActor, typedSpec, type Routable, type Viewable, type PlaySpec, type BaseActorProviderProps, type BaseViewContextValue, } from "./abstract-actor.js";
|
|
19
19
|
export { toAtomState, attachRenderErrorHandler } from "./provider-guards.js";
|
|
20
|
+
export { CONTEXT_STATE_KEY, guardContextWrites, refreshContextSubtree, composePlayState, shallowEqualExcept, reuseComposedState, } from "./context-projection.js";
|
|
21
|
+
export { createViewStoreLifecycle } from "./view-store-lifecycle.js";
|
|
22
|
+
export type { ViewStoreLifecycle, ViewStoreResolution, ResolveViewStoreOptions, } from "./view-store-lifecycle.js";
|
|
20
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;;;;;;;;;;;;;;;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;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
|
@@ -17,4 +17,6 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export { AbstractActor, typedSpec, } from "./abstract-actor.js";
|
|
19
19
|
export { toAtomState, attachRenderErrorHandler } from "./provider-guards.js";
|
|
20
|
+
export { CONTEXT_STATE_KEY, guardContextWrites, refreshContextSubtree, composePlayState, shallowEqualExcept, reuseComposedState, } from "./context-projection.js";
|
|
21
|
+
export { createViewStoreLifecycle } from "./view-store-lifecycle.js";
|
|
20
22
|
//# 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;;;;;;;;;;;;;;;;GAgBG;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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View-store lifecycle — the framework-agnostic reseed-vs-refresh decision
|
|
3
|
+
* every provider wires into its own reactivity.
|
|
4
|
+
*
|
|
5
|
+
* The policy in one place (previously hand-copied per renderer, where the
|
|
6
|
+
* copies drifted into real bugs):
|
|
7
|
+
*
|
|
8
|
+
* - `viewKey` changed (or first resolve, or actor swapped) → RESEED: a fresh
|
|
9
|
+
* store from the view's composed state.
|
|
10
|
+
* - `viewKey` unchanged → REFRESH: only the /context projection moved, so the
|
|
11
|
+
* `/context` subtree is replaced in place and ephemeral root-level state
|
|
12
|
+
* (drafts, toggles) survives.
|
|
13
|
+
* - No `viewKey` (a hand-built Viewable that does not stamp one) → reseed per
|
|
14
|
+
* EMISSION, the safe pre-viewKey behavior.
|
|
15
|
+
* - Actor swap → the kept-alive store dies with its actor (lifetime rule).
|
|
16
|
+
* - Controlled mode (caller-supplied store) → the caller owns seeding and
|
|
17
|
+
* lifecycle, but /context is machinery-owned in both modes and is refreshed
|
|
18
|
+
* here — unless the caller defers it ({@link ResolveViewStoreOptions}),
|
|
19
|
+
* which React's render path needs (store subscribers must not be notified
|
|
20
|
+
* mid-render; its effects refresh instead).
|
|
21
|
+
*
|
|
22
|
+
* What stays in each provider is only reactivity wiring: WHEN to resolve, and
|
|
23
|
+
* how the result reaches children.
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
import type { StateStore } from "@xmachines/json-render-core";
|
|
28
|
+
import type { PlaySpec } from "./abstract-actor.js";
|
|
29
|
+
/** What a resolve produced — see {@link ViewStoreLifecycle.resolve}. */
|
|
30
|
+
export interface ViewStoreResolution {
|
|
31
|
+
/** The UNGUARDED store — the machinery's own refresh reference. */
|
|
32
|
+
store: StateStore;
|
|
33
|
+
/**
|
|
34
|
+
* The store to hand children ($bindState, setState, chained set): writes
|
|
35
|
+
* under /context throw. One wrapper per underlying store — the identity is
|
|
36
|
+
* cached so `useSyncExternalStore`-style consumers stay stable.
|
|
37
|
+
*/
|
|
38
|
+
guardedStore: StateStore;
|
|
39
|
+
/**
|
|
40
|
+
* True when this resolve created a fresh store (first resolve, viewKey
|
|
41
|
+
* change, per-emission fallback, actor swap). Providers that remount a
|
|
42
|
+
* subtree per store (Vue's storeKey) key on it.
|
|
43
|
+
*/
|
|
44
|
+
reseeded: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface ResolveViewStoreOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Controlled mode only: refresh the external store's /context during this
|
|
49
|
+
* resolve (default true). Pass false where notifying store subscribers is
|
|
50
|
+
* not allowed at the call site (React's render path) and refresh from an
|
|
51
|
+
* effect instead.
|
|
52
|
+
*/
|
|
53
|
+
refreshExternalStore?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface ViewStoreLifecycle {
|
|
56
|
+
/**
|
|
57
|
+
* Bring the store in line with an emission and return it (with its guard).
|
|
58
|
+
*
|
|
59
|
+
* @param actor - The actor the emission came from — a swap drops the kept store.
|
|
60
|
+
* @param view - The derived view (non-null; a null emission is a GAP, not a
|
|
61
|
+
* new view — callers simply do not resolve on null, keeping the store).
|
|
62
|
+
* @param externalStore - Controlled mode: the caller-owned store.
|
|
63
|
+
* @param options - See {@link ResolveViewStoreOptions}.
|
|
64
|
+
*/
|
|
65
|
+
resolve(actor: unknown, view: PlaySpec, externalStore?: StateStore | undefined, options?: ResolveViewStoreOptions): ViewStoreResolution;
|
|
66
|
+
/** Drop everything (unmount/disconnect) — the next resolve reseeds. */
|
|
67
|
+
reset(): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a lifecycle coordinator.
|
|
71
|
+
*
|
|
72
|
+
* @param createStore - Framework-supplied store factory; receives the
|
|
73
|
+
* proto-safe seed (`toAtomState(view.state)` — the composed state already
|
|
74
|
+
* carries /context).
|
|
75
|
+
*/
|
|
76
|
+
export declare function createViewStoreLifecycle(createStore: (seed: Record<string, unknown>) => StateStore): ViewStoreLifecycle;
|
|
77
|
+
//# sourceMappingURL=view-store-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-store-lifecycle.d.ts","sourceRoot":"","sources":["../src/view-store-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IACnC,mEAAmE;IACnE,KAAK,EAAE,UAAU,CAAC;IAClB;;;;OAIG;IACH,YAAY,EAAE,UAAU,CAAC;IACzB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACvC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;OAQG;IACH,OAAO,CACN,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,QAAQ,EACd,aAAa,CAAC,EAAE,UAAU,GAAG,SAAS,EACtC,OAAO,CAAC,EAAE,uBAAuB,GAC/B,mBAAmB,CAAC;IACvB,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,UAAU,GACxD,kBAAkB,CA4DpB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View-store lifecycle — the framework-agnostic reseed-vs-refresh decision
|
|
3
|
+
* every provider wires into its own reactivity.
|
|
4
|
+
*
|
|
5
|
+
* The policy in one place (previously hand-copied per renderer, where the
|
|
6
|
+
* copies drifted into real bugs):
|
|
7
|
+
*
|
|
8
|
+
* - `viewKey` changed (or first resolve, or actor swapped) → RESEED: a fresh
|
|
9
|
+
* store from the view's composed state.
|
|
10
|
+
* - `viewKey` unchanged → REFRESH: only the /context projection moved, so the
|
|
11
|
+
* `/context` subtree is replaced in place and ephemeral root-level state
|
|
12
|
+
* (drafts, toggles) survives.
|
|
13
|
+
* - No `viewKey` (a hand-built Viewable that does not stamp one) → reseed per
|
|
14
|
+
* EMISSION, the safe pre-viewKey behavior.
|
|
15
|
+
* - Actor swap → the kept-alive store dies with its actor (lifetime rule).
|
|
16
|
+
* - Controlled mode (caller-supplied store) → the caller owns seeding and
|
|
17
|
+
* lifecycle, but /context is machinery-owned in both modes and is refreshed
|
|
18
|
+
* here — unless the caller defers it ({@link ResolveViewStoreOptions}),
|
|
19
|
+
* which React's render path needs (store subscribers must not be notified
|
|
20
|
+
* mid-render; its effects refresh instead).
|
|
21
|
+
*
|
|
22
|
+
* What stays in each provider is only reactivity wiring: WHEN to resolve, and
|
|
23
|
+
* how the result reaches children.
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
import { guardContextWrites, refreshContextSubtree } from "./context-projection.js";
|
|
28
|
+
import { toAtomState } from "./provider-guards.js";
|
|
29
|
+
/**
|
|
30
|
+
* Create a lifecycle coordinator.
|
|
31
|
+
*
|
|
32
|
+
* @param createStore - Framework-supplied store factory; receives the
|
|
33
|
+
* proto-safe seed (`toAtomState(view.state)` — the composed state already
|
|
34
|
+
* carries /context).
|
|
35
|
+
*/
|
|
36
|
+
export function createViewStoreLifecycle(createStore) {
|
|
37
|
+
let internalStore = null;
|
|
38
|
+
let lastViewKey = undefined;
|
|
39
|
+
let lastView = null;
|
|
40
|
+
let lastActor = null;
|
|
41
|
+
// Guard identity cache — one wrapper per underlying store.
|
|
42
|
+
let guardedSource = null;
|
|
43
|
+
let guardedStore = null;
|
|
44
|
+
const reset = () => {
|
|
45
|
+
internalStore = null;
|
|
46
|
+
lastViewKey = undefined;
|
|
47
|
+
lastView = null;
|
|
48
|
+
lastActor = null;
|
|
49
|
+
guardedSource = null;
|
|
50
|
+
guardedStore = null;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
reset,
|
|
54
|
+
resolve(actor, view, externalStore, options) {
|
|
55
|
+
// Lifetime rule: a kept-alive store must not outlive its actor.
|
|
56
|
+
if (lastActor !== actor) {
|
|
57
|
+
const firstResolve = lastActor === null && internalStore === null;
|
|
58
|
+
if (!firstResolve)
|
|
59
|
+
reset();
|
|
60
|
+
lastActor = actor;
|
|
61
|
+
}
|
|
62
|
+
let resolved;
|
|
63
|
+
let reseeded = false;
|
|
64
|
+
if (externalStore) {
|
|
65
|
+
resolved = externalStore;
|
|
66
|
+
if (options?.refreshExternalStore !== false) {
|
|
67
|
+
refreshContextSubtree(resolved, view);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const viewKey = view.viewKey;
|
|
72
|
+
let nextStore = internalStore;
|
|
73
|
+
if (nextStore === null ||
|
|
74
|
+
(viewKey !== undefined ? lastViewKey !== viewKey : lastView !== view)) {
|
|
75
|
+
nextStore = createStore(toAtomState(view.state));
|
|
76
|
+
internalStore = nextStore;
|
|
77
|
+
lastViewKey = viewKey;
|
|
78
|
+
reseeded = true;
|
|
79
|
+
}
|
|
80
|
+
else if (viewKey !== undefined) {
|
|
81
|
+
refreshContextSubtree(nextStore, view);
|
|
82
|
+
}
|
|
83
|
+
lastView = view;
|
|
84
|
+
resolved = nextStore;
|
|
85
|
+
}
|
|
86
|
+
if (guardedSource !== resolved) {
|
|
87
|
+
guardedSource = resolved;
|
|
88
|
+
guardedStore = guardContextWrites(resolved);
|
|
89
|
+
}
|
|
90
|
+
return { store: resolved, guardedStore: guardedStore, reseeded };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=view-store-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-store-lifecycle.js","sourceRoot":"","sources":["../src/view-store-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAkDnD;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACvC,WAA0D;IAE1D,IAAI,aAAa,GAAsB,IAAI,CAAC;IAC5C,IAAI,WAAW,GAAuB,SAAS,CAAC;IAChD,IAAI,QAAQ,GAAoB,IAAI,CAAC;IACrC,IAAI,SAAS,GAAY,IAAI,CAAC;IAC9B,2DAA2D;IAC3D,IAAI,aAAa,GAAsB,IAAI,CAAC;IAC5C,IAAI,YAAY,GAAsB,IAAI,CAAC;IAE3C,MAAM,KAAK,GAAG,GAAS,EAAE;QACxB,aAAa,GAAG,IAAI,CAAC;QACrB,WAAW,GAAG,SAAS,CAAC;QACxB,QAAQ,GAAG,IAAI,CAAC;QAChB,SAAS,GAAG,IAAI,CAAC;QACjB,aAAa,GAAG,IAAI,CAAC;QACrB,YAAY,GAAG,IAAI,CAAC;IACrB,CAAC,CAAC;IAEF,OAAO;QACN,KAAK;QACL,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO;YAC1C,gEAAgE;YAChE,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;gBACzB,MAAM,YAAY,GAAG,SAAS,KAAK,IAAI,IAAI,aAAa,KAAK,IAAI,CAAC;gBAClE,IAAI,CAAC,YAAY;oBAAE,KAAK,EAAE,CAAC;gBAC3B,SAAS,GAAG,KAAK,CAAC;YACnB,CAAC;YAED,IAAI,QAAoB,CAAC;YACzB,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,IAAI,aAAa,EAAE,CAAC;gBACnB,QAAQ,GAAG,aAAa,CAAC;gBACzB,IAAI,OAAO,EAAE,oBAAoB,KAAK,KAAK,EAAE,CAAC;oBAC7C,qBAAqB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;gBACvC,CAAC;YACF,CAAC;iBAAM,CAAC;gBACP,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;gBAC7B,IAAI,SAAS,GAAsB,aAAa,CAAC;gBACjD,IACC,SAAS,KAAK,IAAI;oBAClB,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,EACpE,CAAC;oBACF,SAAS,GAAG,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;oBACjD,aAAa,GAAG,SAAS,CAAC;oBAC1B,WAAW,GAAG,OAAO,CAAC;oBACtB,QAAQ,GAAG,IAAI,CAAC;gBACjB,CAAC;qBAAM,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;oBAClC,qBAAqB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;gBACxC,CAAC;gBACD,QAAQ,GAAG,IAAI,CAAC;gBAChB,QAAQ,GAAG,SAAS,CAAC;YACtB,CAAC;YAED,IAAI,aAAa,KAAK,QAAQ,EAAE,CAAC;gBAChC,aAAa,GAAG,QAAQ,CAAC;gBACzB,YAAY,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAC7C,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,YAA0B,EAAE,QAAQ,EAAE,CAAC;QAChF,CAAC;KACD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-actor",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Abstract Actor base class for XMachines Play Architecture",
|
|
6
6
|
"keywords": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "XMachines Contributors",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "git+
|
|
17
|
+
"url": "git+https://gitlab.com/xmachin-es/xmachines-js.git",
|
|
18
18
|
"directory": "packages/play-actor"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
@@ -23,45 +23,46 @@
|
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
25
|
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
26
29
|
"exports": {
|
|
27
30
|
".": {
|
|
28
|
-
"source": "./src/index.ts",
|
|
29
31
|
"types": "./dist/index.d.ts",
|
|
30
|
-
"
|
|
31
|
-
}
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
32
35
|
},
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public"
|
|
35
38
|
},
|
|
36
39
|
"scripts": {
|
|
37
|
-
"build": "tsc --build",
|
|
40
|
+
"build": "vite build && tsc --build",
|
|
38
41
|
"clean": "rm -rf dist *.tsbuildinfo coverage node_modules/.svelte2tsx-* node_modules/.vite*",
|
|
39
42
|
"test": "vitest",
|
|
40
43
|
"lint": "oxlint .",
|
|
41
44
|
"lint:fix": "oxlint --fix .",
|
|
42
45
|
"format": "oxfmt .",
|
|
43
|
-
"format:check": "oxfmt --check ."
|
|
44
|
-
"prepublishOnly": "npm run build"
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@xmachines/json-render-core": "^0.19.0-xm.2"
|
|
46
|
+
"format:check": "oxfmt --check ."
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
50
49
|
"@testing-library/jest-dom": "^6.9.1",
|
|
51
|
-
"@types/node": "^26.
|
|
52
|
-
"@xmachines/
|
|
53
|
-
"oxfmt": "^0.
|
|
54
|
-
"oxlint": "^1.
|
|
55
|
-
"
|
|
56
|
-
"
|
|
50
|
+
"@types/node": "^26.2.0",
|
|
51
|
+
"@xmachines/json-render-core": "^0.20.0-xm.2",
|
|
52
|
+
"oxfmt": "^0.64.0",
|
|
53
|
+
"oxlint": "^1.79.0",
|
|
54
|
+
"vite": "^8.0.10",
|
|
55
|
+
"vitest": "^4.1.11",
|
|
56
|
+
"xstate": "^5.31.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@xmachines/
|
|
60
|
-
"@xmachines/play
|
|
61
|
-
"
|
|
59
|
+
"@xmachines/json-render-core": "^0.20.0-xm.2",
|
|
60
|
+
"@xmachines/play": "2.0.0",
|
|
61
|
+
"@xmachines/play-signals": "2.0.0",
|
|
62
|
+
"xstate": "^5.31.0"
|
|
62
63
|
},
|
|
63
64
|
"engines": {
|
|
64
65
|
"node": ">=22.0.0"
|
|
65
66
|
},
|
|
66
|
-
"_devDependencies_note": "xstate appears in both peerDependencies and devDependencies intentionally. devDependencies provides workspace resolution for local builds and tests. peerDependencies declares the consumer version constraint. Both are pinned to ^
|
|
67
|
+
"_devDependencies_note": "xstate appears in both peerDependencies and devDependencies intentionally. devDependencies provides workspace resolution for local builds and tests. peerDependencies declares the consumer version constraint. Both are pinned to ^5.30.0 to prevent drift."
|
|
67
68
|
}
|