@indigoai-us/hq-cloud 6.18.35 → 6.18.37

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 (36) hide show
  1. package/dist/bin/push-failure-backoff.d.ts +22 -0
  2. package/dist/bin/push-failure-backoff.d.ts.map +1 -0
  3. package/dist/bin/push-failure-backoff.js +27 -0
  4. package/dist/bin/push-failure-backoff.js.map +1 -0
  5. package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js +9 -2
  6. package/dist/bin/sync-runner-delete-snapshot-mixed.bench.test.js.map +1 -1
  7. package/dist/bin/sync-runner-delete-snapshot.bench.test.js +3 -1
  8. package/dist/bin/sync-runner-delete-snapshot.bench.test.js.map +1 -1
  9. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-watch-loop.js +164 -34
  11. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  12. package/dist/bin/sync-runner.d.ts +11 -1
  13. package/dist/bin/sync-runner.d.ts.map +1 -1
  14. package/dist/bin/sync-runner.js +23 -4
  15. package/dist/bin/sync-runner.js.map +1 -1
  16. package/dist/bin/sync-runner.test.js +956 -4
  17. package/dist/bin/sync-runner.test.js.map +1 -1
  18. package/dist/lib/describe-error.d.ts +4 -0
  19. package/dist/lib/describe-error.d.ts.map +1 -1
  20. package/dist/lib/describe-error.js +37 -0
  21. package/dist/lib/describe-error.js.map +1 -1
  22. package/dist/lib/describe-error.test.js +18 -1
  23. package/dist/lib/describe-error.test.js.map +1 -1
  24. package/dist/outcome-telemetry.d.ts +16 -6
  25. package/dist/outcome-telemetry.d.ts.map +1 -1
  26. package/dist/outcome-telemetry.js +105 -11
  27. package/dist/outcome-telemetry.js.map +1 -1
  28. package/dist/outcome-telemetry.test.js +236 -0
  29. package/dist/outcome-telemetry.test.js.map +1 -1
  30. package/dist/sync/push-receiver.d.ts +5 -0
  31. package/dist/sync/push-receiver.d.ts.map +1 -1
  32. package/dist/sync/push-receiver.js +115 -52
  33. package/dist/sync/push-receiver.js.map +1 -1
  34. package/dist/sync/push-receiver.test.js +349 -2
  35. package/dist/sync/push-receiver.test.js.map +1 -1
  36. package/package.json +1 -1
@@ -90,6 +90,7 @@ class FakeSqs {
90
90
  visibilityFailure = null;
91
91
  visibilityBatchFailures = new Map();
92
92
  receiveCalls = 0;
93
+ receivedReceiptHandles = [];
93
94
  receiveWaitTimes = [];
94
95
  enqueue(messages) {
95
96
  this.batches.push({ messages, drainable: false });
@@ -120,8 +121,10 @@ class FakeSqs {
120
121
  if (this.released.length > 0 && args.waitTimeSeconds > 1) {
121
122
  const messages = this.released.splice(0);
122
123
  for (const message of messages) {
123
- if (message.ReceiptHandle)
124
+ if (message.ReceiptHandle) {
124
125
  this.inFlight.set(message.ReceiptHandle, message);
126
+ this.receivedReceiptHandles.push(message.ReceiptHandle);
127
+ }
125
128
  }
126
129
  return { messages };
127
130
  }
@@ -135,8 +138,10 @@ class FakeSqs {
135
138
  if (next.messages.length === 0)
136
139
  this.batches.shift();
137
140
  for (const message of messages) {
138
- if (message.ReceiptHandle)
141
+ if (message.ReceiptHandle) {
139
142
  this.inFlight.set(message.ReceiptHandle, message);
143
+ this.receivedReceiptHandles.push(message.ReceiptHandle);
144
+ }
140
145
  }
141
146
  return { messages };
142
147
  }
@@ -1922,6 +1927,348 @@ describe("US-009: createPushReceiver factory", () => {
1922
1927
  });
1923
1928
  });
1924
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
+ });
1925
2272
  const first = makeEvent({
1926
2273
  relativePath: "companies/indigo/retry-first.md",
1927
2274
  sequenceNumber: 101,