@xmachines/play-vue 1.0.0-beta.4 → 1.0.0-beta.40
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/LICENSE +21 -0
- package/README.md +295 -83
- package/dist/ActorProvider.js +8 -0
- package/dist/ActorProvider.js.map +1 -0
- package/dist/ActorProvider.vue.d.ts +51 -0
- package/dist/ActorProvider.vue.d.ts.map +1 -0
- package/dist/ActorProvider.vue_vue_type_script_lang.js +104 -0
- package/dist/ActorProvider.vue_vue_type_script_lang.js.map +1 -0
- package/dist/PlayRenderer.js +2 -4
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/PlayRenderer.vue.d.ts +5 -0
- package/dist/PlayRenderer.vue.d.ts.map +1 -0
- package/dist/PlayRenderer.vue_vue_type_script_lang.js +10 -27
- package/dist/PlayRenderer.vue_vue_type_script_lang.js.map +1 -1
- package/dist/PlayUIProvider.js +7 -0
- package/dist/PlayUIProvider.js.map +1 -0
- package/dist/PlayUIProvider.vue.d.ts +76 -0
- package/dist/PlayUIProvider.vue.d.ts.map +1 -0
- package/dist/PlayUIProvider.vue_vue_type_script_lang.js +86 -0
- package/dist/PlayUIProvider.vue_vue_type_script_lang.js.map +1 -0
- package/dist/actor-provider-context.d.ts +40 -0
- package/dist/actor-provider-context.d.ts.map +1 -0
- package/dist/actor-provider-context.js +11 -0
- package/dist/actor-provider-context.js.map +1 -0
- package/dist/define-registry.d.ts +79 -0
- package/dist/define-registry.d.ts.map +1 -0
- package/dist/define-registry.js +25 -0
- package/dist/define-registry.js.map +1 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/types.d.ts +17 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/useActor.d.ts +35 -0
- package/dist/useActor.d.ts.map +1 -0
- package/dist/useActor.js +14 -0
- package/dist/useActor.js.map +1 -0
- package/package.json +28 -14
- package/dist/_virtual/_plugin-vue_export-helper.js +0 -8
- package/dist/index.css +0 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikael Karon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,137 +1,349 @@
|
|
|
1
1
|
# @xmachines/play-vue
|
|
2
2
|
|
|
3
|
-
Vue renderer
|
|
3
|
+
**Vue 3 renderer for XMachines Play Architecture**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Bridges TC39 Signal-driven actors to Vue's reactivity. Business logic stays in the actor; Vue is purely a rendering target.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`@xmachines/play-vue` provides `PlayRenderer`, a Vue component that:
|
|
10
|
+
|
|
11
|
+
- Subscribes to `actor.currentView` (TC39 Signal) and re-renders on every state transition
|
|
12
|
+
- Renders the current view's JSON spec via `@json-render/vue`
|
|
13
|
+
- Routes action names from spec elements to `actor.send()` via the `actions` prop
|
|
14
|
+
- Manages per-view UI state in an `@xstate/store` atom (automatic or caller-supplied)
|
|
15
|
+
|
|
16
|
+
Per [Play RFC](../docs/rfc/play.md):
|
|
17
|
+
|
|
18
|
+
- **Actor Authority (INV-01):** Guards in the machine decide all state transitions
|
|
19
|
+
- **Passive Infrastructure (INV-04):** Vue observes signals and dispatches events — never decides
|
|
20
|
+
- **Signal-Only Reactivity (INV-05):** `actor.currentView` signal is the sole render trigger
|
|
6
21
|
|
|
7
22
|
## Installation
|
|
8
23
|
|
|
9
24
|
```bash
|
|
10
|
-
npm install @xmachines/play-vue
|
|
25
|
+
npm install @xmachines/play-vue
|
|
26
|
+
npm install @json-render/vue @json-render/core # peer deps
|
|
27
|
+
npm install @json-render/xstate @xstate/store # store integration
|
|
11
28
|
```
|
|
12
29
|
|
|
30
|
+
In this monorepo, the root install applies a `patch-package` patch to `@json-render/vue`
|
|
31
|
+
so `defineRegistry(..., { onRenderError })` can intercept inner element-boundary errors
|
|
32
|
+
without muting console output.
|
|
33
|
+
|
|
13
34
|
## Current Exports
|
|
14
35
|
|
|
15
|
-
- `PlayRenderer` (Vue
|
|
16
|
-
- `
|
|
36
|
+
- `PlayRenderer` — main renderer component (Vue SFC)
|
|
37
|
+
- `useActor` — composable for accessing the actor inside a `PlayRenderer` tree
|
|
38
|
+
- `defineRegistry` — SFC-aware wrapper; auto-wraps `.vue` SFCs via `h(SFC, ctx)`
|
|
39
|
+
- `useBoundProp` — re-exported from `@json-render/vue`
|
|
40
|
+
- `ComponentFn` (type) — re-exported from `@json-render/vue`
|
|
41
|
+
- `ComponentContext` (type) — re-exported from `@json-render/vue`
|
|
42
|
+
- `ActorProvider` — escape hatch primitive (owns actor bridging, signal bridge, store lifecycle)
|
|
43
|
+
- `PlayUIProvider` — batteries-included composite (wraps `ActorProvider` + `JSONUIProvider`)
|
|
44
|
+
- `getPlayViewContext` — composable for accessing the current view spec inside a provider tree
|
|
45
|
+
- `RenderErrorHandler` (type) — inner per-element error callback signature
|
|
46
|
+
- `ActorProviderProps` (type)
|
|
47
|
+
- `ViewContextValue` (type)
|
|
48
|
+
- `PlayActor` (type)
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// catalog.ts — shared contract
|
|
54
|
+
import { defineCatalog } from "@json-render/core";
|
|
55
|
+
import { z } from "zod";
|
|
56
|
+
|
|
57
|
+
export const catalog = defineCatalog({
|
|
58
|
+
elements: {
|
|
59
|
+
Login: { props: z.object({ title: z.string() }), description: "Login form" },
|
|
60
|
+
Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
17
63
|
|
|
18
|
-
|
|
64
|
+
export type Catalog = typeof catalog;
|
|
65
|
+
```
|
|
19
66
|
|
|
20
67
|
```vue
|
|
68
|
+
<!-- Login.vue — Vue SFC; useBoundProp works in <script setup> -->
|
|
21
69
|
<script setup lang="ts">
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
70
|
+
import { useBoundProp } from "@xmachines/play-vue";
|
|
71
|
+
import type { ComponentContext } from "@xmachines/play-vue";
|
|
72
|
+
import type { Catalog } from "./catalog.js";
|
|
73
|
+
|
|
74
|
+
const { props, emit, bindings } = defineProps<ComponentContext<Catalog, "Login">>();
|
|
75
|
+
const [username, setUsername] = useBoundProp<string>(bindings?.username ?? "/username");
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div class="view">
|
|
80
|
+
<h2>{{ props.title }}</h2>
|
|
81
|
+
<form @submit.prevent="emit('submit')">
|
|
82
|
+
<input id="username" v-model="username" @input="setUsername(username)" />
|
|
83
|
+
<button type="submit">Log In</button>
|
|
84
|
+
</form>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
```
|
|
25
88
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
89
|
+
```ts
|
|
90
|
+
// registry.ts — pass SFCs directly; defineRegistry auto-wraps them
|
|
91
|
+
import { defineRegistry } from "@xmachines/play-vue";
|
|
92
|
+
import { catalog } from "./catalog.js";
|
|
93
|
+
import LoginSFC from "./Login.vue";
|
|
94
|
+
import DashboardSFC from "./Dashboard.vue";
|
|
95
|
+
|
|
96
|
+
export const registryResult = defineRegistry(catalog, {
|
|
97
|
+
components: { Login: LoginSFC, Dashboard: DashboardSFC },
|
|
98
|
+
actions: {
|
|
99
|
+
login: async (params) => {
|
|
100
|
+
if (!params) return;
|
|
101
|
+
actor.send({ type: "auth.login", username: params.username });
|
|
102
|
+
},
|
|
103
|
+
logout: async (params) => {
|
|
104
|
+
actor.send({ type: "auth.logout" });
|
|
105
|
+
},
|
|
106
|
+
},
|
|
31
107
|
});
|
|
108
|
+
```
|
|
32
109
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
110
|
+
```ts
|
|
111
|
+
// machine.ts
|
|
112
|
+
import { setup, assign } from "xstate";
|
|
113
|
+
import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
114
|
+
|
|
115
|
+
export const machine = setup({
|
|
116
|
+
types: {
|
|
117
|
+
context: {} as {
|
|
118
|
+
isAuthenticated: boolean;
|
|
119
|
+
username: string | null;
|
|
120
|
+
params: Record<string, string>;
|
|
121
|
+
query: Record<string, string>;
|
|
122
|
+
},
|
|
123
|
+
events: {} as
|
|
124
|
+
| { type: "auth.login"; username: string }
|
|
125
|
+
| { type: "auth.logout" }
|
|
126
|
+
| { type: "play.route"; to: string; params?: Record<string, string> },
|
|
127
|
+
},
|
|
128
|
+
}).createMachine(
|
|
129
|
+
formatPlayRouteTransitions({
|
|
130
|
+
id: "app",
|
|
131
|
+
initial: "login",
|
|
132
|
+
context: { isAuthenticated: false, username: null, params: {}, query: {} },
|
|
133
|
+
states: {
|
|
134
|
+
login: {
|
|
135
|
+
id: "login",
|
|
136
|
+
meta: {
|
|
137
|
+
route: "/login",
|
|
138
|
+
view: {
|
|
139
|
+
root: "root",
|
|
140
|
+
elements: {
|
|
141
|
+
root: { type: "Login", props: { title: "Sign In" }, children: [] },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
dashboard: {
|
|
147
|
+
id: "dashboard",
|
|
148
|
+
meta: {
|
|
149
|
+
route: "/dashboard",
|
|
150
|
+
view: {
|
|
151
|
+
root: "root",
|
|
152
|
+
elements: {
|
|
153
|
+
root: { type: "Dashboard", props: { username: "" }, children: [] },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
on: {
|
|
160
|
+
"auth.login": {
|
|
161
|
+
target: ".dashboard",
|
|
162
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
163
|
+
actions: assign({ isAuthenticated: true, username: ({ event }) => event.username }),
|
|
164
|
+
},
|
|
165
|
+
"auth.logout": {
|
|
166
|
+
target: ".login",
|
|
167
|
+
guard: ({ context }) => context.isAuthenticated,
|
|
168
|
+
actions: assign({ isAuthenticated: false, username: null }),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```vue
|
|
176
|
+
<!-- App.vue -->
|
|
177
|
+
<script setup lang="ts">
|
|
178
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
179
|
+
import { PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";
|
|
180
|
+
import { machine } from "./machine.js";
|
|
181
|
+
import { registryResult } from "./registry.js";
|
|
36
182
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
Login: LoginComponent,
|
|
41
|
-
Dashboard: DashboardComponent,
|
|
42
|
-
};
|
|
183
|
+
const createPlayer = definePlayer({ machine });
|
|
184
|
+
const actor = createPlayer();
|
|
185
|
+
actor.start();
|
|
43
186
|
</script>
|
|
44
187
|
|
|
45
188
|
<template>
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
</template>
|
|
50
|
-
</PlayRenderer>
|
|
189
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
190
|
+
<PlayRenderer />
|
|
191
|
+
</PlayUIProvider>
|
|
51
192
|
</template>
|
|
52
193
|
```
|
|
53
194
|
|
|
54
|
-
## API
|
|
195
|
+
## API Reference
|
|
55
196
|
|
|
56
|
-
###
|
|
197
|
+
### `PlayUIProvider`
|
|
57
198
|
|
|
58
|
-
|
|
199
|
+
Batteries-included composite provider. Wraps `ActorProvider` + `JSONUIProvider`. Pass `actor` and `registryResult` here, then place `<PlayRenderer />` inside as a zero-prop child.
|
|
59
200
|
|
|
60
|
-
|
|
201
|
+
```vue
|
|
202
|
+
<PlayUIProvider
|
|
203
|
+
:actor="actor"
|
|
204
|
+
:registryResult="registryResult"
|
|
205
|
+
:store="myStore"
|
|
206
|
+
:onRenderError="(error, elementType) => console.warn(`<${elementType}> crashed:`, error)"
|
|
207
|
+
>
|
|
208
|
+
<template #fallback><p>Something went wrong.</p></template>
|
|
209
|
+
<PlayRenderer />
|
|
210
|
+
</PlayUIProvider>
|
|
211
|
+
```
|
|
61
212
|
|
|
62
|
-
|
|
63
|
-
- `components` (required) - Map of component names to Vue components
|
|
64
|
-
- `fallback` (optional) - Slot shown when `currentView` is null
|
|
213
|
+
**`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
|
|
65
214
|
|
|
66
|
-
|
|
215
|
+
**`registryResult`** — The full `DefineRegistryResult` returned by `defineRegistry(catalog, { components, actions })` from `@xmachines/play-vue`. Pass `.vue` SFCs directly — they are auto-wrapped via `h(SFC, ctx)` so `useBoundProp` and other composables work inside `<script setup>`.
|
|
67
216
|
|
|
68
|
-
|
|
217
|
+
**`store`** (optional) — Controls per-view UI state (`$state` bindings, form values):
|
|
69
218
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
219
|
+
- **Omitted (uncontrolled, default):** A fresh `@xstate/store` atom is created per view transition, seeded from `view.spec.state`.
|
|
220
|
+
- **Provided (controlled):** The caller owns the store; `spec.state` is ignored.
|
|
72
221
|
|
|
73
|
-
|
|
222
|
+
```ts
|
|
223
|
+
import { createAtom } from "@xstate/store";
|
|
224
|
+
import { xstateStoreStateStore } from "@json-render/xstate";
|
|
225
|
+
import type { StateStore } from "@json-render/core";
|
|
226
|
+
|
|
227
|
+
const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
|
|
228
|
+
```
|
|
74
229
|
|
|
75
230
|
```vue
|
|
76
|
-
<
|
|
77
|
-
|
|
231
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult" :store="store">
|
|
232
|
+
<PlayRenderer />
|
|
233
|
+
</PlayUIProvider>
|
|
234
|
+
```
|
|
78
235
|
|
|
79
|
-
|
|
80
|
-
userId: string;
|
|
81
|
-
send: AbstractActor<any>["send"];
|
|
82
|
-
}>();
|
|
236
|
+
**`onRenderError`** — Called when an individual catalog component throws during render. Caught by `@json-render/vue`'s inner per-element error boundary — the failed component is silently removed while the rest of the spec continues rendering. When both `onRenderError` on `PlayUIProvider` and on `defineRegistry` are set, the prop wins.
|
|
83
237
|
|
|
84
|
-
|
|
85
|
-
send({ type: "user.click", payload: { action: "details" } });
|
|
86
|
-
}
|
|
87
|
-
</script>
|
|
238
|
+
---
|
|
88
239
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
240
|
+
### `ActorProvider`
|
|
241
|
+
|
|
242
|
+
Escape hatch primitive. Owns actor bridging, signal bridge, and store lifecycle. Use this when you need direct control over the provider layer.
|
|
243
|
+
|
|
244
|
+
```vue
|
|
245
|
+
<ActorProvider :actor="actor" :registryResult="registryResult" :onRenderError="handleError">
|
|
246
|
+
<!-- your own JSONUIProvider + PlayRenderer tree -->
|
|
247
|
+
</ActorProvider>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### `PlayRenderer`
|
|
253
|
+
|
|
254
|
+
Zero-prop leaf component. Must be rendered inside a `PlayUIProvider` (or `ActorProvider`) tree. Subscribes to `actor.currentView` via context and renders the current spec.
|
|
255
|
+
|
|
256
|
+
```vue
|
|
257
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
258
|
+
<PlayRenderer />
|
|
259
|
+
</PlayUIProvider>
|
|
95
260
|
```
|
|
96
261
|
|
|
97
|
-
|
|
262
|
+
`PlayRenderer` accepts no props — all configuration (`actor`, `registryResult`, `store`, `fallback`, `onRenderError`) is provided by the enclosing `PlayUIProvider` or `ActorProvider`.
|
|
98
263
|
|
|
99
|
-
|
|
100
|
-
- **Dynamic Rendering:** Renders components based on `actor.currentView.component` string
|
|
101
|
-
- **Type Safe:** Full TypeScript support with generic type inference
|
|
102
|
-
- **Error Handling:** Gracefully handles missing components and null catalogs
|
|
103
|
-
- **One-Shot Re-Watch:** Implements proper signal watcher pattern with microtask batching
|
|
264
|
+
## Error handling
|
|
104
265
|
|
|
105
|
-
|
|
266
|
+
The provider tree has two layers of error boundaries:
|
|
106
267
|
|
|
107
|
-
|
|
268
|
+
### Outer boundary — `fallback` slot
|
|
108
269
|
|
|
109
|
-
|
|
110
|
-
2. `queueMicrotask`
|
|
111
|
-
3. `getPending()`
|
|
112
|
-
4. read actor signals and update framework-local state
|
|
113
|
-
5. re-arm with `watch(...)` or `watch()`
|
|
270
|
+
Vue's `onErrorCaptured` wraps the entire renderer. Triggered when the spec or store setup throws, or when the inner boundary is not present. Use Vue's built-in `onErrorCaptured` in a parent component for observability.
|
|
114
271
|
|
|
115
|
-
|
|
272
|
+
```vue
|
|
273
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
274
|
+
<template #fallback>
|
|
275
|
+
<p>Something went wrong.</p>
|
|
276
|
+
</template>
|
|
277
|
+
<PlayRenderer />
|
|
278
|
+
</PlayUIProvider>
|
|
279
|
+
```
|
|
116
280
|
|
|
117
|
-
|
|
281
|
+
### Inner boundary — `onRenderError`
|
|
118
282
|
|
|
119
|
-
|
|
283
|
+
Each catalog element is individually wrapped in an error boundary by `@json-render/vue`. When a component throws, it is silently removed while the rest of the spec continues rendering. The outer boundary is **not** triggered.
|
|
120
284
|
|
|
121
|
-
|
|
122
|
-
- Treat renderer/provider disposal as deterministic teardown, not GC-only cleanup.
|
|
123
|
-
- Keep actor-driven routing and view decisions in the actor layer.
|
|
285
|
+
Pass `onRenderError` to `PlayUIProvider` (or `ActorProvider`) — overrides any registry-level handler — or bake it into `defineRegistry`:
|
|
124
286
|
|
|
125
|
-
|
|
287
|
+
```vue
|
|
288
|
+
<!-- via PlayUIProvider prop -->
|
|
289
|
+
<PlayUIProvider
|
|
290
|
+
:actor="actor"
|
|
291
|
+
:registryResult="registryResult"
|
|
292
|
+
:onRenderError="(error, elementType) => console.warn(`<${elementType}> crashed:`, error)"
|
|
293
|
+
>
|
|
294
|
+
<PlayRenderer />
|
|
295
|
+
</PlayUIProvider>
|
|
296
|
+
```
|
|
126
297
|
|
|
127
|
-
|
|
298
|
+
```ts
|
|
299
|
+
// via defineRegistry — bakes the handler into the registry
|
|
300
|
+
const registryResult = defineRegistry(catalog, {
|
|
301
|
+
components: { Login: LoginSFC, Dashboard: DashboardSFC },
|
|
302
|
+
actions: { login: async (params) => { ... }, logout: async () => { ... } },
|
|
303
|
+
onRenderError(error, elementType) {
|
|
304
|
+
reportExpectedRenderError(error, elementType);
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`onRenderError` is typed as `RenderErrorHandler` and exported from `@xmachines/play-vue`.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### `useActor`
|
|
314
|
+
|
|
315
|
+
Vue composable for accessing the actor from inside any component rendered by `PlayRenderer`.
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import { useActor } from "@xmachines/play-vue";
|
|
319
|
+
|
|
320
|
+
// Inside any component rendered inside PlayRenderer:
|
|
321
|
+
const actor = useActor();
|
|
322
|
+
actor.send({ type: "auth.logout" });
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Throws `NonNullableError: "useActor() must be called inside <ActorProvider> (or <PlayUIProvider>)"` if called outside the tree.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Route Parameters in Props
|
|
330
|
+
|
|
331
|
+
When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
// spec: { section: undefined, user: "alice" }
|
|
335
|
+
// After play.route to /settings/profile → context.params = { section: "profile" }
|
|
336
|
+
// Component receives: { section: "profile", user: "alice" }
|
|
337
|
+
```
|
|
128
338
|
|
|
129
|
-
|
|
130
|
-
2. **Passive Infrastructure:** Renderer observes signals, sends events
|
|
131
|
-
3. **Signal-Only Reactivity:** Business logic state lives in actor signals
|
|
339
|
+
Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
|
|
132
340
|
|
|
133
|
-
|
|
341
|
+
---
|
|
134
342
|
|
|
135
|
-
##
|
|
343
|
+
## Architecture Notes
|
|
136
344
|
|
|
137
|
-
|
|
345
|
+
- Vue reactivity is only used to trigger re-renders — not for business logic
|
|
346
|
+
- `actor.currentView` (TC39 Signal) is bridged to Vue's reactive system inside `PlayRenderer`
|
|
347
|
+
- Per-view UI state lives in an `@xstate/store` atom, not in Vue reactive state
|
|
348
|
+
- `@json-render/vue` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useBoundProp` from `@xmachines/play-vue`
|
|
349
|
+
- Vue views should be `.vue` SFCs using `ComponentContext<MyCatalog, "X">` — `defineRegistry` from `@xmachines/play-vue` auto-wraps them via `h(SFC, ctx)`, giving each SFC its own `setup()` context where `useBoundProp` and Vue composables work correctly
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActorProvider.js","names":[],"sources":["../src/ActorProvider.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * ActorProvider — escape-hatch primitive for the XMachines Vue render architecture.\n *\n * Owns the full actor lifecycle:\n * - Signal subscription (watchSignal) bridging TC39 Signals to Vue reactivity\n * - Per-view StateStore lifecycle (controlled/uncontrolled)\n * - Handler resolution via ActorProviderInner (must be inside StateProvider)\n * - ViewContextValue provision via ViewKey injection key\n * - ActionProvider + VisibilityProvider wrapping for downstream Renderer\n * - onRenderError injection into registry\n *\n * Library authors who need fine-grained control use this directly.\n * End users should use <PlayUIProvider> instead.\n *\n * @invariant Actor Authority - Actor decides all state transitions via guards\n * @invariant Passive Infrastructure - Component observes signals, sends events\n * @invariant Signal-Only Reactivity - Business logic state lives in actor signals\n */\n\nimport { defineComponent, ref, toRaw, markRaw, onUnmounted, h, provide, shallowRef } from \"vue\";\nimport type { PropType } from \"vue\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport type { AbstractActor, Viewable, PlaySpec } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type { DefineRegistryResult, SetState, RenderErrorHandler } from \"@json-render/vue\";\n\nimport { StateProvider, useStateStore, ActionProvider, VisibilityProvider } from \"@json-render/vue\";\nimport type { StateStore } from \"@json-render/core\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { provideActor, type PlayActor } from \"./useActor.js\";\nimport { ViewKey, type ViewContextValue } from \"./actor-provider-context.js\";\nimport type { ActorProviderProps } from \"./types.js\";\n\n// Re-export props type and context accessors for consumers who import from this file\nexport type { ActorProviderProps } from \"./types.js\";\nexport { getPlayViewContext } from \"./actor-provider-context.js\";\nexport type { ViewContextValue } from \"./actor-provider-context.js\";\n\n// ---------------------------------------------------------------------------\n// ActorProviderInner — renders inside StateProvider to call useStateStore()\n// Provides ViewContextValue + ActionProvider + VisibilityProvider for downstream Renderer.\n// ---------------------------------------------------------------------------\n\nconst ActorProviderInner = defineComponent({\n\tname: \"ActorProviderInner\",\n\tprops: {\n\t\tregistryResult: {\n\t\t\ttype: Object as PropType<DefineRegistryResult>,\n\t\t\trequired: true,\n\t\t},\n\t\tspec: {\n\t\t\ttype: Object as PropType<PlaySpec | null>,\n\t\t\tdefault: null,\n\t\t},\n\t\tstore: {\n\t\t\ttype: Object as PropType<StateStore>,\n\t\t\trequired: true,\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Use shallowRef for the view context value to avoid deep reactivity overhead.\n\t\t// The Proxy below allows inject() to always read the latest value.\n\t\tconst viewRef = shallowRef<ViewContextValue | null>(null);\n\n\t\t// Provide the ViewContextValue via Vue's inject/provide system.\n\t\t// Called synchronously in setup() so Vue registers it on the component instance.\n\t\t// Uses Proxy so descendants always receive the latest viewRef.value.\n\t\tprovide(\n\t\t\tViewKey,\n\t\t\tnew Proxy({} as ViewContextValue, {\n\t\t\t\tget(_target, prop: string) {\n\t\t\t\t\treturn viewRef.value?.[prop as keyof ViewContextValue];\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\n\t\t// Call useStateStore() at setup() time (synchronous, before return).\n\t\t// Vue composables that use inject() must be called during setup(), not in a render function.\n\t\tconst { update, getSnapshot } = useStateStore();\n\n\t\t// Build a SetState adapter: handlers factory expects updater-function pattern\n\t\tconst setStateAdapter: SetState = (updater) => {\n\t\t\tconst prev = getSnapshot();\n\t\t\tupdate(updater(prev));\n\t\t};\n\n\t\treturn () => {\n\t\t\tif (!props.spec) {\n\t\t\t\tviewRef.value = null;\n\t\t\t\treturn slots.default?.() ?? null;\n\t\t\t}\n\n\t\t\tconst handlers = props.registryResult.handlers(\n\t\t\t\t() => setStateAdapter,\n\t\t\t\t() => getSnapshot(),\n\t\t\t);\n\n\t\t\tviewRef.value = {\n\t\t\t\tspec: props.spec,\n\t\t\t\thandlers,\n\t\t\t\tregistry: props.registryResult.registry,\n\t\t\t\tstore: props.store,\n\t\t\t};\n\n\t\t\t// Wrap with ActionProvider + VisibilityProvider so PlayRenderer's Renderer works\n\t\t\t// even when ActorProvider is used directly (without PlayUIProvider / JSONUIProvider)\n\t\t\treturn h(ActionProvider, { handlers }, () =>\n\t\t\t\th(VisibilityProvider, {}, () => slots.default?.() ?? null),\n\t\t\t);\n\t\t};\n\t},\n});\n\n// ---------------------------------------------------------------------------\n// ActorProvider — main export\n// ---------------------------------------------------------------------------\n\nexport default defineComponent({\n\tname: \"ActorProvider\",\n\tprops: {\n\t\tactor: {\n\t\t\ttype: Object as PropType<ActorProviderProps[\"actor\"]>,\n\t\t\trequired: true,\n\t\t},\n\t\tregistryResult: {\n\t\t\ttype: Object as PropType<DefineRegistryResult>,\n\t\t\trequired: true,\n\t\t},\n\t\tstore: {\n\t\t\ttype: Object as PropType<StateStore>,\n\t\t\tdefault: undefined,\n\t\t},\n\t\tonRenderError: {\n\t\t\ttype: Function as PropType<RenderErrorHandler>,\n\t\t\tdefault: undefined,\n\t\t},\n\t},\n\tsetup(props, { slots }) {\n\t\t// Unwrap actor from Vue's reactive proxy to access raw Signal objects\n\t\tconst actor = toRaw(props.actor as AbstractActor<AnyActorLogic> & Viewable);\n\n\t\t// Unwrap the registryResult and mark components as raw to avoid Vue reactivity overhead\n\t\tconst rawRegistryResult: DefineRegistryResult = {\n\t\t\t...toRaw(props.registryResult),\n\t\t\tregistry: markRaw(\n\t\t\t\tObject.fromEntries(\n\t\t\t\t\tObject.entries(toRaw(props.registryResult).registry).map(([k, v]) => [\n\t\t\t\t\t\tk,\n\t\t\t\t\t\tmarkRaw(v as object),\n\t\t\t\t\t]),\n\t\t\t\t) as DefineRegistryResult[\"registry\"],\n\t\t\t),\n\t\t};\n\n\t\t// Inject onRenderError prop into registry (non-enumerable, overrides defineRegistry-level handler)\n\t\tif (props.onRenderError) {\n\t\t\tObject.defineProperty(rawRegistryResult.registry, \"onRenderError\", {\n\t\t\t\tvalue: props.onRenderError,\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\n\t\t// Provide the raw actor to all descendants via Vue's provide/inject mechanism\n\t\tprovideActor(actor as PlayActor);\n\n\t\t// Seed initial value then subscribe — both synchronous, no scheduler gap.\n\t\t// This mirrors the atomic seed+watch pattern used in Solid (createEffect)\n\t\t// and Svelte ($effect) for cross-framework consistency.\n\t\tconst view = ref<PlaySpec | null>(actor.currentView.get());\n\n\t\t// Internal per-view store — recreated on each view transition when no external store\n\t\tlet internalStore: StateStore | null = null;\n\t\tlet lastView: PlaySpec | null = null;\n\t\t// storeKey is only incremented in the internalStore branch (not when props.store is set)\n\t\tlet storeKey = 0;\n\n\t\t// Signal watcher for bridging TC39 Signals to Vue reactivity\n\t\tconst unwatch = watchSignal(actor.currentView, (nextView) => {\n\t\t\tview.value = nextView;\n\t\t});\n\n\t\tonUnmounted(() => {\n\t\t\tunwatch();\n\t\t});\n\n\t\treturn () => {\n\t\t\t// No view — show fallback slot or nothing\n\t\t\tif (!view.value) {\n\t\t\t\treturn slots.fallback ? slots.fallback() : null;\n\t\t\t}\n\n\t\t\tconst spec = view.value;\n\n\t\t\t// Resolve the store: external (controlled) or internal per-view atom\n\t\t\tlet store: StateStore;\n\t\t\tif (props.store) {\n\t\t\t\tstore = props.store;\n\t\t\t} else {\n\t\t\t\tif (internalStore === null || lastView !== view.value) {\n\t\t\t\t\t// T-37-04-02: Proper proto-safe guard for spec.state\n\t\t\t\t\tconst rawState = spec.state;\n\t\t\t\t\tconst initialState: Record<string, unknown> =\n\t\t\t\t\t\trawState !== null &&\n\t\t\t\t\t\ttypeof rawState === \"object\" &&\n\t\t\t\t\t\t!Array.isArray(rawState) &&\n\t\t\t\t\t\t(Object.getPrototypeOf(rawState) === Object.prototype ||\n\t\t\t\t\t\t\tObject.getPrototypeOf(rawState) === null)\n\t\t\t\t\t\t\t? (rawState as Record<string, unknown>)\n\t\t\t\t\t\t\t: {};\n\t\t\t\t\tinternalStore = xstateStoreStateStore({ atom: createAtom(initialState) });\n\t\t\t\t\tlastView = view.value;\n\t\t\t\t\tstoreKey++;\n\t\t\t\t}\n\t\t\t\tstore = internalStore;\n\t\t\t}\n\n\t\t\t// ActorProviderInner renders inside StateProvider so useStateStore() works\n\t\t\treturn h(StateProvider, { store, key: storeKey }, () =>\n\t\t\t\th(ActorProviderInner, { registryResult: rawRegistryResult, spec, store }, slots),\n\t\t\t);\n\t\t};\n\t},\n});\n</script>\n"],"mappings":""}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PropType } from 'vue';
|
|
2
|
+
import { DefineRegistryResult, RenderErrorHandler } from '@json-render/vue';
|
|
3
|
+
import { StateStore } from '@json-render/core';
|
|
4
|
+
import { ActorProviderProps } from './types.js';
|
|
5
|
+
export type { ActorProviderProps } from './types.js';
|
|
6
|
+
export { getPlayViewContext } from './actor-provider-context.js';
|
|
7
|
+
export type { ViewContextValue } from './actor-provider-context.js';
|
|
8
|
+
declare const _default: import('vue', { with: { "resolution-mode": "import" } }).DefineComponent<import('vue', { with: { "resolution-mode": "import" } }).ExtractPropTypes<{
|
|
9
|
+
actor: {
|
|
10
|
+
type: PropType<ActorProviderProps["actor"]>;
|
|
11
|
+
required: true;
|
|
12
|
+
};
|
|
13
|
+
registryResult: {
|
|
14
|
+
type: PropType<DefineRegistryResult>;
|
|
15
|
+
required: true;
|
|
16
|
+
};
|
|
17
|
+
store: {
|
|
18
|
+
type: PropType<StateStore>;
|
|
19
|
+
default: undefined;
|
|
20
|
+
};
|
|
21
|
+
onRenderError: {
|
|
22
|
+
type: PropType<RenderErrorHandler>;
|
|
23
|
+
default: undefined;
|
|
24
|
+
};
|
|
25
|
+
}>, () => import('vue', { with: { "resolution-mode": "import" } }).VNode<import('vue', { with: { "resolution-mode": "import" } }).RendererNode, import('vue', { with: { "resolution-mode": "import" } }).RendererElement, {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}> | import('vue', { with: { "resolution-mode": "import" } }).VNode<import('vue', { with: { "resolution-mode": "import" } }).RendererNode, import('vue', { with: { "resolution-mode": "import" } }).RendererElement, {
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}>[] | null, {}, {}, {}, import('vue', { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import('vue', { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import('vue', { with: { "resolution-mode": "import" } }).PublicProps, Readonly<import('vue', { with: { "resolution-mode": "import" } }).ExtractPropTypes<{
|
|
30
|
+
actor: {
|
|
31
|
+
type: PropType<ActorProviderProps["actor"]>;
|
|
32
|
+
required: true;
|
|
33
|
+
};
|
|
34
|
+
registryResult: {
|
|
35
|
+
type: PropType<DefineRegistryResult>;
|
|
36
|
+
required: true;
|
|
37
|
+
};
|
|
38
|
+
store: {
|
|
39
|
+
type: PropType<StateStore>;
|
|
40
|
+
default: undefined;
|
|
41
|
+
};
|
|
42
|
+
onRenderError: {
|
|
43
|
+
type: PropType<RenderErrorHandler>;
|
|
44
|
+
default: undefined;
|
|
45
|
+
};
|
|
46
|
+
}>> & Readonly<{}>, {
|
|
47
|
+
store: StateStore;
|
|
48
|
+
onRenderError: RenderErrorHandler;
|
|
49
|
+
}, {}, {}, {}, string, import('vue', { with: { "resolution-mode": "import" } }).ComponentProvideOptions, true, {}, any>;
|
|
50
|
+
export default _default;
|
|
51
|
+
//# sourceMappingURL=ActorProvider.vue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActorProvider.vue.d.ts","sourceRoot":"","sources":["../src/ActorProvider.vue"],"names":[],"mappings":"AAyPA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAIpC,OAAO,KAAK,EAAE,oBAAoB,EAAY,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAKpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrD,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;;;cAqFjD,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;;;;cAIrC,QAAQ,CAAC,oBAAoB,CAAC;;;;cAI9B,QAAQ,CAAC,UAAU,CAAC;;;;cAIlB,QAAQ,CAAC,kBAAkB,CAAC;;;;;;;;;cAZ9B,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;;;;cAIrC,QAAQ,CAAC,oBAAoB,CAAC;;;;cAI9B,QAAQ,CAAC,UAAU,CAAC;;;;cAIlB,QAAQ,CAAC,kBAAkB,CAAC;;;;;;;AAhBjD,wBA0GG"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ViewKey as e } from "./actor-provider-context.js";
|
|
2
|
+
import { provideActor as t } from "./useActor.js";
|
|
3
|
+
import { defineComponent as n, h as r, markRaw as i, onUnmounted as a, provide as o, ref as s, shallowRef as c, toRaw as l } from "vue";
|
|
4
|
+
import { ActionProvider as u, StateProvider as d, VisibilityProvider as f, useStateStore as p } from "@json-render/vue";
|
|
5
|
+
import { watchSignal as m } from "@xmachines/play-signals";
|
|
6
|
+
import { createAtom as h } from "@xstate/store";
|
|
7
|
+
import { xstateStoreStateStore as g } from "@json-render/xstate";
|
|
8
|
+
//#region src/ActorProvider.vue?vue&type=script&lang.ts
|
|
9
|
+
var _ = n({
|
|
10
|
+
name: "ActorProviderInner",
|
|
11
|
+
props: {
|
|
12
|
+
registryResult: {
|
|
13
|
+
type: Object,
|
|
14
|
+
required: !0
|
|
15
|
+
},
|
|
16
|
+
spec: {
|
|
17
|
+
type: Object,
|
|
18
|
+
default: null
|
|
19
|
+
},
|
|
20
|
+
store: {
|
|
21
|
+
type: Object,
|
|
22
|
+
required: !0
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
setup(t, { slots: n }) {
|
|
26
|
+
let i = c(null);
|
|
27
|
+
o(e, new Proxy({}, { get(e, t) {
|
|
28
|
+
return i.value?.[t];
|
|
29
|
+
} }));
|
|
30
|
+
let { update: a, getSnapshot: s } = p(), l = (e) => {
|
|
31
|
+
a(e(s()));
|
|
32
|
+
};
|
|
33
|
+
return () => {
|
|
34
|
+
if (!t.spec) return i.value = null, n.default?.() ?? null;
|
|
35
|
+
let e = t.registryResult.handlers(() => l, () => s());
|
|
36
|
+
return i.value = {
|
|
37
|
+
spec: t.spec,
|
|
38
|
+
handlers: e,
|
|
39
|
+
registry: t.registryResult.registry,
|
|
40
|
+
store: t.store
|
|
41
|
+
}, r(u, { handlers: e }, () => r(f, {}, () => n.default?.() ?? null));
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}), v = n({
|
|
45
|
+
name: "ActorProvider",
|
|
46
|
+
props: {
|
|
47
|
+
actor: {
|
|
48
|
+
type: Object,
|
|
49
|
+
required: !0
|
|
50
|
+
},
|
|
51
|
+
registryResult: {
|
|
52
|
+
type: Object,
|
|
53
|
+
required: !0
|
|
54
|
+
},
|
|
55
|
+
store: {
|
|
56
|
+
type: Object,
|
|
57
|
+
default: void 0
|
|
58
|
+
},
|
|
59
|
+
onRenderError: {
|
|
60
|
+
type: Function,
|
|
61
|
+
default: void 0
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
setup(e, { slots: n }) {
|
|
65
|
+
let o = l(e.actor), c = {
|
|
66
|
+
...l(e.registryResult),
|
|
67
|
+
registry: i(Object.fromEntries(Object.entries(l(e.registryResult).registry).map(([e, t]) => [e, i(t)])))
|
|
68
|
+
};
|
|
69
|
+
e.onRenderError && Object.defineProperty(c.registry, "onRenderError", {
|
|
70
|
+
value: e.onRenderError,
|
|
71
|
+
enumerable: !1,
|
|
72
|
+
configurable: !0
|
|
73
|
+
}), t(o);
|
|
74
|
+
let u = s(o.currentView.get()), f = null, p = null, v = 0, y = m(o.currentView, (e) => {
|
|
75
|
+
u.value = e;
|
|
76
|
+
});
|
|
77
|
+
return a(() => {
|
|
78
|
+
y();
|
|
79
|
+
}), () => {
|
|
80
|
+
if (!u.value) return n.fallback ? n.fallback() : null;
|
|
81
|
+
let t = u.value, i;
|
|
82
|
+
if (e.store) i = e.store;
|
|
83
|
+
else {
|
|
84
|
+
if (f === null || p !== u.value) {
|
|
85
|
+
let e = t.state;
|
|
86
|
+
f = g({ atom: h(typeof e == "object" && e && !Array.isArray(e) && (Object.getPrototypeOf(e) === Object.prototype || Object.getPrototypeOf(e) === null) ? e : {}) }), p = u.value, v++;
|
|
87
|
+
}
|
|
88
|
+
i = f;
|
|
89
|
+
}
|
|
90
|
+
return r(d, {
|
|
91
|
+
store: i,
|
|
92
|
+
key: v
|
|
93
|
+
}, () => r(_, {
|
|
94
|
+
registryResult: c,
|
|
95
|
+
spec: t,
|
|
96
|
+
store: i
|
|
97
|
+
}, n));
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
//#endregion
|
|
102
|
+
export { v as default };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=ActorProvider.vue_vue_type_script_lang.js.map
|