@mmapp/react 0.1.0-alpha.1

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.
Files changed (98) hide show
  1. package/dist/atoms/index.d.mts +424 -0
  2. package/dist/atoms/index.d.ts +424 -0
  3. package/dist/atoms/index.js +195 -0
  4. package/dist/atoms/index.mjs +110 -0
  5. package/dist/chunk-2VJQJM7S.mjs +119 -0
  6. package/dist/index.d.mts +6999 -0
  7. package/dist/index.d.ts +6999 -0
  8. package/dist/index.js +8247 -0
  9. package/dist/index.mjs +8016 -0
  10. package/package.json +37 -0
  11. package/package.json.backup +41 -0
  12. package/src/Blueprint.ts +216 -0
  13. package/src/__tests__/Blueprint.test.ts +106 -0
  14. package/src/__tests__/action-context.test.ts +166 -0
  15. package/src/__tests__/actionCreators.test.ts +179 -0
  16. package/src/__tests__/builders.test.ts +336 -0
  17. package/src/__tests__/defineBlueprint-composition.test.ts +106 -0
  18. package/src/__tests__/factories.test.ts +229 -0
  19. package/src/__tests__/loader.test.ts +159 -0
  20. package/src/__tests__/logger.test.ts +70 -0
  21. package/src/__tests__/type-inference.test.ts +160 -0
  22. package/src/__tests__/typed-transitions.test.ts +126 -0
  23. package/src/__tests__/useModuleConfig.test.ts +61 -0
  24. package/src/actionCreators.ts +132 -0
  25. package/src/actions.ts +547 -0
  26. package/src/atoms/index.ts +600 -0
  27. package/src/authoring.ts +92 -0
  28. package/src/browser-player.ts +783 -0
  29. package/src/builders.ts +1342 -0
  30. package/src/components/ExperienceWorkflowBridge.tsx +123 -0
  31. package/src/components/PlayerProvider.tsx +43 -0
  32. package/src/components/atoms/index.tsx +269 -0
  33. package/src/components/index.ts +36 -0
  34. package/src/conditions.ts +692 -0
  35. package/src/config/defineBlueprint.ts +329 -0
  36. package/src/config/defineModel.ts +753 -0
  37. package/src/config/defineWorkspace.ts +24 -0
  38. package/src/core/WorkflowRuntime.ts +153 -0
  39. package/src/factories.ts +425 -0
  40. package/src/grammar/index.ts +173 -0
  41. package/src/hooks/index.ts +106 -0
  42. package/src/hooks/useAuth.ts +288 -0
  43. package/src/hooks/useChannel.ts +304 -0
  44. package/src/hooks/useComputed.ts +154 -0
  45. package/src/hooks/useDomainSubscription.ts +110 -0
  46. package/src/hooks/useDuringAction.ts +99 -0
  47. package/src/hooks/useExperienceState.ts +59 -0
  48. package/src/hooks/useExpressionLibrary.ts +129 -0
  49. package/src/hooks/useForm.ts +352 -0
  50. package/src/hooks/useGeolocation.ts +207 -0
  51. package/src/hooks/useMapView.ts +259 -0
  52. package/src/hooks/useMiddleware.ts +291 -0
  53. package/src/hooks/useModel.ts +363 -0
  54. package/src/hooks/useModule.ts +59 -0
  55. package/src/hooks/useModuleConfig.ts +61 -0
  56. package/src/hooks/useMutation.ts +237 -0
  57. package/src/hooks/useNotification.ts +151 -0
  58. package/src/hooks/useOnChange.ts +30 -0
  59. package/src/hooks/useOnEnter.ts +59 -0
  60. package/src/hooks/useOnEvent.ts +37 -0
  61. package/src/hooks/useOnExit.ts +27 -0
  62. package/src/hooks/useOnTransition.ts +30 -0
  63. package/src/hooks/usePackage.ts +128 -0
  64. package/src/hooks/useParams.ts +33 -0
  65. package/src/hooks/usePlayer.ts +308 -0
  66. package/src/hooks/useQuery.ts +184 -0
  67. package/src/hooks/useRealtimeQuery.ts +222 -0
  68. package/src/hooks/useRole.ts +191 -0
  69. package/src/hooks/useRouteParams.ts +100 -0
  70. package/src/hooks/useRouter.ts +347 -0
  71. package/src/hooks/useServerAction.ts +178 -0
  72. package/src/hooks/useServerState.ts +284 -0
  73. package/src/hooks/useToast.ts +164 -0
  74. package/src/hooks/useTransition.ts +39 -0
  75. package/src/hooks/useView.ts +102 -0
  76. package/src/hooks/useWhileIn.ts +48 -0
  77. package/src/hooks/useWorkflow.ts +63 -0
  78. package/src/index.ts +465 -0
  79. package/src/loader/experience-workflow-loader.ts +192 -0
  80. package/src/loader/index.ts +6 -0
  81. package/src/local/LocalEngine.ts +388 -0
  82. package/src/local/LocalEngineAdapter.ts +175 -0
  83. package/src/local/LocalEngineContext.ts +30 -0
  84. package/src/logger.ts +37 -0
  85. package/src/mixins.ts +1160 -0
  86. package/src/providers/RuntimeContext.ts +20 -0
  87. package/src/providers/WorkflowProvider.tsx +28 -0
  88. package/src/routing/instance-key.ts +107 -0
  89. package/src/server/transition-context.ts +172 -0
  90. package/src/testing/index.ts +9 -0
  91. package/src/testing/useBlueprintTestRunner.ts +91 -0
  92. package/src/testing/useGraphAnalysis.ts +18 -0
  93. package/src/testing/useTestRunner.ts +77 -0
  94. package/src/testing.ts +995 -0
  95. package/src/types/workflow-inference.ts +158 -0
  96. package/src/types.ts +114 -0
  97. package/tsconfig.json +27 -0
  98. package/vitest.config.ts +8 -0
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@mmapp/react",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "React integration for the MindMatrix Player — hooks, components, and WebSocket bridge for browser-side workflow engines",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./atoms": {
15
+ "types": "./dist/atoms/index.d.ts",
16
+ "import": "./dist/atoms/index.mjs",
17
+ "require": "./dist/atoms/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts --external react",
22
+ "dev": "tsup src/index.ts --format cjs,esm --dts --external react --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest --watch",
25
+ "type-check": "tsc --noEmit"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18.0.0",
29
+ "@tanstack/react-query": ">=5.0.0"
30
+ },
31
+ "dependencies": {
32
+ "@mmapp/player-core": "^0.1.0-alpha.1"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@mindmatrix/react",
3
+ "version": "0.1.0",
4
+ "description": "React integration for the MindMatrix Player — hooks, components, and WebSocket bridge for browser-side workflow engines",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./atoms": {
15
+ "types": "./dist/atoms/index.d.ts",
16
+ "import": "./dist/atoms/index.mjs",
17
+ "require": "./dist/atoms/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts --external react",
22
+ "dev": "tsup src/index.ts --format cjs,esm --dts --external react --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest --watch",
25
+ "type-check": "tsc --noEmit"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18.0.0",
29
+ "@tanstack/react-query": ">=5.0.0"
30
+ },
31
+ "dependencies": {
32
+ "@mindmatrix/player-core": "workspace:*"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^19.0.0",
36
+ "react": "^19.0.0",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.4.0",
39
+ "vitest": "^1.5.0"
40
+ }
41
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Blueprint — base class for declarative workflow module authoring.
3
+ *
4
+ * Users extend this class and override define() to declare models,
5
+ * views, server actions, and sub-modules using a clean imperative API.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { Blueprint, field, state, transition } from '@mindmatrix/react';
10
+ *
11
+ * class AuthModule extends Blueprint {
12
+ * slug = 'mod-authentication';
13
+ * version = '1.0.0';
14
+ * category = 'module';
15
+ *
16
+ * define() {
17
+ * this.model('authentication', m => m
18
+ * .field('email', field.email().required())
19
+ * .state('unauthenticated', state.initial())
20
+ * .state('authenticated')
21
+ * .transition('login', transition.from('unauthenticated').to('authenticated'))
22
+ * );
23
+ * this.view('login', { route: '/login', component: 'LoginForm' });
24
+ * this.serverAction('authenticate', { handler: 'actions/auth.server.ts' });
25
+ * }
26
+ * }
27
+ *
28
+ * export default new AuthModule().build();
29
+ * ```
30
+ */
31
+
32
+ import {
33
+ type ModelDefinition,
34
+ } from './config/defineModel';
35
+ import { ModelBuilder } from './builders';
36
+ import { defineBlueprint, type BlueprintConfig, type BlueprintManifest } from './config/defineBlueprint';
37
+
38
+ // =============================================================================
39
+ // Types
40
+ // =============================================================================
41
+
42
+ /** A view declaration within a blueprint. */
43
+ export interface BlueprintViewDeclaration {
44
+ /** Route path (e.g., '/login'). */
45
+ route?: string;
46
+ /** Component reference (file path or component name). */
47
+ component: string;
48
+ /** Route guard expression. */
49
+ guard?: string;
50
+ /** Label for navigation. */
51
+ label?: string;
52
+ /** Route group. */
53
+ group?: string;
54
+ /** Icon name. */
55
+ icon?: string;
56
+ }
57
+
58
+ /** A server action declaration within a blueprint. */
59
+ export interface BlueprintServerActionDeclaration {
60
+ /** Handler file path. */
61
+ handler: string;
62
+ /** Function name in the handler file. */
63
+ functionName?: string;
64
+ /** Description. */
65
+ description?: string;
66
+ }
67
+
68
+ // =============================================================================
69
+ // Blueprint Base Class
70
+ // =============================================================================
71
+
72
+ export abstract class Blueprint {
73
+ /** Unique identifier (kebab-case). Must be set by subclass. */
74
+ abstract slug: string;
75
+ /** Semantic version. Must be set by subclass. */
76
+ abstract version: string;
77
+
78
+ /** Display name (defaults to slug). */
79
+ name?: string;
80
+ /** Description. */
81
+ description?: string;
82
+ /** Category or categories. */
83
+ category?: string | string[];
84
+ /** Author. */
85
+ author?: string;
86
+ /** Tags. */
87
+ tags?: string[];
88
+
89
+ // Internal collections populated by define()
90
+ private _models = new Map<string, ModelDefinition>();
91
+ private _views = new Map<string, BlueprintViewDeclaration>();
92
+ private _serverActions = new Map<string, BlueprintServerActionDeclaration>();
93
+ private _dependencies: { slug: string; version?: string }[] = [];
94
+ private _capabilities: string[] = [];
95
+
96
+ /**
97
+ * Override this method to declare models, views, actions, etc.
98
+ * Called automatically by build().
99
+ */
100
+ abstract define(): void;
101
+
102
+ /**
103
+ * Declare a model within this blueprint.
104
+ *
105
+ * @param name - Model name (will be prefixed with blueprint slug)
106
+ * @param builderFn - Callback that configures a ModelBuilder
107
+ */
108
+ protected model(
109
+ name: string,
110
+ builderFn: (m: ModelBuilder) => ModelBuilder,
111
+ ): void {
112
+ const slug = `${this.slug}-${name}`;
113
+ const builder = new ModelBuilder(slug);
114
+ const configured = builderFn(builder);
115
+ this._models.set(name, configured.build());
116
+ }
117
+
118
+ /**
119
+ * Declare a view/page within this blueprint.
120
+ *
121
+ * @param name - View name
122
+ * @param declaration - Route, component, guard, etc.
123
+ */
124
+ protected view(name: string, declaration: BlueprintViewDeclaration): void {
125
+ this._views.set(name, declaration);
126
+ }
127
+
128
+ /**
129
+ * Declare a server action within this blueprint.
130
+ *
131
+ * @param name - Action name (becomes server:{name})
132
+ * @param declaration - Handler file and function
133
+ */
134
+ protected serverAction(name: string, declaration: BlueprintServerActionDeclaration): void {
135
+ this._serverActions.set(name, declaration);
136
+ }
137
+
138
+ /**
139
+ * Declare a dependency on another module/blueprint.
140
+ */
141
+ protected dependency(slug: string, version?: string): void {
142
+ this._dependencies.push({ slug, version });
143
+ }
144
+
145
+ /**
146
+ * Declare a required capability.
147
+ */
148
+ protected capability(...caps: string[]): void {
149
+ this._capabilities.push(...caps);
150
+ }
151
+
152
+ /**
153
+ * Build the blueprint. Calls define(), collects all declarations,
154
+ * and returns a BlueprintManifest suitable for defineBlueprint().
155
+ */
156
+ build(): BlueprintManifest {
157
+ // Call user's define() to populate collections
158
+ this._models.clear();
159
+ this._views.clear();
160
+ this._serverActions.clear();
161
+ this._dependencies = [];
162
+ this._capabilities = [];
163
+ this.define();
164
+
165
+ // Build blueprint config
166
+ const config: BlueprintConfig = {
167
+ slug: this.slug,
168
+ name: this.name || this.slug,
169
+ version: this.version,
170
+ description: this.description,
171
+ author: this.author,
172
+ tags: this.tags,
173
+ category: this.category || 'blueprint',
174
+ models: Array.from(this._models.keys()).map(k => `models/${k}`),
175
+ };
176
+
177
+ // Routes from views
178
+ if (this._views.size > 0) {
179
+ config.routes = Array.from(this._views.entries()).map(([name, v]) => ({
180
+ path: v.route || `/${name}`,
181
+ view: `app/${name}`,
182
+ label: v.label,
183
+ guard: v.guard,
184
+ group: v.group,
185
+ icon: v.icon,
186
+ }));
187
+ }
188
+
189
+ // Actions from server actions
190
+ if (this._serverActions.size > 0) {
191
+ config.actions = Array.from(this._serverActions.entries()).map(([name, a]) => ({
192
+ id: `server:${name}`,
193
+ handler: a.handler,
194
+ functionName: a.functionName || name,
195
+ description: a.description,
196
+ }));
197
+ }
198
+
199
+ if (this._dependencies.length > 0) {
200
+ config.dependencies = this._dependencies;
201
+ }
202
+ if (this._capabilities.length > 0) {
203
+ config.capabilities = this._capabilities;
204
+ }
205
+
206
+ return defineBlueprint(config);
207
+ }
208
+
209
+ /**
210
+ * Get all model definitions declared by this blueprint.
211
+ */
212
+ getModels(): Map<string, ModelDefinition> {
213
+ if (this._models.size === 0) this.define();
214
+ return new Map(this._models);
215
+ }
216
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Blueprint } from '../Blueprint';
3
+ import { field, state, transition } from '../builders';
4
+
5
+ class TestModule extends Blueprint {
6
+ slug = 'test-module';
7
+ version = '1.0.0';
8
+ name = 'Test Module';
9
+ category = 'module';
10
+
11
+ define() {
12
+ this.model('user', m => m
13
+ .field('email', field.email().required())
14
+ .field('name', field.string())
15
+ .state('active', state.initial())
16
+ .state('inactive', state.end())
17
+ .transition('deactivate', transition.from('active').to('inactive'))
18
+ );
19
+
20
+ this.view('login', { route: '/login', component: 'LoginForm', label: 'Login' });
21
+ this.view('profile', { route: '/profile', component: 'Profile', guard: 'context.actor_id != null' });
22
+
23
+ this.serverAction('authenticate', { handler: 'actions/auth.server.ts', description: 'Validate credentials' });
24
+
25
+ this.dependency('other-module', '>=1.0.0');
26
+ this.capability('email');
27
+ }
28
+ }
29
+
30
+ describe('Blueprint', () => {
31
+ it('builds a valid BlueprintManifest', () => {
32
+ const module = new TestModule();
33
+ const manifest = module.build();
34
+
35
+ expect(manifest.slug).toBe('test-module');
36
+ expect(manifest.version).toBe('1.0.0');
37
+ expect(manifest.name).toBe('Test Module');
38
+ expect(manifest.category).toBe('module');
39
+ });
40
+
41
+ it('collects model declarations', () => {
42
+ const module = new TestModule();
43
+ const manifest = module.build();
44
+
45
+ expect(manifest.models).toEqual(['models/user']);
46
+ });
47
+
48
+ it('collects view declarations as routes', () => {
49
+ const module = new TestModule();
50
+ const manifest = module.build();
51
+
52
+ expect(manifest.routes).toHaveLength(2);
53
+ expect(manifest.routes![0].path).toBe('/login');
54
+ expect(manifest.routes![0].label).toBe('Login');
55
+ expect(manifest.routes![1].path).toBe('/profile');
56
+ expect(manifest.routes![1].guard).toBe('context.actor_id != null');
57
+ });
58
+
59
+ it('collects server action declarations', () => {
60
+ const module = new TestModule();
61
+ const manifest = module.build();
62
+
63
+ expect(manifest.actions).toHaveLength(1);
64
+ expect(manifest.actions![0].id).toBe('server:authenticate');
65
+ expect(manifest.actions![0].handler).toBe('actions/auth.server.ts');
66
+ });
67
+
68
+ it('collects dependencies and capabilities', () => {
69
+ const module = new TestModule();
70
+ const manifest = module.build();
71
+
72
+ expect(manifest.dependencies).toHaveLength(1);
73
+ expect(manifest.dependencies![0].slug).toBe('other-module');
74
+ expect(manifest.capabilities).toEqual(['email']);
75
+ });
76
+
77
+ it('provides model definitions via getModels()', () => {
78
+ const module = new TestModule();
79
+ const models = module.getModels();
80
+
81
+ expect(models.size).toBe(1);
82
+ const userModel = models.get('user')!;
83
+ expect(userModel.slug).toBe('test-module-user');
84
+ expect(Object.keys(userModel.fields)).toContain('email');
85
+ expect(Object.keys(userModel.states)).toContain('active');
86
+ expect(Object.keys(userModel.transitions)).toContain('deactivate');
87
+ });
88
+
89
+ it('resets state on each build() call', () => {
90
+ const module = new TestModule();
91
+ const m1 = module.build();
92
+ const m2 = module.build();
93
+
94
+ // Should not accumulate
95
+ expect(m1.routes).toHaveLength(2);
96
+ expect(m2.routes).toHaveLength(2);
97
+ });
98
+
99
+ it('applies default mode and runtime', () => {
100
+ const module = new TestModule();
101
+ const manifest = module.build();
102
+
103
+ expect(manifest.mode).toBe('infer');
104
+ expect(manifest.defaultRuntime).toBe('local');
105
+ });
106
+ });
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { model, state, field, transition } from '../builders';
3
+ import type { ActionHandler } from '../builders';
4
+ import { setField } from '../actions';
5
+
6
+ describe('Inline action bodies — ActionContext handlers', () => {
7
+ describe('StateBuilder.onEnter with handler', () => {
8
+ it('stores handler as inline_handler action', () => {
9
+ const handler: ActionHandler = async (ctx) => {
10
+ ctx.setField('activatedAt', new Date().toISOString());
11
+ ctx.log('User activated');
12
+ };
13
+
14
+ const result = model('test')
15
+ .state('active', state.initial().onEnter(handler))
16
+ .state('done', state.end())
17
+ .transition('finish', transition.from('active').to('done'))
18
+ .build();
19
+
20
+ const actions = result.states.active.onEnter!;
21
+ expect(actions).toHaveLength(1);
22
+ expect(actions[0].type).toBe('inline_handler');
23
+ expect(actions[0].id).toMatch(/^inline-on-enter-/);
24
+ expect(actions[0].config!.handler).toContain('setField');
25
+ expect(actions[0].config!.handler).toContain('activatedAt');
26
+ });
27
+
28
+ it('still accepts regular ActionDefinition array', () => {
29
+ const result = model('test')
30
+ .state('active', state.initial().onEnter(
31
+ setField('status', '"active"'),
32
+ ))
33
+ .state('done', state.end())
34
+ .transition('finish', transition.from('active').to('done'))
35
+ .build();
36
+
37
+ const actions = result.states.active.onEnter!;
38
+ expect(actions).toHaveLength(1);
39
+ expect(actions[0].type).toBe('set_field');
40
+ });
41
+ });
42
+
43
+ describe('StateBuilder.onExit with handler', () => {
44
+ it('stores handler as inline_handler action', () => {
45
+ const handler: ActionHandler = (ctx) => {
46
+ ctx.log('Leaving state');
47
+ ctx.setFields({ cleanedUp: true });
48
+ };
49
+
50
+ const result = model('test')
51
+ .state('active', state.initial().onExit(handler))
52
+ .state('done', state.end())
53
+ .transition('finish', transition.from('active').to('done'))
54
+ .build();
55
+
56
+ const actions = result.states.active.onExit!;
57
+ expect(actions).toHaveLength(1);
58
+ expect(actions[0].type).toBe('inline_handler');
59
+ expect(actions[0].id).toMatch(/^inline-on-exit-/);
60
+ expect(actions[0].config!.handler).toContain('cleanedUp');
61
+ });
62
+ });
63
+
64
+ describe('TransitionBuilder.do with handler', () => {
65
+ it('stores handler as inline_handler action on transition', () => {
66
+ const handler: ActionHandler = async (ctx) => {
67
+ ctx.setField('approvedAt', new Date().toISOString());
68
+ await ctx.notify('requester', 'Your request was approved');
69
+ await ctx.serverAction('send_email', { template: 'approved' });
70
+ };
71
+
72
+ const result = model('test')
73
+ .state('review', state.initial())
74
+ .state('approved', state.end())
75
+ .transition('approve', transition.from('review').to('approved').do(handler))
76
+ .build();
77
+
78
+ const actions = result.transitions.approve.actions!;
79
+ expect(actions).toHaveLength(1);
80
+ expect(actions[0].type).toBe('inline_handler');
81
+ expect(actions[0].config!.handler).toContain('notify');
82
+ expect(actions[0].config!.handler).toContain('serverAction');
83
+ expect(actions[0].config!.handler).toContain('send_email');
84
+ });
85
+
86
+ it('can mix handler with regular actions', () => {
87
+ const result = model('test')
88
+ .state('a', state.initial())
89
+ .state('b', state.end())
90
+ .transition('go', transition.from('a').to('b')
91
+ .do(setField('step', '"1"'))
92
+ .do(async (ctx) => { ctx.log('step 2'); })
93
+ )
94
+ .build();
95
+
96
+ const actions = result.transitions.go.actions!;
97
+ expect(actions).toHaveLength(2);
98
+ expect(actions[0].type).toBe('set_field');
99
+ expect(actions[1].type).toBe('inline_handler');
100
+ });
101
+ });
102
+
103
+ describe('TypedTransitionBuilder.do with handler', () => {
104
+ it('stores handler via typed callback form', () => {
105
+ const result = model('test')
106
+ .state('draft', state.initial())
107
+ .state('published', state.end())
108
+ .transition('publish', t =>
109
+ t.from('draft').to('published').do(async (ctx) => {
110
+ ctx.setField('publishedAt', new Date().toISOString());
111
+ await ctx.spawn('notification', { type: 'published' });
112
+ })
113
+ )
114
+ .build();
115
+
116
+ const actions = result.transitions.publish.actions!;
117
+ expect(actions).toHaveLength(1);
118
+ expect(actions[0].type).toBe('inline_handler');
119
+ expect(actions[0].config!.handler).toContain('spawn');
120
+ expect(actions[0].config!.handler).toContain('publishedAt');
121
+ });
122
+ });
123
+
124
+ describe('Handler function serialization', () => {
125
+ it('serializes arrow functions', () => {
126
+ const result = model('test')
127
+ .state('a', state.initial().onEnter((ctx) => { ctx.log('hello'); }))
128
+ .state('b', state.end())
129
+ .transition('go', transition.from('a').to('b'))
130
+ .build();
131
+
132
+ const handler = result.states.a.onEnter![0].config!.handler as string;
133
+ expect(handler).toContain('log');
134
+ expect(handler).toContain('hello');
135
+ });
136
+
137
+ it('serializes async arrow functions', () => {
138
+ const result = model('test')
139
+ .state('a', state.initial().onEnter(async (ctx) => {
140
+ await ctx.notify('user', 'hi');
141
+ }))
142
+ .state('b', state.end())
143
+ .transition('go', transition.from('a').to('b'))
144
+ .build();
145
+
146
+ const handler = result.states.a.onEnter![0].config!.handler as string;
147
+ expect(handler).toContain('notify');
148
+ });
149
+
150
+ it('each handler gets a unique id', () => {
151
+ const result = model('test')
152
+ .state('a', state.initial()
153
+ .onEnter((ctx) => { ctx.log('enter'); })
154
+ )
155
+ .state('b', state.end())
156
+ .transition('go', transition.from('a').to('b')
157
+ .do((ctx) => { ctx.log('transition'); })
158
+ )
159
+ .build();
160
+
161
+ const enterId = result.states.a.onEnter![0].id;
162
+ const transId = result.transitions.go.actions![0].id;
163
+ expect(enterId).not.toBe(transId);
164
+ });
165
+ });
166
+ });