blob: 2695cc949051b8615564123acf41b01c9c3a0b58 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { chainWrite } from '../write-queue.js';
// Flush all pending microtasks (e.g. a .finally cleanup callback queued
// after a chain settles) before asserting on Map state.
const flushMicrotasks = () => new Promise<void>((resolve) => setImmediate(resolve));
describe('chainWrite', () => {
it('evicts queue entries once the chain drains', async () => {
const queues = new Map<string, Promise<void>>();
for (let i = 0; i < 50; i++) {
await chainWrite(queues, `k${i}`, async () => {});
}
// Earlier keys' .finally cleanups run during the subsequent awaits;
// setImmediate drains the last key's residual microtask.
await flushMicrotasks();
assert.equal(queues.size, 0);
});
it('does not evict while a newer write is queued behind', async () => {
const queues = new Map<string, Promise<void>>();
let resolveOp1!: () => void;
let resolveOp2!: () => void;
const op1 = () =>
new Promise<void>((resolve) => {
resolveOp1 = resolve;
});
const op2 = () =>
new Promise<void>((resolve) => {
resolveOp2 = resolve;
});
const p1 = chainWrite(queues, 'k', op1);
const p2 = chainWrite(queues, 'k', op2); // queues behind op1; overwrites map entry
// Let op1's body run (it sets resolveOp1) without completing it.
// op2 stays queued behind op1's pending chain.
await flushMicrotasks();
resolveOp1();
await p1; // op1 drains; op2 starts running
await flushMicrotasks();
// op2 is now in flight — the identity guard must keep its entry
// alive (op1's .finally saw get(k) !== its own tracked promise).
assert.equal(queues.has('k'), true);
resolveOp2();
await p2;
await flushMicrotasks();
assert.equal(queues.has('k'), false);
});
it('does not evict when a successor is queued behind a failing write', async () => {
// Combines the two axes the tests above split: the in-flight write
// rejects *while* a successor is already queued behind it (rather than
// a successor queued after the rejection settles). Guards the identity
// check under rejection: op1's .finally must see get(k) !== its own
// tracked promise (op2 overwrote it) and leave the entry in place for
// the still-in-flight op2. Note: this does NOT guard the .catch(noop)
// swallow — op2's .then(op, op) already absorbs op1's rejection — that
// invariant is covered by the "keeps the chain alive" test below.
const queues = new Map<string, Promise<void>>();
let rejectOp1!: (e: Error) => void;
let resolveOp2!: () => void;
const p1 = chainWrite(
queues,
'k',
() =>
new Promise<void>((_, rej) => {
rejectOp1 = rej;
}),
);
const p2 = chainWrite(
queues,
'k',
() =>
new Promise<void>((res) => {
resolveOp2 = res;
}),
);
// op1's body runs and parks on rejectOp1; op2 stays queued behind it.
await flushMicrotasks();
rejectOp1(new Error('boom'));
await assert.rejects(p1, /boom/);
await flushMicrotasks();
// op2 is now in flight — the identity guard kept its entry alive.
assert.equal(queues.has('k'), true);
resolveOp2();
await p2;
await flushMicrotasks();
assert.equal(queues.size, 0);
});
it('serializes operations under the same key in call order', async () => {
const queues = new Map<string, Promise<void>>();
const order: number[] = [];
const promises: Promise<void>[] = [];
for (let i = 0; i < 5; i++) {
promises.push(
chainWrite(queues, 'k', async () => {
order.push(i);
}),
);
}
await Promise.all(promises);
assert.deepEqual(order, [0, 1, 2, 3, 4]);
});
it('propagates rejection to the caller and keeps the chain alive', async () => {
const queues = new Map<string, Promise<void>>();
await assert.rejects(
chainWrite(queues, 'k', async () => {
throw new Error('boom');
}),
/boom/,
);
// The Map-held chain swallowed the rejection; a subsequent write
// under the same key must still run.
let ran = false;
await chainWrite(queues, 'k', async () => {
ran = true;
});
assert.equal(ran, true);
await flushMicrotasks();
assert.equal(queues.size, 0);
});
});