@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/hooks/useChannel.ts
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useChannel — Pub/sub messaging channel between workflow instances.
|
|
3
|
-
*
|
|
4
|
-
* Provides real-time communication between participants (e.g., rider ↔ driver)
|
|
5
|
-
* using WebSocket-backed channels. Messages are published to named channels
|
|
6
|
-
* and delivered to all subscribers.
|
|
7
|
-
*
|
|
8
|
-
* Usage in .workflow.tsx:
|
|
9
|
-
* const { publish, messages, subscribe } = useChannel('ride:abc123');
|
|
10
|
-
*
|
|
11
|
-
* // Publish driver location
|
|
12
|
-
* publish('location', { lat: 40.7128, lng: -74.0060 });
|
|
13
|
-
*
|
|
14
|
-
* // Subscribe to messages
|
|
15
|
-
* useEffect(() => {
|
|
16
|
-
* return subscribe('location', (data) => {
|
|
17
|
-
* updateDriverMarker(data.lat, data.lng);
|
|
18
|
-
* });
|
|
19
|
-
* }, [subscribe]);
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Types
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
/** A message received on a channel. */
|
|
29
|
-
export interface ChannelMessage {
|
|
30
|
-
/** The event type within the channel. */
|
|
31
|
-
event: string;
|
|
32
|
-
/** The message payload. */
|
|
33
|
-
data: unknown;
|
|
34
|
-
/** When the message was received. */
|
|
35
|
-
timestamp: number;
|
|
36
|
-
/** Sender identifier (if available). */
|
|
37
|
-
sender?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Options for configuring a channel. */
|
|
41
|
-
export interface ChannelOptions {
|
|
42
|
-
/** WebSocket URL (defaults to /ws/workflow). */
|
|
43
|
-
wsUrl?: string;
|
|
44
|
-
/** Enable/disable the channel (default: true). */
|
|
45
|
-
enabled?: boolean;
|
|
46
|
-
/** Maximum messages to keep in buffer (default: 100). */
|
|
47
|
-
bufferSize?: number;
|
|
48
|
-
/** Auto-reconnect on disconnect. */
|
|
49
|
-
reconnect?: boolean;
|
|
50
|
-
/** Reconnect delay in ms (default: 3000). */
|
|
51
|
-
reconnectDelay?: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Channel handle returned by useChannel. */
|
|
55
|
-
export interface ChannelHandle {
|
|
56
|
-
/** Publish a message to the channel. */
|
|
57
|
-
publish: (event: string, data: unknown) => void;
|
|
58
|
-
/** Subscribe to a specific event type. Returns unsubscribe function. */
|
|
59
|
-
subscribe: (event: string, handler: (data: unknown, message: ChannelMessage) => void) => () => void;
|
|
60
|
-
/** All messages received (capped by bufferSize). */
|
|
61
|
-
messages: ChannelMessage[];
|
|
62
|
-
/** Whether the WebSocket is connected. */
|
|
63
|
-
connected: boolean;
|
|
64
|
-
/** Disconnect and reconnect. */
|
|
65
|
-
reconnect: () => void;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Channel transport — pluggable WebSocket implementation. */
|
|
69
|
-
export interface ChannelTransport {
|
|
70
|
-
send: (channel: string, event: string, data: unknown) => void;
|
|
71
|
-
subscribe: (channel: string, handler: (message: ChannelMessage) => void) => () => void;
|
|
72
|
-
connect: () => void;
|
|
73
|
-
disconnect: () => void;
|
|
74
|
-
readonly connected: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Global transport (set by provider)
|
|
78
|
-
let _globalTransport: ChannelTransport | null = null;
|
|
79
|
-
|
|
80
|
-
export function setChannelTransport(transport: ChannelTransport | null): void {
|
|
81
|
-
_globalTransport = transport;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// =============================================================================
|
|
85
|
-
// Internal — default WebSocket transport
|
|
86
|
-
// =============================================================================
|
|
87
|
-
|
|
88
|
-
function getToken(): string | null {
|
|
89
|
-
return typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function createDefaultTransport(wsUrl: string, reconnectOpts: { enabled: boolean; delay: number }): ChannelTransport {
|
|
93
|
-
let ws: WebSocket | null = null;
|
|
94
|
-
let isConnected = false;
|
|
95
|
-
const listeners = new Map<string, Set<(message: ChannelMessage) => void>>();
|
|
96
|
-
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
97
|
-
|
|
98
|
-
const doConnect = () => {
|
|
99
|
-
const token = getToken();
|
|
100
|
-
const url = token ? `${wsUrl}?token=${encodeURIComponent(token)}` : wsUrl;
|
|
101
|
-
ws = new WebSocket(url);
|
|
102
|
-
|
|
103
|
-
ws.onopen = () => {
|
|
104
|
-
isConnected = true;
|
|
105
|
-
// Re-subscribe to channels
|
|
106
|
-
for (const channel of listeners.keys()) {
|
|
107
|
-
ws?.send(JSON.stringify({ type: 'subscribe', channel }));
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
ws.onmessage = (e) => {
|
|
112
|
-
try {
|
|
113
|
-
const msg = JSON.parse(e.data);
|
|
114
|
-
const channel = msg.channel as string;
|
|
115
|
-
const handlers = listeners.get(channel);
|
|
116
|
-
if (handlers) {
|
|
117
|
-
const channelMsg: ChannelMessage = {
|
|
118
|
-
event: msg.event,
|
|
119
|
-
data: msg.data,
|
|
120
|
-
timestamp: msg.timestamp ?? Date.now(),
|
|
121
|
-
sender: msg.sender,
|
|
122
|
-
};
|
|
123
|
-
for (const handler of handlers) {
|
|
124
|
-
handler(channelMsg);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// Ignore parse errors
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
ws.onclose = () => {
|
|
133
|
-
isConnected = false;
|
|
134
|
-
if (reconnectOpts.enabled) {
|
|
135
|
-
reconnectTimer = setTimeout(doConnect, reconnectOpts.delay);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
ws.onerror = () => {
|
|
140
|
-
ws?.close();
|
|
141
|
-
};
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
get connected() {
|
|
146
|
-
return isConnected;
|
|
147
|
-
},
|
|
148
|
-
connect: doConnect,
|
|
149
|
-
disconnect: () => {
|
|
150
|
-
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
151
|
-
ws?.close();
|
|
152
|
-
ws = null;
|
|
153
|
-
isConnected = false;
|
|
154
|
-
},
|
|
155
|
-
send: (channel, event, data) => {
|
|
156
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
157
|
-
ws.send(JSON.stringify({ type: 'publish', channel, event, data }));
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
subscribe: (channel, handler) => {
|
|
161
|
-
if (!listeners.has(channel)) {
|
|
162
|
-
listeners.set(channel, new Set());
|
|
163
|
-
// Subscribe on the WebSocket
|
|
164
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
165
|
-
ws.send(JSON.stringify({ type: 'subscribe', channel }));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
listeners.get(channel)!.add(handler);
|
|
169
|
-
|
|
170
|
-
return () => {
|
|
171
|
-
const handlers = listeners.get(channel);
|
|
172
|
-
if (handlers) {
|
|
173
|
-
handlers.delete(handler);
|
|
174
|
-
if (handlers.size === 0) {
|
|
175
|
-
listeners.delete(channel);
|
|
176
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
177
|
-
ws.send(JSON.stringify({ type: 'unsubscribe', channel }));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// =============================================================================
|
|
187
|
-
// Hook
|
|
188
|
-
// =============================================================================
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Pub/sub messaging channel for real-time communication.
|
|
192
|
-
*
|
|
193
|
-
* @param channelName - Channel identifier (e.g., 'ride:abc123').
|
|
194
|
-
* @param options - Channel configuration.
|
|
195
|
-
* @returns Channel handle with publish/subscribe/messages.
|
|
196
|
-
*/
|
|
197
|
-
export function useChannel(
|
|
198
|
-
channelName: string,
|
|
199
|
-
options: ChannelOptions = {},
|
|
200
|
-
): ChannelHandle {
|
|
201
|
-
const {
|
|
202
|
-
wsUrl = '/ws/workflow',
|
|
203
|
-
enabled = true,
|
|
204
|
-
bufferSize = 100,
|
|
205
|
-
reconnect: autoReconnect = true,
|
|
206
|
-
reconnectDelay = 3000,
|
|
207
|
-
} = options;
|
|
208
|
-
|
|
209
|
-
const [messages, setMessages] = useState<ChannelMessage[]>([]);
|
|
210
|
-
const [connected, setConnected] = useState(false);
|
|
211
|
-
const [reconnectKey, setReconnectKey] = useState(0);
|
|
212
|
-
const handlersRef = useRef(new Map<string, Set<(data: unknown, msg: ChannelMessage) => void>>());
|
|
213
|
-
const transportRef = useRef<ChannelTransport | null>(null);
|
|
214
|
-
const bufferSizeRef = useRef(bufferSize);
|
|
215
|
-
bufferSizeRef.current = bufferSize;
|
|
216
|
-
|
|
217
|
-
// Connect transport
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (!enabled) return;
|
|
220
|
-
|
|
221
|
-
const transport = _globalTransport ?? createDefaultTransport(wsUrl, {
|
|
222
|
-
enabled: autoReconnect,
|
|
223
|
-
delay: reconnectDelay,
|
|
224
|
-
});
|
|
225
|
-
transportRef.current = transport;
|
|
226
|
-
|
|
227
|
-
if (!_globalTransport) {
|
|
228
|
-
transport.connect();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Subscribe to the channel
|
|
232
|
-
const unsub = transport.subscribe(channelName, (msg) => {
|
|
233
|
-
setMessages((prev) => {
|
|
234
|
-
const next = [...prev, msg];
|
|
235
|
-
return next.length > bufferSizeRef.current ? next.slice(-bufferSizeRef.current) : next;
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// Dispatch to event-specific handlers
|
|
239
|
-
const eventHandlers = handlersRef.current.get(msg.event);
|
|
240
|
-
if (eventHandlers) {
|
|
241
|
-
for (const handler of eventHandlers) {
|
|
242
|
-
handler(msg.data, msg);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Poll connected status
|
|
248
|
-
const interval = setInterval(() => {
|
|
249
|
-
setConnected(transport.connected);
|
|
250
|
-
}, 1000);
|
|
251
|
-
setConnected(transport.connected);
|
|
252
|
-
|
|
253
|
-
return () => {
|
|
254
|
-
unsub();
|
|
255
|
-
clearInterval(interval);
|
|
256
|
-
if (!_globalTransport) {
|
|
257
|
-
transport.disconnect();
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
}, [channelName, wsUrl, enabled, autoReconnect, reconnectDelay, reconnectKey]);
|
|
261
|
-
|
|
262
|
-
const publish = useCallback(
|
|
263
|
-
(event: string, data: unknown) => {
|
|
264
|
-
transportRef.current?.send(channelName, event, data);
|
|
265
|
-
},
|
|
266
|
-
[channelName],
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
const subscribe = useCallback(
|
|
270
|
-
(event: string, handler: (data: unknown, msg: ChannelMessage) => void): (() => void) => {
|
|
271
|
-
if (!handlersRef.current.has(event)) {
|
|
272
|
-
handlersRef.current.set(event, new Set());
|
|
273
|
-
}
|
|
274
|
-
handlersRef.current.get(event)!.add(handler);
|
|
275
|
-
|
|
276
|
-
return () => {
|
|
277
|
-
const handlers = handlersRef.current.get(event);
|
|
278
|
-
if (handlers) {
|
|
279
|
-
handlers.delete(handler);
|
|
280
|
-
if (handlers.size === 0) {
|
|
281
|
-
handlersRef.current.delete(event);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
},
|
|
286
|
-
[],
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
const forceReconnect = useCallback(() => {
|
|
290
|
-
setReconnectKey((k) => k + 1);
|
|
291
|
-
setMessages([]);
|
|
292
|
-
}, []);
|
|
293
|
-
|
|
294
|
-
return useMemo(
|
|
295
|
-
(): ChannelHandle => ({
|
|
296
|
-
publish,
|
|
297
|
-
subscribe,
|
|
298
|
-
messages,
|
|
299
|
-
connected,
|
|
300
|
-
reconnect: forceReconnect,
|
|
301
|
-
}),
|
|
302
|
-
[publish, subscribe, messages, connected, forceReconnect],
|
|
303
|
-
);
|
|
304
|
-
}
|
package/src/hooks/useComputed.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useComputed — declares a computed field with one of three execution modes.
|
|
3
|
-
*
|
|
4
|
-
* Computed fields derive their value from other fields via expressions.
|
|
5
|
-
* The execution mode determines WHEN the computation runs:
|
|
6
|
-
*
|
|
7
|
-
* - 'read-time' (RT): Computed on every read. No storage overhead.
|
|
8
|
-
* Best for lightweight derivations (string concat, arithmetic).
|
|
9
|
-
* Cannot be filtered/sorted at the database level.
|
|
10
|
-
*
|
|
11
|
-
* - 'transaction-maintained' (TM): Recomputed synchronously whenever
|
|
12
|
-
* a dependency changes within the same transaction. Stored in the
|
|
13
|
-
* database alongside regular fields. Can be indexed and queried.
|
|
14
|
-
* Default for RLS-critical fields.
|
|
15
|
-
*
|
|
16
|
-
* - 'async-materialized' (AM): Recomputed asynchronously via a
|
|
17
|
-
* background job after dependencies change. Stored in the database.
|
|
18
|
-
* Best for expensive computations (aggregations, external API calls).
|
|
19
|
-
* May be stale between updates.
|
|
20
|
-
*
|
|
21
|
-
* At compile time, the react-compiler extracts useComputed() calls into
|
|
22
|
-
* IRFieldDefinition entries with `computed` expression and `computed_mode`.
|
|
23
|
-
*
|
|
24
|
-
* At runtime, this hook reads the materialized value from instance state
|
|
25
|
-
* (for TM/AM) or evaluates the expression on the fly (for RT).
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* // Read-time: evaluated on every render
|
|
29
|
-
* const fullName = useComputed('full_name', () => `${firstName} ${lastName}`, {
|
|
30
|
-
* mode: 'read-time',
|
|
31
|
-
* deps: ['first_name', 'last_name'],
|
|
32
|
-
* });
|
|
33
|
-
*
|
|
34
|
-
* // Transaction-maintained: stored, updated on dependency change
|
|
35
|
-
* const total = useComputed('line_total', () => quantity * unitPrice, {
|
|
36
|
-
* mode: 'transaction-maintained',
|
|
37
|
-
* deps: ['quantity', 'unit_price'],
|
|
38
|
-
* });
|
|
39
|
-
*
|
|
40
|
-
* // Async-materialized: stored, updated via background job
|
|
41
|
-
* const score = useComputed('credit_score', () => calculateScore(income, debt), {
|
|
42
|
-
* mode: 'async-materialized',
|
|
43
|
-
* deps: ['income', 'debt'],
|
|
44
|
-
* staleness: '5m', // acceptable staleness window
|
|
45
|
-
* });
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import { useMemo, useRef } from 'react';
|
|
49
|
-
|
|
50
|
-
// =============================================================================
|
|
51
|
-
// Types
|
|
52
|
-
// =============================================================================
|
|
53
|
-
|
|
54
|
-
/** The three execution modes for computed fields. */
|
|
55
|
-
export type ComputedFieldMode =
|
|
56
|
-
| 'read-time'
|
|
57
|
-
| 'transaction-maintained'
|
|
58
|
-
| 'async-materialized';
|
|
59
|
-
|
|
60
|
-
/** Options for useComputed. */
|
|
61
|
-
export interface UseComputedOptions {
|
|
62
|
-
/** Execution mode. Default: 'read-time'. */
|
|
63
|
-
mode?: ComputedFieldMode;
|
|
64
|
-
/** Field names this computation depends on. Used by the compiler for dependency tracking. */
|
|
65
|
-
deps?: string[];
|
|
66
|
-
/** For async-materialized: acceptable staleness duration (e.g., '5m', '1h'). */
|
|
67
|
-
staleness?: string;
|
|
68
|
-
/** For async-materialized: whether this field is used in RLS policies. */
|
|
69
|
-
rlsStalenessOk?: boolean;
|
|
70
|
-
/** Output field type hint. Default: inferred from expression return type. */
|
|
71
|
-
type?: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The result of a computed field — the value plus metadata. */
|
|
75
|
-
export interface ComputedFieldResult<T> {
|
|
76
|
-
/** The current computed value. */
|
|
77
|
-
value: T;
|
|
78
|
-
/** Whether the value is potentially stale (only for async-materialized). */
|
|
79
|
-
stale: boolean;
|
|
80
|
-
/** When the value was last computed (ISO timestamp). */
|
|
81
|
-
computedAt: string | null;
|
|
82
|
-
/** The execution mode. */
|
|
83
|
-
mode: ComputedFieldMode;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// =============================================================================
|
|
87
|
-
// Hook Implementation
|
|
88
|
-
// =============================================================================
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Declares a computed field.
|
|
92
|
-
*
|
|
93
|
-
* @param name - The canonical field name (snake_case).
|
|
94
|
-
* @param compute - The computation function. References to other fields
|
|
95
|
-
* should use the variables declared by useState() in the same component.
|
|
96
|
-
* @param options - Configuration including execution mode and dependencies.
|
|
97
|
-
* @returns The computed value (for read-time) or a ComputedFieldResult.
|
|
98
|
-
*/
|
|
99
|
-
export function useComputed<T = unknown>(
|
|
100
|
-
_name: string,
|
|
101
|
-
compute: () => T,
|
|
102
|
-
options?: UseComputedOptions,
|
|
103
|
-
): T {
|
|
104
|
-
const mode = options?.mode ?? 'read-time';
|
|
105
|
-
const deps = options?.deps ?? [];
|
|
106
|
-
|
|
107
|
-
// For read-time: just compute on every call (let React.useMemo optimize)
|
|
108
|
-
// The deps array is for compiler metadata, not React deps — we always recompute
|
|
109
|
-
// since the compiler handles dependency extraction at build time.
|
|
110
|
-
const computeRef = useRef(compute);
|
|
111
|
-
computeRef.current = compute;
|
|
112
|
-
|
|
113
|
-
if (mode === 'read-time') {
|
|
114
|
-
// Read-time: evaluate on every render cycle
|
|
115
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
-
return useMemo(() => computeRef.current(), [
|
|
117
|
-
// We intentionally depend on deps.join to recompute when tracked fields change
|
|
118
|
-
// The actual dependency tracking happens at the compiler level
|
|
119
|
-
deps.join(','),
|
|
120
|
-
]);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// For transaction-maintained and async-materialized modes:
|
|
124
|
-
// At runtime, the value comes from the instance's state_data (already computed
|
|
125
|
-
// server-side or by the engine). The compute function serves as:
|
|
126
|
-
// 1. Documentation of the formula
|
|
127
|
-
// 2. Fallback for local/offline execution
|
|
128
|
-
// 3. Compile-time extraction target
|
|
129
|
-
//
|
|
130
|
-
// In a full runtime environment, this would read from WorkflowRuntime context.
|
|
131
|
-
// For now, we evaluate locally as a fallback.
|
|
132
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
-
return useMemo(() => computeRef.current(), [deps.join(',')]);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Overload that returns full metadata including staleness info.
|
|
138
|
-
* Useful for async-materialized fields where you need to show staleness indicators.
|
|
139
|
-
*/
|
|
140
|
-
export function useComputedWithMeta<T = unknown>(
|
|
141
|
-
name: string,
|
|
142
|
-
compute: () => T,
|
|
143
|
-
options?: UseComputedOptions,
|
|
144
|
-
): ComputedFieldResult<T> {
|
|
145
|
-
const value = useComputed(name, compute, options);
|
|
146
|
-
const mode = options?.mode ?? 'read-time';
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
value,
|
|
150
|
-
stale: false, // In full runtime, this would check engine staleness tracking
|
|
151
|
-
computedAt: new Date().toISOString(),
|
|
152
|
-
mode,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useDomainSubscription — Bridges WebSocket domain events to the player-core EventBus.
|
|
3
|
-
*
|
|
4
|
-
* This hook receives domain events from the WebSocket transport (or any
|
|
5
|
-
* event source) and publishes them to a player-core EventBus. This enables
|
|
6
|
-
* on_event subscriptions in experience workflows to react to backend state changes.
|
|
7
|
-
*
|
|
8
|
-
* The hook does NOT manage the WebSocket connection itself — that remains
|
|
9
|
-
* with the existing WorkflowSocketContext. Instead, this hook accepts a
|
|
10
|
-
* generic `subscribe` function, making it testable and transport-agnostic.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { useEffect, useRef } from 'react';
|
|
14
|
-
import type { EventBus } from '@mindmatrix/player-core';
|
|
15
|
-
import type { DomainEvent, DomainSubscriptionConfig } from '../types';
|
|
16
|
-
import { playerLog } from '../logger';
|
|
17
|
-
|
|
18
|
-
export interface DomainSubscriptionTransport {
|
|
19
|
-
/** Subscribe to domain events. Returns unsubscribe function. */
|
|
20
|
-
subscribe: (
|
|
21
|
-
config: {
|
|
22
|
-
instanceIds?: string[];
|
|
23
|
-
entity?: { type: string; id: string };
|
|
24
|
-
},
|
|
25
|
-
onEvent: (eventType: string, data: DomainEvent) => void,
|
|
26
|
-
) => () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Bridge domain events from a transport into a player-core EventBus.
|
|
31
|
-
*
|
|
32
|
-
* Converts domain events into topic strings matching the on_event pattern format:
|
|
33
|
-
* `{definition_slug}:{entity_type}:{entity_id}:{transition_name}.{event_type}`
|
|
34
|
-
*
|
|
35
|
-
* This allows on_event patterns like:
|
|
36
|
-
* - `project:*:*:complete_task.transitioned` — any project completes a task
|
|
37
|
-
* - `**:transitioned` — any transition anywhere
|
|
38
|
-
*/
|
|
39
|
-
export function useDomainSubscription(
|
|
40
|
-
eventBus: EventBus,
|
|
41
|
-
transport: DomainSubscriptionTransport | null,
|
|
42
|
-
config: DomainSubscriptionConfig,
|
|
43
|
-
): void {
|
|
44
|
-
const configRef = useRef(config);
|
|
45
|
-
configRef.current = config;
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (!transport || config.enabled === false) return;
|
|
49
|
-
|
|
50
|
-
const unsub = transport.subscribe(
|
|
51
|
-
{
|
|
52
|
-
instanceIds: config.instanceIds,
|
|
53
|
-
entity: config.entity,
|
|
54
|
-
},
|
|
55
|
-
(eventType: string, data: DomainEvent) => {
|
|
56
|
-
// Build structured topic from domain event
|
|
57
|
-
const slug = data.definition_slug ?? 'unknown';
|
|
58
|
-
const entityType = data.entity_type ?? '*';
|
|
59
|
-
const entityId = data.entity_id ?? '*';
|
|
60
|
-
const transition = data.transition_name ?? 'unknown';
|
|
61
|
-
|
|
62
|
-
// Primary topic: slug:entityType:entityId:transition.eventType
|
|
63
|
-
const topic = `${slug}:${entityType}:${entityId}:${transition}.${eventType}`;
|
|
64
|
-
|
|
65
|
-
playerLog({
|
|
66
|
-
level: 'info',
|
|
67
|
-
category: 'event_match',
|
|
68
|
-
message: `Domain event → topic: "${topic}"`,
|
|
69
|
-
data: {
|
|
70
|
-
eventType,
|
|
71
|
-
from: data.from_state,
|
|
72
|
-
to: data.to_state,
|
|
73
|
-
trigger: data.trigger,
|
|
74
|
-
changed: data.changed_fields,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Publish to event bus with full payload
|
|
79
|
-
eventBus.emit(topic, {
|
|
80
|
-
...data,
|
|
81
|
-
_event_type: eventType,
|
|
82
|
-
_topic: topic,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Also publish a simpler event for broad patterns
|
|
86
|
-
eventBus.emit(`${eventType}:${slug}`, {
|
|
87
|
-
...data,
|
|
88
|
-
_event_type: eventType,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Call user's onEvent callback if provided
|
|
92
|
-
configRef.current.onEvent?.({
|
|
93
|
-
topic,
|
|
94
|
-
payload: data as unknown as Record<string, unknown>,
|
|
95
|
-
});
|
|
96
|
-
},
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
return unsub;
|
|
100
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
-
}, [
|
|
102
|
-
eventBus,
|
|
103
|
-
transport,
|
|
104
|
-
config.enabled,
|
|
105
|
-
// Stable serialization of subscription params
|
|
106
|
-
config.instanceIds?.join(','),
|
|
107
|
-
config.entity?.type,
|
|
108
|
-
config.entity?.id,
|
|
109
|
-
]);
|
|
110
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useDuringAction — Register a during/while-in-state action
|
|
3
|
-
*
|
|
4
|
-
* Runs a recurring action while the workflow is in a specific state.
|
|
5
|
-
* Similar to useWhileIn but designed for action-based patterns:
|
|
6
|
-
* polling, timers, heartbeats, etc.
|
|
7
|
-
* Supports E1 during actions.
|
|
8
|
-
*
|
|
9
|
-
* Usage in .workflow.tsx:
|
|
10
|
-
* useDuringAction({
|
|
11
|
-
* state: 'processing',
|
|
12
|
-
* action: async () => {
|
|
13
|
-
* const status = await checkExternalService();
|
|
14
|
-
* if (status === 'done') transition('complete');
|
|
15
|
-
* },
|
|
16
|
-
* intervalMs: 5000,
|
|
17
|
-
* });
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { useEffect, useRef } from 'react';
|
|
21
|
-
import { useRuntimeContext } from '../providers/RuntimeContext';
|
|
22
|
-
|
|
23
|
-
export interface DuringActionConfig {
|
|
24
|
-
/** State name(s) in which the action runs. */
|
|
25
|
-
state: string | string[];
|
|
26
|
-
/** The action to execute on each interval tick. */
|
|
27
|
-
action: () => void | Promise<void>;
|
|
28
|
-
/** Interval in milliseconds between action executions. Default: 1000. */
|
|
29
|
-
intervalMs?: number;
|
|
30
|
-
/** Whether to execute immediately on state entry. Default: true. */
|
|
31
|
-
immediate?: boolean;
|
|
32
|
-
/** Whether the action is enabled. Default: true. */
|
|
33
|
-
enabled?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function useDuringAction(config: DuringActionConfig): void {
|
|
37
|
-
const runtime = useRuntimeContext();
|
|
38
|
-
const actionRef = useRef(config.action);
|
|
39
|
-
actionRef.current = config.action;
|
|
40
|
-
|
|
41
|
-
const states = Array.isArray(config.state) ? config.state : [config.state];
|
|
42
|
-
const intervalMs = config.intervalMs ?? 1000;
|
|
43
|
-
const immediate = config.immediate ?? true;
|
|
44
|
-
const enabled = config.enabled ?? true;
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (!enabled) return;
|
|
48
|
-
|
|
49
|
-
let timer: ReturnType<typeof setInterval> | null = null;
|
|
50
|
-
let running = false;
|
|
51
|
-
|
|
52
|
-
function executeAction() {
|
|
53
|
-
if (running) return; // Prevent overlapping executions
|
|
54
|
-
running = true;
|
|
55
|
-
try {
|
|
56
|
-
const result = actionRef.current();
|
|
57
|
-
if (result instanceof Promise) {
|
|
58
|
-
result.finally(() => { running = false; });
|
|
59
|
-
} else {
|
|
60
|
-
running = false;
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
running = false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function startAction() {
|
|
68
|
-
if (timer) clearInterval(timer);
|
|
69
|
-
if (immediate) executeAction();
|
|
70
|
-
timer = setInterval(executeAction, intervalMs);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function stopAction() {
|
|
74
|
-
if (timer) {
|
|
75
|
-
clearInterval(timer);
|
|
76
|
-
timer = null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// If already in target state, start immediately
|
|
81
|
-
if (states.includes(runtime.sm.currentState)) {
|
|
82
|
-
startAction();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const unsub = runtime.sm.on((event) => {
|
|
86
|
-
if (event.type === 'state_enter' && event.to_state && states.includes(event.to_state)) {
|
|
87
|
-
startAction();
|
|
88
|
-
}
|
|
89
|
-
if (event.type === 'state_exit' && event.from_state && states.includes(event.from_state)) {
|
|
90
|
-
stopAction();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return () => {
|
|
95
|
-
unsub();
|
|
96
|
-
stopAction();
|
|
97
|
-
};
|
|
98
|
-
}, [runtime, intervalMs, immediate, enabled, ...states]);
|
|
99
|
-
}
|