@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.
- package/.github/workflows/ci.yml +34 -0
- package/dist/bin/sync-runner.d.ts +138 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +288 -16
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +372 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/sync/feature-flags.d.ts +136 -0
- package/dist/sync/feature-flags.d.ts.map +1 -0
- package/dist/sync/feature-flags.js +160 -0
- package/dist/sync/feature-flags.js.map +1 -0
- package/dist/sync/feature-flags.test.d.ts +24 -0
- package/dist/sync/feature-flags.test.d.ts.map +1 -0
- package/dist/sync/feature-flags.test.js +330 -0
- package/dist/sync/feature-flags.test.js.map +1 -0
- package/dist/sync/index.d.ts +19 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +13 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/logger.d.ts +61 -0
- package/dist/sync/logger.d.ts.map +1 -0
- package/dist/sync/logger.js +51 -0
- package/dist/sync/logger.js.map +1 -0
- package/dist/sync/logger.test.d.ts +19 -0
- package/dist/sync/logger.test.d.ts.map +1 -0
- package/dist/sync/logger.test.js +199 -0
- package/dist/sync/logger.test.js.map +1 -0
- package/dist/sync/metrics.d.ts +89 -0
- package/dist/sync/metrics.d.ts.map +1 -0
- package/dist/sync/metrics.js +105 -0
- package/dist/sync/metrics.js.map +1 -0
- package/dist/sync/metrics.test.d.ts +19 -0
- package/dist/sync/metrics.test.d.ts.map +1 -0
- package/dist/sync/metrics.test.js +280 -0
- package/dist/sync/metrics.test.js.map +1 -0
- package/dist/sync/push-event.d.ts +110 -0
- package/dist/sync/push-event.d.ts.map +1 -0
- package/dist/sync/push-event.js +153 -0
- package/dist/sync/push-event.js.map +1 -0
- package/dist/sync/push-event.test.d.ts +15 -0
- package/dist/sync/push-event.test.d.ts.map +1 -0
- package/dist/sync/push-event.test.js +188 -0
- package/dist/sync/push-event.test.js.map +1 -0
- package/dist/sync/push-receiver.d.ts +442 -0
- package/dist/sync/push-receiver.d.ts.map +1 -0
- package/dist/sync/push-receiver.js +782 -0
- package/dist/sync/push-receiver.js.map +1 -0
- package/dist/sync/push-receiver.test.d.ts +25 -0
- package/dist/sync/push-receiver.test.d.ts.map +1 -0
- package/dist/sync/push-receiver.test.js +477 -0
- package/dist/sync/push-receiver.test.js.map +1 -0
- package/dist/sync/push-transport.d.ts +150 -0
- package/dist/sync/push-transport.d.ts.map +1 -0
- package/dist/sync/push-transport.js +150 -0
- package/dist/sync/push-transport.js.map +1 -0
- package/dist/watcher.d.ts +271 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +480 -3
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.d.ts +2 -0
- package/dist/watcher.test.d.ts.map +1 -0
- package/dist/watcher.test.js +334 -0
- package/dist/watcher.test.js.map +1 -0
- package/package.json +10 -5
- package/src/bin/sync-runner.test.ts +487 -1
- package/src/bin/sync-runner.ts +406 -9
- package/src/index.ts +38 -0
- package/src/sync/feature-flags.test.ts +392 -0
- package/src/sync/feature-flags.ts +229 -0
- package/src/sync/index.ts +74 -0
- package/src/sync/logger.test.ts +241 -0
- package/src/sync/logger.ts +79 -0
- package/src/sync/metrics.test.ts +380 -0
- package/src/sync/metrics.ts +158 -0
- package/src/sync/push-event.test.ts +224 -0
- package/src/sync/push-event.ts +208 -0
- package/src/sync/push-receiver.test.ts +545 -0
- package/src/sync/push-receiver.ts +1077 -0
- package/src/sync/push-transport.ts +231 -0
- package/src/watcher.test.ts +388 -0
- package/src/watcher.ts +672 -4
- package/test/e2e/sync/cross-tenant-isolation.test.ts +502 -0
- package/test/e2e/watcher-real-chokidar.test.ts +105 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* US-011 — unit tests for `src/sync/metrics.ts` + the receive-success-path
|
|
3
|
+
* metric emission wired into {@link SqsPushReceiver}.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the hq-pro PR #112 metrics test:
|
|
6
|
+
* - mock the CloudWatchClient via `aws-sdk-client-mock`
|
|
7
|
+
* - inject the mocked client via the `_setSyncCloudWatchClient` test seam
|
|
8
|
+
* - assert namespace, metric name, unit, dimensions, value
|
|
9
|
+
* - assert SDK errors are CAUGHT (the receive loop must never crash on a
|
|
10
|
+
* CloudWatch outage)
|
|
11
|
+
*
|
|
12
|
+
* Plus the US-011-specific receiver wiring:
|
|
13
|
+
* - on the receive-SUCCESS path, the receiver publishes exactly one latency
|
|
14
|
+
* datum carrying the event's tenantId + sequenceNumber (mocked publish seam
|
|
15
|
+
* — no real AWS)
|
|
16
|
+
* - a failing publish seam does NOT crash the receiver loop
|
|
17
|
+
*/
|
|
18
|
+
import { Writable } from "node:stream";
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
20
|
+
import { mockClient } from "aws-sdk-client-mock";
|
|
21
|
+
import { CloudWatchClient, PutMetricDataCommand, } from "@aws-sdk/client-cloudwatch";
|
|
22
|
+
import { SYNC_LATENCY_METRIC_NAME, SYNC_METRIC_NAMESPACE, _setSyncCloudWatchClient, publishSyncLatencyMetric, } from "./metrics.js";
|
|
23
|
+
import { createLogger } from "./logger.js";
|
|
24
|
+
import { encodePushEvent } from "./push-event.js";
|
|
25
|
+
import { SqsPushReceiver, } from "./push-receiver.js";
|
|
26
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
27
|
+
function makeMetric(overrides = {}) {
|
|
28
|
+
return {
|
|
29
|
+
tenantId: "tenant-A",
|
|
30
|
+
relativePath: "docs/overview.md",
|
|
31
|
+
sequenceNumber: 42,
|
|
32
|
+
latencySeconds: 1.234,
|
|
33
|
+
timestamp: new Date("2026-05-19T12:00:00.000Z"),
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function captureStream() {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
const stream = new Writable({
|
|
40
|
+
write(chunk, _enc, cb) {
|
|
41
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
42
|
+
cb();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
stream,
|
|
47
|
+
lines: () => chunks
|
|
48
|
+
.join("")
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter((s) => s.length > 0)
|
|
51
|
+
.map((s) => JSON.parse(s)),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const TENANT = "tenant-indigo";
|
|
55
|
+
const QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/sync-push-indigo-deviceB";
|
|
56
|
+
function makeEvent(overrides = {}) {
|
|
57
|
+
return {
|
|
58
|
+
relativePath: "companies/indigo/notes.md",
|
|
59
|
+
contentHash: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
60
|
+
mtime: "2026-05-21T12:34:56.000Z",
|
|
61
|
+
originDeviceId: "device-A",
|
|
62
|
+
originTenantId: TENANT,
|
|
63
|
+
sequenceNumber: 7,
|
|
64
|
+
eventTimestamp: "2026-05-21T12:34:56.000Z",
|
|
65
|
+
...overrides,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Minimal fake SQS that drains one batch then idles until abort. */
|
|
69
|
+
class OneBatchSqs {
|
|
70
|
+
batch;
|
|
71
|
+
deleted = [];
|
|
72
|
+
constructor(messages) {
|
|
73
|
+
this.batch = messages;
|
|
74
|
+
}
|
|
75
|
+
async receiveMessage(args) {
|
|
76
|
+
if (this.batch) {
|
|
77
|
+
const b = this.batch;
|
|
78
|
+
this.batch = null;
|
|
79
|
+
return { messages: b };
|
|
80
|
+
}
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
if (args.signal.aborted)
|
|
83
|
+
return resolve({ messages: [] });
|
|
84
|
+
const t = setTimeout(() => resolve({ messages: [] }), 5);
|
|
85
|
+
t.unref?.();
|
|
86
|
+
args.signal.addEventListener("abort", () => {
|
|
87
|
+
clearTimeout(t);
|
|
88
|
+
resolve({ messages: [] });
|
|
89
|
+
}, { once: true });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async deleteMessage(args) {
|
|
93
|
+
this.deleted.push(args.receiptHandle);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Wait until `predicate()` is true (polling), or throw after `timeoutMs`. */
|
|
97
|
+
async function until(predicate, timeoutMs = 1000) {
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
while (!predicate()) {
|
|
100
|
+
if (Date.now() - start > timeoutMs) {
|
|
101
|
+
throw new Error("until() timed out");
|
|
102
|
+
}
|
|
103
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ── publishSyncLatencyMetric (CloudWatch contract) ───────────────────────────
|
|
107
|
+
const cwMock = mockClient(CloudWatchClient);
|
|
108
|
+
describe("publishSyncLatencyMetric", () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
cwMock.reset();
|
|
111
|
+
_setSyncCloudWatchClient(cwMock);
|
|
112
|
+
});
|
|
113
|
+
it("sends a single PutMetricData with the documented namespace + metric name", async () => {
|
|
114
|
+
cwMock.on(PutMetricDataCommand).resolves({});
|
|
115
|
+
await publishSyncLatencyMetric(makeMetric());
|
|
116
|
+
const calls = cwMock.commandCalls(PutMetricDataCommand);
|
|
117
|
+
expect(calls).toHaveLength(1);
|
|
118
|
+
const input = calls[0].args[0].input;
|
|
119
|
+
expect(input.Namespace).toBe(SYNC_METRIC_NAMESPACE);
|
|
120
|
+
expect(input.Namespace).toBe("HQPro/Sync");
|
|
121
|
+
expect(input.MetricData).toHaveLength(1);
|
|
122
|
+
expect(input.MetricData[0].MetricName).toBe(SYNC_LATENCY_METRIC_NAME);
|
|
123
|
+
expect(input.MetricData[0].MetricName).toBe("hq-cloud.sync.p95_latency_seconds");
|
|
124
|
+
});
|
|
125
|
+
it("uses Unit=Seconds and stamps the supplied timestamp", async () => {
|
|
126
|
+
cwMock.on(PutMetricDataCommand).resolves({});
|
|
127
|
+
const ts = new Date("2026-05-19T07:00:00.000Z");
|
|
128
|
+
await publishSyncLatencyMetric(makeMetric({ timestamp: ts }));
|
|
129
|
+
const datum = cwMock.commandCalls(PutMetricDataCommand)[0].args[0].input
|
|
130
|
+
.MetricData[0];
|
|
131
|
+
expect(datum.Unit).toBe("Seconds");
|
|
132
|
+
expect(datum.Timestamp).toEqual(ts);
|
|
133
|
+
});
|
|
134
|
+
it("publishes the observed latencySeconds value verbatim", async () => {
|
|
135
|
+
cwMock.on(PutMetricDataCommand).resolves({});
|
|
136
|
+
await publishSyncLatencyMetric(makeMetric({ latencySeconds: 2.71828 }));
|
|
137
|
+
const datum = cwMock.commandCalls(PutMetricDataCommand)[0].args[0].input
|
|
138
|
+
.MetricData[0];
|
|
139
|
+
expect(datum.Value).toBe(2.71828);
|
|
140
|
+
});
|
|
141
|
+
it("attaches a `TenantId` dimension carrying the tenantId", async () => {
|
|
142
|
+
cwMock.on(PutMetricDataCommand).resolves({});
|
|
143
|
+
await publishSyncLatencyMetric(makeMetric({ tenantId: "tenant-prs_xyz" }));
|
|
144
|
+
const datum = cwMock.commandCalls(PutMetricDataCommand)[0].args[0].input
|
|
145
|
+
.MetricData[0];
|
|
146
|
+
expect(datum.Dimensions).toEqual([
|
|
147
|
+
{ Name: "TenantId", Value: "tenant-prs_xyz" },
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
it("does not throw when the CloudWatch SDK rejects (sync loop must not crash)", async () => {
|
|
151
|
+
cwMock
|
|
152
|
+
.on(PutMetricDataCommand)
|
|
153
|
+
.rejects(new Error("CloudWatch unavailable"));
|
|
154
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
155
|
+
await expect(publishSyncLatencyMetric(makeMetric())).resolves.toBeUndefined();
|
|
156
|
+
expect(consoleSpy).toHaveBeenCalledWith("Failed to publish sync latency metric to CloudWatch:", "CloudWatch unavailable");
|
|
157
|
+
consoleSpy.mockRestore();
|
|
158
|
+
});
|
|
159
|
+
it("routes SDK failures through the supplied pino logger when provided", async () => {
|
|
160
|
+
cwMock.on(PutMetricDataCommand).rejects(new Error("ThrottlingException"));
|
|
161
|
+
const { stream, lines } = captureStream();
|
|
162
|
+
const logger = createLogger({
|
|
163
|
+
component: "metrics-test",
|
|
164
|
+
destination: stream,
|
|
165
|
+
level: "debug",
|
|
166
|
+
});
|
|
167
|
+
await publishSyncLatencyMetric(makeMetric({ sequenceNumber: 99, relativePath: "a.md" }), { logger });
|
|
168
|
+
const failureLine = lines().find((l) => l.event === "sync.metric.publish_failed");
|
|
169
|
+
expect(failureLine).toBeDefined();
|
|
170
|
+
expect(failureLine).toMatchObject({
|
|
171
|
+
event: "sync.metric.publish_failed",
|
|
172
|
+
tenantId: "tenant-A",
|
|
173
|
+
relativePath: "a.md",
|
|
174
|
+
sequenceNumber: 99,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
it("accepts a per-call client override (does not call the singleton)", async () => {
|
|
178
|
+
cwMock
|
|
179
|
+
.on(PutMetricDataCommand)
|
|
180
|
+
.rejects(new Error("singleton should not be used"));
|
|
181
|
+
const overrideMock = mockClient(CloudWatchClient);
|
|
182
|
+
overrideMock.on(PutMetricDataCommand).resolves({});
|
|
183
|
+
await publishSyncLatencyMetric(makeMetric(), {
|
|
184
|
+
client: overrideMock,
|
|
185
|
+
});
|
|
186
|
+
expect(overrideMock.commandCalls(PutMetricDataCommand)).toHaveLength(1);
|
|
187
|
+
expect(cwMock.commandCalls(PutMetricDataCommand)).toHaveLength(0);
|
|
188
|
+
overrideMock.restore();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
// ── Receive-success-path metric emission (US-011 AC: "metric emission unit-
|
|
192
|
+
// tested on the receive success path") ──────────────────────────────────
|
|
193
|
+
describe("SqsPushReceiver metric emission on the receive-success path", () => {
|
|
194
|
+
let receiver;
|
|
195
|
+
afterEach(async () => {
|
|
196
|
+
if (receiver) {
|
|
197
|
+
await receiver.dispose();
|
|
198
|
+
receiver = undefined;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
it("publishes exactly one latency datum per processed event (mocked publish, no real AWS)", async () => {
|
|
202
|
+
const published = [];
|
|
203
|
+
const publishMetric = async (m) => {
|
|
204
|
+
published.push(m);
|
|
205
|
+
};
|
|
206
|
+
const event = makeEvent({ sequenceNumber: 7 });
|
|
207
|
+
receiver = new SqsPushReceiver({
|
|
208
|
+
tenantId: TENANT,
|
|
209
|
+
queueUrl: QUEUE_URL,
|
|
210
|
+
sqs: new OneBatchSqs([{ Body: encodePushEvent(event), ReceiptHandle: "rh-7" }]),
|
|
211
|
+
syncFn: async () => {
|
|
212
|
+
/* successful pull */
|
|
213
|
+
},
|
|
214
|
+
enabled: true,
|
|
215
|
+
publishMetric,
|
|
216
|
+
// Fake clock: start measured at T, success at T+1500ms → 1.5s latency.
|
|
217
|
+
now: (() => {
|
|
218
|
+
let t = Date.parse(event.eventTimestamp);
|
|
219
|
+
return () => {
|
|
220
|
+
const cur = t;
|
|
221
|
+
t += 1500;
|
|
222
|
+
return cur;
|
|
223
|
+
};
|
|
224
|
+
})(),
|
|
225
|
+
});
|
|
226
|
+
await receiver.start();
|
|
227
|
+
await until(() => published.length >= 1);
|
|
228
|
+
expect(published).toHaveLength(1);
|
|
229
|
+
expect(published[0]).toMatchObject({
|
|
230
|
+
tenantId: TENANT,
|
|
231
|
+
relativePath: event.relativePath,
|
|
232
|
+
sequenceNumber: 7,
|
|
233
|
+
});
|
|
234
|
+
expect(published[0].latencySeconds).toBeGreaterThanOrEqual(0);
|
|
235
|
+
expect(Number.isFinite(published[0].latencySeconds)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
it("does NOT publish a metric when the syncFn throws (failures don't skew p95)", async () => {
|
|
238
|
+
const published = [];
|
|
239
|
+
const event = makeEvent({ sequenceNumber: 8 });
|
|
240
|
+
receiver = new SqsPushReceiver({
|
|
241
|
+
tenantId: TENANT,
|
|
242
|
+
queueUrl: QUEUE_URL,
|
|
243
|
+
sqs: new OneBatchSqs([{ Body: encodePushEvent(event), ReceiptHandle: "rh-8" }]),
|
|
244
|
+
syncFn: async () => {
|
|
245
|
+
throw new Error("pull failed");
|
|
246
|
+
},
|
|
247
|
+
enabled: true,
|
|
248
|
+
publishMetric: async (m) => {
|
|
249
|
+
published.push(m);
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
await receiver.start();
|
|
253
|
+
// Give the loop a few ticks to process the message + (not) publish.
|
|
254
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
255
|
+
expect(published).toHaveLength(0);
|
|
256
|
+
expect(receiver.processedCount).toBe(0);
|
|
257
|
+
});
|
|
258
|
+
it("a throwing publish seam does not crash the receiver loop", async () => {
|
|
259
|
+
const event = makeEvent({ sequenceNumber: 9 });
|
|
260
|
+
let synced = false;
|
|
261
|
+
receiver = new SqsPushReceiver({
|
|
262
|
+
tenantId: TENANT,
|
|
263
|
+
queueUrl: QUEUE_URL,
|
|
264
|
+
sqs: new OneBatchSqs([{ Body: encodePushEvent(event), ReceiptHandle: "rh-9" }]),
|
|
265
|
+
syncFn: async () => {
|
|
266
|
+
synced = true;
|
|
267
|
+
},
|
|
268
|
+
enabled: true,
|
|
269
|
+
publishMetric: async () => {
|
|
270
|
+
throw new Error("metric backend down");
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
await receiver.start();
|
|
274
|
+
await until(() => synced);
|
|
275
|
+
// The sync still completed and the receiver is still alive (connected).
|
|
276
|
+
expect(receiver.processedCount).toBe(1);
|
|
277
|
+
expect(receiver.connected).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
//# sourceMappingURL=metrics.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.test.js","sourceRoot":"","sources":["../../src/sync/metrics.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PushEvent — the wire-shared payload exchanged between every link in the
|
|
3
|
+
* event-driven-hq-cloud-sync pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Producers (the watcher) emit one PushEvent per local content change.
|
|
6
|
+
* Consumers (the push endpoint / receiver / coalescer) decode the payload,
|
|
7
|
+
* validate it, and act on it. Because the same shape crosses a network
|
|
8
|
+
* boundary, it has its own dedicated module.
|
|
9
|
+
*
|
|
10
|
+
* Conventions
|
|
11
|
+
* ───────────
|
|
12
|
+
* - `contentHash` is `sha256:<64-lowercase-hex>`. The `<algorithm>:<hex>`
|
|
13
|
+
* prefix lets a future hash migration ship without breaking the wire
|
|
14
|
+
* format — consumers can branch on the prefix and fall back to refusing
|
|
15
|
+
* unknown algorithms.
|
|
16
|
+
* - `mtime` and `eventTimestamp` are strict ISO-8601 datetime strings.
|
|
17
|
+
* `mtime` is the filesystem modification time of the source file at the
|
|
18
|
+
* moment of capture; `eventTimestamp` is when the watcher emitted the
|
|
19
|
+
* event. They diverge whenever the watcher coalesces or retries.
|
|
20
|
+
* - `sequenceNumber` is a non-negative safe integer. Monotonicity per
|
|
21
|
+
* `originDeviceId` is a producer-side invariant — a single PushEvent
|
|
22
|
+
* can't be self-monotonic, so the schema only validates the bounds.
|
|
23
|
+
* - Unknown extra fields are dropped silently by `decodePushEvent` to
|
|
24
|
+
* permit forwards-compatible additions on the producer side without
|
|
25
|
+
* forcing every consumer to upgrade in lockstep.
|
|
26
|
+
*
|
|
27
|
+
* Ported from indigoai-us/hq-pro PR #112 (src/sync/push-event.ts) into
|
|
28
|
+
* @indigoai-us/hq-cloud (Path B) per project event-driven-sync-menubar US-007.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
/**
|
|
32
|
+
* `sha256:` + 64 lowercase hex chars. The algorithm prefix is mandatory so
|
|
33
|
+
* future hash migrations stay non-breaking; consumers can switch on the
|
|
34
|
+
* prefix and reject unknown algorithms explicitly.
|
|
35
|
+
*/
|
|
36
|
+
export declare const CONTENT_HASH_PATTERN: RegExp;
|
|
37
|
+
/**
|
|
38
|
+
* Strict ISO-8601 datetime regex matching what `z.iso.datetime()` produces.
|
|
39
|
+
*
|
|
40
|
+
* Format: `YYYY-MM-DDTHH:MM:SS(.fff)?(Z|±HH:MM)`
|
|
41
|
+
*/
|
|
42
|
+
export declare const ISO8601_DATETIME_PATTERN: RegExp;
|
|
43
|
+
/**
|
|
44
|
+
* Runtime schema for PushEvent. Unknown extra keys are dropped via the zod
|
|
45
|
+
* default `.strip()` behavior — that is load-bearing for forwards
|
|
46
|
+
* compatibility (see module JSDoc).
|
|
47
|
+
*/
|
|
48
|
+
export declare const PushEventSchema: z.ZodObject<{
|
|
49
|
+
relativePath: z.ZodString;
|
|
50
|
+
contentHash: z.ZodString;
|
|
51
|
+
mtime: z.ZodString;
|
|
52
|
+
originDeviceId: z.ZodString;
|
|
53
|
+
originTenantId: z.ZodString;
|
|
54
|
+
sequenceNumber: z.ZodNumber;
|
|
55
|
+
eventTimestamp: z.ZodString;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
/**
|
|
58
|
+
* The canonical PushEvent type. All fields are required; producers and
|
|
59
|
+
* consumers share this exact shape.
|
|
60
|
+
*/
|
|
61
|
+
export type PushEvent = z.infer<typeof PushEventSchema>;
|
|
62
|
+
/**
|
|
63
|
+
* Public alias for a single decode issue. Aliased here (rather than re-exporting
|
|
64
|
+
* the `$`-prefixed zod internal symbol directly) so consumers can annotate
|
|
65
|
+
* against `PushEventDecodeIssue` without importing zod internals. The alias is
|
|
66
|
+
* structurally identical to `z.core.$ZodIssue`, so this is a non-breaking
|
|
67
|
+
* narrowing of the public surface.
|
|
68
|
+
*/
|
|
69
|
+
export type PushEventDecodeIssue = z.core.$ZodIssue;
|
|
70
|
+
/**
|
|
71
|
+
* Thrown by `decodePushEvent` (and `encodePushEvent` on invalid input) when
|
|
72
|
+
* the payload fails validation. `.issues` carries the underlying zod issues
|
|
73
|
+
* so callers can render structured diagnostics — see the test suite for an
|
|
74
|
+
* example of asserting on the issue path.
|
|
75
|
+
*/
|
|
76
|
+
export declare class PushEventDecodeError extends Error {
|
|
77
|
+
readonly issues: readonly PushEventDecodeIssue[];
|
|
78
|
+
readonly stage: "json-parse" | "schema-validation";
|
|
79
|
+
constructor(message: string, args: {
|
|
80
|
+
issues: readonly PushEventDecodeIssue[];
|
|
81
|
+
stage: "json-parse" | "schema-validation";
|
|
82
|
+
cause?: unknown;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validate `event` against `PushEventSchema` and return the canonical JSON
|
|
87
|
+
* serialization. Validating on encode catches producer-side mistakes early
|
|
88
|
+
* (and ensures the output always round-trips through `decodePushEvent`).
|
|
89
|
+
*
|
|
90
|
+
* Extra keys on `event` are dropped — the returned JSON string contains
|
|
91
|
+
* only the declared PushEvent fields.
|
|
92
|
+
*/
|
|
93
|
+
export declare function encodePushEvent(event: PushEvent): string;
|
|
94
|
+
/**
|
|
95
|
+
* Parse and validate an incoming PushEvent. Accepts either a raw JSON string
|
|
96
|
+
* (the on-the-wire form) or an already-parsed object (handy for in-process
|
|
97
|
+
* wiring and tests).
|
|
98
|
+
*
|
|
99
|
+
* - Unknown extra fields are dropped silently — see module JSDoc.
|
|
100
|
+
* - Missing required fields throw `PushEventDecodeError` whose `.issues`
|
|
101
|
+
* exposes the underlying zod issues.
|
|
102
|
+
* - Malformed JSON throws `PushEventDecodeError` with `stage:"json-parse"`
|
|
103
|
+
* and a synthetic issue at the root path.
|
|
104
|
+
* - A JSON string that parses to a non-object value (e.g. `'42'`, `'"text"'`,
|
|
105
|
+
* `'null'`) surfaces as `stage: 'schema-validation'` — the JSON itself was
|
|
106
|
+
* syntactically valid, so it clears the parse stage before failing the
|
|
107
|
+
* object-shape check.
|
|
108
|
+
*/
|
|
109
|
+
export declare function decodePushEvent(input: unknown): PushEvent;
|
|
110
|
+
//# sourceMappingURL=push-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-event.d.ts","sourceRoot":"","sources":["../../src/sync/push-event.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,QAA0B,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAC+B,CAAC;AAIrE;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;;;;;iBAqClB,CAAC;AAEX;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAIxD;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;AAEpD;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,mBAAmB,CAAC;gBAGjD,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QACJ,MAAM,EAAE,SAAS,oBAAoB,EAAE,CAAC;QACxC,KAAK,EAAE,YAAY,GAAG,mBAAmB,CAAC;QAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAOJ;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CASxD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAkCzD"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PushEvent — the wire-shared payload exchanged between every link in the
|
|
3
|
+
* event-driven-hq-cloud-sync pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Producers (the watcher) emit one PushEvent per local content change.
|
|
6
|
+
* Consumers (the push endpoint / receiver / coalescer) decode the payload,
|
|
7
|
+
* validate it, and act on it. Because the same shape crosses a network
|
|
8
|
+
* boundary, it has its own dedicated module.
|
|
9
|
+
*
|
|
10
|
+
* Conventions
|
|
11
|
+
* ───────────
|
|
12
|
+
* - `contentHash` is `sha256:<64-lowercase-hex>`. The `<algorithm>:<hex>`
|
|
13
|
+
* prefix lets a future hash migration ship without breaking the wire
|
|
14
|
+
* format — consumers can branch on the prefix and fall back to refusing
|
|
15
|
+
* unknown algorithms.
|
|
16
|
+
* - `mtime` and `eventTimestamp` are strict ISO-8601 datetime strings.
|
|
17
|
+
* `mtime` is the filesystem modification time of the source file at the
|
|
18
|
+
* moment of capture; `eventTimestamp` is when the watcher emitted the
|
|
19
|
+
* event. They diverge whenever the watcher coalesces or retries.
|
|
20
|
+
* - `sequenceNumber` is a non-negative safe integer. Monotonicity per
|
|
21
|
+
* `originDeviceId` is a producer-side invariant — a single PushEvent
|
|
22
|
+
* can't be self-monotonic, so the schema only validates the bounds.
|
|
23
|
+
* - Unknown extra fields are dropped silently by `decodePushEvent` to
|
|
24
|
+
* permit forwards-compatible additions on the producer side without
|
|
25
|
+
* forcing every consumer to upgrade in lockstep.
|
|
26
|
+
*
|
|
27
|
+
* Ported from indigoai-us/hq-pro PR #112 (src/sync/push-event.ts) into
|
|
28
|
+
* @indigoai-us/hq-cloud (Path B) per project event-driven-sync-menubar US-007.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* `sha256:` + 64 lowercase hex chars. The algorithm prefix is mandatory so
|
|
34
|
+
* future hash migrations stay non-breaking; consumers can switch on the
|
|
35
|
+
* prefix and reject unknown algorithms explicitly.
|
|
36
|
+
*/
|
|
37
|
+
export const CONTENT_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
38
|
+
/**
|
|
39
|
+
* Strict ISO-8601 datetime regex matching what `z.iso.datetime()` produces.
|
|
40
|
+
*
|
|
41
|
+
* Format: `YYYY-MM-DDTHH:MM:SS(.fff)?(Z|±HH:MM)`
|
|
42
|
+
*/
|
|
43
|
+
export const ISO8601_DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
44
|
+
// ─── Zod schema ────────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Runtime schema for PushEvent. Unknown extra keys are dropped via the zod
|
|
47
|
+
* default `.strip()` behavior — that is load-bearing for forwards
|
|
48
|
+
* compatibility (see module JSDoc).
|
|
49
|
+
*/
|
|
50
|
+
export const PushEventSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
relativePath: z
|
|
53
|
+
.string()
|
|
54
|
+
.min(1, "relativePath must be a non-empty string"),
|
|
55
|
+
contentHash: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(CONTENT_HASH_PATTERN, "contentHash must match `sha256:<64 lowercase hex>`"),
|
|
58
|
+
mtime: z
|
|
59
|
+
.string()
|
|
60
|
+
.regex(ISO8601_DATETIME_PATTERN, "mtime must be an ISO-8601 datetime"),
|
|
61
|
+
originDeviceId: z
|
|
62
|
+
.string()
|
|
63
|
+
.min(1, "originDeviceId must be a non-empty string"),
|
|
64
|
+
originTenantId: z
|
|
65
|
+
.string()
|
|
66
|
+
.min(1, "originTenantId must be a non-empty string"),
|
|
67
|
+
sequenceNumber: z
|
|
68
|
+
.number()
|
|
69
|
+
.int("sequenceNumber must be an integer")
|
|
70
|
+
.min(0, "sequenceNumber must be non-negative")
|
|
71
|
+
.max(Number.MAX_SAFE_INTEGER, "sequenceNumber must be <= Number.MAX_SAFE_INTEGER"),
|
|
72
|
+
eventTimestamp: z
|
|
73
|
+
.string()
|
|
74
|
+
.regex(ISO8601_DATETIME_PATTERN, "eventTimestamp must be an ISO-8601 datetime"),
|
|
75
|
+
})
|
|
76
|
+
// `.strip()` is the zod 4 default; called explicitly here so the intent is
|
|
77
|
+
// obvious to future readers. Unknown keys MUST NOT throw — see module JSDoc.
|
|
78
|
+
.strip();
|
|
79
|
+
/**
|
|
80
|
+
* Thrown by `decodePushEvent` (and `encodePushEvent` on invalid input) when
|
|
81
|
+
* the payload fails validation. `.issues` carries the underlying zod issues
|
|
82
|
+
* so callers can render structured diagnostics — see the test suite for an
|
|
83
|
+
* example of asserting on the issue path.
|
|
84
|
+
*/
|
|
85
|
+
export class PushEventDecodeError extends Error {
|
|
86
|
+
issues;
|
|
87
|
+
stage;
|
|
88
|
+
constructor(message, args) {
|
|
89
|
+
super(message, args.cause === undefined ? undefined : { cause: args.cause });
|
|
90
|
+
this.name = "PushEventDecodeError";
|
|
91
|
+
this.issues = args.issues;
|
|
92
|
+
this.stage = args.stage;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ─── Encode / Decode ───────────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Validate `event` against `PushEventSchema` and return the canonical JSON
|
|
98
|
+
* serialization. Validating on encode catches producer-side mistakes early
|
|
99
|
+
* (and ensures the output always round-trips through `decodePushEvent`).
|
|
100
|
+
*
|
|
101
|
+
* Extra keys on `event` are dropped — the returned JSON string contains
|
|
102
|
+
* only the declared PushEvent fields.
|
|
103
|
+
*/
|
|
104
|
+
export function encodePushEvent(event) {
|
|
105
|
+
const parsed = PushEventSchema.safeParse(event);
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
throw new PushEventDecodeError("encodePushEvent: input failed PushEvent schema validation", { issues: parsed.error.issues, stage: "schema-validation" });
|
|
108
|
+
}
|
|
109
|
+
return JSON.stringify(parsed.data);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse and validate an incoming PushEvent. Accepts either a raw JSON string
|
|
113
|
+
* (the on-the-wire form) or an already-parsed object (handy for in-process
|
|
114
|
+
* wiring and tests).
|
|
115
|
+
*
|
|
116
|
+
* - Unknown extra fields are dropped silently — see module JSDoc.
|
|
117
|
+
* - Missing required fields throw `PushEventDecodeError` whose `.issues`
|
|
118
|
+
* exposes the underlying zod issues.
|
|
119
|
+
* - Malformed JSON throws `PushEventDecodeError` with `stage:"json-parse"`
|
|
120
|
+
* and a synthetic issue at the root path.
|
|
121
|
+
* - A JSON string that parses to a non-object value (e.g. `'42'`, `'"text"'`,
|
|
122
|
+
* `'null'`) surfaces as `stage: 'schema-validation'` — the JSON itself was
|
|
123
|
+
* syntactically valid, so it clears the parse stage before failing the
|
|
124
|
+
* object-shape check.
|
|
125
|
+
*/
|
|
126
|
+
export function decodePushEvent(input) {
|
|
127
|
+
let candidate = input;
|
|
128
|
+
if (typeof input === "string") {
|
|
129
|
+
try {
|
|
130
|
+
candidate = JSON.parse(input);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw new PushEventDecodeError("decodePushEvent: input string is not valid JSON", {
|
|
134
|
+
issues: [
|
|
135
|
+
{
|
|
136
|
+
code: "custom",
|
|
137
|
+
message: err instanceof Error ? err.message : "invalid JSON input",
|
|
138
|
+
path: [],
|
|
139
|
+
input,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
stage: "json-parse",
|
|
143
|
+
cause: err,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const parsed = PushEventSchema.safeParse(candidate);
|
|
148
|
+
if (!parsed.success) {
|
|
149
|
+
throw new PushEventDecodeError("decodePushEvent: payload failed PushEvent schema validation", { issues: parsed.error.issues, stage: "schema-validation" });
|
|
150
|
+
}
|
|
151
|
+
return parsed.data;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=push-event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-event.js","sourceRoot":"","sources":["../../src/sync/push-event.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=push-event.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-event.test.d.ts","sourceRoot":"","sources":["../../src/sync/push-event.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|