@coda-console/kernel 0.2.0 → 0.3.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 CHANGED
@@ -63,6 +63,23 @@ const lateEnding = row => {
63
63
  };
64
64
  const statusOf = row => JSON.stringify([row.status, row.rev, row.asked, row.asking, row.progress]);
65
65
 
66
+ /**
67
+ * Whether the kernel that sent the letter can read `receipt`, as far as it reads one: a refusal's code, which it
68
+ * compares; and for a call or cancel taken, the view's outcome, and without one the status's rev and asked, integers it
69
+ * compares and writes into its table. It reads nothing else of a view. An answer's receipt goes to its host, and the
70
+ * world reads its applied (C8), so that is a boolean.
71
+ */
72
+ function fits(envelope, receipt) {
73
+ if (receipt?.ok === false) return typeof receipt.code === 'string';
74
+ if (receipt?.ok !== true) return false;
75
+ if (envelope.kind === 'answer') return typeof receipt.applied === 'boolean';
76
+ if (envelope.kind !== 'call' && envelope.kind !== 'cancel') return true;
77
+ // A cancel from someone but the caller is taken without a view (the kernel's #revoke).
78
+ if (envelope.kind === 'cancel' && envelope.from !== envelope.call.from) return true;
79
+ const { view } = receipt;
80
+ return typeof view === 'object' && view !== null && (Boolean(view.outcome) || Number.isInteger(view.rev) && Number.isInteger(view.asked));
81
+ }
82
+
66
83
  // The connection one generation of an instance gets, over the database it runs on: it counts commits and fails them
67
84
  // on purpose, and once that generation dies every call through it, or through a statement it prepared, throws Crash.
68
85
  // Everything else passes through. The kernel's copy also records what it prepares.
