|
20 | 20 | * its barrier. They then skipped both the gate and |
21 | 21 | * `awaitEarlierDeliveries`' macrotask yield. |
22 | 22 | * |
23 | | - * 3. ABORT deliveries. `_setAborted` fires the signal's listeners, and a |
| 23 | + * 3. HOOK behind HOOK. Adjacent payloads for independently awaited hooks |
| 24 | + * must reach their consumers in log order even when the earlier consumer |
| 25 | + * takes more microtask hops to act on its value. |
| 26 | + * |
| 27 | + * 4. ABORT deliveries. `_setAborted` fires the signal's listeners, and a |
24 | 28 | * listener may invoke a step and draw a ULID, so an abort is as |
25 | 29 | * branch-deciding as any other delivery — but it resolved straight off its |
26 | 30 | * `promiseQueue` slot and registered no barrier. |
27 | 31 | * |
28 | | - * Cases 1-3 assert the same thing: the replay allocates its follow-up step |
| 32 | + * Cases 1-4 assert the same thing: the replay allocates its follow-up step |
29 | 33 | * ULIDs in the order the committed log recorded. A regression surfaces as the |
30 | 34 | * production `ReplayDivergenceError`. |
31 | 35 | * |
32 | | - * Section 4 asserts the registry's other job directly, over a registry built |
| 36 | + * Section 5 asserts the registry's other job directly, over a registry built |
33 | 37 | * by hand rather than by a replay: which entries an idle check may ignore. Get |
34 | 38 | * that wrong in one direction and a chain parked on an unclaimed hook payload |
35 | 39 | * deadlocks; wrong in the other and a suspension preempts a batch of parked |
@@ -406,7 +410,75 @@ describe('hook payload delivery ordering against an earlier step result', () => |
406 | 410 | } |
407 | 411 | }); |
408 | 412 |
|
409 | | -// ─── 3. abort deliveries ─────────────────────────────────────────────────── |
| 413 | +// ─── 3. hook behind hook, across different consumer shapes ───────────────── |
| 414 | +describe('hook payload delivery ordering against an earlier hook payload', () => { |
| 415 | + it('keeps the recorded ULID allocation when the earlier hook uses an async iterator', async () => { |
| 416 | + const ops: Promise<unknown>[] = []; |
| 417 | + const [firstPayload, secondPayload] = await Promise.all([ |
| 418 | + dehydrateStepReturnValue({ v: 1 }, 'wrun_test', undefined, ops), |
| 419 | + dehydrateStepReturnValue({ v: 2 }, 'wrun_test', undefined, ops), |
| 420 | + ]); |
| 421 | + |
| 422 | + const events: Event[] = [ |
| 423 | + event('evnt_0', 'hook_created', `hook_${ULIDS[0]}`, { |
| 424 | + token: 'first', |
| 425 | + isWebhook: false, |
| 426 | + }), |
| 427 | + event('evnt_1', 'hook_created', `hook_${ULIDS[1]}`, { |
| 428 | + token: 'second', |
| 429 | + isWebhook: false, |
| 430 | + }), |
| 431 | + event('evnt_2', 'hook_received', `hook_${ULIDS[0]}`, { |
| 432 | + token: 'first', |
| 433 | + payload: firstPayload, |
| 434 | + }), |
| 435 | + event('evnt_3', 'hook_received', `hook_${ULIDS[1]}`, { |
| 436 | + token: 'second', |
| 437 | + payload: secondPayload, |
| 438 | + }), |
| 439 | + event('evnt_4', 'step_created', `step_${ULIDS[2]}`, { |
| 440 | + stepName: 'afterFirst', |
| 441 | + }), |
| 442 | + event('evnt_5', 'step_created', `step_${ULIDS[3]}`, { |
| 443 | + stepName: 'afterSecond', |
| 444 | + }), |
| 445 | + ]; |
| 446 | + |
| 447 | + const ctx = setupWorkflowContext(events); |
| 448 | + const useStep = createUseStep(ctx); |
| 449 | + const createHook = createCreateHook(ctx); |
| 450 | + |
| 451 | + const error = await replay(ctx, async () => { |
| 452 | + const first = createHook<{ v: number }>({ token: 'first' }); |
| 453 | + const second = createHook<{ v: number }>({ token: 'second' }); |
| 454 | + const afterFirst = useStep('afterFirst'); |
| 455 | + const afterSecond = useStep('afterSecond'); |
| 456 | + |
| 457 | + await Promise.all([ |
| 458 | + (async () => { |
| 459 | + for await (const payload of first) { |
| 460 | + void payload; |
| 461 | + // Model a layered hook consumer such as an async inbox merger: |
| 462 | + // the delivery order must survive its continuation depth. |
| 463 | + for (let i = 0; i < 16; i++) { |
| 464 | + await Promise.resolve(); |
| 465 | + } |
| 466 | + await afterFirst(); |
| 467 | + break; |
| 468 | + } |
| 469 | + })(), |
| 470 | + (async () => { |
| 471 | + await second; |
| 472 | + await afterSecond(); |
| 473 | + })(), |
| 474 | + ]); |
| 475 | + }); |
| 476 | + |
| 477 | + expectSuspendedWithPendingSteps(ctx, error, ['afterFirst', 'afterSecond']); |
| 478 | + }); |
| 479 | +}); |
| 480 | + |
| 481 | +// ─── 4. abort deliveries ─────────────────────────────────────────────────── |
410 | 482 | // |
411 | 483 | // evnt_4 wait_completed ← makes the step result defer |
412 | 484 | // evnt_5 step_completed stepA ← deferred behind the wait |
@@ -491,7 +563,7 @@ describe('abort delivery ordering against an earlier step result', () => { |
491 | 563 | }); |
492 | 564 | }); |
493 | 565 |
|
494 | | -// ─── 4. idle reachability over the barrier registry ──────────────────────── |
| 566 | +// ─── 5. idle reachability over the barrier registry ──────────────────────── |
495 | 567 | // |
496 | 568 | // `hasParkedCommittedDelivery` decides whether an idle check may observe idle, |
497 | 569 | // and it is the only remaining caller of the recursive `resolvesOnItsOwn` |
@@ -609,7 +681,7 @@ describe('delivery-barrier idle reachability', () => { |
609 | 681 | }); |
610 | 682 | }); |
611 | 683 |
|
612 | | -// ─── 5. suspension timing: idle must wait out parked deliveries ──────────── |
| 684 | +// ─── 6. suspension timing: idle must wait out parked deliveries ──────────── |
613 | 685 | // |
614 | 686 | // Field shape from vercel/workflow#3183: a fire-and-forget `sleep()` (a |
615 | 687 | // watchdog — never awaited, never completing in-run) plus a parallel batch of |
|
0 commit comments