anear-js-api 1.3.0 → 1.3.2
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.
|
@@ -658,7 +658,8 @@ const ActiveEventStatesConfig = {
|
|
|
658
658
|
states: {
|
|
659
659
|
idle: {},
|
|
660
660
|
waitingForCancelConfirmations: {
|
|
661
|
-
|
|
661
|
+
// Note: Setup actions are called in the transitions that lead here, not in entry
|
|
662
|
+
// This allows different setup for declarative cancel (TRIGGER_CANCEL_SEQUENCE) vs manual cancel (CANCEL_ALL_PARTICIPANT_TIMEOUTS)
|
|
662
663
|
on: {
|
|
663
664
|
TIMER_CANCELED: {
|
|
664
665
|
actions: ['removeFromPendingCancelConfirmations']
|
|
@@ -677,7 +678,7 @@ const ActiveEventStatesConfig = {
|
|
|
677
678
|
},
|
|
678
679
|
always: {
|
|
679
680
|
guard: 'allCancelConfirmationsReceived',
|
|
680
|
-
actions: ['
|
|
681
|
+
actions: ['forwardPendingCancelActionIfExists', 'clearCancelState'],
|
|
681
682
|
target: '#waitingForActions.idle'
|
|
682
683
|
}
|
|
683
684
|
}
|
|
@@ -691,7 +692,12 @@ const ActiveEventStatesConfig = {
|
|
|
691
692
|
actions: ['trackParticipantTimeout', 'processParticipantTimeout'],
|
|
692
693
|
// v5 note: internal transitions are the default (v4 had `internal: true`)
|
|
693
694
|
},
|
|
695
|
+
CANCEL_ALL_PARTICIPANT_TIMEOUTS: {
|
|
696
|
+
actions: ['setupPendingCancelConfirmationsForManualCancel', 'sendCancelTimeoutToAllAPMs'],
|
|
697
|
+
target: '.waitingForCancelConfirmations'
|
|
698
|
+
},
|
|
694
699
|
TRIGGER_CANCEL_SEQUENCE: {
|
|
700
|
+
actions: ['sendCancelTimeoutToAllAPMs', 'setupPendingCancelConfirmations'],
|
|
695
701
|
target: '.waitingForCancelConfirmations'
|
|
696
702
|
},
|
|
697
703
|
TIMER_CANCELED: {
|
|
@@ -1749,6 +1755,19 @@ const AnearEventMachineFunctions = ({
|
|
|
1749
1755
|
suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
|
|
1750
1756
|
}
|
|
1751
1757
|
}),
|
|
1758
|
+
setupPendingCancelConfirmationsForManualCancel: assign(({ context }) => {
|
|
1759
|
+
// Get all active participants (for manual cancelAllParticipantTimeouts call)
|
|
1760
|
+
const activeParticipantIds = getActiveParticipantIds(context)
|
|
1761
|
+
const pendingConfirmations = new Set(activeParticipantIds)
|
|
1762
|
+
|
|
1763
|
+
logger.info(`[AEM] Starting manual cancellation sequence (cancelAllParticipantTimeouts). Waiting for ${pendingConfirmations.size} APM confirmations: ${[...pendingConfirmations].join(', ')}`)
|
|
1764
|
+
|
|
1765
|
+
return {
|
|
1766
|
+
pendingCancelConfirmations: pendingConfirmations,
|
|
1767
|
+
pendingCancelAction: null, // No ACTION to forward for manual cancel
|
|
1768
|
+
suppressParticipantTimeouts: true // Drop PARTICIPANT_TIMEOUT events
|
|
1769
|
+
}
|
|
1770
|
+
}),
|
|
1752
1771
|
removeFromPendingCancelConfirmations: assign(({ context, event }) => {
|
|
1753
1772
|
const participantId = event.participantId || event.data?.id
|
|
1754
1773
|
if (!context.pendingCancelConfirmations || !participantId) return {}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*
|
|
24
24
|
* - Enforce participant-level timeouts and activity rules:
|
|
25
25
|
* - Action timeouts:
|
|
26
|
-
* - After a
|
|
26
|
+
* - After a RENDER_DISPLAY that expects input,
|
|
27
27
|
* starts a per-participant response timer.
|
|
28
28
|
* - If the timer fires before an ACTION arrives, emits PARTICIPANT_TIMEOUT
|
|
29
29
|
* back to the AEM and the appParticipantMachine.
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* Ably wiring or lifecycle plumbing.
|
|
65
65
|
*
|
|
66
66
|
* Incoming events (from AEM / system) – high level:
|
|
67
|
-
* - RENDER_DISPLAY
|
|
67
|
+
* - RENDER_DISPLAY:
|
|
68
68
|
* - Prompt the participant with a new view or interaction on their private channel.
|
|
69
69
|
* - Optionally include an action timeout; APM transitions into a wait state.
|
|
70
70
|
*
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
* 2. It creates and attaches to the participant’s private Ably channel, then
|
|
88
88
|
* signals PARTICIPANT_MACHINE_READY back to the AEM.
|
|
89
89
|
* 3. It enters the live state, handling:
|
|
90
|
-
* -
|
|
90
|
+
* - RENDER_DISPLAY → publish UI, optionally start timeout.
|
|
91
91
|
* - ACTION → forward to appParticipantMachine, clear timeout, go idle.
|
|
92
92
|
* - DISCONNECT / RECONNECT → manage reconnect windows and possible timeouts.
|
|
93
93
|
* - EXIT / BOOT → send shutdown messaging and clean up.
|
|
@@ -166,9 +166,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
166
166
|
RENDER_DISPLAY: {
|
|
167
167
|
target: '#renderDisplay'
|
|
168
168
|
},
|
|
169
|
-
PRIVATE_DISPLAY: {
|
|
170
|
-
target: '#renderDisplay'
|
|
171
|
-
},
|
|
172
169
|
CANCEL_TIMEOUT: {
|
|
173
170
|
// Acknowledge CANCEL_TIMEOUT even when idle (no active timer).
|
|
174
171
|
// This ensures the AEM's cancellation sequence completes for all participants.
|
|
@@ -215,9 +212,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
215
212
|
RENDER_DISPLAY: {
|
|
216
213
|
target: '#renderDisplay'
|
|
217
214
|
},
|
|
218
|
-
PRIVATE_DISPLAY: {
|
|
219
|
-
target: '#renderDisplay'
|
|
220
|
-
},
|
|
221
215
|
PARTICIPANT_EXIT: {
|
|
222
216
|
target: '#cleanupAndExit'
|
|
223
217
|
},
|
|
@@ -264,6 +258,9 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
264
258
|
target: '#participantTimedOut'
|
|
265
259
|
},
|
|
266
260
|
after: {
|
|
261
|
+
// Note: This timer continues running even when in nested .rendering state.
|
|
262
|
+
// XState v5 handles this correctly - the timer fires from the parent state
|
|
263
|
+
// and can transition even during invoke processing.
|
|
267
264
|
actionTimeout: {
|
|
268
265
|
actions: 'nullActionTimeout',
|
|
269
266
|
target: '#participantTimedOut'
|
|
@@ -272,17 +269,27 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
272
269
|
initial: 'waiting',
|
|
273
270
|
states: {
|
|
274
271
|
waiting: {
|
|
275
|
-
// The actual waiting state.
|
|
272
|
+
// The actual waiting state. RENDER_DISPLAY is blocked here
|
|
273
|
+
// to prevent timer conflicts. AppM must cancel timers first.
|
|
276
274
|
},
|
|
277
275
|
rendering: {
|
|
276
|
+
// NOTE: This nested state is currently unreachable as RENDER_DISPLAY
|
|
277
|
+
// is blocked during waitParticipantResponse. Kept for potential future use or
|
|
278
|
+
// other edge cases. The parent's `after: actionTimeout` timer continues running
|
|
279
|
+
// during invoke if this state were to be entered.
|
|
278
280
|
invoke: {
|
|
279
281
|
src: 'publishPrivateDisplay',
|
|
280
282
|
input: ({ context, event }) => ({ context, event }),
|
|
281
|
-
onDone:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
onDone: [
|
|
284
|
+
{
|
|
285
|
+
guard: 'hasActionTimeout',
|
|
286
|
+
actions: 'updateActionTimeoutIfChanged',
|
|
287
|
+
target: 'waiting'
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
target: 'waiting'
|
|
291
|
+
}
|
|
292
|
+
],
|
|
286
293
|
onError: {
|
|
287
294
|
target: '#error'
|
|
288
295
|
}
|
|
@@ -297,13 +304,11 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
297
304
|
},
|
|
298
305
|
on: {
|
|
299
306
|
RENDER_DISPLAY: {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// Same as RENDER_DISPLAY - re-render without interrupting timer
|
|
306
|
-
target: '.rendering'
|
|
307
|
+
// BLOCKED: RENDER_DISPLAY during active timeout wait is not allowed.
|
|
308
|
+
// The AppM must first cancel all participant timers (via cancelAllParticipantTimeouts()
|
|
309
|
+
// or cancel: "ACTION" in meta) and wait for ACK before issuing new RENDER_DISPLAY
|
|
310
|
+
// with timeouts. This prevents timer conflicts and ensures clean state transitions.
|
|
311
|
+
actions: 'logBlockedRenderDuringWait'
|
|
307
312
|
},
|
|
308
313
|
PARTICIPANT_EXIT: {
|
|
309
314
|
actions: 'logExit',
|
|
@@ -346,7 +351,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
346
351
|
// Ignore most events during cleanup, but allow exit displays
|
|
347
352
|
on: {
|
|
348
353
|
RENDER_DISPLAY: { actions: 'logIgnoringRenderDisplayCleanup' },
|
|
349
|
-
PRIVATE_DISPLAY: { actions: 'logIgnoringPrivateDisplayCleanup' },
|
|
350
354
|
PARTICIPANT_EXIT: {
|
|
351
355
|
actions: 'logIgnoringRedundantExit'
|
|
352
356
|
},
|
|
@@ -407,7 +411,6 @@ const AnearParticipantMachineConfig = participantId => ({
|
|
|
407
411
|
// Final state - ignore all events except exit displays
|
|
408
412
|
on: {
|
|
409
413
|
RENDER_DISPLAY: { actions: 'logIgnoringRenderDisplayDone' },
|
|
410
|
-
PRIVATE_DISPLAY: { actions: 'logIgnoringPrivateDisplayDone' },
|
|
411
414
|
'*': {
|
|
412
415
|
actions: 'logIgnoringEventDone'
|
|
413
416
|
}
|
|
@@ -491,6 +494,36 @@ const AnearParticipantMachineFunctions = {
|
|
|
491
494
|
actionTimeoutStart: now
|
|
492
495
|
}
|
|
493
496
|
}),
|
|
497
|
+
updateActionTimeoutIfChanged: assign(({ context, event }) => {
|
|
498
|
+
const timeoutFromEvent = event?.output?.timeout ?? event?.data?.timeout
|
|
499
|
+
const now = CurrentDateTimestamp()
|
|
500
|
+
|
|
501
|
+
// When RENDER_DISPLAY arrives during waitParticipantResponse, check if the timeout has changed.
|
|
502
|
+
// If it has, update the timer. This handles state transitions where a new timeout is needed.
|
|
503
|
+
const existingMsecs = context.actionTimeoutMsecs
|
|
504
|
+
const existingStart = context.actionTimeoutStart
|
|
505
|
+
|
|
506
|
+
if (timeoutFromEvent == null || timeoutFromEvent <= 0) {
|
|
507
|
+
// New display has no timeout - preserve existing timer (this is a visual status update)
|
|
508
|
+
// Don't clear it, as we're still waiting for the original ACTION
|
|
509
|
+
logger.debug(`[APM] Preserving existing timer for ${context.anearParticipant.id} (new display has no timeout, visual status update)`)
|
|
510
|
+
return {}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// If we already have an active timer and receive a new timeout, this is a bug (detected above).
|
|
514
|
+
// Preserve the existing timer to avoid conflicts. The AppM should cancel timers first.
|
|
515
|
+
if (existingMsecs != null && existingStart != null) {
|
|
516
|
+
logger.warn(`[APM] AppM BUG: Received timeout=${timeoutFromEvent}ms while timer already active (${existingMsecs}ms) for ${context.anearParticipant.id}. Preserving existing timer. AppM should cancel timers first.`)
|
|
517
|
+
return {}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// No existing timer - start a new one (shouldn't happen in waitParticipantResponse, but handle gracefully)
|
|
521
|
+
logger.debug(`[APM] Starting new timer for ${context.anearParticipant.id} with ${timeoutFromEvent}ms`)
|
|
522
|
+
return {
|
|
523
|
+
actionTimeoutMsecs: timeoutFromEvent,
|
|
524
|
+
actionTimeoutStart: now
|
|
525
|
+
}
|
|
526
|
+
}),
|
|
494
527
|
nullActionTimeout: assign(({ context }) => {
|
|
495
528
|
if (context.actionTimeoutMsecs != null || context.actionTimeoutStart != null) {
|
|
496
529
|
logger.debug(`[APM] Cancelling timer for ${context.anearParticipant.id} (ACTION received or timeout cleared)`)
|
|
@@ -556,13 +589,25 @@ const AnearParticipantMachineFunctions = {
|
|
|
556
589
|
logReconnected: ({ context }) => logger.info(`[APM] Participant ${context.anearParticipant.id} has RECONNECTED`),
|
|
557
590
|
logImmediateTimeout: ({ context }) => logger.debug(`[APM] Timeout of ${context.actionTimeoutMsecs}ms is immediate for ${context.anearParticipant.id}.`),
|
|
558
591
|
logIgnoringRenderDisplayCleanup: (_args) => logger.debug('[APM] ignoring RENDER_DISPLAY during cleanup'),
|
|
559
|
-
logIgnoringPrivateDisplayCleanup: (_args) => logger.debug('[APM] ignoring PRIVATE_DISPLAY during cleanup'),
|
|
560
592
|
logIgnoringRedundantExit: () => logger.debug('[APM] ignoring redundant PARTICIPIPANT_EXIT during cleanup'),
|
|
561
593
|
logIgnoringReconnectCleanup: () => logger.debug('[APM] ignoring PARTICIPANT_RECONNECT during cleanup - already timed out'),
|
|
562
594
|
logIgnoringActionCleanup: () => logger.debug('[APM] ignoring ACTION during cleanup'),
|
|
563
595
|
logIgnoringRenderDisplayDone: () => logger.debug('[APM] ignoring RENDER_DISPLAY in final state'),
|
|
564
|
-
|
|
565
|
-
|
|
596
|
+
logIgnoringEventDone: ({ event }) => logger.debug('[APM] ignoring event in final state: ', event.type),
|
|
597
|
+
logBlockedRenderDuringWait: ({ context, event }) => {
|
|
598
|
+
const eventType = event?.type ?? 'RENDER_DISPLAY'
|
|
599
|
+
const timeout = event?.timeout ?? event?.data?.timeout
|
|
600
|
+
const participantId = context.anearParticipant.id
|
|
601
|
+
const remainingMsecs = context.actionTimeoutMsecs
|
|
602
|
+
const timeoutInfo = timeout ? ` (new timeout: ${timeout}ms)` : ''
|
|
603
|
+
|
|
604
|
+
logger.error(`[APM] ⚠️ BLOCKED: ${eventType} received during active timeout wait for participant ${participantId}. Current timer: ${remainingMsecs}ms remaining.${timeoutInfo}`)
|
|
605
|
+
logger.error(`[APM] ⚠️ The AppM must first cancel all participant timers using one of these methods:`)
|
|
606
|
+
logger.error(`[APM] ⚠️ 1. Declarative: Add cancel: "ACTION_NAME" to meta.eachParticipant or meta.allParticipants`)
|
|
607
|
+
logger.error(`[APM] ⚠️ 2. Manual: Call anearEvent.cancelAllParticipantTimeouts() and wait for ACK`)
|
|
608
|
+
logger.error(`[APM] ⚠️ Only after timers are cancelled (APM transitions to idle) can new RENDER_DISPLAY with timeouts be issued.`)
|
|
609
|
+
logger.error(`[APM] ⚠️ This ${eventType} event is being IGNORED to prevent timer conflicts.`)
|
|
610
|
+
}
|
|
566
611
|
},
|
|
567
612
|
actors: {
|
|
568
613
|
publishPrivateDisplay: fromPromise(async ({ input }) => {
|
|
@@ -102,6 +102,14 @@ const AppMachineTransition = (anearEvent) => {
|
|
|
102
102
|
let timeoutFn
|
|
103
103
|
let displayEvent
|
|
104
104
|
|
|
105
|
+
// Extract cancel from meta level (for function-based eachParticipant or top-level cancel)
|
|
106
|
+
// Supports both "cancel" (developer-friendly) and "cancelActionType" (internal)
|
|
107
|
+
if (meta.cancel && !cancelActionType) {
|
|
108
|
+
cancelActionType = meta.cancel
|
|
109
|
+
} else if (meta.cancelActionType && !cancelActionType) {
|
|
110
|
+
cancelActionType = meta.cancelActionType
|
|
111
|
+
}
|
|
112
|
+
|
|
105
113
|
// Validate that participants: and participant: are not both defined
|
|
106
114
|
if (meta.allParticipants && meta.eachParticipant) {
|
|
107
115
|
logger.error(`[AppMachineTransition] Invalid meta configuration: both 'allParticipants' and 'eachParticipant' are defined. Only one can be used per state.`)
|