@xmachines/play-react 1.0.0-beta.53 → 1.0.0-beta.54
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 +6 -6
- package/dist/ActorProvider.d.ts +1 -1
- package/dist/ActorProvider.d.ts.map +1 -1
- package/dist/ActorProvider.js +30 -31
- package/dist/ActorProvider.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/useSignalEffect.d.ts +30 -21
- package/dist/useSignalEffect.d.ts.map +1 -1
- package/dist/useSignalEffect.js +43 -53
- package/dist/useSignalEffect.js.map +1 -1
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ function MyComponent({ actor }) {
|
|
|
108
108
|
|
|
109
109
|
useSignalEffect(() => {
|
|
110
110
|
setView(actor.currentView.get());
|
|
111
|
-
});
|
|
111
|
+
}, [actor]); // deps: re-subscribe when the actor prop swaps
|
|
112
112
|
|
|
113
113
|
return <div>{view?.component}</div>;
|
|
114
114
|
}
|
|
@@ -127,11 +127,11 @@ function MyComponent({ actor }) {
|
|
|
127
127
|
|
|
128
128
|
### Hooks
|
|
129
129
|
|
|
130
|
-
| Export
|
|
131
|
-
|
|
|
132
|
-
| `useSignalEffect(callback)` | Subscribes to TC39 signal changes; re-runs the callback
|
|
133
|
-
| `useActor()`
|
|
134
|
-
| `usePlayView()`
|
|
130
|
+
| Export | Description |
|
|
131
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
132
|
+
| `useSignalEffect(callback, deps?)` | Subscribes to TC39 signal changes; re-runs the callback when any accessed signal changes (re-render via the callback's own setState). Optional `deps` recreates the subscription, like `useEffect`. Cleanup is automatic on unmount. |
|
|
133
|
+
| `useActor()` | Returns the raw actor instance. Must be called inside an `ActorProvider`/`PlayUIProvider` tree. |
|
|
134
|
+
| `usePlayView()` | Returns `{ spec, handlers, registry, store }` for the current view. Must be called inside an `ActorProvider`/`PlayUIProvider` tree. |
|
|
135
135
|
|
|
136
136
|
### Types
|
|
137
137
|
|
package/dist/ActorProvider.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import React from "react";
|
|
13
13
|
import type { DefineRegistryResult, ComponentRegistry } from "@json-render/react";
|
|
14
|
-
import type
|
|
14
|
+
import { type BaseActorProviderProps, type BaseViewContextValue } from "@xmachines/play-actor";
|
|
15
15
|
/**
|
|
16
16
|
* Props for the ActorProvider component.
|
|
17
17
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActorProvider.d.ts","sourceRoot":"","sources":["../src/ActorProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAA+D,MAAM,OAAO,CAAC;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAAY,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5F,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"ActorProvider.d.ts","sourceRoot":"","sources":["../src/ActorProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAA+D,MAAM,OAAO,CAAC;AAEpF,OAAO,KAAK,EAAE,oBAAoB,EAAY,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5F,OAAO,EAIN,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,MAAM,uBAAuB,CAAC;AAI/B;;;;GAIG;AACH,MAAM,WAAW,kBAAmB,SAAQ,sBAAsB,CAAC,oBAAoB,CAAC;IACvF,sFAAsF;IACtF,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,4FAA4F;IAC5F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IACxD,0DAA0D;IAC1D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAiB,SAAQ,oBAAoB,CAAC,iBAAiB,CAAC;CAAG;AAQpF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAE9C;AA0ED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqFtD,CAAC"}
|
package/dist/ActorProvider.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* ActorProvider — escape hatch primitive for actor lifecycle management.
|
|
4
4
|
*
|
|
@@ -17,6 +17,7 @@ import { xstateStoreStateStore } from "@json-render/xstate";
|
|
|
17
17
|
import { useSignalEffect } from "./useSignalEffect.js";
|
|
18
18
|
import { PlayErrorBoundary } from "./PlayErrorBoundary.js";
|
|
19
19
|
import { assertNonNullable } from "@xmachines/play";
|
|
20
|
+
import { toAtomState, attachRenderErrorHandler, } from "@xmachines/play-actor";
|
|
20
21
|
import { ActorContext } from "./useActor.js";
|
|
21
22
|
/**
|
|
22
23
|
* Internal React context for ViewContextValue.
|
|
@@ -79,12 +80,15 @@ function ActorProviderInner({ registryResult, spec, store, children, }) {
|
|
|
79
80
|
// recreated when the registry definition itself changes (e.g. a new defineRegistry
|
|
80
81
|
// call), not on every render cycle.
|
|
81
82
|
const handlers = useMemo(() => registryResult.handlers(() => setStateAdapterRef.current, () => stateCtxRef.current.getSnapshot()), [registryResult]);
|
|
82
|
-
|
|
83
|
+
// Memoize the context value on its actual inputs — a fresh object every
|
|
84
|
+
// render would re-render every usePlayView() consumer even when nothing
|
|
85
|
+
// changed (wasted renders).
|
|
86
|
+
const viewValue = useMemo(() => ({
|
|
83
87
|
spec,
|
|
84
88
|
handlers,
|
|
85
89
|
registry: registryResult.registry,
|
|
86
90
|
store,
|
|
87
|
-
};
|
|
91
|
+
}), [spec, handlers, registryResult.registry, store]);
|
|
88
92
|
return _jsx(ViewContext.Provider, { value: viewValue, children: children });
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
@@ -122,30 +126,34 @@ export const ActorProvider = ({ actor, registryResult, store: externalStore, fal
|
|
|
122
126
|
// Ignored when externalStore is provided.
|
|
123
127
|
const internalStoreRef = useRef(null);
|
|
124
128
|
const lastViewRef = useRef(null);
|
|
125
|
-
// Subscribe to signal changes
|
|
129
|
+
// Subscribe to signal changes. The [actor] dep re-creates the watcher when
|
|
130
|
+
// the actor prop swaps — without it the watcher keeps tracking the OLD
|
|
131
|
+
// actor's currentView signal and the rendered view freezes on the old actor
|
|
132
|
+
// while events flow to the new one.
|
|
126
133
|
useSignalEffect(() => {
|
|
127
134
|
const currentView = actor.currentView.get();
|
|
128
135
|
setView(currentView);
|
|
129
|
-
});
|
|
136
|
+
}, [actor]);
|
|
130
137
|
// Inject onRenderError prop into registry (non-enumerable, overrides defineRegistry-level handler)
|
|
131
|
-
// Centralised here per D-19 — one location for all framework renderers
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
// Centralised here per D-19 — one location for all framework renderers.
|
|
139
|
+
// Memoized on its actual inputs: rebuilding the injected registry every render
|
|
140
|
+
// would churn registry identity and invalidate ActorProviderInner's handlers
|
|
141
|
+
// useMemo on every render (wasted work + wasted consumer re-renders).
|
|
142
|
+
const activeRegistryResult = useMemo(() => {
|
|
143
|
+
if (!onRenderError)
|
|
144
|
+
return registryResult;
|
|
145
|
+
return {
|
|
134
146
|
...registryResult,
|
|
135
|
-
registry: (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
})(),
|
|
144
|
-
}
|
|
145
|
-
: registryResult;
|
|
146
|
-
// No view in current state — render fallback
|
|
147
|
+
registry: attachRenderErrorHandler(registryResult.registry, onRenderError),
|
|
148
|
+
};
|
|
149
|
+
}, [registryResult, onRenderError]);
|
|
150
|
+
// No view in current state — render fallback INSIDE ActorContext so a
|
|
151
|
+
// fallback component can call useActor() (e.g. to send a retry event).
|
|
152
|
+
// Parity with the error-boundary fallback below and with the Solid/Svelte/Vue
|
|
153
|
+
// renderers, which all provide context to their null-view fallbacks.
|
|
154
|
+
// ViewContext is intentionally NOT provided: there is no view spec to expose.
|
|
147
155
|
if (!view) {
|
|
148
|
-
return _jsx(
|
|
156
|
+
return (_jsx(ActorContext.Provider, { value: actor, children: fallback }));
|
|
149
157
|
}
|
|
150
158
|
// Resolve the store to use for StateProvider:
|
|
151
159
|
// - External (controlled): use as-is, caller manages lifecycle
|
|
@@ -160,16 +168,7 @@ export const ActorProvider = ({ actor, registryResult, store: externalStore, fal
|
|
|
160
168
|
if (internalStoreRef.current === null || lastViewRef.current !== view) {
|
|
161
169
|
// Proto-safe guard (T-37-03-01): prevents Date/Array/class-instance from being
|
|
162
170
|
// passed to createAtom. Replaces the weak `?? {}` guard from old code_context.
|
|
163
|
-
|
|
164
|
-
const initialState = rawState !== null &&
|
|
165
|
-
rawState !== undefined &&
|
|
166
|
-
typeof rawState === "object" &&
|
|
167
|
-
!Array.isArray(rawState) &&
|
|
168
|
-
(Object.getPrototypeOf(rawState) === Object.prototype ||
|
|
169
|
-
Object.getPrototypeOf(rawState) === null)
|
|
170
|
-
? rawState
|
|
171
|
-
: {};
|
|
172
|
-
internalStoreRef.current = createViewStore(initialState);
|
|
171
|
+
internalStoreRef.current = createViewStore(toAtomState(view.state));
|
|
173
172
|
lastViewRef.current = view;
|
|
174
173
|
}
|
|
175
174
|
store = internalStoreRef.current;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActorProvider.js","sourceRoot":"","sources":["../src/ActorProvider.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"ActorProvider.js","sourceRoot":"","sources":["../src/ActorProvider.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EACN,WAAW,EACX,wBAAwB,GAIxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,YAAY,EAAqB,MAAM,eAAe,CAAC;AAuBhE;;;GAGG;AACH,MAAM,WAAW,GAAG,aAAa,CAA0B,IAAI,CAAC,CAAC;AAEjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,WAAW;IAC1B,OAAO,iBAAiB,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,YAAqC;IAC7D,OAAO,qBAAqB,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAClE,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,EAC3B,cAAc,EACd,IAAI,EACJ,KAAK,EACL,QAAQ,GAMR;IACA,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAC;IAEjC,+EAA+E;IAC/E,iFAAiF;IACjF,kFAAkF;IAClF,kFAAkF;IAClF,2CAA2C;IAC3C,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IAE/B,qFAAqF;IACrF,gFAAgF;IAChF,kEAAkE;IAClE,4EAA4E;IAC5E,MAAM,kBAAkB,GAAG,MAAM,CAAW,CAAC,OAAO,EAAE,EAAE;QACvD,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAC/C,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,8EAA8E;IAC9E,2EAA2E;IAC3E,mFAAmF;IACnF,oCAAoC;IACpC,MAAM,QAAQ,GAAG,OAAO,CACvB,GAAG,EAAE,CACJ,cAAc,CAAC,QAAQ,CACtB,GAAG,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAChC,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,CACvC,EACF,CAAC,cAAc,CAAC,CAChB,CAAC;IAEF,wEAAwE;IACxE,wEAAwE;IACxE,4BAA4B;IAC5B,MAAM,SAAS,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,CAAC;QACN,IAAI;QACJ,QAAQ;QACR,QAAQ,EAAE,cAAc,CAAC,QAAQ;QACjC,KAAK;KACL,CAAC,EACF,CAAC,IAAI,EAAE,QAAQ,EAAE,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,CAChD,CAAC;IAEF,OAAO,KAAC,WAAW,CAAC,QAAQ,IAAC,KAAK,EAAE,SAAS,YAAG,QAAQ,GAAwB,CAAC;AAClF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiC,CAAC,EAC3D,KAAK,EACL,cAAc,EACd,KAAK,EAAE,aAAa,EACpB,QAAQ,GAAG,IAAI,EACf,OAAO,EACP,aAAa,EACb,QAAQ,GACR,EAAE,EAAE;IACJ,mEAAmE;IACnE,qEAAqE;IACrE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAkB,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;IAEjF,+DAA+D;IAC/D,qFAAqF;IACrF,0CAA0C;IAC1C,MAAM,gBAAgB,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAC;IAElD,2EAA2E;IAC3E,uEAAuE;IACvE,4EAA4E;IAC5E,oCAAoC;IACpC,eAAe,CAAC,GAAG,EAAE;QACpB,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC;QAC5C,OAAO,CAAC,WAAW,CAAC,CAAC;IACtB,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,mGAAmG;IACnG,wEAAwE;IACxE,+EAA+E;IAC/E,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,oBAAoB,GAAG,OAAO,CAAC,GAAG,EAAE;QACzC,IAAI,CAAC,aAAa;YAAE,OAAO,cAAc,CAAC;QAC1C,OAAO;YACN,GAAG,cAAc;YACjB,QAAQ,EAAE,wBAAwB,CAAC,cAAc,CAAC,QAAQ,EAAE,aAAa,CAAC;SAC1E,CAAC;IACH,CAAC,EAAE,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,CAAC;IAEpC,sEAAsE;IACtE,uEAAuE;IACvE,8EAA8E;IAC9E,qEAAqE;IACrE,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,OAAO,CACN,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAqB,YAAG,QAAQ,GAAyB,CACvF,CAAC;IACH,CAAC;IAED,8CAA8C;IAC9C,+DAA+D;IAC/D,0EAA0E;IAC1E,IAAI,KAAiB,CAAC;IACtB,IAAI,aAAa,EAAE,CAAC;QACnB,KAAK,GAAG,aAAa,CAAC;IACvB,CAAC;SAAM,CAAC;QACP,6DAA6D;QAC7D,mEAAmE;QACnE,IAAI,gBAAgB,CAAC,OAAO,KAAK,IAAI,IAAI,WAAW,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACvE,+EAA+E;YAC/E,+EAA+E;YAC/E,gBAAgB,CAAC,OAAO,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YACpE,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,CAAC;QACD,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC;IAClC,CAAC;IAED,OAAO,CACN,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAqB,YAClD,KAAC,iBAAiB,IAAC,QAAQ,EAAE,QAAQ,KAAM,CAAC,OAAO,IAAI,EAAE,OAAO,EAAE,CAAC,YAClE,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,YAC1B,KAAC,kBAAkB,IAClB,cAAc,EAAE,oBAAoB,EACpC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,YAEX,QAAQ,GACW,GACN,GACG,GACG,CACxB,CAAC;AACH,CAAC,CAAC"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { PlayError } from "@xmachines/play";
|
|
|
7
7
|
* root. `RendererError` is retained for programmatic use in custom error handlers
|
|
8
8
|
* and parent boundaries — it is no longer emitted by the built-in boundary itself.
|
|
9
9
|
*
|
|
10
|
-
* **Error code:** `
|
|
10
|
+
* **Error code:** `PLAY_REACT_RENDER_ERROR`
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```typescript
|
package/dist/errors.js
CHANGED
|
@@ -7,7 +7,7 @@ import { PlayError } from "@xmachines/play";
|
|
|
7
7
|
* root. `RendererError` is retained for programmatic use in custom error handlers
|
|
8
8
|
* and parent boundaries — it is no longer emitted by the built-in boundary itself.
|
|
9
9
|
*
|
|
10
|
-
* **Error code:** `
|
|
10
|
+
* **Error code:** `PLAY_REACT_RENDER_ERROR`
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```typescript
|
|
@@ -19,7 +19,7 @@ import { PlayError } from "@xmachines/play";
|
|
|
19
19
|
*/
|
|
20
20
|
export class RendererError extends PlayError {
|
|
21
21
|
constructor(message, options) {
|
|
22
|
-
super("PlayRenderer", "
|
|
22
|
+
super("PlayRenderer", "PLAY_REACT_RENDER_ERROR", message, options);
|
|
23
23
|
this.name = "RendererError";
|
|
24
24
|
}
|
|
25
25
|
}
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC3C,YAAY,OAAe,EAAE,OAAsB;QAClD,KAAK,CAAC,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC3C,YAAY,OAAe,EAAE,OAAsB;QAClD,KAAK,CAAC,cAAc,EAAE,yBAAyB,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACnE,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC7B,CAAC;CACD"}
|
|
@@ -3,35 +3,44 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
+
import type { DependencyList } from "react";
|
|
6
7
|
/**
|
|
7
8
|
* React hook that subscribes to signal changes and runs effect callback
|
|
8
9
|
*
|
|
9
|
-
* Wraps Signal.
|
|
10
|
-
* accessed
|
|
11
|
-
* re-
|
|
10
|
+
* Wraps the callback in a Signal.Computed to automatically track signal
|
|
11
|
+
* dependencies accessed inside it, then delegates the watcher lifecycle
|
|
12
|
+
* (microtask batching, re-arming, disposal) to `watchSignal` from
|
|
13
|
+
* `@xmachines/play-signals` — the single canonical implementation shared
|
|
14
|
+
* across framework renderers.
|
|
12
15
|
*
|
|
13
|
-
* Architecture
|
|
16
|
+
* Architecture:
|
|
14
17
|
* - Uses Signal.Computed to wrap callback for automatic dependency tracking
|
|
15
|
-
* - Signal.subtle.Watcher
|
|
18
|
+
* - watchSignal owns the Signal.subtle.Watcher lifecycle (Phase 29 memory safety)
|
|
16
19
|
* - Microtask batching coalesces rapid signal updates
|
|
17
|
-
* -
|
|
20
|
+
* - Re-rendering is driven by the callback's own setState (see remarks)
|
|
18
21
|
* - Handles cleanup on unmount to prevent memory leaks
|
|
19
22
|
*
|
|
20
23
|
* Invariant: Signal-Only Reactivity - Signals accessed in callback are watched.
|
|
21
24
|
* Invariant: Passive Infrastructure - React observes signals and does not control them.
|
|
22
25
|
*
|
|
23
26
|
* @param callback - Effect function that accesses signals. Can return cleanup function.
|
|
27
|
+
* @param deps - Optional dependency list (like useEffect). When any dependency
|
|
28
|
+
* changes, the watcher and Computed are torn down and recreated, re-tracking
|
|
29
|
+
* signals from scratch. Defaults to `[]` (subscribe once per mount). Pass this
|
|
30
|
+
* when the identity of the object whose signals you read can change over the
|
|
31
|
+
* component's lifetime (e.g. an `actor` prop) — otherwise the watcher keeps
|
|
32
|
+
* tracking the OLD object's signals forever.
|
|
24
33
|
*
|
|
25
34
|
* @example
|
|
26
35
|
* ```typescript
|
|
27
36
|
* const MyComponent = ({ actor }) => {
|
|
28
37
|
* const [view, setView] = useState(null);
|
|
29
38
|
*
|
|
30
|
-
* // Subscribe to actor.currentView signal
|
|
39
|
+
* // Subscribe to actor.currentView signal; re-subscribe if the actor swaps
|
|
31
40
|
* useSignalEffect(() => {
|
|
32
41
|
* const currentView = actor.currentView.get();
|
|
33
42
|
* setView(currentView);
|
|
34
|
-
* });
|
|
43
|
+
* }, [actor]);
|
|
35
44
|
*
|
|
36
45
|
* return <div>{view?.component}</div>;
|
|
37
46
|
* };
|
|
@@ -41,22 +50,22 @@
|
|
|
41
50
|
* **CRITICAL:** Signals must be accessed unconditionally (no if statements).
|
|
42
51
|
* Conditional signal access breaks automatic dependency tracking.
|
|
43
52
|
*
|
|
44
|
-
* **Performance:** Microtask batching (queueMicrotask
|
|
45
|
-
* when multiple signals update rapidly.
|
|
53
|
+
* **Performance:** Microtask batching (queueMicrotask, inside watchSignal)
|
|
54
|
+
* prevents React thrashing when multiple signals update rapidly.
|
|
46
55
|
*
|
|
47
|
-
* **
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* **Re-rendering:** the hook itself does NOT force a React render on signal
|
|
57
|
+
* change — the callback drives re-rendering by calling setState with the new
|
|
58
|
+
* signal value (as in the example above). This lets React's setState bailout
|
|
59
|
+
* skip renders when the derived value is unchanged; an unconditional
|
|
60
|
+
* force-update here previously caused a wasted render per notification even
|
|
61
|
+
* when the callback's setState bailed. If you need to re-render on every
|
|
62
|
+
* notification while reading signals during render, mirror the signal value
|
|
63
|
+
* into React state inside the callback.
|
|
50
64
|
*
|
|
51
65
|
* **Implementation note:** We wrap the callback in Signal.Computed because
|
|
52
66
|
* Signal.subtle.Watcher cannot automatically track arbitrary function calls.
|
|
53
|
-
* The Computed handles dependency tracking
|
|
54
|
-
*
|
|
55
|
-
* **Memory safety (Phase 29):**
|
|
56
|
-
* - `disposed` flag prevents post-cleanup callback execution: if cleanup is
|
|
57
|
-
* called before a pending microtask fires, the microtask returns early.
|
|
58
|
-
* - `needsEnqueue` guard dedups rapid synchronous signal changes: only one
|
|
59
|
-
* microtask is ever queued per batch of synchronous mutations.
|
|
67
|
+
* The Computed handles dependency tracking; watchSignal evaluates it (which
|
|
68
|
+
* runs the callback) whenever any tracked signal changes.
|
|
60
69
|
*/
|
|
61
|
-
export declare const useSignalEffect: (callback: () => void | (() => void)) => void;
|
|
70
|
+
export declare const useSignalEffect: (callback: () => void | (() => void), deps?: DependencyList) => void;
|
|
62
71
|
//# sourceMappingURL=useSignalEffect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSignalEffect.d.ts","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"useSignalEffect.d.ts","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAS5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,eAAO,MAAM,eAAe,GAC3B,UAAU,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,EACnC,OAAM,cAAmB,KACvB,IA0CF,CAAC"}
|
package/dist/useSignalEffect.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
-
import { useEffect,
|
|
7
|
-
import { Signal } from "@xmachines/play-signals";
|
|
6
|
+
import { useEffect, useRef } from "react";
|
|
7
|
+
import { Signal, watchSignal } from "@xmachines/play-signals";
|
|
8
8
|
/**
|
|
9
9
|
* Marker symbol returned by effect Computed to satisfy type requirements
|
|
10
10
|
* The actual value doesn't matter - we only care about side effects
|
|
@@ -13,32 +13,40 @@ const EFFECT_RUN_MARKER = Symbol("effect-run");
|
|
|
13
13
|
/**
|
|
14
14
|
* React hook that subscribes to signal changes and runs effect callback
|
|
15
15
|
*
|
|
16
|
-
* Wraps Signal.
|
|
17
|
-
* accessed
|
|
18
|
-
* re-
|
|
16
|
+
* Wraps the callback in a Signal.Computed to automatically track signal
|
|
17
|
+
* dependencies accessed inside it, then delegates the watcher lifecycle
|
|
18
|
+
* (microtask batching, re-arming, disposal) to `watchSignal` from
|
|
19
|
+
* `@xmachines/play-signals` — the single canonical implementation shared
|
|
20
|
+
* across framework renderers.
|
|
19
21
|
*
|
|
20
|
-
* Architecture
|
|
22
|
+
* Architecture:
|
|
21
23
|
* - Uses Signal.Computed to wrap callback for automatic dependency tracking
|
|
22
|
-
* - Signal.subtle.Watcher
|
|
24
|
+
* - watchSignal owns the Signal.subtle.Watcher lifecycle (Phase 29 memory safety)
|
|
23
25
|
* - Microtask batching coalesces rapid signal updates
|
|
24
|
-
* -
|
|
26
|
+
* - Re-rendering is driven by the callback's own setState (see remarks)
|
|
25
27
|
* - Handles cleanup on unmount to prevent memory leaks
|
|
26
28
|
*
|
|
27
29
|
* Invariant: Signal-Only Reactivity - Signals accessed in callback are watched.
|
|
28
30
|
* Invariant: Passive Infrastructure - React observes signals and does not control them.
|
|
29
31
|
*
|
|
30
32
|
* @param callback - Effect function that accesses signals. Can return cleanup function.
|
|
33
|
+
* @param deps - Optional dependency list (like useEffect). When any dependency
|
|
34
|
+
* changes, the watcher and Computed are torn down and recreated, re-tracking
|
|
35
|
+
* signals from scratch. Defaults to `[]` (subscribe once per mount). Pass this
|
|
36
|
+
* when the identity of the object whose signals you read can change over the
|
|
37
|
+
* component's lifetime (e.g. an `actor` prop) — otherwise the watcher keeps
|
|
38
|
+
* tracking the OLD object's signals forever.
|
|
31
39
|
*
|
|
32
40
|
* @example
|
|
33
41
|
* ```typescript
|
|
34
42
|
* const MyComponent = ({ actor }) => {
|
|
35
43
|
* const [view, setView] = useState(null);
|
|
36
44
|
*
|
|
37
|
-
* // Subscribe to actor.currentView signal
|
|
45
|
+
* // Subscribe to actor.currentView signal; re-subscribe if the actor swaps
|
|
38
46
|
* useSignalEffect(() => {
|
|
39
47
|
* const currentView = actor.currentView.get();
|
|
40
48
|
* setView(currentView);
|
|
41
|
-
* });
|
|
49
|
+
* }, [actor]);
|
|
42
50
|
*
|
|
43
51
|
* return <div>{view?.component}</div>;
|
|
44
52
|
* };
|
|
@@ -48,35 +56,31 @@ const EFFECT_RUN_MARKER = Symbol("effect-run");
|
|
|
48
56
|
* **CRITICAL:** Signals must be accessed unconditionally (no if statements).
|
|
49
57
|
* Conditional signal access breaks automatic dependency tracking.
|
|
50
58
|
*
|
|
51
|
-
* **Performance:** Microtask batching (queueMicrotask
|
|
52
|
-
* when multiple signals update rapidly.
|
|
59
|
+
* **Performance:** Microtask batching (queueMicrotask, inside watchSignal)
|
|
60
|
+
* prevents React thrashing when multiple signals update rapidly.
|
|
53
61
|
*
|
|
54
|
-
* **
|
|
55
|
-
*
|
|
56
|
-
*
|
|
62
|
+
* **Re-rendering:** the hook itself does NOT force a React render on signal
|
|
63
|
+
* change — the callback drives re-rendering by calling setState with the new
|
|
64
|
+
* signal value (as in the example above). This lets React's setState bailout
|
|
65
|
+
* skip renders when the derived value is unchanged; an unconditional
|
|
66
|
+
* force-update here previously caused a wasted render per notification even
|
|
67
|
+
* when the callback's setState bailed. If you need to re-render on every
|
|
68
|
+
* notification while reading signals during render, mirror the signal value
|
|
69
|
+
* into React state inside the callback.
|
|
57
70
|
*
|
|
58
71
|
* **Implementation note:** We wrap the callback in Signal.Computed because
|
|
59
72
|
* Signal.subtle.Watcher cannot automatically track arbitrary function calls.
|
|
60
|
-
* The Computed handles dependency tracking
|
|
61
|
-
*
|
|
62
|
-
* **Memory safety (Phase 29):**
|
|
63
|
-
* - `disposed` flag prevents post-cleanup callback execution: if cleanup is
|
|
64
|
-
* called before a pending microtask fires, the microtask returns early.
|
|
65
|
-
* - `needsEnqueue` guard dedups rapid synchronous signal changes: only one
|
|
66
|
-
* microtask is ever queued per batch of synchronous mutations.
|
|
73
|
+
* The Computed handles dependency tracking; watchSignal evaluates it (which
|
|
74
|
+
* runs the callback) whenever any tracked signal changes.
|
|
67
75
|
*/
|
|
68
|
-
export const useSignalEffect = (callback) => {
|
|
76
|
+
export const useSignalEffect = (callback, deps = []) => {
|
|
69
77
|
// Store callback in a ref so the effect closure always calls the latest version
|
|
70
78
|
// WITHOUT triggering watcher teardown/re-setup on each render (standard React ref pattern)
|
|
71
79
|
const callbackRef = useRef(callback);
|
|
72
80
|
// Assignment outside useEffect keeps ref current without triggering re-effect
|
|
73
81
|
callbackRef.current = callback;
|
|
74
|
-
// Force re-render when signal changes (React needs state change to re-render)
|
|
75
|
-
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
76
82
|
useEffect(() => {
|
|
77
83
|
let cleanup;
|
|
78
|
-
let needsEnqueue = true;
|
|
79
|
-
let disposed = false;
|
|
80
84
|
// Wrap callback in a Computed to automatically track dependencies
|
|
81
85
|
// The Computed will re-evaluate when any accessed signal changes
|
|
82
86
|
const effect = new Signal.Computed(() => {
|
|
@@ -88,37 +92,23 @@ export const useSignalEffect = (callback) => {
|
|
|
88
92
|
// Return marker value (Computed requires a return value)
|
|
89
93
|
return EFFECT_RUN_MARKER;
|
|
90
94
|
});
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
queueMicrotask(() => {
|
|
99
|
-
if (disposed)
|
|
100
|
-
return;
|
|
101
|
-
needsEnqueue = true;
|
|
102
|
-
// Re-evaluate the Computed (runs callback with new signal values)
|
|
103
|
-
effect.get();
|
|
104
|
-
// Force React re-render
|
|
105
|
-
forceUpdate();
|
|
106
|
-
// Re-watch for next change
|
|
107
|
-
watcher.watch();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
// Watch the Computed signal
|
|
111
|
-
watcher.watch(effect);
|
|
95
|
+
// watchSignal owns the watcher lifecycle: microtask batching, disposed
|
|
96
|
+
// guard, and re-arming after each notification (Phase 29 memory safety
|
|
97
|
+
// lives there — single source of truth, no copy-pasted watcher code).
|
|
98
|
+
// Reading the Computed inside watchSignal re-evaluates it, which runs the
|
|
99
|
+
// user callback; re-rendering is the callback's job (setState), so no
|
|
100
|
+
// extra work is needed in onValue.
|
|
101
|
+
const unwatch = watchSignal(effect, () => { });
|
|
112
102
|
// Initial run of callback (via Computed)
|
|
113
103
|
effect.get();
|
|
114
|
-
// Cleanup on unmount
|
|
104
|
+
// Cleanup on unmount or deps change
|
|
115
105
|
return () => {
|
|
116
|
-
|
|
106
|
+
// Stop receiving signal updates (idempotent, disposes pending microtasks)
|
|
107
|
+
unwatch();
|
|
117
108
|
if (typeof cleanup === "function")
|
|
118
109
|
cleanup();
|
|
119
|
-
// Unwatch to stop receiving signal updates
|
|
120
|
-
watcher.unwatch(effect);
|
|
121
110
|
};
|
|
122
|
-
|
|
111
|
+
// oxlint-disable-next-line exhaustive-deps -- deps forwarded from caller; callback intentionally excluded (ref pattern)
|
|
112
|
+
}, deps);
|
|
123
113
|
};
|
|
124
114
|
//# sourceMappingURL=useSignalEffect.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSignalEffect.js","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"useSignalEffect.js","sourceRoot":"","sources":["../src/useSignalEffect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9D;;;GAGG;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,QAAmC,EACnC,OAAuB,EAAE,EAClB,EAAE;IACT,gFAAgF;IAChF,2FAA2F;IAC3F,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,8EAA8E;IAC9E,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IAE/B,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,OAA4B,CAAC;QAEjC,kEAAkE;QAClE,iEAAiE;QACjE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE;YACvC,mCAAmC;YACnC,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;YAE7C,kEAAkE;YAClE,OAAO,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;YAEhC,yDAAyD;YACzD,OAAO,iBAAiB,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,0EAA0E;QAC1E,sEAAsE;QACtE,mCAAmC;QACnC,MAAM,OAAO,GAAG,WAAW,CAAC,MAAkC,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAE1E,yCAAyC;QACzC,MAAM,CAAC,GAAG,EAAE,CAAC;QAEb,oCAAoC;QACpC,OAAO,GAAG,EAAE;YACX,0EAA0E;YAC1E,OAAO,EAAE,CAAC;YACV,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,OAAO,EAAE,CAAC;QAC9C,CAAC,CAAC;QACF,wHAAwH;IACzH,CAAC,EAAE,IAAI,CAAC,CAAC;AACV,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-react",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.54",
|
|
4
4
|
"description": "React renderer for XMachines Play architecture with signal-driven rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"actor",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc --build",
|
|
39
|
-
"clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__ node_modules/.svelte2tsx-*",
|
|
39
|
+
"clean": "rm -rf dist *.tsbuildinfo coverage .vitest-attachments test/browser/__screenshots__ node_modules/.svelte2tsx-* node_modules/.vite*",
|
|
40
40
|
"lint": "oxlint .",
|
|
41
41
|
"format": "oxfmt .",
|
|
42
42
|
"test": "vitest",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"prepublishOnly": "npm run build"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@xmachines/play": "1.0.0-beta.
|
|
49
|
-
"@xmachines/play-actor": "1.0.0-beta.
|
|
50
|
-
"@xmachines/play-signals": "1.0.0-beta.
|
|
48
|
+
"@xmachines/play": "1.0.0-beta.54",
|
|
49
|
+
"@xmachines/play-actor": "1.0.0-beta.54",
|
|
50
|
+
"@xmachines/play-signals": "1.0.0-beta.54"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@json-render/core": "^0.18.0",
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
"@types/node": "^25.6.0",
|
|
59
59
|
"@types/react": "^19.2.14",
|
|
60
60
|
"@types/react-dom": "^19.2.3",
|
|
61
|
-
"@
|
|
61
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
62
|
+
"@vitest/browser-playwright": "^4.1.5",
|
|
63
|
+
"@xmachines/shared": "1.0.0-beta.54",
|
|
62
64
|
"@xstate/store": "^3.17.0",
|
|
63
65
|
"jsdom": "^29.1.0",
|
|
64
66
|
"oxfmt": "^0.47.0",
|
|
@@ -67,7 +69,8 @@
|
|
|
67
69
|
"react-dom": "^19.2.5",
|
|
68
70
|
"typescript": "^5.9.3 || ^6.0.3",
|
|
69
71
|
"vitest": "^4.1.5",
|
|
70
|
-
"xstate": "^5.31.0"
|
|
72
|
+
"xstate": "^5.31.0",
|
|
73
|
+
"zod": "^4.4.1"
|
|
71
74
|
},
|
|
72
75
|
"peerDependencies": {
|
|
73
76
|
"@json-render/core": "^0.18.0",
|