@coda-console/kernel 0.2.1 → 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
@@ -98,6 +98,7 @@ function connection(node, generation, record) {
98
98
  if (sql === 'BEGIN IMMEDIATE') {
99
99
  node.called = new Set();
100
100
  node.intents = new Set();
101
+ node.forgetting = [];
101
102
  }
102
103
  if (sql === 'COMMIT') node.commit();
103
104
  else raw.exec(sql);
@@ -152,10 +153,13 @@ function post(node, generation, to, envelope) {
152
153
  * - send: this generation's kernel hands over a letter; the host puts it into the network with the `post` that
153
154
  * `node.ports()` returned, or not.
154
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.
155
158
  * @typedef {{
156
159
  * receive?: (envelope: object, from: SimNode) => object,
157
160
  * send?: (to: string, envelope: object) => Promise<object>,
158
161
  * recover?: () => void,
162
+ * wake?: (due: number | null) => void,
159
163
  * }} Ports
160
164
  */
161
165
 
@@ -182,6 +186,10 @@ export class SimNode {
182
186
  * One asked in a savepoint that rolled back, inside a transaction that committed, still counts.
183
187
  */
184
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;
185
193
  /** whatever the host's setup leaves here for its tests to reach; the world does not look at it */
186
194
  host = undefined;
187
195
  /** @type {(() => void) | undefined} what this generation's host does once its recover() has succeeded */
@@ -244,6 +252,7 @@ export class SimNode {
244
252
  onError: error => this.report(error),
245
253
  wake: at => {
246
254
  this.woke = at;
255
+ generation.ports.wake?.(at);
247
256
  },
248
257
  schedule: fn => this.world.tasks.push(fn),
249
258
  });
@@ -256,6 +265,9 @@ export class SimNode {
256
265
  return fn(ref, ...rest);
257
266
  };
258
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);
259
271
  // Counted for P-rec.
260
272
  const recovering = kernel.recover.bind(kernel);
261
273
  kernel.recover = () => {
@@ -270,9 +282,15 @@ export class SimNode {
270
282
  if (this.delivering) this.taken += 1;
271
283
  return kernel.receive(envelope);
272
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
+ };
273
290
  this.kernel = new Proxy(kernel, {
274
291
  get(target, name) {
275
292
  if (name === 'receive') return receive;
293
+ if (name === 'tick') return tick;
276
294
  const value = Reflect.get(target, name);
277
295
  return typeof value === 'function' ? value.bind(target) : value;
278
296
  },
@@ -336,6 +354,50 @@ export class SimNode {
336
354
  };
337
355
  }
338
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
+
339
401
  recover() {
340
402
  const { generation } = this;
341
403
  try {
@@ -404,6 +466,15 @@ export class SimNode {
404
466
  this.raw.exec('COMMIT');
405
467
  world.commits += 1;
406
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
+ }
407
478
  world.observe(this);
408
479
  if (world.commits === world.crashAt) {
409
480
  world.crashed += 1;
@@ -471,6 +542,15 @@ export class World {
471
542
  intents = new Set();
472
543
  /** @type {Map<string, number>} how often each of the rarer paths was taken */
473
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();
474
554
  violations = [];
475
555
  /** @type {Array<() => void>} the hosts' own checks after every step, run by check() */
476
556
  steps = [];
@@ -521,9 +601,13 @@ export class World {
521
601
  // After every commit: which runs it recorded, and which lane letters it owed, for S5; which runs it asked to cancel,
522
602
  // with the calls they awaited then, for L3; and where each timer that left its table went (7).
523
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;
524
608
  for (const row of node.q('SELECT rowid, from_addr, id FROM kernel_runs WHERE rowid > ?').all(node.seen.run)) {
525
609
  node.seen.run = row.rowid;
526
- 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);
527
611
  }
528
612
  for (const row of node.q('SELECT seq, lane, kind, call_from, call_id FROM kernel_outbox WHERE seq > ?').all(node.seen.seq)) {
529
613
  node.seen.seq = row.seq;
@@ -617,8 +701,14 @@ export class World {
617
701
  if (moment === 'letter') {
618
702
  if (envelope.kind === 'answer') this.noticeAnswer(envelope);
619
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`);
620
708
  if (envelope.kind === 'status' && ENDED.has(row(envelope.to, 'kernel_calls')?.state)) this.count('status after the outcome');
621
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`);
622
712
  return;
623
713
  }
624
714
  if (!receipt?.ok || (envelope.kind !== 'call' && envelope.kind !== 'cancel')) {
@@ -637,6 +727,8 @@ export class World {
637
727
  const node = this.owners.get(envelope.to);
638
728
  if (!node?.up || !node.migrated()) return;
639
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');
640
732
  else if (node.q('SELECT 1 FROM kernel_runs WHERE from_addr = ? AND id = ? AND to_addr = ? AND status = ? AND asked = ?')
641
733
  .get(envelope.call.from, envelope.call.id, envelope.to, 'input-required', envelope.asked)) this.count('answer: to its question');
642
734
  }
@@ -728,10 +820,12 @@ export class World {
728
820
  }
729
821
  }
730
822
 
731
- /** 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. */
732
824
  tick(node) {
733
825
  if (!node.up && this.restart) node.boot();
734
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`);
735
829
  try {
736
830
  node.kernel.tick();
737
831
  } catch (error) {
@@ -769,7 +863,8 @@ export class World {
769
863
  * advance. Otherwise it looks at none of the kernel's tables; what they hold at rest is for checkQuiet() to judge.
770
864
  * @param {() => boolean} [advance] the hosts move on what they hold by a step; true while there was something to move
771
865
  * @param {{ resend?: () => void }} [options] resend: how the hosts have every instance send what it keeps again, once a
772
- * 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
773
868
  */
774
869
  async quiesce(advance = () => false, { resend } = {}) {
775
870
  this.calm = true;
@@ -794,16 +889,43 @@ export class World {
794
889
  const advanced = advance();
795
890
  if (resend) resend();
796
891
  else for (const node of this.nodes) this.pump(node);
797
- // 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.
798
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));
799
- for (const node of this.nodes) this.tick(node);
895
+ for (const node of this.nodes) if (!node.generation.ports.wake) this.tick(node);
800
896
  await this.drain();
801
897
  await Promise.all(this.nodes.filter(node => node.up).map(node => node.kernel.idle()));
802
898
  if (this.crashed === crashed && !advanced && this.net.length === 0 && this.tasks.length === 0
803
899
  && this.nodes.every(node => node.ready && node.count('SELECT count(*) AS n FROM kernel_timers') === 0)) return;
804
900
  }
805
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')} `
806
- + `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;
807
929
  }
808
930
 
809
931
  /** At rest, before the held runs finish: a call that has not ended knows the status its run has now (C6). */
@@ -824,7 +946,7 @@ export class World {
824
946
  checkHalted() {
825
947
  for (const [name, { node, calls }] of this.halted)
826
948
  for (const call of calls) {
827
- 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;
828
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);
829
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`);
830
952
  }
@@ -847,10 +969,20 @@ export class World {
847
969
  for (const row of node.q('SELECT * FROM kernel_calls').all())
848
970
  if (ENDED.has(row.state)) now.set(`${node.name} call ${key(row.from_addr, row.id)}`, ending(row, 'state'));
849
971
  for (const row of node.q('SELECT * FROM kernel_runs').all())
850
- if (ENDED.has(row.status)) now.set(`${node.name} run ${key(row.from_addr, row.id)}`, ending(row, 'status'));
851
- for (const [name, text] of this.ended)
852
- if (name.startsWith(`${node.name} `)) assert.equal(now.get(name), text, `S1: the outcome of ${name} changed`);
853
- 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
+ }
854
986
 
855
987
  for (const [table, what] of [['sim_accepted', 'S2: accept'], ['sim_results', 'S3: result'], ['sim_lates', 'S3: late'],
856
988
  ['sim_cancels', 'the cancel function']]) {
@@ -867,7 +999,7 @@ export class World {
867
999
  // An outcome from the callee, or an abandoned call's late one, is the outcome of its run.
868
1000
  for (const call of node.q('SELECT * FROM kernel_calls').all()) {
869
1001
  if (!ENDED.has(call.state) || (call.local === 1 && call.late === null)) continue;
870
- 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);
871
1003
  assert.ok(run, `S4: call ${key(call.from_addr, call.id)} ended but its callee has no run`);
872
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`);
873
1005
  }
@@ -930,16 +1062,31 @@ export class World {
930
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`);
931
1063
  for (const call of node.q('SELECT * FROM kernel_calls').all())
932
1064
  assert.ok(ENDED.has(call.state), `call ${key(call.from_addr, call.id)} is still ${call.state} at rest`);
933
- for (const run of node.q('SELECT * FROM kernel_runs').all()) {
934
- const name = key(run.from_addr, run.id);
935
- assert.ok(ENDED.has(run.status), `run ${name} is still ${run.status} at rest`);
936
- 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);
937
1073
  const caller = this.owners.get(run.from_addr);
938
- 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);
939
1075
  // An abandoned call has its own outcome; the run's comes as the late one, to `late` instead of `result`.
940
1076
  const abandoned = call?.abandoned === 1;
941
- assert.equal(call && (abandoned ? call.late && lateEnding(call) : ending(call, 'state')), ending(run, 'status'),
942
- `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`);
943
1090
  for (const [table, what] of [['sim_results', 'result'], ['sim_lates', 'late']])
944
1091
  assert.equal(caller.count(`SELECT count(*) AS n FROM ${table} WHERE from_addr = ? AND id = ?`, run.from_addr, run.id),
945
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 };