@xmachines/play-vue 1.0.0-beta.8 → 1.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/LICENSE +21 -0
- package/README.md +137 -208
- 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 +125 -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 +27 -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 +19 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/useActor.d.ts +50 -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 +37 -15
- 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,280 +1,209 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- generated-by: gsd-doc-writer -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# `@xmachines/play-vue`
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Vue 3 renderer for the XMachines Play Architecture — passively observes actor signals and renders UI via `@xmachines/json-render-vue`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Part of the [XMachines Play monorepo](../../README.md).
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](package.json)
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
`@xmachines/play-vue` is the Vue 3 rendering layer for XMachines Play. It bridges TC39 Signals (actor state) to Vue reactivity and drives component rendering through `@xmachines/json-render-vue`.
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
- **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
|
|
18
|
+
**Architecture invariants this package upholds:**
|
|
15
19
|
|
|
16
|
-
**
|
|
20
|
+
- **Passive Infrastructure** — Components observe actor signals; they never decide state transitions.
|
|
21
|
+
- **Signal-Only Reactivity** — TC39 Signals are the source of truth; Vue reactivity is used only to trigger re-renders.
|
|
22
|
+
- **Actor Authority** — The actor controls view selection; the renderer reflects it.
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
---
|
|
19
25
|
|
|
20
26
|
## Installation
|
|
21
27
|
|
|
22
28
|
```bash
|
|
23
|
-
|
|
24
|
-
npm install @xmachines/play-vue
|
|
29
|
+
pnpm add @xmachines/play-vue
|
|
25
30
|
```
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
**Peer dependencies** (install alongside):
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
**Peer dependencies:**
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add vue@^3.5.0 xstate@^5.31.0 @xstate/store@^3.17.0 @xmachines/json-render-vue@^0.18.0 @xmachines/json-render-core@^0.18.0 @xmachines/json-render-xstate@^0.18.0
|
|
36
|
+
```
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
---
|
|
35
39
|
|
|
36
40
|
## Quick Start
|
|
37
41
|
|
|
38
42
|
```vue
|
|
39
43
|
<!-- App.vue -->
|
|
44
|
+
<template>
|
|
45
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
46
|
+
<PlayRenderer />
|
|
47
|
+
</PlayUIProvider>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
40
50
|
<script setup lang="ts">
|
|
51
|
+
import { defineRegistry, PlayUIProvider, PlayRenderer } from "@xmachines/play-vue";
|
|
41
52
|
import { definePlayer } from "@xmachines/play-xstate";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import
|
|
45
|
-
import
|
|
46
|
-
import Dashboard from "./components/Dashboard.vue";
|
|
47
|
-
|
|
48
|
-
// 1. Define catalog (business logic layer)
|
|
49
|
-
const catalog = defineCatalog({
|
|
50
|
-
LoginForm: z.object({ error: z.string().optional() }),
|
|
51
|
-
Dashboard: z.object({
|
|
52
|
-
userId: z.string(),
|
|
53
|
-
username: z.string(),
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// 2. Define component map (view layer)
|
|
58
|
-
const components = {
|
|
59
|
-
LoginForm,
|
|
60
|
-
Dashboard,
|
|
61
|
-
};
|
|
53
|
+
import { myMachine } from "./machine.js";
|
|
54
|
+
import { myCatalog } from "./catalog.js";
|
|
55
|
+
import HomeSFC from "./views/Home.vue";
|
|
56
|
+
import LoginSFC from "./views/Login.vue";
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
const createPlayer = definePlayer({ machine: authMachine, catalog });
|
|
58
|
+
const createPlayer = definePlayer({ machine: myMachine });
|
|
65
59
|
const actor = createPlayer();
|
|
66
60
|
actor.start();
|
|
67
|
-
</script>
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
const registryResult = defineRegistry(myCatalog, {
|
|
63
|
+
components: {
|
|
64
|
+
Home: HomeSFC, // .vue SFCs are auto-wrapped
|
|
65
|
+
Login: LoginSFC,
|
|
66
|
+
},
|
|
67
|
+
actions: {
|
|
68
|
+
login: async (args) => actor.send({ type: "auth.login", ...args }),
|
|
69
|
+
logout: async () => actor.send({ type: "auth.logout" }),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
77
73
|
```
|
|
78
74
|
|
|
79
|
-
|
|
75
|
+
---
|
|
80
76
|
|
|
81
|
-
|
|
77
|
+
## API Summary
|
|
82
78
|
|
|
83
|
-
|
|
79
|
+
### Components
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
interface PlayRendererProps {
|
|
87
|
-
actor: AbstractActor<any>;
|
|
88
|
-
components: Record<string, Component>;
|
|
89
|
-
}
|
|
90
|
-
```
|
|
81
|
+
#### `<PlayUIProvider>`
|
|
91
82
|
|
|
92
|
-
**
|
|
83
|
+
Batteries-included composite provider. Wraps `<ActorProvider>` and `JSONUIProvider` in one component. **Recommended for most apps.**
|
|
93
84
|
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
| Prop | Type | Required | Description |
|
|
86
|
+
| --------------------- | -------------------------- | -------- | ------------------------------------------- |
|
|
87
|
+
| `actor` | `AbstractActor & Viewable` | ✅ | The XMachines actor instance |
|
|
88
|
+
| `registryResult` | `DefineRegistryResult` | ✅ | Result of `defineRegistry()` |
|
|
89
|
+
| `store` | `StateStore` | — | External controlled state store (optional) |
|
|
90
|
+
| `onRenderError` | `RenderErrorHandler` | — | Error handler for render failures |
|
|
91
|
+
| `navigate` | `(path: string) => void` | — | Link navigation function |
|
|
92
|
+
| `validationFunctions` | `Record<string, Function>` | — | Custom validation functions |
|
|
93
|
+
| `functions` | `Record<string, Function>` | — | Named functions for `$computed` expressions |
|
|
96
94
|
|
|
97
|
-
**Slots:**
|
|
95
|
+
**Slots:** `default` (rendered content), `fallback` (shown while actor view is `null`)
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
#### `<PlayRenderer>`
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
Zero-prop leaf component. Reads the current `spec` and `registry` from the nearest `<ActorProvider>` or `<PlayUIProvider>` context and renders via `<Renderer>`. Must be placed inside one of those providers.
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
```vue
|
|
102
|
+
<PlayUIProvider :actor="actor" :registryResult="registryResult">
|
|
103
|
+
<PlayRenderer />
|
|
104
|
+
</PlayUIProvider>
|
|
105
|
+
```
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
#### `<ActorProvider>`
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
<script setup lang="ts">
|
|
111
|
-
import type { AbstractActor } from "@xmachines/play-actor";
|
|
112
|
-
import { ref } from "vue";
|
|
113
|
-
|
|
114
|
-
const props = defineProps<{
|
|
115
|
-
error?: string;
|
|
116
|
-
send: AbstractActor<any>["send"];
|
|
117
|
-
}>();
|
|
118
|
-
|
|
119
|
-
const username = ref("");
|
|
120
|
-
|
|
121
|
-
function handleSubmit() {
|
|
122
|
-
props.send({
|
|
123
|
-
type: "auth.login",
|
|
124
|
-
username: username.value,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
</script>
|
|
109
|
+
Low-level escape hatch for custom provider composition. Owns the full actor lifecycle — signal subscription, per-view state store, handler resolution, and Vue context provision. Use `<PlayUIProvider>` unless you need fine-grained control.
|
|
128
110
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</template>
|
|
136
|
-
```
|
|
111
|
+
| Prop | Type | Required | Description |
|
|
112
|
+
| ---------------- | -------------------------- | -------- | ------------------------------- |
|
|
113
|
+
| `actor` | `AbstractActor & Viewable` | ✅ | The XMachines actor instance |
|
|
114
|
+
| `registryResult` | `DefineRegistryResult` | ✅ | Result of `defineRegistry()` |
|
|
115
|
+
| `store` | `StateStore` | — | External controlled state store |
|
|
116
|
+
| `onRenderError` | `RenderErrorHandler` | — | Override render error handler |
|
|
137
117
|
|
|
138
|
-
|
|
118
|
+
---
|
|
139
119
|
|
|
140
|
-
###
|
|
120
|
+
### Functions
|
|
141
121
|
|
|
142
|
-
|
|
143
|
-
<!-- App.vue -->
|
|
144
|
-
<script setup lang="ts">
|
|
145
|
-
import { PlayVueRouterProvider } from "@xmachines/play-vue-router";
|
|
146
|
-
import { PlayRenderer } from "@xmachines/play-vue";
|
|
147
|
-
import { provide } from "vue";
|
|
148
|
-
import Header from "./components/Header.vue";
|
|
149
|
-
import Footer from "./components/Footer.vue";
|
|
150
|
-
|
|
151
|
-
// Provide actor to nested components like Header
|
|
152
|
-
provide("actor", actor);
|
|
153
|
-
</script>
|
|
122
|
+
#### `defineRegistry(catalog, options)`
|
|
154
123
|
|
|
155
|
-
|
|
156
|
-
<PlayVueRouterProvider :actor="actor" :router="router" :routeMap="routeMap">
|
|
157
|
-
<template #default="{ currentActor, currentRouter }">
|
|
158
|
-
<div>
|
|
159
|
-
<Header />
|
|
160
|
-
<PlayRenderer :actor="currentActor" :components="components" />
|
|
161
|
-
<Footer />
|
|
162
|
-
</div>
|
|
163
|
-
</template>
|
|
164
|
-
</PlayVueRouterProvider>
|
|
165
|
-
</template>
|
|
166
|
-
```
|
|
124
|
+
Drop-in replacement for `defineRegistry` from `@xmachines/json-render-vue`. **Always import from `@xmachines/play-vue`** rather than `@xmachines/json-render-vue` when working with Vue SFCs — this wrapper automatically detects `.vue` SFCs in the `components` map and wraps them via `h()` so Vue composables (including `inject`-based ones) work correctly inside `<script setup>`.
|
|
167
125
|
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
import
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
pending = false;
|
|
186
|
-
for (const s of watcher.getPending()) s.get();
|
|
187
|
-
route.value = actor.currentRoute.get();
|
|
188
|
-
watcher.watch(actor.currentRoute);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
route.value = actor.currentRoute.get();
|
|
193
|
-
watcher.watch(actor.currentRoute);
|
|
126
|
+
```typescript
|
|
127
|
+
import { defineRegistry } from "@xmachines/play-vue";
|
|
128
|
+
// NOT: import { defineRegistry } from "@xmachines/json-render-vue"
|
|
129
|
+
|
|
130
|
+
import LoginSFC from "./views/Login.vue";
|
|
131
|
+
import DashboardSFC from "./views/Dashboard.vue";
|
|
132
|
+
|
|
133
|
+
const registryResult = defineRegistry(catalog, {
|
|
134
|
+
components: {
|
|
135
|
+
Login: LoginSFC, // .vue SFC — auto-wrapped via h()
|
|
136
|
+
Dashboard: DashboardSFC,
|
|
137
|
+
},
|
|
138
|
+
actions: {
|
|
139
|
+
login: async (args, setState, getState) => {
|
|
140
|
+
/* ... */
|
|
141
|
+
},
|
|
142
|
+
},
|
|
194
143
|
});
|
|
144
|
+
```
|
|
195
145
|
|
|
196
|
-
|
|
197
|
-
if (watcher) watcher.unwatch(actor.currentRoute);
|
|
198
|
-
});
|
|
199
|
-
</script>
|
|
146
|
+
Plain `ComponentFn` functions (non-SFC) also work and are passed through unchanged. Mixing SFCs and plain functions in the same registry is supported.
|
|
200
147
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
148
|
+
#### `useActor()`
|
|
149
|
+
|
|
150
|
+
Vue composable for accessing the raw actor inside a `PlayRenderer` tree. Avoids prop drilling for deeply nested components.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { useActor } from "@xmachines/play-vue";
|
|
154
|
+
|
|
155
|
+
// Inside a component rendered by PlayRenderer:
|
|
156
|
+
const actor = useActor();
|
|
157
|
+
actor.send({ type: "SUBMIT" });
|
|
206
158
|
```
|
|
207
159
|
|
|
208
|
-
|
|
160
|
+
Throws if called outside an `<ActorProvider>` or `<PlayUIProvider>` tree.
|
|
209
161
|
|
|
210
|
-
|
|
162
|
+
#### `getPlayViewContext()`
|
|
211
163
|
|
|
212
|
-
|
|
213
|
-
- No ref/reactive for business state
|
|
214
|
-
- No watch/watchEffect for business side effects
|
|
215
|
-
- Vue only triggers renders, doesn't control state
|
|
164
|
+
Access the current `ViewContextValue` — `{ spec, handlers, registry, store }` — from inside an `<ActorProvider>` tree.
|
|
216
165
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- `actor.currentRoute.get()` provides navigation state
|
|
220
|
-
- Components observe signals via explicit watcher patterns
|
|
166
|
+
```typescript
|
|
167
|
+
import { getPlayViewContext } from "@xmachines/play-vue";
|
|
221
168
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
169
|
+
// Inside setup() of a component within an ActorProvider tree:
|
|
170
|
+
const view = getPlayViewContext();
|
|
171
|
+
// view.spec, view.handlers, view.registry, view.store
|
|
172
|
+
```
|
|
226
173
|
|
|
227
|
-
|
|
228
|
-
- `Signal.subtle.Watcher` coalesces rapid signal changes
|
|
229
|
-
- Prevents Vue thrashing from multiple signal updates
|
|
230
|
-
- Single Vue render per microtask batch
|
|
174
|
+
---
|
|
231
175
|
|
|
232
|
-
|
|
233
|
-
- Component teardown calls watcher `unwatch` in `onUnmounted`
|
|
234
|
-
- Do not rely on GC-only cleanup
|
|
176
|
+
### Re-exported from `@xmachines/json-render-vue`
|
|
235
177
|
|
|
236
|
-
|
|
178
|
+
The following are re-exported so consumers import everything from `@xmachines/play-vue`:
|
|
237
179
|
|
|
238
|
-
|
|
239
|
-
- Enables composition with navigation, headers, footers
|
|
240
|
-
- Supports multiple renderers in same app
|
|
180
|
+
**Components:** `JSONUIProvider`, `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`, `Renderer`
|
|
241
181
|
|
|
242
|
-
**
|
|
182
|
+
**Composables:** `useBoundProp`
|
|
243
183
|
|
|
244
|
-
|
|
245
|
-
- **Passive Infrastructure (INV-04):** Components reflect, never decide
|
|
184
|
+
**Types:** `JSONUIProviderProps`, `StateProviderProps`, `ActionProviderProps`, `ValidationProviderProps`, `RendererProps`, `ComponentFn`, `ComponentContext`, `DefineRegistryResult`
|
|
246
185
|
|
|
247
|
-
|
|
186
|
+
---
|
|
248
187
|
|
|
249
|
-
|
|
188
|
+
## Testing
|
|
250
189
|
|
|
251
|
-
|
|
252
|
-
2. Schedule work with `queueMicrotask`
|
|
253
|
-
3. Drain `watcher.getPending()`
|
|
254
|
-
4. Read actor signals and update Vue-local ref state
|
|
255
|
-
5. Re-arm with `watch(...)` or `watch()`
|
|
190
|
+
Run tests for this package in isolation:
|
|
256
191
|
|
|
257
|
-
|
|
192
|
+
```bash
|
|
193
|
+
# From the monorepo root
|
|
194
|
+
pnpm --filter @xmachines/play-vue test
|
|
258
195
|
|
|
259
|
-
|
|
196
|
+
# Watch mode
|
|
197
|
+
pnpm --filter @xmachines/play-vue run test:watch
|
|
260
198
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
- **Performance:** Microtask batching reduces unnecessary renders
|
|
265
|
-
- **Composability:** Renderer prop enables complex layouts
|
|
199
|
+
# With coverage (80% threshold enforced on lines, functions, branches, statements)
|
|
200
|
+
pnpm exec vitest run --coverage --config packages/play-vue/vitest.config.ts
|
|
201
|
+
```
|
|
266
202
|
|
|
267
|
-
|
|
203
|
+
Tests use [Vitest](https://vitest.dev/) with `jsdom` environment and `@vue/test-utils` for component mounting.
|
|
268
204
|
|
|
269
|
-
|
|
270
|
-
- **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
|
|
271
|
-
- **[@xmachines/play-vue-router](../play-vue-router)** - Vue Router integration
|
|
272
|
-
- **[@xmachines/play-actor](../play-actor)** - Actor base
|
|
273
|
-
- **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
|
|
205
|
+
---
|
|
274
206
|
|
|
275
207
|
## License
|
|
276
208
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
This work is licensed under the terms of the MIT license.
|
|
280
|
-
For a copy, see <https://opensource.org/licenses/MIT>.
|
|
209
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -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 {\n\tdefineComponent,\n\tref,\n\tcomputed,\n\ttoRaw,\n\tmarkRaw,\n\tonUnmounted,\n\th,\n\tprovide,\n\tshallowRef,\n\twatch,\n} from \"vue\";\nimport type { PropType } from \"vue\";\nimport { watchSignal } from \"@xmachines/play-signals\";\nimport { toAtomState, attachRenderErrorHandler } from \"@xmachines/play-actor\";\nimport type { AbstractActor, Viewable, PlaySpec } from \"@xmachines/play-actor\";\nimport type { AnyActorLogic } from \"xstate\";\nimport type {\n\tDefineRegistryResult,\n\tSetState,\n\tRenderErrorHandler,\n} from \"@xmachines/json-render-vue\";\n\nimport {\n\tStateProvider,\n\tuseStateStore,\n\tActionProvider,\n\tVisibilityProvider,\n} from \"@xmachines/json-render-vue\";\nimport type { StateStore } from \"@xmachines/json-render-core\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@xmachines/json-render-xstate\";\nimport { provideActor, type AnyPlayActor } 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 stateStore = useStateStore();\n\n\t\t// Build a SetState adapter: handlers factory expects updater-function pattern.\n\t\t// Defined once in setup() — reads stateStore.getSnapshot/update at invocation\n\t\t// time so it always operates on the live store state without needing to be\n\t\t// recreated when stateStore identity changes across renders.\n\t\tconst setStateAdapter: SetState = (updater) => {\n\t\t\tconst prev = stateStore.getSnapshot();\n\t\t\tstateStore.update(updater(prev));\n\t\t};\n\n\t\t// Memoize handlers keyed to registryResult prop identity. The getter functions\n\t\t// are stable closures capturing stateStore from setup(), so they do not\n\t\t// contribute to invalidation. Handlers are only recomputed when the registry\n\t\t// definition itself changes — not on every render triggered by view updates.\n\t\tconst handlers = computed(() =>\n\t\t\tprops.registryResult.handlers(\n\t\t\t\t() => setStateAdapter,\n\t\t\t\t() => stateStore.getSnapshot(),\n\t\t\t),\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\tviewRef.value = {\n\t\t\t\tspec: props.spec,\n\t\t\t\thandlers: handlers.value,\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: handlers.value }, () =>\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\t// Held in a shallowRef so the provided injection and the signal watcher\n\t\t// can follow `props.actor` when the prop swaps after mount.\n\t\tconst actorRef = shallowRef(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 rawRegistry = Object.fromEntries(\n\t\t\tObject.entries(toRaw(props.registryResult).registry).map(([k, v]) => [\n\t\t\t\tk,\n\t\t\t\tmarkRaw(v as object),\n\t\t\t]),\n\t\t) as DefineRegistryResult[\"registry\"];\n\n\t\t// Inject onRenderError prop into registry (non-enumerable, overrides defineRegistry-level\n\t\t// handler). attachRenderErrorHandler clones — the caller's registry is never mutated.\n\t\t// The clone must be re-markRaw'd: markRaw's non-enumerable flag is not carried by the clone.\n\t\tconst rawRegistryResult: DefineRegistryResult = {\n\t\t\t...toRaw(props.registryResult),\n\t\t\tregistry: markRaw(\n\t\t\t\tprops.onRenderError\n\t\t\t\t\t? attachRenderErrorHandler(rawRegistry, props.onRenderError)\n\t\t\t\t\t: rawRegistry,\n\t\t\t),\n\t\t};\n\n\t\t// Provide the actor to all descendants via Vue's provide/inject mechanism.\n\t\t//\n\t\t// A stable Proxy (same pattern as ActorProviderInner's ViewKey proxy) is\n\t\t// provided instead of the raw actor so the injection tracks the CURRENT\n\t\t// `props.actor`: injectors keep the reference obtained at setup() time,\n\t\t// yet every trap resolves against the latest actor (actorRef.value) after a\n\t\t// prop swap. All relevant traps are forwarded so the proxy behaves like the\n\t\t// underlying actor for reads, writes, membership (`in`), enumeration\n\t\t// (Object.keys / spread), and prototype checks (`instanceof`). Methods are\n\t\t// bound to the current raw actor so `this` (including private fields) works\n\t\t// exactly as with a direct call.\n\t\t//\n\t\t// IMPORTANT: this proxy is NOT identity-equal to `props.actor` — it is a\n\t\t// distinct object, hence a distinct WeakMap key. Identity-keyed consumers —\n\t\t// notably RouterBridgeBase's one-bridge-per-actor guard, which keys a\n\t\t// module-level WeakMap on actor identity — must be handed the `props.actor`\n\t\t// value itself, never this injected proxy.\n\t\t//\n\t\t// Method identity: bound methods are cached per underlying method so repeated\n\t\t// reads return the SAME function (actor.send === actor.send — allocation-free\n\t\t// hot path). A read rebinds only when the underlying method changes; the cache\n\t\t// is CLEARED on actor swap (see the watch handler below) so prototype methods\n\t\t// rebind to the new actor.\n\t\tconst boundMethodCache = new Map<PropertyKey, { source: unknown; bound: unknown }>();\n\t\tprovideActor(\n\t\t\tnew Proxy({} as AnyPlayActor, {\n\t\t\t\tget(_target, prop) {\n\t\t\t\t\tconst current = actorRef.value;\n\t\t\t\t\tconst value = Reflect.get(current, prop, current) as unknown;\n\t\t\t\t\tif (typeof value !== \"function\") return value;\n\t\t\t\t\tconst cached = boundMethodCache.get(prop);\n\t\t\t\t\tif (cached && cached.source === value) return cached.bound;\n\t\t\t\t\tconst bound = (value as (...args: unknown[]) => unknown).bind(current);\n\t\t\t\t\tboundMethodCache.set(prop, { source: value, bound });\n\t\t\t\t\treturn bound;\n\t\t\t\t},\n\t\t\t\thas(_target, prop) {\n\t\t\t\t\treturn prop in actorRef.value;\n\t\t\t\t},\n\t\t\t\tset(_target, prop, value) {\n\t\t\t\t\treturn Reflect.set(actorRef.value, prop, value, actorRef.value);\n\t\t\t\t},\n\t\t\t\tdeleteProperty(_target, prop) {\n\t\t\t\t\treturn Reflect.deleteProperty(actorRef.value, prop);\n\t\t\t\t},\n\t\t\t\townKeys() {\n\t\t\t\t\treturn Reflect.ownKeys(actorRef.value);\n\t\t\t\t},\n\t\t\t\tgetOwnPropertyDescriptor(_target, prop) {\n\t\t\t\t\tconst desc = Reflect.getOwnPropertyDescriptor(actorRef.value, prop);\n\t\t\t\t\tif (!desc) return undefined;\n\t\t\t\t\t// Proxy invariant: the proxy target is an empty {}, so every key\n\t\t\t\t\t// reported via ownKeys must resolve to a configurable descriptor here\n\t\t\t\t\t// — otherwise the [[GetOwnProperty]] invariant throws (a non-existent\n\t\t\t\t\t// target property may not be reported as non-configurable). The actor's\n\t\t\t\t\t// own props are runtime-mutable anyway, so reporting configurable:true\n\t\t\t\t\t// is faithful enough for Object.keys / spread enumeration.\n\t\t\t\t\tdesc.configurable = true;\n\t\t\t\t\treturn desc;\n\t\t\t\t},\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\treturn Reflect.getPrototypeOf(actorRef.value);\n\t\t\t\t},\n\t\t\t}) as AnyPlayActor,\n\t\t);\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>(actorRef.value.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\t// Re-assigned on actor swap so cleanup always releases the live watcher.\n\t\tlet unwatch = watchSignal(actorRef.value.currentView, (nextView) => {\n\t\t\tview.value = nextView;\n\t\t});\n\n\t\t// React to `props.actor` swaps: unsubscribe the OLD actor's currentView,\n\t\t// re-seed the view from the NEW actor synchronously (flush: \"sync\" — no\n\t\t// scheduler gap, matching the mount-time seed+watch pattern above), and\n\t\t// subscribe the new signal. The re-seeded view is a new object identity,\n\t\t// so the internal-store branch below rebuilds the store and bumps\n\t\t// storeKey exactly like a regular view transition.\n\t\twatch(\n\t\t\t() => toRaw(props.actor as AbstractActor<AnyActorLogic> & Viewable),\n\t\t\t(nextActor) => {\n\t\t\t\tif (nextActor === actorRef.value) return;\n\t\t\t\tunwatch();\n\t\t\t\tactorRef.value = nextActor;\n\t\t\t\t// Invalidate cached bound methods so prototype methods rebind to the\n\t\t\t\t// new actor on the next read (see the injected proxy's get trap).\n\t\t\t\tboundMethodCache.clear();\n\t\t\t\tview.value = nextActor.currentView.get();\n\t\t\t\tunwatch = watchSignal(nextActor.currentView, (nextView) => {\n\t\t\t\t\tview.value = nextView;\n\t\t\t\t});\n\t\t\t},\n\t\t\t{ flush: \"sync\" },\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\tinternalStore = xstateStoreStateStore({\n\t\t\t\t\t\tatom: createAtom(toAtomState(spec.state)),\n\t\t\t\t\t});\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 '@xmachines/json-render-vue';
|
|
3
|
+
import { StateStore } from '@xmachines/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/runtime-core').DefineComponent<import('@vue/runtime-core').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/runtime-core').VNode<import('@vue/runtime-core').RendererNode, import('@vue/runtime-core').RendererElement, {
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}> | import('@vue/runtime-core').VNode<import('@vue/runtime-core').RendererNode, import('@vue/runtime-core').RendererElement, {
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}>[] | null, {}, {}, {}, import('@vue/runtime-core').ComponentOptionsMixin, import('@vue/runtime-core').ComponentOptionsMixin, {}, string, import('@vue/runtime-core').PublicProps, Readonly<import('@vue/runtime-core').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
|
+
onRenderError: RenderErrorHandler;
|
|
48
|
+
store: StateStore;
|
|
49
|
+
}, {}, {}, {}, string, import('@vue/runtime-core').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":"AAkXA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAKpC,OAAO,KAAK,EACX,oBAAoB,EAEpB,kBAAkB,EAClB,MAAM,4BAA4B,CAAC;AAQpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAK9D,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;;;cA8FjD,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,wBA0LG"}
|