@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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.mts +1378 -94
  3. package/dist/index.d.ts +1378 -94
  4. package/dist/index.js +1094 -1309
  5. package/dist/index.mjs +1038 -1296
  6. package/package.json +4 -3
  7. package/package.json.backup +0 -41
  8. package/src/Blueprint.ts +0 -216
  9. package/src/__tests__/Blueprint.test.ts +0 -106
  10. package/src/__tests__/action-context.test.ts +0 -166
  11. package/src/__tests__/actionCreators.test.ts +0 -179
  12. package/src/__tests__/builders.test.ts +0 -336
  13. package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
  14. package/src/__tests__/factories.test.ts +0 -229
  15. package/src/__tests__/loader.test.ts +0 -159
  16. package/src/__tests__/logger.test.ts +0 -70
  17. package/src/__tests__/type-inference.test.ts +0 -160
  18. package/src/__tests__/typed-transitions.test.ts +0 -126
  19. package/src/__tests__/useModuleConfig.test.ts +0 -61
  20. package/src/actionCreators.ts +0 -132
  21. package/src/actions.ts +0 -547
  22. package/src/atoms/index.ts +0 -600
  23. package/src/authoring.ts +0 -92
  24. package/src/browser-player.ts +0 -783
  25. package/src/builders.ts +0 -1342
  26. package/src/components/ExperienceWorkflowBridge.tsx +0 -123
  27. package/src/components/PlayerProvider.tsx +0 -43
  28. package/src/components/atoms/index.tsx +0 -269
  29. package/src/components/index.ts +0 -36
  30. package/src/conditions.ts +0 -692
  31. package/src/config/defineBlueprint.ts +0 -329
  32. package/src/config/defineModel.ts +0 -753
  33. package/src/config/defineWorkspace.ts +0 -24
  34. package/src/core/WorkflowRuntime.ts +0 -153
  35. package/src/factories.ts +0 -425
  36. package/src/grammar/index.ts +0 -173
  37. package/src/hooks/index.ts +0 -106
  38. package/src/hooks/useAuth.ts +0 -288
  39. package/src/hooks/useChannel.ts +0 -304
  40. package/src/hooks/useComputed.ts +0 -154
  41. package/src/hooks/useDomainSubscription.ts +0 -110
  42. package/src/hooks/useDuringAction.ts +0 -99
  43. package/src/hooks/useExperienceState.ts +0 -59
  44. package/src/hooks/useExpressionLibrary.ts +0 -129
  45. package/src/hooks/useForm.ts +0 -352
  46. package/src/hooks/useGeolocation.ts +0 -207
  47. package/src/hooks/useMapView.ts +0 -259
  48. package/src/hooks/useMiddleware.ts +0 -291
  49. package/src/hooks/useModel.ts +0 -363
  50. package/src/hooks/useModule.ts +0 -59
  51. package/src/hooks/useModuleConfig.ts +0 -61
  52. package/src/hooks/useMutation.ts +0 -237
  53. package/src/hooks/useNotification.ts +0 -151
  54. package/src/hooks/useOnChange.ts +0 -30
  55. package/src/hooks/useOnEnter.ts +0 -59
  56. package/src/hooks/useOnEvent.ts +0 -37
  57. package/src/hooks/useOnExit.ts +0 -27
  58. package/src/hooks/useOnTransition.ts +0 -30
  59. package/src/hooks/usePackage.ts +0 -128
  60. package/src/hooks/useParams.ts +0 -33
  61. package/src/hooks/usePlayer.ts +0 -308
  62. package/src/hooks/useQuery.ts +0 -184
  63. package/src/hooks/useRealtimeQuery.ts +0 -222
  64. package/src/hooks/useRole.ts +0 -191
  65. package/src/hooks/useRouteParams.ts +0 -100
  66. package/src/hooks/useRouter.ts +0 -347
  67. package/src/hooks/useServerAction.ts +0 -178
  68. package/src/hooks/useServerState.ts +0 -284
  69. package/src/hooks/useToast.ts +0 -164
  70. package/src/hooks/useTransition.ts +0 -39
  71. package/src/hooks/useView.ts +0 -102
  72. package/src/hooks/useWhileIn.ts +0 -48
  73. package/src/hooks/useWorkflow.ts +0 -63
  74. package/src/index.ts +0 -465
  75. package/src/loader/experience-workflow-loader.ts +0 -192
  76. package/src/loader/index.ts +0 -6
  77. package/src/local/LocalEngine.ts +0 -388
  78. package/src/local/LocalEngineAdapter.ts +0 -175
  79. package/src/local/LocalEngineContext.ts +0 -30
  80. package/src/logger.ts +0 -37
  81. package/src/mixins.ts +0 -1160
  82. package/src/providers/RuntimeContext.ts +0 -20
  83. package/src/providers/WorkflowProvider.tsx +0 -28
  84. package/src/routing/instance-key.ts +0 -107
  85. package/src/server/transition-context.ts +0 -172
  86. package/src/testing/index.ts +0 -9
  87. package/src/testing/useBlueprintTestRunner.ts +0 -91
  88. package/src/testing/useGraphAnalysis.ts +0 -18
  89. package/src/testing/useTestRunner.ts +0 -77
  90. package/src/testing.ts +0 -995
  91. package/src/types/workflow-inference.ts +0 -158
  92. package/src/types.ts +0 -114
  93. package/tsconfig.json +0 -27
  94. package/vitest.config.ts +0 -8
@@ -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
- }
@@ -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
- }