@coffre/vault 0.14.4 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cloudflare.d.ts +343 -9
- package/dist/cloudflare.js +802 -6
- package/dist/{index-CIq4i_KT.d.ts → index-DQ-FKVF3.d.ts} +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/log-entry-BYWqj3O5.d.ts +364 -0
- package/dist/node.d.ts +182 -7
- package/dist/node.js +358 -16
- package/dist/{src-NM5NsYFs.js → src-DheG92EM.js} +9 -2
- package/dist/vault-kmtojYnW.js +14623 -0
- package/package.json +4 -5
- package/src/bulk.ts +89 -0
- package/src/changes.ts +478 -0
- package/src/checkpointer-node.ts +342 -0
- package/src/checkpointer-workers.ts +514 -0
- package/src/checkpointer.ts +1165 -0
- package/src/cloudflare.ts +207 -13
- package/src/config.ts +15 -3
- package/src/credentials.postgres.ts +78 -0
- package/src/credentials.sqlite.ts +240 -0
- package/src/credentials.ts +630 -0
- package/src/cutover.ts +275 -0
- package/src/decide.ts +437 -2
- package/src/inputs.ts +499 -0
- package/src/local.ts +20 -4
- package/src/log-checkpointer.ts +116 -0
- package/src/log-entry.ts +113 -0
- package/src/log-replay.ts +188 -0
- package/src/log-store.ts +695 -0
- package/src/node.ts +73 -9
- package/src/objects.ts +141 -0
- package/src/reads.ts +502 -0
- package/src/retention.ts +229 -0
- package/src/snapshot.postgres.ts +39 -9
- package/src/snapshot.sqlite.ts +28 -3
- package/src/snapshot.ts +72 -12
- package/src/store.ts +862 -37
- package/src/use.postgres.ts +111 -0
- package/src/use.sqlite.ts +542 -0
- package/src/use.ts +1060 -0
- package/src/vault.ts +1513 -240
- package/src/verifier.ts +136 -0
- package/src/witness-workers.ts +163 -0
- package/dist/vault-BBdXsny3.js +0 -5077
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { appendFile, open, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { PairNotes } from '@coffre/core/merkle';
|
|
5
|
+
import type { Sealing } from '@coffre/core/vault';
|
|
6
|
+
import type { Database } from '@coffre/db';
|
|
7
|
+
|
|
8
|
+
import { Fence, heldIn, sealingOf, Stalled, TICK_BOUND_MS, Unsealed, within, type Alarm, type Checkpointer, type Memory, type SealThrough, type Witness } from './checkpointer.ts';
|
|
9
|
+
import { daysAround, openLogCheckpointer, reasonOf, TICK_MS, upkeep } from './log-checkpointer.ts';
|
|
10
|
+
import { Projector } from './reads.ts';
|
|
11
|
+
import { verifierKeys } from './verifier.ts';
|
|
12
|
+
import { logStore } from './log-store.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The 0.15 checkpointer on Node (docs/design/audit-tree.md): one process
|
|
16
|
+
* seals the log every 500 ms, its memory a file beside the vault, synced
|
|
17
|
+
* before the database has the pair it holds (RememberFirst). The vault's
|
|
18
|
+
* process runs it (`keepCheckpointer`, node.ts). Nothing it awaits is
|
|
19
|
+
* unbounded (T169): a tick past its bound stalls the checkpointer, and the
|
|
20
|
+
* vault's process opens another.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The signer's memory in a file: written whole, synced, renamed over the last, and the directory synced, so a power loss keeps it. */
|
|
24
|
+
export function fileMemory(path: string): Memory {
|
|
25
|
+
return {
|
|
26
|
+
last: () => readFile(path, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
|
27
|
+
if (error.code === 'ENOENT') return null;
|
|
28
|
+
throw error;
|
|
29
|
+
}),
|
|
30
|
+
async remember(record) {
|
|
31
|
+
const file = await open(`${path}.part`, 'w');
|
|
32
|
+
try {
|
|
33
|
+
await file.writeFile(record);
|
|
34
|
+
await file.sync();
|
|
35
|
+
} finally {
|
|
36
|
+
await file.close();
|
|
37
|
+
}
|
|
38
|
+
await rename(`${path}.part`, path);
|
|
39
|
+
const dir = await open(dirname(path), 'r');
|
|
40
|
+
try {
|
|
41
|
+
await dir.sync();
|
|
42
|
+
} finally {
|
|
43
|
+
await dir.close();
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type RunningCheckpointer = {
|
|
50
|
+
checkpointer: Checkpointer;
|
|
51
|
+
/** For the change path (plan PR 4): seal now, then wait for the witnesses (R51). */
|
|
52
|
+
sealThrough: SealThrough;
|
|
53
|
+
/** The alarms raised since `since` (ms), oldest first, from the file beside the memory. */
|
|
54
|
+
alarms(since?: number): Promise<Alarm[]>;
|
|
55
|
+
/** The pair the signer remembers, from its file: what the vault's verifier holds its walks to (`VaultOptions.heldOutside`). */
|
|
56
|
+
heldOutside(): Promise<PairNotes[]>;
|
|
57
|
+
/** The reads' projection run until it stands at the newest pair (T166): for tests and tools, never on a request's path. */
|
|
58
|
+
projected(): Promise<void>;
|
|
59
|
+
/** Whether it seals (`Vault.sealing`): ticking, its last tick ended well, lately. */
|
|
60
|
+
sealing(): Sealing;
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** How long the runner waits on an open, the next days of uses, or the log's upkeep after a tick (T169). */
|
|
65
|
+
const OPEN_BOUND_MS = TICK_BOUND_MS;
|
|
66
|
+
const UPKEEP_BOUND_MS = 60_000;
|
|
67
|
+
|
|
68
|
+
/** A connection of the checkpointer's own (T169), which its runner cuts once it stalls: `openDatabase(url, { max })`. */
|
|
69
|
+
export type Session = { db: Database; cut(): Promise<void> };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Opens the checkpointer over `database` (the vault's login) and ticks it
|
|
73
|
+
* every `intervalMs`, a tick after the last one ends, each followed by the
|
|
74
|
+
* log's upkeep (days made ahead, retention past `retainDays`, the bulk
|
|
75
|
+
* cap's rows pruned). Each alarm is appended to `<memory>.alarms`, one JSON
|
|
76
|
+
* line each. Given `connect` instead, over a session of its own, cut once
|
|
77
|
+
* it stalls or stops, so that what it still awaits holds no connection of
|
|
78
|
+
* the vault's (review r1).
|
|
79
|
+
*/
|
|
80
|
+
export async function runCheckpointer(options: {
|
|
81
|
+
/** The vault's database, or `connect`, a session of its own. */
|
|
82
|
+
database?: Database;
|
|
83
|
+
connect?: () => Promise<Session>;
|
|
84
|
+
seed: Uint8Array;
|
|
85
|
+
/** The memory's file. */
|
|
86
|
+
memory: string;
|
|
87
|
+
witnesses?: readonly Witness[];
|
|
88
|
+
intervalMs?: number;
|
|
89
|
+
tileRange?: number;
|
|
90
|
+
/** Full days of uses kept before today's; null keeps every day (retention.ts). */
|
|
91
|
+
retainDays?: number | null;
|
|
92
|
+
/** Told once its checkpointer stalls (T169), which ticks no more: `keepCheckpointer` opens another. */
|
|
93
|
+
stalled?: (why: string) => void;
|
|
94
|
+
/** `TICK_BOUND_MS`, which bounds the open and the next days too: for the tests. */
|
|
95
|
+
tickBoundMs?: number;
|
|
96
|
+
/** The memory over its file, `fileMemory` unless a test holds its writes. */
|
|
97
|
+
memoryOf?: (path: string) => Memory;
|
|
98
|
+
/** `UPKEEP_BOUND_MS`, for the tests. */
|
|
99
|
+
upkeepBoundMs?: number;
|
|
100
|
+
}): Promise<RunningCheckpointer> {
|
|
101
|
+
const alarmsPath = `${options.memory}.alarms`;
|
|
102
|
+
let appending: Promise<void> = Promise.resolve();
|
|
103
|
+
if ((options.connect === undefined) === (options.database === undefined)) throw new Error('the checkpointer runs over its database or a session it connects, one of the two');
|
|
104
|
+
const session = options.connect === undefined ? null : await within(options.connect(), options.tickBoundMs ?? OPEN_BOUND_MS, 'connecting');
|
|
105
|
+
const store = logStore(session?.db ?? options.database!, { tileRange: options.tileRange });
|
|
106
|
+
const fence = new Fence();
|
|
107
|
+
// The next days of uses first: a use needs its day's partition even if the checkpointer then refuses to open. Each
|
|
108
|
+
// bounded (T169); an open past its bound fenced, so that it does nothing more once another opens.
|
|
109
|
+
const checkpointer = await within(store.makeDays(daysAround(Date.now())), options.tickBoundMs ?? OPEN_BOUND_MS, 'making the next days of uses').then(() => within(openLogCheckpointer({
|
|
110
|
+
store,
|
|
111
|
+
seed: options.seed,
|
|
112
|
+
memory: (options.memoryOf ?? fileMemory)(options.memory),
|
|
113
|
+
witnesses: options.witnesses,
|
|
114
|
+
record: (alarm) => record(alarm),
|
|
115
|
+
fence,
|
|
116
|
+
...(options.tickBoundMs === undefined ? {} : { tickBoundMs: options.tickBoundMs }),
|
|
117
|
+
}), options.tickBoundMs ?? OPEN_BOUND_MS, 'the open')).catch(async (error: unknown) => {
|
|
118
|
+
// Fenced, its session cut, and its memory write in flight landed, before the keeper may open another over the same
|
|
119
|
+
// file (review r1).
|
|
120
|
+
await session?.cut().catch(() => {});
|
|
121
|
+
await fence.close('its open failed');
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
/** In order, one after the other; a file grown past a megabyte keeps its last 30 days. */
|
|
125
|
+
function record(alarm: Alarm): Promise<void> {
|
|
126
|
+
appending = appending.then(async () => {
|
|
127
|
+
await appendFile(alarmsPath, `${JSON.stringify(alarm)}\n`);
|
|
128
|
+
if ((await stat(alarmsPath)).size > 1_000_000) {
|
|
129
|
+
const kept = (await readAlarms(alarmsPath)).filter((a) => a.at >= alarm.at - 30 * 86_400_000);
|
|
130
|
+
await writeFile(`${alarmsPath}.part`, kept.map((a) => `${JSON.stringify(a)}\n`).join(''));
|
|
131
|
+
await rename(`${alarmsPath}.part`, alarmsPath);
|
|
132
|
+
}
|
|
133
|
+
}).catch((error: Error) => console.warn(`coffre audit: an alarm could not be kept: ${error.message}`));
|
|
134
|
+
return appending;
|
|
135
|
+
}
|
|
136
|
+
// Behind the checkpointer's fence: an upkeep still running once it stalled does nothing beside the next one's.
|
|
137
|
+
const keep = upkeep(checkpointer, fence.store(store), {
|
|
138
|
+
keepDays: options.retainDays ?? null,
|
|
139
|
+
seed: options.seed,
|
|
140
|
+
alert: (message) => {
|
|
141
|
+
console.warn(`coffre audit: ${message}`);
|
|
142
|
+
void record({ at: Date.now(), message });
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
// The reads' projection (T166), kicked after each tick and never waited on: a projection behind lags the audit pages,
|
|
146
|
+
// never a seal (reads.ts).
|
|
147
|
+
const keys = await verifierKeys([options.seed]);
|
|
148
|
+
const projector = new Projector(store, keys, (message) => {
|
|
149
|
+
console.warn(`coffre audit: ${message}`);
|
|
150
|
+
void record({ at: Date.now(), message });
|
|
151
|
+
});
|
|
152
|
+
const interval = options.intervalMs ?? TICK_MS;
|
|
153
|
+
let stopped = false;
|
|
154
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
155
|
+
let ticking: Promise<unknown> = Promise.resolve();
|
|
156
|
+
// A tick that throws (a failed write may be a tamper signal: a tile changed under the signer) is an alarm, kept once.
|
|
157
|
+
let failed: string | null = null;
|
|
158
|
+
// The last tick that ended, for readiness; and whether the checkpointer stalled, which ends the loop.
|
|
159
|
+
let last: { at: number; failures: string[] } | null = null;
|
|
160
|
+
let stalled = false;
|
|
161
|
+
// The upkeep, bounded, beside the ticks: one past its bound goes on behind them, and none starts beside it. Its
|
|
162
|
+
// store is behind the checkpointer's fence, and its connection the runner's own session's, where it has one.
|
|
163
|
+
let upkeeping: Promise<void> | null = null;
|
|
164
|
+
// Told once to its keeper, a stall's or an upkeep's past its bound.
|
|
165
|
+
let retired = false;
|
|
166
|
+
const retire = (why: string) => {
|
|
167
|
+
if (retired) return;
|
|
168
|
+
retired = true;
|
|
169
|
+
options.stalled?.(why);
|
|
170
|
+
};
|
|
171
|
+
const upkeepOnce = async () => {
|
|
172
|
+
if (upkeeping !== null) return;
|
|
173
|
+
const run = (upkeeping = keep().finally(() => {
|
|
174
|
+
if (upkeeping === run) upkeeping = null;
|
|
175
|
+
}));
|
|
176
|
+
// Past its bound, the runner is retired as a stall is (review r5): its keeper cuts the session the upkeep waits on
|
|
177
|
+
// and opens another, whose upkeep starts afresh. Kept, it would let no upkeep run again, and the days run out.
|
|
178
|
+
await within(run, options.upkeepBoundMs ?? UPKEEP_BOUND_MS, "the log's upkeep").catch(async (error: Error) => {
|
|
179
|
+
const message = `a checkpoint failed: ${error.message}`;
|
|
180
|
+
console.warn(`coffre audit: ${message}`);
|
|
181
|
+
failed = message;
|
|
182
|
+
stalled = true;
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
await record({ at: Date.now(), message });
|
|
185
|
+
void ticking.finally(() => retire(message));
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
// The upkeep after a tick that held; after one refused, only the next days (retention would fail on it, alarmed again).
|
|
189
|
+
// A window sealed only in part (T169, WINDOW_MAX): the next tick at once, the upkeep once it has caught up.
|
|
190
|
+
let more = false;
|
|
191
|
+
const loop = () => {
|
|
192
|
+
ticking = checkpointer.tick().then((t) => {
|
|
193
|
+
failed = null;
|
|
194
|
+
last = { at: Date.now(), failures: t.failures };
|
|
195
|
+
projector.kick();
|
|
196
|
+
more = t.more;
|
|
197
|
+
// Beside the ticks, never ahead of them: an upkeep that waits on its database holds no tick back (CI, review of
|
|
198
|
+
// the stall tests), and none starts beside another (`upkeepOnce`).
|
|
199
|
+
if (!more) void upkeepOnce();
|
|
200
|
+
}, async (error: Error) => {
|
|
201
|
+
more = false;
|
|
202
|
+
const message = `a checkpoint failed: ${reasonOf(error)}`;
|
|
203
|
+
if (message !== failed) {
|
|
204
|
+
failed = message;
|
|
205
|
+
console.warn(`coffre audit: ${message}`);
|
|
206
|
+
await record({ at: Date.now(), message });
|
|
207
|
+
}
|
|
208
|
+
// A checkpointer that stalled (T169) ticks no more: its keeper opens another.
|
|
209
|
+
if (error instanceof Stalled) stalled = true;
|
|
210
|
+
await within(store.makeDays(daysAround(Date.now())), options.tickBoundMs ?? OPEN_BOUND_MS, 'making the next days of uses').catch(() => {});
|
|
211
|
+
}).finally(() => {
|
|
212
|
+
if (stalled) retire(failed ?? "the log's upkeep did not end in time");
|
|
213
|
+
else if (!stopped) timer = setTimeout(loop, more ? 0 : interval);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
loop();
|
|
217
|
+
return {
|
|
218
|
+
checkpointer,
|
|
219
|
+
sealThrough: (number, deadline) => checkpointer.sealThrough(number, deadline),
|
|
220
|
+
async heldOutside() {
|
|
221
|
+
return heldIn(await fileMemory(options.memory).last());
|
|
222
|
+
},
|
|
223
|
+
async projected() {
|
|
224
|
+
// A run goes a few steps: as many runs as the ranges above the cursor take, and no more than a hundred.
|
|
225
|
+
for (let run = 0; run < 100; run += 1) {
|
|
226
|
+
await projector.settled();
|
|
227
|
+
projector.kick();
|
|
228
|
+
await projector.settled();
|
|
229
|
+
const [cursor, newest] = await Promise.all([store.readsCursor(), store.newest()]);
|
|
230
|
+
if (newest === null || cursor.through >= newest.upto) return;
|
|
231
|
+
}
|
|
232
|
+
throw new Error("the reads' projection did not catch up with the newest pair");
|
|
233
|
+
},
|
|
234
|
+
sealing: () => sealingOf({ open: !stalled && !stopped, failed: failed ?? checkpointer.standing, last }),
|
|
235
|
+
async alarms(since = 0) {
|
|
236
|
+
await appending;
|
|
237
|
+
return (await readAlarms(alarmsPath)).filter((a) => a.at >= since);
|
|
238
|
+
},
|
|
239
|
+
async stop() {
|
|
240
|
+
stopped = true;
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
// A stalled one's session cut first, so that what its tick awaits fails now (T169).
|
|
243
|
+
if (stalled) await session?.cut().catch(() => {});
|
|
244
|
+
await ticking;
|
|
245
|
+
// The reads' projection runs no more. One that stalled leaves the run going, unawaited: it keeps none of the
|
|
246
|
+
// signer's memory, and writes nothing once another projector has overtaken it (FromChecked), so a projection
|
|
247
|
+
// hung in the database never holds back the runner that replaces this one (review r8). Otherwise its run ends
|
|
248
|
+
// first, within its bound.
|
|
249
|
+
const projecting = projector.stop();
|
|
250
|
+
if (!stalled) await within(projecting, options.tickBoundMs ?? OPEN_BOUND_MS, "the reads' projection").catch(() => {});
|
|
251
|
+
// Closed, so that a handle kept after stop() never ticks beside a runner started over the same memory.
|
|
252
|
+
await checkpointer.close();
|
|
253
|
+
await session?.cut().catch(() => {});
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The alarms in the file, a line each; a line a crash left torn is skipped. */
|
|
259
|
+
async function readAlarms(path: string): Promise<Alarm[]> {
|
|
260
|
+
const text = await readFile(path, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
|
261
|
+
if (error.code === 'ENOENT') return '';
|
|
262
|
+
throw error;
|
|
263
|
+
});
|
|
264
|
+
return text.split('\n').flatMap((line) => {
|
|
265
|
+
try {
|
|
266
|
+
return line.length > 0 ? [JSON.parse(line) as Alarm] : [];
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** How long after a refused open the vault's checkpointer tries again (`keepCheckpointer`). */
|
|
274
|
+
const OPEN_AGAIN_MS = 10_000;
|
|
275
|
+
|
|
276
|
+
export type KeptCheckpointer = Pick<RunningCheckpointer, 'sealThrough' | 'heldOutside' | 'alarms' | 'sealing' | 'stop'>;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The checkpointer as the vault's process keeps it: `runCheckpointer`, tried
|
|
280
|
+
* again every `retryMs` while it cannot open (a log it refuses, as after a
|
|
281
|
+
* restore, docs/restore.md; the database out of reach), each try making the
|
|
282
|
+
* next days of uses first, so that uses go on; its failure an alarm, kept
|
|
283
|
+
* once while it stands. Meanwhile a change's seal fails with why, as on
|
|
284
|
+
* Workers, and the pair held outside is read from the file as ever. One
|
|
285
|
+
* that stalls (T169) is stopped, fenced, and another opened a tick later.
|
|
286
|
+
*/
|
|
287
|
+
export async function keepCheckpointer(options: Parameters<typeof runCheckpointer>[0] & { retryMs?: number }): Promise<KeptCheckpointer> {
|
|
288
|
+
let running: RunningCheckpointer | null = null;
|
|
289
|
+
let failed: string | null = null;
|
|
290
|
+
let stopped = false;
|
|
291
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
292
|
+
const alarmsPath = `${options.memory}.alarms`;
|
|
293
|
+
// The runners that stalled, stopping: their memory writes in flight land before `stop` returns, so that nothing opened
|
|
294
|
+
// after it over the same file has a newer pair overwritten (review r3).
|
|
295
|
+
let retiring: Promise<void> = Promise.resolve();
|
|
296
|
+
const tryOpen = async (): Promise<void> => {
|
|
297
|
+
try {
|
|
298
|
+
const opened: RunningCheckpointer = await runCheckpointer({
|
|
299
|
+
...options,
|
|
300
|
+
// Its stop waits for the memory writes it sent (its fence); then another opens, as after a refused open.
|
|
301
|
+
stalled: (why) => {
|
|
302
|
+
if (running === opened) running = null;
|
|
303
|
+
failed = why;
|
|
304
|
+
const retired = opened.stop().catch(() => {});
|
|
305
|
+
retiring = Promise.all([retiring, retired]).then(() => {});
|
|
306
|
+
void retired.finally(() => {
|
|
307
|
+
if (!stopped) timer = setTimeout(() => void (trying = tryOpen()), TICK_MS);
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
running = opened;
|
|
312
|
+
failed = null;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const message = `the checkpointer cannot open: ${reasonOf(error)}`;
|
|
315
|
+
if (message !== failed) {
|
|
316
|
+
failed = message;
|
|
317
|
+
console.warn(`coffre audit: ${message}`);
|
|
318
|
+
await appendFile(alarmsPath, `${JSON.stringify({ at: Date.now(), message } satisfies Alarm)}\n`).catch((e: Error) => console.warn(`coffre audit: an alarm could not be kept: ${e.message}`));
|
|
319
|
+
}
|
|
320
|
+
if (!stopped) timer = setTimeout(() => void (trying = tryOpen()), options.retryMs ?? OPEN_AGAIN_MS);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
let trying = tryOpen();
|
|
324
|
+
await trying;
|
|
325
|
+
return {
|
|
326
|
+
sealThrough: (number, deadline) => (running === null ? Promise.reject(new Unsealed(number, failed ?? 'the checkpointer is not open')) : running.sealThrough(number, deadline)),
|
|
327
|
+
sealing: () => (running === null ? { ok: false, reason: failed ?? 'the checkpointer is not open' } : running.sealing()),
|
|
328
|
+
async heldOutside() {
|
|
329
|
+
return heldIn(await fileMemory(options.memory).last());
|
|
330
|
+
},
|
|
331
|
+
async alarms(since = 0) {
|
|
332
|
+
return running === null ? (await readAlarms(alarmsPath)).filter((a) => a.at >= since) : running.alarms(since);
|
|
333
|
+
},
|
|
334
|
+
async stop() {
|
|
335
|
+
stopped = true;
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
await trying;
|
|
338
|
+
await running?.stop();
|
|
339
|
+
await retiring;
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|