@indigoai-us/hq-cloud 5.25.0 → 5.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.github/workflows/ci.yml +34 -0
  2. package/dist/bin/sync-runner.d.ts +138 -1
  3. package/dist/bin/sync-runner.d.ts.map +1 -1
  4. package/dist/bin/sync-runner.js +288 -16
  5. package/dist/bin/sync-runner.js.map +1 -1
  6. package/dist/bin/sync-runner.test.js +372 -1
  7. package/dist/bin/sync-runner.test.js.map +1 -1
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/sync/feature-flags.d.ts +136 -0
  13. package/dist/sync/feature-flags.d.ts.map +1 -0
  14. package/dist/sync/feature-flags.js +160 -0
  15. package/dist/sync/feature-flags.js.map +1 -0
  16. package/dist/sync/feature-flags.test.d.ts +24 -0
  17. package/dist/sync/feature-flags.test.d.ts.map +1 -0
  18. package/dist/sync/feature-flags.test.js +330 -0
  19. package/dist/sync/feature-flags.test.js.map +1 -0
  20. package/dist/sync/index.d.ts +19 -0
  21. package/dist/sync/index.d.ts.map +1 -0
  22. package/dist/sync/index.js +13 -0
  23. package/dist/sync/index.js.map +1 -0
  24. package/dist/sync/logger.d.ts +61 -0
  25. package/dist/sync/logger.d.ts.map +1 -0
  26. package/dist/sync/logger.js +51 -0
  27. package/dist/sync/logger.js.map +1 -0
  28. package/dist/sync/logger.test.d.ts +19 -0
  29. package/dist/sync/logger.test.d.ts.map +1 -0
  30. package/dist/sync/logger.test.js +199 -0
  31. package/dist/sync/logger.test.js.map +1 -0
  32. package/dist/sync/metrics.d.ts +89 -0
  33. package/dist/sync/metrics.d.ts.map +1 -0
  34. package/dist/sync/metrics.js +105 -0
  35. package/dist/sync/metrics.js.map +1 -0
  36. package/dist/sync/metrics.test.d.ts +19 -0
  37. package/dist/sync/metrics.test.d.ts.map +1 -0
  38. package/dist/sync/metrics.test.js +280 -0
  39. package/dist/sync/metrics.test.js.map +1 -0
  40. package/dist/sync/push-event.d.ts +110 -0
  41. package/dist/sync/push-event.d.ts.map +1 -0
  42. package/dist/sync/push-event.js +153 -0
  43. package/dist/sync/push-event.js.map +1 -0
  44. package/dist/sync/push-event.test.d.ts +15 -0
  45. package/dist/sync/push-event.test.d.ts.map +1 -0
  46. package/dist/sync/push-event.test.js +188 -0
  47. package/dist/sync/push-event.test.js.map +1 -0
  48. package/dist/sync/push-receiver.d.ts +442 -0
  49. package/dist/sync/push-receiver.d.ts.map +1 -0
  50. package/dist/sync/push-receiver.js +782 -0
  51. package/dist/sync/push-receiver.js.map +1 -0
  52. package/dist/sync/push-receiver.test.d.ts +25 -0
  53. package/dist/sync/push-receiver.test.d.ts.map +1 -0
  54. package/dist/sync/push-receiver.test.js +477 -0
  55. package/dist/sync/push-receiver.test.js.map +1 -0
  56. package/dist/sync/push-transport.d.ts +150 -0
  57. package/dist/sync/push-transport.d.ts.map +1 -0
  58. package/dist/sync/push-transport.js +150 -0
  59. package/dist/sync/push-transport.js.map +1 -0
  60. package/dist/watcher.d.ts +271 -0
  61. package/dist/watcher.d.ts.map +1 -1
  62. package/dist/watcher.js +480 -3
  63. package/dist/watcher.js.map +1 -1
  64. package/dist/watcher.test.d.ts +2 -0
  65. package/dist/watcher.test.d.ts.map +1 -0
  66. package/dist/watcher.test.js +334 -0
  67. package/dist/watcher.test.js.map +1 -0
  68. package/package.json +10 -5
  69. package/src/bin/sync-runner.test.ts +487 -1
  70. package/src/bin/sync-runner.ts +406 -9
  71. package/src/index.ts +38 -0
  72. package/src/sync/feature-flags.test.ts +392 -0
  73. package/src/sync/feature-flags.ts +229 -0
  74. package/src/sync/index.ts +74 -0
  75. package/src/sync/logger.test.ts +241 -0
  76. package/src/sync/logger.ts +79 -0
  77. package/src/sync/metrics.test.ts +380 -0
  78. package/src/sync/metrics.ts +158 -0
  79. package/src/sync/push-event.test.ts +224 -0
  80. package/src/sync/push-event.ts +208 -0
  81. package/src/sync/push-receiver.test.ts +545 -0
  82. package/src/sync/push-receiver.ts +1077 -0
  83. package/src/sync/push-transport.ts +231 -0
  84. package/src/watcher.test.ts +388 -0
  85. package/src/watcher.ts +672 -4
  86. package/test/e2e/sync/cross-tenant-isolation.test.ts +502 -0
  87. package/test/e2e/watcher-real-chokidar.test.ts +105 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Unit tests for `src/sync/push-event.ts` (US-007 port).
