@indigoai-us/hq-cloud 6.18.44-alpha.9 → 6.18.44
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/bin/sync-runner-planning.d.ts +12 -0
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +18 -0
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +18 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +75 -6
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +22 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +33 -4
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +2 -1
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +6 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +10 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-windows-test-harness.d.ts +19 -0
- package/dist/cli/rescue-windows-test-harness.d.ts.map +1 -0
- package/dist/cli/rescue-windows-test-harness.js +443 -0
- package/dist/cli/rescue-windows-test-harness.js.map +1 -0
- package/dist/company-resolver.d.ts +22 -0
- package/dist/company-resolver.d.ts.map +1 -1
- package/dist/company-resolver.js +1 -1
- package/dist/company-resolver.js.map +1 -1
- package/dist/ignore.d.ts +4 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +10 -12
- package/dist/ignore.js.map +1 -1
- package/dist/journal-snapshot-decoder.js +15 -2
- package/dist/journal-snapshot-decoder.js.map +1 -1
- package/dist/sync/durable-apply.d.ts +2 -0
- package/dist/sync/durable-apply.d.ts.map +1 -1
- package/dist/sync/durable-apply.js +9 -12
- package/dist/sync/durable-apply.js.map +1 -1
- package/dist/sync/fsync-compat.d.ts +13 -0
- package/dist/sync/fsync-compat.d.ts.map +1 -0
- package/dist/sync/fsync-compat.js +33 -0
- package/dist/sync/fsync-compat.js.map +1 -0
- package/dist/sync/multipart-uploader.d.ts.map +1 -1
- package/dist/sync/multipart-uploader.js +2 -1
- package/dist/sync/multipart-uploader.js.map +1 -1
- package/dist/sync/scope-inventory-client.d.ts.map +1 -1
- package/dist/sync/scope-inventory-client.js +7 -2
- package/dist/sync/scope-inventory-client.js.map +1 -1
- package/dist/telemetry.d.ts +31 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +177 -17
- package/dist/telemetry.js.map +1 -1
- package/dist/usage-company-fallback.d.ts +104 -0
- package/dist/usage-company-fallback.d.ts.map +1 -0
- package/dist/usage-company-fallback.js +599 -0
- package/dist/usage-company-fallback.js.map +1 -0
- package/dist/watch-owner-lease.d.ts +3 -0
- package/dist/watch-owner-lease.d.ts.map +1 -1
- package/dist/watch-owner-lease.js +14 -3
- package/dist/watch-owner-lease.js.map +1 -1
- package/package.json +2 -2
- package/dist/active-company.test.d.ts +0 -2
- package/dist/active-company.test.d.ts.map +0 -1
- package/dist/active-company.test.js +0 -149
- package/dist/active-company.test.js.map +0 -1
- package/dist/agent-codex-instructions.test.d.ts +0 -2
- package/dist/agent-codex-instructions.test.d.ts.map +0 -1
- package/dist/agent-codex-instructions.test.js +0 -271
- package/dist/agent-codex-instructions.test.js.map +0 -1
- package/dist/backup-prune.test.d.ts +0 -2
- package/dist/backup-prune.test.d.ts.map +0 -1
- package/dist/backup-prune.test.js +0 -154
- package/dist/backup-prune.test.js.map +0 -1
- package/dist/bandwidth.test.d.ts +0 -2
- package/dist/bandwidth.test.d.ts.map +0 -1
- package/dist/bandwidth.test.js +0 -342
- package/dist/bandwidth.test.js.map +0 -1
- package/dist/best-effort-error.test.d.ts +0 -2
- package/dist/best-effort-error.test.d.ts.map +0 -1
- package/dist/best-effort-error.test.js +0 -54
- package/dist/best-effort-error.test.js.map +0 -1
- package/dist/bin/area-ledger-migration.test.d.ts +0 -2
- package/dist/bin/area-ledger-migration.test.d.ts.map +0 -1
- package/dist/bin/area-ledger-migration.test.js +0 -410
- package/dist/bin/area-ledger-migration.test.js.map +0 -1
- package/dist/bin/backup-prune-runner.test.d.ts +0 -2
- package/dist/bin/backup-prune-runner.test.d.ts.map +0 -1
- package/dist/bin/backup-prune-runner.test.js +0 -22
- package/dist/bin/backup-prune-runner.test.js.map +0 -1
- package/dist/bin/journal-repair.test.d.ts +0 -2
- package/dist/bin/journal-repair.test.d.ts.map +0 -1
- package/dist/bin/journal-repair.test.js +0 -53
- package/dist/bin/journal-repair.test.js.map +0 -1
- package/dist/bin/layout-compatibility.test.d.ts +0 -2
- package/dist/bin/layout-compatibility.test.d.ts.map +0 -1
- package/dist/bin/layout-compatibility.test.js +0 -87
- package/dist/bin/layout-compatibility.test.js.map +0 -1
- package/dist/bin/outpost-heartbeat.test.d.ts +0 -7
- package/dist/bin/outpost-heartbeat.test.d.ts.map +0 -1
- package/dist/bin/outpost-heartbeat.test.js +0 -84
- package/dist/bin/outpost-heartbeat.test.js.map +0 -1
- package/dist/bin/outpost-session-host.test.d.ts +0 -7
- package/dist/bin/outpost-session-host.test.d.ts.map +0 -1
- package/dist/bin/outpost-session-host.test.js +0 -52
- package/dist/bin/outpost-session-host.test.js.map +0 -1
- package/dist/bin/sync-mutation.test.d.ts +0 -2
- package/dist/bin/sync-mutation.test.d.ts.map +0 -1
- package/dist/bin/sync-mutation.test.js +0 -64
- package/dist/bin/sync-mutation.test.js.map +0 -1
- package/dist/bin/sync-runner-args.test.d.ts +0 -2
- package/dist/bin/sync-runner-args.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-args.test.js +0 -82
- package/dist/bin/sync-runner-args.test.js.map +0 -1
- package/dist/bin/sync-runner-auth-wait.test.d.ts +0 -2
- package/dist/bin/sync-runner-auth-wait.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-auth-wait.test.js +0 -406
- package/dist/bin/sync-runner-auth-wait.test.js.map +0 -1
- package/dist/bin/sync-runner-backoff.regression.test.d.ts +0 -29
- package/dist/bin/sync-runner-backoff.regression.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-backoff.regression.test.js +0 -224
- package/dist/bin/sync-runner-backoff.regression.test.js.map +0 -1
- package/dist/bin/sync-runner-company.test.d.ts +0 -2
- package/dist/bin/sync-runner-company.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-company.test.js +0 -2439
- package/dist/bin/sync-runner-company.test.js.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.d.ts +0 -2
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js +0 -648
- package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot.bench.test.d.ts +0 -2
- package/dist/bin/sync-runner-delete-snapshot.bench.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-delete-snapshot.bench.test.js +0 -329
- package/dist/bin/sync-runner-delete-snapshot.bench.test.js.map +0 -1
- package/dist/bin/sync-runner-events.test.d.ts +0 -2
- package/dist/bin/sync-runner-events.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-events.test.js +0 -364
- package/dist/bin/sync-runner-events.test.js.map +0 -1
- package/dist/bin/sync-runner-heap-telemetry.test.d.ts +0 -2
- package/dist/bin/sync-runner-heap-telemetry.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-heap-telemetry.test.js +0 -1966
- package/dist/bin/sync-runner-heap-telemetry.test.js.map +0 -1
- package/dist/bin/sync-runner-manifest.test.d.ts +0 -12
- package/dist/bin/sync-runner-manifest.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-manifest.test.js +0 -921
- package/dist/bin/sync-runner-manifest.test.js.map +0 -1
- package/dist/bin/sync-runner-planning.test.d.ts +0 -2
- package/dist/bin/sync-runner-planning.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-planning.test.js +0 -448
- package/dist/bin/sync-runner-planning.test.js.map +0 -1
- package/dist/bin/sync-runner-post-sync-flags.test.d.ts +0 -2
- package/dist/bin/sync-runner-post-sync-flags.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-post-sync-flags.test.js +0 -64
- package/dist/bin/sync-runner-post-sync-flags.test.js.map +0 -1
- package/dist/bin/sync-runner-publish-expansion.test.d.ts +0 -2
- package/dist/bin/sync-runner-publish-expansion.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-publish-expansion.test.js +0 -54
- package/dist/bin/sync-runner-publish-expansion.test.js.map +0 -1
- package/dist/bin/sync-runner-receiver-unresolved-company.test.d.ts +0 -2
- package/dist/bin/sync-runner-receiver-unresolved-company.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-receiver-unresolved-company.test.js +0 -379
- package/dist/bin/sync-runner-receiver-unresolved-company.test.js.map +0 -1
- package/dist/bin/sync-runner-rollup.test.d.ts +0 -2
- package/dist/bin/sync-runner-rollup.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-rollup.test.js +0 -24
- package/dist/bin/sync-runner-rollup.test.js.map +0 -1
- package/dist/bin/sync-runner-watch-loop.test.d.ts +0 -2
- package/dist/bin/sync-runner-watch-loop.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-watch-loop.test.js +0 -734
- package/dist/bin/sync-runner-watch-loop.test.js.map +0 -1
- package/dist/bin/sync-runner-watch-routes.test.d.ts +0 -2
- package/dist/bin/sync-runner-watch-routes.test.d.ts.map +0 -1
- package/dist/bin/sync-runner-watch-routes.test.js +0 -181
- package/dist/bin/sync-runner-watch-routes.test.js.map +0 -1
- package/dist/bin/sync-runner.test.d.ts +0 -10
- package/dist/bin/sync-runner.test.d.ts.map +0 -1
- package/dist/bin/sync-runner.test.js +0 -18984
- package/dist/bin/sync-runner.test.js.map +0 -1
- package/dist/cli/case-alias.test.d.ts +0 -2
- package/dist/cli/case-alias.test.d.ts.map +0 -1
- package/dist/cli/case-alias.test.js +0 -96
- package/dist/cli/case-alias.test.js.map +0 -1
- package/dist/cli/conflict-recovery.test.d.ts +0 -2
- package/dist/cli/conflict-recovery.test.d.ts.map +0 -1
- package/dist/cli/conflict-recovery.test.js +0 -201
- package/dist/cli/conflict-recovery.test.js.map +0 -1
- package/dist/cli/conflict-winner.test.d.ts +0 -7
- package/dist/cli/conflict-winner.test.d.ts.map +0 -1
- package/dist/cli/conflict-winner.test.js +0 -98
- package/dist/cli/conflict-winner.test.js.map +0 -1
- package/dist/cli/doctor.test.d.ts +0 -13
- package/dist/cli/doctor.test.d.ts.map +0 -1
- package/dist/cli/doctor.test.js +0 -607
- package/dist/cli/doctor.test.js.map +0 -1
- package/dist/cli/invite.test.d.ts +0 -5
- package/dist/cli/invite.test.d.ts.map +0 -1
- package/dist/cli/invite.test.js +0 -178
- package/dist/cli/invite.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.js +0 -300
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner.test.js +0 -266
- package/dist/cli/onepass-pull-planner.test.js.map +0 -1
- package/dist/cli/push-scan-exclusions.test.d.ts +0 -2
- package/dist/cli/push-scan-exclusions.test.d.ts.map +0 -1
- package/dist/cli/push-scan-exclusions.test.js +0 -99
- package/dist/cli/push-scan-exclusions.test.js.map +0 -1
- package/dist/cli/reindex-knowledge.test.d.ts +0 -12
- package/dist/cli/reindex-knowledge.test.d.ts.map +0 -1
- package/dist/cli/reindex-knowledge.test.js +0 -260
- package/dist/cli/reindex-knowledge.test.js.map +0 -1
- package/dist/cli/reindex.test.d.ts +0 -11
- package/dist/cli/reindex.test.d.ts.map +0 -1
- package/dist/cli/reindex.test.js +0 -811
- package/dist/cli/reindex.test.js.map +0 -1
- package/dist/cli/rescue-breaker.test.d.ts +0 -2
- package/dist/cli/rescue-breaker.test.d.ts.map +0 -1
- package/dist/cli/rescue-breaker.test.js +0 -812
- package/dist/cli/rescue-breaker.test.js.map +0 -1
- package/dist/cli/rescue-classify-ordering.test.d.ts +0 -2
- package/dist/cli/rescue-classify-ordering.test.d.ts.map +0 -1
- package/dist/cli/rescue-classify-ordering.test.js +0 -500
- package/dist/cli/rescue-classify-ordering.test.js.map +0 -1
- package/dist/cli/rescue-clone-diagnostics.test.d.ts +0 -2
- package/dist/cli/rescue-clone-diagnostics.test.d.ts.map +0 -1
- package/dist/cli/rescue-clone-diagnostics.test.js +0 -101
- package/dist/cli/rescue-clone-diagnostics.test.js.map +0 -1
- package/dist/cli/rescue-clone-network-retry.test.d.ts +0 -2
- package/dist/cli/rescue-clone-network-retry.test.d.ts.map +0 -1
- package/dist/cli/rescue-clone-network-retry.test.js +0 -415
- package/dist/cli/rescue-clone-network-retry.test.js.map +0 -1
- package/dist/cli/rescue-core-stamp.test.d.ts +0 -2
- package/dist/cli/rescue-core-stamp.test.d.ts.map +0 -1
- package/dist/cli/rescue-core-stamp.test.js +0 -364
- package/dist/cli/rescue-core-stamp.test.js.map +0 -1
- package/dist/cli/rescue-drift-reconcile.test.d.ts +0 -2
- package/dist/cli/rescue-drift-reconcile.test.d.ts.map +0 -1
- package/dist/cli/rescue-drift-reconcile.test.js +0 -162
- package/dist/cli/rescue-drift-reconcile.test.js.map +0 -1
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts +0 -2
- package/dist/cli/rescue-drop-dir-symlink.test.d.ts.map +0 -1
- package/dist/cli/rescue-drop-dir-symlink.test.js +0 -255
- package/dist/cli/rescue-drop-dir-symlink.test.js.map +0 -1
- package/dist/cli/rescue-exec-bit-preserve.test.d.ts +0 -2
- package/dist/cli/rescue-exec-bit-preserve.test.d.ts.map +0 -1
- package/dist/cli/rescue-exec-bit-preserve.test.js +0 -177
- package/dist/cli/rescue-exec-bit-preserve.test.js.map +0 -1
- package/dist/cli/rescue-hq-root-guard.test.d.ts +0 -2
- package/dist/cli/rescue-hq-root-guard.test.d.ts.map +0 -1
- package/dist/cli/rescue-hq-root-guard.test.js +0 -210
- package/dist/cli/rescue-hq-root-guard.test.js.map +0 -1
- package/dist/cli/rescue-journal-reconcile.test.d.ts +0 -2
- package/dist/cli/rescue-journal-reconcile.test.d.ts.map +0 -1
- package/dist/cli/rescue-journal-reconcile.test.js +0 -186
- package/dist/cli/rescue-journal-reconcile.test.js.map +0 -1
- package/dist/cli/rescue-keep-local-ignore.test.d.ts +0 -2
- package/dist/cli/rescue-keep-local-ignore.test.d.ts.map +0 -1
- package/dist/cli/rescue-keep-local-ignore.test.js +0 -315
- package/dist/cli/rescue-keep-local-ignore.test.js.map +0 -1
- package/dist/cli/rescue-move.test.d.ts +0 -2
- package/dist/cli/rescue-move.test.d.ts.map +0 -1
- package/dist/cli/rescue-move.test.js +0 -166
- package/dist/cli/rescue-move.test.js.map +0 -1
- package/dist/cli/rescue-mtime-preserve.test.d.ts +0 -2
- package/dist/cli/rescue-mtime-preserve.test.d.ts.map +0 -1
- package/dist/cli/rescue-mtime-preserve.test.js +0 -186
- package/dist/cli/rescue-mtime-preserve.test.js.map +0 -1
- package/dist/cli/rescue-preserve-copy-symlink.test.d.ts +0 -2
- package/dist/cli/rescue-preserve-copy-symlink.test.d.ts.map +0 -1
- package/dist/cli/rescue-preserve-copy-symlink.test.js +0 -483
- package/dist/cli/rescue-preserve-copy-symlink.test.js.map +0 -1
- package/dist/cli/rescue-realpath.test.d.ts +0 -2
- package/dist/cli/rescue-realpath.test.d.ts.map +0 -1
- package/dist/cli/rescue-realpath.test.js +0 -109
- package/dist/cli/rescue-realpath.test.js.map +0 -1
- package/dist/cli/rescue-restore-flag.test.d.ts +0 -2
- package/dist/cli/rescue-restore-flag.test.d.ts.map +0 -1
- package/dist/cli/rescue-restore-flag.test.js +0 -82
- package/dist/cli/rescue-restore-flag.test.js.map +0 -1
- package/dist/cli/rescue-rsync-preflight.test.d.ts +0 -2
- package/dist/cli/rescue-rsync-preflight.test.d.ts.map +0 -1
- package/dist/cli/rescue-rsync-preflight.test.js +0 -579
- package/dist/cli/rescue-rsync-preflight.test.js.map +0 -1
- package/dist/cli/rescue-rsync-status.test.d.ts +0 -2
- package/dist/cli/rescue-rsync-status.test.d.ts.map +0 -1
- package/dist/cli/rescue-rsync-status.test.js +0 -406
- package/dist/cli/rescue-rsync-status.test.js.map +0 -1
- package/dist/cli/rescue-settings-reconcile.test.d.ts +0 -2
- package/dist/cli/rescue-settings-reconcile.test.d.ts.map +0 -1
- package/dist/cli/rescue-settings-reconcile.test.js +0 -610
- package/dist/cli/rescue-settings-reconcile.test.js.map +0 -1
- package/dist/cli/rescue-snapshot-copy.test.d.ts +0 -2
- package/dist/cli/rescue-snapshot-copy.test.d.ts.map +0 -1
- package/dist/cli/rescue-snapshot-copy.test.js +0 -790
- package/dist/cli/rescue-snapshot-copy.test.js.map +0 -1
- package/dist/cli/rescue-snapshot-recovery.test.d.ts +0 -2
- package/dist/cli/rescue-snapshot-recovery.test.d.ts.map +0 -1
- package/dist/cli/rescue-snapshot-recovery.test.js +0 -265
- package/dist/cli/rescue-snapshot-recovery.test.js.map +0 -1
- package/dist/cli/rescue-snapshot.test.d.ts +0 -2
- package/dist/cli/rescue-snapshot.test.d.ts.map +0 -1
- package/dist/cli/rescue-snapshot.test.js +0 -46
- package/dist/cli/rescue-snapshot.test.js.map +0 -1
- package/dist/cli/rescue-source-switch-floor.test.d.ts +0 -2
- package/dist/cli/rescue-source-switch-floor.test.d.ts.map +0 -1
- package/dist/cli/rescue-source-switch-floor.test.js +0 -247
- package/dist/cli/rescue-source-switch-floor.test.js.map +0 -1
- package/dist/cli/rescue-unchanged-delete.test.d.ts +0 -2
- package/dist/cli/rescue-unchanged-delete.test.d.ts.map +0 -1
- package/dist/cli/rescue-unchanged-delete.test.js +0 -305
- package/dist/cli/rescue-unchanged-delete.test.js.map +0 -1
- package/dist/cli/rescue.reindex.test.d.ts +0 -2
- package/dist/cli/rescue.reindex.test.d.ts.map +0 -1
- package/dist/cli/rescue.reindex.test.js +0 -72
- package/dist/cli/rescue.reindex.test.js.map +0 -1
- package/dist/cli/rescue.test.d.ts +0 -2
- package/dist/cli/rescue.test.d.ts.map +0 -1
- package/dist/cli/rescue.test.js +0 -127
- package/dist/cli/rescue.test.js.map +0 -1
- package/dist/cli/share-plan-limit-presign.test.d.ts +0 -2
- package/dist/cli/share-plan-limit-presign.test.d.ts.map +0 -1
- package/dist/cli/share-plan-limit-presign.test.js +0 -271
- package/dist/cli/share-plan-limit-presign.test.js.map +0 -1
- package/dist/cli/share-presign-transport.test.d.ts +0 -22
- package/dist/cli/share-presign-transport.test.d.ts.map +0 -1
- package/dist/cli/share-presign-transport.test.js +0 -388
- package/dist/cli/share-presign-transport.test.js.map +0 -1
- package/dist/cli/share.test.d.ts +0 -5
- package/dist/cli/share.test.d.ts.map +0 -1
- package/dist/cli/share.test.js +0 -12335
- package/dist/cli/share.test.js.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +0 -15
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.js +0 -273
- package/dist/cli/streaming-pull-planner.bench.test.js.map +0 -1
- package/dist/cli/sync-scope.test.d.ts +0 -22
- package/dist/cli/sync-scope.test.d.ts.map +0 -1
- package/dist/cli/sync-scope.test.js +0 -903
- package/dist/cli/sync-scope.test.js.map +0 -1
- package/dist/cli/sync.test.d.ts +0 -5
- package/dist/cli/sync.test.d.ts.map +0 -1
- package/dist/cli/sync.test.js +0 -9028
- package/dist/cli/sync.test.js.map +0 -1
- package/dist/cli/tombstones.test.d.ts +0 -2
- package/dist/cli/tombstones.test.d.ts.map +0 -1
- package/dist/cli/tombstones.test.js +0 -120
- package/dist/cli/tombstones.test.js.map +0 -1
- package/dist/cli/watch-event-push-conflict.test.d.ts +0 -25
- package/dist/cli/watch-event-push-conflict.test.d.ts.map +0 -1
- package/dist/cli/watch-event-push-conflict.test.js +0 -230
- package/dist/cli/watch-event-push-conflict.test.js.map +0 -1
- package/dist/client-info.test.d.ts +0 -11
- package/dist/client-info.test.d.ts.map +0 -1
- package/dist/client-info.test.js +0 -168
- package/dist/client-info.test.js.map +0 -1
- package/dist/cognito-auth-lock-no-hard-links.test.d.ts +0 -11
- package/dist/cognito-auth-lock-no-hard-links.test.d.ts.map +0 -1
- package/dist/cognito-auth-lock-no-hard-links.test.js +0 -76
- package/dist/cognito-auth-lock-no-hard-links.test.js.map +0 -1
- package/dist/cognito-auth.test.d.ts +0 -9
- package/dist/cognito-auth.test.d.ts.map +0 -1
- package/dist/cognito-auth.test.js +0 -745
- package/dist/cognito-auth.test.js.map +0 -1
- package/dist/company-resolver.test.d.ts +0 -7
- package/dist/company-resolver.test.d.ts.map +0 -1
- package/dist/company-resolver.test.js +0 -533
- package/dist/company-resolver.test.js.map +0 -1
- package/dist/company-slug.test.d.ts +0 -7
- package/dist/company-slug.test.d.ts.map +0 -1
- package/dist/company-slug.test.js +0 -43
- package/dist/company-slug.test.js.map +0 -1
- package/dist/company-vault-transport.test.d.ts +0 -12
- package/dist/company-vault-transport.test.d.ts.map +0 -1
- package/dist/company-vault-transport.test.js +0 -97
- package/dist/company-vault-transport.test.js.map +0 -1
- package/dist/context.test.d.ts +0 -7
- package/dist/context.test.d.ts.map +0 -1
- package/dist/context.test.js +0 -646
- package/dist/context.test.js.map +0 -1
- package/dist/entity-resolver.test.d.ts +0 -10
- package/dist/entity-resolver.test.d.ts.map +0 -1
- package/dist/entity-resolver.test.js +0 -244
- package/dist/entity-resolver.test.js.map +0 -1
- package/dist/getindigo-dead-refs-contract.test.d.ts +0 -2
- package/dist/getindigo-dead-refs-contract.test.d.ts.map +0 -1
- package/dist/getindigo-dead-refs-contract.test.js +0 -58
- package/dist/getindigo-dead-refs-contract.test.js.map +0 -1
- package/dist/hermetic-test-env.test.d.ts +0 -2
- package/dist/hermetic-test-env.test.d.ts.map +0 -1
- package/dist/hermetic-test-env.test.js +0 -61
- package/dist/hermetic-test-env.test.js.map +0 -1
- package/dist/hq-root-recovery.test.d.ts +0 -2
- package/dist/hq-root-recovery.test.d.ts.map +0 -1
- package/dist/hq-root-recovery.test.js +0 -65
- package/dist/hq-root-recovery.test.js.map +0 -1
- package/dist/ignore.test.d.ts +0 -10
- package/dist/ignore.test.d.ts.map +0 -1
- package/dist/ignore.test.js +0 -926
- package/dist/ignore.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -26
- package/dist/index.test.js.map +0 -1
- package/dist/journal-area-ledger-read-boundary.test.d.ts +0 -2
- package/dist/journal-area-ledger-read-boundary.test.d.ts.map +0 -1
- package/dist/journal-area-ledger-read-boundary.test.js +0 -420
- package/dist/journal-area-ledger-read-boundary.test.js.map +0 -1
- package/dist/journal-baseline.test.d.ts +0 -2
- package/dist/journal-baseline.test.d.ts.map +0 -1
- package/dist/journal-baseline.test.js +0 -218
- package/dist/journal-baseline.test.js.map +0 -1
- package/dist/journal-decode-window.test.d.ts +0 -2
- package/dist/journal-decode-window.test.d.ts.map +0 -1
- package/dist/journal-decode-window.test.js +0 -99
- package/dist/journal-decode-window.test.js.map +0 -1
- package/dist/journal-delta-writes.test.d.ts +0 -2
- package/dist/journal-delta-writes.test.d.ts.map +0 -1
- package/dist/journal-delta-writes.test.js +0 -159
- package/dist/journal-delta-writes.test.js.map +0 -1
- package/dist/journal-ledger-index.test.d.ts +0 -15
- package/dist/journal-ledger-index.test.d.ts.map +0 -1
- package/dist/journal-ledger-index.test.js +0 -269
- package/dist/journal-ledger-index.test.js.map +0 -1
- package/dist/journal-public-boundary.test.d.ts +0 -2
- package/dist/journal-public-boundary.test.d.ts.map +0 -1
- package/dist/journal-public-boundary.test.js +0 -216
- package/dist/journal-public-boundary.test.js.map +0 -1
- package/dist/journal-row-store.test.d.ts +0 -2
- package/dist/journal-row-store.test.d.ts.map +0 -1
- package/dist/journal-row-store.test.js +0 -729
- package/dist/journal-row-store.test.js.map +0 -1
- package/dist/journal-snapshot-memory-helpers.d.ts +0 -33
- package/dist/journal-snapshot-memory-helpers.d.ts.map +0 -1
- package/dist/journal-snapshot-memory-helpers.js +0 -212
- package/dist/journal-snapshot-memory-helpers.js.map +0 -1
- package/dist/journal-snapshot-memory.test.d.ts +0 -2
- package/dist/journal-snapshot-memory.test.d.ts.map +0 -1
- package/dist/journal-snapshot-memory.test.js +0 -47
- package/dist/journal-snapshot-memory.test.js.map +0 -1
- package/dist/journal-snapshot-v4-memory.test.d.ts +0 -2
- package/dist/journal-snapshot-v4-memory.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4-memory.test.js +0 -24
- package/dist/journal-snapshot-v4-memory.test.js.map +0 -1
- package/dist/journal-snapshot-v4-writer-memory.test.d.ts +0 -2
- package/dist/journal-snapshot-v4-writer-memory.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4-writer-memory.test.js +0 -76
- package/dist/journal-snapshot-v4-writer-memory.test.js.map +0 -1
- package/dist/journal-snapshot-v4-writer.test.d.ts +0 -2
- package/dist/journal-snapshot-v4-writer.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4-writer.test.js +0 -282
- package/dist/journal-snapshot-v4-writer.test.js.map +0 -1
- package/dist/journal-snapshot-v4.test.d.ts +0 -2
- package/dist/journal-snapshot-v4.test.d.ts.map +0 -1
- package/dist/journal-snapshot-v4.test.js +0 -281
- package/dist/journal-snapshot-v4.test.js.map +0 -1
- package/dist/journal-store.test.d.ts +0 -2
- package/dist/journal-store.test.d.ts.map +0 -1
- package/dist/journal-store.test.js +0 -1227
- package/dist/journal-store.test.js.map +0 -1
- package/dist/journal-streaming.heap.test.d.ts +0 -2
- package/dist/journal-streaming.heap.test.d.ts.map +0 -1
- package/dist/journal-streaming.heap.test.js +0 -190
- package/dist/journal-streaming.heap.test.js.map +0 -1
- package/dist/journal.test.d.ts +0 -9
- package/dist/journal.test.d.ts.map +0 -1
- package/dist/journal.test.js +0 -1074
- package/dist/journal.test.js.map +0 -1
- package/dist/kms10-etag-safe-pull.test.d.ts +0 -2
- package/dist/kms10-etag-safe-pull.test.d.ts.map +0 -1
- package/dist/kms10-etag-safe-pull.test.js +0 -53
- package/dist/kms10-etag-safe-pull.test.js.map +0 -1
- package/dist/lib/cloud-authoritative.test.d.ts +0 -2
- package/dist/lib/cloud-authoritative.test.d.ts.map +0 -1
- package/dist/lib/cloud-authoritative.test.js +0 -42
- package/dist/lib/cloud-authoritative.test.js.map +0 -1
- package/dist/lib/conflict-reconcile.test.d.ts +0 -7
- package/dist/lib/conflict-reconcile.test.d.ts.map +0 -1
- package/dist/lib/conflict-reconcile.test.js +0 -246
- package/dist/lib/conflict-reconcile.test.js.map +0 -1
- package/dist/lib/conflict.test.d.ts +0 -9
- package/dist/lib/conflict.test.d.ts.map +0 -1
- package/dist/lib/conflict.test.js +0 -340
- package/dist/lib/conflict.test.js.map +0 -1
- package/dist/lib/describe-error.test.d.ts +0 -2
- package/dist/lib/describe-error.test.d.ts.map +0 -1
- package/dist/lib/describe-error.test.js +0 -148
- package/dist/lib/describe-error.test.js.map +0 -1
- package/dist/lib/machine-id.test.d.ts +0 -8
- package/dist/lib/machine-id.test.d.ts.map +0 -1
- package/dist/lib/machine-id.test.js +0 -228
- package/dist/lib/machine-id.test.js.map +0 -1
- package/dist/lib/net-errors.test.d.ts +0 -2
- package/dist/lib/net-errors.test.d.ts.map +0 -1
- package/dist/lib/net-errors.test.js +0 -111
- package/dist/lib/net-errors.test.js.map +0 -1
- package/dist/lib/ontology-entity-meta.test.d.ts +0 -2
- package/dist/lib/ontology-entity-meta.test.d.ts.map +0 -1
- package/dist/lib/ontology-entity-meta.test.js +0 -79
- package/dist/lib/ontology-entity-meta.test.js.map +0 -1
- package/dist/lib/ontology-entity-push.test.d.ts +0 -2
- package/dist/lib/ontology-entity-push.test.d.ts.map +0 -1
- package/dist/lib/ontology-entity-push.test.js +0 -156
- package/dist/lib/ontology-entity-push.test.js.map +0 -1
- package/dist/lib/personal-connector-settings-sync.test.d.ts +0 -2
- package/dist/lib/personal-connector-settings-sync.test.d.ts.map +0 -1
- package/dist/lib/personal-connector-settings-sync.test.js +0 -34
- package/dist/lib/personal-connector-settings-sync.test.js.map +0 -1
- package/dist/lib/readlink-safe.test.d.ts +0 -2
- package/dist/lib/readlink-safe.test.d.ts.map +0 -1
- package/dist/lib/readlink-safe.test.js +0 -34
- package/dist/lib/readlink-safe.test.js.map +0 -1
- package/dist/lib/s3-content-checksum.test.d.ts +0 -2
- package/dist/lib/s3-content-checksum.test.d.ts.map +0 -1
- package/dist/lib/s3-content-checksum.test.js +0 -26
- package/dist/lib/s3-content-checksum.test.js.map +0 -1
- package/dist/lib/skill-register-landed-bytes.test.d.ts +0 -2
- package/dist/lib/skill-register-landed-bytes.test.d.ts.map +0 -1
- package/dist/lib/skill-register-landed-bytes.test.js +0 -83
- package/dist/lib/skill-register-landed-bytes.test.js.map +0 -1
- package/dist/local-path-codec.test.d.ts +0 -14
- package/dist/local-path-codec.test.d.ts.map +0 -1
- package/dist/local-path-codec.test.js +0 -122
- package/dist/local-path-codec.test.js.map +0 -1
- package/dist/machine-auth.test.d.ts +0 -14
- package/dist/machine-auth.test.d.ts.map +0 -1
- package/dist/machine-auth.test.js +0 -1492
- package/dist/machine-auth.test.js.map +0 -1
- package/dist/machine-token-cache-isolation.test.d.ts +0 -24
- package/dist/machine-token-cache-isolation.test.d.ts.map +0 -1
- package/dist/machine-token-cache-isolation.test.js +0 -284
- package/dist/machine-token-cache-isolation.test.js.map +0 -1
- package/dist/manifest/US-002.acceptance.test.d.ts +0 -11
- package/dist/manifest/US-002.acceptance.test.d.ts.map +0 -1
- package/dist/manifest/US-002.acceptance.test.js +0 -160
- package/dist/manifest/US-002.acceptance.test.js.map +0 -1
- package/dist/manifest/US-004.acceptance.test.d.ts +0 -12
- package/dist/manifest/US-004.acceptance.test.d.ts.map +0 -1
- package/dist/manifest/US-004.acceptance.test.js +0 -173
- package/dist/manifest/US-004.acceptance.test.js.map +0 -1
- package/dist/manifest/build-manifest-resident-parity.test.d.ts +0 -2
- package/dist/manifest/build-manifest-resident-parity.test.d.ts.map +0 -1
- package/dist/manifest/build-manifest-resident-parity.test.js +0 -69
- package/dist/manifest/build-manifest-resident-parity.test.js.map +0 -1
- package/dist/manifest/build-manifest.bench.test.d.ts +0 -22
- package/dist/manifest/build-manifest.bench.test.d.ts.map +0 -1
- package/dist/manifest/build-manifest.bench.test.js +0 -930
- package/dist/manifest/build-manifest.bench.test.js.map +0 -1
- package/dist/manifest/build-manifest.test.d.ts +0 -10
- package/dist/manifest/build-manifest.test.d.ts.map +0 -1
- package/dist/manifest/build-manifest.test.js +0 -1217
- package/dist/manifest/build-manifest.test.js.map +0 -1
- package/dist/manifest/contract.test.d.ts +0 -17
- package/dist/manifest/contract.test.d.ts.map +0 -1
- package/dist/manifest/contract.test.js +0 -346
- package/dist/manifest/contract.test.js.map +0 -1
- package/dist/manifest/import-graph.test.d.ts +0 -25
- package/dist/manifest/import-graph.test.d.ts.map +0 -1
- package/dist/manifest/import-graph.test.js +0 -120
- package/dist/manifest/import-graph.test.js.map +0 -1
- package/dist/manifest/ledger-shard-isolation.test.d.ts +0 -18
- package/dist/manifest/ledger-shard-isolation.test.d.ts.map +0 -1
- package/dist/manifest/ledger-shard-isolation.test.js +0 -153
- package/dist/manifest/ledger-shard-isolation.test.js.map +0 -1
- package/dist/manifest/snapshot-store.test.d.ts +0 -11
- package/dist/manifest/snapshot-store.test.d.ts.map +0 -1
- package/dist/manifest/snapshot-store.test.js +0 -206
- package/dist/manifest/snapshot-store.test.js.map +0 -1
- package/dist/manifest/upload-manifest.test.d.ts +0 -10
- package/dist/manifest/upload-manifest.test.d.ts.map +0 -1
- package/dist/manifest/upload-manifest.test.js +0 -1335
- package/dist/manifest/upload-manifest.test.js.map +0 -1
- package/dist/manifest-reconcile.test.d.ts +0 -2
- package/dist/manifest-reconcile.test.d.ts.map +0 -1
- package/dist/manifest-reconcile.test.js +0 -1029
- package/dist/manifest-reconcile.test.js.map +0 -1
- package/dist/object-io.test.d.ts +0 -11
- package/dist/object-io.test.d.ts.map +0 -1
- package/dist/object-io.test.js +0 -2376
- package/dist/object-io.test.js.map +0 -1
- package/dist/operation-lock.test.d.ts +0 -5
- package/dist/operation-lock.test.d.ts.map +0 -1
- package/dist/operation-lock.test.js +0 -796
- package/dist/operation-lock.test.js.map +0 -1
- package/dist/outcome-telemetry.test.d.ts +0 -9
- package/dist/outcome-telemetry.test.d.ts.map +0 -1
- package/dist/outcome-telemetry.test.js +0 -663
- package/dist/outcome-telemetry.test.js.map +0 -1
- package/dist/outposts/client.test.d.ts +0 -9
- package/dist/outposts/client.test.d.ts.map +0 -1
- package/dist/outposts/client.test.js +0 -415
- package/dist/outposts/client.test.js.map +0 -1
- package/dist/outposts/command.test.d.ts +0 -18
- package/dist/outposts/command.test.d.ts.map +0 -1
- package/dist/outposts/command.test.js +0 -1230
- package/dist/outposts/command.test.js.map +0 -1
- package/dist/outposts/replica-sync.test.d.ts +0 -2
- package/dist/outposts/replica-sync.test.d.ts.map +0 -1
- package/dist/outposts/replica-sync.test.js +0 -224
- package/dist/outposts/replica-sync.test.js.map +0 -1
- package/dist/outposts/self-deploy.test.d.ts +0 -2
- package/dist/outposts/self-deploy.test.d.ts.map +0 -1
- package/dist/outposts/self-deploy.test.js +0 -218
- package/dist/outposts/self-deploy.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-bounds.test.d.ts +0 -25
- package/dist/outposts/session-heartbeat-bounds.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-bounds.test.js +0 -156
- package/dist/outposts/session-heartbeat-bounds.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-guard.test.d.ts +0 -16
- package/dist/outposts/session-heartbeat-guard.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-guard.test.js +0 -88
- package/dist/outposts/session-heartbeat-guard.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-publisher.test.d.ts +0 -10
- package/dist/outposts/session-heartbeat-publisher.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-publisher.test.js +0 -153
- package/dist/outposts/session-heartbeat-publisher.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-retain-guard.test.d.ts +0 -18
- package/dist/outposts/session-heartbeat-retain-guard.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-retain-guard.test.js +0 -106
- package/dist/outposts/session-heartbeat-retain-guard.test.js.map +0 -1
- package/dist/outposts/session-heartbeat-scan.test.d.ts +0 -20
- package/dist/outposts/session-heartbeat-scan.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat-scan.test.js +0 -241
- package/dist/outposts/session-heartbeat-scan.test.js.map +0 -1
- package/dist/outposts/session-heartbeat.test.d.ts +0 -11
- package/dist/outposts/session-heartbeat.test.d.ts.map +0 -1
- package/dist/outposts/session-heartbeat.test.js +0 -370
- package/dist/outposts/session-heartbeat.test.js.map +0 -1
- package/dist/outposts/session-host-auth.test.d.ts +0 -16
- package/dist/outposts/session-host-auth.test.d.ts.map +0 -1
- package/dist/outposts/session-host-auth.test.js +0 -284
- package/dist/outposts/session-host-auth.test.js.map +0 -1
- package/dist/outposts/session-host-command.test.d.ts +0 -10
- package/dist/outposts/session-host-command.test.d.ts.map +0 -1
- package/dist/outposts/session-host-command.test.js +0 -180
- package/dist/outposts/session-host-command.test.js.map +0 -1
- package/dist/outposts/session-host-connection.test.d.ts +0 -18
- package/dist/outposts/session-host-connection.test.d.ts.map +0 -1
- package/dist/outposts/session-host-connection.test.js +0 -392
- package/dist/outposts/session-host-connection.test.js.map +0 -1
- package/dist/outposts/session-host-fakes.d.ts +0 -61
- package/dist/outposts/session-host-fakes.d.ts.map +0 -1
- package/dist/outposts/session-host-fakes.js +0 -115
- package/dist/outposts/session-host-fakes.js.map +0 -1
- package/dist/outposts/session-host-ndjson.test.d.ts +0 -11
- package/dist/outposts/session-host-ndjson.test.d.ts.map +0 -1
- package/dist/outposts/session-host-ndjson.test.js +0 -73
- package/dist/outposts/session-host-ndjson.test.js.map +0 -1
- package/dist/outposts/session-host-protocol.test.d.ts +0 -15
- package/dist/outposts/session-host-protocol.test.d.ts.map +0 -1
- package/dist/outposts/session-host-protocol.test.js +0 -0
- package/dist/outposts/session-host-protocol.test.js.map +0 -1
- package/dist/outposts/session-host-server.test.d.ts +0 -10
- package/dist/outposts/session-host-server.test.d.ts.map +0 -1
- package/dist/outposts/session-host-server.test.js +0 -118
- package/dist/outposts/session-host-server.test.js.map +0 -1
- package/dist/outposts/session-host-sessions.test.d.ts +0 -18
- package/dist/outposts/session-host-sessions.test.d.ts.map +0 -1
- package/dist/outposts/session-host-sessions.test.js +0 -439
- package/dist/outposts/session-host-sessions.test.js.map +0 -1
- package/dist/outposts/terminal.test.d.ts +0 -2
- package/dist/outposts/terminal.test.d.ts.map +0 -1
- package/dist/outposts/terminal.test.js +0 -146
- package/dist/outposts/terminal.test.js.map +0 -1
- package/dist/outposts/transport.test.d.ts +0 -10
- package/dist/outposts/transport.test.d.ts.map +0 -1
- package/dist/outposts/transport.test.js +0 -205
- package/dist/outposts/transport.test.js.map +0 -1
- package/dist/packed-journal-phase2.test.d.ts +0 -2
- package/dist/packed-journal-phase2.test.d.ts.map +0 -1
- package/dist/packed-journal-phase2.test.js +0 -273
- package/dist/packed-journal-phase2.test.js.map +0 -1
- package/dist/path-ancestors.test.d.ts +0 -2
- package/dist/path-ancestors.test.d.ts.map +0 -1
- package/dist/path-ancestors.test.js +0 -21
- package/dist/path-ancestors.test.js.map +0 -1
- package/dist/personal-vault-exclusions.test.d.ts +0 -22
- package/dist/personal-vault-exclusions.test.d.ts.map +0 -1
- package/dist/personal-vault-exclusions.test.js +0 -294
- package/dist/personal-vault-exclusions.test.js.map +0 -1
- package/dist/personal-vault.test.d.ts +0 -18
- package/dist/personal-vault.test.d.ts.map +0 -1
- package/dist/personal-vault.test.js +0 -1099
- package/dist/personal-vault.test.js.map +0 -1
- package/dist/plan-limit-pause.test.d.ts +0 -2
- package/dist/plan-limit-pause.test.d.ts.map +0 -1
- package/dist/plan-limit-pause.test.js +0 -150
- package/dist/plan-limit-pause.test.js.map +0 -1
- package/dist/plan-limit.fixtures.d.ts +0 -34
- package/dist/plan-limit.fixtures.d.ts.map +0 -1
- package/dist/plan-limit.fixtures.js +0 -96
- package/dist/plan-limit.fixtures.js.map +0 -1
- package/dist/plan-limit.test.d.ts +0 -2
- package/dist/plan-limit.test.d.ts.map +0 -1
- package/dist/plan-limit.test.js +0 -132
- package/dist/plan-limit.test.js.map +0 -1
- package/dist/prefix-coalesce.test.d.ts +0 -2
- package/dist/prefix-coalesce.test.d.ts.map +0 -1
- package/dist/prefix-coalesce.test.js +0 -260
- package/dist/prefix-coalesce.test.js.map +0 -1
- package/dist/private-folder-transport.test.d.ts +0 -2
- package/dist/private-folder-transport.test.d.ts.map +0 -1
- package/dist/private-folder-transport.test.js +0 -33
- package/dist/private-folder-transport.test.js.map +0 -1
- package/dist/public-surface.test.d.ts +0 -15
- package/dist/public-surface.test.d.ts.map +0 -1
- package/dist/public-surface.test.js +0 -110
- package/dist/public-surface.test.js.map +0 -1
- package/dist/qmd-reindex.test.d.ts +0 -14
- package/dist/qmd-reindex.test.d.ts.map +0 -1
- package/dist/qmd-reindex.test.js +0 -899
- package/dist/qmd-reindex.test.js.map +0 -1
- package/dist/read-only-state-dir.test.d.ts +0 -27
- package/dist/read-only-state-dir.test.d.ts.map +0 -1
- package/dist/read-only-state-dir.test.js +0 -167
- package/dist/read-only-state-dir.test.js.map +0 -1
- package/dist/remote-pull.test.d.ts +0 -2
- package/dist/remote-pull.test.d.ts.map +0 -1
- package/dist/remote-pull.test.js +0 -1147
- package/dist/remote-pull.test.js.map +0 -1
- package/dist/s3-download-stream-hash.test.d.ts +0 -2
- package/dist/s3-download-stream-hash.test.d.ts.map +0 -1
- package/dist/s3-download-stream-hash.test.js +0 -70
- package/dist/s3-download-stream-hash.test.js.map +0 -1
- package/dist/s3.list-cost.test.d.ts +0 -2
- package/dist/s3.list-cost.test.d.ts.map +0 -1
- package/dist/s3.list-cost.test.js +0 -51
- package/dist/s3.list-cost.test.js.map +0 -1
- package/dist/s3.rename-retry.test.d.ts +0 -2
- package/dist/s3.rename-retry.test.d.ts.map +0 -1
- package/dist/s3.rename-retry.test.js +0 -217
- package/dist/s3.rename-retry.test.js.map +0 -1
- package/dist/s3.symlink-materialize.test.d.ts +0 -18
- package/dist/s3.symlink-materialize.test.d.ts.map +0 -1
- package/dist/s3.symlink-materialize.test.js +0 -579
- package/dist/s3.symlink-materialize.test.js.map +0 -1
- package/dist/s3.test.d.ts +0 -9
- package/dist/s3.test.d.ts.map +0 -1
- package/dist/s3.test.js +0 -2047
- package/dist/s3.test.js.map +0 -1
- package/dist/s3.windows-portability.test.d.ts +0 -12
- package/dist/s3.windows-portability.test.d.ts.map +0 -1
- package/dist/s3.windows-portability.test.js +0 -223
- package/dist/s3.windows-portability.test.js.map +0 -1
- package/dist/schemas/signal-types.test.d.ts +0 -2
- package/dist/schemas/signal-types.test.d.ts.map +0 -1
- package/dist/schemas/signal-types.test.js +0 -65
- package/dist/schemas/signal-types.test.js.map +0 -1
- package/dist/schemas/source-channels.test.d.ts +0 -2
- package/dist/schemas/source-channels.test.d.ts.map +0 -1
- package/dist/schemas/source-channels.test.js +0 -65
- package/dist/schemas/source-channels.test.js.map +0 -1
- package/dist/scope-shrink.test.d.ts +0 -13
- package/dist/scope-shrink.test.d.ts.map +0 -1
- package/dist/scope-shrink.test.js +0 -558
- package/dist/scope-shrink.test.js.map +0 -1
- package/dist/session-log-capture.test.d.ts +0 -2
- package/dist/session-log-capture.test.d.ts.map +0 -1
- package/dist/session-log-capture.test.js +0 -533
- package/dist/session-log-capture.test.js.map +0 -1
- package/dist/session-log-retention.test.d.ts +0 -2
- package/dist/session-log-retention.test.d.ts.map +0 -1
- package/dist/session-log-retention.test.js +0 -383
- package/dist/session-log-retention.test.js.map +0 -1
- package/dist/signals/get.test.d.ts +0 -5
- package/dist/signals/get.test.d.ts.map +0 -1
- package/dist/signals/get.test.js +0 -254
- package/dist/signals/get.test.js.map +0 -1
- package/dist/signals/list.test.d.ts +0 -9
- package/dist/signals/list.test.d.ts.map +0 -1
- package/dist/signals/list.test.js +0 -330
- package/dist/signals/list.test.js.map +0 -1
- package/dist/skill-telemetry.test.d.ts +0 -2
- package/dist/skill-telemetry.test.d.ts.map +0 -1
- package/dist/skill-telemetry.test.js +0 -1484
- package/dist/skill-telemetry.test.js.map +0 -1
- package/dist/sources/get.test.d.ts +0 -5
- package/dist/sources/get.test.d.ts.map +0 -1
- package/dist/sources/get.test.js +0 -236
- package/dist/sources/get.test.js.map +0 -1
- package/dist/sources/list.test.d.ts +0 -8
- package/dist/sources/list.test.d.ts.map +0 -1
- package/dist/sources/list.test.js +0 -318
- package/dist/sources/list.test.js.map +0 -1
- package/dist/sts-credential-refresh.test.d.ts +0 -2
- package/dist/sts-credential-refresh.test.d.ts.map +0 -1
- package/dist/sts-credential-refresh.test.js +0 -181
- package/dist/sts-credential-refresh.test.js.map +0 -1
- package/dist/swallowed-error-sites.test.d.ts +0 -2
- package/dist/swallowed-error-sites.test.d.ts.map +0 -1
- package/dist/swallowed-error-sites.test.js +0 -199
- package/dist/swallowed-error-sites.test.js.map +0 -1
- package/dist/swallowed-error-w32.test.d.ts +0 -2
- package/dist/swallowed-error-w32.test.d.ts.map +0 -1
- package/dist/swallowed-error-w32.test.js +0 -171
- package/dist/swallowed-error-w32.test.js.map +0 -1
- package/dist/swallowed-error-w33.test.d.ts +0 -2
- package/dist/swallowed-error-w33.test.d.ts.map +0 -1
- package/dist/swallowed-error-w33.test.js +0 -125
- package/dist/swallowed-error-w33.test.js.map +0 -1
- package/dist/swallowed-error-w34.test.d.ts +0 -2
- package/dist/swallowed-error-w34.test.d.ts.map +0 -1
- package/dist/swallowed-error-w34.test.js +0 -110
- package/dist/swallowed-error-w34.test.js.map +0 -1
- package/dist/swallowed-error-w35.test.d.ts +0 -2
- package/dist/swallowed-error-w35.test.d.ts.map +0 -1
- package/dist/swallowed-error-w35.test.js +0 -223
- package/dist/swallowed-error-w35.test.js.map +0 -1
- package/dist/sync/area-ledger-auto-migration.test.d.ts +0 -2
- package/dist/sync/area-ledger-auto-migration.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-auto-migration.test.js +0 -291
- package/dist/sync/area-ledger-auto-migration.test.js.map +0 -1
- package/dist/sync/area-ledger-migration.test.d.ts +0 -2
- package/dist/sync/area-ledger-migration.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-migration.test.js +0 -554
- package/dist/sync/area-ledger-migration.test.js.map +0 -1
- package/dist/sync/area-ledger-recovery.bench.test.d.ts +0 -2
- package/dist/sync/area-ledger-recovery.bench.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-recovery.bench.test.js +0 -60
- package/dist/sync/area-ledger-recovery.bench.test.js.map +0 -1
- package/dist/sync/area-ledger-writer-lock.bench.test.d.ts +0 -2
- package/dist/sync/area-ledger-writer-lock.bench.test.d.ts.map +0 -1
- package/dist/sync/area-ledger-writer-lock.bench.test.js +0 -57
- package/dist/sync/area-ledger-writer-lock.bench.test.js.map +0 -1
- package/dist/sync/area-ledger.test.d.ts +0 -2
- package/dist/sync/area-ledger.test.d.ts.map +0 -1
- package/dist/sync/area-ledger.test.js +0 -1401
- package/dist/sync/area-ledger.test.js.map +0 -1
- package/dist/sync/area-resolver.test.d.ts +0 -2
- package/dist/sync/area-resolver.test.d.ts.map +0 -1
- package/dist/sync/area-resolver.test.js +0 -234
- package/dist/sync/area-resolver.test.js.map +0 -1
- package/dist/sync/base-version-resolver.test.d.ts +0 -2
- package/dist/sync/base-version-resolver.test.d.ts.map +0 -1
- package/dist/sync/base-version-resolver.test.js +0 -24
- package/dist/sync/base-version-resolver.test.js.map +0 -1
- package/dist/sync/candidate-uploader.test.d.ts +0 -2
- package/dist/sync/candidate-uploader.test.d.ts.map +0 -1
- package/dist/sync/candidate-uploader.test.js +0 -132
- package/dist/sync/candidate-uploader.test.js.map +0 -1
- package/dist/sync/child-process-sync-worker-env.test.d.ts +0 -2
- package/dist/sync/child-process-sync-worker-env.test.d.ts.map +0 -1
- package/dist/sync/child-process-sync-worker-env.test.js +0 -38
- package/dist/sync/child-process-sync-worker-env.test.js.map +0 -1
- package/dist/sync/child-process-sync-worker-errors.test.d.ts +0 -2
- package/dist/sync/child-process-sync-worker-errors.test.d.ts.map +0 -1
- package/dist/sync/child-process-sync-worker-errors.test.js +0 -121
- package/dist/sync/child-process-sync-worker-errors.test.js.map +0 -1
- package/dist/sync/child-process-sync-worker.test.d.ts +0 -2
- package/dist/sync/child-process-sync-worker.test.d.ts.map +0 -1
- package/dist/sync/child-process-sync-worker.test.js +0 -262
- package/dist/sync/child-process-sync-worker.test.js.map +0 -1
- package/dist/sync/content-hash.test.d.ts +0 -2
- package/dist/sync/content-hash.test.d.ts.map +0 -1
- package/dist/sync/content-hash.test.js +0 -16
- package/dist/sync/content-hash.test.js.map +0 -1
- package/dist/sync/delta-client.test.d.ts +0 -2
- package/dist/sync/delta-client.test.d.ts.map +0 -1
- package/dist/sync/delta-client.test.js +0 -97
- package/dist/sync/delta-client.test.js.map +0 -1
- package/dist/sync/durable-apply.test.d.ts +0 -2
- package/dist/sync/durable-apply.test.d.ts.map +0 -1
- package/dist/sync/durable-apply.test.js +0 -180
- package/dist/sync/durable-apply.test.js.map +0 -1
- package/dist/sync/event-sync.test.d.ts +0 -14
- package/dist/sync/event-sync.test.d.ts.map +0 -1
- package/dist/sync/event-sync.test.js +0 -1714
- package/dist/sync/event-sync.test.js.map +0 -1
- package/dist/sync/feature-flags.test.d.ts +0 -24
- package/dist/sync/feature-flags.test.d.ts.map +0 -1
- package/dist/sync/feature-flags.test.js +0 -594
- package/dist/sync/feature-flags.test.js.map +0 -1
- package/dist/sync/full-pass-cap-governor.bench.test.d.ts +0 -2
- package/dist/sync/full-pass-cap-governor.bench.test.d.ts.map +0 -1
- package/dist/sync/full-pass-cap-governor.bench.test.js +0 -179
- package/dist/sync/full-pass-cap-governor.bench.test.js.map +0 -1
- package/dist/sync/full-pass-caps.test.d.ts +0 -2
- package/dist/sync/full-pass-caps.test.d.ts.map +0 -1
- package/dist/sync/full-pass-caps.test.js +0 -550
- package/dist/sync/full-pass-caps.test.js.map +0 -1
- package/dist/sync/idle-full-reconcile.test.d.ts +0 -2
- package/dist/sync/idle-full-reconcile.test.d.ts.map +0 -1
- package/dist/sync/idle-full-reconcile.test.js +0 -62
- package/dist/sync/idle-full-reconcile.test.js.map +0 -1
- package/dist/sync/inbound-event-admission.test.d.ts +0 -2
- package/dist/sync/inbound-event-admission.test.d.ts.map +0 -1
- package/dist/sync/inbound-event-admission.test.js +0 -125
- package/dist/sync/inbound-event-admission.test.js.map +0 -1
- package/dist/sync/journal-ledger-stream.test.d.ts +0 -12
- package/dist/sync/journal-ledger-stream.test.d.ts.map +0 -1
- package/dist/sync/journal-ledger-stream.test.js +0 -134
- package/dist/sync/journal-ledger-stream.test.js.map +0 -1
- package/dist/sync/lease-client.test.d.ts +0 -2
- package/dist/sync/lease-client.test.d.ts.map +0 -1
- package/dist/sync/lease-client.test.js +0 -119
- package/dist/sync/lease-client.test.js.map +0 -1
- package/dist/sync/logger.test.d.ts +0 -19
- package/dist/sync/logger.test.d.ts.map +0 -1
- package/dist/sync/logger.test.js +0 -204
- package/dist/sync/logger.test.js.map +0 -1
- package/dist/sync/metrics.test.d.ts +0 -19
- package/dist/sync/metrics.test.d.ts.map +0 -1
- package/dist/sync/metrics.test.js +0 -357
- package/dist/sync/metrics.test.js.map +0 -1
- package/dist/sync/multipart-uploader.test.d.ts +0 -2
- package/dist/sync/multipart-uploader.test.d.ts.map +0 -1
- package/dist/sync/multipart-uploader.test.js +0 -135
- package/dist/sync/multipart-uploader.test.js.map +0 -1
- package/dist/sync/mutation-client.test.d.ts +0 -2
- package/dist/sync/mutation-client.test.d.ts.map +0 -1
- package/dist/sync/mutation-client.test.js +0 -100
- package/dist/sync/mutation-client.test.js.map +0 -1
- package/dist/sync/mutation-session.test.d.ts +0 -2
- package/dist/sync/mutation-session.test.d.ts.map +0 -1
- package/dist/sync/mutation-session.test.js +0 -190
- package/dist/sync/mutation-session.test.js.map +0 -1
- package/dist/sync/parallel-sync-coordinator.test.d.ts +0 -2
- package/dist/sync/parallel-sync-coordinator.test.d.ts.map +0 -1
- package/dist/sync/parallel-sync-coordinator.test.js +0 -564
- package/dist/sync/parallel-sync-coordinator.test.js.map +0 -1
- package/dist/sync/path-leases.test.d.ts +0 -2
- package/dist/sync/path-leases.test.d.ts.map +0 -1
- package/dist/sync/path-leases.test.js +0 -51
- package/dist/sync/path-leases.test.js.map +0 -1
- package/dist/sync/per-area-maintenance.test.d.ts +0 -2
- package/dist/sync/per-area-maintenance.test.d.ts.map +0 -1
- package/dist/sync/per-area-maintenance.test.js +0 -100
- package/dist/sync/per-area-maintenance.test.js.map +0 -1
- package/dist/sync/push-event.test.d.ts +0 -15
- package/dist/sync/push-event.test.d.ts.map +0 -1
- package/dist/sync/push-event.test.js +0 -282
- package/dist/sync/push-event.test.js.map +0 -1
- package/dist/sync/push-receiver.test.d.ts +0 -25
- package/dist/sync/push-receiver.test.d.ts.map +0 -1
- package/dist/sync/push-receiver.test.js +0 -2851
- package/dist/sync/push-receiver.test.js.map +0 -1
- package/dist/sync/push-transport.test.d.ts +0 -2
- package/dist/sync/push-transport.test.d.ts.map +0 -1
- package/dist/sync/push-transport.test.js +0 -70
- package/dist/sync/push-transport.test.js.map +0 -1
- package/dist/sync/push-walk-matchers.bench.test.d.ts +0 -2
- package/dist/sync/push-walk-matchers.bench.test.d.ts.map +0 -1
- package/dist/sync/push-walk-matchers.bench.test.js +0 -230
- package/dist/sync/push-walk-matchers.bench.test.js.map +0 -1
- package/dist/sync/realtime-network-integration.test.d.ts +0 -2
- package/dist/sync/realtime-network-integration.test.d.ts.map +0 -1
- package/dist/sync/realtime-network-integration.test.js +0 -83
- package/dist/sync/realtime-network-integration.test.js.map +0 -1
- package/dist/sync/realtime-rollout.test.d.ts +0 -2
- package/dist/sync/realtime-rollout.test.d.ts.map +0 -1
- package/dist/sync/realtime-rollout.test.js +0 -94
- package/dist/sync/realtime-rollout.test.js.map +0 -1
- package/dist/sync/realtime-telemetry.test.d.ts +0 -2
- package/dist/sync/realtime-telemetry.test.d.ts.map +0 -1
- package/dist/sync/realtime-telemetry.test.js +0 -300
- package/dist/sync/realtime-telemetry.test.js.map +0 -1
- package/dist/sync/receiver-origin-config.test.d.ts +0 -2
- package/dist/sync/receiver-origin-config.test.d.ts.map +0 -1
- package/dist/sync/receiver-origin-config.test.js +0 -39
- package/dist/sync/receiver-origin-config.test.js.map +0 -1
- package/dist/sync/reconcile-cursor.test.d.ts +0 -2
- package/dist/sync/reconcile-cursor.test.d.ts.map +0 -1
- package/dist/sync/reconcile-cursor.test.js +0 -139
- package/dist/sync/reconcile-cursor.test.js.map +0 -1
- package/dist/sync/remote-file-spool.test.d.ts +0 -2
- package/dist/sync/remote-file-spool.test.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.test.js +0 -77
- package/dist/sync/remote-file-spool.test.js.map +0 -1
- package/dist/sync/scope-inventory-client.test.d.ts +0 -2
- package/dist/sync/scope-inventory-client.test.d.ts.map +0 -1
- package/dist/sync/scope-inventory-client.test.js +0 -103
- package/dist/sync/scope-inventory-client.test.js.map +0 -1
- package/dist/sync/secret-path-policy.test.d.ts +0 -2
- package/dist/sync/secret-path-policy.test.d.ts.map +0 -1
- package/dist/sync/secret-path-policy.test.js +0 -183
- package/dist/sync/secret-path-policy.test.js.map +0 -1
- package/dist/sync/snapshot-client.test.d.ts +0 -2
- package/dist/sync/snapshot-client.test.d.ts.map +0 -1
- package/dist/sync/snapshot-client.test.js +0 -169
- package/dist/sync/snapshot-client.test.js.map +0 -1
- package/dist/sync/state-store.test.d.ts +0 -2
- package/dist/sync/state-store.test.d.ts.map +0 -1
- package/dist/sync/state-store.test.js +0 -1556
- package/dist/sync/state-store.test.js.map +0 -1
- package/dist/sync/sync-contract.test.d.ts +0 -2
- package/dist/sync/sync-contract.test.d.ts.map +0 -1
- package/dist/sync/sync-contract.test.js +0 -141
- package/dist/sync/sync-contract.test.js.map +0 -1
- package/dist/sync/targeted-pull-prefixes.test.d.ts +0 -2
- package/dist/sync/targeted-pull-prefixes.test.d.ts.map +0 -1
- package/dist/sync/targeted-pull-prefixes.test.js +0 -33
- package/dist/sync/targeted-pull-prefixes.test.js.map +0 -1
- package/dist/sync/targeted-upsert-exact-list.test.d.ts +0 -2
- package/dist/sync/targeted-upsert-exact-list.test.d.ts.map +0 -1
- package/dist/sync/targeted-upsert-exact-list.test.js +0 -247
- package/dist/sync/targeted-upsert-exact-list.test.js.map +0 -1
- package/dist/sync/unchanged-push-events.test.d.ts +0 -2
- package/dist/sync/unchanged-push-events.test.d.ts.map +0 -1
- package/dist/sync/unchanged-push-events.test.js +0 -76
- package/dist/sync/unchanged-push-events.test.js.map +0 -1
- package/dist/sync/uploader-finalization.test.d.ts +0 -2
- package/dist/sync/uploader-finalization.test.d.ts.map +0 -1
- package/dist/sync/uploader-finalization.test.js +0 -107
- package/dist/sync/uploader-finalization.test.js.map +0 -1
- package/dist/sync-core.test.d.ts +0 -2
- package/dist/sync-core.test.d.ts.map +0 -1
- package/dist/sync-core.test.js +0 -189
- package/dist/sync-core.test.js.map +0 -1
- package/dist/sync-progress.test.d.ts +0 -2
- package/dist/sync-progress.test.d.ts.map +0 -1
- package/dist/sync-progress.test.js +0 -277
- package/dist/sync-progress.test.js.map +0 -1
- package/dist/telemetry-events.test.d.ts +0 -2
- package/dist/telemetry-events.test.d.ts.map +0 -1
- package/dist/telemetry-events.test.js +0 -113
- package/dist/telemetry-events.test.js.map +0 -1
- package/dist/telemetry.test.d.ts +0 -11
- package/dist/telemetry.test.d.ts.map +0 -1
- package/dist/telemetry.test.js +0 -1603
- package/dist/telemetry.test.js.map +0 -1
- package/dist/vault-client.test.d.ts +0 -7
- package/dist/vault-client.test.d.ts.map +0 -1
- package/dist/vault-client.test.js +0 -1334
- package/dist/vault-client.test.js.map +0 -1
- package/dist/watch-coverage.test.d.ts +0 -2
- package/dist/watch-coverage.test.d.ts.map +0 -1
- package/dist/watch-coverage.test.js +0 -56
- package/dist/watch-coverage.test.js.map +0 -1
- package/dist/watch-owner-lease.test.d.ts +0 -2
- package/dist/watch-owner-lease.test.d.ts.map +0 -1
- package/dist/watch-owner-lease.test.js +0 -353
- package/dist/watch-owner-lease.test.js.map +0 -1
- package/dist/watch-parent.test.d.ts +0 -2
- package/dist/watch-parent.test.d.ts.map +0 -1
- package/dist/watch-parent.test.js +0 -131
- package/dist/watch-parent.test.js.map +0 -1
- package/dist/watch-roots.test.d.ts +0 -18
- package/dist/watch-roots.test.d.ts.map +0 -1
- package/dist/watch-roots.test.js +0 -395
- package/dist/watch-roots.test.js.map +0 -1
- package/dist/watcher-continuity-hot-path.test.d.ts +0 -2
- package/dist/watcher-continuity-hot-path.test.d.ts.map +0 -1
- package/dist/watcher-continuity-hot-path.test.js +0 -40
- package/dist/watcher-continuity-hot-path.test.js.map +0 -1
- package/dist/watcher-event-gate.test.d.ts +0 -16
- package/dist/watcher-event-gate.test.d.ts.map +0 -1
- package/dist/watcher-event-gate.test.js +0 -191
- package/dist/watcher-event-gate.test.js.map +0 -1
- package/dist/watcher-replan-sweep.test.d.ts +0 -2
- package/dist/watcher-replan-sweep.test.d.ts.map +0 -1
- package/dist/watcher-replan-sweep.test.js +0 -245
- package/dist/watcher-replan-sweep.test.js.map +0 -1
- package/dist/watcher-stdout.test.d.ts +0 -2
- package/dist/watcher-stdout.test.d.ts.map +0 -1
- package/dist/watcher-stdout.test.js +0 -86
- package/dist/watcher-stdout.test.js.map +0 -1
- package/dist/watcher.test.d.ts +0 -2
- package/dist/watcher.test.d.ts.map +0 -1
- package/dist/watcher.test.js +0 -4301
- package/dist/watcher.test.js.map +0 -1
- package/dist/watcher.warmup-cancel.test.d.ts +0 -2
- package/dist/watcher.warmup-cancel.test.d.ts.map +0 -1
- package/dist/watcher.warmup-cancel.test.js +0 -59
- package/dist/watcher.warmup-cancel.test.js.map +0 -1
- package/dist/watcher.warmup-responsive.test.d.ts +0 -2
- package/dist/watcher.warmup-responsive.test.d.ts.map +0 -1
- package/dist/watcher.warmup-responsive.test.js +0 -62
- package/dist/watcher.warmup-responsive.test.js.map +0 -1
|
@@ -1,2851 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* US-009 — push-receiver unit tests.
|
|
3
|
-
*
|
|
4
|
-
* Covers the five required behaviors from the PRD AC, against the SQS-backed
|
|
5
|
-
* {@link SqsPushReceiver} (mocked SQS client — NO real AWS) and the
|
|
6
|
-
* {@link InMemoryPushReceiver} (in-process fanout analogue):
|
|
7
|
-
*
|
|
8
|
-
* 1. subscribe lifecycle — start opens the poll loop / subscription, dispose
|
|
9
|
-
* stops it; double-start is a no-op; flag-OFF stays dormant.
|
|
10
|
-
* 2. dedupe — an event with sequenceNumber <= the highest seen
|
|
11
|
-
* for its path is skipped; per-path counters are independent.
|
|
12
|
-
* 3. reconnect-replay — a transient receiveMessage failure backs off and
|
|
13
|
-
* resumes; SQS retention redelivers the buffered events; dedupe absorbs
|
|
14
|
-
* redelivery (in-memory: disconnect buffers, reconnect drains).
|
|
15
|
-
* 4. flag gating — dormant when the per-tenant flag is OFF; no queue
|
|
16
|
-
* polled, connected stays false.
|
|
17
|
-
* 5. dispose drain — dispose aborts + awaits the in-flight syncFn, then
|
|
18
|
-
* disconnects; the loop terminates with no leaked timers.
|
|
19
|
-
*
|
|
20
|
-
* The live per-client SQS queue is NOT provisioned yet (server provisioning
|
|
21
|
-
* Lambda is an unbuilt follow-up — see references.md). Every test injects a
|
|
22
|
-
* fake SQS client / in-memory fanout.
|
|
23
|
-
*/
|
|
24
|
-
import * as fs from "node:fs";
|
|
25
|
-
import * as os from "node:os";
|
|
26
|
-
import * as path from "node:path";
|
|
27
|
-
import { describe, expect, it, vi } from "vitest";
|
|
28
|
-
import { encodePushEvent } from "./push-event.js";
|
|
29
|
-
import { hashFile } from "../journal.js";
|
|
30
|
-
import { InMemoryFanout, InMemoryPushReceiver, NoopPushReceiver, RealtimeWakeReceiver, SqsPushReceiver, createPushReceiver, decodeRealtimeWake, } from "./push-receiver.js";
|
|
31
|
-
import { isInboundEventPlaceable } from "./inbound-event-admission.js";
|
|
32
|
-
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
33
|
-
const TENANT = "tenant-indigo";
|
|
34
|
-
const QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/sync-push-indigo-deviceA";
|
|
35
|
-
function makeEvent(overrides = {}) {
|
|
36
|
-
return {
|
|
37
|
-
relativePath: "companies/indigo/notes.md",
|
|
38
|
-
kind: "upsert",
|
|
39
|
-
contentHash: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
40
|
-
mtime: "2026-05-21T12:34:56.000Z",
|
|
41
|
-
originDeviceId: "device-A",
|
|
42
|
-
originTenantId: TENANT,
|
|
43
|
-
sequenceNumber: 1,
|
|
44
|
-
eventTimestamp: "2026-05-21T12:34:56.500Z",
|
|
45
|
-
...overrides,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function msg(event, receiptHandle = `rh-${event.sequenceNumber}`) {
|
|
49
|
-
return {
|
|
50
|
-
Body: encodePushEvent(event),
|
|
51
|
-
ReceiptHandle: receiptHandle,
|
|
52
|
-
MessageId: `m-${event.relativePath}-${event.sequenceNumber}`,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
async function changeVisibilityBatchIndividually(changeVisibility, args) {
|
|
56
|
-
const failedEntries = [];
|
|
57
|
-
for (const entry of args.entries) {
|
|
58
|
-
try {
|
|
59
|
-
await changeVisibility({
|
|
60
|
-
queueUrl: args.queueUrl,
|
|
61
|
-
receiptHandle: entry.receiptHandle,
|
|
62
|
-
visibilityTimeoutSeconds: args.visibilityTimeoutSeconds,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
const failure = error;
|
|
67
|
-
failedEntries.push({
|
|
68
|
-
id: entry.id,
|
|
69
|
-
...(failure.code === undefined ? {} : { code: failure.code }),
|
|
70
|
-
message: failure.message ?? String(error),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return { failedEntries };
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* A scriptable fake SQS client. `enqueue` adds batches that successive
|
|
78
|
-
* `receiveMessage` calls drain (one batch per call); once drained, it blocks
|
|
79
|
-
* on the abort signal (modeling an idle long-poll) so dispose ends it cleanly.
|
|
80
|
-
* `failNext(n)` makes the next `n` receiveMessage calls reject (transient).
|
|
81
|
-
*/
|
|
82
|
-
class FakeSqs {
|
|
83
|
-
batches = [];
|
|
84
|
-
inFlight = new Map();
|
|
85
|
-
released = [];
|
|
86
|
-
failCount = 0;
|
|
87
|
-
deleted = [];
|
|
88
|
-
visibilityChanges = [];
|
|
89
|
-
visibilityBatchCalls = [];
|
|
90
|
-
visibilityFailure = null;
|
|
91
|
-
visibilityBatchFailures = new Map();
|
|
92
|
-
receiveCalls = 0;
|
|
93
|
-
receivedReceiptHandles = [];
|
|
94
|
-
receiveWaitTimes = [];
|
|
95
|
-
enqueue(messages) {
|
|
96
|
-
this.batches.push({ messages, drainable: false });
|
|
97
|
-
}
|
|
98
|
-
/** A page already available to a zero-wait drain receive. */
|
|
99
|
-
enqueueDrainable(messages) {
|
|
100
|
-
this.batches.push({ messages, drainable: true });
|
|
101
|
-
}
|
|
102
|
-
failNext(n) {
|
|
103
|
-
this.failCount += n;
|
|
104
|
-
}
|
|
105
|
-
failNextVisibilityChange(error = new Error("simulated visibility change failure")) {
|
|
106
|
-
this.visibilityFailure = error;
|
|
107
|
-
}
|
|
108
|
-
failVisibilityBatchEntry(receiptHandle, failure = {
|
|
109
|
-
code: "InvalidParameterValue",
|
|
110
|
-
message: "simulated per-entry visibility failure",
|
|
111
|
-
}) {
|
|
112
|
-
this.visibilityBatchFailures.set(receiptHandle, failure);
|
|
113
|
-
}
|
|
114
|
-
async receiveMessage(args) {
|
|
115
|
-
this.receiveCalls += 1;
|
|
116
|
-
this.receiveWaitTimes.push(args.waitTimeSeconds);
|
|
117
|
-
if (this.failCount > 0) {
|
|
118
|
-
this.failCount -= 1;
|
|
119
|
-
throw new Error("simulated SQS receive failure");
|
|
120
|
-
}
|
|
121
|
-
if (this.released.length > 0 && args.waitTimeSeconds > 1) {
|
|
122
|
-
const messages = this.released.splice(0);
|
|
123
|
-
for (const message of messages) {
|
|
124
|
-
if (message.ReceiptHandle) {
|
|
125
|
-
this.inFlight.set(message.ReceiptHandle, message);
|
|
126
|
-
this.receivedReceiptHandles.push(message.ReceiptHandle);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return { messages };
|
|
130
|
-
}
|
|
131
|
-
const next = this.batches[0];
|
|
132
|
-
// Ordinary enqueued batches model work that arrives after this drain's
|
|
133
|
-
// one-second confirmation window; only a regular long poll starts their
|
|
134
|
-
// next receive cycle. Dedicated drainable and false-negative fakes cover
|
|
135
|
-
// pages that must be collected by the current drain.
|
|
136
|
-
if (next && (next.drainable || args.waitTimeSeconds > 1)) {
|
|
137
|
-
const messages = next.messages.splice(0, args.maxMessages);
|
|
138
|
-
if (next.messages.length === 0)
|
|
139
|
-
this.batches.shift();
|
|
140
|
-
for (const message of messages) {
|
|
141
|
-
if (message.ReceiptHandle) {
|
|
142
|
-
this.inFlight.set(message.ReceiptHandle, message);
|
|
143
|
-
this.receivedReceiptHandles.push(message.ReceiptHandle);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return { messages };
|
|
147
|
-
}
|
|
148
|
-
if (args.waitTimeSeconds === 0)
|
|
149
|
-
return { messages: [] };
|
|
150
|
-
// Idle long-poll: resolve empty on the next tick (or promptly on abort).
|
|
151
|
-
return new Promise((resolve) => {
|
|
152
|
-
if (args.signal.aborted)
|
|
153
|
-
return resolve({ messages: [] });
|
|
154
|
-
const t = setTimeout(() => resolve({ messages: [] }), 5);
|
|
155
|
-
t.unref?.();
|
|
156
|
-
args.signal.addEventListener("abort", () => {
|
|
157
|
-
clearTimeout(t);
|
|
158
|
-
resolve({ messages: [] });
|
|
159
|
-
}, { once: true });
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
async deleteMessage(args) {
|
|
163
|
-
this.deleted.push(args.receiptHandle);
|
|
164
|
-
this.inFlight.delete(args.receiptHandle);
|
|
165
|
-
}
|
|
166
|
-
async changeMessageVisibility(args) {
|
|
167
|
-
this.visibilityChanges.push({
|
|
168
|
-
receiptHandle: args.receiptHandle,
|
|
169
|
-
visibilityTimeoutSeconds: args.visibilityTimeoutSeconds,
|
|
170
|
-
});
|
|
171
|
-
if (this.visibilityFailure) {
|
|
172
|
-
const error = this.visibilityFailure;
|
|
173
|
-
this.visibilityFailure = null;
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
if (args.visibilityTimeoutSeconds !== 0)
|
|
177
|
-
return;
|
|
178
|
-
const message = this.inFlight.get(args.receiptHandle);
|
|
179
|
-
if (!message)
|
|
180
|
-
throw new Error(`unknown receipt ${args.receiptHandle}`);
|
|
181
|
-
this.inFlight.delete(args.receiptHandle);
|
|
182
|
-
this.released.push(message);
|
|
183
|
-
}
|
|
184
|
-
async changeMessageVisibilityBatch(args) {
|
|
185
|
-
this.visibilityBatchCalls.push({
|
|
186
|
-
entries: args.entries.map(({ id, receiptHandle }) => ({ id, receiptHandle })),
|
|
187
|
-
visibilityTimeoutSeconds: args.visibilityTimeoutSeconds,
|
|
188
|
-
});
|
|
189
|
-
const failedEntries = [];
|
|
190
|
-
for (const entry of args.entries) {
|
|
191
|
-
const failure = this.visibilityBatchFailures.get(entry.receiptHandle);
|
|
192
|
-
if (failure) {
|
|
193
|
-
this.visibilityBatchFailures.delete(entry.receiptHandle);
|
|
194
|
-
failedEntries.push({ id: entry.id, ...failure });
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
await this.changeMessageVisibility({
|
|
198
|
-
queueUrl: args.queueUrl,
|
|
199
|
-
receiptHandle: entry.receiptHandle,
|
|
200
|
-
visibilityTimeoutSeconds: args.visibilityTimeoutSeconds,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
return { failedEntries };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/** FIFO fake used to prove that a slow pass cannot stall backlog intake. */
|
|
207
|
-
class BacklogSqs {
|
|
208
|
-
visible;
|
|
209
|
-
inFlight = new Map();
|
|
210
|
-
deleted = [];
|
|
211
|
-
released = [];
|
|
212
|
-
received = [];
|
|
213
|
-
constructor(messages) {
|
|
214
|
-
this.visible = [...messages];
|
|
215
|
-
}
|
|
216
|
-
get remaining() {
|
|
217
|
-
return this.visible.length + this.inFlight.size;
|
|
218
|
-
}
|
|
219
|
-
async receiveMessage(args) {
|
|
220
|
-
const messages = this.visible.splice(0, args.maxMessages);
|
|
221
|
-
if (messages.length > 0) {
|
|
222
|
-
for (const message of messages) {
|
|
223
|
-
if (message.ReceiptHandle) {
|
|
224
|
-
this.inFlight.set(message.ReceiptHandle, message);
|
|
225
|
-
this.received.push(message.ReceiptHandle);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return { messages };
|
|
229
|
-
}
|
|
230
|
-
if (args.waitTimeSeconds === 0)
|
|
231
|
-
return { messages: [] };
|
|
232
|
-
// Drain confirmations must observe an empty queue; the outer long-poll
|
|
233
|
-
// remains blocked until disposal so this fixture cannot busy-spin.
|
|
234
|
-
if (args.waitTimeSeconds <= 1)
|
|
235
|
-
return { messages: [] };
|
|
236
|
-
return new Promise((resolve) => {
|
|
237
|
-
if (args.signal.aborted)
|
|
238
|
-
return resolve({ messages: [] });
|
|
239
|
-
args.signal.addEventListener("abort", () => resolve({ messages: [] }), { once: true });
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
async deleteMessage(args) {
|
|
243
|
-
if (!this.inFlight.delete(args.receiptHandle)) {
|
|
244
|
-
throw new Error(`delete received an unknown receipt ${args.receiptHandle}`);
|
|
245
|
-
}
|
|
246
|
-
this.deleted.push(args.receiptHandle);
|
|
247
|
-
}
|
|
248
|
-
async changeMessageVisibility(args) {
|
|
249
|
-
if (args.visibilityTimeoutSeconds !== 0)
|
|
250
|
-
return;
|
|
251
|
-
const message = this.inFlight.get(args.receiptHandle);
|
|
252
|
-
if (!message)
|
|
253
|
-
throw new Error(`release received an unknown receipt ${args.receiptHandle}`);
|
|
254
|
-
this.inFlight.delete(args.receiptHandle);
|
|
255
|
-
this.released.push(args.receiptHandle);
|
|
256
|
-
this.visible.unshift(message);
|
|
257
|
-
}
|
|
258
|
-
async changeMessageVisibilityBatch(args) {
|
|
259
|
-
return changeVisibilityBatchIndividually((entry) => this.changeMessageVisibility(entry), args);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
/** Models SQS short-poll sampling: receive #2 is empty while work remains. */
|
|
263
|
-
class FalseNegativeSqs {
|
|
264
|
-
messages;
|
|
265
|
-
skippedSecondReceive = false;
|
|
266
|
-
deleted = [];
|
|
267
|
-
receiveWaitTimes = [];
|
|
268
|
-
constructor(messages) {
|
|
269
|
-
this.messages = [...messages];
|
|
270
|
-
}
|
|
271
|
-
async receiveMessage(args) {
|
|
272
|
-
this.receiveWaitTimes.push(args.waitTimeSeconds);
|
|
273
|
-
if (!this.skippedSecondReceive && this.receiveWaitTimes.length === 2) {
|
|
274
|
-
this.skippedSecondReceive = true;
|
|
275
|
-
return { messages: [] };
|
|
276
|
-
}
|
|
277
|
-
const page = this.messages.splice(0, args.maxMessages);
|
|
278
|
-
if (page.length > 0)
|
|
279
|
-
return { messages: page };
|
|
280
|
-
if (args.waitTimeSeconds === 0)
|
|
281
|
-
return { messages: [] };
|
|
282
|
-
return new Promise((resolve) => {
|
|
283
|
-
if (args.signal.aborted)
|
|
284
|
-
return resolve({ messages: [] });
|
|
285
|
-
const t = setTimeout(() => resolve({ messages: [] }), 5);
|
|
286
|
-
t.unref?.();
|
|
287
|
-
args.signal.addEventListener("abort", () => {
|
|
288
|
-
clearTimeout(t);
|
|
289
|
-
resolve({ messages: [] });
|
|
290
|
-
}, { once: true });
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
async deleteMessage(args) {
|
|
294
|
-
this.deleted.push(args.receiptHandle);
|
|
295
|
-
}
|
|
296
|
-
async changeMessageVisibility(_args) {
|
|
297
|
-
// This fixture never returns more paths than its configured cap.
|
|
298
|
-
}
|
|
299
|
-
async changeMessageVisibilityBatch(_args) {
|
|
300
|
-
return { failedEntries: [] };
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Scaled-time SQS fixture: an omitted receive visibility lease expires before
|
|
305
|
-
* the delayed pass can delete its receipt. A supplied 600-second lease does
|
|
306
|
-
* not. This makes the production redelivery failure deterministic in millis.
|
|
307
|
-
*/
|
|
308
|
-
class VisibilityExpirySqs {
|
|
309
|
-
event;
|
|
310
|
-
visibleAt = 0;
|
|
311
|
-
leaseExpiresAt = 0;
|
|
312
|
-
currentReceipt = "";
|
|
313
|
-
nextReceipt = 0;
|
|
314
|
-
deliveries = [];
|
|
315
|
-
deleted = [];
|
|
316
|
-
deleteAttempts = [];
|
|
317
|
-
requestedVisibilityTimeouts = [];
|
|
318
|
-
visibilityChanges = [];
|
|
319
|
-
deleteDelayMs = 0;
|
|
320
|
-
constructor(event) {
|
|
321
|
-
this.event = event;
|
|
322
|
-
}
|
|
323
|
-
async receiveMessage(args) {
|
|
324
|
-
this.requestedVisibilityTimeouts.push(args.visibilityTimeoutSeconds);
|
|
325
|
-
if (Date.now() < this.visibleAt) {
|
|
326
|
-
if (args.waitTimeSeconds <= 1)
|
|
327
|
-
return { messages: [] };
|
|
328
|
-
return new Promise((resolve) => {
|
|
329
|
-
if (args.signal.aborted)
|
|
330
|
-
return resolve({ messages: [] });
|
|
331
|
-
args.signal.addEventListener("abort", () => resolve({ messages: [] }), { once: true });
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
const receiptHandle = `visibility-rh-${++this.nextReceipt}`;
|
|
335
|
-
this.currentReceipt = receiptHandle;
|
|
336
|
-
this.deliveries.push(receiptHandle);
|
|
337
|
-
// One fake second is five real milliseconds. The queue's default lease is
|
|
338
|
-
// deliberately one fake second; the receiver's 600-second lease is 3s.
|
|
339
|
-
this.leaseExpiresAt = Date.now() + (args.visibilityTimeoutSeconds ?? 1) * 5;
|
|
340
|
-
this.visibleAt = this.leaseExpiresAt;
|
|
341
|
-
return { messages: [msg(this.event, receiptHandle)] };
|
|
342
|
-
}
|
|
343
|
-
async deleteMessage(args) {
|
|
344
|
-
if (this.deleteDelayMs > 0) {
|
|
345
|
-
await new Promise((resolve) => setTimeout(resolve, this.deleteDelayMs));
|
|
346
|
-
}
|
|
347
|
-
if (Date.now() >= this.leaseExpiresAt) {
|
|
348
|
-
this.visibleAt = 0;
|
|
349
|
-
this.deleteAttempts.push({ receiptHandle: args.receiptHandle, expired: true });
|
|
350
|
-
throw new Error("stale receipt handle after visibility expiry");
|
|
351
|
-
}
|
|
352
|
-
if (args.receiptHandle !== this.currentReceipt) {
|
|
353
|
-
this.deleteAttempts.push({ receiptHandle: args.receiptHandle, expired: true });
|
|
354
|
-
throw new Error("stale receipt handle");
|
|
355
|
-
}
|
|
356
|
-
this.deleted.push(args.receiptHandle);
|
|
357
|
-
this.deleteAttempts.push({ receiptHandle: args.receiptHandle, expired: false });
|
|
358
|
-
this.visibleAt = Number.POSITIVE_INFINITY;
|
|
359
|
-
}
|
|
360
|
-
async changeMessageVisibility(args) {
|
|
361
|
-
if (args.receiptHandle !== this.currentReceipt)
|
|
362
|
-
throw new Error("stale receipt handle");
|
|
363
|
-
this.visibilityChanges.push({
|
|
364
|
-
receiptHandle: args.receiptHandle,
|
|
365
|
-
visibilityTimeoutSeconds: args.visibilityTimeoutSeconds,
|
|
366
|
-
});
|
|
367
|
-
this.visibleAt = Date.now() + args.visibilityTimeoutSeconds * 5;
|
|
368
|
-
this.leaseExpiresAt = this.visibleAt;
|
|
369
|
-
}
|
|
370
|
-
async changeMessageVisibilityBatch(args) {
|
|
371
|
-
return changeVisibilityBatchIndividually((entry) => this.changeMessageVisibility(entry), args);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/** Queue fixture that honors ReceiveMessage.MaxNumberOfMessages. */
|
|
375
|
-
class OverCapSqs {
|
|
376
|
-
queued;
|
|
377
|
-
inFlight = new Map();
|
|
378
|
-
receiveCalls = [];
|
|
379
|
-
deleted = [];
|
|
380
|
-
releases = [];
|
|
381
|
-
constructor(initialMessages) {
|
|
382
|
-
this.queued = [...initialMessages];
|
|
383
|
-
}
|
|
384
|
-
enqueue(message) {
|
|
385
|
-
this.queued.push(message);
|
|
386
|
-
}
|
|
387
|
-
async receiveMessage(args) {
|
|
388
|
-
this.receiveCalls.push({
|
|
389
|
-
waitTimeSeconds: args.waitTimeSeconds,
|
|
390
|
-
maxMessages: args.maxMessages,
|
|
391
|
-
});
|
|
392
|
-
if (this.queued.length > 0) {
|
|
393
|
-
const messages = this.queued.splice(0, args.maxMessages);
|
|
394
|
-
for (const message of messages) {
|
|
395
|
-
if (message.ReceiptHandle)
|
|
396
|
-
this.inFlight.set(message.ReceiptHandle, message);
|
|
397
|
-
}
|
|
398
|
-
return { messages };
|
|
399
|
-
}
|
|
400
|
-
if (args.waitTimeSeconds <= 1)
|
|
401
|
-
return { messages: [] };
|
|
402
|
-
return new Promise((resolve) => {
|
|
403
|
-
if (args.signal.aborted)
|
|
404
|
-
return resolve({ messages: [] });
|
|
405
|
-
args.signal.addEventListener("abort", () => resolve({ messages: [] }), { once: true });
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
async deleteMessage(args) {
|
|
409
|
-
this.inFlight.delete(args.receiptHandle);
|
|
410
|
-
this.deleted.push(args.receiptHandle);
|
|
411
|
-
}
|
|
412
|
-
async changeMessageVisibility(args) {
|
|
413
|
-
this.releases.push({ receiptHandle: args.receiptHandle, visibilityTimeoutSeconds: args.visibilityTimeoutSeconds });
|
|
414
|
-
const released = this.inFlight.get(args.receiptHandle);
|
|
415
|
-
if (!released)
|
|
416
|
-
throw new Error("unknown receipt handle");
|
|
417
|
-
this.inFlight.delete(args.receiptHandle);
|
|
418
|
-
this.queued.unshift(released);
|
|
419
|
-
}
|
|
420
|
-
async changeMessageVisibilityBatch(args) {
|
|
421
|
-
return changeVisibilityBatchIndividually((entry) => this.changeMessageVisibility(entry), args);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/** Wait until `predicate()` is true or `timeoutMs` elapses. */
|
|
425
|
-
async function waitFor(predicate, timeoutMs = 1000) {
|
|
426
|
-
const start = Date.now();
|
|
427
|
-
while (!predicate()) {
|
|
428
|
-
if (Date.now() - start > timeoutMs) {
|
|
429
|
-
throw new Error("waitFor: timed out");
|
|
430
|
-
}
|
|
431
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
// fast, abortable sleep so backoff doesn't slow the suite
|
|
435
|
-
const fastSleep = (ms, signal) => new Promise((resolve) => {
|
|
436
|
-
if (signal.aborted)
|
|
437
|
-
return resolve();
|
|
438
|
-
const t = setTimeout(resolve, Math.min(ms, 5));
|
|
439
|
-
signal.addEventListener("abort", () => { clearTimeout(t); resolve(); }, { once: true });
|
|
440
|
-
});
|
|
441
|
-
describe("realtime v2 wake envelope", () => {
|
|
442
|
-
it("accepts only the cursor-only normative envelope", () => {
|
|
443
|
-
expect(decodeRealtimeWake(JSON.stringify({
|
|
444
|
-
contractVersion: 2,
|
|
445
|
-
eventType: "vault.changed",
|
|
446
|
-
scopeHandle: "scope_handle_1",
|
|
447
|
-
cursor: "cursor_hint_1",
|
|
448
|
-
}))).toEqual({
|
|
449
|
-
contractVersion: 2,
|
|
450
|
-
eventType: "vault.changed",
|
|
451
|
-
scopeHandle: "scope_handle_1",
|
|
452
|
-
cursor: "cursor_hint_1",
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
it("accepts the existing opaque cursor alphabet without interpreting it", () => {
|
|
456
|
-
expect(decodeRealtimeWake(JSON.stringify({
|
|
457
|
-
contractVersion: 2,
|
|
458
|
-
eventType: "vault.changed",
|
|
459
|
-
scopeHandle: "scope_handle_1",
|
|
460
|
-
cursor: "cursor/+==",
|
|
461
|
-
})).cursor).toBe("cursor/+==");
|
|
462
|
-
});
|
|
463
|
-
it("rejects path-bearing or extended wakes before they reach scheduling", () => {
|
|
464
|
-
expect(() => decodeRealtimeWake(JSON.stringify({
|
|
465
|
-
contractVersion: 2,
|
|
466
|
-
eventType: "vault.changed",
|
|
467
|
-
scopeHandle: "scope_handle_1",
|
|
468
|
-
cursor: "cursor_hint_1",
|
|
469
|
-
relativePath: "companies/indigo/private.md",
|
|
470
|
-
}))).toThrow(/unknown field/);
|
|
471
|
-
});
|
|
472
|
-
it("does not compare opaque cursors and stops delivery after disposal", async () => {
|
|
473
|
-
const sourceState = { listener: null };
|
|
474
|
-
const source = {
|
|
475
|
-
start: (next) => { sourceState.listener = next; },
|
|
476
|
-
dispose: vi.fn(),
|
|
477
|
-
};
|
|
478
|
-
const received = [];
|
|
479
|
-
const receiver = new RealtimeWakeReceiver({
|
|
480
|
-
source,
|
|
481
|
-
onWake: (wake) => received.push(wake.scopeHandle),
|
|
482
|
-
});
|
|
483
|
-
await receiver.start();
|
|
484
|
-
sourceState.listener?.(JSON.stringify({ contractVersion: 2, eventType: "vault.changed", scopeHandle: "scope_a", cursor: "cursor_z" }));
|
|
485
|
-
sourceState.listener?.(JSON.stringify({ contractVersion: 2, eventType: "vault.changed", scopeHandle: "scope_a", cursor: "cursor_a" }));
|
|
486
|
-
expect(received).toEqual(["scope_a", "scope_a"]);
|
|
487
|
-
await receiver.dispose();
|
|
488
|
-
sourceState.listener?.(JSON.stringify({ contractVersion: 2, eventType: "vault.changed", scopeHandle: "scope_a", cursor: "cursor_new" }));
|
|
489
|
-
expect(received).toEqual(["scope_a", "scope_a"]);
|
|
490
|
-
expect(source.dispose).toHaveBeenCalledOnce();
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
// ─── SqsPushReceiver ─────────────────────────────────────────────────────────
|
|
494
|
-
describe("US-OBS: SqsPushReceiver — origin observability", () => {
|
|
495
|
-
async function collectOriginRecords(logOrigins) {
|
|
496
|
-
const sqs = new FakeSqs();
|
|
497
|
-
const records = [];
|
|
498
|
-
sqs.enqueue([msg(makeEvent({ originDeviceId: "device-origin", sequenceNumber: 1 }))]);
|
|
499
|
-
const receiver = new SqsPushReceiver({
|
|
500
|
-
tenantId: TENANT,
|
|
501
|
-
queueUrl: QUEUE_URL,
|
|
502
|
-
sqs,
|
|
503
|
-
syncBatchFn: async () => undefined,
|
|
504
|
-
sleep: fastSleep,
|
|
505
|
-
publishMetric: async () => undefined,
|
|
506
|
-
...(logOrigins === undefined ? {} : { logOrigins }),
|
|
507
|
-
logger: {
|
|
508
|
-
debug: () => undefined,
|
|
509
|
-
warn: () => undefined,
|
|
510
|
-
error: () => undefined,
|
|
511
|
-
info: (record) => { records.push(record); },
|
|
512
|
-
},
|
|
513
|
-
});
|
|
514
|
-
await receiver.start();
|
|
515
|
-
await waitFor(() => records.some((record) => record.event === "fanout.receive") &&
|
|
516
|
-
records.some((record) => record.event === "receiver.drain"));
|
|
517
|
-
await receiver.dispose();
|
|
518
|
-
return records;
|
|
519
|
-
}
|
|
520
|
-
it("omits both origin fields by default", async () => {
|
|
521
|
-
const records = await collectOriginRecords();
|
|
522
|
-
const fanout = records.find((record) => record.event === "fanout.receive");
|
|
523
|
-
const drain = records.find((record) => record.event === "receiver.drain");
|
|
524
|
-
expect(fanout).toBeDefined();
|
|
525
|
-
expect(drain).toBeDefined();
|
|
526
|
-
expect([
|
|
527
|
-
Object.hasOwn(fanout, "originDeviceId"),
|
|
528
|
-
Object.hasOwn(drain, "origins"),
|
|
529
|
-
]).toEqual([false, false]);
|
|
530
|
-
});
|
|
531
|
-
it("omits both origin fields when logOrigins is false", async () => {
|
|
532
|
-
const records = await collectOriginRecords(false);
|
|
533
|
-
const fanout = records.find((record) => record.event === "fanout.receive");
|
|
534
|
-
const drain = records.find((record) => record.event === "receiver.drain");
|
|
535
|
-
expect(fanout).toBeDefined();
|
|
536
|
-
expect(drain).toBeDefined();
|
|
537
|
-
expect([
|
|
538
|
-
Object.hasOwn(fanout, "originDeviceId"),
|
|
539
|
-
Object.hasOwn(drain, "origins"),
|
|
540
|
-
]).toEqual([false, false]);
|
|
541
|
-
});
|
|
542
|
-
it("includes both origin fields when logOrigins is true", async () => {
|
|
543
|
-
const records = await collectOriginRecords(true);
|
|
544
|
-
const fanout = records.find((record) => record.event === "fanout.receive");
|
|
545
|
-
const drain = records.find((record) => record.event === "receiver.drain");
|
|
546
|
-
expect(fanout).toMatchObject({ originDeviceId: "device-origin" });
|
|
547
|
-
expect(drain).toMatchObject({ origins: { "device-origin": 1 } });
|
|
548
|
-
});
|
|
549
|
-
it("includes the event originDeviceId in each fanout.receive record", async () => {
|
|
550
|
-
const sqs = new FakeSqs();
|
|
551
|
-
const receivedRecords = [];
|
|
552
|
-
sqs.enqueue([msg(makeEvent({ originDeviceId: "device-origin", sequenceNumber: 1 }))]);
|
|
553
|
-
const receiver = new SqsPushReceiver({
|
|
554
|
-
tenantId: TENANT,
|
|
555
|
-
queueUrl: QUEUE_URL,
|
|
556
|
-
sqs,
|
|
557
|
-
syncBatchFn: async () => undefined,
|
|
558
|
-
sleep: fastSleep,
|
|
559
|
-
publishMetric: async () => undefined,
|
|
560
|
-
logOrigins: true,
|
|
561
|
-
logger: {
|
|
562
|
-
debug: () => undefined,
|
|
563
|
-
warn: () => undefined,
|
|
564
|
-
error: () => undefined,
|
|
565
|
-
info: (record) => { receivedRecords.push(record); },
|
|
566
|
-
},
|
|
567
|
-
});
|
|
568
|
-
await receiver.start();
|
|
569
|
-
await waitFor(() => receivedRecords.some((record) => record.event === "fanout.receive" && record.relativePath === "companies/indigo/notes.md"));
|
|
570
|
-
await receiver.dispose();
|
|
571
|
-
expect(receivedRecords.find((record) => record.event === "fanout.receive"))
|
|
572
|
-
.toMatchObject({ originDeviceId: "device-origin" });
|
|
573
|
-
});
|
|
574
|
-
it("counts filtered and processed origin events in the receiver.drain record", async () => {
|
|
575
|
-
const sqs = new FakeSqs();
|
|
576
|
-
const drainRecords = [];
|
|
577
|
-
sqs.enqueueDrainable([
|
|
578
|
-
msg(makeEvent({ originDeviceId: "device-A", relativePath: "companies/indigo/a.md", sequenceNumber: 1 }), "rh-origin-a1"),
|
|
579
|
-
msg(makeEvent({ originDeviceId: "device-A", relativePath: "companies/indigo/b.md", sequenceNumber: 2 }), "rh-origin-a2"),
|
|
580
|
-
msg(makeEvent({ originDeviceId: "device-self", relativePath: "companies/indigo/self.md", sequenceNumber: 3 }), "rh-origin-self"),
|
|
581
|
-
]);
|
|
582
|
-
const receiver = new SqsPushReceiver({
|
|
583
|
-
tenantId: TENANT,
|
|
584
|
-
queueUrl: QUEUE_URL,
|
|
585
|
-
sqs,
|
|
586
|
-
shouldProcess: (event) => event.originDeviceId !== "device-self",
|
|
587
|
-
syncBatchFn: async () => undefined,
|
|
588
|
-
sleep: fastSleep,
|
|
589
|
-
publishMetric: async () => undefined,
|
|
590
|
-
logOrigins: true,
|
|
591
|
-
logger: {
|
|
592
|
-
debug: () => undefined,
|
|
593
|
-
warn: () => undefined,
|
|
594
|
-
error: () => undefined,
|
|
595
|
-
info: (record) => {
|
|
596
|
-
if (record.event === "receiver.drain")
|
|
597
|
-
drainRecords.push(record);
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
await receiver.start();
|
|
602
|
-
await waitFor(() => drainRecords.some((record) => record.received === 3));
|
|
603
|
-
await receiver.dispose();
|
|
604
|
-
expect(drainRecords.find((record) => record.received === 3)?.origins)
|
|
605
|
-
.toEqual({ "device-A": 2, "device-self": 1 });
|
|
606
|
-
});
|
|
607
|
-
it("caps receiver.drain origins at twenty device keys plus _other", async () => {
|
|
608
|
-
const sqs = new FakeSqs();
|
|
609
|
-
const drainRecords = [];
|
|
610
|
-
const events = Array.from({ length: 25 }, (_, index) => makeEvent({
|
|
611
|
-
originDeviceId: `device-${String(index).padStart(2, "0")}`,
|
|
612
|
-
relativePath: `companies/indigo/origins/${index}.md`,
|
|
613
|
-
sequenceNumber: index + 1,
|
|
614
|
-
}));
|
|
615
|
-
sqs.enqueueDrainable(events.map((event, index) => msg(event, `rh-origin-cap-${index}`)));
|
|
616
|
-
const receiver = new SqsPushReceiver({
|
|
617
|
-
tenantId: TENANT,
|
|
618
|
-
queueUrl: QUEUE_URL,
|
|
619
|
-
sqs,
|
|
620
|
-
syncBatchFn: async () => undefined,
|
|
621
|
-
sleep: fastSleep,
|
|
622
|
-
publishMetric: async () => undefined,
|
|
623
|
-
logOrigins: true,
|
|
624
|
-
logger: {
|
|
625
|
-
debug: () => undefined,
|
|
626
|
-
warn: () => undefined,
|
|
627
|
-
error: () => undefined,
|
|
628
|
-
info: (record) => {
|
|
629
|
-
if (record.event === "receiver.drain")
|
|
630
|
-
drainRecords.push(record);
|
|
631
|
-
},
|
|
632
|
-
},
|
|
633
|
-
});
|
|
634
|
-
await receiver.start();
|
|
635
|
-
await waitFor(() => drainRecords.some((record) => record.received === 25));
|
|
636
|
-
await receiver.dispose();
|
|
637
|
-
const origins = drainRecords.find((record) => record.received === 25)?.origins;
|
|
638
|
-
expect(origins).toEqual({
|
|
639
|
-
...Object.fromEntries(Array.from({ length: 20 }, (_, index) => [
|
|
640
|
-
`device-${String(index).padStart(2, "0")}`,
|
|
641
|
-
1,
|
|
642
|
-
])),
|
|
643
|
-
_other: 5,
|
|
644
|
-
});
|
|
645
|
-
expect(Object.keys(origins)).toHaveLength(21);
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
describe("US-009: SqsPushReceiver — subscribe lifecycle", () => {
|
|
649
|
-
it("start opens the poll loop and processes a received event via syncFn, then deletes it", async () => {
|
|
650
|
-
const sqs = new FakeSqs();
|
|
651
|
-
const seen = [];
|
|
652
|
-
const syncFn = async (ctx) => {
|
|
653
|
-
seen.push(ctx.event.relativePath);
|
|
654
|
-
};
|
|
655
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }))]);
|
|
656
|
-
const receiver = new SqsPushReceiver({
|
|
657
|
-
tenantId: TENANT,
|
|
658
|
-
queueUrl: QUEUE_URL,
|
|
659
|
-
sqs,
|
|
660
|
-
syncFn,
|
|
661
|
-
sleep: fastSleep,
|
|
662
|
-
});
|
|
663
|
-
await receiver.start();
|
|
664
|
-
expect(receiver.connected).toBe(true);
|
|
665
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
666
|
-
expect(seen).toEqual(["companies/indigo/notes.md"]);
|
|
667
|
-
expect(sqs.deleted).toContain("rh-1");
|
|
668
|
-
await receiver.dispose();
|
|
669
|
-
expect(receiver.connected).toBe(false);
|
|
670
|
-
});
|
|
671
|
-
it("double-start is a no-op (single poll loop)", async () => {
|
|
672
|
-
const sqs = new FakeSqs();
|
|
673
|
-
const receiver = new SqsPushReceiver({
|
|
674
|
-
tenantId: TENANT,
|
|
675
|
-
queueUrl: QUEUE_URL,
|
|
676
|
-
sqs,
|
|
677
|
-
syncFn: async () => undefined,
|
|
678
|
-
sleep: fastSleep,
|
|
679
|
-
});
|
|
680
|
-
await receiver.start();
|
|
681
|
-
await receiver.start();
|
|
682
|
-
// Let a couple poll iterations happen; a duplicated loop would roughly
|
|
683
|
-
// double the receive call rate. We just assert it stays connected + clean.
|
|
684
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
685
|
-
expect(receiver.connected).toBe(true);
|
|
686
|
-
await receiver.dispose();
|
|
687
|
-
});
|
|
688
|
-
it("subscribes ONLY to its own tenant's queue (cross-tenant isolation boundary)", async () => {
|
|
689
|
-
const sqs = new FakeSqs();
|
|
690
|
-
const urls = [];
|
|
691
|
-
const spy = {
|
|
692
|
-
receiveMessage: (a) => {
|
|
693
|
-
urls.push(a.queueUrl);
|
|
694
|
-
return sqs.receiveMessage(a);
|
|
695
|
-
},
|
|
696
|
-
deleteMessage: (a) => sqs.deleteMessage(a),
|
|
697
|
-
changeMessageVisibility: (a) => sqs.changeMessageVisibility(a),
|
|
698
|
-
changeMessageVisibilityBatch: (a) => sqs.changeMessageVisibilityBatch(a),
|
|
699
|
-
};
|
|
700
|
-
const receiver = new SqsPushReceiver({
|
|
701
|
-
tenantId: TENANT,
|
|
702
|
-
queueUrl: QUEUE_URL,
|
|
703
|
-
sqs: spy,
|
|
704
|
-
syncFn: async () => undefined,
|
|
705
|
-
sleep: fastSleep,
|
|
706
|
-
});
|
|
707
|
-
await receiver.start();
|
|
708
|
-
await waitFor(() => urls.length >= 1);
|
|
709
|
-
await receiver.dispose();
|
|
710
|
-
expect(new Set(urls)).toEqual(new Set([QUEUE_URL]));
|
|
711
|
-
});
|
|
712
|
-
it("constructor rejects empty tenantId / queueUrl", () => {
|
|
713
|
-
expect(() => new SqsPushReceiver({
|
|
714
|
-
tenantId: "",
|
|
715
|
-
queueUrl: QUEUE_URL,
|
|
716
|
-
sqs: new FakeSqs(),
|
|
717
|
-
syncFn: async () => undefined,
|
|
718
|
-
})).toThrow(/tenantId is required/);
|
|
719
|
-
expect(() => new SqsPushReceiver({
|
|
720
|
-
tenantId: TENANT,
|
|
721
|
-
queueUrl: "",
|
|
722
|
-
sqs: new FakeSqs(),
|
|
723
|
-
syncFn: async () => undefined,
|
|
724
|
-
})).toThrow(/queueUrl is required/);
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
describe("receiver drain diagnostics", () => {
|
|
728
|
-
it("acknowledges paths proven unchanged without dispatching a pull", async () => {
|
|
729
|
-
const event = makeEvent({ relativePath: "companies/indigo/unchanged.md" });
|
|
730
|
-
const sqs = new FakeSqs();
|
|
731
|
-
sqs.enqueueDrainable([msg(event, "rh-unchanged")]);
|
|
732
|
-
const syncBatchFn = vi.fn(async () => undefined);
|
|
733
|
-
const drainRecords = [];
|
|
734
|
-
const receiver = new SqsPushReceiver({
|
|
735
|
-
tenantId: TENANT,
|
|
736
|
-
queueUrl: QUEUE_URL,
|
|
737
|
-
sqs,
|
|
738
|
-
syncBatchFn,
|
|
739
|
-
skipUnchangedFn: async (events) => new Set(events.map((entry) => entry.relativePath)),
|
|
740
|
-
sleep: fastSleep,
|
|
741
|
-
publishMetric: async () => undefined,
|
|
742
|
-
logger: {
|
|
743
|
-
info: (record) => {
|
|
744
|
-
if (record.event === "receiver.drain")
|
|
745
|
-
drainRecords.push(record);
|
|
746
|
-
},
|
|
747
|
-
warn: () => undefined,
|
|
748
|
-
error: () => undefined,
|
|
749
|
-
debug: () => undefined,
|
|
750
|
-
},
|
|
751
|
-
});
|
|
752
|
-
await receiver.start();
|
|
753
|
-
await waitFor(() => sqs.deleted.length === 1 && drainRecords.length === 1);
|
|
754
|
-
await receiver.dispose();
|
|
755
|
-
expect(sqs.deleted).toEqual(["rh-unchanged"]);
|
|
756
|
-
expect(syncBatchFn).not.toHaveBeenCalled();
|
|
757
|
-
expect(drainRecords[0]).toMatchObject({
|
|
758
|
-
received: 1,
|
|
759
|
-
coalesced: 1,
|
|
760
|
-
processed: 0,
|
|
761
|
-
skippedUnchanged: 1,
|
|
762
|
-
deleted: 1,
|
|
763
|
-
remaining: 0,
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
it("falls back to a targeted pull when the unchanged proof fails", async () => {
|
|
767
|
-
const event = makeEvent({ relativePath: "companies/indigo/proof-error.md" });
|
|
768
|
-
const sqs = new FakeSqs();
|
|
769
|
-
sqs.enqueueDrainable([msg(event, "rh-proof-error")]);
|
|
770
|
-
const syncBatchFn = vi.fn(async () => undefined);
|
|
771
|
-
const warnings = [];
|
|
772
|
-
const receiver = new SqsPushReceiver({
|
|
773
|
-
tenantId: TENANT,
|
|
774
|
-
queueUrl: QUEUE_URL,
|
|
775
|
-
sqs,
|
|
776
|
-
syncBatchFn,
|
|
777
|
-
skipUnchangedFn: async () => { throw new Error("journal unavailable"); },
|
|
778
|
-
sleep: fastSleep,
|
|
779
|
-
publishMetric: async () => undefined,
|
|
780
|
-
logger: {
|
|
781
|
-
info: () => undefined,
|
|
782
|
-
warn: (record) => warnings.push(record),
|
|
783
|
-
error: () => undefined,
|
|
784
|
-
debug: () => undefined,
|
|
785
|
-
},
|
|
786
|
-
});
|
|
787
|
-
await receiver.start();
|
|
788
|
-
await waitFor(() => sqs.deleted.length === 1 && syncBatchFn.mock.calls.length === 1);
|
|
789
|
-
await receiver.dispose();
|
|
790
|
-
expect(syncBatchFn.mock.calls[0]?.[0].events).toEqual([event]);
|
|
791
|
-
expect(warnings).toContainEqual(expect.objectContaining({
|
|
792
|
-
event: "receiver.unchanged.check.failed",
|
|
793
|
-
err: { message: "journal unavailable" },
|
|
794
|
-
}));
|
|
795
|
-
});
|
|
796
|
-
it("reports redelivery count, oldest sent age, and filtered self echoes", async () => {
|
|
797
|
-
const nowMs = 10_000;
|
|
798
|
-
const sqs = new FakeSqs();
|
|
799
|
-
sqs.enqueueDrainable([
|
|
800
|
-
{
|
|
801
|
-
...msg(makeEvent({ relativePath: "companies/indigo/first.md", sequenceNumber: 1 })),
|
|
802
|
-
approximateReceiveCount: 1,
|
|
803
|
-
sentTimestampMs: 7_000,
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
...msg(makeEvent({ relativePath: "companies/indigo/second.md", sequenceNumber: 2 })),
|
|
807
|
-
approximateReceiveCount: 3,
|
|
808
|
-
sentTimestampMs: 8_000,
|
|
809
|
-
},
|
|
810
|
-
{
|
|
811
|
-
...msg(makeEvent({
|
|
812
|
-
relativePath: "companies/indigo/self.md",
|
|
813
|
-
sequenceNumber: 3,
|
|
814
|
-
originDeviceId: "this-device",
|
|
815
|
-
})),
|
|
816
|
-
approximateReceiveCount: 2,
|
|
817
|
-
sentTimestampMs: 9_000,
|
|
818
|
-
},
|
|
819
|
-
]);
|
|
820
|
-
const drainRecords = [];
|
|
821
|
-
const receiver = new SqsPushReceiver({
|
|
822
|
-
tenantId: TENANT,
|
|
823
|
-
queueUrl: QUEUE_URL,
|
|
824
|
-
sqs,
|
|
825
|
-
shouldProcess: (event) => event.originDeviceId !== "this-device",
|
|
826
|
-
syncBatchFn: async () => undefined,
|
|
827
|
-
sleep: fastSleep,
|
|
828
|
-
now: () => nowMs,
|
|
829
|
-
publishMetric: async () => undefined,
|
|
830
|
-
logger: {
|
|
831
|
-
info: (record) => {
|
|
832
|
-
if (record.event === "receiver.drain")
|
|
833
|
-
drainRecords.push(record);
|
|
834
|
-
},
|
|
835
|
-
warn: () => undefined,
|
|
836
|
-
error: () => undefined,
|
|
837
|
-
debug: () => undefined,
|
|
838
|
-
},
|
|
839
|
-
});
|
|
840
|
-
await receiver.start();
|
|
841
|
-
await waitFor(() => sqs.deleted.length === 3);
|
|
842
|
-
await receiver.dispose();
|
|
843
|
-
expect(sqs.deleted).toHaveLength(3);
|
|
844
|
-
expect(drainRecords).toHaveLength(1);
|
|
845
|
-
expect(drainRecords[0]).toMatchObject({
|
|
846
|
-
received: 3,
|
|
847
|
-
coalesced: 2,
|
|
848
|
-
processed: 2,
|
|
849
|
-
deleted: 3,
|
|
850
|
-
released: 0,
|
|
851
|
-
remaining: 0,
|
|
852
|
-
redelivered: 2,
|
|
853
|
-
maxReceiveCount: 3,
|
|
854
|
-
oldestAgeMs: 3_000,
|
|
855
|
-
filtered: 1,
|
|
856
|
-
});
|
|
857
|
-
});
|
|
858
|
-
it("clamps oldest message age to zero when the local clock trails SQS", async () => {
|
|
859
|
-
const sqs = new FakeSqs();
|
|
860
|
-
sqs.enqueueDrainable([{
|
|
861
|
-
...msg(makeEvent()),
|
|
862
|
-
sentTimestampMs: 11_000,
|
|
863
|
-
}]);
|
|
864
|
-
const drainRecords = [];
|
|
865
|
-
const receiver = new SqsPushReceiver({
|
|
866
|
-
tenantId: TENANT,
|
|
867
|
-
queueUrl: QUEUE_URL,
|
|
868
|
-
sqs,
|
|
869
|
-
syncBatchFn: async () => undefined,
|
|
870
|
-
sleep: fastSleep,
|
|
871
|
-
now: () => 10_000,
|
|
872
|
-
publishMetric: async () => undefined,
|
|
873
|
-
logger: {
|
|
874
|
-
info: (record) => {
|
|
875
|
-
if (record.event === "receiver.drain")
|
|
876
|
-
drainRecords.push(record);
|
|
877
|
-
},
|
|
878
|
-
warn: () => undefined,
|
|
879
|
-
error: () => undefined,
|
|
880
|
-
debug: () => undefined,
|
|
881
|
-
},
|
|
882
|
-
});
|
|
883
|
-
await receiver.start();
|
|
884
|
-
await waitFor(() => sqs.deleted.length === 1);
|
|
885
|
-
await receiver.dispose();
|
|
886
|
-
expect(drainRecords).toHaveLength(1);
|
|
887
|
-
expect(drainRecords[0]?.oldestAgeMs).toBe(0);
|
|
888
|
-
});
|
|
889
|
-
});
|
|
890
|
-
describe("US-009: SqsPushReceiver — dedupe", () => {
|
|
891
|
-
it("REGRESSION: collapses 500 received messages across 3 paths into one sync pass", async () => {
|
|
892
|
-
const sqs = new FakeSqs();
|
|
893
|
-
const batches = [];
|
|
894
|
-
const syncBatchFn = async (ctx) => {
|
|
895
|
-
batches.push(ctx);
|
|
896
|
-
};
|
|
897
|
-
const paths = [
|
|
898
|
-
"companies/indigo/a.md",
|
|
899
|
-
"companies/indigo/b.md",
|
|
900
|
-
"companies/indigo/c.md",
|
|
901
|
-
];
|
|
902
|
-
const messages = Array.from({ length: 500 }, (_, index) => msg(makeEvent({
|
|
903
|
-
relativePath: paths[index % paths.length],
|
|
904
|
-
sequenceNumber: index + 1,
|
|
905
|
-
}), `rh-batch-${index}`));
|
|
906
|
-
for (let start = 0; start < messages.length; start += 10) {
|
|
907
|
-
sqs.enqueueDrainable(messages.slice(start, start + 10));
|
|
908
|
-
}
|
|
909
|
-
const receiver = new SqsPushReceiver({
|
|
910
|
-
tenantId: TENANT,
|
|
911
|
-
queueUrl: QUEUE_URL,
|
|
912
|
-
sqs,
|
|
913
|
-
syncBatchFn,
|
|
914
|
-
sleep: fastSleep,
|
|
915
|
-
});
|
|
916
|
-
await receiver.start();
|
|
917
|
-
await waitFor(() => receiver.processedCount === 3);
|
|
918
|
-
await receiver.dispose();
|
|
919
|
-
expect(batches).toHaveLength(1);
|
|
920
|
-
expect(batches[0].events.map((event) => event.relativePath).sort()).toEqual(paths);
|
|
921
|
-
});
|
|
922
|
-
it("fills a 500-receipt drain after an empty short-poll false negative", async () => {
|
|
923
|
-
const receipts = Array.from({ length: 500 }, (_, index) => msg(makeEvent({
|
|
924
|
-
relativePath: `companies/indigo/false-negative/${index}.md`,
|
|
925
|
-
sequenceNumber: index + 1,
|
|
926
|
-
}), `rh-false-negative-${index}`));
|
|
927
|
-
const sqs = new FalseNegativeSqs(receipts);
|
|
928
|
-
const batches = [];
|
|
929
|
-
const receiver = new SqsPushReceiver({
|
|
930
|
-
tenantId: TENANT,
|
|
931
|
-
queueUrl: QUEUE_URL,
|
|
932
|
-
sqs,
|
|
933
|
-
drainMaxMessages: 500,
|
|
934
|
-
batchMaxPaths: 500,
|
|
935
|
-
syncBatchFn: async (ctx) => { batches.push(ctx); },
|
|
936
|
-
sleep: fastSleep,
|
|
937
|
-
publishMetric: async () => undefined,
|
|
938
|
-
});
|
|
939
|
-
await receiver.start();
|
|
940
|
-
await waitFor(() => receiver.processedCount === 500);
|
|
941
|
-
await receiver.dispose();
|
|
942
|
-
expect(batches.map((batch) => batch.events.length)).toEqual([500]);
|
|
943
|
-
expect(sqs.deleted).toHaveLength(500);
|
|
944
|
-
expect(sqs.receiveWaitTimes.slice(0, 3)).toEqual([20, 0, 1]);
|
|
945
|
-
});
|
|
946
|
-
it("caps each 1,000-receipt drain at 256 paths and processes the remaining receipts", async () => {
|
|
947
|
-
const sqs = new FakeSqs();
|
|
948
|
-
const receipts = Array.from({ length: 1_000 }, (_, index) => msg(makeEvent({
|
|
949
|
-
relativePath: `companies/indigo/deep/${index % 256}.md`,
|
|
950
|
-
sequenceNumber: index + 1,
|
|
951
|
-
}), `rh-deep-${index}`));
|
|
952
|
-
for (let start = 0; start < receipts.length; start += 10) {
|
|
953
|
-
sqs.enqueueDrainable(receipts.slice(start, start + 10));
|
|
954
|
-
}
|
|
955
|
-
const batches = [];
|
|
956
|
-
const receiver = new SqsPushReceiver({
|
|
957
|
-
tenantId: TENANT,
|
|
958
|
-
queueUrl: QUEUE_URL,
|
|
959
|
-
sqs,
|
|
960
|
-
syncBatchFn: async (ctx) => { batches.push(ctx); },
|
|
961
|
-
sleep: fastSleep,
|
|
962
|
-
publishMetric: async () => undefined,
|
|
963
|
-
});
|
|
964
|
-
await receiver.start();
|
|
965
|
-
await waitFor(() => sqs.deleted.length === 1_000, 5_000);
|
|
966
|
-
await receiver.dispose();
|
|
967
|
-
expect(batches.map((batch) => batch.events.length)).toEqual([256]);
|
|
968
|
-
expect(sqs.deleted).toHaveLength(1_000);
|
|
969
|
-
expect(sqs.visibilityChanges).toEqual([]);
|
|
970
|
-
expect(sqs.receiveWaitTimes[0]).toBe(20);
|
|
971
|
-
});
|
|
972
|
-
it("reports a nonempty large drain before it can later report an empty long-poll", async () => {
|
|
973
|
-
const sqs = new FakeSqs();
|
|
974
|
-
const activity = [];
|
|
975
|
-
const receipts = Array.from({ length: 1_000 }, (_, index) => msg(makeEvent({
|
|
976
|
-
relativePath: `companies/indigo/bulk/${index % 256}.md`,
|
|
977
|
-
sequenceNumber: index + 1,
|
|
978
|
-
}), `rh-activity-${index}`));
|
|
979
|
-
for (let start = 0; start < receipts.length; start += 10) {
|
|
980
|
-
sqs.enqueueDrainable(receipts.slice(start, start + 10));
|
|
981
|
-
}
|
|
982
|
-
const receiver = new SqsPushReceiver({
|
|
983
|
-
tenantId: TENANT,
|
|
984
|
-
queueUrl: QUEUE_URL,
|
|
985
|
-
sqs,
|
|
986
|
-
syncBatchFn: async () => undefined,
|
|
987
|
-
onReceiveActivity: (hasMessages) => activity.push(hasMessages),
|
|
988
|
-
sleep: fastSleep,
|
|
989
|
-
publishMetric: async () => undefined,
|
|
990
|
-
});
|
|
991
|
-
await receiver.start();
|
|
992
|
-
await waitFor(() => receiver.processedCount >= 256 && activity.includes(false));
|
|
993
|
-
await receiver.dispose();
|
|
994
|
-
expect(activity[0]).toBe(true);
|
|
995
|
-
expect(activity).toContain(false);
|
|
996
|
-
expect(sqs.deleted).toHaveLength(1_000);
|
|
997
|
-
});
|
|
998
|
-
it("ends a near-empty drain after three short-poll confirmations without busy-looping", async () => {
|
|
999
|
-
const sqs = new FakeSqs();
|
|
1000
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-near-empty")]);
|
|
1001
|
-
const batches = [];
|
|
1002
|
-
const startedAt = Date.now();
|
|
1003
|
-
const receiver = new SqsPushReceiver({
|
|
1004
|
-
tenantId: TENANT,
|
|
1005
|
-
queueUrl: QUEUE_URL,
|
|
1006
|
-
sqs,
|
|
1007
|
-
syncBatchFn: async (ctx) => { batches.push(ctx); },
|
|
1008
|
-
sleep: fastSleep,
|
|
1009
|
-
publishMetric: async () => undefined,
|
|
1010
|
-
});
|
|
1011
|
-
await receiver.start();
|
|
1012
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
1013
|
-
const elapsedMs = Date.now() - startedAt;
|
|
1014
|
-
await receiver.dispose();
|
|
1015
|
-
expect(batches).toHaveLength(1);
|
|
1016
|
-
expect(sqs.receiveWaitTimes.slice(0, 5)).toEqual([20, 0, 1, 1, 1]);
|
|
1017
|
-
expect(sqs.receiveCalls).toBeLessThanOrEqual(6);
|
|
1018
|
-
expect(elapsedMs).toBeLessThan(5_000);
|
|
1019
|
-
});
|
|
1020
|
-
it("REGRESSION: leases a receipt through a slow pass so it is received exactly once", async () => {
|
|
1021
|
-
const sqs = new VisibilityExpirySqs(makeEvent({ sequenceNumber: 41 }));
|
|
1022
|
-
const receiver = new SqsPushReceiver({
|
|
1023
|
-
tenantId: TENANT,
|
|
1024
|
-
queueUrl: QUEUE_URL,
|
|
1025
|
-
sqs,
|
|
1026
|
-
syncBatchFn: async () => {
|
|
1027
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1028
|
-
},
|
|
1029
|
-
sleep: fastSleep,
|
|
1030
|
-
publishMetric: async () => undefined,
|
|
1031
|
-
});
|
|
1032
|
-
await receiver.start();
|
|
1033
|
-
await waitFor(() => sqs.deleted.length === 1);
|
|
1034
|
-
await receiver.dispose();
|
|
1035
|
-
expect(sqs.requestedVisibilityTimeouts[0]).toBe(600);
|
|
1036
|
-
expect(sqs.deliveries).toHaveLength(1);
|
|
1037
|
-
});
|
|
1038
|
-
it("heartbeats a receipt when a targeted pull outlives its visibility lease", async () => {
|
|
1039
|
-
const sqs = new VisibilityExpirySqs(makeEvent({ sequenceNumber: 42 }));
|
|
1040
|
-
const receiver = new SqsPushReceiver({
|
|
1041
|
-
tenantId: TENANT,
|
|
1042
|
-
queueUrl: QUEUE_URL,
|
|
1043
|
-
sqs,
|
|
1044
|
-
visibilityTimeoutSeconds: 1,
|
|
1045
|
-
visibilityHeartbeatIntervalMs: 1,
|
|
1046
|
-
syncBatchFn: async () => {
|
|
1047
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1048
|
-
},
|
|
1049
|
-
sleep: fastSleep,
|
|
1050
|
-
publishMetric: async () => undefined,
|
|
1051
|
-
});
|
|
1052
|
-
await receiver.start();
|
|
1053
|
-
try {
|
|
1054
|
-
await waitFor(() => sqs.deleteAttempts.length === 1);
|
|
1055
|
-
}
|
|
1056
|
-
finally {
|
|
1057
|
-
await receiver.dispose();
|
|
1058
|
-
}
|
|
1059
|
-
expect(sqs.requestedVisibilityTimeouts[0]).toBe(1);
|
|
1060
|
-
expect(sqs.visibilityChanges.length).toBeGreaterThan(0);
|
|
1061
|
-
expect(sqs.deliveries).toHaveLength(1);
|
|
1062
|
-
expect(sqs.deleted).toHaveLength(1);
|
|
1063
|
-
});
|
|
1064
|
-
it("keeps the receipt lease alive until a slow successful delete completes", async () => {
|
|
1065
|
-
const sqs = new VisibilityExpirySqs(makeEvent({ sequenceNumber: 43 }));
|
|
1066
|
-
sqs.deleteDelayMs = 30;
|
|
1067
|
-
const receiver = new SqsPushReceiver({
|
|
1068
|
-
tenantId: TENANT,
|
|
1069
|
-
queueUrl: QUEUE_URL,
|
|
1070
|
-
sqs,
|
|
1071
|
-
visibilityTimeoutSeconds: 1,
|
|
1072
|
-
visibilityHeartbeatIntervalMs: 1,
|
|
1073
|
-
syncBatchFn: async () => undefined,
|
|
1074
|
-
sleep: fastSleep,
|
|
1075
|
-
publishMetric: async () => undefined,
|
|
1076
|
-
});
|
|
1077
|
-
await receiver.start();
|
|
1078
|
-
try {
|
|
1079
|
-
await waitFor(() => sqs.deleteAttempts.length === 1);
|
|
1080
|
-
}
|
|
1081
|
-
finally {
|
|
1082
|
-
await receiver.dispose();
|
|
1083
|
-
}
|
|
1084
|
-
expect(sqs.visibilityChanges.some((change) => change.visibilityTimeoutSeconds === 1)).toBe(true);
|
|
1085
|
-
expect(sqs.deleteAttempts).toEqual([{ receiptHandle: "visibility-rh-1", expired: false }]);
|
|
1086
|
-
expect(sqs.deliveries).toHaveLength(1);
|
|
1087
|
-
expect(sqs.deleted).toHaveLength(1);
|
|
1088
|
-
});
|
|
1089
|
-
it("waits for an in-flight receipt heartbeat before releasing a failed delete", async () => {
|
|
1090
|
-
let delivered = false;
|
|
1091
|
-
let resolveHeartbeat;
|
|
1092
|
-
const heartbeatGate = new Promise((resolve) => { resolveHeartbeat = resolve; });
|
|
1093
|
-
let releaseDelete;
|
|
1094
|
-
const deleteGate = new Promise((resolve) => { releaseDelete = resolve; });
|
|
1095
|
-
const calls = [];
|
|
1096
|
-
const sqs = {
|
|
1097
|
-
receiveMessage: async ({ signal, waitTimeSeconds }) => {
|
|
1098
|
-
if (!delivered) {
|
|
1099
|
-
delivered = true;
|
|
1100
|
-
return { messages: [msg(makeEvent({ sequenceNumber: 44 }), "rh-failed-delete")] };
|
|
1101
|
-
}
|
|
1102
|
-
if (signal.aborted || waitTimeSeconds <= 1)
|
|
1103
|
-
return { messages: [] };
|
|
1104
|
-
return new Promise((resolve) => {
|
|
1105
|
-
signal.addEventListener("abort", () => resolve({ messages: [] }), { once: true });
|
|
1106
|
-
});
|
|
1107
|
-
},
|
|
1108
|
-
deleteMessage: async () => {
|
|
1109
|
-
calls.push("delete-start");
|
|
1110
|
-
await deleteGate;
|
|
1111
|
-
calls.push("delete-failed");
|
|
1112
|
-
throw new Error("simulated delete failure");
|
|
1113
|
-
},
|
|
1114
|
-
changeMessageVisibility: async ({ visibilityTimeoutSeconds }) => {
|
|
1115
|
-
if (visibilityTimeoutSeconds === 0) {
|
|
1116
|
-
calls.push("release");
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
calls.push("heartbeat-start");
|
|
1120
|
-
await heartbeatGate;
|
|
1121
|
-
calls.push("heartbeat-end");
|
|
1122
|
-
},
|
|
1123
|
-
changeMessageVisibilityBatch: async ({ entries, visibilityTimeoutSeconds }) => {
|
|
1124
|
-
for (const _entry of entries) {
|
|
1125
|
-
if (visibilityTimeoutSeconds === 0) {
|
|
1126
|
-
calls.push("release");
|
|
1127
|
-
}
|
|
1128
|
-
else {
|
|
1129
|
-
calls.push("heartbeat-start");
|
|
1130
|
-
await heartbeatGate;
|
|
1131
|
-
calls.push("heartbeat-end");
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
return { failedEntries: [] };
|
|
1135
|
-
},
|
|
1136
|
-
};
|
|
1137
|
-
const receiver = new SqsPushReceiver({
|
|
1138
|
-
tenantId: TENANT,
|
|
1139
|
-
queueUrl: QUEUE_URL,
|
|
1140
|
-
sqs,
|
|
1141
|
-
visibilityHeartbeatIntervalMs: 5,
|
|
1142
|
-
syncBatchFn: async () => undefined,
|
|
1143
|
-
sleep: fastSleep,
|
|
1144
|
-
publishMetric: async () => undefined,
|
|
1145
|
-
});
|
|
1146
|
-
await receiver.start();
|
|
1147
|
-
try {
|
|
1148
|
-
await waitFor(() => calls.includes("delete-start"));
|
|
1149
|
-
await waitFor(() => calls.includes("heartbeat-start"));
|
|
1150
|
-
releaseDelete();
|
|
1151
|
-
await waitFor(() => calls.includes("delete-failed"));
|
|
1152
|
-
expect(calls).toEqual(["delete-start", "heartbeat-start", "delete-failed"]);
|
|
1153
|
-
resolveHeartbeat();
|
|
1154
|
-
await waitFor(() => calls.includes("release"));
|
|
1155
|
-
expect(calls).toEqual([
|
|
1156
|
-
"delete-start",
|
|
1157
|
-
"heartbeat-start",
|
|
1158
|
-
"delete-failed",
|
|
1159
|
-
"heartbeat-end",
|
|
1160
|
-
"release",
|
|
1161
|
-
]);
|
|
1162
|
-
}
|
|
1163
|
-
finally {
|
|
1164
|
-
releaseDelete();
|
|
1165
|
-
resolveHeartbeat();
|
|
1166
|
-
await receiver.dispose();
|
|
1167
|
-
}
|
|
1168
|
-
});
|
|
1169
|
-
it("REGRESSION: drains a 5,000-message FIFO backlog through bounded slow passes without lease-expiry redelivery", async () => {
|
|
1170
|
-
const total = 5_000;
|
|
1171
|
-
const sqs = new BacklogSqs(Array.from({ length: total }, (_, index) => msg(makeEvent({
|
|
1172
|
-
relativePath: `companies/indigo/backlog/${index}.md`,
|
|
1173
|
-
sequenceNumber: index + 1,
|
|
1174
|
-
}), `rh-backlog-${index + 1}`)));
|
|
1175
|
-
const drains = [];
|
|
1176
|
-
let activePasses = 0;
|
|
1177
|
-
let maxActivePasses = 0;
|
|
1178
|
-
const startedAt = Date.now();
|
|
1179
|
-
const receiver = new SqsPushReceiver({
|
|
1180
|
-
tenantId: TENANT,
|
|
1181
|
-
queueUrl: QUEUE_URL,
|
|
1182
|
-
sqs,
|
|
1183
|
-
batchMaxPaths: 256,
|
|
1184
|
-
drainMaxMessages: 256,
|
|
1185
|
-
maxInFlightDrains: 2,
|
|
1186
|
-
syncBatchFn: async () => {
|
|
1187
|
-
activePasses += 1;
|
|
1188
|
-
maxActivePasses = Math.max(maxActivePasses, activePasses);
|
|
1189
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1190
|
-
activePasses -= 1;
|
|
1191
|
-
},
|
|
1192
|
-
sleep: fastSleep,
|
|
1193
|
-
publishMetric: async () => undefined,
|
|
1194
|
-
logger: {
|
|
1195
|
-
debug: () => undefined,
|
|
1196
|
-
warn: () => undefined,
|
|
1197
|
-
error: () => undefined,
|
|
1198
|
-
info: (record) => {
|
|
1199
|
-
if (record.event === "receiver.drain")
|
|
1200
|
-
drains.push(record);
|
|
1201
|
-
},
|
|
1202
|
-
},
|
|
1203
|
-
});
|
|
1204
|
-
await receiver.start();
|
|
1205
|
-
await waitFor(() => sqs.deleted.length === total, 10_000);
|
|
1206
|
-
const elapsedMs = Date.now() - startedAt;
|
|
1207
|
-
await receiver.dispose();
|
|
1208
|
-
const throughputPerMinute = total / elapsedMs * 60_000;
|
|
1209
|
-
expect(throughputPerMinute).toBeGreaterThanOrEqual(300);
|
|
1210
|
-
expect(maxActivePasses).toBe(2);
|
|
1211
|
-
expect(sqs.received).toHaveLength(total);
|
|
1212
|
-
expect(sqs.deleted).toHaveLength(total);
|
|
1213
|
-
expect(sqs.released).toEqual([]);
|
|
1214
|
-
expect(sqs.remaining).toBe(0);
|
|
1215
|
-
expect(sqs.deleted[0]).toBe("rh-backlog-1");
|
|
1216
|
-
expect(drains).toHaveLength(Math.ceil(total / 256));
|
|
1217
|
-
expect(drains).toEqual(expect.arrayContaining([
|
|
1218
|
-
expect.objectContaining({ remaining: 0 }),
|
|
1219
|
-
]));
|
|
1220
|
-
}, 15_000);
|
|
1221
|
-
it("stops a capped distinct-path drain before receiving and releasing more messages", async () => {
|
|
1222
|
-
const first = [
|
|
1223
|
-
msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 1 }), "rh-a"),
|
|
1224
|
-
msg(makeEvent({ relativePath: "companies/indigo/b.md", sequenceNumber: 2 }), "rh-b"),
|
|
1225
|
-
msg(makeEvent({ relativePath: "companies/indigo/backlog.md", sequenceNumber: 3 }), "rh-backlog"),
|
|
1226
|
-
];
|
|
1227
|
-
const sqs = new OverCapSqs(first);
|
|
1228
|
-
const batches = [];
|
|
1229
|
-
const fresh = msg(makeEvent({ relativePath: "companies/indigo/fresh.md", sequenceNumber: 4 }), "rh-fresh");
|
|
1230
|
-
const receiver = new SqsPushReceiver({
|
|
1231
|
-
tenantId: TENANT,
|
|
1232
|
-
queueUrl: QUEUE_URL,
|
|
1233
|
-
sqs,
|
|
1234
|
-
batchMaxPaths: 2,
|
|
1235
|
-
drainMaxPaths: 2,
|
|
1236
|
-
drainMaxMessages: 3,
|
|
1237
|
-
syncBatchFn: async ({ events }) => {
|
|
1238
|
-
batches.push(events.map((event) => event.relativePath));
|
|
1239
|
-
if (batches.length === 1)
|
|
1240
|
-
sqs.enqueue(fresh);
|
|
1241
|
-
},
|
|
1242
|
-
sleep: fastSleep,
|
|
1243
|
-
publishMetric: async () => undefined,
|
|
1244
|
-
});
|
|
1245
|
-
await receiver.start();
|
|
1246
|
-
await waitFor(() => batches.some((batch) => batch.includes("companies/indigo/fresh.md")));
|
|
1247
|
-
await receiver.dispose();
|
|
1248
|
-
expect(sqs.receiveCalls[0]).toEqual({ waitTimeSeconds: 20, maxMessages: 2 });
|
|
1249
|
-
expect(sqs.receiveCalls.filter((call) => call.waitTimeSeconds === 0)).toEqual([]);
|
|
1250
|
-
expect(sqs.releases).toEqual([]);
|
|
1251
|
-
expect(sqs.deleted).toHaveLength(4);
|
|
1252
|
-
expect(batches[0]).toEqual([
|
|
1253
|
-
"companies/indigo/a.md",
|
|
1254
|
-
"companies/indigo/b.md",
|
|
1255
|
-
]);
|
|
1256
|
-
expect(batches[1]).toContain("companies/indigo/fresh.md");
|
|
1257
|
-
});
|
|
1258
|
-
it("dispatches the collected batch and keeps polling when a confirmation receive fails", async () => {
|
|
1259
|
-
const initial = msg(makeEvent({ sequenceNumber: 1 }), "rh-confirmation-failure");
|
|
1260
|
-
const receiveWaitTimes = [];
|
|
1261
|
-
let receiveCalls = 0;
|
|
1262
|
-
let initialDelivered = false;
|
|
1263
|
-
let confirmationFailed = false;
|
|
1264
|
-
const deleted = [];
|
|
1265
|
-
const warnings = [];
|
|
1266
|
-
const sqs = {
|
|
1267
|
-
receiveMessage: async (args) => {
|
|
1268
|
-
receiveCalls += 1;
|
|
1269
|
-
receiveWaitTimes.push(args.waitTimeSeconds);
|
|
1270
|
-
if (!initialDelivered && args.waitTimeSeconds === 20) {
|
|
1271
|
-
initialDelivered = true;
|
|
1272
|
-
return { messages: [initial] };
|
|
1273
|
-
}
|
|
1274
|
-
if (args.waitTimeSeconds === 0)
|
|
1275
|
-
return { messages: [] };
|
|
1276
|
-
if (args.waitTimeSeconds === 1 && !confirmationFailed) {
|
|
1277
|
-
confirmationFailed = true;
|
|
1278
|
-
throw new Error("transient confirmation receive failure");
|
|
1279
|
-
}
|
|
1280
|
-
return new Promise((resolve) => {
|
|
1281
|
-
if (args.signal.aborted)
|
|
1282
|
-
return resolve({ messages: [] });
|
|
1283
|
-
args.signal.addEventListener("abort", () => resolve({ messages: [] }), { once: true });
|
|
1284
|
-
});
|
|
1285
|
-
},
|
|
1286
|
-
deleteMessage: async ({ receiptHandle }) => { deleted.push(receiptHandle); },
|
|
1287
|
-
changeMessageVisibility: async () => { },
|
|
1288
|
-
changeMessageVisibilityBatch: async () => ({ failedEntries: [] }),
|
|
1289
|
-
};
|
|
1290
|
-
const batches = [];
|
|
1291
|
-
const receiver = new SqsPushReceiver({
|
|
1292
|
-
tenantId: TENANT,
|
|
1293
|
-
queueUrl: QUEUE_URL,
|
|
1294
|
-
sqs,
|
|
1295
|
-
syncBatchFn: async (ctx) => { batches.push(ctx); },
|
|
1296
|
-
sleep: fastSleep,
|
|
1297
|
-
publishMetric: async () => undefined,
|
|
1298
|
-
reconnect: { initialMs: 1, maxMs: 4, jitter: false },
|
|
1299
|
-
logger: {
|
|
1300
|
-
info: () => undefined,
|
|
1301
|
-
warn: (record) => { warnings.push(record); },
|
|
1302
|
-
error: () => undefined,
|
|
1303
|
-
debug: () => undefined,
|
|
1304
|
-
},
|
|
1305
|
-
});
|
|
1306
|
-
await receiver.start();
|
|
1307
|
-
await waitFor(() => batches.length === 1 && deleted.length === 1 && receiveCalls >= 4);
|
|
1308
|
-
await receiver.dispose();
|
|
1309
|
-
expect(batches).toHaveLength(1);
|
|
1310
|
-
expect(batches[0].events).toHaveLength(1);
|
|
1311
|
-
expect(deleted).toEqual(["rh-confirmation-failure"]);
|
|
1312
|
-
expect(receiveWaitTimes.slice(0, 4)).toEqual([20, 0, 1, 20]);
|
|
1313
|
-
expect(receiver.receiveErrorCount).toBe(1);
|
|
1314
|
-
expect(warnings).toContainEqual(expect.objectContaining({
|
|
1315
|
-
event: "receiver.receive.failed",
|
|
1316
|
-
tenantId: TENANT,
|
|
1317
|
-
err: { message: "transient confirmation receive failure" },
|
|
1318
|
-
}));
|
|
1319
|
-
});
|
|
1320
|
-
it("keeps only the highest sequence number for each path within a coalesced pass", async () => {
|
|
1321
|
-
const sqs = new FakeSqs();
|
|
1322
|
-
const batches = [];
|
|
1323
|
-
sqs.enqueue([
|
|
1324
|
-
msg(makeEvent({ sequenceNumber: 1 }), "rh-seq-1"),
|
|
1325
|
-
msg(makeEvent({ sequenceNumber: 3 }), "rh-seq-3"),
|
|
1326
|
-
msg(makeEvent({ sequenceNumber: 2 }), "rh-seq-2"),
|
|
1327
|
-
]);
|
|
1328
|
-
const receiver = new SqsPushReceiver({
|
|
1329
|
-
tenantId: TENANT,
|
|
1330
|
-
queueUrl: QUEUE_URL,
|
|
1331
|
-
sqs,
|
|
1332
|
-
syncBatchFn: async (ctx) => { batches.push(ctx); },
|
|
1333
|
-
sleep: fastSleep,
|
|
1334
|
-
publishMetric: async () => undefined,
|
|
1335
|
-
});
|
|
1336
|
-
await receiver.start();
|
|
1337
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
1338
|
-
await receiver.dispose();
|
|
1339
|
-
expect(batches).toHaveLength(1);
|
|
1340
|
-
expect(batches[0].events).toHaveLength(1);
|
|
1341
|
-
expect(batches[0].events[0].sequenceNumber).toBe(3);
|
|
1342
|
-
expect(sqs.deleted).toEqual(expect.arrayContaining(["rh-seq-1", "rh-seq-2", "rh-seq-3"]));
|
|
1343
|
-
});
|
|
1344
|
-
it("REGRESSION: coalesces create-then-delete receipts across receive pages before bounded pulls", async () => {
|
|
1345
|
-
const pathCount = 256;
|
|
1346
|
-
const creates = Array.from({ length: pathCount }, (_, index) => msg(makeEvent({
|
|
1347
|
-
relativePath: `companies/indigo/dead-burst/${index}.md`,
|
|
1348
|
-
sequenceNumber: index + 1,
|
|
1349
|
-
kind: "upsert",
|
|
1350
|
-
}), `rh-create-${index}`));
|
|
1351
|
-
const deletes = Array.from({ length: pathCount }, (_, index) => msg(makeEvent({
|
|
1352
|
-
relativePath: `companies/indigo/dead-burst/${index}.md`,
|
|
1353
|
-
sequenceNumber: pathCount + index + 1,
|
|
1354
|
-
kind: "delete",
|
|
1355
|
-
}), `rh-delete-${index}`));
|
|
1356
|
-
const sqs = new FakeSqs();
|
|
1357
|
-
sqs.enqueueDrainable([...creates, ...deletes]);
|
|
1358
|
-
const batches = [];
|
|
1359
|
-
const receiver = new SqsPushReceiver({
|
|
1360
|
-
tenantId: TENANT,
|
|
1361
|
-
queueUrl: QUEUE_URL,
|
|
1362
|
-
sqs,
|
|
1363
|
-
batchMaxPaths: 64,
|
|
1364
|
-
drainMaxPaths: pathCount * 2,
|
|
1365
|
-
drainMaxMessages: pathCount * 2,
|
|
1366
|
-
syncBatchFn: async ({ events }) => { batches.push([...events]); },
|
|
1367
|
-
sleep: fastSleep,
|
|
1368
|
-
publishMetric: async () => undefined,
|
|
1369
|
-
});
|
|
1370
|
-
await receiver.start();
|
|
1371
|
-
await waitFor(() => receiver.processedCount === pathCount);
|
|
1372
|
-
await receiver.dispose();
|
|
1373
|
-
expect(batches).toHaveLength(4);
|
|
1374
|
-
expect(batches.flat()).toHaveLength(pathCount);
|
|
1375
|
-
expect(batches.every((batch) => batch.length <= 64)).toBe(true);
|
|
1376
|
-
expect(batches.flat().every((event) => event.kind === "delete")).toBe(true);
|
|
1377
|
-
expect(sqs.deleted).toHaveLength(pathCount * 2);
|
|
1378
|
-
});
|
|
1379
|
-
it("keeps a newer create after delete and the latest repeated overwrite", async () => {
|
|
1380
|
-
const sqs = new FakeSqs();
|
|
1381
|
-
const earlierDelete = makeEvent({
|
|
1382
|
-
relativePath: "companies/indigo/revived.md",
|
|
1383
|
-
sequenceNumber: 10,
|
|
1384
|
-
kind: "delete",
|
|
1385
|
-
});
|
|
1386
|
-
const latestCreate = makeEvent({
|
|
1387
|
-
relativePath: "companies/indigo/revived.md",
|
|
1388
|
-
sequenceNumber: 11,
|
|
1389
|
-
kind: "upsert",
|
|
1390
|
-
contentHash: `sha256:${"b".repeat(64)}`,
|
|
1391
|
-
});
|
|
1392
|
-
const firstOverwrite = makeEvent({
|
|
1393
|
-
relativePath: "companies/indigo/overwritten.md",
|
|
1394
|
-
sequenceNumber: 12,
|
|
1395
|
-
kind: "upsert",
|
|
1396
|
-
contentHash: `sha256:${"c".repeat(64)}`,
|
|
1397
|
-
});
|
|
1398
|
-
const latestOverwrite = makeEvent({
|
|
1399
|
-
relativePath: "companies/indigo/overwritten.md",
|
|
1400
|
-
sequenceNumber: 13,
|
|
1401
|
-
kind: "upsert",
|
|
1402
|
-
contentHash: `sha256:${"d".repeat(64)}`,
|
|
1403
|
-
});
|
|
1404
|
-
sqs.enqueue([
|
|
1405
|
-
msg(earlierDelete, "rh-revived-delete"),
|
|
1406
|
-
msg(latestCreate, "rh-revived-create"),
|
|
1407
|
-
msg(firstOverwrite, "rh-overwrite-1"),
|
|
1408
|
-
msg(latestOverwrite, "rh-overwrite-2"),
|
|
1409
|
-
]);
|
|
1410
|
-
const batches = [];
|
|
1411
|
-
const receiver = new SqsPushReceiver({
|
|
1412
|
-
tenantId: TENANT,
|
|
1413
|
-
queueUrl: QUEUE_URL,
|
|
1414
|
-
sqs,
|
|
1415
|
-
syncBatchFn: async (context) => { batches.push(context); },
|
|
1416
|
-
sleep: fastSleep,
|
|
1417
|
-
publishMetric: async () => undefined,
|
|
1418
|
-
});
|
|
1419
|
-
await receiver.start();
|
|
1420
|
-
await waitFor(() => receiver.processedCount === 2);
|
|
1421
|
-
await receiver.dispose();
|
|
1422
|
-
expect(batches).toHaveLength(1);
|
|
1423
|
-
expect(batches[0].events).toEqual([latestCreate, latestOverwrite]);
|
|
1424
|
-
expect(sqs.deleted).toHaveLength(4);
|
|
1425
|
-
});
|
|
1426
|
-
it("splits a backlog above the distinct-path bound into bounded sequential passes", async () => {
|
|
1427
|
-
const sqs = new FakeSqs();
|
|
1428
|
-
const batches = [];
|
|
1429
|
-
sqs.enqueue(Array.from({ length: 5 }, (_, index) => msg(makeEvent({ relativePath: `companies/indigo/${index}.md`, sequenceNumber: index + 1 }), `rh-limit-${index}`)));
|
|
1430
|
-
const receiver = new SqsPushReceiver({
|
|
1431
|
-
tenantId: TENANT,
|
|
1432
|
-
queueUrl: QUEUE_URL,
|
|
1433
|
-
sqs,
|
|
1434
|
-
batchMaxPaths: 3,
|
|
1435
|
-
syncBatchFn: async ({ events }) => { batches.push(events.length); },
|
|
1436
|
-
sleep: fastSleep,
|
|
1437
|
-
publishMetric: async () => undefined,
|
|
1438
|
-
});
|
|
1439
|
-
await receiver.start();
|
|
1440
|
-
await waitFor(() => receiver.processedCount === 5);
|
|
1441
|
-
await receiver.dispose();
|
|
1442
|
-
expect(batches).toEqual([3, 2]);
|
|
1443
|
-
expect(batches.every((count) => count <= 3)).toBe(true);
|
|
1444
|
-
});
|
|
1445
|
-
it("dispatches each bounded partition array itself so the runner keeps its lane", async () => {
|
|
1446
|
-
// The runner keys each partition array's fast/slow lane in a WeakMap.
|
|
1447
|
-
// Copying an array that already fits the bound loses that lane and gives
|
|
1448
|
-
// every chunk a fresh fast-lane budget.
|
|
1449
|
-
const sqs = new FakeSqs();
|
|
1450
|
-
sqs.enqueue(Array.from({ length: 5 }, (_, index) => msg(makeEvent({ relativePath: `companies/indigo/${index}.md`, sequenceNumber: index + 1 }), `rh-lane-${index}`)));
|
|
1451
|
-
const returned = [];
|
|
1452
|
-
const dispatched = [];
|
|
1453
|
-
const syncBatchFn = async ({ events }) => { dispatched.push(events); };
|
|
1454
|
-
syncBatchFn.partition = (events, maxPaths) => {
|
|
1455
|
-
const bound = maxPaths ?? events.length;
|
|
1456
|
-
const chunks = [];
|
|
1457
|
-
for (let offset = 0; offset < events.length; offset += bound) {
|
|
1458
|
-
chunks.push(events.slice(offset, offset + bound));
|
|
1459
|
-
}
|
|
1460
|
-
returned.push(...chunks);
|
|
1461
|
-
return chunks;
|
|
1462
|
-
};
|
|
1463
|
-
const receiver = new SqsPushReceiver({
|
|
1464
|
-
tenantId: TENANT,
|
|
1465
|
-
queueUrl: QUEUE_URL,
|
|
1466
|
-
sqs,
|
|
1467
|
-
batchMaxPaths: 3,
|
|
1468
|
-
syncBatchFn,
|
|
1469
|
-
sleep: fastSleep,
|
|
1470
|
-
publishMetric: async () => undefined,
|
|
1471
|
-
});
|
|
1472
|
-
await receiver.start();
|
|
1473
|
-
await waitFor(() => receiver.processedCount === 5);
|
|
1474
|
-
await receiver.dispose();
|
|
1475
|
-
expect(dispatched.flat().map((event) => event.sequenceNumber)).toEqual([1, 2, 3, 4, 5]);
|
|
1476
|
-
expect(dispatched.every((events) => events.length <= 3)).toBe(true);
|
|
1477
|
-
expect(dispatched).toHaveLength(returned.length);
|
|
1478
|
-
dispatched.forEach((events, index) => expect(events).toBe(returned[index]));
|
|
1479
|
-
});
|
|
1480
|
-
it("acknowledges receipts per successful lane and retains failed slow residue", async () => {
|
|
1481
|
-
const sqs = new FakeSqs();
|
|
1482
|
-
const fast = makeEvent({ relativePath: "companies/indigo/fast.md", sequenceNumber: 1 });
|
|
1483
|
-
const slow = makeEvent({ relativePath: "companies/indigo/slow.md", sequenceNumber: 2 });
|
|
1484
|
-
sqs.enqueue([msg(fast, "rh-fast"), msg(slow, "rh-slow")]);
|
|
1485
|
-
sqs.enqueue([msg(slow, "rh-slow-redelivered")]);
|
|
1486
|
-
const dispatched = [];
|
|
1487
|
-
let slowAttempts = 0;
|
|
1488
|
-
const syncBatchFn = async ({ events }) => {
|
|
1489
|
-
const relativePath = events[0].relativePath;
|
|
1490
|
-
dispatched.push(relativePath);
|
|
1491
|
-
if (relativePath === slow.relativePath && slowAttempts++ === 0) {
|
|
1492
|
-
throw new Error("slow lane failed");
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
syncBatchFn.partition = (events) => [[events[0]], [events[1]]];
|
|
1496
|
-
const receiver = new SqsPushReceiver({
|
|
1497
|
-
tenantId: TENANT,
|
|
1498
|
-
queueUrl: QUEUE_URL,
|
|
1499
|
-
sqs,
|
|
1500
|
-
syncBatchFn,
|
|
1501
|
-
sleep: fastSleep,
|
|
1502
|
-
publishMetric: async () => undefined,
|
|
1503
|
-
});
|
|
1504
|
-
await receiver.start();
|
|
1505
|
-
await waitFor(() => receiver.processedCount === 2);
|
|
1506
|
-
await receiver.dispose();
|
|
1507
|
-
expect(sqs.deleted).toContain("rh-fast");
|
|
1508
|
-
expect(sqs.deleted).not.toContain("rh-slow");
|
|
1509
|
-
expect(sqs.deleted).toContain("rh-slow-redelivered");
|
|
1510
|
-
// Regression: a slow failure must redeliver only slow. Replaying fast
|
|
1511
|
-
// here would pull it again even though its receipt was already durable.
|
|
1512
|
-
expect(dispatched).toEqual([fast.relativePath, slow.relativePath, slow.relativePath]);
|
|
1513
|
-
});
|
|
1514
|
-
it("deletes a self echo immediately and performs zero sync passes", async () => {
|
|
1515
|
-
const sqs = new FakeSqs();
|
|
1516
|
-
const syncBatchFn = vi.fn(async () => undefined);
|
|
1517
|
-
sqs.enqueue([msg(makeEvent({ originDeviceId: "this-device" }), "rh-self")]);
|
|
1518
|
-
const receiver = new SqsPushReceiver({
|
|
1519
|
-
tenantId: TENANT,
|
|
1520
|
-
queueUrl: QUEUE_URL,
|
|
1521
|
-
sqs,
|
|
1522
|
-
shouldProcess: (event) => event.originDeviceId !== "this-device",
|
|
1523
|
-
syncBatchFn,
|
|
1524
|
-
sleep: fastSleep,
|
|
1525
|
-
publishMetric: async () => undefined,
|
|
1526
|
-
});
|
|
1527
|
-
await receiver.start();
|
|
1528
|
-
await waitFor(() => sqs.deleted.includes("rh-self"));
|
|
1529
|
-
await receiver.dispose();
|
|
1530
|
-
expect(syncBatchFn).not.toHaveBeenCalled();
|
|
1531
|
-
expect(receiver.processedCount).toBe(0);
|
|
1532
|
-
});
|
|
1533
|
-
it("deletes filtered retired-overlay events while dispatching a valid absent path", async () => {
|
|
1534
|
-
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-push-receiver-"));
|
|
1535
|
-
const personalPath = path.join(hqRoot, "personal/policies/legacy-rule.md");
|
|
1536
|
-
fs.mkdirSync(path.dirname(personalPath), { recursive: true });
|
|
1537
|
-
fs.writeFileSync(personalPath, "fixture\n");
|
|
1538
|
-
const sqs = new FakeSqs();
|
|
1539
|
-
const syncBatchFn = vi.fn(async () => undefined);
|
|
1540
|
-
const filtered = vi.fn();
|
|
1541
|
-
const valid = makeEvent({
|
|
1542
|
-
relativePath: "personal/policies/new-on-this-device.md",
|
|
1543
|
-
sequenceNumber: 1,
|
|
1544
|
-
});
|
|
1545
|
-
const retiredUpsert = makeEvent({
|
|
1546
|
-
relativePath: "core/policies/legacy-rule.md",
|
|
1547
|
-
contentHash: `sha256:${hashFile(personalPath)}`,
|
|
1548
|
-
sequenceNumber: 2,
|
|
1549
|
-
});
|
|
1550
|
-
const retiredDelete = makeEvent({
|
|
1551
|
-
relativePath: "core/policies/legacy-rule.md",
|
|
1552
|
-
kind: "delete",
|
|
1553
|
-
contentHash: undefined,
|
|
1554
|
-
mtime: undefined,
|
|
1555
|
-
sequenceNumber: 3,
|
|
1556
|
-
});
|
|
1557
|
-
sqs.enqueue([msg(valid, "rh-valid"), msg(retiredUpsert, "rh-retired-upsert"), msg(retiredDelete, "rh-retired-delete")]);
|
|
1558
|
-
const receiver = new SqsPushReceiver({
|
|
1559
|
-
tenantId: TENANT,
|
|
1560
|
-
queueUrl: QUEUE_URL,
|
|
1561
|
-
sqs,
|
|
1562
|
-
shouldProcess: (event) => isInboundEventPlaceable(hqRoot, event),
|
|
1563
|
-
onFilteredEvents: filtered,
|
|
1564
|
-
syncBatchFn,
|
|
1565
|
-
sleep: fastSleep,
|
|
1566
|
-
publishMetric: async () => undefined,
|
|
1567
|
-
});
|
|
1568
|
-
try {
|
|
1569
|
-
await receiver.start();
|
|
1570
|
-
await waitFor(() => syncBatchFn.mock.calls.length === 1);
|
|
1571
|
-
await waitFor(() => sqs.deleted.includes("rh-retired-delete"));
|
|
1572
|
-
await receiver.dispose();
|
|
1573
|
-
expect(syncBatchFn).toHaveBeenCalledWith(expect.objectContaining({
|
|
1574
|
-
events: [valid],
|
|
1575
|
-
}));
|
|
1576
|
-
expect(sqs.deleted).toEqual(expect.arrayContaining(["rh-valid", "rh-retired-upsert", "rh-retired-delete"]));
|
|
1577
|
-
expect(filtered).toHaveBeenCalledWith([retiredUpsert, retiredDelete]);
|
|
1578
|
-
}
|
|
1579
|
-
finally {
|
|
1580
|
-
await receiver.dispose();
|
|
1581
|
-
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
1582
|
-
}
|
|
1583
|
-
});
|
|
1584
|
-
it("skips an event whose sequenceNumber <= the highest seen for that path", async () => {
|
|
1585
|
-
const sqs = new FakeSqs();
|
|
1586
|
-
const processed = [];
|
|
1587
|
-
const syncFn = async (ctx) => {
|
|
1588
|
-
processed.push(ctx.event.sequenceNumber);
|
|
1589
|
-
};
|
|
1590
|
-
// seq 1, then a redelivery of 1, then 2 — same path.
|
|
1591
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-a")]);
|
|
1592
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-b")]);
|
|
1593
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 2 }), "rh-c")]);
|
|
1594
|
-
const receiver = new SqsPushReceiver({
|
|
1595
|
-
tenantId: TENANT,
|
|
1596
|
-
queueUrl: QUEUE_URL,
|
|
1597
|
-
sqs,
|
|
1598
|
-
syncFn,
|
|
1599
|
-
sleep: fastSleep,
|
|
1600
|
-
});
|
|
1601
|
-
await receiver.start();
|
|
1602
|
-
await waitFor(() => receiver.processedCount === 2);
|
|
1603
|
-
await receiver.dispose();
|
|
1604
|
-
expect(processed).toEqual([1, 2]);
|
|
1605
|
-
expect(receiver.dedupedCount).toBe(1);
|
|
1606
|
-
// The deduped redelivery is still deleted (we don't need it again).
|
|
1607
|
-
expect(sqs.deleted).toContain("rh-b");
|
|
1608
|
-
});
|
|
1609
|
-
it("dedupe is per-path — same sequence number on different paths both process", async () => {
|
|
1610
|
-
const sqs = new FakeSqs();
|
|
1611
|
-
const processed = [];
|
|
1612
|
-
const syncFn = async (ctx) => {
|
|
1613
|
-
processed.push(ctx.event.relativePath);
|
|
1614
|
-
};
|
|
1615
|
-
sqs.enqueue([
|
|
1616
|
-
msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 5 }), "rh-1"),
|
|
1617
|
-
msg(makeEvent({ relativePath: "companies/indigo/b.md", sequenceNumber: 5 }), "rh-2"),
|
|
1618
|
-
]);
|
|
1619
|
-
const receiver = new SqsPushReceiver({
|
|
1620
|
-
tenantId: TENANT,
|
|
1621
|
-
queueUrl: QUEUE_URL,
|
|
1622
|
-
sqs,
|
|
1623
|
-
syncFn,
|
|
1624
|
-
sleep: fastSleep,
|
|
1625
|
-
});
|
|
1626
|
-
await receiver.start();
|
|
1627
|
-
await waitFor(() => receiver.processedCount === 2);
|
|
1628
|
-
await receiver.dispose();
|
|
1629
|
-
expect(new Set(processed)).toEqual(new Set(["companies/indigo/a.md", "companies/indigo/b.md"]));
|
|
1630
|
-
expect(receiver.dedupedCount).toBe(0);
|
|
1631
|
-
});
|
|
1632
|
-
it("drops an undecodable message at the wire boundary without crashing the loop", async () => {
|
|
1633
|
-
const sqs = new FakeSqs();
|
|
1634
|
-
sqs.enqueue([{ Body: "{not json", ReceiptHandle: "rh-bad", MessageId: "m-bad" }]);
|
|
1635
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-good")]);
|
|
1636
|
-
const receiver = new SqsPushReceiver({
|
|
1637
|
-
tenantId: TENANT,
|
|
1638
|
-
queueUrl: QUEUE_URL,
|
|
1639
|
-
sqs,
|
|
1640
|
-
syncFn: async () => undefined,
|
|
1641
|
-
sleep: fastSleep,
|
|
1642
|
-
});
|
|
1643
|
-
await receiver.start();
|
|
1644
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
1645
|
-
await receiver.dispose();
|
|
1646
|
-
expect(receiver.decodeFailureCount).toBe(1);
|
|
1647
|
-
expect(sqs.deleted).toContain("rh-bad"); // poison message deleted
|
|
1648
|
-
expect(sqs.deleted).toContain("rh-good");
|
|
1649
|
-
});
|
|
1650
|
-
});
|
|
1651
|
-
describe("US-009: SqsPushReceiver — reconnect / catch-up replay", () => {
|
|
1652
|
-
it("leaves only the failed path unacknowledged and retries it", async () => {
|
|
1653
|
-
const sqs = new FakeSqs();
|
|
1654
|
-
const events = [
|
|
1655
|
-
makeEvent({ relativePath: "companies/indigo/retry-a.md", sequenceNumber: 1 }),
|
|
1656
|
-
makeEvent({ relativePath: "companies/indigo/retry-b.md", sequenceNumber: 1 }),
|
|
1657
|
-
makeEvent({ relativePath: "companies/indigo/retry-c.md", sequenceNumber: 1 }),
|
|
1658
|
-
];
|
|
1659
|
-
sqs.enqueue(events.map((event, index) => msg(event, `rh-failed-${index}`)));
|
|
1660
|
-
sqs.enqueue([msg(events[0], "rh-redelivered-a")]);
|
|
1661
|
-
let batchCalls = 0;
|
|
1662
|
-
let retryAPathCalls = 0;
|
|
1663
|
-
const receiver = new SqsPushReceiver({
|
|
1664
|
-
tenantId: TENANT,
|
|
1665
|
-
queueUrl: QUEUE_URL,
|
|
1666
|
-
sqs,
|
|
1667
|
-
maxInFlightDrains: 1,
|
|
1668
|
-
syncBatchFn: async ({ events: batch }) => {
|
|
1669
|
-
if (batch.length > 1) {
|
|
1670
|
-
batchCalls += 1;
|
|
1671
|
-
throw new Error("coalesced pull failed for one path");
|
|
1672
|
-
}
|
|
1673
|
-
if (batch[0]?.relativePath === events[0].relativePath) {
|
|
1674
|
-
retryAPathCalls += 1;
|
|
1675
|
-
if (retryAPathCalls === 1)
|
|
1676
|
-
throw new Error("retry-a is temporarily unavailable");
|
|
1677
|
-
}
|
|
1678
|
-
},
|
|
1679
|
-
sleep: fastSleep,
|
|
1680
|
-
publishMetric: async () => undefined,
|
|
1681
|
-
});
|
|
1682
|
-
await receiver.start();
|
|
1683
|
-
await waitFor(() => receiver.processedCount === 3 && sqs.deleted.includes("rh-redelivered-a"));
|
|
1684
|
-
await receiver.dispose();
|
|
1685
|
-
expect(batchCalls).toBe(2);
|
|
1686
|
-
expect(retryAPathCalls).toBe(2);
|
|
1687
|
-
expect(sqs.deleted).not.toContain("rh-failed-0");
|
|
1688
|
-
expect(sqs.deleted).toEqual(expect.arrayContaining(["rh-failed-1", "rh-failed-2", "rh-redelivered-a"]));
|
|
1689
|
-
expect(sqs.visibilityChanges).toEqual([
|
|
1690
|
-
{ receiptHandle: "rh-failed-0", visibilityTimeoutSeconds: 20 },
|
|
1691
|
-
]);
|
|
1692
|
-
});
|
|
1693
|
-
it("recovers from transient receiveMessage failures (backoff + resume) and replays the queue", async () => {
|
|
1694
|
-
const sqs = new FakeSqs();
|
|
1695
|
-
sqs.failNext(2); // two transient disconnects before the message lands
|
|
1696
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-1")]);
|
|
1697
|
-
const receiver = new SqsPushReceiver({
|
|
1698
|
-
tenantId: TENANT,
|
|
1699
|
-
queueUrl: QUEUE_URL,
|
|
1700
|
-
sqs,
|
|
1701
|
-
syncFn: async () => undefined,
|
|
1702
|
-
sleep: fastSleep,
|
|
1703
|
-
reconnect: { initialMs: 1, maxMs: 4, jitter: false },
|
|
1704
|
-
});
|
|
1705
|
-
await receiver.start();
|
|
1706
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
1707
|
-
await receiver.dispose();
|
|
1708
|
-
expect(receiver.receiveErrorCount).toBe(2);
|
|
1709
|
-
expect(sqs.deleted).toContain("rh-1");
|
|
1710
|
-
});
|
|
1711
|
-
it("a syncFn throw does not crash the loop and the next event still processes", async () => {
|
|
1712
|
-
const sqs = new FakeSqs();
|
|
1713
|
-
let calls = 0;
|
|
1714
|
-
const syncFn = async () => {
|
|
1715
|
-
calls += 1;
|
|
1716
|
-
if (calls === 1)
|
|
1717
|
-
throw new Error("boom");
|
|
1718
|
-
};
|
|
1719
|
-
sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/x.md", sequenceNumber: 1 }), "rh-1")]);
|
|
1720
|
-
sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/y.md", sequenceNumber: 1 }), "rh-2")]);
|
|
1721
|
-
const receiver = new SqsPushReceiver({
|
|
1722
|
-
tenantId: TENANT,
|
|
1723
|
-
queueUrl: QUEUE_URL,
|
|
1724
|
-
sqs,
|
|
1725
|
-
syncFn,
|
|
1726
|
-
sleep: fastSleep,
|
|
1727
|
-
});
|
|
1728
|
-
await receiver.start();
|
|
1729
|
-
await waitFor(() => calls === 2);
|
|
1730
|
-
await receiver.dispose();
|
|
1731
|
-
// First threw (processedCount not incremented), second succeeded.
|
|
1732
|
-
expect(receiver.processedCount).toBe(1);
|
|
1733
|
-
// F10: a failed targeted pull is left undeleted for SQS redelivery; only the
|
|
1734
|
-
// successful message (rh-2) is acknowledged. The thrown one (rh-1) stays on
|
|
1735
|
-
// the queue so the visibility-timeout redelivery can retry it.
|
|
1736
|
-
expect(sqs.deleted).toContain("rh-2");
|
|
1737
|
-
expect(sqs.deleted).not.toContain("rh-1");
|
|
1738
|
-
});
|
|
1739
|
-
it("F10: leaves failed targeted pull messages undeleted so redelivery retries", async () => {
|
|
1740
|
-
const sqs = new FakeSqs();
|
|
1741
|
-
let calls = 0;
|
|
1742
|
-
const syncFn = async () => {
|
|
1743
|
-
calls += 1;
|
|
1744
|
-
if (calls === 1)
|
|
1745
|
-
throw new Error("targeted pull failed");
|
|
1746
|
-
};
|
|
1747
|
-
const event = makeEvent({
|
|
1748
|
-
relativePath: "companies/indigo/retry-after-failure.md",
|
|
1749
|
-
sequenceNumber: 7,
|
|
1750
|
-
});
|
|
1751
|
-
sqs.enqueue([msg(event, "rh-first-attempt")]);
|
|
1752
|
-
sqs.enqueue([msg(event, "rh-redelivery")]);
|
|
1753
|
-
const receiver = new SqsPushReceiver({
|
|
1754
|
-
tenantId: TENANT,
|
|
1755
|
-
queueUrl: QUEUE_URL,
|
|
1756
|
-
sqs,
|
|
1757
|
-
syncFn,
|
|
1758
|
-
sleep: fastSleep,
|
|
1759
|
-
});
|
|
1760
|
-
await receiver.start();
|
|
1761
|
-
await waitFor(() => sqs.deleted.includes("rh-redelivery"));
|
|
1762
|
-
await receiver.dispose();
|
|
1763
|
-
expect(calls).toBe(2);
|
|
1764
|
-
expect(receiver.processedCount).toBe(1);
|
|
1765
|
-
expect(receiver.dedupedCount).toBe(0);
|
|
1766
|
-
expect(sqs.deleted).not.toContain("rh-first-attempt");
|
|
1767
|
-
expect(sqs.deleted).toContain("rh-redelivery");
|
|
1768
|
-
});
|
|
1769
|
-
it("F29: bounds dedupe state by evicting least-recently-used paths", async () => {
|
|
1770
|
-
const sqs = new FakeSqs();
|
|
1771
|
-
const processed = [];
|
|
1772
|
-
const syncFn = async (ctx) => {
|
|
1773
|
-
processed.push(ctx.event.relativePath);
|
|
1774
|
-
};
|
|
1775
|
-
sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 1 }), "rh-a1")]);
|
|
1776
|
-
sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/b.md", sequenceNumber: 1 }), "rh-b1")]);
|
|
1777
|
-
sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/c.md", sequenceNumber: 1 }), "rh-c1")]);
|
|
1778
|
-
sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 1 }), "rh-a-redelivered")]);
|
|
1779
|
-
const receiver = new SqsPushReceiver({
|
|
1780
|
-
tenantId: TENANT,
|
|
1781
|
-
queueUrl: QUEUE_URL,
|
|
1782
|
-
sqs,
|
|
1783
|
-
syncFn,
|
|
1784
|
-
sleep: fastSleep,
|
|
1785
|
-
dedupeMaxPaths: 2,
|
|
1786
|
-
});
|
|
1787
|
-
await receiver.start();
|
|
1788
|
-
await waitFor(() => receiver.processedCount === 4);
|
|
1789
|
-
await receiver.dispose();
|
|
1790
|
-
expect(processed).toEqual([
|
|
1791
|
-
"companies/indigo/a.md",
|
|
1792
|
-
"companies/indigo/b.md",
|
|
1793
|
-
"companies/indigo/c.md",
|
|
1794
|
-
"companies/indigo/a.md",
|
|
1795
|
-
]);
|
|
1796
|
-
expect(receiver.dedupedCount).toBe(0);
|
|
1797
|
-
expect(sqs.deleted).toContain("rh-a-redelivered");
|
|
1798
|
-
});
|
|
1799
|
-
});
|
|
1800
|
-
describe("U15: SqsPushReceiver — server-granted handoff", () => {
|
|
1801
|
-
it("starts from supplied server queue material with no tenant client gate", async () => {
|
|
1802
|
-
const sqs = new FakeSqs();
|
|
1803
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }))]);
|
|
1804
|
-
const receiver = new SqsPushReceiver({
|
|
1805
|
-
tenantId: TENANT,
|
|
1806
|
-
queueUrl: QUEUE_URL,
|
|
1807
|
-
sqs,
|
|
1808
|
-
syncFn: async () => undefined,
|
|
1809
|
-
sleep: fastSleep,
|
|
1810
|
-
});
|
|
1811
|
-
await receiver.start();
|
|
1812
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
1813
|
-
expect(receiver.connected).toBe(true);
|
|
1814
|
-
await receiver.dispose();
|
|
1815
|
-
});
|
|
1816
|
-
});
|
|
1817
|
-
describe("US-009: SqsPushReceiver — dispose drain", () => {
|
|
1818
|
-
it("aborts and drains the in-flight syncFn, then disconnects", async () => {
|
|
1819
|
-
const sqs = new FakeSqs();
|
|
1820
|
-
let aborted = false;
|
|
1821
|
-
let resolveSync = null;
|
|
1822
|
-
const syncFn = (ctx) => new Promise((resolve) => {
|
|
1823
|
-
resolveSync = resolve;
|
|
1824
|
-
ctx.signal.addEventListener("abort", () => {
|
|
1825
|
-
aborted = true;
|
|
1826
|
-
resolve();
|
|
1827
|
-
});
|
|
1828
|
-
});
|
|
1829
|
-
sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }))]);
|
|
1830
|
-
const receiver = new SqsPushReceiver({
|
|
1831
|
-
tenantId: TENANT,
|
|
1832
|
-
queueUrl: QUEUE_URL,
|
|
1833
|
-
sqs,
|
|
1834
|
-
syncFn,
|
|
1835
|
-
sleep: fastSleep,
|
|
1836
|
-
disposeDrainMs: 200,
|
|
1837
|
-
});
|
|
1838
|
-
await receiver.start();
|
|
1839
|
-
await waitFor(() => resolveSync !== null);
|
|
1840
|
-
await receiver.dispose();
|
|
1841
|
-
expect(aborted).toBe(true);
|
|
1842
|
-
expect(receiver.connected).toBe(false);
|
|
1843
|
-
});
|
|
1844
|
-
it("dispose is idempotent", async () => {
|
|
1845
|
-
const sqs = new FakeSqs();
|
|
1846
|
-
const receiver = new SqsPushReceiver({
|
|
1847
|
-
tenantId: TENANT,
|
|
1848
|
-
queueUrl: QUEUE_URL,
|
|
1849
|
-
sqs,
|
|
1850
|
-
syncFn: async () => undefined,
|
|
1851
|
-
sleep: fastSleep,
|
|
1852
|
-
});
|
|
1853
|
-
await receiver.start();
|
|
1854
|
-
await Promise.all([receiver.dispose(), receiver.dispose()]);
|
|
1855
|
-
expect(receiver.connected).toBe(false);
|
|
1856
|
-
});
|
|
1857
|
-
});
|
|
1858
|
-
// ─── InMemoryPushReceiver (transport analogue) ───────────────────────────────
|
|
1859
|
-
describe("US-009: InMemoryPushReceiver — disconnect buffering + reconnect drain", () => {
|
|
1860
|
-
it("buffers events during disconnect and drains them on reconnect, deduping replays", async () => {
|
|
1861
|
-
const fanout = new InMemoryFanout();
|
|
1862
|
-
const processed = [];
|
|
1863
|
-
const receiver = new InMemoryPushReceiver({
|
|
1864
|
-
tenantId: TENANT,
|
|
1865
|
-
fanout,
|
|
1866
|
-
syncFn: async (ctx) => {
|
|
1867
|
-
processed.push(ctx.event.sequenceNumber);
|
|
1868
|
-
},
|
|
1869
|
-
});
|
|
1870
|
-
await receiver.start();
|
|
1871
|
-
fanout.publish(encodePushEvent(makeEvent({ sequenceNumber: 1 })));
|
|
1872
|
-
await waitFor(() => receiver.processedCount === 1);
|
|
1873
|
-
receiver.simulateDisconnect();
|
|
1874
|
-
expect(receiver.connected).toBe(false);
|
|
1875
|
-
// Published while disconnected — buffered, not processed.
|
|
1876
|
-
fanout.publish(encodePushEvent(makeEvent({ sequenceNumber: 2 })));
|
|
1877
|
-
fanout.publish(encodePushEvent(makeEvent({ sequenceNumber: 1 }))); // stale replay
|
|
1878
|
-
expect(receiver.bufferedCount).toBe(2);
|
|
1879
|
-
receiver.simulateReconnect();
|
|
1880
|
-
await waitFor(() => receiver.processedCount === 2);
|
|
1881
|
-
await receiver.dispose();
|
|
1882
|
-
// seq 2 processed; the stale seq-1 replay deduped.
|
|
1883
|
-
expect(processed).toEqual([1, 2]);
|
|
1884
|
-
expect(receiver.dedupedCount).toBe(1);
|
|
1885
|
-
});
|
|
1886
|
-
it("does not derive activation from a tenant flag", async () => {
|
|
1887
|
-
const fanout = new InMemoryFanout();
|
|
1888
|
-
let calls = 0;
|
|
1889
|
-
const receiver = new InMemoryPushReceiver({
|
|
1890
|
-
tenantId: TENANT,
|
|
1891
|
-
fanout,
|
|
1892
|
-
syncFn: async () => {
|
|
1893
|
-
calls += 1;
|
|
1894
|
-
},
|
|
1895
|
-
});
|
|
1896
|
-
await receiver.start();
|
|
1897
|
-
fanout.publish(encodePushEvent(makeEvent()));
|
|
1898
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
1899
|
-
await waitFor(() => calls === 1);
|
|
1900
|
-
expect(receiver.connected).toBe(true);
|
|
1901
|
-
expect(calls).toBe(1);
|
|
1902
|
-
await receiver.dispose();
|
|
1903
|
-
});
|
|
1904
|
-
});
|
|
1905
|
-
// ─── Factory + Noop ──────────────────────────────────────────────────────────
|
|
1906
|
-
describe("US-009: createPushReceiver factory", () => {
|
|
1907
|
-
it("returns NoopPushReceiver for kind:noop", () => {
|
|
1908
|
-
const r = createPushReceiver({ kind: "noop" });
|
|
1909
|
-
expect(r).toBeInstanceOf(NoopPushReceiver);
|
|
1910
|
-
});
|
|
1911
|
-
it("returns SqsPushReceiver when a queue + sqs client are supplied", () => {
|
|
1912
|
-
const r = createPushReceiver({
|
|
1913
|
-
tenantId: TENANT,
|
|
1914
|
-
queueUrl: QUEUE_URL,
|
|
1915
|
-
sqs: new FakeSqs(),
|
|
1916
|
-
syncFn: async () => undefined,
|
|
1917
|
-
});
|
|
1918
|
-
expect(r).toBeInstanceOf(SqsPushReceiver);
|
|
1919
|
-
});
|
|
1920
|
-
it("NoopPushReceiver flips connected on start/dispose with no work", async () => {
|
|
1921
|
-
const r = new NoopPushReceiver();
|
|
1922
|
-
expect(r.connected).toBe(false);
|
|
1923
|
-
await r.start();
|
|
1924
|
-
expect(r.connected).toBe(true);
|
|
1925
|
-
await r.dispose();
|
|
1926
|
-
expect(r.connected).toBe(false);
|
|
1927
|
-
});
|
|
1928
|
-
});
|
|
1929
|
-
describe("SqsPushReceiver failed-batch retry visibility", () => {
|
|
1930
|
-
it("releases an overlong drain so the receiver resumes polling after two stuck drains", async () => {
|
|
1931
|
-
vi.useFakeTimers();
|
|
1932
|
-
const sqs = new FakeSqs();
|
|
1933
|
-
const events = [
|
|
1934
|
-
makeEvent({ relativePath: "companies/indigo/stuck-one.md", sequenceNumber: 1 }),
|
|
1935
|
-
makeEvent({ relativePath: "companies/indigo/stuck-two.md", sequenceNumber: 2 }),
|
|
1936
|
-
makeEvent({ relativePath: "companies/indigo/stuck-three.md", sequenceNumber: 3 }),
|
|
1937
|
-
];
|
|
1938
|
-
sqs.enqueueDrainable([msg(events[0])]);
|
|
1939
|
-
let syncCalls = 0;
|
|
1940
|
-
const receiver = new SqsPushReceiver({
|
|
1941
|
-
tenantId: TENANT,
|
|
1942
|
-
queueUrl: QUEUE_URL,
|
|
1943
|
-
sqs,
|
|
1944
|
-
maxInFlightDrains: 2,
|
|
1945
|
-
visibilityTimeoutSeconds: 600,
|
|
1946
|
-
visibilityHeartbeatIntervalMs: 300_000,
|
|
1947
|
-
disposeDrainMs: 1,
|
|
1948
|
-
syncBatchFn: async () => {
|
|
1949
|
-
if (syncCalls === 0)
|
|
1950
|
-
sqs.enqueueDrainable([msg(events[1])]);
|
|
1951
|
-
syncCalls += 1;
|
|
1952
|
-
return new Promise(() => undefined);
|
|
1953
|
-
},
|
|
1954
|
-
sleep: fastSleep,
|
|
1955
|
-
publishMetric: async () => undefined,
|
|
1956
|
-
logger: makeDrainLogger([]),
|
|
1957
|
-
});
|
|
1958
|
-
try {
|
|
1959
|
-
await receiver.start();
|
|
1960
|
-
await vi.waitFor(() => expect(syncCalls).toBe(2));
|
|
1961
|
-
await vi.advanceTimersByTimeAsync(600_000);
|
|
1962
|
-
await vi.waitFor(() => expect(sqs.receiveCalls).toBeGreaterThan(2));
|
|
1963
|
-
expect(sqs.visibilityChanges).toContainEqual({
|
|
1964
|
-
receiptHandle: "rh-1",
|
|
1965
|
-
visibilityTimeoutSeconds: 20,
|
|
1966
|
-
});
|
|
1967
|
-
expect(sqs.visibilityChanges).toContainEqual({
|
|
1968
|
-
receiptHandle: "rh-2",
|
|
1969
|
-
visibilityTimeoutSeconds: 20,
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
finally {
|
|
1973
|
-
const disposing = receiver.dispose();
|
|
1974
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
1975
|
-
await disposing;
|
|
1976
|
-
vi.useRealTimers();
|
|
1977
|
-
}
|
|
1978
|
-
});
|
|
1979
|
-
it("settles a completed route group's receipt while another group is still running", async () => {
|
|
1980
|
-
const sqs = new FakeSqs();
|
|
1981
|
-
const fast = makeEvent({ relativePath: "personal/notes/fast.md", sequenceNumber: 1 });
|
|
1982
|
-
const slow = makeEvent({ relativePath: "companies/indigo/slow.md", sequenceNumber: 2 });
|
|
1983
|
-
sqs.enqueueDrainable([msg(fast, "rh-fast"), msg(slow, "rh-slow")]);
|
|
1984
|
-
let started;
|
|
1985
|
-
const passStarted = new Promise((resolve) => { started = resolve; });
|
|
1986
|
-
let releaseSlow;
|
|
1987
|
-
const slowGate = new Promise((resolve) => { releaseSlow = resolve; });
|
|
1988
|
-
const receiver = new SqsPushReceiver({
|
|
1989
|
-
tenantId: TENANT,
|
|
1990
|
-
queueUrl: QUEUE_URL,
|
|
1991
|
-
sqs,
|
|
1992
|
-
maxInFlightDrains: 1,
|
|
1993
|
-
syncBatchFn: async (context) => {
|
|
1994
|
-
started();
|
|
1995
|
-
await context.settleEvents?.([fast]);
|
|
1996
|
-
await slowGate;
|
|
1997
|
-
await context.settleEvents?.([slow]);
|
|
1998
|
-
},
|
|
1999
|
-
sleep: fastSleep,
|
|
2000
|
-
publishMetric: async () => undefined,
|
|
2001
|
-
logger: makeDrainLogger([]),
|
|
2002
|
-
});
|
|
2003
|
-
try {
|
|
2004
|
-
await receiver.start();
|
|
2005
|
-
await passStarted;
|
|
2006
|
-
await vi.waitFor(() => expect(sqs.deleted).toContain("rh-fast"));
|
|
2007
|
-
expect(sqs.deleted).not.toContain("rh-slow");
|
|
2008
|
-
releaseSlow();
|
|
2009
|
-
await vi.waitFor(() => expect(sqs.deleted).toContain("rh-slow"));
|
|
2010
|
-
}
|
|
2011
|
-
finally {
|
|
2012
|
-
releaseSlow();
|
|
2013
|
-
await receiver.dispose();
|
|
2014
|
-
}
|
|
2015
|
-
});
|
|
2016
|
-
it("resumes lease heartbeats and accepts a later route settle after one settle throws", async () => {
|
|
2017
|
-
const sqs = new FakeSqs();
|
|
2018
|
-
const failed = makeEvent({ relativePath: "companies/indigo/failed-settle.md", sequenceNumber: 21 });
|
|
2019
|
-
const next = makeEvent({ relativePath: "companies/indigo/next-settle.md", sequenceNumber: 22 });
|
|
2020
|
-
const held = makeEvent({ relativePath: "companies/indigo/held-settle.md", sequenceNumber: 23 });
|
|
2021
|
-
sqs.enqueueDrainable([
|
|
2022
|
-
msg(failed, "rh-failed-settle"),
|
|
2023
|
-
msg(next, "rh-next-settle"),
|
|
2024
|
-
msg(held, "rh-held-settle"),
|
|
2025
|
-
]);
|
|
2026
|
-
let releasePass;
|
|
2027
|
-
const passGate = new Promise((resolve) => { releasePass = resolve; });
|
|
2028
|
-
let laterSettleFinished = false;
|
|
2029
|
-
const receiver = new SqsPushReceiver({
|
|
2030
|
-
tenantId: TENANT,
|
|
2031
|
-
queueUrl: QUEUE_URL,
|
|
2032
|
-
sqs,
|
|
2033
|
-
visibilityTimeoutSeconds: 20,
|
|
2034
|
-
visibilityHeartbeatIntervalMs: 5,
|
|
2035
|
-
syncBatchFn: async (context) => {
|
|
2036
|
-
const internals = receiver;
|
|
2037
|
-
const originalSafeDelete = internals.safeDelete.bind(receiver);
|
|
2038
|
-
internals.safeDelete = async (message) => {
|
|
2039
|
-
if (message.ReceiptHandle === "rh-failed-settle") {
|
|
2040
|
-
throw new Error("simulated acknowledgement failure");
|
|
2041
|
-
}
|
|
2042
|
-
return originalSafeDelete(message);
|
|
2043
|
-
};
|
|
2044
|
-
await expect(context.settleEvents?.([failed])).rejects.toThrow("simulated acknowledgement failure");
|
|
2045
|
-
await context.settleEvents?.([next]);
|
|
2046
|
-
laterSettleFinished = true;
|
|
2047
|
-
await passGate;
|
|
2048
|
-
},
|
|
2049
|
-
sleep: fastSleep,
|
|
2050
|
-
publishMetric: async () => undefined,
|
|
2051
|
-
logger: makeDrainLogger([]),
|
|
2052
|
-
});
|
|
2053
|
-
try {
|
|
2054
|
-
await receiver.start();
|
|
2055
|
-
await vi.waitFor(() => expect(laterSettleFinished).toBe(true));
|
|
2056
|
-
await vi.waitFor(() => expect(sqs.deleted).toContain("rh-next-settle"));
|
|
2057
|
-
await vi.waitFor(() => expect(sqs.visibilityChanges).toContainEqual({
|
|
2058
|
-
receiptHandle: "rh-held-settle",
|
|
2059
|
-
visibilityTimeoutSeconds: 20,
|
|
2060
|
-
}));
|
|
2061
|
-
}
|
|
2062
|
-
finally {
|
|
2063
|
-
releasePass();
|
|
2064
|
-
await receiver.dispose();
|
|
2065
|
-
}
|
|
2066
|
-
});
|
|
2067
|
-
it("keeps the outer heartbeat pause active when a nested delete fails", async () => {
|
|
2068
|
-
const sqs = new FakeSqs();
|
|
2069
|
-
const event = makeEvent({ relativePath: "companies/indigo/nested-settle.md", sequenceNumber: 24 });
|
|
2070
|
-
const message = msg(event, "rh-delete-fails");
|
|
2071
|
-
vi.useFakeTimers();
|
|
2072
|
-
const receiver = new SqsPushReceiver({
|
|
2073
|
-
tenantId: TENANT,
|
|
2074
|
-
queueUrl: QUEUE_URL,
|
|
2075
|
-
sqs,
|
|
2076
|
-
visibilityTimeoutSeconds: 20,
|
|
2077
|
-
visibilityHeartbeatIntervalMs: 5,
|
|
2078
|
-
syncFn: async () => undefined,
|
|
2079
|
-
sleep: fastSleep,
|
|
2080
|
-
publishMetric: async () => undefined,
|
|
2081
|
-
logger: makeDrainLogger([]),
|
|
2082
|
-
});
|
|
2083
|
-
const internals = receiver;
|
|
2084
|
-
const heartbeat = internals.startReceiptLeaseHeartbeat();
|
|
2085
|
-
heartbeat.add(message);
|
|
2086
|
-
sqs.deleteMessage = async () => { throw new Error("simulated delete failure"); };
|
|
2087
|
-
try {
|
|
2088
|
-
await heartbeat.pause(); // settleEvents owns the outer pause.
|
|
2089
|
-
try {
|
|
2090
|
-
await sqs.deleteMessage({ queueUrl: QUEUE_URL, receiptHandle: "rh-delete-fails" });
|
|
2091
|
-
}
|
|
2092
|
-
catch {
|
|
2093
|
-
await heartbeat.pause(); // acknowledge pauses while releasing a failed delete.
|
|
2094
|
-
}
|
|
2095
|
-
finally {
|
|
2096
|
-
heartbeat.resume(); // acknowledge's finally must not resume the outer pause.
|
|
2097
|
-
}
|
|
2098
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
2099
|
-
expect(sqs.visibilityBatchCalls).toHaveLength(0);
|
|
2100
|
-
heartbeat.resume();
|
|
2101
|
-
await vi.advanceTimersByTimeAsync(5);
|
|
2102
|
-
expect(sqs.visibilityBatchCalls).toHaveLength(1);
|
|
2103
|
-
expect(sqs.visibilityBatchCalls[0]?.entries).toContainEqual({
|
|
2104
|
-
id: "receipt-0",
|
|
2105
|
-
receiptHandle: "rh-delete-fails",
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
finally {
|
|
2109
|
-
await heartbeat.stop();
|
|
2110
|
-
vi.useRealTimers();
|
|
2111
|
-
await receiver.dispose();
|
|
2112
|
-
}
|
|
2113
|
-
});
|
|
2114
|
-
it("waits for a timed-out pull to stop before applying its redelivery once", async () => {
|
|
2115
|
-
const sqs = new FakeSqs();
|
|
2116
|
-
const event = makeEvent({ relativePath: "companies/indigo/timeout-redelivery.md" });
|
|
2117
|
-
sqs.enqueueDrainable([msg(event, "rh-timeout-original")]);
|
|
2118
|
-
let oldPullStarted;
|
|
2119
|
-
const oldPullStartedPromise = new Promise((resolve) => { oldPullStarted = resolve; });
|
|
2120
|
-
let syncCalls = 0;
|
|
2121
|
-
let oldPullStopped = false;
|
|
2122
|
-
let applied = 0;
|
|
2123
|
-
const receiver = new SqsPushReceiver({
|
|
2124
|
-
tenantId: TENANT,
|
|
2125
|
-
queueUrl: QUEUE_URL,
|
|
2126
|
-
sqs,
|
|
2127
|
-
maxInFlightDrains: 2,
|
|
2128
|
-
drainTimeoutMs: 100,
|
|
2129
|
-
disposeDrainMs: 1,
|
|
2130
|
-
syncBatchFn: async (context) => {
|
|
2131
|
-
syncCalls += 1;
|
|
2132
|
-
if (syncCalls === 1) {
|
|
2133
|
-
oldPullStarted();
|
|
2134
|
-
await new Promise((resolve) => {
|
|
2135
|
-
if (context.signal.aborted)
|
|
2136
|
-
resolve();
|
|
2137
|
-
else
|
|
2138
|
-
context.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
2139
|
-
});
|
|
2140
|
-
oldPullStopped = context.signal.aborted;
|
|
2141
|
-
return;
|
|
2142
|
-
}
|
|
2143
|
-
expect(oldPullStopped).toBe(true);
|
|
2144
|
-
applied += 1;
|
|
2145
|
-
},
|
|
2146
|
-
sleep: fastSleep,
|
|
2147
|
-
publishMetric: async () => undefined,
|
|
2148
|
-
logger: makeDrainLogger([]),
|
|
2149
|
-
});
|
|
2150
|
-
try {
|
|
2151
|
-
await receiver.start();
|
|
2152
|
-
await oldPullStartedPromise;
|
|
2153
|
-
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
2154
|
-
await vi.waitFor(() => expect(sqs.visibilityChanges).toContainEqual({
|
|
2155
|
-
receiptHandle: "rh-timeout-original",
|
|
2156
|
-
visibilityTimeoutSeconds: 20,
|
|
2157
|
-
}));
|
|
2158
|
-
sqs.enqueueDrainable([msg(event, "rh-timeout-redelivered")]);
|
|
2159
|
-
await vi.waitFor(() => expect(sqs.receivedReceiptHandles).toContain("rh-timeout-redelivered"));
|
|
2160
|
-
await vi.waitFor(() => expect(applied).toBe(1));
|
|
2161
|
-
expect(oldPullStopped).toBe(true);
|
|
2162
|
-
expect(sqs.deleted).toContain("rh-timeout-redelivered");
|
|
2163
|
-
expect(applied).toBe(1);
|
|
2164
|
-
}
|
|
2165
|
-
finally {
|
|
2166
|
-
await receiver.dispose();
|
|
2167
|
-
}
|
|
2168
|
-
});
|
|
2169
|
-
it("releases a timed-out multi-path batch once without reporting success telemetry", async () => {
|
|
2170
|
-
const sqs = new FakeSqs();
|
|
2171
|
-
const firstEvent = makeEvent({ relativePath: "companies/indigo/timeout-first.md", sequenceNumber: 31 });
|
|
2172
|
-
const secondEvent = makeEvent({ relativePath: "companies/indigo/timeout-second.md", sequenceNumber: 32 });
|
|
2173
|
-
sqs.enqueueDrainable([
|
|
2174
|
-
msg(firstEvent, "rh-timeout-first"),
|
|
2175
|
-
msg(secondEvent, "rh-timeout-second"),
|
|
2176
|
-
]);
|
|
2177
|
-
const metric = vi.fn(async () => undefined);
|
|
2178
|
-
const infos = [];
|
|
2179
|
-
let syncCalls = 0;
|
|
2180
|
-
const receiver = new SqsPushReceiver({
|
|
2181
|
-
tenantId: TENANT,
|
|
2182
|
-
queueUrl: QUEUE_URL,
|
|
2183
|
-
sqs,
|
|
2184
|
-
maxInFlightDrains: 2,
|
|
2185
|
-
drainTimeoutMs: 30,
|
|
2186
|
-
syncBatchFn: async ({ signal }) => {
|
|
2187
|
-
syncCalls += 1;
|
|
2188
|
-
await new Promise((resolve) => {
|
|
2189
|
-
if (signal.aborted)
|
|
2190
|
-
resolve();
|
|
2191
|
-
else
|
|
2192
|
-
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
2193
|
-
});
|
|
2194
|
-
},
|
|
2195
|
-
sleep: fastSleep,
|
|
2196
|
-
publishMetric: metric,
|
|
2197
|
-
logger: {
|
|
2198
|
-
debug: () => undefined,
|
|
2199
|
-
warn: () => undefined,
|
|
2200
|
-
error: () => undefined,
|
|
2201
|
-
info: (record) => infos.push(record),
|
|
2202
|
-
},
|
|
2203
|
-
});
|
|
2204
|
-
try {
|
|
2205
|
-
await receiver.start();
|
|
2206
|
-
await vi.waitFor(() => {
|
|
2207
|
-
expect(sqs.visibilityChanges).toEqual(expect.arrayContaining([
|
|
2208
|
-
{ receiptHandle: "rh-timeout-first", visibilityTimeoutSeconds: 20 },
|
|
2209
|
-
{ receiptHandle: "rh-timeout-second", visibilityTimeoutSeconds: 20 },
|
|
2210
|
-
]));
|
|
2211
|
-
});
|
|
2212
|
-
expect(syncCalls).toBe(1);
|
|
2213
|
-
expect(metric).not.toHaveBeenCalled();
|
|
2214
|
-
expect(infos.some((record) => record.event === "fanout.receive")).toBe(false);
|
|
2215
|
-
}
|
|
2216
|
-
finally {
|
|
2217
|
-
await receiver.dispose();
|
|
2218
|
-
}
|
|
2219
|
-
});
|
|
2220
|
-
it("applies a released receipt once when SQS redelivers it", async () => {
|
|
2221
|
-
vi.useFakeTimers();
|
|
2222
|
-
const sqs = new FakeSqs();
|
|
2223
|
-
const event = makeEvent({ relativePath: "companies/indigo/redelivery-once.md" });
|
|
2224
|
-
sqs.enqueueDrainable([msg(event)]);
|
|
2225
|
-
let syncCalls = 0;
|
|
2226
|
-
let applied = 0;
|
|
2227
|
-
const receiver = new SqsPushReceiver({
|
|
2228
|
-
tenantId: TENANT,
|
|
2229
|
-
queueUrl: QUEUE_URL,
|
|
2230
|
-
sqs,
|
|
2231
|
-
maxInFlightDrains: 2,
|
|
2232
|
-
drainTimeoutMs: 100,
|
|
2233
|
-
visibilityTimeoutSeconds: 600,
|
|
2234
|
-
visibilityHeartbeatIntervalMs: 300_000,
|
|
2235
|
-
disposeDrainMs: 1,
|
|
2236
|
-
syncBatchFn: async ({ signal }) => {
|
|
2237
|
-
syncCalls += 1;
|
|
2238
|
-
if (syncCalls === 1) {
|
|
2239
|
-
await new Promise((resolve) => {
|
|
2240
|
-
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
2241
|
-
});
|
|
2242
|
-
return;
|
|
2243
|
-
}
|
|
2244
|
-
applied += 1;
|
|
2245
|
-
},
|
|
2246
|
-
sleep: fastSleep,
|
|
2247
|
-
publishMetric: async () => undefined,
|
|
2248
|
-
logger: makeDrainLogger([]),
|
|
2249
|
-
});
|
|
2250
|
-
try {
|
|
2251
|
-
await receiver.start();
|
|
2252
|
-
await vi.waitFor(() => expect(syncCalls).toBe(1));
|
|
2253
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
2254
|
-
expect(sqs.visibilityChanges).toContainEqual({
|
|
2255
|
-
receiptHandle: "rh-1",
|
|
2256
|
-
visibilityTimeoutSeconds: 20,
|
|
2257
|
-
});
|
|
2258
|
-
// A new receipt handle for the same SQS message models the retry delivery.
|
|
2259
|
-
sqs.enqueueDrainable([msg(event, "rh-redelivered")]);
|
|
2260
|
-
await vi.waitFor(() => expect(syncCalls).toBe(2));
|
|
2261
|
-
expect(applied).toBe(1);
|
|
2262
|
-
expect(sqs.deleted).toEqual(["rh-redelivered"]);
|
|
2263
|
-
expect(syncCalls).toBe(2);
|
|
2264
|
-
}
|
|
2265
|
-
finally {
|
|
2266
|
-
const disposing = receiver.dispose();
|
|
2267
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
2268
|
-
await disposing;
|
|
2269
|
-
vi.useRealTimers();
|
|
2270
|
-
}
|
|
2271
|
-
});
|
|
2272
|
-
const first = makeEvent({
|
|
2273
|
-
relativePath: "companies/indigo/retry-first.md",
|
|
2274
|
-
sequenceNumber: 101,
|
|
2275
|
-
});
|
|
2276
|
-
const second = makeEvent({
|
|
2277
|
-
relativePath: "companies/indigo/retry-second.md",
|
|
2278
|
-
sequenceNumber: 102,
|
|
2279
|
-
});
|
|
2280
|
-
function partitionEachEvent(events) {
|
|
2281
|
-
return events.map((event) => [event]);
|
|
2282
|
-
}
|
|
2283
|
-
function makeDrainLogger(drains, warnings = []) {
|
|
2284
|
-
return {
|
|
2285
|
-
debug: () => undefined,
|
|
2286
|
-
warn: (record) => warnings.push(record),
|
|
2287
|
-
error: () => undefined,
|
|
2288
|
-
info: (record) => {
|
|
2289
|
-
if (record.event === "receiver.drain")
|
|
2290
|
-
drains.push(record);
|
|
2291
|
-
},
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
it("removes an invalid heartbeat receipt after logging the failure once", async () => {
|
|
2295
|
-
const sqs = new FakeSqs();
|
|
2296
|
-
sqs.enqueueDrainable([msg(first, "rh-invalid-heartbeat")]);
|
|
2297
|
-
sqs.failVisibilityBatchEntry("rh-invalid-heartbeat", {
|
|
2298
|
-
code: "ReceiptHandleIsInvalid",
|
|
2299
|
-
message: "receipt handle is no longer valid",
|
|
2300
|
-
senderFault: true,
|
|
2301
|
-
});
|
|
2302
|
-
const warnings = [];
|
|
2303
|
-
let started = false;
|
|
2304
|
-
let releasePass;
|
|
2305
|
-
const passBlockedOnAdmission = new Promise((resolve) => {
|
|
2306
|
-
releasePass = resolve;
|
|
2307
|
-
});
|
|
2308
|
-
const receiver = new SqsPushReceiver({
|
|
2309
|
-
tenantId: TENANT,
|
|
2310
|
-
queueUrl: QUEUE_URL,
|
|
2311
|
-
sqs,
|
|
2312
|
-
syncBatchFn: async () => {
|
|
2313
|
-
started = true;
|
|
2314
|
-
await passBlockedOnAdmission;
|
|
2315
|
-
},
|
|
2316
|
-
sleep: fastSleep,
|
|
2317
|
-
publishMetric: async () => undefined,
|
|
2318
|
-
visibilityHeartbeatIntervalMs: 5,
|
|
2319
|
-
logger: makeDrainLogger([], warnings),
|
|
2320
|
-
});
|
|
2321
|
-
try {
|
|
2322
|
-
await receiver.start();
|
|
2323
|
-
await waitFor(() => started && sqs.visibilityBatchCalls.length >= 1);
|
|
2324
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2325
|
-
expect(sqs.visibilityBatchCalls).toHaveLength(1);
|
|
2326
|
-
expect(warnings.filter((warning) => warning.event === "receiver.lease.extend.failed"))
|
|
2327
|
-
.toHaveLength(1);
|
|
2328
|
-
expect(warnings[0]).toMatchObject({
|
|
2329
|
-
event: "receiver.lease.extend.failed",
|
|
2330
|
-
code: "ReceiptHandleIsInvalid",
|
|
2331
|
-
});
|
|
2332
|
-
}
|
|
2333
|
-
finally {
|
|
2334
|
-
releasePass();
|
|
2335
|
-
await receiver.dispose();
|
|
2336
|
-
}
|
|
2337
|
-
});
|
|
2338
|
-
it("sets the failed batch receipts to 20 seconds without deleting them", async () => {
|
|
2339
|
-
const sqs = new FakeSqs();
|
|
2340
|
-
sqs.enqueueDrainable([
|
|
2341
|
-
msg(first, "rh-retry-first"),
|
|
2342
|
-
msg(second, "rh-retry-second"),
|
|
2343
|
-
]);
|
|
2344
|
-
const calls = [];
|
|
2345
|
-
const drains = [];
|
|
2346
|
-
const syncBatchFn = Object.assign(async ({ events }) => {
|
|
2347
|
-
calls.push(events.map((event) => event.relativePath));
|
|
2348
|
-
if (calls.length === 1)
|
|
2349
|
-
throw new Error("first targeted pull failed");
|
|
2350
|
-
}, { partition: partitionEachEvent });
|
|
2351
|
-
const receiver = new SqsPushReceiver({
|
|
2352
|
-
tenantId: TENANT,
|
|
2353
|
-
queueUrl: QUEUE_URL,
|
|
2354
|
-
sqs,
|
|
2355
|
-
syncBatchFn,
|
|
2356
|
-
sleep: fastSleep,
|
|
2357
|
-
publishMetric: async () => undefined,
|
|
2358
|
-
logger: makeDrainLogger(drains),
|
|
2359
|
-
});
|
|
2360
|
-
await receiver.start();
|
|
2361
|
-
await waitFor(() => drains.length === 1);
|
|
2362
|
-
await receiver.dispose();
|
|
2363
|
-
expect(calls[0]).toEqual([first.relativePath]);
|
|
2364
|
-
expect(sqs.visibilityChanges).toEqual([
|
|
2365
|
-
{ receiptHandle: "rh-retry-first", visibilityTimeoutSeconds: 20 },
|
|
2366
|
-
]);
|
|
2367
|
-
expect(sqs.deleted).not.toContain("rh-retry-first");
|
|
2368
|
-
expect(drains[0]).toMatchObject({ retried: 1 });
|
|
2369
|
-
});
|
|
2370
|
-
it("dispatches the next batch in the same drain and deletes its receipts", async () => {
|
|
2371
|
-
const sqs = new FakeSqs();
|
|
2372
|
-
sqs.enqueueDrainable([
|
|
2373
|
-
msg(first, "rh-next-first"),
|
|
2374
|
-
msg(second, "rh-next-second"),
|
|
2375
|
-
]);
|
|
2376
|
-
const calls = [];
|
|
2377
|
-
const drains = [];
|
|
2378
|
-
const syncBatchFn = Object.assign(async ({ events }) => {
|
|
2379
|
-
calls.push(events.map((event) => event.relativePath));
|
|
2380
|
-
if (calls.length === 1)
|
|
2381
|
-
throw new Error("first targeted pull failed");
|
|
2382
|
-
}, { partition: partitionEachEvent });
|
|
2383
|
-
const receiver = new SqsPushReceiver({
|
|
2384
|
-
tenantId: TENANT,
|
|
2385
|
-
queueUrl: QUEUE_URL,
|
|
2386
|
-
sqs,
|
|
2387
|
-
syncBatchFn,
|
|
2388
|
-
sleep: fastSleep,
|
|
2389
|
-
publishMetric: async () => undefined,
|
|
2390
|
-
logger: makeDrainLogger(drains),
|
|
2391
|
-
});
|
|
2392
|
-
await receiver.start();
|
|
2393
|
-
await waitFor(() => drains.length === 1);
|
|
2394
|
-
await receiver.dispose();
|
|
2395
|
-
expect(calls).toEqual([[first.relativePath], [second.relativePath]]);
|
|
2396
|
-
expect(sqs.deleted).toContain("rh-next-second");
|
|
2397
|
-
expect(sqs.deleted).not.toContain("rh-next-first");
|
|
2398
|
-
});
|
|
2399
|
-
it("acks successful paths and retries only paths that fail inside a coalesced batch", async () => {
|
|
2400
|
-
const blocked = makeEvent({
|
|
2401
|
-
relativePath: "companies/indigo/out-of-scope.md",
|
|
2402
|
-
sequenceNumber: 103,
|
|
2403
|
-
});
|
|
2404
|
-
const allowed = makeEvent({
|
|
2405
|
-
relativePath: "companies/indigo/in-scope.md",
|
|
2406
|
-
sequenceNumber: 104,
|
|
2407
|
-
});
|
|
2408
|
-
const sqs = new FakeSqs();
|
|
2409
|
-
sqs.enqueueDrainable([
|
|
2410
|
-
msg(blocked, "rh-out-of-scope"),
|
|
2411
|
-
msg(allowed, "rh-in-scope"),
|
|
2412
|
-
]);
|
|
2413
|
-
const calls = [];
|
|
2414
|
-
const drains = [];
|
|
2415
|
-
const syncBatchFn = async ({ events }) => {
|
|
2416
|
-
const paths = events.map((event) => event.relativePath);
|
|
2417
|
-
calls.push(paths);
|
|
2418
|
-
if (paths.length > 1 || paths[0] === blocked.relativePath) {
|
|
2419
|
-
throw new Error("targeted batch includes an out-of-scope path");
|
|
2420
|
-
}
|
|
2421
|
-
};
|
|
2422
|
-
const receiver = new SqsPushReceiver({
|
|
2423
|
-
tenantId: TENANT,
|
|
2424
|
-
queueUrl: QUEUE_URL,
|
|
2425
|
-
sqs,
|
|
2426
|
-
syncBatchFn,
|
|
2427
|
-
sleep: fastSleep,
|
|
2428
|
-
publishMetric: async () => undefined,
|
|
2429
|
-
logger: makeDrainLogger(drains),
|
|
2430
|
-
});
|
|
2431
|
-
await receiver.start();
|
|
2432
|
-
await waitFor(() => drains.length === 1);
|
|
2433
|
-
await receiver.dispose();
|
|
2434
|
-
expect(calls).toEqual([
|
|
2435
|
-
[blocked.relativePath, allowed.relativePath],
|
|
2436
|
-
[blocked.relativePath],
|
|
2437
|
-
[allowed.relativePath],
|
|
2438
|
-
]);
|
|
2439
|
-
expect(sqs.deleted).toEqual(["rh-in-scope"]);
|
|
2440
|
-
expect(sqs.visibilityChanges).toEqual([
|
|
2441
|
-
{ receiptHandle: "rh-out-of-scope", visibilityTimeoutSeconds: 20 },
|
|
2442
|
-
]);
|
|
2443
|
-
expect(drains[0]).toMatchObject({ deleted: 1, retried: 1 });
|
|
2444
|
-
});
|
|
2445
|
-
it("holds the lease and resumes locally after a realtime-admission timeout", async () => {
|
|
2446
|
-
const sqs = new FakeSqs();
|
|
2447
|
-
sqs.enqueueDrainable([msg(first, "rh-admission-timeout")]);
|
|
2448
|
-
const drains = [];
|
|
2449
|
-
const warnings = [];
|
|
2450
|
-
const errors = [];
|
|
2451
|
-
const infos = [];
|
|
2452
|
-
let attempts = 0;
|
|
2453
|
-
const admissionTimeout = new Error("realtime admission timeout after 120000ms");
|
|
2454
|
-
admissionTimeout.name = "RealtimeAdmissionTimeout";
|
|
2455
|
-
const drainLogger = makeDrainLogger(drains, warnings);
|
|
2456
|
-
const receiver = new SqsPushReceiver({
|
|
2457
|
-
tenantId: TENANT,
|
|
2458
|
-
queueUrl: QUEUE_URL,
|
|
2459
|
-
sqs,
|
|
2460
|
-
syncBatchFn: async () => {
|
|
2461
|
-
attempts += 1;
|
|
2462
|
-
if (attempts === 1)
|
|
2463
|
-
throw admissionTimeout;
|
|
2464
|
-
},
|
|
2465
|
-
sleep: fastSleep,
|
|
2466
|
-
publishMetric: async () => undefined,
|
|
2467
|
-
logger: {
|
|
2468
|
-
...drainLogger,
|
|
2469
|
-
error: (record) => errors.push(record),
|
|
2470
|
-
info: (record) => {
|
|
2471
|
-
infos.push(record);
|
|
2472
|
-
drainLogger.info(record);
|
|
2473
|
-
},
|
|
2474
|
-
},
|
|
2475
|
-
});
|
|
2476
|
-
await receiver.start();
|
|
2477
|
-
await waitFor(() => sqs.deleted.length === 1);
|
|
2478
|
-
await receiver.dispose();
|
|
2479
|
-
expect(attempts).toBe(2);
|
|
2480
|
-
expect(sqs.visibilityChanges).toEqual([]);
|
|
2481
|
-
expect(sqs.deleted).toEqual(["rh-admission-timeout"]);
|
|
2482
|
-
expect(warnings.some((warning) => warning.event === "receiver.retry.failed")).toBe(false);
|
|
2483
|
-
expect(drains[0]).toMatchObject({ deleted: 1, retried: 0 });
|
|
2484
|
-
// A deferral, not a failure: no error-level line, one info line.
|
|
2485
|
-
expect(errors).toEqual([]);
|
|
2486
|
-
expect(infos.filter((record) => record.event === "receiver.sync.admission-timeout"))
|
|
2487
|
-
.toHaveLength(1);
|
|
2488
|
-
});
|
|
2489
|
-
it("renews the receipt lease while it waits out repeated realtime-admission timeouts", async () => {
|
|
2490
|
-
// The admission retry can hold a receipt for minutes behind a long
|
|
2491
|
-
// reconcile. The lease heartbeat must keep renewing it for the whole
|
|
2492
|
-
// wait, so the message cannot become visible to another receive and be
|
|
2493
|
-
// delivered twice while this receiver still owns it.
|
|
2494
|
-
const sqs = new FakeSqs();
|
|
2495
|
-
sqs.enqueueDrainable([msg(first, "rh-admission-lease")]);
|
|
2496
|
-
let attempts = 0;
|
|
2497
|
-
const admissionTimeout = new Error("realtime admission timeout after 30000ms");
|
|
2498
|
-
admissionTimeout.name = "RealtimeAdmissionTimeout";
|
|
2499
|
-
const receiver = new SqsPushReceiver({
|
|
2500
|
-
tenantId: TENANT,
|
|
2501
|
-
queueUrl: QUEUE_URL,
|
|
2502
|
-
sqs,
|
|
2503
|
-
visibilityTimeoutSeconds: 600,
|
|
2504
|
-
visibilityHeartbeatIntervalMs: 5,
|
|
2505
|
-
syncBatchFn: async () => {
|
|
2506
|
-
attempts += 1;
|
|
2507
|
-
if (attempts <= 3)
|
|
2508
|
-
throw admissionTimeout;
|
|
2509
|
-
},
|
|
2510
|
-
// Each retry wait spans several heartbeat intervals.
|
|
2511
|
-
sleep: (_ms, signal) => new Promise((resolve) => {
|
|
2512
|
-
if (signal.aborted)
|
|
2513
|
-
return resolve();
|
|
2514
|
-
setTimeout(resolve, 30);
|
|
2515
|
-
}),
|
|
2516
|
-
publishMetric: async () => undefined,
|
|
2517
|
-
logger: { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined },
|
|
2518
|
-
});
|
|
2519
|
-
await receiver.start();
|
|
2520
|
-
await waitFor(() => sqs.deleted.length === 1);
|
|
2521
|
-
await receiver.dispose();
|
|
2522
|
-
expect(attempts).toBe(4);
|
|
2523
|
-
const leaseChanges = sqs.visibilityChanges.filter((change) => change.receiptHandle === "rh-admission-lease");
|
|
2524
|
-
// Renewed at least once per retry wait, always to the full lease, and
|
|
2525
|
-
// never released for redelivery.
|
|
2526
|
-
expect(leaseChanges.length).toBeGreaterThanOrEqual(3);
|
|
2527
|
-
expect(leaseChanges.every((change) => change.visibilityTimeoutSeconds === 600)).toBe(true);
|
|
2528
|
-
expect(sqs.deleted).toEqual(["rh-admission-lease"]);
|
|
2529
|
-
});
|
|
2530
|
-
it("keeps the all-success path free of visibility changes and deletes every receipt", async () => {
|
|
2531
|
-
const sqs = new FakeSqs();
|
|
2532
|
-
sqs.enqueueDrainable([
|
|
2533
|
-
msg(first, "rh-success-first"),
|
|
2534
|
-
msg(second, "rh-success-second"),
|
|
2535
|
-
]);
|
|
2536
|
-
const drains = [];
|
|
2537
|
-
const syncBatchFn = Object.assign(async () => undefined, { partition: partitionEachEvent });
|
|
2538
|
-
const receiver = new SqsPushReceiver({
|
|
2539
|
-
tenantId: TENANT,
|
|
2540
|
-
queueUrl: QUEUE_URL,
|
|
2541
|
-
sqs,
|
|
2542
|
-
syncBatchFn,
|
|
2543
|
-
sleep: fastSleep,
|
|
2544
|
-
publishMetric: async () => undefined,
|
|
2545
|
-
logger: makeDrainLogger(drains),
|
|
2546
|
-
});
|
|
2547
|
-
await receiver.start();
|
|
2548
|
-
await waitFor(() => drains.length === 1);
|
|
2549
|
-
await receiver.dispose();
|
|
2550
|
-
expect(sqs.visibilityChanges).toEqual([]);
|
|
2551
|
-
expect(sqs.deleted).toEqual(["rh-success-first", "rh-success-second"]);
|
|
2552
|
-
});
|
|
2553
|
-
it("logs a retry visibility failure and still dispatches the next batch", async () => {
|
|
2554
|
-
const sqs = new FakeSqs();
|
|
2555
|
-
sqs.failNextVisibilityChange();
|
|
2556
|
-
sqs.enqueueDrainable([
|
|
2557
|
-
msg(first, "rh-visibility-failure-first"),
|
|
2558
|
-
msg(second, "rh-visibility-failure-second"),
|
|
2559
|
-
]);
|
|
2560
|
-
const calls = [];
|
|
2561
|
-
const drains = [];
|
|
2562
|
-
const warnings = [];
|
|
2563
|
-
const syncBatchFn = Object.assign(async ({ events }) => {
|
|
2564
|
-
calls.push(events.map((event) => event.relativePath));
|
|
2565
|
-
if (calls.length === 1)
|
|
2566
|
-
throw new Error("first targeted pull failed");
|
|
2567
|
-
}, { partition: partitionEachEvent });
|
|
2568
|
-
const receiver = new SqsPushReceiver({
|
|
2569
|
-
tenantId: TENANT,
|
|
2570
|
-
queueUrl: QUEUE_URL,
|
|
2571
|
-
sqs,
|
|
2572
|
-
syncBatchFn,
|
|
2573
|
-
sleep: fastSleep,
|
|
2574
|
-
publishMetric: async () => undefined,
|
|
2575
|
-
logger: makeDrainLogger(drains, warnings),
|
|
2576
|
-
});
|
|
2577
|
-
await receiver.start();
|
|
2578
|
-
await waitFor(() => drains.length === 1);
|
|
2579
|
-
await receiver.dispose();
|
|
2580
|
-
expect(calls).toEqual([[first.relativePath], [second.relativePath]]);
|
|
2581
|
-
expect(sqs.deleted).toContain("rh-visibility-failure-second");
|
|
2582
|
-
expect(warnings).toContainEqual(expect.objectContaining({
|
|
2583
|
-
event: "receiver.retry.failed",
|
|
2584
|
-
messageId: `m-${first.relativePath}-${first.sequenceNumber}`,
|
|
2585
|
-
}));
|
|
2586
|
-
});
|
|
2587
|
-
it("bounds a 64-path transport failure to one dispatch and batches the retry lease", async () => {
|
|
2588
|
-
const total = 64;
|
|
2589
|
-
const events = Array.from({ length: total }, (_, index) => makeEvent({
|
|
2590
|
-
relativePath: `companies/indigo/transport-${index}.md`,
|
|
2591
|
-
sequenceNumber: index + 1,
|
|
2592
|
-
}));
|
|
2593
|
-
const sqs = new FakeSqs();
|
|
2594
|
-
sqs.enqueueDrainable(events.map((event, index) => msg(event, `rh-transport-${index}`)));
|
|
2595
|
-
const drains = [];
|
|
2596
|
-
let dispatchCalls = 0;
|
|
2597
|
-
const transportError = Object.assign(new Error("ECONNRESET: socket hang up while pulling remote objects"), { code: "ECONNRESET" });
|
|
2598
|
-
const startedAt = Date.now();
|
|
2599
|
-
const receiver = new SqsPushReceiver({
|
|
2600
|
-
tenantId: TENANT,
|
|
2601
|
-
queueUrl: QUEUE_URL,
|
|
2602
|
-
sqs,
|
|
2603
|
-
drainMaxMessages: total,
|
|
2604
|
-
batchMaxPaths: total,
|
|
2605
|
-
syncBatchFn: async () => {
|
|
2606
|
-
dispatchCalls += 1;
|
|
2607
|
-
throw transportError;
|
|
2608
|
-
},
|
|
2609
|
-
sleep: fastSleep,
|
|
2610
|
-
publishMetric: async () => undefined,
|
|
2611
|
-
logger: makeDrainLogger(drains),
|
|
2612
|
-
});
|
|
2613
|
-
await receiver.start();
|
|
2614
|
-
await waitFor(() => drains.length === 1, 5_000);
|
|
2615
|
-
await receiver.dispose();
|
|
2616
|
-
expect(dispatchCalls).toBe(1);
|
|
2617
|
-
expect(Date.now() - startedAt).toBeLessThan(5_000);
|
|
2618
|
-
expect(sqs.deleted).toEqual([]);
|
|
2619
|
-
expect(sqs.visibilityChanges.filter((change) => change.visibilityTimeoutSeconds === 20))
|
|
2620
|
-
.toHaveLength(total);
|
|
2621
|
-
const retryCalls = sqs.visibilityBatchCalls.filter((call) => call.visibilityTimeoutSeconds === 20);
|
|
2622
|
-
expect(retryCalls).toHaveLength(7);
|
|
2623
|
-
expect(retryCalls.every((call) => call.entries.length <= 10)).toBe(true);
|
|
2624
|
-
});
|
|
2625
|
-
it("bisects a path-specific batch failure and acknowledges the other 63 paths", async () => {
|
|
2626
|
-
const total = 64;
|
|
2627
|
-
const blocked = makeEvent({
|
|
2628
|
-
relativePath: `companies/indigo/blocked-${total - 1}.md`,
|
|
2629
|
-
sequenceNumber: total,
|
|
2630
|
-
});
|
|
2631
|
-
const events = Array.from({ length: total - 1 }, (_, index) => makeEvent({
|
|
2632
|
-
relativePath: `companies/indigo/allowed-${index}.md`,
|
|
2633
|
-
sequenceNumber: index + 1,
|
|
2634
|
-
})).concat(blocked);
|
|
2635
|
-
const sqs = new FakeSqs();
|
|
2636
|
-
sqs.enqueueDrainable(events.map((event, index) => msg(event, `rh-path-${index}`)));
|
|
2637
|
-
const drains = [];
|
|
2638
|
-
let dispatchCalls = 0;
|
|
2639
|
-
const receiver = new SqsPushReceiver({
|
|
2640
|
-
tenantId: TENANT,
|
|
2641
|
-
queueUrl: QUEUE_URL,
|
|
2642
|
-
sqs,
|
|
2643
|
-
drainMaxMessages: total,
|
|
2644
|
-
batchMaxPaths: total,
|
|
2645
|
-
syncBatchFn: async ({ events: batch }) => {
|
|
2646
|
-
dispatchCalls += 1;
|
|
2647
|
-
if (batch.some((event) => event.relativePath === blocked.relativePath)) {
|
|
2648
|
-
throw new Error("out-of-scope path rejected by targeted pull");
|
|
2649
|
-
}
|
|
2650
|
-
},
|
|
2651
|
-
sleep: fastSleep,
|
|
2652
|
-
publishMetric: async () => undefined,
|
|
2653
|
-
logger: makeDrainLogger(drains),
|
|
2654
|
-
});
|
|
2655
|
-
await receiver.start();
|
|
2656
|
-
await waitFor(() => drains.length === 1, 5_000);
|
|
2657
|
-
await receiver.dispose();
|
|
2658
|
-
expect(dispatchCalls).toBeLessThanOrEqual(13);
|
|
2659
|
-
expect(sqs.deleted).toHaveLength(total - 1);
|
|
2660
|
-
expect(sqs.deleted).not.toContain(`rh-path-${total - 1}`);
|
|
2661
|
-
expect(sqs.visibilityChanges.filter((change) => change.visibilityTimeoutSeconds === 20))
|
|
2662
|
-
.toEqual([{ receiptHandle: `rh-path-${total - 1}`, visibilityTimeoutSeconds: 20 }]);
|
|
2663
|
-
expect(drains[0]).toMatchObject({ processed: total - 1, deleted: total - 1, retried: 1 });
|
|
2664
|
-
});
|
|
2665
|
-
it("reports failed retry entries separately from successful entries", async () => {
|
|
2666
|
-
const sqs = new FakeSqs();
|
|
2667
|
-
sqs.failVisibilityBatchEntry("rh-retry-entry-failed", {
|
|
2668
|
-
code: "ReceiptHandleIsInvalid",
|
|
2669
|
-
message: "one receipt was rejected",
|
|
2670
|
-
senderFault: true,
|
|
2671
|
-
});
|
|
2672
|
-
sqs.enqueueDrainable([
|
|
2673
|
-
msg(first, "rh-retry-entry-failed"),
|
|
2674
|
-
msg(second, "rh-retry-entry-succeeded"),
|
|
2675
|
-
]);
|
|
2676
|
-
const warnings = [];
|
|
2677
|
-
const drains = [];
|
|
2678
|
-
const transportError = Object.assign(new Error("network connection reset"), {
|
|
2679
|
-
code: "ECONNRESET",
|
|
2680
|
-
});
|
|
2681
|
-
const receiver = new SqsPushReceiver({
|
|
2682
|
-
tenantId: TENANT,
|
|
2683
|
-
queueUrl: QUEUE_URL,
|
|
2684
|
-
sqs,
|
|
2685
|
-
syncBatchFn: async () => { throw transportError; },
|
|
2686
|
-
sleep: fastSleep,
|
|
2687
|
-
publishMetric: async () => undefined,
|
|
2688
|
-
logger: makeDrainLogger(drains, warnings),
|
|
2689
|
-
});
|
|
2690
|
-
await receiver.start();
|
|
2691
|
-
await waitFor(() => drains.length === 1);
|
|
2692
|
-
await receiver.dispose();
|
|
2693
|
-
const failures = warnings.filter((warning) => warning.event === "receiver.retry.failed");
|
|
2694
|
-
expect(failures).toHaveLength(1);
|
|
2695
|
-
expect(failures[0]).toMatchObject({
|
|
2696
|
-
messageId: `m-${first.relativePath}-${first.sequenceNumber}`,
|
|
2697
|
-
code: "ReceiptHandleIsInvalid",
|
|
2698
|
-
failureMessage: "one receipt was rejected",
|
|
2699
|
-
senderFault: true,
|
|
2700
|
-
});
|
|
2701
|
-
expect(sqs.visibilityChanges).toEqual([
|
|
2702
|
-
{ receiptHandle: "rh-retry-entry-succeeded", visibilityTimeoutSeconds: 20 },
|
|
2703
|
-
]);
|
|
2704
|
-
expect(drains[0]).toMatchObject({ retried: 1 });
|
|
2705
|
-
expect(failures[0]).not.toMatchObject({
|
|
2706
|
-
messageId: `m-${second.relativePath}-${second.sequenceNumber}`,
|
|
2707
|
-
});
|
|
2708
|
-
});
|
|
2709
|
-
it("uses about 100 ten-receipt API calls for a 1,000-receipt heartbeat", async () => {
|
|
2710
|
-
const total = 1_000;
|
|
2711
|
-
const sqs = new FakeSqs();
|
|
2712
|
-
const receipts = Array.from({ length: total }, (_, index) => msg(makeEvent({
|
|
2713
|
-
relativePath: `companies/indigo/heartbeat-${index}.md`,
|
|
2714
|
-
sequenceNumber: index + 1,
|
|
2715
|
-
}), `rh-heartbeat-${index}`));
|
|
2716
|
-
for (let offset = 0; offset < receipts.length; offset += 10) {
|
|
2717
|
-
sqs.enqueueDrainable(receipts.slice(offset, offset + 10));
|
|
2718
|
-
}
|
|
2719
|
-
let startPass;
|
|
2720
|
-
const passStarted = new Promise((resolve) => { startPass = resolve; });
|
|
2721
|
-
let releasePass;
|
|
2722
|
-
const passGate = new Promise((resolve) => { releasePass = resolve; });
|
|
2723
|
-
let finishDrain;
|
|
2724
|
-
const drainFinished = new Promise((resolve) => { finishDrain = resolve; });
|
|
2725
|
-
const receiver = new SqsPushReceiver({
|
|
2726
|
-
tenantId: TENANT,
|
|
2727
|
-
queueUrl: QUEUE_URL,
|
|
2728
|
-
sqs,
|
|
2729
|
-
drainMaxMessages: total,
|
|
2730
|
-
batchMaxPaths: total,
|
|
2731
|
-
maxInFlightDrains: 1,
|
|
2732
|
-
visibilityTimeoutSeconds: 600,
|
|
2733
|
-
visibilityHeartbeatIntervalMs: 1_000,
|
|
2734
|
-
syncBatchFn: async () => {
|
|
2735
|
-
startPass();
|
|
2736
|
-
await passGate;
|
|
2737
|
-
throw Object.assign(new Error("ECONNRESET while pulling the batch"), {
|
|
2738
|
-
code: "ECONNRESET",
|
|
2739
|
-
});
|
|
2740
|
-
},
|
|
2741
|
-
sleep: fastSleep,
|
|
2742
|
-
publishMetric: async () => undefined,
|
|
2743
|
-
logger: {
|
|
2744
|
-
debug: () => undefined,
|
|
2745
|
-
warn: () => undefined,
|
|
2746
|
-
error: () => undefined,
|
|
2747
|
-
info: (record) => {
|
|
2748
|
-
if (record.event === "receiver.drain")
|
|
2749
|
-
finishDrain();
|
|
2750
|
-
},
|
|
2751
|
-
},
|
|
2752
|
-
});
|
|
2753
|
-
vi.useFakeTimers();
|
|
2754
|
-
try {
|
|
2755
|
-
await receiver.start();
|
|
2756
|
-
await passStarted;
|
|
2757
|
-
await vi.advanceTimersByTimeAsync(1_000);
|
|
2758
|
-
const heartbeatCalls = sqs.visibilityBatchCalls.filter((call) => call.visibilityTimeoutSeconds === 600);
|
|
2759
|
-
expect(heartbeatCalls.length).toBeLessThanOrEqual(110);
|
|
2760
|
-
expect(heartbeatCalls.every((call) => call.entries.length <= 10)).toBe(true);
|
|
2761
|
-
expect(heartbeatCalls.reduce((count, call) => count + call.entries.length, 0))
|
|
2762
|
-
.toBe(total);
|
|
2763
|
-
releasePass();
|
|
2764
|
-
await drainFinished;
|
|
2765
|
-
await receiver.dispose();
|
|
2766
|
-
const retryCalls = sqs.visibilityBatchCalls.filter((call) => call.visibilityTimeoutSeconds === 20);
|
|
2767
|
-
expect(retryCalls.length).toBeLessThanOrEqual(110);
|
|
2768
|
-
expect(retryCalls.every((call) => call.entries.length <= 10)).toBe(true);
|
|
2769
|
-
expect(retryCalls.reduce((count, call) => count + call.entries.length, 0))
|
|
2770
|
-
.toBe(total);
|
|
2771
|
-
}
|
|
2772
|
-
finally {
|
|
2773
|
-
releasePass();
|
|
2774
|
-
await receiver.dispose();
|
|
2775
|
-
vi.useRealTimers();
|
|
2776
|
-
}
|
|
2777
|
-
});
|
|
2778
|
-
it("updates a large failed batch with bounded parallel visibility calls", async () => {
|
|
2779
|
-
const total = 512;
|
|
2780
|
-
const baseSqs = new FakeSqs();
|
|
2781
|
-
const receipts = Array.from({ length: total }, (_, index) => msg(makeEvent({
|
|
2782
|
-
relativePath: "companies/indigo/retry-large-batch.md",
|
|
2783
|
-
sequenceNumber: index + 1,
|
|
2784
|
-
}), `rh-retry-large-${index}`));
|
|
2785
|
-
for (let offset = 0; offset < receipts.length; offset += 10) {
|
|
2786
|
-
baseSqs.enqueueDrainable(receipts.slice(offset, offset + 10));
|
|
2787
|
-
}
|
|
2788
|
-
const visibilityBatchCalls = [];
|
|
2789
|
-
let activeCalls = 0;
|
|
2790
|
-
let maxActiveCalls = 0;
|
|
2791
|
-
let releaseVisibility;
|
|
2792
|
-
const visibilityGate = new Promise((resolve) => {
|
|
2793
|
-
releaseVisibility = resolve;
|
|
2794
|
-
});
|
|
2795
|
-
let firstCallStarted;
|
|
2796
|
-
const firstStarted = new Promise((resolve) => {
|
|
2797
|
-
firstCallStarted = resolve;
|
|
2798
|
-
});
|
|
2799
|
-
const sqs = {
|
|
2800
|
-
receiveMessage: (args) => baseSqs.receiveMessage(args),
|
|
2801
|
-
deleteMessage: (args) => baseSqs.deleteMessage(args),
|
|
2802
|
-
changeMessageVisibility: async ({ receiptHandle }) => {
|
|
2803
|
-
visibilityBatchCalls.push({ receiptHandles: [receiptHandle], visibilityTimeoutSeconds: 20 });
|
|
2804
|
-
activeCalls += 1;
|
|
2805
|
-
maxActiveCalls = Math.max(maxActiveCalls, activeCalls);
|
|
2806
|
-
firstCallStarted();
|
|
2807
|
-
await visibilityGate;
|
|
2808
|
-
activeCalls -= 1;
|
|
2809
|
-
},
|
|
2810
|
-
changeMessageVisibilityBatch: async ({ entries, visibilityTimeoutSeconds }) => {
|
|
2811
|
-
visibilityBatchCalls.push({
|
|
2812
|
-
receiptHandles: entries.map((entry) => entry.receiptHandle),
|
|
2813
|
-
visibilityTimeoutSeconds,
|
|
2814
|
-
});
|
|
2815
|
-
activeCalls += 1;
|
|
2816
|
-
maxActiveCalls = Math.max(maxActiveCalls, activeCalls);
|
|
2817
|
-
firstCallStarted();
|
|
2818
|
-
await visibilityGate;
|
|
2819
|
-
activeCalls -= 1;
|
|
2820
|
-
return { failedEntries: [] };
|
|
2821
|
-
},
|
|
2822
|
-
};
|
|
2823
|
-
const drains = [];
|
|
2824
|
-
const receiver = new SqsPushReceiver({
|
|
2825
|
-
tenantId: TENANT,
|
|
2826
|
-
queueUrl: QUEUE_URL,
|
|
2827
|
-
sqs,
|
|
2828
|
-
syncBatchFn: async () => {
|
|
2829
|
-
throw new Error("large targeted pull failed");
|
|
2830
|
-
},
|
|
2831
|
-
drainMaxMessages: total,
|
|
2832
|
-
sleep: fastSleep,
|
|
2833
|
-
publishMetric: async () => undefined,
|
|
2834
|
-
logger: makeDrainLogger(drains),
|
|
2835
|
-
});
|
|
2836
|
-
await receiver.start();
|
|
2837
|
-
await firstStarted;
|
|
2838
|
-
const parallelCallsBeforeRelease = maxActiveCalls;
|
|
2839
|
-
releaseVisibility();
|
|
2840
|
-
await waitFor(() => drains.length === 1);
|
|
2841
|
-
await receiver.dispose();
|
|
2842
|
-
expect(parallelCallsBeforeRelease).toBeGreaterThan(1);
|
|
2843
|
-
expect(parallelCallsBeforeRelease).toBeLessThan(total);
|
|
2844
|
-
expect(visibilityBatchCalls).toHaveLength(Math.ceil(total / 10));
|
|
2845
|
-
expect(visibilityBatchCalls.every((call) => call.receiptHandles.length <= 10)).toBe(true);
|
|
2846
|
-
expect(visibilityBatchCalls.flatMap((call) => call.receiptHandles)).toHaveLength(total);
|
|
2847
|
-
expect(visibilityBatchCalls.every((call) => call.visibilityTimeoutSeconds === 20)).toBe(true);
|
|
2848
|
-
expect(baseSqs.deleted).toEqual([]);
|
|
2849
|
-
});
|
|
2850
|
-
});
|
|
2851
|
-
//# sourceMappingURL=push-receiver.test.js.map
|