Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified dist/webbrain-chrome-32.0.0.zip
Binary file not shown.
Binary file modified dist/webbrain-edge-32.0.0.zip
Binary file not shown.
Binary file modified dist/webbrain-firefox-32.0.0.zip
Binary file not shown.
27 changes: 20 additions & 7 deletions src/chrome/src/agent/apocalypse-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,19 @@ export function createApocalypseArchiveManager(options = {}) {
let processing = false;
if (!store || !storage) throw new Error('Apocalypse Mode requires state and archive storage adapters.');

async function cancelledResult() {
try {
const config = await store.getConfig();
if (config?.enabled === true) {
const nextDelay = nextArchiveScheduleDelay(await store.listArchives(), now());
if (nextDelay != null) schedule(nextDelay);
}
} catch {
// Preserve the cancellation result if rearming the shared alarm fails.
}
return { processed: false, reason: 'cancelled' };
}

async function getSnapshot() {
const [config, archives] = await Promise.all([store.getConfig(), store.listArchives()]);
return {
Expand Down Expand Up @@ -973,7 +986,7 @@ export function createApocalypseArchiveManager(options = {}) {
const saved = await putArchiveIfCurrent(store, recovered, {
status: 'downloading', generation, leaseToken, updatedAt: record.updatedAt,
});
if (!saved) return { processed: false, reason: 'cancelled' };
if (!saved) return await cancelledResult();
record = recovered;
}
let writer = null;
Expand Down Expand Up @@ -1006,7 +1019,7 @@ export function createApocalypseArchiveManager(options = {}) {
const saved = await putArchiveIfCurrent(store, marked, {
status: 'downloading', generation, leaseToken, updatedAt: record.updatedAt,
});
if (!saved) return { processed: false, reason: 'cancelled' };
if (!saved) return await cancelledResult();
record = marked;
writer = await storage.createWriter(record.target, record);
usedWriteSession = true;
Expand All @@ -1033,7 +1046,7 @@ export function createApocalypseArchiveManager(options = {}) {
let current = await store.getArchive(record.id);
let currentConfig = await store.getConfig();
if (!ownsDownloadClaim(current, generation, leaseToken, currentConfig)) {
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
if (writer) await writer.write(offset, bytes);
else await storage.write(record.target, offset, bytes, record);
Expand All @@ -1042,7 +1055,7 @@ export function createApocalypseArchiveManager(options = {}) {
if (!ownsDownloadClaim(current, generation, leaseToken, currentConfig)) {
await abortWriteSession(new Error('Archive download was cancelled.')).catch(() => {});
if (!current) await storage.remove(record.target, record).catch(() => {});
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
const bytesDownloaded = offset + bytes.byteLength;
const finished = bytesDownloaded >= Number(record.size);
Expand Down Expand Up @@ -1082,7 +1095,7 @@ export function createApocalypseArchiveManager(options = {}) {
if (writeSessionCommitted && !await store.getArchive(record.id)) {
await storage.remove(record.target, record).catch(() => {});
}
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
piecesProcessed += 1;
if (continueInWake) {
Expand All @@ -1098,7 +1111,7 @@ export function createApocalypseArchiveManager(options = {}) {
const current = await store.getArchive(record.id);
if (!current || current.generation !== generation || current.leaseToken !== leaseToken || controller.signal.aborted) {
if (!current && writeSessionCommitted) await storage.remove(record.target, record).catch(() => {});
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
const rollbackWriteSession = current.writeSessionStartPiece != null && !writeSessionCommitted;
const permissionRequired = isFilePermissionError(error, current.target);
Expand Down Expand Up @@ -1128,7 +1141,7 @@ export function createApocalypseArchiveManager(options = {}) {
const saved = await putArchiveIfCurrent(store, next, {
status: 'downloading', generation, leaseToken, updatedAt: current.updatedAt,
});
if (!saved) return { processed: false, reason: 'cancelled' };
if (!saved) return await cancelledResult();
const nextDelay = nextArchiveScheduleDelay(await store.listArchives(), now());
if (nextDelay != null) schedule(nextDelay);
return { processed: false, reason: retrying ? 'retrying' : 'error', archive: next };
Expand Down
27 changes: 20 additions & 7 deletions src/firefox/src/agent/apocalypse-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,19 @@ export function createApocalypseArchiveManager(options = {}) {
let processing = false;
if (!store || !storage) throw new Error('Apocalypse Mode requires state and archive storage adapters.');

async function cancelledResult() {
try {
const config = await store.getConfig();
if (config?.enabled === true) {
const nextDelay = nextArchiveScheduleDelay(await store.listArchives(), now());
if (nextDelay != null) schedule(nextDelay);
}
} catch {
// Preserve the cancellation result if rearming the shared alarm fails.
}
return { processed: false, reason: 'cancelled' };
}

async function getSnapshot() {
const [config, archives] = await Promise.all([store.getConfig(), store.listArchives()]);
return {
Expand Down Expand Up @@ -973,7 +986,7 @@ export function createApocalypseArchiveManager(options = {}) {
const saved = await putArchiveIfCurrent(store, recovered, {
status: 'downloading', generation, leaseToken, updatedAt: record.updatedAt,
});
if (!saved) return { processed: false, reason: 'cancelled' };
if (!saved) return await cancelledResult();
record = recovered;
}
let writer = null;
Expand Down Expand Up @@ -1006,7 +1019,7 @@ export function createApocalypseArchiveManager(options = {}) {
const saved = await putArchiveIfCurrent(store, marked, {
status: 'downloading', generation, leaseToken, updatedAt: record.updatedAt,
});
if (!saved) return { processed: false, reason: 'cancelled' };
if (!saved) return await cancelledResult();
record = marked;
writer = await storage.createWriter(record.target, record);
usedWriteSession = true;
Expand All @@ -1033,7 +1046,7 @@ export function createApocalypseArchiveManager(options = {}) {
let current = await store.getArchive(record.id);
let currentConfig = await store.getConfig();
if (!ownsDownloadClaim(current, generation, leaseToken, currentConfig)) {
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
if (writer) await writer.write(offset, bytes);
else await storage.write(record.target, offset, bytes, record);
Expand All @@ -1042,7 +1055,7 @@ export function createApocalypseArchiveManager(options = {}) {
if (!ownsDownloadClaim(current, generation, leaseToken, currentConfig)) {
await abortWriteSession(new Error('Archive download was cancelled.')).catch(() => {});
if (!current) await storage.remove(record.target, record).catch(() => {});
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
const bytesDownloaded = offset + bytes.byteLength;
const finished = bytesDownloaded >= Number(record.size);
Expand Down Expand Up @@ -1082,7 +1095,7 @@ export function createApocalypseArchiveManager(options = {}) {
if (writeSessionCommitted && !await store.getArchive(record.id)) {
await storage.remove(record.target, record).catch(() => {});
}
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
piecesProcessed += 1;
if (continueInWake) {
Expand All @@ -1098,7 +1111,7 @@ export function createApocalypseArchiveManager(options = {}) {
const current = await store.getArchive(record.id);
if (!current || current.generation !== generation || current.leaseToken !== leaseToken || controller.signal.aborted) {
if (!current && writeSessionCommitted) await storage.remove(record.target, record).catch(() => {});
return { processed: false, reason: 'cancelled' };
return await cancelledResult();
}
const rollbackWriteSession = current.writeSessionStartPiece != null && !writeSessionCommitted;
const permissionRequired = isFilePermissionError(error, current.target);
Expand Down Expand Up @@ -1128,7 +1141,7 @@ export function createApocalypseArchiveManager(options = {}) {
const saved = await putArchiveIfCurrent(store, next, {
status: 'downloading', generation, leaseToken, updatedAt: current.updatedAt,
});
if (!saved) return { processed: false, reason: 'cancelled' };
if (!saved) return await cancelledResult();
const nextDelay = nextArchiveScheduleDelay(await store.listArchives(), now());
if (nextDelay != null) schedule(nextDelay);
return { processed: false, reason: retrying ? 'retrying' : 'error', archive: next };
Expand Down
54 changes: 54 additions & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -21366,6 +21366,60 @@ test('Apocalypse Mode requires opt-in and removal wins an in-flight download rac
}
});

test('Apocalypse Mode rearms queued work after pausing or deleting the active download', async () => {
for (const [label, runtime] of [['chrome', ApocalypseModeCh], ['firefox', ApocalypseModeFx]]) {
for (const action of ['pause', 'remove']) {
const active = {
id: `${action}-active`, status: 'queued', generation: 1, updatedAt: 100,
filename: 'active.zim', size: 1, pieceLength: 1, pieceHashAlgorithm: 'sha-1', pieceHashes: ['aa'],
downloadUrl: 'https://example.test/active.zim', target: { kind: 'opfs', key: 'active.zim' },
pieceIndex: 0, bytesDownloaded: 0, retryCount: 0,
};
const queued = {
...active,
id: `${action}-queued`,
filename: 'queued.zim',
downloadUrl: 'https://example.test/queued.zim',
target: { kind: 'opfs', key: 'queued.zim' },
};
const records = new Map([[active.id, active], [queued.id, queued]]);
const store = {
async getConfig() { return { enabled: true }; },
async listArchives() { return [...records.values()].map(record => ({ ...record })); },
async getArchive(id) { const record = records.get(id); return record ? { ...record } : null; },
async putArchive(record) { records.set(record.id, { ...record }); return record; },
async deleteArchive(id) { records.delete(id); },
};
let markFetchStarted;
const fetchStarted = new Promise(resolve => { markFetchStarted = resolve; });
const scheduled = [];
const manager = runtime.createApocalypseArchiveManager({
store,
storage: { async remove() {} },
fetchImpl: async (_url, request) => await new Promise((_resolve, reject) => {
request.signal.addEventListener('abort', () => reject(new DOMException('cancelled', 'AbortError')), { once: true });
markFetchStarted();
}),
digestHex: async () => 'aa',
schedule: delay => scheduled.push(delay),
randomId: () => `${action}-lease`,
now: () => 1000,
});

const running = manager.processNext();
await fetchStarted;
await manager[action](active.id);
const result = await running;

assert.equal(result.reason, 'cancelled', `${label}: ${action} did not cancel the active download`);
assert.deepEqual(scheduled, [0], `${label}: ${action} stranded the next queued archive`);
assert.equal(records.get(queued.id)?.status, 'queued', `${label}: ${action} changed the next archive state`);
if (action === 'pause') assert.equal(records.get(active.id)?.status, 'paused', `${label}: pause state was lost`);
else assert.equal(records.has(active.id), false, `${label}: removed active archive metadata was retained`);
}
}
});

test('Apocalypse Mode disabling loses atomically to concurrent archive deletion', async () => {
for (const [label, runtime] of [['chrome', ApocalypseModeCh], ['firefox', ApocalypseModeFx]]) {
const config = { enabled: true, updatePolicy: 'manual' };
Expand Down
Loading