190 lines
4.6 KiB
JavaScript
190 lines
4.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
RecursiveDeletePageLimitError,
|
|
runRecursiveDelete
|
|
} from '../src/workflows/recursive-delete.ts';
|
|
import {
|
|
browserRecursiveDeletePersistence
|
|
} from '../src/workflows/recursive-delete-persistence.ts';
|
|
|
|
const session = {
|
|
user_id: 'user-1',
|
|
display: null,
|
|
csrf: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
};
|
|
|
|
test('recursive delete continues from the first page through concurrent additions', async () => {
|
|
const persistence = memoryPersistence();
|
|
const plans = [
|
|
{ paths: ['tree/a'], continuation: 'page-2' },
|
|
{ paths: ['tree/b'], continuation: null },
|
|
{ paths: ['tree/new'], continuation: null },
|
|
{ paths: [], continuation: null }
|
|
];
|
|
const continuations = [];
|
|
const batches = [];
|
|
const client = recursiveClient({
|
|
plan(request) {
|
|
continuations.push(request.continuation);
|
|
return plans.shift();
|
|
},
|
|
batch(items) {
|
|
batches.push(items.map((item) => item.path));
|
|
return succeeded(items);
|
|
}
|
|
});
|
|
let operation = 0;
|
|
const result = await runRecursiveDelete(
|
|
client,
|
|
persistence,
|
|
session,
|
|
'personal',
|
|
'tree',
|
|
() => `operation-${++operation}`
|
|
);
|
|
assert.deepEqual(result, { deleted: 3, pages: 3 });
|
|
assert.deepEqual(continuations, [undefined, 'page-2', undefined, undefined]);
|
|
assert.deepEqual(batches, [['tree/a'], ['tree/b'], ['tree/new']]);
|
|
assert.equal(persistence.read(), null);
|
|
});
|
|
|
|
test('recursive delete reload preserves pending IDs and restarts terminal IDs', async () => {
|
|
const storage = new MemoryStorage();
|
|
const firstAdapter = browserRecursiveDeletePersistence(storage);
|
|
firstAdapter.save('shared', 'tree', [
|
|
{ path: 'tree/pending', operation_id: 'pending-id' },
|
|
{ path: 'tree/failed', operation_id: 'failed-id' }
|
|
]);
|
|
const reloaded = browserRecursiveDeletePersistence(storage);
|
|
const client = recursiveClient({
|
|
plan() {
|
|
throw new Error('saved batch should replay before planning');
|
|
},
|
|
batch(items) {
|
|
return [
|
|
{
|
|
path: items[0].path,
|
|
operation_id: items[0].operation_id,
|
|
result: 'pending',
|
|
error: null
|
|
},
|
|
{
|
|
path: items[1].path,
|
|
operation_id: items[1].operation_id,
|
|
result: 'failed',
|
|
error: null
|
|
}
|
|
];
|
|
}
|
|
});
|
|
await assert.rejects(
|
|
runRecursiveDelete(
|
|
client,
|
|
reloaded,
|
|
session,
|
|
'shared',
|
|
'tree',
|
|
() => 'replacement-id'
|
|
),
|
|
/2 objects remain/
|
|
);
|
|
assert.deepEqual(reloaded.read()?.items, [
|
|
{ path: 'tree/pending', operation_id: 'pending-id' },
|
|
{ path: 'tree/failed', operation_id: 'replacement-id' }
|
|
]);
|
|
});
|
|
|
|
test('recursive delete enforces the page bound and retains resumable work', async () => {
|
|
const persistence = memoryPersistence();
|
|
const client = recursiveClient({
|
|
plan() {
|
|
return { paths: ['tree/item'], continuation: null };
|
|
},
|
|
batch(items) {
|
|
return succeeded(items);
|
|
}
|
|
});
|
|
let operation = 0;
|
|
await assert.rejects(
|
|
runRecursiveDelete(
|
|
client,
|
|
persistence,
|
|
session,
|
|
'personal',
|
|
'tree',
|
|
() => `operation-${++operation}`,
|
|
100
|
|
),
|
|
(error) =>
|
|
error instanceof RecursiveDeletePageLimitError && error.pages === 100
|
|
);
|
|
assert.equal(operation, 100);
|
|
});
|
|
|
|
test('invalid persisted state is removed on reload', () => {
|
|
const storage = new MemoryStorage();
|
|
storage.setItem('nuthatch-recursive-delete', '{"items":[null]}');
|
|
const persistence = browserRecursiveDeletePersistence(storage);
|
|
assert.equal(persistence.read(), null);
|
|
assert.equal(storage.length, 0);
|
|
});
|
|
|
|
function recursiveClient(behavior) {
|
|
return {
|
|
async deletePlan(request) {
|
|
const plan = behavior.plan(request);
|
|
return {
|
|
ok: true,
|
|
value: plan
|
|
};
|
|
},
|
|
async deleteBatch({ body }) {
|
|
return {
|
|
ok: true,
|
|
value: { results: behavior.batch(body.items) }
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
function succeeded(items) {
|
|
return items.map((item) => ({
|
|
path: item.path,
|
|
operation_id: item.operation_id,
|
|
result: 'succeeded',
|
|
error: null
|
|
}));
|
|
}
|
|
|
|
function memoryPersistence() {
|
|
return browserRecursiveDeletePersistence(new MemoryStorage());
|
|
}
|
|
|
|
class MemoryStorage {
|
|
values = new Map();
|
|
|
|
get length() {
|
|
return this.values.size;
|
|
}
|
|
|
|
clear() {
|
|
this.values.clear();
|
|
}
|
|
|
|
getItem(key) {
|
|
return this.values.get(key) ?? null;
|
|
}
|
|
|
|
key(index) {
|
|
return [...this.values.keys()][index] ?? null;
|
|
}
|
|
|
|
removeItem(key) {
|
|
this.values.delete(key);
|
|
}
|
|
|
|
setItem(key, value) {
|
|
this.values.set(key, value);
|
|
}
|
|
}
|