@@ -81,6 +98,7 @@ function connection(node, generation, record) {
81
98
  if (sql === 'BEGIN IMMEDIATE') {
82
99
  node.called = new Set();
83
100
  node.intents = new Set();
101
+ node.forgetting = [];
84
102
  }
85
103
  if (sql === 'COMMIT') node.commit();
86
104
  else raw.exec(sql);
@@ -131,14 +149,17 @@ function post(node, generation, to, envelope) {
131
149
  /**
132
150
  * Where a host stands between the world and its kernel, for one generation; each one left out is the world's own way.
133
151
  * - 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.
152
+ * which the world vouches for (P3). Returns the receipt, as its sending kernel reads one (P-receipt).
135
153
  * - send: this generation's kernel hands over a letter; the host puts it into the network with the `post` that
136
154
  * `node.ports()` returned, or not.
137
155
  * - recover: the world recovers this generation; the host calls `kernel.recover()`.
156
+ * - wake: this generation's kernel tells of its earliest due time, after the world has kept it in `node.woke` (7). The
157
+ * host sets its own timer and calls `kernel.tick()` when it goes off; the world no longer ticks this instance.
138
158
  * @typedef {{
139
159
  * receive?: (envelope: object, from: SimNode) => object,
140
160
  * send?: (to: string, envelope: object) => Promise<object>,
141
161
  * recover?: () => void,
162
+ * wake?: (due: number | null) => void,
142
163
  * }} Ports
143
164
  */
144
165
 
@@ -165,6 +186,10 @@ export class SimNode {
165
186
  * One asked in a savepoint that rolled back, inside a transaction that committed, still counts.
166
187
  */
167
188
  intents = new Set();
189
+ /** @type {Array<{ name: string, table: string, row: object }>} the rows forget() took in the open transaction (9) */
190
+ forgetting = [];
191
+ /** the last commit took rows: observe() looks again at every run (9) */
192
+ forgot = false;
168
193
  /** whatever the host's setup leaves here for its tests to reach; the world does not look at it */
169
194
  host = undefined;
170
195
  /** @type {(() => void) | undefined} what this generation's host does once its recover() has succeeded */
@@ -227,6 +252,7 @@ export class SimNode {
227
252
  onError: error => this.report(error),
228
253
  wake: at => {
229
254
  this.woke = at;
255
+ generation.ports.wake?.(at);
230
256
  },
231
257
  schedule: fn => this.world.tasks.push(fn),
232
258
  });
@@ -239,6 +265,9 @@ export class SimNode {
239
265
  return fn(ref, ...rest);
240
266
  };
241
267
  }
268
+ // What forget() takes is checked as it takes it, and kept once it commits (9).
269
+ const forgetting = kernel.forget.bind(kernel);
270
+ kernel.forget = options => this.forgets(() => forgetting(options), options.before);
242
271
  // Counted for P-rec.
243
272
  const recovering = kernel.recover.bind(kernel);
244
273
  kernel.recover = () => {
@@ -253,9 +282,15 @@ export class SimNode {
253
282
  if (this.delivering) this.taken += 1;
254
283
  return kernel.receive(envelope);
255
284
  };
285
+ // A tick of a host given wake is its own, on its own clock; the kernel's recover() ticking itself is not one.
286
+ const tick = () => {
287
+ if (generation.ports.wake) this.world.count('ports: a tick through the host');
288
+ return kernel.tick();
289
+ };
256
290
  this.kernel = new Proxy(kernel, {
257
291
  get(target, name) {
258
292
  if (name === 'receive') return receive;
293
+ if (name === 'tick') return tick;
259
294
  const value = Reflect.get(target, name);
260
295
  return typeof value === 'function' ? value.bind(target) : value;
261
296
  },
@@ -319,6 +354,50 @@ export class SimNode {
319
354
  };
320
355
  }
321
356
 
357
+ /**
358
+ * forget() runs in `fn`, in the host's transaction. Every row it takes must have ended and been quiet since before
359
+ * `before`, with no letter about it in the outbox, and a call with its handler committed; an answer's number goes with
360
+ * its run, and only then. What it leaves must not be such a row. The rows taken wait for the commit, which keeps those
361
+ * still gone. A run it leaves that the world saw end before `before`, only as it is not quiet yet, is noted (9).
362
+ * @param {() => { calls: number, runs: number, answers: number }} fn
363
+ * @param {number} before
364
+ */
365
+ forgets(fn, before) {
366
+ const tables = ['kernel_calls', 'kernel_runs', 'kernel_answers'];
367
+ const read = () => tables.map(table => new Map(this.q(`SELECT * FROM ${table}`).all().map(row => [key(row.from_addr, row.id), row])));
368
+ const owed = () => new Set(this.q('SELECT call_from, call_id FROM kernel_outbox').all().map(row => key(row.call_from, row.call_id)));
369
+ // A row without a quiet time is never quiet: one not ended, or a run imported once ended (5.5).
370
+ const quiet = row => row.quiet_at !== null && row.quiet_at < before;
371
+ const may = (table, name, row) => table === 'kernel_calls' ? ENDED.has(row.state) && quiet(row) && row.owed === 0 && !letters.has(name)
372
+ : ENDED.has(row.status) && quiet(row) && !letters.has(name);
373
+ const untouched = () => JSON.stringify(['kernel_outbox', 'kernel_timers'].map(table => this.q(`SELECT * FROM ${table} ORDER BY rowid`).all()));
374
+ const letters = owed();
375
+ const kept = untouched();
376
+ const was = read();
377
+ const counts = fn();
378
+ const now = read();
379
+ if (untouched() !== kept) this.world.violations.push(`9: forget() on ${this.name} changed its outbox or its timers`);
380
+ const runs = new Set();
381
+ for (const [index, table] of tables.entries()) {
382
+ const taken = [...was[index]].filter(([name]) => !now[index].has(name));
383
+ const told = counts[['calls', 'runs', 'answers'][index]];
384
+ if (told !== taken.length) this.world.violations.push(`9: forget() on ${this.name} said it took ${told} of ${table}, not ${taken.length}`);
385
+ for (const [name, row] of taken) {
386
+ if (table === 'kernel_runs') runs.add(name);
387
+ const fits = table === 'kernel_answers' ? runs.has(key(row.run_from, row.run_id)) : may(table, name, row);
388
+ if (!fits) this.world.violations.push(`9: forget() on ${this.name} took ${table} ${name}, which it may not: ${JSON.stringify(row)}`);
389
+ this.forgetting.push({ name, table, row });
390
+ }
391
+ for (const [name, row] of now[index]) {
392
+ const left = table === 'kernel_answers' ? runs.has(key(row.run_from, row.run_id)) : may(table, name, row);
393
+ if (left) this.world.violations.push(`9: forget() on ${this.name} left ${table} ${name}, which it should take: ${JSON.stringify(row)}`);
394
+ if (table === 'kernel_runs' && ENDED.has(row.status) && !letters.has(name) && this.world.endSeen.get(`${this.name} run ${name}`) < before)
395
+ this.world.spared.add(`${this.name} run ${name}`);
396
+ }
397
+ }
398
+ return counts;
399
+ }
400
+
322
401
  recover() {
323
402
  const { generation } = this;
324
403
  try {
@@ -387,6 +466,15 @@ export class SimNode {
387
466
  this.raw.exec('COMMIT');
388
467
  world.commits += 1;
389
468
  for (const intent of this.intents) world.intents.add(intent);
469
+ // A savepoint rolled back may have put back what forget() took in it.
470
+ for (const { name, table, row } of this.forgetting) {
471
+ if (this.q(`SELECT 1 FROM ${table} WHERE from_addr = ? AND id = ?`).get(row.from_addr, row.id)) continue;
472
+ const what = { kernel_calls: 'call', kernel_runs: 'run', kernel_answers: 'answer number' }[table];
473
+ // The first row taken under a name is the one the checks judge by: a later one is the record a late cancel left (9).
474
+ if (!world.forgotten.has(`${this.name} ${what} ${name}`)) world.forgotten.set(`${this.name} ${what} ${name}`, row);
475
+ world.count(`forget: ${what} taken${world.calm ? ' at rest' : ''}`);
476
+ this.forgot = true;
477
+ }
390
478
  world.observe(this);
391
479
  if (world.commits === world.crashAt) {
392
480
  world.crashed += 1;
@@ -405,8 +493,10 @@ export class SimNode {
405
493
  if (this.called.size > 1) this.world.violations.push(`S9: one transaction of ${this.name} called handlers of ${[...this.called].join(', ')}`);
406
494
  }
407
495
 
496
+ // A first generation's migration committed. The world's own tables tell, as they commit with the kernel's: a database
497
+ // the host opened may come with the kernel's tables already made, and without the world's.
408
498
  migrated() {
409
- return this.q("SELECT 1 FROM sqlite_master WHERE name = 'kernel_runs'").get() !== undefined;
499
+ return this.q("SELECT 1 FROM sqlite_master WHERE name = 'sim_accepted'").get() !== undefined;
410
500
  }
411
501
 
412
502
  // The checks' own statements, kept per database; they bypass the generation's connection.
@@ -452,6 +542,15 @@ export class World {
452
542
  intents = new Set();
453
543
  /** @type {Map<string, number>} how often each of the rarer paths was taken */
454
544
  tally = new Map();
545
+ /**
546
+ * @type {Map<string, object>} the rows forget() took, as they were then, by `<node> call|run|answer number <key>`: the
547
+ * checks that read a row it may take read this one once it is gone (9)
548
+ */
549
+ forgotten = new Map();
550
+ /** @type {Map<string, number>} when check() first saw each ended row, by `<node> call|run <key>`: no earlier than it ended */
551
+ endSeen = new Map();
552
+ /** @type {Set<string>} runs, by `<node> run <key>`, that forget() left though they had ended before its `before` (9) */
553
+ spared = new Set();
455
554
  violations = [];
456
555
  /** @type {Array<() => void>} the hosts' own checks after every step, run by check() */
457
556
  steps = [];
@@ -502,9 +601,13 @@ export class World {
502
601
  // After every commit: which runs it recorded, and which lane letters it owed, for S5; which runs it asked to cancel,
503
602
  // with the calls they awaited then, for L3; and where each timer that left its table went (7).
504
603
  observe(node) {
604
+ // Once rows were taken, a new run may get the rowid of one taken (9): every run is looked at once more. A call's
605
+ // first record is the one that counts.
606
+ if (node.forgot) node.seen.run = 0;
607
+ node.forgot = false;
505
608
  for (const row of node.q('SELECT rowid, from_addr, id FROM kernel_runs WHERE rowid > ?').all(node.seen.run)) {
506
609
  node.seen.run = row.rowid;
507
- this.order.set(key(row.from_addr, row.id), this.commits);
610
+ if (!this.order.has(key(row.from_addr, row.id))) this.order.set(key(row.from_addr, row.id), this.commits);
508
611
  }
509
612
  for (const row of node.q('SELECT seq, lane, kind, call_from, call_id FROM kernel_outbox WHERE seq > ?').all(node.seen.seq)) {
510
613
  node.seen.seq = row.seq;
@@ -598,8 +701,14 @@ export class World {
598
701
  if (moment === 'letter') {
599
702
  if (envelope.kind === 'answer') this.noticeAnswer(envelope);
600
703
  if (envelope.kind === 'cancel' && !row(envelope.to, 'kernel_runs')) this.count('cancel before its call');
704
+ if (envelope.kind === 'call' && row(envelope.to, 'kernel_runs') && this.spared.has(`${this.owners.get(envelope.to).name} run ${key(call.from, call.id)}`))
705
+ this.count(`forget: a call letter${envelope.confirmed ? ' (confirmed)' : ''} for a run kept, ended before the retention but not quiet`);
706
+ if ((envelope.kind === 'call' || envelope.kind === 'cancel') && !row(envelope.to, 'kernel_runs') && this.owners.get(envelope.to)?.migrated()
707
+ && this.forgotten.has(`${this.owners.get(envelope.to).name} run ${key(call.from, call.id)}`)) this.count(`forget: a ${envelope.kind} letter${envelope.confirmed ? ' (confirmed)' : ''} for a run taken`);
601
708
  if (envelope.kind === 'status' && ENDED.has(row(envelope.to, 'kernel_calls')?.state)) this.count('status after the outcome');
602
709
  if (envelope.kind === 'result' && row(envelope.to, 'kernel_calls')?.abandoned === 1) this.count('outcome letter after an abandon');
710
+ if ((envelope.kind === 'status' || envelope.kind === 'result') && this.owners.get(envelope.to)?.migrated() && !row(envelope.to, 'kernel_calls')
711
+ && this.forgotten.has(`${this.owners.get(envelope.to).name} call ${key(call.from, call.id)}`)) this.count(`forget: a ${envelope.kind} letter for a call taken`);
603
712
  return;
604
713
  }
605
714
  if (!receipt?.ok || (envelope.kind !== 'call' && envelope.kind !== 'cancel')) {
@@ -618,6 +727,8 @@ export class World {
618
727
  const node = this.owners.get(envelope.to);
619
728
  if (!node?.up || !node.migrated()) return;
620
729
  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');
730
+ else if (this.forgotten.has(`${node.name} run ${key(envelope.call.from, envelope.call.id)}`)
731
+ && !node.q('SELECT 1 FROM kernel_runs WHERE from_addr = ? AND id = ?').get(envelope.call.from, envelope.call.id)) this.count('forget: an answer to a run taken');
621
732
  else if (node.q('SELECT 1 FROM kernel_runs WHERE from_addr = ? AND id = ? AND to_addr = ? AND status = ? AND asked = ?')
622
733
  .get(envelope.call.from, envelope.call.id, envelope.to, 'input-required', envelope.asked)) this.count('answer: to its question');
623
734
  }
@@ -670,7 +781,14 @@ export class World {
670
781
  try {
671
782
  if (!ports.receive) return JSON.parse(JSON.stringify(node.kernel.receive(envelope)));
672
783
  this.count('ports: a letter in through the host');
673
- return JSON.parse(JSON.stringify(ports.receive(envelope, from)));
784
+ // What goes over the wire is what the sending kernel gets, and what is checked; nothing at all comes as null.
785
+ const receipt = JSON.parse(JSON.stringify(ports.receive(envelope, from)) ?? 'null');
786
+ // The sending kernel could not read it: the world tells of it, and the receipt is lost on its way.
787
+ if (!fits(envelope, receipt)) {
788
+ this.violations.push(`P-receipt: the host of ${node.name} answered a ${envelope.kind} letter with no receipt of the kernel's: ${JSON.stringify(receipt)}`);
789
+ return undefined;
790
+ }
791
+ return receipt;
674
792
  } catch (error) {
675
793
  if (!lost(error)) throw error;
676
794
  return undefined;
@@ -702,10 +820,12 @@ export class World {
702
820
  }
703
821
  }
704
822
 
705
- /** The host of `node` ticks, as its timer would make it (7). */
823
+ /** The host of `node` ticks, as its timer would make it (7); not one given wake, which ticks on its own clock. */
706
824
  tick(node) {
707
825
  if (!node.up && this.restart) node.boot();
708
826
  if (!node.up) return;
827
+ // Such a tick would be counted as the host's own (see wrap()), and the tally would say what no host did.
828
+ if (node.generation.ports.wake) throw new Error(`the host of ${node.name} was given wake: its clock is the host's, and the world does not tick it`);
709
829
  try {
710
830
  node.kernel.tick();
711
831
  } catch (error) {
@@ -743,13 +863,16 @@ export class World {
743
863
  * advance. Otherwise it looks at none of the kernel's tables; what they hold at rest is for checkQuiet() to judge.
744
864
  * @param {() => boolean} [advance] the hosts move on what they hold by a step; true while there was something to move
745
865
  * @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
866
+ * round, and tick an instance whose host was given wake; without it the world pumps every instance itself, which
867
+ * would make up for a host that never does
747
868
  */
748
869
  async quiesce(advance = () => false, { resend } = {}) {
749
870
  this.calm = true;
750
871
  // Letters already in flight may still come back refused by faults chosen earlier; the pumps that count come after.
751
872
  await this.drain();
752
873
  await this.rest(() => false, resend);
874
+ // A host's fault met on the way is told of first, not as the kernel's invariant it leaves broken.
875
+ assert.deepEqual(this.violations, []);
753
876
  this.checkStatus();
754
877
  this.checkHalted();
755
878
  await this.rest(advance, resend);
@@ -766,16 +889,43 @@ export class World {
766
889
  const advanced = advance();
767
890
  if (resend) resend();
768
891
  else for (const node of this.nodes) this.pump(node);
769
- // The clock passes every timer, and every instance ticks.
892
+ // The clock passes every timer, and every instance ticks, but one whose host was given wake: its own timer, on its
893
+ // own clock, ticks it once its host moves that clock on, which resend() does.
770
894
  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);
895
+ for (const node of this.nodes) if (!node.generation.ports.wake) this.tick(node);
772
896
  await this.drain();
773
897
  await Promise.all(this.nodes.filter(node => node.up).map(node => node.kernel.idle()));
774
898
  if (this.crashed === crashed && !advanced && this.net.length === 0 && this.tasks.length === 0
775
899
  && this.nodes.every(node => node.ready && node.count('SELECT count(*) AS n FROM kernel_timers') === 0)) return;
776
900
  }
777
901
  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(', ')}`);
902
+ + `letters in its outbox, ${node.count(`SELECT count(*) AS n FROM kernel_calls WHERE state IN ${UNENDED}`)} calls not ended `
903
+ + `and ${node.count('SELECT count(*) AS n FROM kernel_timers')} timers`).join(', ')}`);
904
+ }
905
+
906
+ /**
907
+ * A call of `node` as the checks read it: as it is, or as forget() took it (9).
908
+ * @param {SimNode} node
909
+ * @param {string} from
910
+ * @param {string} id
911
+ */
912
+ call(node, from, id) {
913
+ return node.q('SELECT * FROM kernel_calls WHERE from_addr = ? AND id = ?').get(from, id) ?? this.forgotten.get(`${node.name} call ${key(from, id)}`);
914
+ }
915
+
916
+ /**
917
+ * The run of a call to `to`, as it is or as forget() took it (9).
918
+ * @param {string} from
919
+ * @param {string} id
920
+ * @param {string} to
921
+ */
922
+ run(from, id, to) {
923
+ const node = this.owners.get(to);
924
+ const row = node.q('SELECT * FROM kernel_runs WHERE from_addr = ? AND id = ?').get(from, id);
925
+ const gone = this.forgotten.get(`${node.name} run ${key(from, id)}`);
926
+ // The cancel of an abandoned call that its caller got through only after the retention finds the run forgotten,
927
+ // and leaves a record of its own (C7); the run's outcome is the one forget() took (9).
928
+ return row && !(row.op === null && gone) ? row : gone;
779
929
  }
780
930
 
781
931
  /** At rest, before the held runs finish: a call that has not ended knows the status its run has now (C6). */
@@ -796,7 +946,7 @@ export class World {
796
946
  checkHalted() {
797
947
  for (const [name, { node, calls }] of this.halted)
798
948
  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;
949
+ if (ENDED.has(this.call(node, call.from_addr, call.id).state)) continue;
800
950
  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
951
  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
952
  }
@@ -819,10 +969,20 @@ export class World {
819
969
  for (const row of node.q('SELECT * FROM kernel_calls').all())
820
970
  if (ENDED.has(row.state)) now.set(`${node.name} call ${key(row.from_addr, row.id)}`, ending(row, 'state'));
821
971
  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);
972
+ if (ENDED.has(row.status)) {
973
+ const name = `${node.name} run ${key(row.from_addr, row.id)}`;
974
+ // A record a late cancel left: the run's outcome is the one forget() took (see run()).
975
+ now.set(name, ending((row.op === null && this.forgotten.get(name)) || row, 'status'));
976
+ }
977
+ for (const [name, text] of this.ended) {
978
+ if (!name.startsWith(`${node.name} `)) continue;
979
+ const gone = this.forgotten.get(name);
980
+ assert.equal(now.get(name) ?? (gone && ending(gone, name.startsWith(`${node.name} call `) ? 'state' : 'status')), text, `S1: the outcome of ${name} changed`);
981
+ }
982
+ for (const [name, text] of now) {
983
+ if (!this.ended.has(name)) this.endSeen.set(name, this.time);
984
+ this.ended.set(name, text);
985
+ }
826
986
 
827
987
  for (const [table, what] of [['sim_accepted', 'S2: accept'], ['sim_results', 'S3: result'], ['sim_lates', 'S3: late'],
828
988
  ['sim_cancels', 'the cancel function']]) {
@@ -839,7 +999,7 @@ export class World {
839
999
  // An outcome from the callee, or an abandoned call's late one, is the outcome of its run.
840
1000
  for (const call of node.q('SELECT * FROM kernel_calls').all()) {
841
1001
  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);
1002
+ const run = this.run(call.from_addr, call.id, call.to_addr);
843
1003
  assert.ok(run, `S4: call ${key(call.from_addr, call.id)} ended but its callee has no run`);
844
1004
  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
1005
  }
@@ -902,16 +1062,31 @@ export class World {
902
1062
  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
1063
  for (const call of node.q('SELECT * FROM kernel_calls').all())
904
1064
  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;
1065
+ // The runs it has, and those forget() took, which L2 holds for all the same (9).
1066
+ const runs = new Map(node.q('SELECT * FROM kernel_runs').all().map(row => [key(row.from_addr, row.id), row]));
1067
+ for (const [name, row] of this.forgotten)
1068
+ if (name.startsWith(`${node.name} run `) && !runs.has(key(row.from_addr, row.id))) runs.set(key(row.from_addr, row.id), row);
1069
+ for (const [name, row] of runs) {
1070
+ assert.ok(ENDED.has(row.status), `run ${name} is still ${row.status} at rest`);
1071
+ if (row.root === 1) continue;
1072
+ const run = this.run(row.from_addr, row.id, row.to_addr);
909
1073
  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);
1074
+ const call = this.call(caller, run.from_addr, run.id);
911
1075
  // An abandoned call has its own outcome; the run's comes as the late one, to `late` instead of `result`.
912
1076
  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`);
1077
+ // forget() took an abandoned call before the run's own outcome came: a late outcome is no longer taken (9).
1078
+ if (abandoned && call.late === null && this.forgotten.has(`${caller.name} call ${name}`)) {
1079
+ for (const table of ['sim_results', 'sim_lates'])
1080
+ assert.equal(caller.count(`SELECT count(*) AS n FROM ${table} WHERE from_addr = ? AND id = ?`, run.from_addr, run.id), 0,
1081
+ `L2: ${name} was forgotten before its late outcome came, but a handler took one`);
1082
+ this.count('forget: an abandoned call taken before its late outcome');
1083
+ continue;
1084
+ }
1085
+ // An outcome the caller's kernel wrote itself is not compared: the callee's, coming after, does not change it (C4).
1086
+ // Say a call imported open whose callee had no record: lost, and the cancel sent after it left a canceled record.
1087
+ if (!(call?.local === 1 && !abandoned))
1088
+ assert.equal(call && (abandoned ? call.late && lateEnding(call) : ending(call, 'state')), ending(run, 'status'),
1089
+ `L2: the caller of ${name} does not have its ${abandoned ? 'late ' : ''}outcome`);
915
1090
  for (const [table, what] of [['sim_results', 'result'], ['sim_lates', 'late']])
916
1091
  assert.equal(caller.count(`SELECT count(*) AS n FROM ${table} WHERE from_addr = ? AND id = ?`, run.from_addr, run.id),
917
1092
  (what === 'late') === abandoned ? 1 : 0, `L2: ${what} of ${name} was not committed exactly as often as it should`);
package/src/kernel.mjs CHANGED
@@ -135,7 +135,7 @@ class Kernel {
135
135
  // would break the later one.
136
136
  if (version > MIGRATIONS.length) throw new Error(`the database is at schema version ${version}, newer than this kernel's ${MIGRATIONS.length}`);
137
137
  if (version === MIGRATIONS.length) return;
138
- for (const step of MIGRATIONS.slice(version)) this.#db.exec(step);
138
+ for (const step of MIGRATIONS.slice(version)) this.#db.exec(typeof step === 'string' ? step : step(this.#now()));
139
139
  this.#q(SQL.setVersion).run(MIGRATIONS.length);
140
140
  }
141
141
 
@@ -171,6 +171,47 @@ class Kernel {
171
171
  return defined({ from, id, to, op, state: 'sending', memo, by: by && { from: by.from, id: by.id }, awaited });
172
172
  }
173
173
 
174
+ /**
175
+ * Imports a call its callee had already taken before this kernel kept it (导入): it is open, at version 0, and owes no
176
+ * letter. A call letter would have a callee without the record run it again; recover() inquires instead (C5).
177
+ * @param {{ from: string, id: string, to: string, op: string, input?: unknown, lane?: string, by?: Ref,
178
+ * awaited?: boolean, parent?: unknown, memo?: unknown }} call
179
+ */
180
+ adoptCall({ from, id, to, op, input = null, lane = null, by, awaited = false, parent = null, memo }) {
181
+ this.#adopting();
182
+ const digest = digestOf([to, op, input, parent]);
183
+ const found = this.#found(from, id, digest);
184
+ if (found) return found;
185
+ awaited = this.#awaits(from, by, awaited);
186
+ return callView(this.#q(SQL.adoptCall).get(from, id, to, op, digest, json(input), json(parent), lane, by?.from ?? null, by?.id ?? null,
187
+ awaited ? 1 : 0, json(memo)));
188
+ }
189
+
190
+ /**
191
+ * Imports a run taken before this kernel kept it (导入): no handler is called and no letter owed. Without an outcome
192
+ * it is working, or waiting for input on its first question; with one, it has ended.
193
+ * @param {{ from: string, id: string, to: string, op: string, input?: unknown, parent?: unknown, awaited?: boolean,
194
+ * status?: 'working' | 'input-required', asking?: unknown, progress?: unknown, outcome?: Outcome }} run
195
+ */
196
+ adoptRun({ from, id, to, op, input = null, parent = null, awaited = false, status = 'working', asking, progress, outcome }) {
197
+ this.#adopting();
198
+ // A run without op is the record a cancel leaves, whose digest is never compared (C1).
199
+ if (op == null) throw new Error(`an imported run needs its op: ${from}/${id}`);
200
+ const digest = digestOf([to, op, input, parent]);
201
+ const found = this.#q(SQL.run).get(from, id);
202
+ if (found) {
203
+ if (found.digest !== digest) throw new Error(`run ${from}/${id} already exists with other content`);
204
+ return adoptedView(found);
205
+ }
206
+ if (outcome) {
207
+ if (!ENDED.has(outcome.outcome)) throw new Error(`${outcome.outcome} is not an outcome`);
208
+ status = outcome.outcome;
209
+ } else if (!WORKING.has(status)) throw new Error(`a run cannot be imported as ${status}`);
210
+ const asks = status === 'input-required';
211
+ return adoptedView(this.#q(SQL.adoptRun).get(from, id, to, op, digest, json(parent), awaited ? 1 : 0, status, asks ? 1 : 0,
212
+ asks ? json(asking) : null, json(outcome?.result), outcome?.outcome === 'failed' ? json(written(outcome.error)) : null, json(progress)));
213
+ }
214
+
174
215
  /**
175
216
  * A call to make once `delayMs` has passed (7). Until then it is a timer, which shares its id with the calls.
176
217
  * @param {{ from: string, id: string, delayMs: number, to: string, op: string, input?: unknown, lane?: string, by?: Ref,
@@ -355,7 +396,25 @@ class Kernel {
355
396
  const row = this.#made(call);
356
397
  if (ENDED.has(row.state)) return;
357
398
  this.#recall(row);
358
- this.#q(SQL.abandonCall).run(json({ code: 'abandoned', message: 'the caller abandoned this call', effects: 'unknown' }), row.from_addr, row.id);
399
+ this.#q(SQL.abandonCall).run(json({ code: 'abandoned', message: 'the caller abandoned this call', effects: 'unknown' }), this.#now(),
400
+ row.from_addr, row.id);
401
+ }
402
+
403
+ /**
404
+ * Deletes what has been quiet since before `before` and is no longer needed to tell a repeat (9): ended calls whose
405
+ * handler has committed, and ended runs, about which no letter is owed and none has left the outbox since, with the
406
+ * numbers of the answers a deleted run took. A repeat comes no later than the host's retention after the last letter
407
+ * about it, which `before` stands for; after it, its id is unknown here.
408
+ * @param {{ before: number }} options
409
+ * @returns {{ calls: number, runs: number, answers: number }} how many rows of each were deleted
410
+ */
411
+ forget({ before }) {
412
+ this.#inside();
413
+ const calls = this.#q(SQL.forgetCalls).run(before).changes;
414
+ const runs = this.#q(SQL.forgetRuns).all(before);
415
+ let answers = 0;
416
+ for (const run of runs) answers += this.#q(SQL.forgetAnswers).run(run.from_addr, run.id).changes;
417
+ return { calls, runs: runs.length, answers };
359
418
  }
360
419
 
361
420
  /**
@@ -402,7 +461,12 @@ class Kernel {
402
461
  for (const { from_addr, id } of this.#q(SQL.owedCalls).all()) this.transaction(() => this.#notify(from_addr, id));
403
462
  const rereads = [...this.#rereads.values()];
404
463
  this.#rereads.clear();
405
- for (const call of rereads) this.inquire(call);
464
+ // Read the row itself, not through inquire(): forget() may have deleted a call kept here since, and that is no
465
+ // reason to throw and lose the other rereads (9, C5).
466
+ for (const call of rereads) {
467
+ const row = this.#q(SQL.call).get(call.from, call.id);
468
+ if (row?.state === 'open') this.#ask(row);
469
+ }
406
470
  this.#kick();
407
471
  }
408
472
 
@@ -428,6 +492,13 @@ class Kernel {
428
492
  if (!this.#tx.open) throw new Error('this must be called inside kernel.transaction()');
429
493
  }
430
494
 
495
+ // Imports come before recover(): its first rereads and letters are what moves them on, and no letter or memory of
496
+ // this instance has met them yet (R1, R2).
497
+ #adopting() {
498
+ this.#inside();
499
+ if (this.#started) throw new Error('imports must come before recover()');
500
+ }
501
+
431
502
  #outside() {
432
503
  if (this.#closed) throw new Error('the kernel is closed');
433
504
  if (this.#tx.open) throw new Error('this opens its own transaction and must not be called inside one');
@@ -573,7 +644,7 @@ class Kernel {
573
644
 
574
645
  // Writes a run's outcome and owes its caller a letter carrying it in place of any status not yet confirmed (C3, C4, C6).
575
646
  #end(from, id, { outcome, result, error }, progress) {
576
- const row = this.#q(SQL.endRun).get(outcome, json(result), json(error), json(progress), from, id);
647
+ const row = this.#q(SQL.endRun).get(outcome, json(result), json(error), json(progress), this.#now(), from, id);
577
648
  if (row.root === 1) return;
578
649
  this.#q(SQL.dropStatusLetter).get(from, id);
579
650
  this.#owe(from, null, 'result', { from, id },
@@ -658,13 +729,24 @@ class Kernel {
658
729
  * @param {Ref} call
659
730
  * @param {string} sender
660
731
  * @param {{ rev: number, status: string, asked: number, asking?: unknown, progress?: unknown }} status
732
+ * @param {boolean} [muted] the callee's cancel function may have run in this same transaction (S9)
733
+ * @param {boolean} [receipt] this is the receipt of the letter just dropped from the outbox, not a status letter
661
734
  */
662
- #hear(call, sender, { rev, status, asked, asking, progress }) {
735
+ #hear(call, sender, { rev, status, asked, asking, progress }, muted = false, receipt = false) {
663
736
  const row = this.#known(call, sender, 'a status');
664
- if (!row || ENDED.has(row.state) || rev <= row.rev) return { ok: true };
737
+ // A receipt for a sending call comes as its letter is dropped: whatever version it says, the callee took the call,
738
+ // and the call leaves sending (S10). A status letter at a version not above the row's is an old one, and changes
739
+ // nothing (C6).
740
+ if (!row || ENDED.has(row.state) || (!(receipt && row.state === 'sending') && rev <= row.rev)) return { ok: true };
665
741
  if (row.state === 'sending') this.#q(SQL.dropCallLetter).run(call.from, call.id);
666
742
  const known = WORKING.has(status) ? status : 'working';
667
743
  const now = this.#q(SQL.hearCall).get(known, rev, asked, known === 'input-required' ? json(asking) : null, json(progress), call.from, call.id);
744
+ // The callee's cancel function may have just run in this transaction: the caller's are not called (S9).
745
+ if (muted) return { ok: true };
746
+ // The receipt that takes a call is no status of its own (C6): at version 1, or below it from a callee that breaks
747
+ // C6, it is the run as it was taken. An imported run may have been taken waiting for input (5.5), and no letter of
748
+ // its own will ask: the caller hears the question here.
749
+ if (row.state === 'sending' && rev <= 1 && known === 'working') return { ok: true };
668
750
  // T6: a status handler that throws loses its own changes only, and is not called again.
669
751
  try {
670
752
  this.transaction(() => this.#results.get(call.from)?.status?.(callView(now)));
@@ -718,7 +800,7 @@ class Kernel {
718
800
  // confirmed goes too; an abandoned call keeps its cancel until confirmed.
719
801
  if (row.state === 'sending') this.#q(SQL.dropCallLetter).run(call.from, call.id);
720
802
  this.#q(SQL.dropCancelLetter).run(call.from, call.id);
721
- this.#q(SQL.endCall).run(outcome, json(result), json(error), json(progress), local ? 1 : 0, call.from, call.id);
803
+ this.#q(SQL.endCall).run(outcome, json(result), json(error), json(progress), local ? 1 : 0, this.#now(), call.from, call.id);
722
804
  }
723
805
  this.#notify(call.from, call.id);
724
806
  return { ok: true };
@@ -743,9 +825,14 @@ class Kernel {
743
825
  * @param {object} row
744
826
  * @param {Receipt} receipt
745
827
  * @param {boolean} [direct] the letter was delivered locally, in this same transaction
828
+ * @param {boolean} [muted] the callee may have run a function of its own in this same transaction (S9)
746
829
  */
747
- #heard(row, receipt, direct = false) {
830
+ #heard(row, receipt, direct = false, muted = false) {
748
831
  this.#q(SQL.dropLetter).run(row.seq);
832
+ // A copy of the letter may still be on its way, and the other side may still send what it answers, until it heard
833
+ // this one: an ended row is kept for a repeat from now on (9). A call or cancel is about the caller's call, a status
834
+ // or an outcome about the callee's run.
835
+ this.#q(row.kind === 'call' || row.kind === 'cancel' ? SQL.quietCall : SQL.quietRun).run(this.#now(), row.call_from, row.call_id);
749
836
  const call = { from: row.call_from, id: row.call_id };
750
837
  if (!receipt.ok) {
751
838
  if (row.kind === 'call') this.#conclude(call, row.to_addr, failure(receipt.code, `${row.to_addr} answered ${receipt.code}`), true);
@@ -759,10 +846,8 @@ class Kernel {
759
846
  // A local callee has just run its own functions in this transaction; its outcome and its status come in their own
760
847
  // letters (4, C6).
761
848
  if (!direct && receipt.view.outcome) this.#conclude(call, row.to_addr, receipt.view);
762
- else {
763
- this.#q(SQL.openCall).run(call.from, call.id);
764
- if (!direct) this.#hear(call, row.to_addr, receipt.view);
765
- }
849
+ else if (direct) this.#q(SQL.openCall).run(call.from, call.id);
850
+ else this.#hear(call, row.to_addr, receipt.view, muted, true);
766
851
  }
767
852
 
768
853
  /**
@@ -884,8 +969,19 @@ class Kernel {
884
969
  #deliver(row) {
885
970
  try {
886
971
  const receipt = this.transaction(() => {
972
+ // A callee that took this call before, an imported run say, runs nothing of its own now and owes nothing more:
973
+ // its receipt is all the caller hears, as from a callee elsewhere (5.5, S9). So too a cancel that finds the run
974
+ // ended, and one in the place of the call letter that leaves such a run as it was before the cancel, at the same
975
+ // version: no letter of its own will tell either. The run may have changed since it was imported, its status
976
+ // letter dropped by a caller that had no call yet. A cancel function that changed the run owes its own letters
977
+ // (C6, C7).
978
+ const run = row.kind === 'call' || row.kind === 'cancel' ? this.#q(SQL.run).get(row.call_from, row.call_id) : undefined;
887
979
  const receipt = this.#take(JSON.parse(row.envelope));
888
- if (!HOLD.has(receipt.code)) this.#heard(row, receipt, true);
980
+ const taken = row.kind === 'call' ? run !== undefined
981
+ : ENDED.has(run?.status) || (run !== undefined && !receipt.view.outcome && receipt.view.rev === run.rev
982
+ && this.#q(SQL.call).get(row.call_from, row.call_id).state === 'sending');
983
+ // A cancel delivered here may have just run the callee's cancel function: its receipt calls no status (S9).
984
+ if (!HOLD.has(receipt.code)) this.#heard(row, receipt, !taken, row.kind === 'cancel');
889
985
  return receipt;
890
986
  });
891
987
  if (HOLD.has(receipt.code)) this.#held.add(row.seq);
@@ -1005,6 +1101,11 @@ function runView(row) {
1005
1101
  ...(row.root === 1 && { root: true }) });
1006
1102
  }
1007
1103
 
1104
+ // An imported run as adoptRun() returns it: as runs() lists it, with its outcome once ended.
1105
+ function adoptedView(row) {
1106
+ return defined({ ...runView(row), ...(ENDED.has(row.status) && outcomeOf(row.status, row)) });
1107
+ }
1108
+
1008
1109
  // The run that made a call or a timer (C9).
1009
1110
  function byOf(row) {
1010
1111
  return row.by_from === null ? undefined : { from: row.by_from, id: row.by_id };