316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { AppEnv } from "@autumn/shared";
|
|
import chalk from "chalk";
|
|
import {
|
|
type QueueSyncPayload,
|
|
SyncBatchingManagerV2,
|
|
} from "@/internal/balances/utils/sync/SyncBatchingManagerV2.js";
|
|
|
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
const createMockQueue = () => {
|
|
const calls: QueueSyncPayload[] = [];
|
|
const fn = async (args: QueueSyncPayload) => {
|
|
calls.push(structuredClone(args));
|
|
};
|
|
return { fn, calls };
|
|
};
|
|
|
|
const addItems = ({
|
|
manager,
|
|
count,
|
|
customerId = "cust-1",
|
|
cusEntIds = ["ce-1"],
|
|
rolloverIds,
|
|
}: {
|
|
manager: SyncBatchingManagerV2;
|
|
count: number;
|
|
customerId?: string;
|
|
cusEntIds?: string[];
|
|
rolloverIds?: string[];
|
|
}) => {
|
|
for (let i = 0; i < count; i++) {
|
|
manager.addSyncItem({
|
|
customerId,
|
|
orgId: "org-1",
|
|
env: AppEnv.Sandbox,
|
|
cusEntIds,
|
|
rolloverIds,
|
|
region: "us-east-1",
|
|
});
|
|
}
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 1. Fixed window: rapid items within 1 window → 1 flush
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-1: rapid addSyncItem calls within one window → 1 flush")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 100,
|
|
});
|
|
|
|
// Fire 500 sync items rapidly (same customer, same cusEntIds)
|
|
addItems({ manager, count: 500 });
|
|
|
|
// Nothing queued yet — timer hasn't fired
|
|
expect(calls.length).toBe(0);
|
|
|
|
// Wait for window to fire
|
|
await wait(200);
|
|
|
|
expect(calls.length).toBe(1);
|
|
expect(calls[0].payload.cusEntIds).toEqual(["ce-1"]);
|
|
expect(calls[0].payload.customerId).toBe("cust-1");
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 2. Fixed window fires on schedule even during continuous load
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-2: fixed window fires on schedule during continuous load")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 100,
|
|
});
|
|
|
|
// Add items continuously every 20ms for 500ms
|
|
const interval = setInterval(() => {
|
|
addItems({ manager, count: 1 });
|
|
}, 20);
|
|
|
|
await wait(600);
|
|
clearInterval(interval);
|
|
|
|
// Flush any leftovers
|
|
await manager.flush();
|
|
|
|
// Fixed window of 100ms over 500ms → should produce ~5 flushes
|
|
// (NOT 1, which would indicate debounce behavior)
|
|
expect(calls.length).toBeGreaterThanOrEqual(3);
|
|
expect(calls.length).toBeLessThanOrEqual(8);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 3. Different customers get independent batches
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-3: different customers produce separate flushes")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 50,
|
|
});
|
|
|
|
addItems({ manager, count: 10, customerId: "cust-A" });
|
|
addItems({ manager, count: 10, customerId: "cust-B" });
|
|
|
|
const stats = manager.getStats();
|
|
expect(stats.totalCustomers).toBe(2);
|
|
|
|
await wait(150);
|
|
|
|
expect(calls.length).toBe(2);
|
|
const customerIds = calls.map((c) => c.payload.customerId).sort();
|
|
expect(customerIds).toEqual(["cust-A", "cust-B"]);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 4. cusEntIds from multiple adds merge into one flush
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-4: cusEntIds merge across multiple addSyncItem calls")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 100,
|
|
});
|
|
|
|
manager.addSyncItem({
|
|
customerId: "cust-1",
|
|
orgId: "org-1",
|
|
env: AppEnv.Sandbox,
|
|
cusEntIds: ["ce-1"],
|
|
region: "us-east-1",
|
|
});
|
|
|
|
manager.addSyncItem({
|
|
customerId: "cust-1",
|
|
orgId: "org-1",
|
|
env: AppEnv.Sandbox,
|
|
cusEntIds: ["ce-2"],
|
|
region: "us-east-1",
|
|
});
|
|
|
|
manager.addSyncItem({
|
|
customerId: "cust-1",
|
|
orgId: "org-1",
|
|
env: AppEnv.Sandbox,
|
|
cusEntIds: ["ce-1", "ce-3"],
|
|
rolloverIds: ["r-1"],
|
|
region: "us-east-1",
|
|
});
|
|
|
|
await wait(200);
|
|
|
|
expect(calls.length).toBe(1);
|
|
expect(calls[0].payload.cusEntIds.sort()).toEqual(["ce-1", "ce-2", "ce-3"]);
|
|
expect(calls[0].payload.rolloverIds).toEqual(["r-1"]);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 5. Same content in same dedup bucket → same dedup ID
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-5: same cusEntIds produce stable dedup IDs within a bucket")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 30,
|
|
dedupBucketMs: 60_000, // Large bucket so all calls land in the same bucket
|
|
});
|
|
|
|
// First batch
|
|
addItems({ manager, count: 5 });
|
|
await wait(80);
|
|
|
|
// Second batch (new window, same cusEntIds)
|
|
addItems({ manager, count: 5 });
|
|
await wait(80);
|
|
|
|
expect(calls.length).toBe(2);
|
|
expect(calls[0].messageDeduplicationId).toBe(
|
|
calls[1].messageDeduplicationId,
|
|
);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 6. Different cusEntIds → different dedup ID
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-6: different cusEntIds produce different dedup IDs")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 30,
|
|
dedupBucketMs: 60_000,
|
|
});
|
|
|
|
addItems({ manager, count: 1, cusEntIds: ["ce-1"] });
|
|
await wait(80);
|
|
|
|
addItems({ manager, count: 1, cusEntIds: ["ce-2"] });
|
|
await wait(80);
|
|
|
|
expect(calls.length).toBe(2);
|
|
expect(calls[0].messageDeduplicationId).not.toBe(
|
|
calls[1].messageDeduplicationId,
|
|
);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 7. MAX_BATCH_SIZE triggers immediate flush
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-7: exceeding MAX_BATCH_SIZE triggers immediate flush")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 5000, // Won't fire naturally
|
|
});
|
|
|
|
const cusEntIds = Array.from({ length: 1001 }, (_, i) => `ce-${i}`);
|
|
manager.addSyncItem({
|
|
customerId: "cust-1",
|
|
orgId: "org-1",
|
|
env: AppEnv.Sandbox,
|
|
cusEntIds,
|
|
region: "us-east-1",
|
|
});
|
|
|
|
await wait(50);
|
|
expect(calls.length).toBe(1);
|
|
expect(calls[0].payload.cusEntIds.length).toBe(1001);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 8. flush() drains all pending batches
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-8: flush() drains all pending batches")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 10_000, // Will never fire naturally
|
|
});
|
|
|
|
addItems({ manager, count: 3, customerId: "cust-A" });
|
|
addItems({ manager, count: 3, customerId: "cust-B" });
|
|
|
|
expect(calls.length).toBe(0);
|
|
await manager.flush();
|
|
|
|
expect(calls.length).toBe(2);
|
|
expect(manager.getStats().totalCustomers).toBe(0);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// 9. 10k burst → bounded number of flushes
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
test(
|
|
`${chalk.yellowBright("sync-batch-9: 10k rapid items produce bounded flushes")}`,
|
|
async () => {
|
|
const { fn, calls } = createMockQueue();
|
|
const manager = new SyncBatchingManagerV2({
|
|
addTaskToQueueFn: fn,
|
|
batchWindowMs: 100,
|
|
});
|
|
|
|
// All 10k items arrive synchronously — faster than the timer can fire
|
|
addItems({ manager, count: 10_000 });
|
|
|
|
await wait(300);
|
|
|
|
// Fixed window: all items land in a single batch since they arrive
|
|
// before the first timer fires. Should be exactly 1 flush.
|
|
expect(calls.length).toBe(1);
|
|
},
|
|
{ timeout: 5_000 },
|
|
);
|