@coda-console/kernel 0.0.0 → 0.2.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/sim/world.mjs ADDED
@@ -0,0 +1,922 @@
1
+ import assert from 'node:assert/strict';
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ import { createKernel } from '../kernel.mjs';
4
+
5
+ // A deterministic simulation: several kernel instances in one process, with the network, the clock, `schedule` and
6
+ // crashes in the test's hands. Nothing here waits on real time; `flush()` only lets pending promise callbacks run.
7
+ //
8
+ // The world knows no host. Each instance runs whatever handlers its `setup` registers; the world wraps them to record,
9
+ // in its own `sim_` tables and in the handler's own transaction, what committed. Its checks read those tables and the
10
+ // kernel's, so they hold for any host; a host adds its own with onStep() and onQuiet(). A host with a layer of its own
11
+ // around the kernel puts it between the world and the kernel with node.ports(), and the world checks that layer too.
12
+
13
+ /** The instance died right after a commit: the commit stands, everything in memory is gone. */
14
+ export class Crash extends Error {
15
+ name = 'Crash';
16
+ }
17
+
18
+ /** The COMMIT itself failed: the transaction is rolled back. */
19
+ export class CommitFailed extends Error {
20
+ name = 'CommitFailed';
21
+ }
22
+
23
+ const lost = error => error instanceof Crash || error instanceof CommitFailed;
24
+
25
+ export const flush = () => new Promise(resolve => setImmediate(resolve));
26
+
27
+ /** @param {number} seed */
28
+ export function random(seed) {
29
+ let state = seed >>> 0;
30
+ return () => {
31
+ state = (state + 0x6d2b79f5) >>> 0;
32
+ let t = state;
33
+ t = Math.imul(t ^ (t >>> 15), t | 1);
34
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
35
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
36
+ };
37
+ }
38
+
39
+ export const NO_FAULTS = Object.freeze({ dropRequest: 0, dropReceipt: 0, duplicate: 0, commitFail: 0 });
40
+
41
+ /** Every statement a kernel prepared, across all worlds of this process. */
42
+ export const prepared = new Set();
43
+
44
+ const ENDED = new Set(['completed', 'failed', 'canceled']);
45
+ const UNENDED = "('sending', 'open')";
46
+
47
+ // What the world records of the handlers it wraps, each row in the transaction that called the handler.
48
+ const SIM_TABLES = `
49
+ CREATE TABLE IF NOT EXISTS sim_accepted (from_addr TEXT NOT NULL, id TEXT NOT NULL);
50
+ CREATE TABLE IF NOT EXISTS sim_results (from_addr TEXT NOT NULL, id TEXT NOT NULL);
51
+ CREATE TABLE IF NOT EXISTS sim_lates (from_addr TEXT NOT NULL, id TEXT NOT NULL);
52
+ CREATE TABLE IF NOT EXISTS sim_cancels (from_addr TEXT NOT NULL, id TEXT NOT NULL);
53
+ CREATE TABLE IF NOT EXISTS sim_statuses (from_addr TEXT NOT NULL, id TEXT NOT NULL, rev INTEGER NOT NULL);
54
+ CREATE TABLE IF NOT EXISTS sim_answers (run_from TEXT NOT NULL, run_id TEXT NOT NULL, asked INTEGER NOT NULL, from_addr TEXT NOT NULL,
55
+ id TEXT NOT NULL);`;
56
+
57
+ const key = (from, id) => `${from}/${id}`;
58
+ const ending = (row, column) => JSON.stringify([row[column], row.result, row.error, row.progress]);
59
+ // An abandoned call's late outcome, in the form of ending().
60
+ const lateEnding = row => {
61
+ const late = JSON.parse(row.late);
62
+ return JSON.stringify([late.outcome, ...[late.result, late.error, late.progress].map(value => value === undefined ? null : JSON.stringify(value))]);
63
+ };
64
+ const statusOf = row => JSON.stringify([row.status, row.rev, row.asked, row.asking, row.progress]);
65
+
66
+ // The connection one generation of an instance gets, over the database it runs on: it counts commits and fails them
67
+ // on purpose, and once that generation dies every call through it, or through a statement it prepared, throws Crash.
68
+ // Everything else passes through. The kernel's copy also records what it prepares.
69
+ function connection(node, generation, record) {
70
+ const alive = () => {
71
+ if (generation.dead) throw new Crash(`${node.name} is down`);
72
+ };
73
+ const through = (object, name) => {
74
+ const value = Reflect.get(object, name, object);
75
+ return typeof value === 'function' ? (...args) => (alive(), value.apply(object, args)) : value;
76
+ };
77
+ return new Proxy(node.raw, {
78
+ get(raw, name) {
79
+ if (name === 'exec') return sql => {
80
+ alive();
81
+ if (sql === 'BEGIN IMMEDIATE') {
82
+ node.called = new Set();
83
+ node.intents = new Set();
84
+ }
85
+ if (sql === 'COMMIT') node.commit();
86
+ else raw.exec(sql);
87
+ };
88
+ if (name === 'prepare') return sql => {
89
+ alive();
90
+ if (record) prepared.add(sql);
91
+ return new Proxy(raw.prepare(sql), { get: through });
92
+ };
93
+ return through(raw, name);
94
+ },
95
+ });
96
+ }
97
+
98
+ /**
99
+ * A generation's host puts a letter its kernel handed over into the network, for `to`, an address of some instance of
100
+ * the world. P-out counts by the letter's text, over the whole generation: a letter may be posted no more often than
101
+ * this generation's kernel has handed it over so far. A handing over the host did not post stays to its credit, so a
102
+ * letter handed over twice and posted once may be posted again later. Once that generation has died it throws Crash,
103
+ * whatever generation has come since.
104
+ * @param {SimNode} node
105
+ * @param {object} generation
106
+ * @param {string} to
107
+ * @param {object} envelope
108
+ * @returns {Promise<object>} the receipt; rejected when the letter or its receipt is lost
109
+ */
110
+ function post(node, generation, to, envelope) {
111
+ if (generation.dead) throw new Crash(`${node.name} is down`);
112
+ const { handed } = generation;
113
+ const text = JSON.stringify(envelope);
114
+ const owed = handed.get(text) ?? 0;
115
+ if (owed === 0) node.world.violations.push(`P-out: ${node.name} posted a letter more often than its kernel handed it over: ${text}`);
116
+ else if (owed === 1) handed.delete(text);
117
+ else handed.set(text, owed - 1);
118
+ node.posted += 1;
119
+ return new Promise((resolve, reject) => node.world.net.push({ request: true, from: node, to, envelope: text, resolve, reject }));
120
+ }
121
+
122
+ /**
123
+ * What a host gives an instance: called on every generation inside its first transaction, with migrate(), so what it
124
+ * writes commits with it. It creates the host's own tables and registers its handlers with `kernel.handle` and
125
+ * `kernel.results`. Being inside a transaction, it cannot call what runs only outside one (recover(), tick(), pump(),
126
+ * inquire(), answer(), when()); it hands that to `node.recovered(fn)`. A host with a layer of its own around the kernel
127
+ * puts it between the world and the kernel with `node.ports()`.
128
+ * @typedef {(context: { kernel: object, node: SimNode, world: World }) => void} Setup
129
+ */
130
+
131
+ /**
132
+ * Where a host stands between the world and its kernel, for one generation; each one left out is the world's own way.
133
+ * - receive: the world hands the host a letter for this instance; `from` is the instance that put it into the network,
134
+ * which the world vouches for (P3). Returns the receipt.
135
+ * - send: this generation's kernel hands over a letter; the host puts it into the network with the `post` that
136
+ * `node.ports()` returned, or not.
137
+ * - recover: the world recovers this generation; the host calls `kernel.recover()`.
138
+ * @typedef {{
139
+ * receive?: (envelope: object, from: SimNode) => object,
140
+ * send?: (to: string, envelope: object) => Promise<object>,
141
+ * recover?: () => void,
142
+ * }} Ports
143
+ */
144
+
145
+ /** One instance: a database that outlives crashes, and the kernel generation running on it. */
146
+ export class SimNode {
147
+ up = false;
148
+ /** recover() has run on this generation: until then every letter to it gets not_ready */
149
+ ready = false;
150
+ /** @type {Array<unknown>} errors given to onError, other than crashes and failed commits */
151
+ errors = [];
152
+ seen = { run: 0, seq: 0 };
153
+ /** @type {Map<string, object>} the timers in its table at its last commit, by key */
154
+ timers = new Map();
155
+ /** the earliest due time this generation's kernel last gave to `wake`, null until it gives one (7) */
156
+ woke = null;
157
+ /** @type {Map<string, string[]>} call keys per lane, in outbox order */
158
+ lanes = new Map();
159
+ /** the checks' own statements on `raw` */
160
+ statements = new Map();
161
+ /** @type {Set<string>} addresses whose handlers the open transaction has called (S9) */
162
+ called = new Set();
163
+ /**
164
+ * @type {Set<string>} what the host asked of its kernel in the open transaction, joined to the world's at its commit.
165
+ * One asked in a savepoint that rolled back, inside a transaction that committed, still counts.
166
+ */
167
+ intents = new Set();
168
+ /** whatever the host's setup leaves here for its tests to reach; the world does not look at it */
169
+ host = undefined;
170
+ /** @type {(() => void) | undefined} what this generation's host does once its recover() has succeeded */
171
+ afterRecover = undefined;
172
+ /**
173
+ * calls of receive() on the host's handle while the world was handing this instance a letter, over all generations:
174
+ * the world's own call on its way, and every one of the host's, whatever the letter and at whatever depth. The kernel
175
+ * delivering to itself, as answer() does to a local address, does not go through the handle and does not count.
176
+ */
177
+ taken = 0;
178
+ /** letters put into the network with a generation's post, over all generations */
179
+ posted = 0;
180
+ /** the world is handing this instance a letter */
181
+ delivering = false;
182
+
183
+ /**
184
+ * @param {World} world
185
+ * @param {string} name
186
+ * @param {string[]} addresses
187
+ * @param {Setup} setup
188
+ * @param {{ open?: () => import('node:sqlite').DatabaseSync }} options open: the host's own database, opened once
189
+ */
190
+ constructor(world, name, addresses, setup, { open } = {}) {
191
+ this.world = world;
192
+ this.name = name;
193
+ this.addresses = addresses;
194
+ this.setup = setup;
195
+ /** the database itself, which outlives every generation; for checks only */
196
+ this.raw = open ? open() : new DatabaseSync(':memory:');
197
+ }
198
+
199
+ /**
200
+ * A new generation on the same database: migrate, let the host set up and, unless told otherwise, recover().
201
+ * `db` is this generation's connection, the one the host uses.
202
+ * @param {{ recover?: boolean }} [options]
203
+ */
204
+ boot({ recover = true } = {}) {
205
+ // A process that dies takes its connection's temporary tables, views and triggers with it; this connection outlives
206
+ // every generation, so the world drops them itself. The first generation too: nothing a host's open() made in temp
207
+ // is left for it. Triggers go first, as dropping a table drops its own. What SQLite keeps itself, such as the
208
+ // sqlite_sequence of an AUTOINCREMENT table, cannot be dropped and is no host's. Nor are a virtual table's shadow
209
+ // tables, which go with it: SQLite itself tells them apart, asking the table's module, in table_list. SQL functions
210
+ // cannot be taken back: a host registers its own again on every generation.
211
+ for (const { type, name } of this.q(`SELECT type, name FROM sqlite_temp_master AS m
212
+ WHERE type IN ('trigger', 'view', 'table') AND name NOT GLOB 'sqlite_*'
213
+ AND NOT EXISTS (SELECT 1 FROM pragma_table_list AS l WHERE l.schema = 'temp' AND l.name = m.name AND l.type = 'shadow')
214
+ ORDER BY CASE type WHEN 'trigger' THEN 0 WHEN 'view' THEN 1 ELSE 2 END`).all())
215
+ this.raw.exec(`DROP ${type.toUpperCase()} temp."${name.replaceAll('"', '""')}"`);
216
+ // handed: per letter text, how often this generation's kernel handed it over and its host has not posted it yet.
217
+ const generation = { dead: false, /** @type {Ports} */ ports: {}, handed: new Map(), recovers: 0 };
218
+ // Bound to this generation, so that a host of a generation that died cannot post through the next one.
219
+ generation.post = (to, envelope) => post(this, generation, to, envelope);
220
+ this.generation = generation;
221
+ this.db = connection(this, this.generation, false);
222
+ this.woke = null;
223
+ const kernel = createKernel({
224
+ db: connection(this, this.generation, true),
225
+ now: () => this.world.time,
226
+ send: (to, envelope) => this.world.send(this, to, envelope),
227
+ onError: error => this.report(error),
228
+ wake: at => {
229
+ this.woke = at;
230
+ },
231
+ schedule: fn => this.world.tasks.push(fn),
232
+ });
233
+ // What the host asks for, once it commits: a run asked to cancel, and a timer gone from its table without coming
234
+ // due, must be traceable to one of these (C7, 7).
235
+ for (const name of ['cancel', 'abandon', 'stop', 'unschedule']) {
236
+ const fn = kernel[name].bind(kernel);
237
+ kernel[name] = (ref, ...rest) => {
238
+ this.intents.add(`${name} ${key(ref.from, ref.id)}`);
239
+ return fn(ref, ...rest);
240
+ };
241
+ }
242
+ // Counted for P-rec.
243
+ const recovering = kernel.recover.bind(kernel);
244
+ kernel.recover = () => {
245
+ generation.recovers += 1;
246
+ return recovering();
247
+ };
248
+ this.wrap(kernel);
249
+ // The handle the host and the world hold, counted for P-in by where a call of receive() comes from, not by the letter
250
+ // it carries: one through the handle during a delivery is taken. Every other function is the kernel's own, bound to
251
+ // it, so the kernel calling its own receive() never comes back through the handle.
252
+ const receive = envelope => {
253
+ if (this.delivering) this.taken += 1;
254
+ return kernel.receive(envelope);
255
+ };
256
+ this.kernel = new Proxy(kernel, {
257
+ get(target, name) {
258
+ if (name === 'receive') return receive;
259
+ const value = Reflect.get(target, name);
260
+ return typeof value === 'function' ? value.bind(target) : value;
261
+ },
262
+ });
263
+ this.afterRecover = undefined;
264
+ this.up = true;
265
+ this.ready = false;
266
+ try {
267
+ this.kernel.transaction(() => {
268
+ this.kernel.migrate();
269
+ this.raw.exec(SIM_TABLES);
270
+ this.setup({ kernel: this.kernel, node: this, world: this.world });
271
+ });
272
+ } catch (error) {
273
+ if (!lost(error)) throw error;
274
+ if (this.up) this.down();
275
+ }
276
+ if (recover && this.up) this.recover();
277
+ }
278
+
279
+ /**
280
+ * Every handler the host registers is wrapped: once called, it marks its address as called in this transaction (S9)
281
+ * and records a row in the world's table, in the handler's own savepoint, so that the row commits exactly when what
282
+ * the handler did commits. The checks count those rows. An address the host registers anything for in this generation
283
+ * hears its statuses, results and late outcomes through the world, which records each whether or not the host gave a
284
+ * function for it. A cancel or answer function is wrapped only when given: without one, the kernel answers otherwise.
285
+ * The host's functions are called as methods of the object it registered (8).
286
+ */
287
+ wrap(kernel) {
288
+ const record = (address, sql, ...args) => {
289
+ this.touch(address);
290
+ this.q(sql).run(...args);
291
+ };
292
+ /** @type {Map<string, object | undefined>} per address registered, the result handlers the host gave */
293
+ const heard = new Map();
294
+ const results = kernel.results.bind(kernel);
295
+ const listen = address => {
296
+ if (heard.has(address)) return;
297
+ heard.set(address, undefined);
298
+ results(address, {
299
+ status: call => (record(address, 'INSERT INTO sim_statuses (from_addr, id, rev) VALUES (?, ?, ?)', call.from, call.id, call.rev),
300
+ heard.get(address)?.status?.(call)),
301
+ result: call => (record(address, 'INSERT INTO sim_results (from_addr, id) VALUES (?, ?)', call.from, call.id), heard.get(address)?.result?.(call)),
302
+ late: call => (record(address, 'INSERT INTO sim_lates (from_addr, id) VALUES (?, ?)', call.from, call.id), heard.get(address)?.late?.(call)),
303
+ });
304
+ };
305
+ kernel.results = (address, handlers) => {
306
+ listen(address);
307
+ heard.set(address, handlers);
308
+ };
309
+ const handle = kernel.handle.bind(kernel);
310
+ kernel.handle = (address, op, handlers) => {
311
+ listen(address);
312
+ handle(address, op, {
313
+ accept: handlers.accept && (call => (record(address, 'INSERT INTO sim_accepted (from_addr, id) VALUES (?, ?)', call.from, call.id), handlers.accept(call))),
314
+ cancel: handlers.cancel && ((run, reason) => (record(address, 'INSERT INTO sim_cancels (from_addr, id) VALUES (?, ?)', run.from, run.id),
315
+ handlers.cancel(run, reason))),
316
+ answer: handlers.answer && ((run, given) => (record(address, 'INSERT INTO sim_answers (run_from, run_id, asked, from_addr, id) VALUES (?, ?, ?, ?, ?)',
317
+ run.from, run.id, given.asked, given.from, given.id), handlers.answer(run, given))),
318
+ });
319
+ };
320
+ }
321
+
322
+ recover() {
323
+ const { generation } = this;
324
+ try {
325
+ if (generation.ports.recover) {
326
+ this.world.count('ports: a recovery through the host');
327
+ generation.ports.recover();
328
+ if (generation.recovers !== 1)
329
+ this.world.violations.push(`P-rec: the host of ${this.name} recovered, its kernel's recover() called ${generation.recovers} times, not once`);
330
+ } else this.kernel.recover();
331
+ this.ready = true;
332
+ this.afterRecover?.();
333
+ } catch (error) {
334
+ if (!lost(error)) throw error;
335
+ if (this.up) this.down();
336
+ }
337
+ }
338
+
339
+ down() {
340
+ this.up = false;
341
+ this.ready = false;
342
+ this.generation.dead = true;
343
+ this.kernel.close();
344
+ }
345
+
346
+ /**
347
+ * The host's part of starting up (10): `fn` runs once this generation's recover() has succeeded, at once and outside
348
+ * any transaction, where the host scans its runs and calls again (T4). A crash or a failed commit in it is handled as
349
+ * one in recover(). Called from the setup, once per generation.
350
+ * @param {() => void} fn
351
+ */
352
+ recovered(fn) {
353
+ this.afterRecover = fn;
354
+ }
355
+
356
+ /**
357
+ * Puts the host between the world and this generation's kernel (see Ports). Called from the setup, once per
358
+ * generation; the next generation starts without them.
359
+ * @param {Ports} ports
360
+ * @returns {(to: string, envelope: object) => Promise<object>} this generation's post (see post())
361
+ */
362
+ ports(ports) {
363
+ this.generation.ports = ports;
364
+ return this.generation.post;
365
+ }
366
+
367
+ /**
368
+ * The host's way to do something later: `fn` runs when the world runs its tasks, unless this generation has died by
369
+ * then. A crash or a failed commit in it is the world's business, as in act().
370
+ * @param {() => void} fn
371
+ */
372
+ schedule(fn) {
373
+ const generation = this.generation;
374
+ this.world.tasks.push(() => {
375
+ if (generation.dead) return;
376
+ try {
377
+ fn();
378
+ } catch (error) {
379
+ if (!lost(error)) throw error;
380
+ }
381
+ });
382
+ }
383
+
384
+ commit() {
385
+ const world = this.world;
386
+ if (world.chance(world.faults.commitFail)) throw new CommitFailed(`a commit of ${this.name} failed`);
387
+ this.raw.exec('COMMIT');
388
+ world.commits += 1;
389
+ for (const intent of this.intents) world.intents.add(intent);
390
+ world.observe(this);
391
+ if (world.commits === world.crashAt) {
392
+ world.crashed += 1;
393
+ this.down();
394
+ throw new Crash(`${this.name} crashed after commit ${world.commits}`);
395
+ }
396
+ }
397
+
398
+ report(error) {
399
+ if (!lost(error)) this.errors.push(error);
400
+ }
401
+
402
+ // The kernel called a handler of `address`; one transaction may call those of one address only (S9).
403
+ touch(address) {
404
+ this.called.add(address);
405
+ if (this.called.size > 1) this.world.violations.push(`S9: one transaction of ${this.name} called handlers of ${[...this.called].join(', ')}`);
406
+ }
407
+
408
+ migrated() {
409
+ return this.q("SELECT 1 FROM sqlite_master WHERE name = 'kernel_runs'").get() !== undefined;
410
+ }
411
+
412
+ // The checks' own statements, kept per database; they bypass the generation's connection.
413
+ q(sql) {
414
+ let statement = this.statements.get(sql);
415
+ if (!statement) this.statements.set(sql, statement = this.raw.prepare(sql));
416
+ return statement;
417
+ }
418
+
419
+ count(sql, ...args) {
420
+ return this.q(sql).get(...args).n;
421
+ }
422
+ }
423
+
424
+ export class World {
425
+ /** @type {SimNode[]} */
426
+ nodes = [];
427
+ /** @type {Map<string, SimNode>} */
428
+ owners = new Map();
429
+ /** Pending network events: requests not delivered yet, and receipts not handed back yet. */
430
+ net = [];
431
+ /** @type {Array<() => void>} what the kernels gave to `schedule` */
432
+ tasks = [];
433
+ time = 0;
434
+ commits = 0;
435
+ crashAt = Infinity;
436
+ crashed = 0;
437
+ faults = NO_FAULTS;
438
+ /** Hosts are to stop refusing on purpose: no Busy, no throwing handlers. Set when the world is brought to rest. */
439
+ calm = false;
440
+ /** @type {Map<string, number>} call key → the commit that first recorded its run on the callee */
441
+ order = new Map();
442
+ /** @type {Map<string, string>} ended rows as last seen (S1) */
443
+ ended = new Map();
444
+ /** @type {Map<string, number>} the version of each call's status as last seen */
445
+ revs = new Map();
446
+ /** @type {Map<string, { node: SimNode, calls: object[] }>} per run asked to cancel, the calls it awaited at that commit (L3) */
447
+ halted = new Map();
448
+ /** @type {Set<string>} the calls those runs awaited then: their own runs may be asked to cancel (C7) */
449
+ passed = new Set();
450
+ /** @type {Set<string>} what hosts asked of their kernels in transactions that committed: `<function> <ref>` for cancel, abandon,
451
+ * stop and unschedule */
452
+ intents = new Set();
453
+ /** @type {Map<string, number>} how often each of the rarer paths was taken */
454
+ tally = new Map();
455
+ violations = [];
456
+ /** @type {Array<() => void>} the hosts' own checks after every step, run by check() */
457
+ steps = [];
458
+ /** @type {Array<() => void>} the hosts' own checks at rest, run by checkQuiet() */
459
+ quiets = [];
460
+
461
+ /** @param {{ rng?: () => number, restart?: boolean }} [options] restart: a crashed instance comes back at once */
462
+ constructor({ rng = null, restart = false } = {}) {
463
+ this.rng = rng;
464
+ this.restart = restart;
465
+ }
466
+
467
+ chance(p) {
468
+ return p > 0 && this.rng() < p;
469
+ }
470
+
471
+ count(name) {
472
+ this.tally.set(name, (this.tally.get(name) ?? 0) + 1);
473
+ }
474
+
475
+ /**
476
+ * @param {string} name
477
+ * @param {string[]} addresses
478
+ * @param {Setup} setup
479
+ * @param {{ open?: () => import('node:sqlite').DatabaseSync }} [options]
480
+ */
481
+ node(name, addresses, setup, options) {
482
+ const node = new SimNode(this, name, addresses, setup, options);
483
+ this.nodes.push(node);
484
+ for (const address of addresses) this.owners.set(address, node);
485
+ return node;
486
+ }
487
+
488
+ start() {
489
+ for (const node of this.nodes) node.boot();
490
+ }
491
+
492
+ /** A host's own check, run after the world's by every check(). @param {() => void} fn */
493
+ onStep(fn) {
494
+ this.steps.push(fn);
495
+ }
496
+
497
+ /** A host's own check at rest, run after the world's by checkQuiet(). @param {() => void} fn */
498
+ onQuiet(fn) {
499
+ this.quiets.push(fn);
500
+ }
501
+
502
+ // After every commit: which runs it recorded, and which lane letters it owed, for S5; which runs it asked to cancel,
503
+ // with the calls they awaited then, for L3; and where each timer that left its table went (7).
504
+ observe(node) {
505
+ for (const row of node.q('SELECT rowid, from_addr, id FROM kernel_runs WHERE rowid > ?').all(node.seen.run)) {
506
+ node.seen.run = row.rowid;
507
+ this.order.set(key(row.from_addr, row.id), this.commits);
508
+ }
509
+ for (const row of node.q('SELECT seq, lane, kind, call_from, call_id FROM kernel_outbox WHERE seq > ?').all(node.seen.seq)) {
510
+ node.seen.seq = row.seq;
511
+ if (row.kind !== 'call' || row.lane === null) continue;
512
+ if (!node.lanes.has(row.lane)) node.lanes.set(row.lane, []);
513
+ node.lanes.get(row.lane).push(key(row.call_from, row.call_id));
514
+ }
515
+ const newly = new Set();
516
+ for (const run of node.q('SELECT from_addr, id FROM kernel_runs WHERE cancel_asked = 1').all()) {
517
+ const name = `${node.name} ${key(run.from_addr, run.id)}`;
518
+ if (this.halted.has(name)) continue;
519
+ const calls = node.q(`SELECT from_addr, id, to_addr FROM kernel_calls
520
+ WHERE by_from = ? AND by_id = ? AND awaited = 1 AND state IN ${UNENDED}`).all(run.from_addr, run.id);
521
+ this.halted.set(name, { node, calls });
522
+ for (const call of calls) this.passed.add(key(call.from_addr, call.id));
523
+ newly.add(key(run.from_addr, run.id));
524
+ }
525
+ // A timer leaves its table as a call of its id, once due; taken back by its host; or with its run asked to cancel
526
+ // in this very commit, which leaves none of the run's older timers behind.
527
+ const timers = new Map(node.q('SELECT from_addr, id, due, by_from, by_id FROM kernel_timers').all().map(row => [key(row.from_addr, row.id), row]));
528
+ for (const [name, timer] of node.timers) {
529
+ const by = timer.by_from === null ? undefined : key(timer.by_from, timer.by_id);
530
+ if (timers.has(name)) {
531
+ if (newly.has(by)) this.violations.push(`7: timer ${name} of ${node.name} outlived its run ${by} being asked to cancel`);
532
+ continue;
533
+ }
534
+ const taken = ['unschedule', 'cancel', 'abandon'].filter(how => this.intents.delete(`${how} ${name}`));
535
+ if (node.q('SELECT 1 FROM kernel_calls WHERE from_addr = ? AND id = ?').get(timer.from_addr, timer.id) && this.time >= timer.due) this.count('timer: came due and became a call');
536
+ else if (taken.length > 0) this.count(`timer: taken back by ${taken.join(', ')}`);
537
+ else if (newly.has(by)) this.count('timer: dropped with its run asked to cancel');
538
+ else this.violations.push(`7: timer ${name} of ${node.name} left its table at commit ${this.commits}: not due and called, not taken back, not dropped with its run`);
539
+ }
540
+ node.timers = timers;
541
+ }
542
+
543
+ /**
544
+ * The `send` every instance gets. It checks S6 at the moment of handing over; an answer is no letter of the outbox.
545
+ * The letter then goes to the host's send, or straight into the network.
546
+ */
547
+ send(node, to, envelope) {
548
+ const call = envelope.kind === 'call' ? envelope : envelope.call;
549
+ const text = JSON.stringify(envelope);
550
+ if (envelope.confirmed) this.count('inquiry sent');
551
+ const owed = envelope.kind === 'answer' || !node.raw.isTransaction && (envelope.confirmed
552
+ ? node.q("SELECT 1 FROM kernel_calls WHERE from_addr = ? AND id = ? AND state = 'open' AND to_addr = ? AND digest = ?")
553
+ .get(call.from, call.id, to, envelope.digest) !== undefined
554
+ : node.q('SELECT envelope FROM kernel_outbox WHERE call_from = ? AND call_id = ?').all(call.from, call.id).some(row => row.envelope === text));
555
+ if (!owed) this.violations.push(`S6: ${node.name} sent a letter that is no committed outbox row nor an inquiry about an open call: ${text}`);
556
+ const { handed, ports } = node.generation;
557
+ handed.set(text, (handed.get(text) ?? 0) + 1);
558
+ if (!ports.send) return node.generation.post(to, envelope);
559
+ this.count('ports: a letter out through the host');
560
+ return ports.send(to, envelope);
561
+ }
562
+
563
+ /**
564
+ * Handles one pending network event. A request is delivered now and its receipt queued as a later event; with
565
+ * faults, the request or the receipt may be lost, and the request may also be queued again as a late duplicate.
566
+ * @param {number} index
567
+ * @param {boolean} [faulty]
568
+ */
569
+ deliver(index, faulty = false) {
570
+ const [event] = this.net.splice(index, 1);
571
+ if (!event.request) {
572
+ if (event.receipt) event.resolve(event.receipt);
573
+ else event.reject(new Error('not delivered'));
574
+ return;
575
+ }
576
+ const faults = faulty ? this.faults : NO_FAULTS;
577
+ if (this.chance(faults.dropRequest)) {
578
+ this.net.push({ resolve: event.resolve, reject: event.reject });
579
+ return;
580
+ }
581
+ if (this.chance(faults.duplicate)) this.net.push({ ...event, resolve() {}, reject() {} });
582
+ const envelope = JSON.parse(event.envelope);
583
+ this.notice('letter', envelope);
584
+ let receipt = this.receive(event.to, envelope, event.from);
585
+ if (envelope.kind === 'answer') this.checkAnswer(envelope, receipt);
586
+ this.notice('receipt', envelope, receipt);
587
+ if (this.chance(faults.dropReceipt)) receipt = undefined;
588
+ this.net.push({ receipt, resolve: event.resolve, reject: event.reject });
589
+ }
590
+
591
+ // Counts the rarer paths a letter or its receipt is about to take, as the two sides' tables show them now.
592
+ notice(moment, envelope, receipt) {
593
+ const call = envelope.kind === 'call' ? envelope : envelope.call;
594
+ const row = (address, table) => {
595
+ const node = this.owners.get(address);
596
+ return node?.migrated() ? node.q(`SELECT * FROM ${table} WHERE from_addr = ? AND id = ?`).get(call.from, call.id) : undefined;
597
+ };
598
+ if (moment === 'letter') {
599
+ if (envelope.kind === 'answer') this.noticeAnswer(envelope);
600
+ if (envelope.kind === 'cancel' && !row(envelope.to, 'kernel_runs')) this.count('cancel before its call');
601
+ if (envelope.kind === 'status' && ENDED.has(row(envelope.to, 'kernel_calls')?.state)) this.count('status after the outcome');
602
+ if (envelope.kind === 'result' && row(envelope.to, 'kernel_calls')?.abandoned === 1) this.count('outcome letter after an abandon');
603
+ return;
604
+ }
605
+ if (!receipt?.ok || (envelope.kind !== 'call' && envelope.kind !== 'cancel')) {
606
+ if (envelope.confirmed && receipt?.code === 'unknown_call') this.count('inquiry: no record');
607
+ return;
608
+ }
609
+ const mine = row(envelope.from, 'kernel_calls');
610
+ if (receipt.view.outcome && mine?.abandoned === 1) this.count('outcome receipt after an abandon');
611
+ if (!envelope.confirmed) return;
612
+ if (receipt.view.outcome) this.count('inquiry: an outcome');
613
+ else if (receipt.view.rev > (mine?.rev ?? 0)) this.count('inquiry: a newer status');
614
+ }
615
+
616
+ // Counts the paths an answer is about to take that its receipt does not tell apart.
617
+ noticeAnswer(envelope) {
618
+ const node = this.owners.get(envelope.to);
619
+ if (!node?.up || !node.migrated()) return;
620
+ if (node.q('SELECT 1 FROM kernel_answers WHERE from_addr = ? AND id = ?').get(envelope.from, envelope.id)) this.count('answer: its number was taken before');
621
+ else if (node.q('SELECT 1 FROM kernel_runs WHERE from_addr = ? AND id = ? AND to_addr = ? AND status = ? AND asked = ?')
622
+ .get(envelope.call.from, envelope.call.id, envelope.to, 'input-required', envelope.asked)) this.count('answer: to its question');
623
+ }
624
+
625
+ /**
626
+ * `node` answers a run (C8). No instance keeps an answer: what comes back is the receipt, or undefined when the
627
+ * answer or its receipt was lost, or the answerer crashed.
628
+ * @param {SimNode} node
629
+ * @param {{ from: string, id: string, to: string, call: { from: string, id: string }, asked: number, answer: { mode: string } }} answer
630
+ * @returns {Promise<object | undefined>}
631
+ */
632
+ answer(node, answer) {
633
+ if (!node.up && this.restart) node.boot();
634
+ if (!node.up) return Promise.resolve(undefined);
635
+ const local = node.addresses.includes(answer.to);
636
+ this.count(local ? 'answer: to a local address' : 'answer: to another instance');
637
+ if (local) this.noticeAnswer({ ...answer, kind: 'answer' });
638
+ return node.kernel.answer(answer).then(receipt => {
639
+ if (local) this.checkAnswer(answer, receipt);
640
+ this.count(`answer receipt: ${receipt.ok ? (receipt.applied ? 'applied' : 'not applied') : receipt.code}`);
641
+ return receipt;
642
+ }, error => {
643
+ if (!lost(error) && error.message !== 'not delivered') throw error;
644
+ return undefined;
645
+ });
646
+ }
647
+
648
+ // A receipt that takes an answer says it was applied exactly when that answer's number has been taken, now or before
649
+ // (C8). The world's table says which numbers were taken: it records each answer function's call.
650
+ checkAnswer(answer, receipt) {
651
+ if (!receipt?.ok) return;
652
+ const taken = this.owners.get(answer.to).q('SELECT 1 FROM sim_answers WHERE from_addr = ? AND id = ?').get(answer.from, answer.id) !== undefined;
653
+ if (receipt.applied !== taken)
654
+ this.violations.push(`C8: an answer ${key(answer.from, answer.id)} got applied: ${receipt.applied}, but its number was ${taken ? '' : 'not '}taken`);
655
+ }
656
+
657
+ /**
658
+ * Hands a letter to the instance that owns `to`, through its host's receive when it gave one. The receipt comes back
659
+ * as it would over a wire; undefined when that instance is down or crashed meanwhile.
660
+ * @param {string} to
661
+ * @param {object} envelope
662
+ * @param {SimNode} [from] the instance that put the letter into the network; called directly, the owner of its `from`
663
+ */
664
+ receive(to, envelope, from = this.owners.get(envelope.from)) {
665
+ const node = this.owners.get(to);
666
+ if (!node?.up) return undefined;
667
+ const { ports } = node.generation;
668
+ const taken = node.taken;
669
+ node.delivering = true;
670
+ try {
671
+ if (!ports.receive) return JSON.parse(JSON.stringify(node.kernel.receive(envelope)));
672
+ this.count('ports: a letter in through the host');
673
+ return JSON.parse(JSON.stringify(ports.receive(envelope, from)));
674
+ } catch (error) {
675
+ if (!lost(error)) throw error;
676
+ return undefined;
677
+ } finally {
678
+ node.delivering = false;
679
+ if (node.taken - taken > 1)
680
+ this.violations.push(`P-in: the host of ${node.name} handed one ${envelope.kind} letter to its kernel ${node.taken - taken} times`);
681
+ }
682
+ }
683
+
684
+ /** The host of `node` runs `fn` in a transaction; a crash or a failed commit is the world's business, not an error. */
685
+ act(node, fn) {
686
+ if (!node.up && this.restart) node.boot();
687
+ if (!node.up) return undefined;
688
+ try {
689
+ return node.kernel.transaction(() => fn(node.kernel));
690
+ } catch (error) {
691
+ if (!lost(error)) throw error;
692
+ return undefined;
693
+ }
694
+ }
695
+
696
+ pump(node) {
697
+ if (!node.up) return;
698
+ try {
699
+ node.kernel.pump();
700
+ } catch (error) {
701
+ if (!lost(error)) throw error;
702
+ }
703
+ }
704
+
705
+ /** The host of `node` ticks, as its timer would make it (7). */
706
+ tick(node) {
707
+ if (!node.up && this.restart) node.boot();
708
+ if (!node.up) return;
709
+ try {
710
+ node.kernel.tick();
711
+ } catch (error) {
712
+ if (!lost(error)) throw error;
713
+ }
714
+ }
715
+
716
+ /** @param {SimNode} node @param {{ from: string, id: string }} call */
717
+ inquire(node, call) {
718
+ if (!node.up) return;
719
+ try {
720
+ node.kernel.inquire(call);
721
+ } catch (error) {
722
+ if (!lost(error)) throw error;
723
+ }
724
+ }
725
+
726
+ /** Runs everything pending, without faults, until nothing is left to do. */
727
+ async drain() {
728
+ for (;;) {
729
+ await flush();
730
+ const down = this.restart ? this.nodes.filter(node => !node.up) : [];
731
+ for (const node of down) node.boot();
732
+ if (down.length > 0) continue;
733
+ if (this.tasks.length > 0) this.tasks.shift()();
734
+ else if (this.net.length > 0) this.deliver(0);
735
+ else return;
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Brings the world to rest as section 12 defines it: no more faults, everyone up and recovered, everyone pumped,
741
+ * idle() done, and the network and `schedule` queues empty. It first rests with what the hosts hold still working and
742
+ * checks their statuses there; then it rests again, calling `advance` every round, until a round finds nothing to
743
+ * advance. Otherwise it looks at none of the kernel's tables; what they hold at rest is for checkQuiet() to judge.
744
+ * @param {() => boolean} [advance] the hosts move on what they hold by a step; true while there was something to move
745
+ * @param {{ resend?: () => void }} [options] resend: how the hosts have every instance send what it keeps again, once a
746
+ * round; without it the world pumps every instance itself, which would make up for a host that never does
747
+ */
748
+ async quiesce(advance = () => false, { resend } = {}) {
749
+ this.calm = true;
750
+ // Letters already in flight may still come back refused by faults chosen earlier; the pumps that count come after.
751
+ await this.drain();
752
+ await this.rest(() => false, resend);
753
+ this.checkStatus();
754
+ this.checkHalted();
755
+ await this.rest(advance, resend);
756
+ }
757
+
758
+ /** @param {() => boolean} advance @param {(() => void) | undefined} resend */
759
+ async rest(advance, resend) {
760
+ for (let round = 0; round < 50; round++) {
761
+ const crashed = this.crashed;
762
+ for (const node of this.nodes) {
763
+ if (!node.up) node.boot();
764
+ else if (!node.ready) node.recover();
765
+ }
766
+ const advanced = advance();
767
+ if (resend) resend();
768
+ else for (const node of this.nodes) this.pump(node);
769
+ // The clock passes every timer, and every instance ticks.
770
+ this.time = Math.max(this.time, ...this.nodes.filter(node => node.migrated()).map(node => node.q('SELECT max(due) AS due FROM kernel_timers').get().due ?? 0));
771
+ for (const node of this.nodes) this.tick(node);
772
+ await this.drain();
773
+ await Promise.all(this.nodes.filter(node => node.up).map(node => node.kernel.idle()));
774
+ if (this.crashed === crashed && !advanced && this.net.length === 0 && this.tasks.length === 0
775
+ && this.nodes.every(node => node.ready && node.count('SELECT count(*) AS n FROM kernel_timers') === 0)) return;
776
+ }
777
+ throw new Error(`the world did not come to rest: ${this.nodes.map(node => `${node.name} has ${node.count('SELECT count(*) AS n FROM kernel_outbox')} `
778
+ + `letters in its outbox and ${node.count(`SELECT count(*) AS n FROM kernel_calls WHERE state IN ${UNENDED}`)} calls not ended`).join(', ')}`);
779
+ }
780
+
781
+ /** At rest, before the held runs finish: a call that has not ended knows the status its run has now (C6). */
782
+ checkStatus() {
783
+ for (const node of this.nodes)
784
+ for (const call of node.q(`SELECT * FROM kernel_calls WHERE state IN ${UNENDED}`).all()) {
785
+ const name = key(call.from_addr, call.id);
786
+ const run = this.owners.get(call.to_addr).q('SELECT * FROM kernel_runs WHERE from_addr = ? AND id = ?').get(call.from_addr, call.id);
787
+ assert.ok(run, `call ${name} is ${call.state} at rest but its callee has no run`);
788
+ assert.equal(statusOf(call), statusOf(run), `the caller of ${name} does not have the status its run has at rest`);
789
+ }
790
+ }
791
+
792
+ /**
793
+ * L3, at rest: each call that a run awaited when it was asked to cancel has ended, or its callee has its run marked as
794
+ * asked to cancel.
795
+ */
796
+ checkHalted() {
797
+ for (const [name, { node, calls }] of this.halted)
798
+ for (const call of calls) {
799
+ if (ENDED.has(node.q('SELECT state FROM kernel_calls WHERE from_addr = ? AND id = ?').get(call.from_addr, call.id).state)) continue;
800
+ const run = this.owners.get(call.to_addr).q('SELECT cancel_asked FROM kernel_runs WHERE from_addr = ? AND id = ?').get(call.from_addr, call.id);
801
+ assert.equal(run?.cancel_asked, 1, `L3: run ${name} was asked to cancel, but the callee of ${key(call.from_addr, call.id)}, which it awaited, was not`);
802
+ }
803
+ }
804
+
805
+ /**
806
+ * The invariants that hold after every step: S1 to S10; that a request id names a timer or a call, not both; that
807
+ * a recovered instance last told its host of its earliest timer; that every timer left its table for a reason (see
808
+ * observe()); that a cancel function commits at most once per run,
809
+ * and a status handler at most once per version; that a caller's version of a status never goes back; that every run
810
+ * asked to cancel was asked by someone entitled to; and that every cancel owed is about a call that has not ended, or
811
+ * was abandoned. Then the hosts' own.
812
+ */
813
+ check() {
814
+ assert.deepEqual(this.violations, []);
815
+ for (const node of this.nodes) {
816
+ assert.ok(!node.raw.isTransaction, `${node.name} is inside a transaction between steps`);
817
+ if (!node.migrated()) continue;
818
+ const now = new Map();
819
+ for (const row of node.q('SELECT * FROM kernel_calls').all())
820
+ if (ENDED.has(row.state)) now.set(`${node.name} call ${key(row.from_addr, row.id)}`, ending(row, 'state'));
821
+ for (const row of node.q('SELECT * FROM kernel_runs').all())
822
+ if (ENDED.has(row.status)) now.set(`${node.name} run ${key(row.from_addr, row.id)}`, ending(row, 'status'));
823
+ for (const [name, text] of this.ended)
824
+ if (name.startsWith(`${node.name} `)) assert.equal(now.get(name), text, `S1: the outcome of ${name} changed`);
825
+ for (const [name, text] of now) this.ended.set(name, text);
826
+
827
+ for (const [table, what] of [['sim_accepted', 'S2: accept'], ['sim_results', 'S3: result'], ['sim_lates', 'S3: late'],
828
+ ['sim_cancels', 'the cancel function']]) {
829
+ const twice = node.q(`SELECT from_addr, id, count(*) AS n FROM ${table} GROUP BY from_addr, id HAVING n > 1`).all();
830
+ assert.deepEqual(twice, [], `${what} committed more than once on ${node.name}`);
831
+ }
832
+ const statuses = node.q('SELECT from_addr, id, rev, count(*) AS n FROM sim_statuses GROUP BY from_addr, id, rev HAVING n > 1').all();
833
+ assert.deepEqual(statuses, [], `the status handler committed one version twice on ${node.name}`);
834
+ for (const [columns, what] of [['run_from, run_id, asked', 'one question of a run took two answers'], ['from_addr, id', 'one answer was taken twice']]) {
835
+ const twice = node.q(`SELECT ${columns}, count(*) AS n FROM sim_answers GROUP BY ${columns} HAVING n > 1`).all();
836
+ assert.deepEqual(twice, [], `S8: ${what} on ${node.name}`);
837
+ }
838
+
839
+ // An outcome from the callee, or an abandoned call's late one, is the outcome of its run.
840
+ for (const call of node.q('SELECT * FROM kernel_calls').all()) {
841
+ if (!ENDED.has(call.state) || (call.local === 1 && call.late === null)) continue;
842
+ const run = this.owners.get(call.to_addr).q('SELECT * FROM kernel_runs WHERE from_addr = ? AND id = ?').get(call.from_addr, call.id);
843
+ assert.ok(run, `S4: call ${key(call.from_addr, call.id)} ended but its callee has no run`);
844
+ assert.equal(call.late === null ? ending(call, 'state') : lateEnding(call), ending(run, 'status'), `S4: call ${key(call.from_addr, call.id)} ended unlike its run`);
845
+ }
846
+
847
+ const orphans = node.q(`SELECT r.from_addr, r.id FROM kernel_runs AS r JOIN kernel_calls AS c ON c.by_from = r.from_addr AND c.by_id = r.id
848
+ WHERE r.status IN ('completed', 'failed', 'canceled') AND c.awaited = 1 AND c.state IN ${UNENDED}
849
+ UNION ALL SELECT r.from_addr, r.id FROM kernel_runs AS r JOIN kernel_timers AS t ON t.by_from = r.from_addr AND t.by_id = r.id
850
+ WHERE r.status IN ('completed', 'failed', 'canceled') AND t.awaited = 1`).all();
851
+ assert.deepEqual(orphans.map(run => key(run.from_addr, run.id)), [], `S7: runs of ${node.name} ended while awaiting calls or timers they made`);
852
+
853
+ const both = node.q('SELECT t.from_addr, t.id FROM kernel_timers AS t JOIN kernel_calls AS c ON c.from_addr = t.from_addr AND c.id = t.id').all();
854
+ assert.deepEqual(both.map(row => key(row.from_addr, row.id)), [], `7: request ids of ${node.name} name both a timer and a call`);
855
+ if (node.ready) assert.equal(node.woke, node.q('SELECT min(due) AS due FROM kernel_timers').get().due,
856
+ `7: the wake-up ${node.name} last gave its host is not its earliest timer`);
857
+
858
+ const silent = node.q(`SELECT from_addr, id FROM kernel_calls AS c WHERE state = 'sending' AND NOT EXISTS
859
+ (SELECT 1 FROM kernel_outbox AS o WHERE o.call_from = c.from_addr AND o.call_id = c.id AND o.kind IN ('call', 'cancel'))`).all();
860
+ assert.deepEqual(silent.map(call => key(call.from_addr, call.id)), [], `S10: sending calls of ${node.name} have no letter in its outbox`);
861
+
862
+ for (const letter of node.q(`SELECT o.call_from, o.call_id, c.state, c.abandoned FROM kernel_outbox AS o
863
+ LEFT JOIN kernel_calls AS c ON c.from_addr = o.call_from AND c.id = o.call_id WHERE o.kind = 'cancel'`).all())
864
+ assert.ok(letter.state !== null && (!ENDED.has(letter.state) || letter.abandoned === 1),
865
+ `${node.name} owes a cancel for ${key(letter.call_from, letter.call_id)}, which is ${letter.state ?? 'no call of its own'}`);
866
+
867
+ // A run is asked to cancel by its caller, by its own host, or because a run asked to cancel awaited its call (C7).
868
+ for (const run of node.q('SELECT from_addr, id FROM kernel_runs WHERE cancel_asked = 1').all()) {
869
+ const name = key(run.from_addr, run.id);
870
+ assert.ok(['cancel', 'abandon', 'stop'].some(how => this.intents.has(`${how} ${name}`)) || this.passed.has(name),
871
+ `run ${name} of ${node.name} was asked to cancel, but no host asked for it and no run asked to cancel awaited it`);
872
+ }
873
+
874
+ for (const call of node.q('SELECT from_addr, id, rev FROM kernel_calls').all()) {
875
+ const name = `${node.name} ${key(call.from_addr, call.id)}`;
876
+ assert.ok(call.rev >= (this.revs.get(name) ?? 0), `the status version of call ${name} went back`);
877
+ this.revs.set(name, call.rev);
878
+ }
879
+
880
+ for (const [lane, keys] of node.lanes) {
881
+ let last = -Infinity;
882
+ for (const name of keys) {
883
+ const at = this.order.get(name);
884
+ if (at === undefined) continue;
885
+ assert.ok(at > last, `S5: in lane ${lane} of ${node.name}, ${name} was recorded before an earlier call of the lane`);
886
+ last = at;
887
+ }
888
+ }
889
+ }
890
+ for (const fn of this.steps) fn();
891
+ }
892
+
893
+ /**
894
+ * The invariants of a world at rest: L1, no result still owed, L2, L4, and that every call and run has ended. L3 is
895
+ * quiesce()'s.
896
+ */
897
+ checkQuiet() {
898
+ for (const node of this.nodes) {
899
+ assert.equal(node.count('SELECT count(*) AS n FROM kernel_outbox'), 0, `L1: the outbox of ${node.name} is not empty`);
900
+ // With the clock past them all, every timer has become a call; the call has ended like any other, below.
901
+ assert.equal(node.count('SELECT count(*) AS n FROM kernel_timers'), 0, `L4: timers of ${node.name} are left at rest`);
902
+ assert.equal(node.count('SELECT count(*) AS n FROM kernel_calls WHERE owed = 1'), 0, `results of ${node.name} are still owed at rest`);
903
+ for (const call of node.q('SELECT * FROM kernel_calls').all())
904
+ assert.ok(ENDED.has(call.state), `call ${key(call.from_addr, call.id)} is still ${call.state} at rest`);
905
+ for (const run of node.q('SELECT * FROM kernel_runs').all()) {
906
+ const name = key(run.from_addr, run.id);
907
+ assert.ok(ENDED.has(run.status), `run ${name} is still ${run.status} at rest`);
908
+ if (run.root === 1) continue;
909
+ const caller = this.owners.get(run.from_addr);
910
+ const call = caller.q('SELECT * FROM kernel_calls WHERE from_addr = ? AND id = ?').get(run.from_addr, run.id);
911
+ // An abandoned call has its own outcome; the run's comes as the late one, to `late` instead of `result`.
912
+ const abandoned = call?.abandoned === 1;
913
+ assert.equal(call && (abandoned ? call.late && lateEnding(call) : ending(call, 'state')), ending(run, 'status'),
914
+ `L2: the caller of ${name} does not have its ${abandoned ? 'late ' : ''}outcome`);
915
+ for (const [table, what] of [['sim_results', 'result'], ['sim_lates', 'late']])
916
+ assert.equal(caller.count(`SELECT count(*) AS n FROM ${table} WHERE from_addr = ? AND id = ?`, run.from_addr, run.id),
917
+ (what === 'late') === abandoned ? 1 : 0, `L2: ${what} of ${name} was not committed exactly as often as it should`);
918
+ }
919
+ }
920
+ for (const fn of this.quiets) fn();
921
+ }
922
+ }