3
+ *
4
+ * Covers the three required acceptance assertions:
5
+ * 1. A known-good fixture round-trips through encode → decode unchanged.
6
+ * 2. Unknown extra fields on the input are dropped silently (no throw).
7
+ * 3. Missing required fields throw a typed `PushEventDecodeError` whose
8
+ * `.issues` exposes the underlying zod issues.
9
+ *
10
+ * Also covers the supporting invariants needed to keep the wire contract
11
+ * stable across the watcher → server → receiver hop: malformed JSON, bad
12
+ * hash/timestamp formats, and the integer/range bounds on `sequenceNumber`.
13
+ */
14
+ import { describe, expect, it } from "vitest";
15
+ import { PushEventDecodeError, decodePushEvent, encodePushEvent, } from "../../src/sync/index.js";
16
+ // A canonical, valid PushEvent. All other tests derive from this fixture so
17
+ // any single field mutation can't accidentally pass for the wrong reason.
18
+ const validFixture = {
19
+ relativePath: "docs/architecture/overview.md",
20
+ contentHash: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
21
+ mtime: "2026-05-18T12:34:56.789Z",
22
+ originDeviceId: "device-laptop-a",
23
+ originTenantId: "tenant-indigo",
24
+ sequenceNumber: 42,
25
+ eventTimestamp: "2026-05-18T12:35:00.000Z",
26
+ };
27
+ describe("PushEvent encode/decode", () => {
28
+ // ── Acceptance #1: round-trip ──────────────────────────────────────────
29
+ it("round-trips a known-good fixture through encode → decode", () => {
30
+ const encoded = encodePushEvent(validFixture);
31
+ expect(typeof encoded).toBe("string");
32
+ const decoded = decodePushEvent(encoded);
33
+ expect(decoded).toEqual(validFixture);
34
+ // Re-encoding the decoded value must produce byte-identical output —
35
+ // catches accidental field re-ordering or coercion drift.
36
+ expect(encodePushEvent(decoded)).toBe(encoded);
37
+ });
38
+ it("decodes from a pre-parsed object as well as from a JSON string", () => {
39
+ const fromObject = decodePushEvent({ ...validFixture });
40
+ const fromString = decodePushEvent(JSON.stringify(validFixture));
41
+ expect(fromObject).toEqual(validFixture);
42
+ expect(fromString).toEqual(validFixture);
43
+ });
44
+ // ── Acceptance #2: unknown fields dropped ──────────────────────────────
45
+ it("drops unknown extra fields silently (does not throw)", () => {
46
+ const withExtras = {
47
+ ...validFixture,
48
+ // Future producers may add fields like `originAppVersion`; today's
49
+ // consumers must not crash on them.
50
+ originAppVersion: "1.2.3",
51
+ experimentalFlag: true,
52
+ nested: { ignored: "yes" },
53
+ };
54
+ const decoded = decodePushEvent(withExtras);
55
+ expect(decoded).toEqual(validFixture);
56
+ expect(decoded).not.toHaveProperty("originAppVersion");
57
+ expect(decoded).not.toHaveProperty("experimentalFlag");
58
+ expect(decoded).not.toHaveProperty("nested");
59
+ // Round-tripping through JSON behaves the same way.
60
+ const decodedFromJson = decodePushEvent(JSON.stringify(withExtras));
61
+ expect(decodedFromJson).toEqual(validFixture);
62
+ });
63
+ // ── Acceptance #3: missing required fields throw typed error ───────────
64
+ it.each([
65
+ "relativePath",
66
+ "contentHash",
67
+ "mtime",
68
+ "originDeviceId",
69
+ "originTenantId",
70
+ "sequenceNumber",
71
+ "eventTimestamp",
72
+ ])("throws PushEventDecodeError when %s is missing", (field) => {
73
+ const partial = { ...validFixture };
74
+ delete partial[field];
75
+ let caught;
76
+ try {
77
+ decodePushEvent(partial);
78
+ }
79
+ catch (err) {
80
+ caught = err;
81
+ }
82
+ expect(caught).toBeInstanceOf(PushEventDecodeError);
83
+ const error = caught;
84
+ expect(error.stage).toBe("schema-validation");
85
+ expect(error.issues.length).toBeGreaterThan(0);
86
+ // The zod issue path must point at the missing field — that's what
87
+ // downstream callers rely on to render structured diagnostics.
88
+ expect(error.issues.some((issue) => issue.path.includes(field))).toBe(true);
89
+ });
90
+ it("PushEventDecodeError carries the underlying zod issues array", () => {
91
+ // Multiple missing fields → multiple issues, all reachable via `.issues`.
92
+ const sparse = { relativePath: "x.md" };
93
+ let caught;
94
+ try {
95
+ decodePushEvent(sparse);
96
+ }
97
+ catch (err) {
98
+ caught = err;
99
+ }
100
+ expect(caught).toBeInstanceOf(PushEventDecodeError);
101
+ const error = caught;
102
+ expect(error.issues.length).toBeGreaterThanOrEqual(6);
103
+ });
104
+ // ── Supporting wire-contract invariants ────────────────────────────────
105
+ it("throws PushEventDecodeError on malformed JSON input", () => {
106
+ let caught;
107
+ try {
108
+ decodePushEvent("{not valid json");
109
+ }
110
+ catch (err) {
111
+ caught = err;
112
+ }
113
+ expect(caught).toBeInstanceOf(PushEventDecodeError);
114
+ const error = caught;
115
+ expect(error.stage).toBe("json-parse");
116
+ expect(error.issues.length).toBe(1);
117
+ });
118
+ it("surfaces a JSON-parseable non-object payload as schema-validation, not json-parse", () => {
119
+ // `'42'` is syntactically valid JSON, so the parse stage clears; the
120
+ // object-shape check is what fails. Pinning this here keeps the JSDoc
121
+ // contract (see decodePushEvent docs) honest.
122
+ let caught;
123
+ try {
124
+ decodePushEvent("42");
125
+ }
126
+ catch (err) {
127
+ caught = err;
128
+ }
129
+ expect(caught).toBeInstanceOf(PushEventDecodeError);
130
+ const error = caught;
131
+ expect(error.stage).toBe("schema-validation");
132
+ });
133
+ it.each([
134
+ ["raw hex without `sha256:` prefix", "e".repeat(64)],
135
+ ["wrong algorithm prefix", `md5:${"a".repeat(64)}`],
136
+ ["uppercase hex", `sha256:${"A".repeat(64)}`],
137
+ ["too few hex chars", `sha256:${"a".repeat(63)}`],
138
+ ])("rejects contentHash: %s", (_label, badHash) => {
139
+ expect(() => decodePushEvent({ ...validFixture, contentHash: badHash })).toThrow(PushEventDecodeError);
140
+ });
141
+ it.each([
142
+ ["missing timezone", "2026-05-18T12:34:56.789"],
143
+ ["space separator", "2026-05-18 12:34:56Z"],
144
+ ["date only", "2026-05-18"],
145
+ ])("rejects ISO-8601 timestamps: %s", (_label, badTimestamp) => {
146
+ expect(() => decodePushEvent({ ...validFixture, mtime: badTimestamp })).toThrow(PushEventDecodeError);
147
+ expect(() => decodePushEvent({ ...validFixture, eventTimestamp: badTimestamp })).toThrow(PushEventDecodeError);
148
+ });
149
+ it("accepts a sequenceNumber of 0 and rejects negative / fractional / oversized values", () => {
150
+ // 0 is allowed — sequence numbers are non-negative, not strictly positive.
151
+ expect(decodePushEvent({ ...validFixture, sequenceNumber: 0 })).toEqual({
152
+ ...validFixture,
153
+ sequenceNumber: 0,
154
+ });
155
+ expect(() => decodePushEvent({ ...validFixture, sequenceNumber: -1 })).toThrow(PushEventDecodeError);
156
+ expect(() => decodePushEvent({ ...validFixture, sequenceNumber: 1.5 })).toThrow(PushEventDecodeError);
157
+ expect(() => decodePushEvent({
158
+ ...validFixture,
159
+ sequenceNumber: Number.MAX_SAFE_INTEGER + 1,
160
+ })).toThrow(PushEventDecodeError);
161
+ });
162
+ it("encodePushEvent validates and drops unknown fields from the output", () => {
163
+ // We cast through `unknown` because the public type forbids extra keys —
164
+ // this models a producer that hands us a wider object by mistake.
165
+ const wider = {
166
+ ...validFixture,
167
+ stray: "field",
168
+ };
169
+ const encoded = encodePushEvent(wider);
170
+ expect(encoded.includes("stray")).toBe(false);
171
+ expect(JSON.parse(encoded)).toEqual(validFixture);
172
+ });
173
+ it("encodePushEvent throws PushEventDecodeError when input is invalid", () => {
174
+ const bad = { ...validFixture, contentHash: "not-a-hash" };
175
+ let caught;
176
+ try {
177
+ encodePushEvent(bad);
178
+ }
179
+ catch (err) {
180
+ caught = err;
181
+ }
182
+ expect(caught).toBeInstanceOf(PushEventDecodeError);
183
+ const error = caught;
184
+ expect(error.stage).toBe("schema-validation");
185
+ expect(error.issues.some((issue) => issue.path.includes("contentHash"))).toBe(true);
186
+ });
187
+ });
188
+ //# sourceMappingURL=push-event.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-event.test.js","sourceRoot":"","sources":["../../src/sync/push-event.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,442 @@
1
+ /**
2
+ * PushReceiver — inbound subscription seam for the hq-cloud watcher daemon
3
+ * (project event-driven-sync-menubar US-009).
4
+ *
5
+ * Mirrors {@link PushTransport} (`./push-transport.ts`) but for the opposite
6
+ * direction of travel: where the transport SHIPS local file changes out to the
7
+ * cloud, the receiver SUBSCRIBES to the tenant fanout and triggers an
8
+ * immediate, TARGETED local pull the moment a peer device of the same tenant
9
+ * publishes a change. Together they form the event-driven primary path; the
10
+ * existing `--poll-remote-ms` poll in `runRunnerWithLoop` is the safety net
11
+ * behind both.
12
+ *
13
+ * Transport: SNS → per-client SQS (US-000 decision)
14
+ * ─────────────────────────────────────────────────
15
+ * Per the US-000 transport investigation (companies/indigo/projects/
16
+ * event-driven-sync-menubar/references.md): reuse PR #112's SNS publish +
17
+ * DynamoDB catch-up log, and build the client RECEIVE side as a per-client
18
+ * SQS queue subscribed to `sync-push-{tenantId}`. The receiver long-polls its
19
+ * own queue, decodes each message body as a {@link PushEvent}, dedupes by
20
+ * `sequenceNumber` per `relativePath`, and bridges into the existing sync
21
+ * engine via an injected {@link SyncEngineFn} (→ targeted `runRunner` pull).
22
+ *
23
+ * The live queue is NOT provisioned yet (the server SQS-provisioning Lambda is
24
+ * an unbuilt follow-up — see references.md "Open items handed to the plan").
25
+ * So this module ships:
26
+ * - {@link SqsClientLike} — the narrow SQS surface the receiver depends on
27
+ * (`receiveMessage` / `deleteMessage`). Production callers pass an
28
+ * `@aws-sdk/client-sqs` `SQSClient` adapted to this shape; unit tests inject
29
+ * a fake. NO real AWS is required to exercise the receiver.
30
+ * - {@link SqsPushReceiver} — the real receiver. Long-polls the queue,
31
+ * dispatches each event through the shared dedupe path, deletes the message
32
+ * on successful handoff, and reconnects on transient `receiveMessage`
33
+ * failures with exponential backoff. SQS's own 14-day retention buffers
34
+ * messages while the device is offline → reconnect-replay is "free": on
35
+ * reconnect the poll loop simply resumes and the retained messages are
36
+ * redelivered, then dedupe skips anything already processed.
37
+ * - {@link NoopPushReceiver} — the dormant default. Flips `connected` on
38
+ * start, opens no subscription. Wired when the daemon runs without a real
39
+ * queue (or when the feature flag is OFF).
40
+ * - {@link createPushReceiver} — factory the daemon uses; returns the noop
41
+ * unless an SQS client + queue URL are supplied.
42
+ *
43
+ * Lifecycle (mirrors PushTransport)
44
+ * ─────────────────────────────────
45
+ * - `start()` opens the subscription (begins the long-poll loop). Awaited
46
+ * AFTER the watcher starts so a synthetic event can't race a half-built
47
+ * daemon. When the feature flag is OFF, `start()` is a no-op and `connected`
48
+ * stays false — NO queue is polled (dormant; AC#4).
49
+ * - On each received message: validate with {@link decodePushEvent} (defense
50
+ * in depth at the wire boundary), dedupe by `relativePath` against the
51
+ * highest `sequenceNumber` seen for that path, then call the injected
52
+ * {@link SyncEngineFn}. The sync engine is an injected seam — this story
53
+ * does NOT re-implement download logic; it bridges to `runRunner` pull.
54
+ * - `dispose()` aborts in-flight via AbortController, stops the poll loop,
55
+ * awaits the in-flight sync up to a drain deadline, then disconnects.
56
+ *
57
+ * Dedupe (AC#3)
58
+ * ─────────────
59
+ * A per-`relativePath` map of the highest `sequenceNumber` already passed to
60
+ * `syncFn`. An incoming event with `sequenceNumber <= seen` is skipped. SQS
61
+ * at-least-once delivery + reconnect-replay means the SAME event can arrive
62
+ * twice; dedupe makes that idempotent.
63
+ *
64
+ * Disconnect / reconnect with catch-up replay (AC#3/#4)
65
+ * ─────────────────────────────────────────────────────
66
+ * `receiveMessage` failures (network blip, throttling) are caught; the loop
67
+ * backs off (exponential + jitter, capped) and resumes. Because the per-client
68
+ * SQS queue retains undelivered messages for 14 days, anything published while
69
+ * the device was offline/disconnected is redelivered when the poll resumes —
70
+ * catch-up replay with no server round-trip. Redelivered-but-already-processed
71
+ * events are absorbed by the dedupe path. The in-memory fake's
72
+ * `simulateDisconnect()` / `simulateReconnect()` model exactly this buffering.
73
+ *
74
+ * Feature flag (AC#4)
75
+ * ───────────────────
76
+ * Gated by the per-tenant {@link EventDrivenPushFlagProvider} (US-008's
77
+ * feature-flags.ts) — honors `HQ_SYNC_EVENT_DRIVEN_PUSH_ENABLED_TENANTS`
78
+ * (per-tenant) + the legacy global `HQ_SYNC_EVENT_DRIVEN_PUSH_ENABLED=true`.
79
+ * Default DISABLED. Resolution precedence: explicit `enabled` > injected
80
+ * `flagProvider.isEnabled(tenantId)` > default env-driven provider.
81
+ *
82
+ * Cross-tenant isolation (US-010)
83
+ * ───────────────────────────────
84
+ * Each receiver instance binds exactly ONE `tenantId` and polls exactly ONE
85
+ * queue URL (its own tenant's per-client queue). Isolation is enforced at the
86
+ * subscription boundary — the receiver never reads another tenant's queue, and
87
+ * never filters cross-tenant data post-hoc.
88
+ *
89
+ * @see ./push-transport.ts (the outbound seam this mirrors)
90
+ * @see ./feature-flags.ts (the per-tenant flag provider — US-008)
91
+ * @see ../bin/sync-runner.ts (the wiring site — runRunnerWithLoop)
92
+ * @see companies/indigo/projects/event-driven-sync-menubar/references.md (US-000)
93
+ *
94
+ * Adapted from indigoai-us/hq-pro PR #112 (src/sync/push-receiver.ts) into
95
+ * @indigoai-us/hq-cloud (Path B). The hq-pro source shipped only Noop +
96
+ * InMemory receivers (the production SQS path was deferred there); this module
97
+ * builds the real SQS receiver behind the same lifecycle/dedupe/flag seam.
98
+ */
99
+ import { type PushEvent } from "./push-event.js";
100
+ import { type EventDrivenPushFlagProvider } from "./feature-flags.js";
101
+ import { type SyncLatencyMetric } from "./metrics.js";
102
+ /**
103
+ * How long `dispose()` awaits an in-flight `syncFn` after aborting its signal,
104
+ * before abandoning it (the poll/cadence safety net re-pulls on the next tick).
105
+ */
106
+ export declare const DEFAULT_RECEIVER_DISPOSE_DRAIN_MS = 5000;
107
+ /** Default SQS long-poll wait (seconds). 20 is the SQS max — true long-poll. */
108
+ export declare const DEFAULT_WAIT_TIME_SECONDS = 20;
109
+ /** Default max messages pulled per `receiveMessage` call (SQS max is 10). */
110
+ export declare const DEFAULT_MAX_MESSAGES = 10;
111
+ /** Reconnect backoff defaults. */
112
+ export declare const DEFAULT_RECONNECT_INITIAL_MS = 250;
113
+ export declare const DEFAULT_RECONNECT_MAX_MS = 30000;
114
+ /**
115
+ * One SQS message as the receiver consumes it. A structural subset of the AWS
116
+ * SDK `Message` so a real `SQSClient` response satisfies it without adaptation
117
+ * and tests can build literals.
118
+ */
119
+ export interface SqsMessageLike {
120
+ /** The message payload — a JSON-encoded {@link PushEvent}. */
121
+ readonly Body?: string;
122
+ /** Opaque handle used to delete the message after successful handoff. */
123
+ readonly ReceiptHandle?: string;
124
+ /** Optional SQS message id (logged for diagnostics). */
125
+ readonly MessageId?: string;
126
+ }
127
+ /**
128
+ * The narrow SQS client surface the receiver depends on. The AWS SDK
129
+ * `SQSClient` does NOT match this shape directly (it exposes a single
130
+ * `send(command)`); production callers adapt it with a thin wrapper (see
131
+ * {@link sqsClientFromAwsSdk} in the wiring site / tests). Keeping the seam
132
+ * this narrow means unit tests inject a hand-written fake with zero AWS deps.
133
+ */
134
+ export interface SqsClientLike {
135
+ /**
136
+ * Long-poll the queue. Resolves with zero or more messages. MUST honor the
137
+ * abort signal (resolve/reject promptly on abort) so `dispose()` doesn't
138
+ * block on an in-flight 20s long-poll.
139
+ */
140
+ receiveMessage(args: {
141
+ queueUrl: string;
142
+ maxMessages: number;
143
+ waitTimeSeconds: number;
144
+ signal: AbortSignal;
145
+ }): Promise<{
146
+ messages: SqsMessageLike[];
147
+ }>;
148
+ /** Delete a successfully-handled message so it isn't redelivered. */
149
+ deleteMessage(args: {
150
+ queueUrl: string;
151
+ receiptHandle: string;
152
+ }): Promise<void>;
153
+ }
154
+ /**
155
+ * Context handed to {@link SyncEngineFn} on every received event.
156
+ *
157
+ * - `event` — the validated, deduped PushEvent. `relativePath` is what the
158
+ * sync engine pulls; `sequenceNumber` is for observability.
159
+ * - `signal` — aborts when `dispose()` runs past its drain deadline. A
160
+ * well-behaved sync fn checks `signal.aborted` between stages and returns
161
+ * early.
162
+ */
163
+ export interface PushReceiverContext {
164
+ readonly event: PushEvent;
165
+ readonly signal: AbortSignal;
166
+ }
167
+ /**
168
+ * The injected sync function. The receiver does NOT perform the actual fetch —
169
+ * it hands off the relativePath to whatever the deployment supplies. In
170
+ * production this bridges to a targeted `runRunner` pull for the affected
171
+ * company/path; in tests it's a fake recording invocations.
172
+ *
173
+ * Errors from `syncFn` are CAUGHT by the receiver — they log and the loop
174
+ * continues. A misbehaving sync fn cannot crash the receiver. The poll/cadence
175
+ * safety net is the recovery path for a thrown pull.
176
+ */
177
+ export type SyncEngineFn = (ctx: PushReceiverContext) => Promise<void>;
178
+ /**
179
+ * Best-effort CloudWatch metric publish seam (US-011). Invoked on the
180
+ * receive-SUCCESS path with the measured save-on-A → visible-on-B latency.
181
+ * Defaults to {@link publishSyncLatencyMetric} (the module singleton client);
182
+ * tests inject a spy so no real AWS is touched. The receiver awaits it inside a
183
+ * try/catch — a metric failure can never crash the loop (it's also best-effort
184
+ * inside the default impl).
185
+ */
186
+ export type PublishMetricFn = (metric: SyncLatencyMetric) => Promise<void>;
187
+ /** Minimal structured logger. Defaults to a no-op (quiet daemon). */
188
+ export interface ReceiverLogger {
189
+ info(obj: Record<string, unknown>, msg?: string): void;
190
+ warn(obj: Record<string, unknown>, msg?: string): void;
191
+ error(obj: Record<string, unknown>, msg?: string): void;
192
+ debug(obj: Record<string, unknown>, msg?: string): void;
193
+ }
194
+ /**
195
+ * Lifecycle handle. Mirrors {@link PushTransport} so daemon wiring is
196
+ * mechanically identical on both sides.
197
+ */
198
+ export interface PushReceiver {
199
+ /** Open the subscription / poll loop. No-op when the feature flag is OFF. */
200
+ start(): Promise<void>;
201
+ /** Idempotent teardown — stop polling, abort + drain in-flight, disconnect. */
202
+ dispose(): Promise<void>;
203
+ /** Advisory: is the subscription currently believed to be open? */
204
+ readonly connected: boolean;
205
+ }
206
+ /**
207
+ * Default `PushReceiver` used when no real queue is wired (or the flag is OFF
208
+ * at the factory). `start()` flips `connected` true; `dispose()` flips it
209
+ * false. No subscription work, no events. Mirrors {@link NoopPushTransport}.
210
+ */
211
+ export declare class NoopPushReceiver implements PushReceiver {
212
+ private _connected;
213
+ get connected(): boolean;
214
+ start(): Promise<void>;
215
+ dispose(): Promise<void>;
216
+ }
217
+ /**
218
+ * Configuration for {@link SqsPushReceiver}.
219
+ */
220
+ export interface SqsPushReceiverOptions {
221
+ /**
222
+ * Tenant id this receiver subscribes to. Each instance binds exactly one
223
+ * tenant — cross-tenant isolation is enforced by the subscription boundary
224
+ * (this queue belongs to this tenant), not post-hoc filtering. (US-010)
225
+ */
226
+ tenantId: string;
227
+ /**
228
+ * The caller's own per-tenant SQS queue URL (minted server-side by the
229
+ * provisioning Lambda and subscribed to `sync-push-{tenantId}`). The
230
+ * receiver polls ONLY this URL.
231
+ */
232
+ queueUrl: string;
233
+ /** The injected SQS client. Tests pass a fake; production an SDK adapter. */
234
+ sqs: SqsClientLike;
235
+ /**
236
+ * The sync engine that performs the actual targeted pull. The receiver only
237
+ * invokes this; errors are logged + isolated from the loop.
238
+ */
239
+ syncFn: SyncEngineFn;
240
+ /** Structured logger. Default: a no-op (quiet). */
241
+ logger?: ReceiverLogger;
242
+ /**
243
+ * Feature-flag override — wins over provider + env when set explicitly.
244
+ * When the resolved value is false, `start()` is a no-op, `connected` stays
245
+ * false, and NO queue is polled. (AC#4)
246
+ */
247
+ enabled?: boolean;
248
+ /** Per-tenant flag provider (US-008). Consulted when `enabled` is unset. */
249
+ flagProvider?: EventDrivenPushFlagProvider;
250
+ /** Env snapshot for flag resolution. Default: `process.env`. */
251
+ env?: Record<string, string | undefined>;
252
+ /** SQS long-poll wait seconds. Default {@link DEFAULT_WAIT_TIME_SECONDS}. */
253
+ waitTimeSeconds?: number;
254
+ /** Max messages per receive. Default {@link DEFAULT_MAX_MESSAGES}. */
255
+ maxMessages?: number;
256
+ /** Max time `dispose()` waits for an in-flight syncFn after abort. */
257
+ disposeDrainMs?: number;
258
+ /** Reconnect backoff config. */
259
+ reconnect?: {
260
+ initialMs?: number;
261
+ maxMs?: number;
262
+ jitter?: boolean;
263
+ };
264
+ /**
265
+ * Sleep seam for backoff (tests inject a fast/abortable sleep). Default:
266
+ * host `setTimeout` that resolves early on abort.
267
+ */
268
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
269
+ /**
270
+ * Best-effort latency-metric publish (US-011). Called on the receive-success
271
+ * path with the measured save→visible latency. Default:
272
+ * {@link publishSyncLatencyMetric}; tests inject a spy. (AC#1/#3)
273
+ */
274
+ publishMetric?: PublishMetricFn;
275
+ /**
276
+ * Clock for latency measurement + metric timestamps. Default
277
+ * `() => Date.now()`. Tests inject a fake clock to assert the latency value.
278
+ */
279
+ now?: () => number;
280
+ }
281
+ /**
282
+ * Real client `PushReceiver` backed by a per-tenant SQS queue.
283
+ *
284
+ * Poll loop: long-poll `receiveMessage` → for each message, decode + dedupe +
285
+ * dispatch through `syncFn`, then `deleteMessage` on successful handoff. A
286
+ * `receiveMessage` rejection is treated as a transient disconnect: log, back
287
+ * off, resume. SQS retention covers offline catch-up; dedupe covers redelivery.
288
+ */
289
+ export declare class SqsPushReceiver implements PushReceiver {
290
+ private readonly tenantId;
291
+ private readonly queueUrl;
292
+ private readonly sqs;
293
+ private readonly syncFn;
294
+ private readonly logger;
295
+ private readonly enabled;
296
+ private readonly waitTimeSeconds;
297
+ private readonly maxMessages;
298
+ private readonly disposeDrainMs;
299
+ private readonly reconnectInitialMs;
300
+ private readonly reconnectMaxMs;
301
+ private readonly reconnectJitter;
302
+ private readonly sleep;
303
+ private readonly publishMetric;
304
+ private readonly now;
305
+ private _connected;
306
+ private disposed;
307
+ private disposing;
308
+ private disposePromise;
309
+ /** Abort signal shared by the poll loop + in-flight sync; fired on dispose. */
310
+ private loopAbort;
311
+ /** The running poll loop promise; awaited (best-effort) during dispose. */
312
+ private loopPromise;
313
+ /** AbortController for the in-flight syncFn; refreshed each dispatch. */
314
+ private inFlightAbort;
315
+ private inFlightSync;
316
+ /** Per-path highest sequence number already PROCESSED by syncFn. */
317
+ private readonly seenSequencePerPath;
318
+ private _processedCount;
319
+ private _dedupedCount;
320
+ private _decodeFailureCount;
321
+ private _receiveErrorCount;
322
+ constructor(opts: SqsPushReceiverOptions);
323
+ get connected(): boolean;
324
+ start(): Promise<void>;
325
+ dispose(): Promise<void>;
326
+ /** Events that passed dedupe AND completed `syncFn` successfully. */
327
+ get processedCount(): number;
328
+ /** Events skipped by dedupe. */
329
+ get dedupedCount(): number;
330
+ /** Events dropped at the wire-boundary decode step. */
331
+ get decodeFailureCount(): number;
332
+ /** `receiveMessage` failures (transient disconnects) the loop recovered from. */
333
+ get receiveErrorCount(): number;
334
+ /**
335
+ * The long-poll loop. Runs until the loop abort signal fires (dispose). A
336
+ * `receiveMessage` rejection is a transient disconnect — log, back off,
337
+ * resume. Because the SQS queue retains messages, resuming after a blip
338
+ * replays the gap (catch-up). The loop never throws past this method; it
339
+ * is fire-and-forgotten by `start()` and awaited best-effort by `dispose()`.
340
+ */
341
+ private pollLoop;
342
+ /**
343
+ * Decode → dedupe → dispatch → delete a single SQS message. Decode failures
344
+ * and syncFn throws are logged + absorbed (never crash the loop). The message
345
+ * is deleted only after a successful handoff so an unprocessed message stays
346
+ * on the queue for redelivery (the dedupe path makes redelivery idempotent).
347
+ */
348
+ private handleMessage;
349
+ /**
350
+ * Dedupe + invoke `syncFn`. Returns true once the event has been accounted
351
+ * for (deduped, or syncFn settled) so the caller can delete the message.
352
+ * Stores the in-flight promise so `dispose()` can drain it.
353
+ */
354
+ private dispatch;
355
+ /** Delete a message, swallowing transport errors (redelivery is harmless). */
356
+ private safeDelete;
357
+ /**
358
+ * Publish one best-effort latency datum (US-011). Awaits `publishMetric` and
359
+ * swallows any rejection so a metric-backend outage can never reach the poll
360
+ * loop. Called fire-and-forget (`void`) off the dispatch critical path.
361
+ */
362
+ private emitLatencyMetric;
363
+ /** Exponential backoff (capped) with optional full-jitter. */
364
+ private computeBackoff;
365
+ }
366
+ /**
367
+ * A tiny in-process fanout the {@link InMemoryPushReceiver} subscribes against.
368
+ * Models SNS publish + the per-client SQS queue's disconnect buffering so unit
369
+ * tests can drive the receive path without AWS. `publish` raw strings (the
370
+ * wire form) so decode-failure paths are testable too.
371
+ */
372
+ export declare class InMemoryFanout {
373
+ private readonly subscribers;
374
+ subscribe(handler: (raw: string) => void): () => void;
375
+ /** Publish a raw (already-encoded) message body to all subscribers. */
376
+ publish(raw: string): void;
377
+ }
378
+ /** Options for {@link InMemoryPushReceiver}. */
379
+ export interface InMemoryPushReceiverOptions {
380
+ tenantId: string;
381
+ fanout: InMemoryFanout;
382
+ syncFn: SyncEngineFn;
383
+ logger?: ReceiverLogger;
384
+ enabled?: boolean;
385
+ flagProvider?: EventDrivenPushFlagProvider;
386
+ env?: Record<string, string | undefined>;
387
+ disposeDrainMs?: number;
388
+ }
389
+ /**
390
+ * In-memory receiver paired with {@link InMemoryFanout}. Powers the unit
391
+ * tests for dedupe, reconnect-replay, flag gating, and dispose-drain WITHOUT
392
+ * any AWS SDK. The dedupe / dispatch / dispose semantics are identical to
393
+ * {@link SqsPushReceiver} (shared design); the disconnect buffer is the
394
+ * in-process analogue of the per-client SQS queue's 14-day retention.
395
+ */
396
+ export declare class InMemoryPushReceiver implements PushReceiver {
397
+ private readonly tenantId;
398
+ private readonly fanout;
399
+ private readonly syncFn;
400
+ private readonly logger;
401
+ private readonly enabled;
402
+ private readonly disposeDrainMs;
403
+ private _connected;
404
+ private disposed;
405
+ private disposing;
406
+ private disposePromise;
407
+ private unsubscribe;
408
+ private disconnectedFlag;
409
+ private readonly pendingDuringDisconnect;
410
+ private readonly seenSequencePerPath;
411
+ private inFlightAbort;
412
+ private inFlightSync;
413
+ private _processedCount;
414
+ private _dedupedCount;
415
+ private _decodeFailureCount;
416
+ constructor(opts: InMemoryPushReceiverOptions);
417
+ get connected(): boolean;
418
+ start(): Promise<void>;
419
+ dispose(): Promise<void>;
420
+ get processedCount(): number;
421
+ get dedupedCount(): number;
422
+ get decodeFailureCount(): number;
423
+ get bufferedCount(): number;
424
+ /** Emulate a network blip — events buffer instead of dispatching. */
425
+ simulateDisconnect(): void;
426
+ /** Emulate reconnect — drain the buffer through the same dedupe path. */
427
+ simulateReconnect(): void;
428
+ private dispatch;
429
+ }
430
+ /**
431
+ * Build a PushReceiver. The daemon defaults to the noop so wiring is
432
+ * regression-safe — production deployments wire the SQS impl explicitly once
433
+ * the server provisioning Lambda mints a queue URL. Mirrors PushTransport's
434
+ * noop-default opt-in posture.
435
+ */
436
+ export type CreatePushReceiverOptions = (SqsPushReceiverOptions & {
437
+ kind?: "sqs";
438
+ }) | {
439
+ kind: "noop";
440
+ };
441
+ export declare function createPushReceiver(opts: CreatePushReceiverOptions): PushReceiver;
442
+ //# sourceMappingURL=push-receiver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-receiver.d.ts","sourceRoot":"","sources":["../../src/sync/push-receiver.ts"],"names":[],"mappings":"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"}