@dudousxd/nestjs-durable-core 0.53.0 → 0.55.0

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/dist/index.cjs CHANGED
@@ -30,6 +30,8 @@ __export(index_exports, {
30
30
  DURABLE_STEP_CONFIG: () => DURABLE_STEP_CONFIG,
31
31
  DURABLE_STEP_NAME: () => DURABLE_STEP_NAME,
32
32
  DurableWorkflow: () => DurableWorkflow,
33
+ EVENT_BUFFER_SCAN_LIMIT: () => EVENT_BUFFER_SCAN_LIMIT,
34
+ EVENT_TOKEN_PREFIX: () => EVENT_TOKEN_PREFIX,
33
35
  Entities: () => Entities,
34
36
  EventAccumulators: () => EventAccumulators,
35
37
  FatalError: () => FatalError,
@@ -68,6 +70,7 @@ __export(index_exports, {
68
70
  eventPrefix: () => eventPrefix,
69
71
  eventToken: () => eventToken,
70
72
  indexWaitersByRun: () => indexWaitersByRun,
73
+ isEventToken: () => isEventToken,
71
74
  isWorkflowControlFlowSignal: () => isWorkflowControlFlowSignal,
72
75
  matchesAttributes: () => matchesAttributes,
73
76
  normalizeAttributeRows: () => normalizeAttributeRows,
@@ -871,10 +874,16 @@ var EventAccumulators = class {
871
874
  // src/events.ts
872
875
  var enc = /* @__PURE__ */ __name((s) => Buffer.from(s, "utf8").toString("base64"), "enc");
873
876
  var dec = /* @__PURE__ */ __name((s) => Buffer.from(s, "base64").toString("utf8"), "dec");
877
+ var EVENT_BUFFER_SCAN_LIMIT = 50;
878
+ var EVENT_TOKEN_PREFIX = "event:";
874
879
  function eventPrefix(name) {
875
- return `event:${enc(name)}:`;
880
+ return `${EVENT_TOKEN_PREFIX}${enc(name)}:`;
876
881
  }
877
882
  __name(eventPrefix, "eventPrefix");
883
+ function isEventToken(token) {
884
+ return token.startsWith(EVENT_TOKEN_PREFIX);
885
+ }
886
+ __name(isEventToken, "isEventToken");
878
887
  function eventToken(name, match, runId, seq) {
879
888
  return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
880
889
  }
@@ -957,7 +966,7 @@ var RemoteWorkflowExecutor = class {
957
966
  var WEBHOOK_PREFIX = "wh:";
958
967
  var CHILD_PREFIX = "child:";
959
968
  var BREAKPOINT_PREFIX = "bp:";
960
- var EVENT_PREFIX = "event:";
969
+ var EVENT_PREFIX = EVENT_TOKEN_PREFIX;
961
970
  function classifyWaiterToken(token) {
962
971
  if (token.startsWith(WEBHOOK_PREFIX)) return {
963
972
  on: "webhook",
@@ -1181,6 +1190,21 @@ var TransportPool = class {
1181
1190
  ];
1182
1191
  }
1183
1192
  /**
1193
+ * The first transport (in pool order) reporting a LIVE heartbeat for `group` — the one a
1194
+ * convention-routed remote workflow must dispatch on. In a mixed pool (e.g. a tenant-scoped
1195
+ * control plane pairing its namespaced transport with a bare-prefix one for operator-convention
1196
+ * tenant workers), each transport sees a DIFFERENT keyspace: dispatching on `primary` while the
1197
+ * live worker heartbeats on another transport parks the task on a queue nobody consumes. Returns
1198
+ * `undefined` when no transport reports the group (mirrors the merged `listWorkerGroups` miss).
1199
+ */
1200
+ async transportWithLiveGroup(group) {
1201
+ for (const { transport } of this.transports) {
1202
+ if (!transport.listWorkerGroups) continue;
1203
+ if ((await transport.listWorkerGroups()).includes(group)) return transport;
1204
+ }
1205
+ return void 0;
1206
+ }
1207
+ /**
1184
1208
  * Propagate the engine's `namespace` to every transport that partitions by it — so the same
1185
1209
  * namespace that scopes the store also scopes each transport's queues/keys. A transport that
1186
1210
  * doesn't partition (no `useNamespace`) is skipped. Idempotent; a transport given an explicit
@@ -1423,6 +1447,29 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
1423
1447
  value: buffered.payload
1424
1448
  };
1425
1449
  }, "consumeBuffered");
1450
+ const consumeBufferedEvent = /* @__PURE__ */ __name(async (name, match, token, seq) => {
1451
+ const candidates = await store.listBufferedEvents(name, EVENT_BUFFER_SCAN_LIMIT);
1452
+ for (const candidate of candidates) {
1453
+ if (!eventMatches(candidate.payload, match ?? {})) continue;
1454
+ if (!await store.removeBufferedEvent(candidate.id)) continue;
1455
+ await store.removeSignalWaiter({
1456
+ token,
1457
+ runId,
1458
+ seq
1459
+ });
1460
+ await writeCheckpoint(instantCheckpoint({
1461
+ runId,
1462
+ seq,
1463
+ name: `signal:${token}`,
1464
+ kind: "signal",
1465
+ output: candidate.payload
1466
+ }));
1467
+ return {
1468
+ value: candidate.payload
1469
+ };
1470
+ }
1471
+ return null;
1472
+ }, "consumeBufferedEvent");
1426
1473
  const stampDeadline = /* @__PURE__ */ __name(async (deadlineSeq, label, timeoutMs) => {
1427
1474
  const recorded = await readCheckpoint(deadlineSeq);
1428
1475
  const deadline = recorded?.wakeAt ?? host.clock() + timeoutMs;
@@ -1503,6 +1550,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
1503
1550
  runId,
1504
1551
  seq: current
1505
1552
  });
1553
+ const bufferedHit2 = await consumeBufferedEvent(name, opts?.match, token2, current);
1554
+ if (bufferedHit2) return bufferedHit2.value;
1506
1555
  throw new WorkflowSuspended();
1507
1556
  }
1508
1557
  const timeoutMs = opts.timeoutMs;
@@ -1525,6 +1574,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
1525
1574
  runId,
1526
1575
  seq: waitSeq
1527
1576
  });
1577
+ const bufferedHit = await consumeBufferedEvent(name, opts.match, token, waitSeq);
1578
+ if (bufferedHit) return bufferedHit.value;
1528
1579
  throw new WorkflowSuspended(deadline);
1529
1580
  }, "waitForEvent");
1530
1581
  const task = /* @__PURE__ */ __name(async (name, dispatch, options) => {
@@ -1918,6 +1969,7 @@ var WorkflowEngine = class {
1918
1969
  maxRecoveryAttempts;
1919
1970
  remoteAdvanceSilenceMs;
1920
1971
  reconcileMs;
1972
+ eventBufferTtlMs;
1921
1973
  remoteRedispatchMs;
1922
1974
  remoteRedispatchMax;
1923
1975
  webhookUrl;
@@ -1991,6 +2043,7 @@ var WorkflowEngine = class {
1991
2043
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1992
2044
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
1993
2045
  this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
2046
+ this.eventBufferTtlMs = deps.eventBufferTtlMs || void 0;
1994
2047
  this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
1995
2048
  this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
1996
2049
  this.webhookUrl = deps.webhookUrl;
@@ -2467,10 +2520,10 @@ var WorkflowEngine = class {
2467
2520
  * liveness changes between calls, exactly as {@link synthesizeRemoteChild} does.
2468
2521
  */
2469
2522
  async resolveRemoteByConvention(run) {
2470
- const liveGroups = await this.pool.listWorkerGroups();
2471
2523
  const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
2472
- if (!liveGroups.includes(group)) return void 0;
2473
- const executor = new RemoteWorkflowExecutor(this.pool.primary, run.workflow, run.namespace);
2524
+ const liveTransport = await this.pool.transportWithLiveGroup(group);
2525
+ if (!liveTransport) return void 0;
2526
+ const executor = new RemoteWorkflowExecutor(liveTransport, run.workflow, run.namespace);
2474
2527
  return {
2475
2528
  name: run.workflow,
2476
2529
  version: run.workflowVersion,
@@ -2664,14 +2717,16 @@ var WorkflowEngine = class {
2664
2717
  * Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
2665
2718
  * boot. A run still not due re-suspends cheaply without running new work.
2666
2719
  *
2667
- * Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
2668
- * between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
2669
- * `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
2670
- * `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
2671
- * waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
2672
- * suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
2673
- * they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
2674
- * bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
2720
+ * Piggy-backs the signal/event-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a
2721
+ * crash between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
2722
+ * `takeBufferedSignal`/`listBufferedEvents`, or the other side's `takeSignalWaiter`/fan-out →
2723
+ * `bufferSignal`/`bufferEvent` → re-check — see `signal`'s interleaving proof, which `publishEvent`
2724
+ * mirrors for events) can leave both rows stranded with nothing left to pair them: the waiter's turn
2725
+ * already suspended, the other side that buffered it already returned. Event-wait suspends already
2726
+ * carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so they show up
2727
+ * HERE — that's what makes this due-timer pass the natural place for the sweep (and, for events, also
2728
+ * where an `eventBufferTtlMs` prune piggy-backs — see {@link reconcileBufferedEvent}). One bulk waiter
2729
+ * scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
2675
2730
  */
2676
2731
  async resumeDueTimers(nowMs = this.clock()) {
2677
2732
  const due = await this.store.listDueTimers(nowMs, this.namespace);
@@ -2681,11 +2736,15 @@ var WorkflowEngine = class {
2681
2736
  }
2682
2737
  /**
2683
2738
  * The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
2684
- * a registered signal waiter AND its token already has a buffered payload, pair and deliver them
2685
- * directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
2686
- * never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
2687
- * once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
2688
- * miss, and this is naturally signal-only without needing to classify the token.
2739
+ * a registered signal OR event waiter and a matching payload is already buffered, pair and deliver
2740
+ * them directly instead of falling through to a full replay. Child/breakpoint waiter tokens are
2741
+ * never buffered (a child/breakpoint notifies via `signal` only once its counterpart is already
2742
+ * registered) — `takeBufferedSignal` on those is a guaranteed miss, so this only ever does real work
2743
+ * for a plain signal or an event waiter (classified via {@link isEventToken}).
2744
+ *
2745
+ * Also opportunistically prunes expired buffered EVENTS for this waiter's name when
2746
+ * {@link eventBufferTtlMs} is set (see its doc — scoped to names a registered waiter references in
2747
+ * this tick's batch, piggy-backing the SAME scan rather than a separate pass), even on a miss.
2689
2748
  *
2690
2749
  * Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
2691
2750
  * no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
@@ -2695,6 +2754,7 @@ var WorkflowEngine = class {
2695
2754
  */
2696
2755
  async reconcileSignalWaiter(run, waiter) {
2697
2756
  if (!waiter) return void 0;
2757
+ if (isEventToken(waiter.token)) return this.reconcileBufferedEvent(run, waiter);
2698
2758
  const buffered = await this.store.takeBufferedSignal(waiter.token);
2699
2759
  if (!buffered) return void 0;
2700
2760
  await this.store.removeSignalWaiter(waiter);
@@ -2705,6 +2765,34 @@ var WorkflowEngine = class {
2705
2765
  };
2706
2766
  }
2707
2767
  /**
2768
+ * The EVENT half of {@link reconcileSignalWaiter}: scan this waiter's name for a matching buffered
2769
+ * event (same list+evaluate+claim shape as `waitForEvent`'s own consumption — the match predicate
2770
+ * belongs to the waiter, never the store), pruning any expired candidates it encounters along the
2771
+ * way when {@link eventBufferTtlMs} is configured. A pruned-but-non-matching candidate does not stop
2772
+ * the scan; only a MATCHING hit (or exhausting the scan window) does.
2773
+ */
2774
+ async reconcileBufferedEvent(run, waiter) {
2775
+ const name = eventNameOf(waiter.token);
2776
+ const match = eventMatchOf(waiter.token);
2777
+ const candidates = await this.store.listBufferedEvents(name, EVENT_BUFFER_SCAN_LIMIT);
2778
+ const nowMs = this.clock();
2779
+ for (const candidate of candidates) {
2780
+ if (this.eventBufferTtlMs != null && nowMs - candidate.publishedAt > this.eventBufferTtlMs) {
2781
+ await this.store.removeBufferedEvent(candidate.id);
2782
+ continue;
2783
+ }
2784
+ if (!eventMatches(candidate.payload, match)) continue;
2785
+ if (!await this.store.removeBufferedEvent(candidate.id)) continue;
2786
+ await this.store.removeSignalWaiter(waiter);
2787
+ const settled = await this.deliverSignal(waiter, candidate.payload);
2788
+ return settled ?? {
2789
+ runId: run.id,
2790
+ status: run.status
2791
+ };
2792
+ }
2793
+ return void 0;
2794
+ }
2795
+ /**
2708
2796
  * Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
2709
2797
  * exactly one instance runs it), then resumes the body. Returns the result, or null if another
2710
2798
  * instance holds the lease or the engine is draining. The default in-process dispatcher calls this;
@@ -2892,6 +2980,21 @@ var WorkflowEngine = class {
2892
2980
  * 2. **Starts** a fresh run of every workflow registered with `onEvent: [name]`, passing the
2893
2981
  * payload as input. Idempotent by `evt:<id>:<workflow>` — pass `opts.id` to dedupe redeliveries
2894
2982
  * of the same logical event (default: a fresh uuid, so each publish triggers once).
2983
+ *
2984
+ * Semantics (deliberate — mirrors `signalWithStart`'s reliability contract, so document it exactly):
2985
+ * a publish that resumes ≥1 live waiter, OR routes into an `eventBatch` accumulator / starts ≥1
2986
+ * subscriber, behaves exactly as above and is NOT buffered — fan-out stays live-only, same as
2987
+ * today. A publish that touches NEITHER — no live waiter matched, no subscriber exists/started —
2988
+ * buffers ONE copy (via {@link StateStore.bufferEvent}) unless `opts.buffer === false`, so it isn't
2989
+ * silently dropped just because nobody happened to be listening yet. That buffered copy is consumed
2990
+ * by the FIRST future `ctx.waitForEvent(name, { match })` whose match accepts its payload —
2991
+ * point-to-point on redelivery, by design (only one waiter ever gets it, even though the LIVE path
2992
+ * above is fan-out) — never by a second, later-registered onEvent subscriber (subscriber starts stay
2993
+ * live-only; `opts.id` dedupe applies to those only, unchanged). Right after buffering, this re-checks
2994
+ * `listSignalWaiters` ONCE — sandwich parity with `signal`'s own take → buffer → re-check — so a
2995
+ * waiter that registers in the sliver between the initial miss and the buffer write is still paired
2996
+ * instead of leaving both rows stranded (see `signal`'s interleaving proof; `waitForEvent`'s own
2997
+ * post-registration buffer scan is the mirror-image half of this same race).
2895
2998
  */
2896
2999
  async publishEvent(name, payload, opts) {
2897
3000
  let touched = 0;
@@ -2918,6 +3021,23 @@ var WorkflowEngine = class {
2918
3021
  }
2919
3022
  }
2920
3023
  }
3024
+ if (touched > 0 || opts?.buffer === false) return touched;
3025
+ const bufferedId = globalThis.crypto.randomUUID();
3026
+ await this.store.bufferEvent({
3027
+ name,
3028
+ payload,
3029
+ id: bufferedId,
3030
+ publishedAt: this.clock()
3031
+ });
3032
+ const lateWaiters = await this.store.listSignalWaiters(eventPrefix(name));
3033
+ const lateWaiter = lateWaiters.find((w) => eventMatches(payload, eventMatchOf(w.token)));
3034
+ if (!lateWaiter) return touched;
3035
+ if (!await this.store.removeBufferedEvent(bufferedId)) return touched;
3036
+ const waiter = await this.store.takeSignalWaiter(lateWaiter.token);
3037
+ if (waiter) {
3038
+ const settled = await this.deliverSignal(waiter, payload);
3039
+ if (settled) touched += 1;
3040
+ }
2921
3041
  return touched;
2922
3042
  }
2923
3043
  /**
@@ -3022,6 +3142,27 @@ var WorkflowEngine = class {
3022
3142
  void this.signal(`child:${runId}`, completion).catch(() => void 0);
3023
3143
  }
3024
3144
  /**
3145
+ * Deferred child start shared by the in-process ctx host and the remote `startChild` command.
3146
+ * Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
3147
+ * that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
3148
+ * failure) must NOT be swallowed: the parent is already suspended on `child:<childId>` (the waiter
3149
+ * is put BEFORE start on every path), so a silent drop parks it in suspended-forever, invisibly
3150
+ * re-attempting on every recovery wake — a misconfigured remote child looks exactly like a healthy
3151
+ * long wait. Instead the failure is delivered to that waiter like a failed child (notifyParent):
3152
+ * the parent resumes and fails loudly with the cause. For a fire-and-forget `ctx.startChild`
3153
+ * (spawn — no waiter) the completion is buffered; a later join by the same id consumes it and
3154
+ * correctly observes the failed start.
3155
+ */
3156
+ startChildDeferred(workflow, input, childId, opts) {
3157
+ queueMicrotask(() => void this.start(workflow, input, childId, opts).catch((err) => {
3158
+ const message = err instanceof Error ? err.message : String(err);
3159
+ this.notifyParent(childId, {
3160
+ ok: false,
3161
+ error: `child workflow "${workflow}" failed to start: ${message}`
3162
+ });
3163
+ }));
3164
+ }
3165
+ /**
3025
3166
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
3026
3167
  * `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
3027
3168
  * steps' compensations run in reverse (visible as `compensate:<step>` events), then it's marked
@@ -3845,9 +3986,9 @@ var WorkflowEngine = class {
3845
3986
  parallelGroup: cmd.parallelGroup
3846
3987
  });
3847
3988
  if (!await this.store.getRun(childId)) {
3848
- queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId, {
3989
+ this.startChildDeferred(cmd.workflow, cmd.input, childId, {
3849
3990
  namespace: run.namespace
3850
- }).catch(() => void 0));
3991
+ });
3851
3992
  }
3852
3993
  } else {
3853
3994
  throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
@@ -4226,10 +4367,10 @@ var WorkflowEngine = class {
4226
4367
  // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
4227
4368
  // tenant's worker pool onto the shared/dev workers.
4228
4369
  startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
4229
- queueMicrotask(() => void this.start(workflow, input, id, {
4370
+ this.startChildDeferred(workflow, input, id, {
4230
4371
  priority,
4231
4372
  namespace: parentNamespace
4232
- }).catch(() => void 0));
4373
+ });
4233
4374
  }, "startChild"),
4234
4375
  // Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
4235
4376
  // idempotent on a terminal/cancelled run (returns its existing status without side effects), so
@@ -4809,6 +4950,22 @@ var CodecStateStore = class {
4809
4950
  payload: this.dec(buffered.payload)
4810
4951
  };
4811
4952
  }
4953
+ bufferEvent(input) {
4954
+ return this.inner.bufferEvent({
4955
+ ...input,
4956
+ payload: this.enc(input.payload)
4957
+ });
4958
+ }
4959
+ async listBufferedEvents(name, limit) {
4960
+ const events = await this.inner.listBufferedEvents(name, limit);
4961
+ return events.map((e) => ({
4962
+ ...e,
4963
+ payload: this.dec(e.payload)
4964
+ }));
4965
+ }
4966
+ removeBufferedEvent(id) {
4967
+ return this.inner.removeBufferedEvent(id);
4968
+ }
4812
4969
  async listRuns(query) {
4813
4970
  return (await this.inner.listRuns(query)).map((r) => this.decRun(r));
4814
4971
  }
@@ -5281,6 +5438,28 @@ var InMemoryStateStore = class {
5281
5438
  payload
5282
5439
  };
5283
5440
  }
5441
+ /** `id -> row`, so {@link removeBufferedEvent} can delete an exact row regardless of iteration
5442
+ * order — mirrors the SQL stores' PK-by-id delete. */
5443
+ bufferedEvents = /* @__PURE__ */ new Map();
5444
+ async bufferEvent(input) {
5445
+ this.bufferedEvents.set(input.id, {
5446
+ name: input.name,
5447
+ payload: input.payload,
5448
+ publishedAt: input.publishedAt
5449
+ });
5450
+ }
5451
+ async listBufferedEvents(name, limit) {
5452
+ return [
5453
+ ...this.bufferedEvents.entries()
5454
+ ].filter(([, e]) => e.name === name).sort((a, b) => a[1].publishedAt - b[1].publishedAt).slice(0, limit).map(([id, e]) => ({
5455
+ id,
5456
+ payload: e.payload,
5457
+ publishedAt: e.publishedAt
5458
+ }));
5459
+ }
5460
+ async removeBufferedEvent(id) {
5461
+ return this.bufferedEvents.delete(id);
5462
+ }
5284
5463
  /**
5285
5464
  * Candidate run ids satisfying ONE attribute predicate, served from {@link attributeIndex} — the
5286
5465
  * pushdown step. Numeric predicates scan only `numValue` rows under the key; string/boolean ones
@@ -5440,6 +5619,8 @@ var InMemoryTransport = class {
5440
5619
  DURABLE_STEP_CONFIG,
5441
5620
  DURABLE_STEP_NAME,
5442
5621
  DurableWorkflow,
5622
+ EVENT_BUFFER_SCAN_LIMIT,
5623
+ EVENT_TOKEN_PREFIX,
5443
5624
  Entities,
5444
5625
  EventAccumulators,
5445
5626
  FatalError,
@@ -5478,6 +5659,7 @@ var InMemoryTransport = class {
5478
5659
  eventPrefix,
5479
5660
  eventToken,
5480
5661
  indexWaitersByRun,
5662
+ isEventToken,
5481
5663
  isWorkflowControlFlowSignal,
5482
5664
  matchesAttributes,
5483
5665
  normalizeAttributeRows,