@indigoai-us/hq-cloud 6.15.75 → 6.15.76
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-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +12 -2
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +4 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +63 -3
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +5 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +3 -0
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/event-sync.test.js +45 -0
- package/dist/sync/event-sync.test.js.map +1 -1
- package/dist/watcher.d.ts +59 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +186 -15
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +311 -3
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
package/dist/watcher.test.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as os from "os";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { watch } from "chokidar";
|
|
7
|
-
import { FakeClock, WatchPushDriver, TreeWatcher, ChokidarWatchBudget, createWatchPathFilter, toChokidarIgnored, PushEventEmitter, PUBLISH_OUTCOME_JOURNALD_CEILING_BYTES, PUBLISH_OUTCOME_SUCCESS_SAMPLE_CAP, resolveEventDebounceConfig, summarizePublishBatchOutcome, DEFAULT_EVENT_DEBOUNCE_MS, DEFAULT_EVENT_MAX_WAIT_MS, EVENT_DEBOUNCE_MS_ENV, EVENT_MAX_WAIT_MS_ENV, DEFAULT_TREE_WATCHER_MAX_WATCHED_PATHS, MAX_TREE_WATCHER_MAX_WATCHED_PATHS, TREE_WATCHER_MAX_WATCHED_PATHS_ENV, resolveMaxWatchedPaths, } from "./watcher.js";
|
|
7
|
+
import { FakeClock, WatchPushDriver, TreeWatcher, ChokidarWatchBudget, createWatchPathFilter, toChokidarIgnored, PushEventEmitter, PUBLISH_OUTCOME_JOURNALD_CEILING_BYTES, PUBLISH_OUTCOME_SUCCESS_SAMPLE_CAP, resolveEventDebounceConfig, summarizePublishBatchOutcome, DEFAULT_EVENT_DEBOUNCE_MS, DEFAULT_EVENT_MAX_WAIT_MS, DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS, DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS, DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES, DEFAULT_PUBLISH_MIN_INTERVAL_MS, EVENT_DEBOUNCE_MS_ENV, EVENT_MAX_WAIT_MS_ENV, EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV, EVENT_SMALL_BATCH_MAX_PATHS_ENV, EVENT_SMALL_BATCH_MAX_BYTES_ENV, DEFAULT_TREE_WATCHER_MAX_WATCHED_PATHS, MAX_TREE_WATCHER_MAX_WATCHED_PATHS, TREE_WATCHER_MAX_WATCHED_PATHS_ENV, resolveMaxWatchedPaths, PUBLISH_MIN_INTERVAL_MS_ENV, resolvePublishMinIntervalMs, } from "./watcher.js";
|
|
8
8
|
import { StaticFlagProvider } from "./sync/feature-flags.js";
|
|
9
9
|
import { computePersonalVaultPaths } from "./personal-vault.js";
|
|
10
10
|
/**
|
|
@@ -790,6 +790,167 @@ describe("US-002: TreeWatcher — debounce coalesce (FakeClock seam)", () => {
|
|
|
790
790
|
clock.advance(1);
|
|
791
791
|
expect(changed).toHaveBeenCalledTimes(1);
|
|
792
792
|
});
|
|
793
|
+
it("flushes a quiet, known-small batch through the short debounce window", () => {
|
|
794
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-small-debounce-"));
|
|
795
|
+
try {
|
|
796
|
+
const small = path.join(dir, "small.md");
|
|
797
|
+
fs.writeFileSync(small, "sixteen-byte-doc");
|
|
798
|
+
const clock = new FakeClock();
|
|
799
|
+
const changed = vi.fn();
|
|
800
|
+
const watcher = new TreeWatcher({
|
|
801
|
+
hqRoot: dir,
|
|
802
|
+
debounceMs: 15_000,
|
|
803
|
+
smallBatchDebounceMs: 2_500,
|
|
804
|
+
smallBatchMaxPaths: 8,
|
|
805
|
+
smallBatchMaxBytes: 1024 * 1024,
|
|
806
|
+
clock,
|
|
807
|
+
pathFilter: () => true,
|
|
808
|
+
});
|
|
809
|
+
watcher.onChange(changed);
|
|
810
|
+
watcher.handleEvent(small);
|
|
811
|
+
clock.advance(2_499);
|
|
812
|
+
expect(changed).not.toHaveBeenCalled();
|
|
813
|
+
clock.advance(1);
|
|
814
|
+
expect(changed).toHaveBeenCalledTimes(1);
|
|
815
|
+
expect(changed.mock.calls[0][1].paths.get(small)).toBe("small.md");
|
|
816
|
+
watcher.dispose();
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
it("keeps a burst above the small-path cap on the 15s debounce", () => {
|
|
823
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-burst-debounce-"));
|
|
824
|
+
try {
|
|
825
|
+
const clock = new FakeClock();
|
|
826
|
+
const changed = vi.fn();
|
|
827
|
+
const watcher = new TreeWatcher({
|
|
828
|
+
hqRoot: dir,
|
|
829
|
+
debounceMs: 15_000,
|
|
830
|
+
smallBatchDebounceMs: 2_500,
|
|
831
|
+
smallBatchMaxPaths: 8,
|
|
832
|
+
smallBatchMaxBytes: 1024 * 1024,
|
|
833
|
+
clock,
|
|
834
|
+
pathFilter: () => true,
|
|
835
|
+
});
|
|
836
|
+
watcher.onChange(changed);
|
|
837
|
+
for (let index = 0; index < 9; index += 1) {
|
|
838
|
+
const file = path.join(dir, `burst-${index}.md`);
|
|
839
|
+
fs.writeFileSync(file, "small");
|
|
840
|
+
watcher.handleEvent(file);
|
|
841
|
+
clock.advance(100);
|
|
842
|
+
}
|
|
843
|
+
// The final event landed at t=800ms; the loop's final 100ms advance is
|
|
844
|
+
// outside the quiet window, so stop one millisecond before t=15_800ms.
|
|
845
|
+
clock.advance(14_899);
|
|
846
|
+
expect(changed).not.toHaveBeenCalled();
|
|
847
|
+
clock.advance(1);
|
|
848
|
+
expect(changed).toHaveBeenCalledTimes(1);
|
|
849
|
+
expect(changed.mock.calls[0][1].paths.size).toBe(9);
|
|
850
|
+
watcher.dispose();
|
|
851
|
+
}
|
|
852
|
+
finally {
|
|
853
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
it("preserves max-wait for a continuous small batch that never becomes quiet", () => {
|
|
857
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-small-max-wait-"));
|
|
858
|
+
try {
|
|
859
|
+
const file = path.join(dir, "continuous.md");
|
|
860
|
+
fs.writeFileSync(file, "small");
|
|
861
|
+
const clock = new FakeClock();
|
|
862
|
+
const changed = vi.fn();
|
|
863
|
+
const watcher = new TreeWatcher({
|
|
864
|
+
hqRoot: dir,
|
|
865
|
+
debounceMs: 15_000,
|
|
866
|
+
maxWaitMs: 120_000,
|
|
867
|
+
smallBatchDebounceMs: 2_500,
|
|
868
|
+
smallBatchMaxPaths: 8,
|
|
869
|
+
smallBatchMaxBytes: 1024 * 1024,
|
|
870
|
+
clock,
|
|
871
|
+
pathFilter: () => true,
|
|
872
|
+
});
|
|
873
|
+
watcher.onChange(changed);
|
|
874
|
+
for (let elapsed = 0; elapsed < 120_000; elapsed += 2_000) {
|
|
875
|
+
watcher.handleEvent(file);
|
|
876
|
+
if (elapsed < 118_000)
|
|
877
|
+
clock.advance(2_000);
|
|
878
|
+
}
|
|
879
|
+
expect(changed).not.toHaveBeenCalled();
|
|
880
|
+
clock.advance(2_000);
|
|
881
|
+
expect(changed).toHaveBeenCalledTimes(1);
|
|
882
|
+
watcher.dispose();
|
|
883
|
+
}
|
|
884
|
+
finally {
|
|
885
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
it("keeps a duplicate directory delete out of the short debounce lane", () => {
|
|
889
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-delete-debounce-"));
|
|
890
|
+
try {
|
|
891
|
+
const deleted = path.join(dir, "deleted-directory");
|
|
892
|
+
const clock = new FakeClock();
|
|
893
|
+
const changed = vi.fn();
|
|
894
|
+
const watcher = new TreeWatcher({
|
|
895
|
+
hqRoot: dir,
|
|
896
|
+
debounceMs: 15_000,
|
|
897
|
+
smallBatchDebounceMs: 2_500,
|
|
898
|
+
smallBatchMaxPaths: 8,
|
|
899
|
+
smallBatchMaxBytes: 1024 * 1024,
|
|
900
|
+
clock,
|
|
901
|
+
pathFilter: () => true,
|
|
902
|
+
});
|
|
903
|
+
watcher.onChange(changed);
|
|
904
|
+
watcher.handleEvent(deleted, "unlinkDir");
|
|
905
|
+
watcher.handleEvent(deleted, "unlink");
|
|
906
|
+
clock.advance(2_500);
|
|
907
|
+
expect(changed).not.toHaveBeenCalled();
|
|
908
|
+
clock.advance(12_500);
|
|
909
|
+
expect(changed).toHaveBeenCalledTimes(1);
|
|
910
|
+
expect(changed.mock.calls[0][1].changes.get(deleted)).toMatchObject({
|
|
911
|
+
kind: "unlinkDir",
|
|
912
|
+
});
|
|
913
|
+
watcher.dispose();
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
it("keeps an overflowed batch out of the short debounce lane", () => {
|
|
920
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-watcher-overflow-debounce-"));
|
|
921
|
+
try {
|
|
922
|
+
const first = path.join(dir, "first.md");
|
|
923
|
+
const dropped = path.join(dir, "dropped.md");
|
|
924
|
+
fs.writeFileSync(first, "small");
|
|
925
|
+
fs.writeFileSync(dropped, "small");
|
|
926
|
+
const clock = new FakeClock();
|
|
927
|
+
const changed = vi.fn();
|
|
928
|
+
const watcher = new TreeWatcher({
|
|
929
|
+
hqRoot: dir,
|
|
930
|
+
debounceMs: 15_000,
|
|
931
|
+
smallBatchDebounceMs: 2_500,
|
|
932
|
+
smallBatchMaxPaths: 8,
|
|
933
|
+
smallBatchMaxBytes: 1024 * 1024,
|
|
934
|
+
maxPendingPaths: 1,
|
|
935
|
+
maxPendingBytes: 1024 * 1024,
|
|
936
|
+
clock,
|
|
937
|
+
pathFilter: () => true,
|
|
938
|
+
onBacklogOverflow: () => undefined,
|
|
939
|
+
});
|
|
940
|
+
watcher.onChange(changed);
|
|
941
|
+
watcher.handleEvent(first);
|
|
942
|
+
watcher.handleEvent(dropped);
|
|
943
|
+
clock.advance(2_500);
|
|
944
|
+
expect(changed).not.toHaveBeenCalled();
|
|
945
|
+
clock.advance(12_500);
|
|
946
|
+
expect(changed).toHaveBeenCalledTimes(1);
|
|
947
|
+
expect(changed.mock.calls[0][1]).toMatchObject({ overflowed: true });
|
|
948
|
+
watcher.dispose();
|
|
949
|
+
}
|
|
950
|
+
finally {
|
|
951
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
952
|
+
}
|
|
953
|
+
});
|
|
793
954
|
it("does NOT emit for an ignored / excluded path", () => {
|
|
794
955
|
const { clock, changed, watcher } = makeWatcher({ personalMode: true });
|
|
795
956
|
watcher.handleEvent(path.join(ROOT, ".env"));
|
|
@@ -1249,9 +1410,11 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
|
|
|
1249
1410
|
transport,
|
|
1250
1411
|
flagProvider: new StaticFlagProvider(["tenant-indigo"]),
|
|
1251
1412
|
now: () => new Date("2026-06-18T12:00:00.000Z"),
|
|
1413
|
+
clock: opts?.clock,
|
|
1252
1414
|
getSequenceNumber: opts?.getSequenceNumber,
|
|
1253
1415
|
onError: opts?.onError,
|
|
1254
1416
|
lastPublishedContentHashMaxPaths: opts?.lastPublishedContentHashMaxPaths,
|
|
1417
|
+
publishMinIntervalMs: opts?.publishMinIntervalMs ?? 0,
|
|
1255
1418
|
...(opts?.lastPublishedContentHashStateDir
|
|
1256
1419
|
? { lastPublishedContentHashStateDir: opts.lastPublishedContentHashStateDir }
|
|
1257
1420
|
: {}),
|
|
@@ -1371,6 +1534,109 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
|
|
|
1371
1534
|
]);
|
|
1372
1535
|
expect(published[0].contentHash).not.toBe(published[1].contentHash);
|
|
1373
1536
|
});
|
|
1537
|
+
it("coalesces ten rewrites of one path into an immediate and trailing latest publish", async () => {
|
|
1538
|
+
const clock = new FakeClock();
|
|
1539
|
+
const published = [];
|
|
1540
|
+
const rewritten = path.join(dir, "high-churn-prd.json");
|
|
1541
|
+
const relativePath = "personal/projects/churn/prd.json";
|
|
1542
|
+
const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
|
|
1543
|
+
const batch = { paths: new Map([[rewritten, relativePath]]) };
|
|
1544
|
+
for (let version = 0; version < 10; version += 1) {
|
|
1545
|
+
fs.writeFileSync(rewritten, `{"version":${version}}\n`);
|
|
1546
|
+
await emitter.emitForBatch(batch);
|
|
1547
|
+
if (version < 9)
|
|
1548
|
+
clock.advance(5_000);
|
|
1549
|
+
}
|
|
1550
|
+
expect(published).toHaveLength(1);
|
|
1551
|
+
clock.advance(15_000);
|
|
1552
|
+
await emitter.emitForBatch({ paths: new Map() });
|
|
1553
|
+
expect(published).toHaveLength(2);
|
|
1554
|
+
expect(clock.pendingTimerCount()).toBe(0);
|
|
1555
|
+
expect(published[1]).toMatchObject({ kind: "upsert", relativePath });
|
|
1556
|
+
expect(published[1].contentHash).toBe(`sha256:${createHash("sha256").update('{"version":9}\n').digest("hex")}`);
|
|
1557
|
+
});
|
|
1558
|
+
it("publishes the uploaded deferred revision when the local file changes before flush", async () => {
|
|
1559
|
+
const clock = new FakeClock();
|
|
1560
|
+
const published = [];
|
|
1561
|
+
const rewritten = path.join(dir, "captured-deferred-revision.json");
|
|
1562
|
+
const relativePath = "personal/projects/captured/prd.json";
|
|
1563
|
+
const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
|
|
1564
|
+
const batch = { paths: new Map([[rewritten, relativePath]]) };
|
|
1565
|
+
fs.writeFileSync(rewritten, '{"revision":"uploaded"}\n');
|
|
1566
|
+
await emitter.emitForBatch(batch);
|
|
1567
|
+
fs.writeFileSync(rewritten, '{"revision":"deferred-uploaded"}\n');
|
|
1568
|
+
await emitter.emitForBatch(batch);
|
|
1569
|
+
const expectedHash = `sha256:${createHash("sha256")
|
|
1570
|
+
.update('{"revision":"deferred-uploaded"}\n')
|
|
1571
|
+
.digest("hex")}`;
|
|
1572
|
+
// This local revision has not completed its scoped push yet. The trailing
|
|
1573
|
+
// announcement must remain bound to the previously uploaded bytes.
|
|
1574
|
+
fs.writeFileSync(rewritten, '{"revision":"newer-local"}\n');
|
|
1575
|
+
clock.advance(60_000);
|
|
1576
|
+
await emitter.emitForBatch({ paths: new Map() });
|
|
1577
|
+
expect(published).toHaveLength(2);
|
|
1578
|
+
expect(published[1]).toMatchObject({
|
|
1579
|
+
kind: "upsert",
|
|
1580
|
+
relativePath,
|
|
1581
|
+
contentHash: expectedHash,
|
|
1582
|
+
});
|
|
1583
|
+
expect(published[1].contentHash).not.toBe(`sha256:${createHash("sha256")
|
|
1584
|
+
.update('{"revision":"newer-local"}\n')
|
|
1585
|
+
.digest("hex")}`);
|
|
1586
|
+
});
|
|
1587
|
+
it("skips the trailing publish when the final rewrite restores the last published hash", async () => {
|
|
1588
|
+
const clock = new FakeClock();
|
|
1589
|
+
const published = [];
|
|
1590
|
+
const rewritten = path.join(dir, "reverted-prd.json");
|
|
1591
|
+
const relativePath = "personal/projects/reverted/prd.json";
|
|
1592
|
+
const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
|
|
1593
|
+
const batch = { paths: new Map([[rewritten, relativePath]]) };
|
|
1594
|
+
fs.writeFileSync(rewritten, "A\n");
|
|
1595
|
+
await emitter.emitForBatch(batch);
|
|
1596
|
+
fs.writeFileSync(rewritten, "B\n");
|
|
1597
|
+
await emitter.emitForBatch(batch);
|
|
1598
|
+
fs.writeFileSync(rewritten, "A\n");
|
|
1599
|
+
await emitter.emitForBatch(batch);
|
|
1600
|
+
clock.advance(60_000);
|
|
1601
|
+
await emitter.emitForBatch({ paths: new Map() });
|
|
1602
|
+
expect(published).toHaveLength(1);
|
|
1603
|
+
expect(clock.pendingTimerCount()).toBe(0);
|
|
1604
|
+
expect(published[0]).toMatchObject({ kind: "upsert", relativePath });
|
|
1605
|
+
});
|
|
1606
|
+
it("publishes a delete immediately while an upsert is deferred", async () => {
|
|
1607
|
+
const clock = new FakeClock();
|
|
1608
|
+
const published = [];
|
|
1609
|
+
const deleted = path.join(dir, "delete-during-quiet.jsonl");
|
|
1610
|
+
const relativePath = "workspace/.session-logs/delete-during-quiet.jsonl";
|
|
1611
|
+
const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000, clock });
|
|
1612
|
+
const batch = { paths: new Map([[deleted, relativePath]]) };
|
|
1613
|
+
fs.writeFileSync(deleted, "A\n");
|
|
1614
|
+
await emitter.emitForBatch(batch);
|
|
1615
|
+
fs.writeFileSync(deleted, "B\n");
|
|
1616
|
+
await emitter.emitForBatch(batch);
|
|
1617
|
+
fs.rmSync(deleted);
|
|
1618
|
+
await emitter.emitForBatch(batch);
|
|
1619
|
+
expect(published.map((event) => event.kind)).toEqual(["upsert", "delete"]);
|
|
1620
|
+
clock.advance(60_000);
|
|
1621
|
+
await emitter.emitForBatch({ paths: new Map() });
|
|
1622
|
+
expect(published).toHaveLength(2);
|
|
1623
|
+
});
|
|
1624
|
+
it("does not make a second path wait behind another path's quiet interval", async () => {
|
|
1625
|
+
const published = [];
|
|
1626
|
+
const first = path.join(dir, "first-high-churn.jsonl");
|
|
1627
|
+
const second = path.join(dir, "independent.jsonl");
|
|
1628
|
+
const emitter = makeEmitter({ published, publishMinIntervalMs: 60_000 });
|
|
1629
|
+
fs.writeFileSync(first, "A\n");
|
|
1630
|
+
fs.writeFileSync(second, "B\n");
|
|
1631
|
+
await emitter.emitForBatch({ paths: new Map([[first, "workspace/.session-logs/first.jsonl"]]) });
|
|
1632
|
+
fs.writeFileSync(first, "A2\n");
|
|
1633
|
+
await emitter.emitForBatch({ paths: new Map([[first, "workspace/.session-logs/first.jsonl"]]) });
|
|
1634
|
+
await emitter.emitForBatch({ paths: new Map([[second, "workspace/.session-logs/second.jsonl"]]) });
|
|
1635
|
+
expect(published.map((event) => event.relativePath)).toEqual([
|
|
1636
|
+
"workspace/.session-logs/first.jsonl",
|
|
1637
|
+
"workspace/.session-logs/second.jsonl",
|
|
1638
|
+
]);
|
|
1639
|
+
});
|
|
1374
1640
|
it("publishes a content revert because it compares only with the last publication", async () => {
|
|
1375
1641
|
const published = [];
|
|
1376
1642
|
const reverted = path.join(dir, "reverted-session-log.jsonl");
|
|
@@ -1395,6 +1661,7 @@ describe("PushEventEmitter — directory and delete tombstone handling", () => {
|
|
|
1395
1661
|
originTenantId: "tenant-indigo",
|
|
1396
1662
|
originDeviceId: "device-a",
|
|
1397
1663
|
flagProvider: new StaticFlagProvider(["tenant-indigo"]),
|
|
1664
|
+
publishMinIntervalMs: 0,
|
|
1398
1665
|
transport: {
|
|
1399
1666
|
start: async () => { },
|
|
1400
1667
|
dispose: async () => { },
|
|
@@ -1758,7 +2025,13 @@ describe("TreeWatcher deferred delete snapshots", () => {
|
|
|
1758
2025
|
// Event-pass debounce configuration (HQ_SYNC_EVENT_DEBOUNCE_MS / _MAX_WAIT_MS)
|
|
1759
2026
|
// ---------------------------------------------------------------------------
|
|
1760
2027
|
describe("resolveEventDebounceConfig", () => {
|
|
1761
|
-
const ENV_KEYS = [
|
|
2028
|
+
const ENV_KEYS = [
|
|
2029
|
+
EVENT_DEBOUNCE_MS_ENV,
|
|
2030
|
+
EVENT_MAX_WAIT_MS_ENV,
|
|
2031
|
+
EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV,
|
|
2032
|
+
EVENT_SMALL_BATCH_MAX_PATHS_ENV,
|
|
2033
|
+
EVENT_SMALL_BATCH_MAX_BYTES_ENV,
|
|
2034
|
+
];
|
|
1762
2035
|
let saved;
|
|
1763
2036
|
beforeEach(() => {
|
|
1764
2037
|
saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
@@ -1777,6 +2050,9 @@ describe("resolveEventDebounceConfig", () => {
|
|
|
1777
2050
|
expect(resolveEventDebounceConfig()).toEqual({
|
|
1778
2051
|
debounceMs: DEFAULT_EVENT_DEBOUNCE_MS,
|
|
1779
2052
|
maxWaitMs: DEFAULT_EVENT_MAX_WAIT_MS,
|
|
2053
|
+
smallBatchDebounceMs: DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS,
|
|
2054
|
+
smallBatchMaxPaths: DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS,
|
|
2055
|
+
smallBatchMaxBytes: DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES,
|
|
1780
2056
|
});
|
|
1781
2057
|
expect(DEFAULT_EVENT_DEBOUNCE_MS).toBe(15_000);
|
|
1782
2058
|
expect(DEFAULT_EVENT_MAX_WAIT_MS).toBe(120_000);
|
|
@@ -1784,9 +2060,15 @@ describe("resolveEventDebounceConfig", () => {
|
|
|
1784
2060
|
it("honors env overrides", () => {
|
|
1785
2061
|
process.env[EVENT_DEBOUNCE_MS_ENV] = "5000";
|
|
1786
2062
|
process.env[EVENT_MAX_WAIT_MS_ENV] = "30000";
|
|
2063
|
+
process.env[EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV] = "2000";
|
|
2064
|
+
process.env[EVENT_SMALL_BATCH_MAX_PATHS_ENV] = "6";
|
|
2065
|
+
process.env[EVENT_SMALL_BATCH_MAX_BYTES_ENV] = "4096";
|
|
1787
2066
|
expect(resolveEventDebounceConfig()).toEqual({
|
|
1788
2067
|
debounceMs: 5000,
|
|
1789
2068
|
maxWaitMs: 30_000,
|
|
2069
|
+
smallBatchDebounceMs: 2000,
|
|
2070
|
+
smallBatchMaxPaths: 6,
|
|
2071
|
+
smallBatchMaxBytes: 4096,
|
|
1790
2072
|
});
|
|
1791
2073
|
});
|
|
1792
2074
|
it("treats 0, negative, and non-numeric values as unset (defaults)", () => {
|
|
@@ -1796,6 +2078,9 @@ describe("resolveEventDebounceConfig", () => {
|
|
|
1796
2078
|
expect(resolveEventDebounceConfig()).toEqual({
|
|
1797
2079
|
debounceMs: DEFAULT_EVENT_DEBOUNCE_MS,
|
|
1798
2080
|
maxWaitMs: DEFAULT_EVENT_MAX_WAIT_MS,
|
|
2081
|
+
smallBatchDebounceMs: DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS,
|
|
2082
|
+
smallBatchMaxPaths: DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS,
|
|
2083
|
+
smallBatchMaxBytes: DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES,
|
|
1799
2084
|
});
|
|
1800
2085
|
}
|
|
1801
2086
|
});
|
|
@@ -1805,13 +2090,36 @@ describe("resolveEventDebounceConfig", () => {
|
|
|
1805
2090
|
expect(resolveEventDebounceConfig()).toEqual({
|
|
1806
2091
|
debounceMs: 60_000,
|
|
1807
2092
|
maxWaitMs: 60_000,
|
|
2093
|
+
smallBatchDebounceMs: DEFAULT_EVENT_SMALL_BATCH_DEBOUNCE_MS,
|
|
2094
|
+
smallBatchMaxPaths: DEFAULT_EVENT_SMALL_BATCH_MAX_PATHS,
|
|
2095
|
+
smallBatchMaxBytes: DEFAULT_EVENT_SMALL_BATCH_MAX_BYTES,
|
|
1808
2096
|
});
|
|
1809
2097
|
});
|
|
1810
2098
|
it("accepts an injected env record without touching process.env", () => {
|
|
1811
2099
|
expect(resolveEventDebounceConfig({
|
|
1812
2100
|
[EVENT_DEBOUNCE_MS_ENV]: "1000",
|
|
1813
2101
|
[EVENT_MAX_WAIT_MS_ENV]: "2000",
|
|
1814
|
-
|
|
2102
|
+
[EVENT_SMALL_BATCH_DEBOUNCE_MS_ENV]: "750",
|
|
2103
|
+
[EVENT_SMALL_BATCH_MAX_PATHS_ENV]: "4",
|
|
2104
|
+
[EVENT_SMALL_BATCH_MAX_BYTES_ENV]: "1024",
|
|
2105
|
+
})).toEqual({
|
|
2106
|
+
debounceMs: 1000,
|
|
2107
|
+
maxWaitMs: 2000,
|
|
2108
|
+
smallBatchDebounceMs: 750,
|
|
2109
|
+
smallBatchMaxPaths: 4,
|
|
2110
|
+
smallBatchMaxBytes: 1024,
|
|
2111
|
+
});
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
describe("resolvePublishMinIntervalMs", () => {
|
|
2115
|
+
it("defaults to 60 seconds, accepts zero to disable coalescing, and rejects invalid values", () => {
|
|
2116
|
+
expect(resolvePublishMinIntervalMs({})).toBe(DEFAULT_PUBLISH_MIN_INTERVAL_MS);
|
|
2117
|
+
expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "0" })).toBe(0);
|
|
2118
|
+
expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "invalid" })).toBe(DEFAULT_PUBLISH_MIN_INTERVAL_MS);
|
|
2119
|
+
expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "-1" })).toBe(DEFAULT_PUBLISH_MIN_INTERVAL_MS);
|
|
2120
|
+
});
|
|
2121
|
+
it("uses the environment override in milliseconds", () => {
|
|
2122
|
+
expect(resolvePublishMinIntervalMs({ [PUBLISH_MIN_INTERVAL_MS_ENV]: "1234.9" })).toBe(1234);
|
|
1815
2123
|
});
|
|
1816
2124
|
});
|
|
1817
2125
|
//# sourceMappingURL=watcher.test.js.map
|