143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|||
|
|
|
||
|
|
import { createPlanSaveQueue } from './planSaveQueue';
|
||
|
|
|
||
|
|
type Deferred = { promise: Promise<void>; resolve: () => void; reject: () => void };
|
||
|
|
|
||
|
|
const deferred = (): Deferred => {
|
||
|
|
let resolve!: () => void;
|
||
|
|
let reject!: () => void;
|
||
|
|
const promise = new Promise<void>((res, rej) => {
|
||
|
|
resolve = res;
|
||
|
|
reject = rej;
|
||
|
|
});
|
||
|
|
return { promise, resolve, reject };
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('planSaveQueue', () => {
|
||
|
|
test('runs writes for one document in schedule order even when they resolve out of order', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
const order: string[] = [];
|
||
|
|
const first = deferred();
|
||
|
|
const second = deferred();
|
||
|
|
|
||
|
|
const firstDone = queue.schedule('doc', 1, async () => {
|
||
|
|
await first.promise;
|
||
|
|
order.push('first');
|
||
|
|
});
|
||
|
|
const secondDone = queue.schedule('doc', 2, async () => {
|
||
|
|
order.push('second');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Second started only after first settles, regardless of timing.
|
||
|
|
first.resolve();
|
||
|
|
await firstDone;
|
||
|
|
second.resolve();
|
||
|
|
await secondDone;
|
||
|
|
|
||
|
|
expect(order).toEqual(['first', 'second']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('skips a revision at or below the last queued revision for the same document', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
let writes = 0;
|
||
|
|
|
||
|
|
await queue.schedule('doc', 3, async () => {
|
||
|
|
writes += 1;
|
||
|
|
});
|
||
|
|
await queue.schedule('doc', 3, async () => {
|
||
|
|
writes += 1;
|
||
|
|
});
|
||
|
|
await queue.schedule('doc', 2, async () => {
|
||
|
|
writes += 1;
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(writes).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('never lets a write for one document block another document', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
const blocked = deferred();
|
||
|
|
|
||
|
|
const blockedDone = queue.schedule('a', 1, async () => {
|
||
|
|
await blocked.promise;
|
||
|
|
});
|
||
|
|
let otherRan = false;
|
||
|
|
await queue.schedule('b', 1, async () => {
|
||
|
|
otherRan = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(otherRan).toBe(true);
|
||
|
|
blocked.resolve();
|
||
|
|
await blockedDone;
|
||
|
|
});
|
||
|
|
|
||
|
|
test('pendingFor waits for the outstanding chain of that document only', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
const slow = deferred();
|
||
|
|
let slowSettled = false;
|
||
|
|
|
||
|
|
void queue.schedule('a', 1, async () => {
|
||
|
|
await slow.promise;
|
||
|
|
slowSettled = true;
|
||
|
|
});
|
||
|
|
await queue.schedule('b', 1, async () => {});
|
||
|
|
|
||
|
|
await queue.pendingFor('b');
|
||
|
|
expect(slowSettled).toBe(false);
|
||
|
|
|
||
|
|
slow.resolve();
|
||
|
|
await queue.pendingFor('a');
|
||
|
|
expect(slowSettled).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('reset clears the revision watermark so a reloaded document can save again', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
let writes = 0;
|
||
|
|
|
||
|
|
await queue.schedule('doc', 5, async () => {
|
||
|
|
writes += 1;
|
||
|
|
});
|
||
|
|
queue.reset('doc');
|
||
|
|
await queue.schedule('doc', 1, async () => {
|
||
|
|
writes += 1;
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(writes).toBe(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a failed write does not poison the chain for later writes', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
|
||
|
|
const failing = queue.schedule('doc', 1, async () => {
|
||
|
|
throw new Error('write failed');
|
||
|
|
});
|
||
|
|
let secondRan = false;
|
||
|
|
const second = queue.schedule('doc', 2, async () => {
|
||
|
|
secondRan = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(failing).rejects.toThrow('write failed');
|
||
|
|
await second;
|
||
|
|
expect(secondRan).toBe(true);
|
||
|
|
await queue.pendingFor('doc');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('allows the same revision to retry after its write fails', async () => {
|
||
|
|
const queue = createPlanSaveQueue();
|
||
|
|
let attempts = 0;
|
||
|
|
|
||
|
|
const failing = queue.schedule('doc', 1, async () => {
|
||
|
|
attempts += 1;
|
||
|
|
throw new Error('write failed');
|
||
|
|
});
|
||
|
|
await expect(failing).rejects.toThrow('write failed');
|
||
|
|
|
||
|
|
await queue.schedule('doc', 1, async () => {
|
||
|
|
attempts += 1;
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(attempts).toBe(2);
|
||
|
|
});
|
||
|
|
});
|