@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -0
- package/dist/index.d.mts +1378 -94
- package/dist/index.d.ts +1378 -94
- package/dist/index.js +1094 -1309
- package/dist/index.mjs +1038 -1296
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- package/vitest.config.ts +0 -8
package/src/local/LocalEngine.ts
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocalWorkflowEngine — Browser-side workflow state machine
|
|
3
|
-
*
|
|
4
|
-
* Lightweight, pure-function engine that runs workflow definitions locally.
|
|
5
|
-
* Used by blueprint-specific bridges to drive state via workflow transitions
|
|
6
|
-
* instead of React dispatch actions.
|
|
7
|
-
*
|
|
8
|
-
* - Immutable: applyTransition returns a new WorkflowInstance
|
|
9
|
-
* - Lamport versioning: monotonic counter for ordering transitions across peers
|
|
10
|
-
* - No side effects: safe to call in React render path
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// TYPES — Subset of mm-engine WorkflowDefinition relevant to frontend
|
|
15
|
-
// ============================================================================
|
|
16
|
-
|
|
17
|
-
export interface WorkflowState {
|
|
18
|
-
name: string;
|
|
19
|
-
state_type: 'START' | 'REGULAR' | 'END';
|
|
20
|
-
description?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface WorkflowTransition {
|
|
24
|
-
name: string;
|
|
25
|
-
/** States this transition can fire from. */
|
|
26
|
-
from: string[];
|
|
27
|
-
/** Target state, or null for self-loop (stays in current state). */
|
|
28
|
-
to: string | null;
|
|
29
|
-
description?: string;
|
|
30
|
-
required_fields?: string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface WorkflowFieldDef {
|
|
34
|
-
name: string;
|
|
35
|
-
field_type: string;
|
|
36
|
-
default_value?: unknown;
|
|
37
|
-
[key: string]: unknown;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface WorkflowDefinition {
|
|
41
|
-
id: string;
|
|
42
|
-
slug: string;
|
|
43
|
-
name: string;
|
|
44
|
-
version: string;
|
|
45
|
-
description?: string;
|
|
46
|
-
states: WorkflowState[];
|
|
47
|
-
transitions: WorkflowTransition[];
|
|
48
|
-
fields: WorkflowFieldDef[];
|
|
49
|
-
state_data?: Record<string, unknown>;
|
|
50
|
-
[key: string]: unknown;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ============================================================================
|
|
54
|
-
// WORKFLOW INSTANCE — Runtime state of a single workflow
|
|
55
|
-
// ============================================================================
|
|
56
|
-
|
|
57
|
-
export interface WorkflowInstance {
|
|
58
|
-
/** Unique instance ID */
|
|
59
|
-
id: string;
|
|
60
|
-
/** Slug of the definition this instance was created from */
|
|
61
|
-
definitionSlug: string;
|
|
62
|
-
/** Current state name */
|
|
63
|
-
currentState: string;
|
|
64
|
-
/** Field values (mutable data carried by this instance) */
|
|
65
|
-
fields: Record<string, unknown>;
|
|
66
|
-
/** Lamport version counter — incremented on every transition */
|
|
67
|
-
version: number;
|
|
68
|
-
/** Timestamp of last transition (ms since epoch) */
|
|
69
|
-
updatedAt: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ============================================================================
|
|
73
|
-
// WORKFLOW EVENT — emitted by on_enter / transition actions
|
|
74
|
-
// ============================================================================
|
|
75
|
-
|
|
76
|
-
export interface WorkflowEvent {
|
|
77
|
-
event: string;
|
|
78
|
-
instanceId: string;
|
|
79
|
-
definitionSlug: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Extended instance type carrying pending events from on_enter actions */
|
|
83
|
-
export interface WorkflowInstanceWithEvents extends WorkflowInstance {
|
|
84
|
-
_pendingEvents?: WorkflowEvent[];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// ENGINE
|
|
89
|
-
// ============================================================================
|
|
90
|
-
|
|
91
|
-
/** Callback type for event subscriptions */
|
|
92
|
-
export type WorkflowEventHandler = (event: WorkflowEvent) => void;
|
|
93
|
-
|
|
94
|
-
export class LocalWorkflowEngine {
|
|
95
|
-
private definitions = new Map<string, WorkflowDefinition>();
|
|
96
|
-
private eventSubscriptions = new Map<string, Set<WorkflowEventHandler>>();
|
|
97
|
-
|
|
98
|
-
/** Register a workflow definition by slug */
|
|
99
|
-
registerDefinition(def: WorkflowDefinition): void {
|
|
100
|
-
this.definitions.set(def.slug, def);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Get a registered definition */
|
|
104
|
-
getDefinition(slug: string): WorkflowDefinition | undefined {
|
|
105
|
-
return this.definitions.get(slug);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Get all registered definitions */
|
|
109
|
-
getAllDefinitions(): WorkflowDefinition[] {
|
|
110
|
-
return Array.from(this.definitions.values());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Check if a definition is registered */
|
|
114
|
-
hasDefinition(slug: string): boolean {
|
|
115
|
-
return this.definitions.has(slug);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Subscribe to a workflow event by name.
|
|
120
|
-
* Returns an unsubscribe function.
|
|
121
|
-
*/
|
|
122
|
-
onEvent(eventName: string, handler: WorkflowEventHandler): () => void {
|
|
123
|
-
let handlers = this.eventSubscriptions.get(eventName);
|
|
124
|
-
if (!handlers) {
|
|
125
|
-
handlers = new Set();
|
|
126
|
-
this.eventSubscriptions.set(eventName, handlers);
|
|
127
|
-
}
|
|
128
|
-
handlers.add(handler);
|
|
129
|
-
return () => {
|
|
130
|
-
handlers!.delete(handler);
|
|
131
|
-
if (handlers!.size === 0) {
|
|
132
|
-
this.eventSubscriptions.delete(eventName);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Emit a workflow event to all local subscribers.
|
|
139
|
-
* Called after applyTransition when _pendingEvents are present.
|
|
140
|
-
*/
|
|
141
|
-
emitEvent(event: WorkflowEvent): void {
|
|
142
|
-
const handlers = this.eventSubscriptions.get(event.event);
|
|
143
|
-
if (handlers) {
|
|
144
|
-
for (const handler of handlers) {
|
|
145
|
-
try {
|
|
146
|
-
handler(event);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
console.error(`[WorkflowEngine] Event handler error for "${event.event}":`, err);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Process all pending events from a transition result.
|
|
156
|
-
* Emits locally and returns the events for P2P broadcast.
|
|
157
|
-
*/
|
|
158
|
-
processPendingEvents(instance: WorkflowInstance): WorkflowEvent[] {
|
|
159
|
-
const events = (instance as WorkflowInstanceWithEvents)._pendingEvents;
|
|
160
|
-
if (!events?.length) return [];
|
|
161
|
-
for (const evt of events) {
|
|
162
|
-
this.emitEvent(evt);
|
|
163
|
-
}
|
|
164
|
-
return events;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Create a new workflow instance from a definition */
|
|
168
|
-
createInstance(
|
|
169
|
-
slug: string,
|
|
170
|
-
id: string,
|
|
171
|
-
initialFields?: Record<string, unknown>
|
|
172
|
-
): WorkflowInstance {
|
|
173
|
-
const def = this.definitions.get(slug);
|
|
174
|
-
if (!def) {
|
|
175
|
-
throw new Error(`WorkflowDefinition not found: ${slug}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Find the START state (support both state_type and type field naming)
|
|
179
|
-
const startState = def.states.find(
|
|
180
|
-
(s) => s.state_type === 'START' || (s as unknown as Record<string, unknown>).type === 'START',
|
|
181
|
-
);
|
|
182
|
-
if (!startState) {
|
|
183
|
-
throw new Error(`No START state in definition: ${slug}`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Build default field values from definition
|
|
187
|
-
const fields: Record<string, unknown> = {};
|
|
188
|
-
for (const fieldDef of def.fields) {
|
|
189
|
-
fields[fieldDef.name] = fieldDef.default_value ?? null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Merge state_data defaults
|
|
193
|
-
if (def.state_data) {
|
|
194
|
-
for (const [key, value] of Object.entries(def.state_data)) {
|
|
195
|
-
if (!(key in fields)) {
|
|
196
|
-
fields[key] = value;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Override with caller-provided initial values
|
|
202
|
-
if (initialFields) {
|
|
203
|
-
Object.assign(fields, initialFields);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
id,
|
|
208
|
-
definitionSlug: slug,
|
|
209
|
-
currentState: startState.name,
|
|
210
|
-
fields,
|
|
211
|
-
version: 0,
|
|
212
|
-
updatedAt: Date.now(),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Check if a transition can fire from the instance's current state */
|
|
217
|
-
canTransition(instance: WorkflowInstance, transitionName: string): boolean {
|
|
218
|
-
const def = this.definitions.get(instance.definitionSlug);
|
|
219
|
-
if (!def) return false;
|
|
220
|
-
|
|
221
|
-
const transition = def.transitions.find((t) => t.name === transitionName);
|
|
222
|
-
if (!transition) return false;
|
|
223
|
-
|
|
224
|
-
return transition.from.includes(instance.currentState);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Get all transitions available from the instance's current state */
|
|
228
|
-
getAvailableTransitions(instance: WorkflowInstance): string[] {
|
|
229
|
-
const def = this.definitions.get(instance.definitionSlug);
|
|
230
|
-
if (!def) return [];
|
|
231
|
-
|
|
232
|
-
return def.transitions
|
|
233
|
-
.filter((t) => t.from.includes(instance.currentState))
|
|
234
|
-
.map((t) => t.name);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Apply a transition, returning a new WorkflowInstance (immutable).
|
|
239
|
-
*
|
|
240
|
-
* @param instance - Current instance state
|
|
241
|
-
* @param transitionName - Name of the transition to apply
|
|
242
|
-
* @param data - Optional field updates to merge into instance fields
|
|
243
|
-
* @returns New WorkflowInstance with updated state and version
|
|
244
|
-
* @throws If transition is not valid from current state
|
|
245
|
-
*/
|
|
246
|
-
applyTransition(
|
|
247
|
-
instance: WorkflowInstance,
|
|
248
|
-
transitionName: string,
|
|
249
|
-
data?: Record<string, unknown>
|
|
250
|
-
): WorkflowInstance {
|
|
251
|
-
const def = this.definitions.get(instance.definitionSlug);
|
|
252
|
-
if (!def) {
|
|
253
|
-
throw new Error(`Definition not found: ${instance.definitionSlug}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const transition = def.transitions.find((t) => t.name === transitionName);
|
|
257
|
-
if (!transition) {
|
|
258
|
-
throw new Error(
|
|
259
|
-
`Transition "${transitionName}" not found in ${instance.definitionSlug}`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (!transition.from.includes(instance.currentState)) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
`Transition "${transitionName}" cannot fire from state "${instance.currentState}" ` +
|
|
266
|
-
`(allowed from: ${transition.from.join(', ')})`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Determine target state: null means self-loop (stay in current state)
|
|
271
|
-
const nextState = transition.to ?? instance.currentState;
|
|
272
|
-
|
|
273
|
-
// Merge field updates
|
|
274
|
-
const fields = data
|
|
275
|
-
? { ...instance.fields, ...data }
|
|
276
|
-
: { ...instance.fields };
|
|
277
|
-
|
|
278
|
-
let result: WorkflowInstance = {
|
|
279
|
-
...instance,
|
|
280
|
-
currentState: nextState,
|
|
281
|
-
fields,
|
|
282
|
-
version: instance.version + 1,
|
|
283
|
-
updatedAt: Date.now(),
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
// Execute on_enter actions for the target state
|
|
287
|
-
const targetStateDef = def.states.find((s) => s.name === nextState);
|
|
288
|
-
if (targetStateDef) {
|
|
289
|
-
const onEnter = (targetStateDef as unknown as Record<string, unknown>).on_enter as
|
|
290
|
-
| Array<{ action_type: string; config?: Record<string, unknown> }>
|
|
291
|
-
| undefined;
|
|
292
|
-
|
|
293
|
-
if (onEnter) {
|
|
294
|
-
const pendingEvents: WorkflowEvent[] = [];
|
|
295
|
-
|
|
296
|
-
for (const action of onEnter) {
|
|
297
|
-
if (action.action_type === 'emit_event' && action.config) {
|
|
298
|
-
pendingEvents.push({
|
|
299
|
-
event: action.config.event as string,
|
|
300
|
-
instanceId: result.id,
|
|
301
|
-
definitionSlug: result.definitionSlug,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
if (action.action_type === 'set_field' && action.config) {
|
|
305
|
-
const field = action.config.field as string;
|
|
306
|
-
const expression = action.config.expression as string;
|
|
307
|
-
if (field && expression) {
|
|
308
|
-
if (expression.startsWith('$.transition_data.') && data) {
|
|
309
|
-
const key = expression.replace('$.transition_data.', '');
|
|
310
|
-
result = {
|
|
311
|
-
...result,
|
|
312
|
-
fields: { ...result.fields, [field]: data[key] },
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (pendingEvents.length > 0) {
|
|
320
|
-
(result as WorkflowInstanceWithEvents)._pendingEvents = pendingEvents;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Execute transition-level actions (e.g., set_field, emit_event)
|
|
326
|
-
const transitionActions = (transition as unknown as Record<string, unknown>).actions as
|
|
327
|
-
| Array<{ action_type: string; config?: Record<string, unknown> }>
|
|
328
|
-
| undefined;
|
|
329
|
-
|
|
330
|
-
if (transitionActions) {
|
|
331
|
-
for (const action of transitionActions) {
|
|
332
|
-
if (action.action_type === 'set_field' && action.config) {
|
|
333
|
-
const field = action.config.field as string;
|
|
334
|
-
const expression = action.config.expression as string;
|
|
335
|
-
if (field && expression) {
|
|
336
|
-
if (expression.startsWith('$.transition_data.') && data) {
|
|
337
|
-
const key = expression.replace('$.transition_data.', '');
|
|
338
|
-
result = {
|
|
339
|
-
...result,
|
|
340
|
-
fields: { ...result.fields, [field]: data[key] },
|
|
341
|
-
};
|
|
342
|
-
} else if (expression === 'now()') {
|
|
343
|
-
result = {
|
|
344
|
-
...result,
|
|
345
|
-
fields: { ...result.fields, [field]: new Date().toISOString() },
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
if (action.action_type === 'emit_event' && action.config) {
|
|
351
|
-
const events = (result as WorkflowInstanceWithEvents)._pendingEvents || [];
|
|
352
|
-
events.push({
|
|
353
|
-
event: action.config.event as string,
|
|
354
|
-
instanceId: result.id,
|
|
355
|
-
definitionSlug: result.definitionSlug,
|
|
356
|
-
});
|
|
357
|
-
(result as WorkflowInstanceWithEvents)._pendingEvents = events;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return result;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Apply a transition only if the incoming version is newer.
|
|
367
|
-
* Used for P2P sync — skips stale transitions.
|
|
368
|
-
*/
|
|
369
|
-
applyRemoteTransition(
|
|
370
|
-
instance: WorkflowInstance,
|
|
371
|
-
transitionName: string,
|
|
372
|
-
data: Record<string, unknown> | undefined,
|
|
373
|
-
remoteVersion: number
|
|
374
|
-
): WorkflowInstance | null {
|
|
375
|
-
// Skip stale transitions
|
|
376
|
-
if (remoteVersion <= instance.version) {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (!this.canTransition(instance, transitionName)) {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const updated = this.applyTransition(instance, transitionName, data);
|
|
385
|
-
// Use the remote version to stay in sync
|
|
386
|
-
return { ...updated, version: remoteVersion };
|
|
387
|
-
}
|
|
388
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocalEngineDataSourceAdapter — Adapter for querying LocalWorkflowEngine
|
|
3
|
-
* from within ComponentTreeRenderer context.
|
|
4
|
-
*
|
|
5
|
-
* Provides the same interface as backend API queries but reads from the
|
|
6
|
-
* local workflow engine instead. Fully generic — no workflow-specific concepts.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { LocalWorkflowEngine, WorkflowInstance } from './LocalEngine';
|
|
10
|
-
import type { LocalEngineStore } from './LocalEngineContext';
|
|
11
|
-
|
|
12
|
-
export interface WorkflowDataSource {
|
|
13
|
-
type: 'workflow';
|
|
14
|
-
slug?: string;
|
|
15
|
-
query?: 'latest' | 'list';
|
|
16
|
-
filters?: Record<string, unknown>;
|
|
17
|
-
filter?: Record<string, unknown>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DataSource {
|
|
21
|
-
type: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface DataSourceResult {
|
|
25
|
-
instance?: unknown;
|
|
26
|
-
instances?: unknown[];
|
|
27
|
-
loading: boolean;
|
|
28
|
-
error: Error | null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface InstanceData {
|
|
32
|
-
id: string;
|
|
33
|
-
current_state: string;
|
|
34
|
-
state_data?: Record<string, unknown>;
|
|
35
|
-
/** Alias for state_data — compiled views reference $item.fields.x */
|
|
36
|
-
fields?: Record<string, unknown>;
|
|
37
|
-
created_at?: string;
|
|
38
|
-
updated_at?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type LocalDataResolver = (source: DataSource, localState: Record<string, unknown>) => DataSourceResult | null;
|
|
42
|
-
|
|
43
|
-
export interface LocalEngineAdapter {
|
|
44
|
-
query(dataSource: WorkflowDataSource, localState: Record<string, unknown>): DataSourceResult;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Convert a WorkflowInstance to InstanceData for CTR consumption.
|
|
49
|
-
*/
|
|
50
|
-
function instanceToData(inst: WorkflowInstance): InstanceData {
|
|
51
|
-
const fieldsCopy = { ...inst.fields };
|
|
52
|
-
return {
|
|
53
|
-
id: inst.id,
|
|
54
|
-
current_state: inst.currentState,
|
|
55
|
-
state_data: fieldsCopy,
|
|
56
|
-
// Alias: compiled views reference $item.fields.x instead of $item.state_data.x
|
|
57
|
-
fields: fieldsCopy,
|
|
58
|
-
created_at: inst.fields.created_at as string | undefined,
|
|
59
|
-
updated_at: inst.fields.updated_at as string | undefined,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Create an adapter that queries a LocalWorkflowEngine.
|
|
65
|
-
* Supports list queries with filter expressions.
|
|
66
|
-
*/
|
|
67
|
-
export function createLocalEngineAdapter(
|
|
68
|
-
_engine: LocalWorkflowEngine,
|
|
69
|
-
store: LocalEngineStore,
|
|
70
|
-
): LocalEngineAdapter {
|
|
71
|
-
return {
|
|
72
|
-
query(dataSource: WorkflowDataSource, localState: Record<string, unknown>): DataSourceResult {
|
|
73
|
-
const { slug, query, filters, filter: staticFilter } = dataSource;
|
|
74
|
-
|
|
75
|
-
if (!slug) {
|
|
76
|
-
return { loading: false, error: new Error('LocalEngineAdapter: slug is required') };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle app instance
|
|
80
|
-
if (store.app && store.app.definitionSlug === slug) {
|
|
81
|
-
return {
|
|
82
|
-
instance: instanceToData(store.app),
|
|
83
|
-
loading: false,
|
|
84
|
-
error: null,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Look up the instance map for this slug
|
|
89
|
-
const instanceMap = store.instances?.get(slug);
|
|
90
|
-
if (!instanceMap || typeof instanceMap.values !== 'function') {
|
|
91
|
-
return { instances: [], loading: false, error: null };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Convert map values to InstanceData array
|
|
95
|
-
let instances = Array.from(instanceMap.values()).map(instanceToData);
|
|
96
|
-
|
|
97
|
-
// Apply filters (merge static + dynamic)
|
|
98
|
-
const allFilters = { ...staticFilter, ...filters };
|
|
99
|
-
if (allFilters && Object.keys(allFilters).length > 0) {
|
|
100
|
-
instances = instances.filter((inst) => {
|
|
101
|
-
for (const [key, valueExpr] of Object.entries(allFilters)) {
|
|
102
|
-
let expectedValue: unknown;
|
|
103
|
-
|
|
104
|
-
// Resolve bind expressions like { bind: '$local.some_field_id' }
|
|
105
|
-
if (typeof valueExpr === 'object' && valueExpr !== null && 'bind' in valueExpr) {
|
|
106
|
-
const bindExpr = (valueExpr as { bind: string }).bind;
|
|
107
|
-
if (bindExpr.startsWith('$local.')) {
|
|
108
|
-
const localKey = bindExpr.slice(7);
|
|
109
|
-
expectedValue = localState[localKey];
|
|
110
|
-
} else if (bindExpr.startsWith('$item.')) {
|
|
111
|
-
// For slot contributions, $item is the parent instance
|
|
112
|
-
// This is handled by the slot resolver, not here
|
|
113
|
-
continue;
|
|
114
|
-
} else {
|
|
115
|
-
expectedValue = bindExpr;
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
expectedValue = valueExpr;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Compare field value (look in state_data)
|
|
122
|
-
const actualValue = inst.state_data?.[key];
|
|
123
|
-
if (actualValue !== expectedValue) {
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return true;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// For 'latest', return single instance
|
|
132
|
-
if (query === 'latest') {
|
|
133
|
-
return {
|
|
134
|
-
instance: instances[0],
|
|
135
|
-
instances: undefined,
|
|
136
|
-
loading: false,
|
|
137
|
-
error: null,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// For 'list', return array
|
|
142
|
-
return {
|
|
143
|
-
instances,
|
|
144
|
-
loading: false,
|
|
145
|
-
error: null,
|
|
146
|
-
};
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Create a LocalDataResolver function compatible with useExperienceData.
|
|
153
|
-
* Returns non-null DataSourceResult for slugs handled by the local engine,
|
|
154
|
-
* null for everything else (falls through to server API).
|
|
155
|
-
*/
|
|
156
|
-
export function createLocalDataResolver(
|
|
157
|
-
engine: LocalWorkflowEngine,
|
|
158
|
-
store: LocalEngineStore,
|
|
159
|
-
): LocalDataResolver {
|
|
160
|
-
const adapter = createLocalEngineAdapter(engine, store);
|
|
161
|
-
|
|
162
|
-
return (source: DataSource, localState: Record<string, unknown>): DataSourceResult | null => {
|
|
163
|
-
// Only handle workflow dataSources
|
|
164
|
-
if (source.type !== 'workflow') return null;
|
|
165
|
-
const wfSource = source as WorkflowDataSource;
|
|
166
|
-
if (!wfSource.slug) return null;
|
|
167
|
-
|
|
168
|
-
// Check if this slug is served by the local engine
|
|
169
|
-
if (store.app?.definitionSlug === wfSource.slug || store.instances?.has(wfSource.slug)) {
|
|
170
|
-
return adapter.query(wfSource, localState);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return null;
|
|
174
|
-
};
|
|
175
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocalEngineContext — Provides local workflow engine to ComponentTreeRenderer
|
|
3
|
-
*
|
|
4
|
-
* Used for P2P features where workflow instances are stored
|
|
5
|
-
* locally instead of on the server. Fully generic — no workflow-specific concepts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createContext, useContext } from 'react';
|
|
9
|
-
import type { LocalWorkflowEngine, WorkflowInstance } from './LocalEngine';
|
|
10
|
-
|
|
11
|
-
export interface LocalEngineStore {
|
|
12
|
-
app?: WorkflowInstance;
|
|
13
|
-
/** Generic slug-keyed instance maps: slug → (id → instance) */
|
|
14
|
-
instances: Map<string, Map<string, WorkflowInstance>>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface LocalEngineContextValue {
|
|
18
|
-
engine: LocalWorkflowEngine;
|
|
19
|
-
store: LocalEngineStore;
|
|
20
|
-
/** Generic action dispatch — blueprint-specific actions keyed by name */
|
|
21
|
-
actions?: Record<string, (data?: unknown) => void>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const LocalEngineContext = createContext<LocalEngineContextValue | null>(null);
|
|
25
|
-
|
|
26
|
-
export function useLocalEngine(): LocalEngineContextValue | null {
|
|
27
|
-
return useContext(LocalEngineContext);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const LocalEngineProvider = LocalEngineContext.Provider;
|
package/src/logger.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Player Logger — structured console.warn logging for event matching and action dispatch.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { PlayerLogEntry } from './types';
|
|
6
|
-
|
|
7
|
-
let debugEnabled = false;
|
|
8
|
-
|
|
9
|
-
export function setPlayerDebug(enabled: boolean): void {
|
|
10
|
-
debugEnabled = enabled;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function isPlayerDebug(): boolean {
|
|
14
|
-
return debugEnabled;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function playerLog(entry: Omit<PlayerLogEntry, 'timestamp'>): void {
|
|
18
|
-
if (!debugEnabled && entry.level === 'debug') return;
|
|
19
|
-
|
|
20
|
-
const prefix = `[player-web:${entry.category}]`;
|
|
21
|
-
const msg = `${prefix} ${entry.message}`;
|
|
22
|
-
|
|
23
|
-
switch (entry.level) {
|
|
24
|
-
case 'error':
|
|
25
|
-
console.error(msg, entry.data ?? '');
|
|
26
|
-
break;
|
|
27
|
-
case 'warn':
|
|
28
|
-
console.warn(msg, entry.data ?? '');
|
|
29
|
-
break;
|
|
30
|
-
case 'info':
|
|
31
|
-
console.info(msg, entry.data ?? '');
|
|
32
|
-
break;
|
|
33
|
-
case 'debug':
|
|
34
|
-
console.debug(msg, entry.data ?? '');
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
}
|