mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
|
|
|
|
describe("DatabaseSaveTrigger", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
DatabaseSaveTrigger.cleanup();
|
|
});
|
|
|
|
it("batches informational touches without scheduling per-session saves", async () => {
|
|
vi.useFakeTimers();
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
for (let minute = 0; minute < 4; minute++) {
|
|
DatabaseSaveTrigger.markDirty();
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
}
|
|
expect(save).not.toHaveBeenCalled();
|
|
expect(DatabaseSaveTrigger.isDirty).toBe(true);
|
|
expect(DatabaseSaveTrigger.getStatus().hasPendingTimeout).toBe(false);
|
|
const flushed = DatabaseSaveTrigger.forceSave("periodic_flush");
|
|
// A save yields one event-loop turn, which fake timers also control.
|
|
await vi.runOnlyPendingTimersAsync();
|
|
await flushed;
|
|
expect(save).toHaveBeenCalledOnce();
|
|
expect(DatabaseSaveTrigger.isDirty).toBe(false);
|
|
});
|
|
|
|
it("force saves through the initialized save function", async () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
|
|
await DatabaseSaveTrigger.forceSave("test_force_save");
|
|
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(DatabaseSaveTrigger.getStatus()).toMatchObject({
|
|
initialized: true,
|
|
pendingSave: false,
|
|
hasPendingTimeout: false,
|
|
});
|
|
});
|
|
|
|
it("lets the event loop turn before a force save resolves", async () => {
|
|
DatabaseSaveTrigger.initialize(vi.fn().mockResolvedValue(undefined));
|
|
let turned = false;
|
|
setImmediate(() => {
|
|
turned = true;
|
|
});
|
|
|
|
await DatabaseSaveTrigger.forceSave("bulk_write");
|
|
|
|
expect(turned).toBe(true);
|
|
});
|
|
|
|
it("debounces dirty saves and marks the database clean after saving", async () => {
|
|
vi.useFakeTimers();
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
|
|
await DatabaseSaveTrigger.triggerSave("first");
|
|
await DatabaseSaveTrigger.triggerSave("second");
|
|
|
|
expect(DatabaseSaveTrigger.isDirty).toBe(true);
|
|
expect(DatabaseSaveTrigger.getStatus().hasPendingTimeout).toBe(true);
|
|
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
await vi.runOnlyPendingTimersAsync();
|
|
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(DatabaseSaveTrigger.isDirty).toBe(false);
|
|
expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false);
|
|
});
|
|
|
|
it("queues a force save behind an in-flight save", async () => {
|
|
let finishFirstSave: (() => void) | undefined;
|
|
const firstSave = new Promise<void>((resolve) => {
|
|
finishFirstSave = resolve;
|
|
});
|
|
const save = vi
|
|
.fn<() => Promise<void>>()
|
|
.mockReturnValueOnce(firstSave)
|
|
.mockResolvedValueOnce(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
|
|
const first = DatabaseSaveTrigger.forceSave("first_write");
|
|
await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1));
|
|
|
|
const second = DatabaseSaveTrigger.forceSave("sso_provider_write");
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
|
|
finishFirstSave?.();
|
|
await Promise.all([first, second]);
|
|
|
|
expect(save).toHaveBeenCalledTimes(2);
|
|
expect(DatabaseSaveTrigger.getStatus().pendingSave).toBe(false);
|
|
});
|
|
|
|
it("collapses force saves inside a batch into one save", async () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
|
|
const result = await DatabaseSaveTrigger.batched(async (rows: number) => {
|
|
for (let i = 0; i < rows; i++) {
|
|
await DatabaseSaveTrigger.forceSave("row_write");
|
|
}
|
|
expect(save).not.toHaveBeenCalled();
|
|
return rows;
|
|
})(50);
|
|
|
|
expect(result).toBe(50);
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(DatabaseSaveTrigger.isDirty).toBe(false);
|
|
});
|
|
|
|
it("folds a nested batch into the outer one", async () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
const inner = DatabaseSaveTrigger.batched(() =>
|
|
DatabaseSaveTrigger.forceSave("inner_write"),
|
|
);
|
|
|
|
await DatabaseSaveTrigger.batched(async () => {
|
|
await inner();
|
|
await inner();
|
|
})();
|
|
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("skips the save when a batch wrote nothing", async () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
|
|
await DatabaseSaveTrigger.batched(async () => {})();
|
|
|
|
expect(save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("still saves what a failed batch wrote", async () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
|
|
await expect(
|
|
DatabaseSaveTrigger.batched(async () => {
|
|
await DatabaseSaveTrigger.forceSave("row_write");
|
|
throw new Error("boom");
|
|
})(),
|
|
).rejects.toThrow("boom");
|
|
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("saves immediately for work that outlives its batch", async () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
DatabaseSaveTrigger.initialize(save);
|
|
let release: (() => void) | undefined;
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
let detached: Promise<void> | undefined;
|
|
|
|
await DatabaseSaveTrigger.batched(async () => {
|
|
detached = gate.then(() => DatabaseSaveTrigger.forceSave("late_write"));
|
|
})();
|
|
release?.();
|
|
await detached;
|
|
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|