@coda-console/kernel 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/docs/design.md +402 -0
- package/kernel.mjs +1 -0
- package/package.json +28 -4
- package/sim/host.mjs +256 -0
- package/sim/scenarios.mjs +484 -0
- package/sim/solo.mjs +47 -0
- package/sim/world.mjs +793 -0
- package/src/kernel.mjs +1038 -0
- package/src/schema.mjs +169 -0
- package/src/transaction.mjs +75 -0
package/src/kernel.mjs
ADDED
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { META, MIGRATIONS, SQL } from './schema.mjs';
|
|
3
|
+
import { Transactions } from './transaction.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by `accept` to refuse a call: the run ends `failed` with `effects: 'none'`, and a repeat gets the same. Thrown
|
|
7
|
+
* by `answer` to refuse an answer: the run stays as it was, and the receipt says why.
|
|
8
|
+
*/
|
|
9
|
+
export class Rejected extends Error {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} code
|
|
12
|
+
* @param {string} [message]
|
|
13
|
+
*/
|
|
14
|
+
constructor(code, message = code) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'Rejected';
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Thrown by `accept` to not take a call now: nothing is kept, and the caller sends it again on a later pump(). */
|
|
22
|
+
export class Busy extends Error {
|
|
23
|
+
constructor(message = 'busy') {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'Busy';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ENDED = new Set(['completed', 'failed', 'canceled']);
|
|
30
|
+
const EFFECTS = new Set(['none', 'unknown', 'some']);
|
|
31
|
+
const WORKING = new Set(['working', 'input-required']);
|
|
32
|
+
// Receipts after which a letter stays in the outbox, untouched, until the next pump() (C10).
|
|
33
|
+
const HOLD = new Set(['busy', 'not_ready']);
|
|
34
|
+
// Letters the send loop hands over before it yields to the event loop once.
|
|
35
|
+
const BATCH = 8;
|
|
36
|
+
// The fields each kind of letter has (6); a name in `must` outside these makes the letter unsupported.
|
|
37
|
+
const fields = names => new Set(`v kind from to must ${names}`.split(' '));
|
|
38
|
+
const FIELDS = {
|
|
39
|
+
call: fields('id op input digest awaited parent confirmed'),
|
|
40
|
+
status: fields('call rev status asked asking progress'),
|
|
41
|
+
result: fields('call outcome result error progress'),
|
|
42
|
+
cancel: fields('call reason'),
|
|
43
|
+
answer: fields('id call asked answer'),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {{ from: string, id: string }} Ref
|
|
48
|
+
* @typedef {{ outcome: 'completed', result?: unknown } | { outcome: 'failed', error?: unknown } | { outcome: 'canceled' }} Outcome
|
|
49
|
+
* @typedef {{ from: string, id: string, to: string, op: string, input: unknown, awaited: boolean, parent: unknown }} Incoming
|
|
50
|
+
* @typedef {{ from: string, id: string, asked: number, answer: unknown }} Answer
|
|
51
|
+
* @typedef {{ accept: (call: Incoming) => Outcome | undefined, cancel?: (run: object, reason: unknown) => void,
|
|
52
|
+
* answer?: (run: object, answer: Answer) => void }} Handlers
|
|
53
|
+
* @typedef {{ status?: (call: object) => void, result?: (call: object) => void, late?: (call: object) => void }} ResultHandlers
|
|
54
|
+
* @typedef {{ ok: true, view?: object, applied?: boolean } | { ok: false, code: string, error?: { code: string, message: string } }} Receipt
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {{
|
|
59
|
+
* db: import('node:sqlite').DatabaseSync,
|
|
60
|
+
* now: () => number,
|
|
61
|
+
* send: (to: string, envelope: object) => Promise<Receipt>,
|
|
62
|
+
* onError?: (error: unknown) => void,
|
|
63
|
+
* wake?: (due: number | null) => void,
|
|
64
|
+
* schedule?: (fn: () => void) => void,
|
|
65
|
+
* }} options
|
|
66
|
+
*/
|
|
67
|
+
export function createKernel(options) {
|
|
68
|
+
return new Kernel(options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class Kernel {
|
|
72
|
+
#db;
|
|
73
|
+
#now;
|
|
74
|
+
#send;
|
|
75
|
+
#onError;
|
|
76
|
+
#wake;
|
|
77
|
+
#schedule;
|
|
78
|
+
#tx;
|
|
79
|
+
/** @type {Map<string, import('node:sqlite').StatementSync>} */
|
|
80
|
+
#statements = new Map();
|
|
81
|
+
/** @type {Map<string, Map<string, Handlers>>} */
|
|
82
|
+
#handlers = new Map();
|
|
83
|
+
/** @type {Map<string, ResultHandlers>} */
|
|
84
|
+
#results = new Map();
|
|
85
|
+
#started = false;
|
|
86
|
+
#closed = false;
|
|
87
|
+
// The send loop's memory (R1): all of it may be lost, the outbox says what is still owed.
|
|
88
|
+
#running = false;
|
|
89
|
+
#again = false;
|
|
90
|
+
/** @type {Set<number>} letters, by `seq`, handed to send() and not answered yet */
|
|
91
|
+
#flying = new Set();
|
|
92
|
+
/** letters and inquiries handed to send() and not answered yet */
|
|
93
|
+
#out = 0;
|
|
94
|
+
/** @type {Set<number>} letters that wait for the next pump() */
|
|
95
|
+
#held = new Set();
|
|
96
|
+
/** @type {Map<string, Ref>} inquiries that got no receipt and go again on the next pump() (C5) */
|
|
97
|
+
#rereads = new Map();
|
|
98
|
+
/** @type {Array<() => void>} */
|
|
99
|
+
#idle = [];
|
|
100
|
+
/** @type {number | null} the earliest due time the host was last told of; null: it holds no timer (7, R1) */
|
|
101
|
+
#told = null;
|
|
102
|
+
/** @type {Array<{ ref: Ref, what: string, resolve: (call: object) => void, reject: (error: Error) => void }>} when()'s (R1) */
|
|
103
|
+
#waiters = [];
|
|
104
|
+
|
|
105
|
+
constructor({ db, now, send, onError = error => console.error(error), wake, schedule = setImmediate }) {
|
|
106
|
+
this.#db = db;
|
|
107
|
+
this.#now = now;
|
|
108
|
+
this.#send = send;
|
|
109
|
+
this.#onError = onError;
|
|
110
|
+
this.#wake = wake;
|
|
111
|
+
this.#schedule = schedule;
|
|
112
|
+
this.#tx = new Transactions(db, error => this.#onError(error), () => this.#committed());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @template T
|
|
117
|
+
* @param {() => T} fn
|
|
118
|
+
* @returns {T}
|
|
119
|
+
*/
|
|
120
|
+
transaction(fn) {
|
|
121
|
+
if (this.#closed) throw new Error('the kernel is closed');
|
|
122
|
+
return this.#tx.run(fn);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @param {() => void} fn */
|
|
126
|
+
afterCommit(fn) {
|
|
127
|
+
this.#tx.after(fn);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
migrate() {
|
|
131
|
+
this.#inside();
|
|
132
|
+
this.#db.exec(META);
|
|
133
|
+
const version = this.#q(SQL.version).get()?.value ?? 0;
|
|
134
|
+
// A database newer than this code was written by a later kernel: running on it, or writing the older version back,
|
|
135
|
+
// would break the later one.
|
|
136
|
+
if (version > MIGRATIONS.length) throw new Error(`the database is at schema version ${version}, newer than this kernel's ${MIGRATIONS.length}`);
|
|
137
|
+
if (version === MIGRATIONS.length) return;
|
|
138
|
+
for (const step of MIGRATIONS.slice(version)) this.#db.exec(step);
|
|
139
|
+
this.#q(SQL.setVersion).run(MIGRATIONS.length);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} address
|
|
144
|
+
* @param {string} op
|
|
145
|
+
* @param {Handlers} handlers
|
|
146
|
+
*/
|
|
147
|
+
handle(address, op, handlers) {
|
|
148
|
+
if (!this.#handlers.has(address)) this.#handlers.set(address, new Map());
|
|
149
|
+
this.#handlers.get(address).set(op, handlers);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} address
|
|
154
|
+
* @param {ResultHandlers} handlers
|
|
155
|
+
*/
|
|
156
|
+
results(address, handlers) {
|
|
157
|
+
this.#results.set(address, handlers);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {{ from: string, id: string, to: string, op: string, input?: unknown, lane?: string, by?: Ref,
|
|
162
|
+
* awaited?: boolean, parent?: unknown, memo?: unknown }} call
|
|
163
|
+
*/
|
|
164
|
+
call({ from, id, to, op, input = null, lane = null, by, awaited = false, parent = null, memo }) {
|
|
165
|
+
this.#inside();
|
|
166
|
+
const digest = digestOf([to, op, input, parent]);
|
|
167
|
+
const found = this.#found(from, id, digest);
|
|
168
|
+
if (found) return found;
|
|
169
|
+
awaited = this.#awaits(from, by, awaited);
|
|
170
|
+
this.#start({ from, id, to, op, digest, input, parent, lane, by, awaited, memo });
|
|
171
|
+
return defined({ from, id, to, op, state: 'sending', memo, by: by && { from: by.from, id: by.id }, awaited });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A call to make once `delayMs` has passed (7). Until then it is a timer, which shares its id with the calls.
|
|
176
|
+
* @param {{ from: string, id: string, delayMs: number, to: string, op: string, input?: unknown, lane?: string, by?: Ref,
|
|
177
|
+
* awaited?: boolean, parent?: unknown, memo?: unknown }} timer
|
|
178
|
+
*/
|
|
179
|
+
after({ from, id, delayMs, to, op, input = null, lane = null, by, awaited = false, parent = null, memo }) {
|
|
180
|
+
this.#inside();
|
|
181
|
+
if (!Number.isFinite(delayMs) || delayMs < 0) throw new Error(`delayMs must be a finite number not below 0, not ${delayMs}`);
|
|
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
|
+
this.#q(SQL.addTimer).run(from, id, to, op, digest, json(input), json(parent), lane, by?.from ?? null, by?.id ?? null, awaited ? 1 : 0,
|
|
187
|
+
json(memo), this.#now() + delayMs);
|
|
188
|
+
return timerView(this.#q(SQL.timer).get(from, id));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Takes back a timer not yet due (7). A timer that has become a call is left alone.
|
|
193
|
+
* @param {Ref} timer
|
|
194
|
+
*/
|
|
195
|
+
unschedule(timer) {
|
|
196
|
+
this.#inside();
|
|
197
|
+
if (!this.#unschedule(timer)) this.#made(timer);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Turns every timer that has come due into a call, in one transaction that calls no handler (7). */
|
|
201
|
+
tick() {
|
|
202
|
+
this.#outside();
|
|
203
|
+
const due = this.#q(SQL.dueTimers).all(this.#now());
|
|
204
|
+
if (due.length > 0) {
|
|
205
|
+
this.transaction(() => {
|
|
206
|
+
for (const row of due) {
|
|
207
|
+
this.#q(SQL.dropTimer).run(row.from_addr, row.id);
|
|
208
|
+
this.#start({ from: row.from_addr, id: row.id, to: row.to_addr, op: row.op, digest: row.digest, input: parse(row.input), parent: parse(row.parent),
|
|
209
|
+
lane: row.lane, by: byOf(row), awaited: row.awaited === 1, memo: parse(row.memo) });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// The host's timer has just gone off, and its clock may run a little early: while timers are left, it hears again.
|
|
214
|
+
if (!this.#started || !this.#wake) return;
|
|
215
|
+
const next = this.#q(SQL.firstDue).get().due;
|
|
216
|
+
if (next !== null) this.#tell(next);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A run reports how it is going (C6).
|
|
221
|
+
* @param {Ref} run
|
|
222
|
+
* @param {{ status?: 'working' | 'input-required', asking?: unknown, progress?: unknown }} change
|
|
223
|
+
* what is not given stays as it was; `asking` only means something while the run is `input-required`
|
|
224
|
+
*/
|
|
225
|
+
update(run, { status, asking, progress }) {
|
|
226
|
+
this.#inside();
|
|
227
|
+
const row = this.#open(run);
|
|
228
|
+
const next = status ?? row.status;
|
|
229
|
+
if (!WORKING.has(next)) throw new Error(`a run cannot be updated to ${next}`);
|
|
230
|
+
const question = next === 'input-required' ? (asking === undefined ? row.asking : json(asking)) : null;
|
|
231
|
+
const asks = next === 'input-required' && (row.status !== 'input-required' || question !== row.asking);
|
|
232
|
+
const now = this.#q(SQL.updateRun).get(next, row.asked + (asks ? 1 : 0), question, progress === undefined ? row.progress : json(progress),
|
|
233
|
+
row.from_addr, row.id);
|
|
234
|
+
if (now.root === 1) return;
|
|
235
|
+
// The newest status replaces one not yet confirmed, in its place (R3).
|
|
236
|
+
const old = this.#q(SQL.dropStatusLetter).get(now.from_addr, now.id);
|
|
237
|
+
this.#owe(now.from_addr, null, 'status', { from: now.from_addr, id: now.id },
|
|
238
|
+
{ v: 1, kind: 'status', from: now.to_addr, to: now.from_addr, call: { from: now.from_addr, id: now.id }, rev: now.rev,
|
|
239
|
+
status: now.status, asked: now.asked, asking: parse(now.asking), progress: parse(now.progress) }, old?.pos);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {Ref} run
|
|
244
|
+
* @param {Outcome} outcome
|
|
245
|
+
* @param {{ progress?: unknown }} [options] without it, the run's last progress stays
|
|
246
|
+
*/
|
|
247
|
+
settle(run, outcome, { progress } = {}) {
|
|
248
|
+
this.#inside();
|
|
249
|
+
if (!ENDED.has(outcome.outcome)) throw new Error(`${outcome.outcome} is not an outcome`);
|
|
250
|
+
if (outcome.outcome === 'failed') outcome = { ...outcome, error: written(outcome.error) };
|
|
251
|
+
const row = this.#open(run);
|
|
252
|
+
if (this.#q(SQL.awaiting).get(row.from_addr, row.id) || this.#q(SQL.awaitedTimer).get(row.from_addr, row.id))
|
|
253
|
+
throw new Error(`run ${run.from}/${run.id} still awaits a call it made`);
|
|
254
|
+
this.#end(row.from_addr, row.id, outcome, progress);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Starts a run that nobody called (2). Its status and outcome go to no one.
|
|
259
|
+
* @param {{ address: string, op: string, id: string, parent?: unknown }} run
|
|
260
|
+
*/
|
|
261
|
+
root({ address, op, id, parent = null }) {
|
|
262
|
+
this.#inside();
|
|
263
|
+
this.#q(SQL.addRun).run(address, id, address, op, null, json(parent), 0, 1);
|
|
264
|
+
return runView(this.#q(SQL.run).get(address, id));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* A handler stops a run of its own, a root run say: as if its caller had canceled it, without calling `cancel` (C7).
|
|
269
|
+
* @param {Ref} run
|
|
270
|
+
*/
|
|
271
|
+
stop(run) {
|
|
272
|
+
this.#inside();
|
|
273
|
+
const row = this.#q(SQL.run).get(run.from, run.id);
|
|
274
|
+
if (!row) throw new Error(`there is no run ${run.from}/${run.id}`);
|
|
275
|
+
this.#halt(row);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The runs a handler took and that have not ended, root runs included: what a host scans again on start (T4).
|
|
280
|
+
* @param {{ address: string, status?: string }} filter
|
|
281
|
+
*/
|
|
282
|
+
runs({ address, status = null }) {
|
|
283
|
+
return this.#q(SQL.runs).all(address, status).map(runView);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The calls an address made that have not ended (T4).
|
|
288
|
+
* @param {{ address: string, state?: string }} filter
|
|
289
|
+
*/
|
|
290
|
+
calls({ address, state = null }) {
|
|
291
|
+
return this.#q(SQL.calls).all(address, state).map(callView);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* What an address has not finished (8): its calls and runs, its timers not yet due, and the letters it owes, in the
|
|
296
|
+
* order they go.
|
|
297
|
+
* @param {{ address: string }} filter
|
|
298
|
+
*/
|
|
299
|
+
pending({ address }) {
|
|
300
|
+
return {
|
|
301
|
+
calls: this.calls({ address }),
|
|
302
|
+
runs: this.runs({ address }),
|
|
303
|
+
timers: this.#q(SQL.timers).all(address).map(timerView),
|
|
304
|
+
letters: this.#q(SQL.letters).all(address).map(row => ({ kind: row.kind, to: row.to_addr, call: { from: row.call_from, id: row.call_id } })),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Waits until a call has left `sending`, or has ended (8); a timer is waited for until it is a call. Only memory
|
|
310
|
+
* holds the wait: after close() or a crash it never ends (R1).
|
|
311
|
+
* @param {Ref} ref
|
|
312
|
+
* @param {'accepted' | 'ended'} what
|
|
313
|
+
* @returns {Promise<object>} the call, as call() returns it
|
|
314
|
+
*/
|
|
315
|
+
when(ref, what) {
|
|
316
|
+
this.#outside();
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const waiter = { ref: { from: ref.from, id: ref.id }, what, resolve, reject };
|
|
319
|
+
if (!this.#met(waiter)) this.#waiters.push(waiter);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The caller no longer waits for this call (C9). The callee is not told; its outcome still comes to `result`.
|
|
325
|
+
* @param {Ref} call
|
|
326
|
+
*/
|
|
327
|
+
detach(call) {
|
|
328
|
+
this.#inside();
|
|
329
|
+
if (this.#q(SQL.detachTimer).run(call.from, call.id).changes > 0) return;
|
|
330
|
+
// An ended call no longer changes (C3).
|
|
331
|
+
if (!ENDED.has(this.#made(call).state)) this.#q(SQL.detach).run(call.from, call.id);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The caller asks the callee to cancel the call's run (C7). How and when the run ends is the callee's; its outcome
|
|
336
|
+
* still comes. A call that has ended is not canceled.
|
|
337
|
+
* @param {Ref} call
|
|
338
|
+
* @param {unknown} [reason] goes to the callee's `cancel` as it is
|
|
339
|
+
*/
|
|
340
|
+
cancel(call, reason) {
|
|
341
|
+
this.#inside();
|
|
342
|
+
if (this.#unschedule(call)) return;
|
|
343
|
+
const row = this.#made(call);
|
|
344
|
+
if (!ENDED.has(row.state)) this.#recall(row, reason);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The caller gives up on a call: it ends failed here, without a call to `result`, and the callee is asked to cancel
|
|
349
|
+
* (C12). The callee's own outcome, when it comes, goes to `late`.
|
|
350
|
+
* @param {Ref} call
|
|
351
|
+
*/
|
|
352
|
+
abandon(call) {
|
|
353
|
+
this.#inside();
|
|
354
|
+
if (this.#unschedule(call)) return;
|
|
355
|
+
const row = this.#made(call);
|
|
356
|
+
if (ENDED.has(row.state)) return;
|
|
357
|
+
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);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Sends an open call again with `confirmed`, to learn how its run is now (C5). What it learns goes to the result
|
|
363
|
+
* handlers; a call that is not open is left alone.
|
|
364
|
+
* @param {Ref} call
|
|
365
|
+
*/
|
|
366
|
+
inquire(call) {
|
|
367
|
+
this.#outside();
|
|
368
|
+
// A timer not yet due has no run to ask about.
|
|
369
|
+
if (this.#q(SQL.timer).get(call.from, call.id)) return;
|
|
370
|
+
const row = this.#made(call);
|
|
371
|
+
if (row.state === 'open') this.#ask(row);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Answers a run waiting for input (C8). Nothing of it is kept here: it goes once, and the answerer decides whether to
|
|
376
|
+
* send it again under the same id.
|
|
377
|
+
* @param {{ from: string, id: string, to: string, call: Ref, asked: number, answer?: unknown }} answer
|
|
378
|
+
* @returns {Promise<Receipt>} rejected when send() is
|
|
379
|
+
*/
|
|
380
|
+
answer({ from, id, to, call, asked, answer }) {
|
|
381
|
+
this.#outside();
|
|
382
|
+
const envelope = { v: 1, kind: 'answer', from, to, id, call: { from: call.from, id: call.id }, asked, answer };
|
|
383
|
+
return new Promise(resolve => resolve(this.#local(to) ? this.receive(envelope) : this.#send(to, envelope)));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {object} envelope
|
|
388
|
+
* @returns {Receipt}
|
|
389
|
+
*/
|
|
390
|
+
receive(envelope) {
|
|
391
|
+
this.#outside();
|
|
392
|
+
if (!this.#started) return { ok: false, code: 'not_ready' };
|
|
393
|
+
const known = envelope.v === 1 ? FIELDS[envelope.kind] : undefined;
|
|
394
|
+
if (!known || envelope.must?.some(name => !known.has(name))) return { ok: false, code: 'unsupported' };
|
|
395
|
+
return this.transaction(() => this.#take(envelope));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pump() {
|
|
399
|
+
this.#outside();
|
|
400
|
+
this.#held.clear();
|
|
401
|
+
// One transaction per owed result: one transaction calls the functions of one address only.
|
|
402
|
+
for (const { from_addr, id } of this.#q(SQL.owedCalls).all()) this.transaction(() => this.#notify(from_addr, id));
|
|
403
|
+
const rereads = [...this.#rereads.values()];
|
|
404
|
+
this.#rereads.clear();
|
|
405
|
+
for (const call of rereads) this.inquire(call);
|
|
406
|
+
this.#kick();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
recover() {
|
|
410
|
+
this.#outside();
|
|
411
|
+
this.#started = true;
|
|
412
|
+
this.pump();
|
|
413
|
+
for (const row of this.#q(SQL.openCalls).all()) this.#ask(row);
|
|
414
|
+
this.tick();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @returns {Promise<void>} */
|
|
418
|
+
idle() {
|
|
419
|
+
if (!this.#running && this.#out === 0) return Promise.resolve();
|
|
420
|
+
return new Promise(resolve => this.#idle.push(resolve));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
close() {
|
|
424
|
+
this.#closed = true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#inside() {
|
|
428
|
+
if (!this.#tx.open) throw new Error('this must be called inside kernel.transaction()');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#outside() {
|
|
432
|
+
if (this.#closed) throw new Error('the kernel is closed');
|
|
433
|
+
if (this.#tx.open) throw new Error('this opens its own transaction and must not be called inside one');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* What a request id already names, a call or a timer, if anything: ids are shared between the two (C1, 7). Throws
|
|
438
|
+
* when it was made with other content.
|
|
439
|
+
* @param {string} from
|
|
440
|
+
* @param {string} id
|
|
441
|
+
* @param {string} digest
|
|
442
|
+
*/
|
|
443
|
+
#found(from, id, digest) {
|
|
444
|
+
const call = this.#q(SQL.call).get(from, id);
|
|
445
|
+
const found = call ?? this.#q(SQL.timer).get(from, id);
|
|
446
|
+
if (!found) return undefined;
|
|
447
|
+
if (found.digest !== digest) throw new Error(`call ${from}/${id} already exists with other content`);
|
|
448
|
+
return call ? callView(call) : timerView(found);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Whether a call or a timer that run `by` makes is awaited (C9). `by` must be a run of `from`.
|
|
453
|
+
* @param {string} from
|
|
454
|
+
* @param {Ref | undefined} by
|
|
455
|
+
* @param {boolean} awaited
|
|
456
|
+
*/
|
|
457
|
+
#awaits(from, by, awaited) {
|
|
458
|
+
if (!by) return awaited;
|
|
459
|
+
const run = this.#q(SQL.run).get(by.from, by.id);
|
|
460
|
+
if (run?.to_addr !== from) throw new Error(`there is no run ${by.from}/${by.id} at ${from}`);
|
|
461
|
+
// A run asked to cancel no longer waits for what it starts (C9).
|
|
462
|
+
if (run.cancel_asked === 1) return false;
|
|
463
|
+
// An ended run awaiting a call would break C9.
|
|
464
|
+
if (awaited && ENDED.has(run.status)) throw new Error(`run ${by.from}/${by.id} has already ended and cannot await a call`);
|
|
465
|
+
return awaited;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Writes a call and owes its letter (5.1): for call(), and for a timer that has come due.
|
|
469
|
+
#start({ from, id, to, op, digest, input, parent, lane, by, awaited, memo }) {
|
|
470
|
+
this.#q(SQL.addCall).run(from, id, to, op, digest, json(input), json(parent), lane, by?.from ?? null, by?.id ?? null, awaited ? 1 : 0, json(memo));
|
|
471
|
+
this.#owe(to, lane, 'call', { from, id }, { v: 1, kind: 'call', from, to, id, op, input, digest, awaited, parent });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Takes back a timer not yet due, without a trace (7); false when there is none.
|
|
476
|
+
* @param {Ref} timer
|
|
477
|
+
*/
|
|
478
|
+
#unschedule(timer) {
|
|
479
|
+
return this.#q(SQL.dropTimer).run(timer.from, timer.id).changes > 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* A call this instance made.
|
|
484
|
+
* @param {Ref} call
|
|
485
|
+
*/
|
|
486
|
+
#made(call) {
|
|
487
|
+
const row = this.#q(SQL.call).get(call.from, call.id);
|
|
488
|
+
if (!row) throw new Error(`there is no call ${call.from}/${call.id}`);
|
|
489
|
+
return row;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* A run that may still change (C3).
|
|
494
|
+
* @param {Ref} run
|
|
495
|
+
*/
|
|
496
|
+
#open(run) {
|
|
497
|
+
const row = this.#q(SQL.run).get(run.from, run.id);
|
|
498
|
+
if (!row) throw new Error(`there is no run ${run.from}/${run.id}`);
|
|
499
|
+
if (ENDED.has(row.status)) throw new Error(`run ${run.from}/${run.id} has already ended`);
|
|
500
|
+
return row;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** @param {string} sql */
|
|
504
|
+
#q(sql) {
|
|
505
|
+
let statement = this.#statements.get(sql);
|
|
506
|
+
if (!statement) this.#statements.set(sql, statement = this.#db.prepare(sql));
|
|
507
|
+
return statement;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** @param {string} address */
|
|
511
|
+
#local(address) {
|
|
512
|
+
return this.#handlers.has(address) || this.#results.has(address);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {string} to
|
|
517
|
+
* @param {string | null} lane
|
|
518
|
+
* @param {'call' | 'status' | 'result' | 'cancel'} kind
|
|
519
|
+
* @param {Ref} call
|
|
520
|
+
* @param {object} envelope
|
|
521
|
+
* @param {number} [pos] the place of the letter this one replaces; a new letter goes last
|
|
522
|
+
*/
|
|
523
|
+
#owe(to, lane, kind, call, envelope, pos = null) {
|
|
524
|
+
this.#q(SQL.owe).run(pos, envelope.from, to, lane, kind, call.from, call.id, JSON.stringify(envelope));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** @param {object} envelope of a kind in FIELDS */
|
|
528
|
+
#take(envelope) {
|
|
529
|
+
switch (envelope.kind) {
|
|
530
|
+
case 'call':
|
|
531
|
+
return this.#accept(envelope);
|
|
532
|
+
case 'cancel':
|
|
533
|
+
return this.#revoke(envelope);
|
|
534
|
+
case 'status':
|
|
535
|
+
return this.#hear(envelope.call, envelope.from, envelope);
|
|
536
|
+
case 'result':
|
|
537
|
+
return this.#conclude(envelope.call, envelope.from, envelope);
|
|
538
|
+
case 'answer':
|
|
539
|
+
return this.#reply(envelope);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// The callee's side of a call letter (5.2, C1).
|
|
544
|
+
#accept({ from, id, to, op, input, digest, awaited, parent, confirmed }) {
|
|
545
|
+
const found = this.#q(SQL.run).get(from, id);
|
|
546
|
+
// A record that a cancel left alone has no digest to compare (C1, C7).
|
|
547
|
+
if (found) return found.digest === digest || found.op === null ? { ok: true, view: view(found) } : { ok: false, code: 'conflict' };
|
|
548
|
+
// A confirmed call was taken once; with no record of it here, it is not run now (C5).
|
|
549
|
+
if (confirmed) return { ok: false, code: 'unknown_call' };
|
|
550
|
+
this.#q(SQL.addRun).run(from, id, to, op, digest, json(parent), awaited ? 1 : 0, 0);
|
|
551
|
+
const handlers = this.#handlers.get(to)?.get(op);
|
|
552
|
+
if (!handlers) this.#end(from, id, failure('no_handler', `nothing handles ${op} at ${to}`));
|
|
553
|
+
else {
|
|
554
|
+
try {
|
|
555
|
+
this.transaction(() => {
|
|
556
|
+
const outcome = handlers.accept({ from, id, to, op, input, awaited, parent });
|
|
557
|
+
if (outcome) this.settle({ from, id }, outcome);
|
|
558
|
+
});
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error instanceof Busy) {
|
|
561
|
+
this.#q(SQL.dropRun).run(from, id);
|
|
562
|
+
return { ok: false, code: 'busy' };
|
|
563
|
+
}
|
|
564
|
+
if (error instanceof Rejected) this.#end(from, id, failure(error.code, error.message));
|
|
565
|
+
else {
|
|
566
|
+
this.#tx.after(() => this.#onError(error));
|
|
567
|
+
this.#end(from, id, failure('handler_error', error?.message));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return { ok: true, view: view(this.#q(SQL.run).get(from, id)) };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 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
|
+
#end(from, id, { outcome, result, error }, progress) {
|
|
576
|
+
const row = this.#q(SQL.endRun).get(outcome, json(result), json(error), json(progress), from, id);
|
|
577
|
+
if (row.root === 1) return;
|
|
578
|
+
this.#q(SQL.dropStatusLetter).get(from, id);
|
|
579
|
+
this.#owe(from, null, 'result', { from, id },
|
|
580
|
+
{ v: 1, kind: 'result', from: row.to_addr, to: from, call: { from, id }, outcome, result, error, progress: parse(row.progress) });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// The callee's side of a cancel letter (C7).
|
|
584
|
+
#revoke({ from, to, call, reason }) {
|
|
585
|
+
if (from !== call.from) {
|
|
586
|
+
this.#tx.after(() => this.#onError(new Error(`${from} sent a cancel for call ${call.from}/${call.id}, which ${call.from} made`)));
|
|
587
|
+
return { ok: true };
|
|
588
|
+
}
|
|
589
|
+
const row = this.#q(SQL.run).get(call.from, call.id);
|
|
590
|
+
if (!row) {
|
|
591
|
+
// The cancel came before its call: this record, without op, parent or digest, answers the call when it comes.
|
|
592
|
+
this.#q(SQL.addRun).run(call.from, call.id, to, null, null, null, 0, 0);
|
|
593
|
+
this.#end(call.from, call.id, { outcome: 'canceled' });
|
|
594
|
+
} else if (this.#halt(row, reason)) {
|
|
595
|
+
// T6: a cancel function that throws loses its own changes only, and is not called again.
|
|
596
|
+
try {
|
|
597
|
+
this.transaction(() => this.#handlers.get(row.to_addr)?.get(row.op)?.cancel?.(runView(this.#q(SQL.run).get(call.from, call.id)), reason));
|
|
598
|
+
} catch (error) {
|
|
599
|
+
this.#tx.after(() => this.#onError(error));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { ok: true, view: view(this.#q(SQL.run).get(call.from, call.id)) };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// The callee's side of an answer (C8): taken at most once per question, and per number.
|
|
606
|
+
#reply({ from, to, id, call, asked, answer }) {
|
|
607
|
+
const row = this.#q(SQL.run).get(call.from, call.id);
|
|
608
|
+
if (row?.to_addr !== to) return { ok: false, code: 'unknown_call' };
|
|
609
|
+
if (this.#q(SQL.answer).get(from, id)) return { ok: true, applied: true, view: view(row) };
|
|
610
|
+
if (row.status !== 'input-required' || asked !== row.asked) return { ok: true, applied: false, view: view(row) };
|
|
611
|
+
const handlers = this.#handlers.get(row.to_addr)?.get(row.op);
|
|
612
|
+
if (!handlers?.answer) return { ok: false, code: 'rejected', error: { code: 'no_handler', message: `nothing answers ${row.op} at ${row.to_addr}` } };
|
|
613
|
+
try {
|
|
614
|
+
// The run is working again before its handler sees the answer; whatever the handler throws, the question stands.
|
|
615
|
+
this.transaction(() => {
|
|
616
|
+
this.update(call, { status: 'working' });
|
|
617
|
+
this.#q(SQL.addAnswer).run(from, id, call.from, call.id, asked);
|
|
618
|
+
handlers.answer(runView(this.#q(SQL.run).get(call.from, call.id)), { from, id, asked, answer });
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error instanceof Rejected) return { ok: false, code: 'rejected', error: { code: error.code, message: error.message } };
|
|
622
|
+
this.#tx.after(() => this.#onError(error));
|
|
623
|
+
return { ok: false, code: 'handler_error' };
|
|
624
|
+
}
|
|
625
|
+
return { ok: true, applied: true, view: view(this.#q(SQL.run).get(call.from, call.id)) };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Marks a run asked to cancel, drops its timers and cancels the calls it awaits (C7, 7). False when the run has ended
|
|
630
|
+
* or was asked before.
|
|
631
|
+
* @param {object} row
|
|
632
|
+
* @param {unknown} [reason]
|
|
633
|
+
*/
|
|
634
|
+
#halt(row, reason) {
|
|
635
|
+
if (ENDED.has(row.status) || row.cancel_asked === 1) return false;
|
|
636
|
+
this.#q(SQL.haltRun).run(row.from_addr, row.id);
|
|
637
|
+
this.#q(SQL.dropRunTimers).run(row.from_addr, row.id);
|
|
638
|
+
for (const call of this.#q(SQL.awaiting).all(row.from_addr, row.id)) this.#recall(call, reason);
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Owes the callee a cancel (C7): in the place of a call letter not yet confirmed, holding its lane, or else outside
|
|
644
|
+
* any lane. A cancel already owed is the one that goes.
|
|
645
|
+
* @param {object} row the call, not ended
|
|
646
|
+
* @param {unknown} [reason]
|
|
647
|
+
*/
|
|
648
|
+
#recall(row, reason) {
|
|
649
|
+
if (this.#q(SQL.cancelLetter).get(row.from_addr, row.id)) return;
|
|
650
|
+
const call = { from: row.from_addr, id: row.id };
|
|
651
|
+
const letter = this.#q(SQL.dropCallLetter).get(row.from_addr, row.id);
|
|
652
|
+
this.#owe(row.to_addr, letter ? row.lane : null, 'cancel', call, { v: 1, kind: 'cancel', from: row.from_addr, to: row.to_addr, call, reason },
|
|
653
|
+
letter?.pos);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The caller hears a status of the run its call started (C6).
|
|
658
|
+
* @param {Ref} call
|
|
659
|
+
* @param {string} sender
|
|
660
|
+
* @param {{ rev: number, status: string, asked: number, asking?: unknown, progress?: unknown }} status
|
|
661
|
+
*/
|
|
662
|
+
#hear(call, sender, { rev, status, asked, asking, progress }) {
|
|
663
|
+
const row = this.#known(call, sender, 'a status');
|
|
664
|
+
if (!row || ENDED.has(row.state) || rev <= row.rev) return { ok: true };
|
|
665
|
+
if (row.state === 'sending') this.#q(SQL.dropCallLetter).run(call.from, call.id);
|
|
666
|
+
const known = WORKING.has(status) ? status : 'working';
|
|
667
|
+
const now = this.#q(SQL.hearCall).get(known, rev, asked, known === 'input-required' ? json(asking) : null, json(progress), call.from, call.id);
|
|
668
|
+
// T6: a status handler that throws loses its own changes only, and is not called again.
|
|
669
|
+
try {
|
|
670
|
+
this.transaction(() => this.#results.get(call.from)?.status?.(callView(now)));
|
|
671
|
+
} catch (error) {
|
|
672
|
+
this.#tx.after(() => this.#onError(error));
|
|
673
|
+
}
|
|
674
|
+
return { ok: true };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* The caller's row of a call that a status or an outcome is about, if it came from the call's callee (C4).
|
|
679
|
+
* @param {Ref} call
|
|
680
|
+
* @param {string} sender
|
|
681
|
+
* @param {string} what
|
|
682
|
+
*/
|
|
683
|
+
#known(call, sender, what) {
|
|
684
|
+
const row = this.#q(SQL.call).get(call.from, call.id);
|
|
685
|
+
if (!row) {
|
|
686
|
+
this.#tx.after(() => this.#onError(new Error(`${what} arrived for unknown call ${call.from}/${call.id}`)));
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
if (sender !== row.to_addr) {
|
|
690
|
+
this.#tx.after(() => this.#onError(new Error(`${sender} sent ${what} for call ${call.from}/${call.id}, which went to ${row.to_addr}`)));
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
return row;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* The caller learns an outcome, from a result letter or from a receipt that carries one (C4).
|
|
698
|
+
* @param {Ref} call
|
|
699
|
+
* @param {string} sender the address the outcome came from
|
|
700
|
+
* @param {{ outcome: string, result?: unknown, error?: unknown, progress?: unknown }} ending
|
|
701
|
+
* @param {boolean} [local] the caller's kernel wrote this outcome itself
|
|
702
|
+
*/
|
|
703
|
+
#conclude(call, sender, ending, local = false) {
|
|
704
|
+
const row = this.#known(call, sender, 'an outcome');
|
|
705
|
+
// After an abandon, the callee's own outcome is still taken once, as the late one (C12).
|
|
706
|
+
const late = row?.abandoned === 1 && row.late === null && !local;
|
|
707
|
+
if (!row || (ENDED.has(row.state) && !late)) return { ok: true };
|
|
708
|
+
let { outcome, result, error, progress } = ending;
|
|
709
|
+
if (!ENDED.has(outcome)) {
|
|
710
|
+
error = { code: 'unknown_outcome', effects: 'unknown', original: { outcome, result, error } };
|
|
711
|
+
outcome = 'failed';
|
|
712
|
+
result = undefined;
|
|
713
|
+
local = true;
|
|
714
|
+
} else if (outcome === 'failed' && !local) error = heard(error);
|
|
715
|
+
if (late) this.#q(SQL.lateCall).run(JSON.stringify({ outcome, result, error, progress }), call.from, call.id);
|
|
716
|
+
else {
|
|
717
|
+
// Hearing the outcome means the callee took the call: its letter goes, and its lane moves on. A cancel not yet
|
|
718
|
+
// confirmed goes too; an abandoned call keeps its cancel until confirmed.
|
|
719
|
+
if (row.state === 'sending') this.#q(SQL.dropCallLetter).run(call.from, call.id);
|
|
720
|
+
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);
|
|
722
|
+
}
|
|
723
|
+
this.#notify(call.from, call.id);
|
|
724
|
+
return { ok: true };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Hands an ended call to its result handler, or an abandoned one's late outcome to `late`. Until that commits, the call
|
|
728
|
+
// stays owed and pump() tries again (C4, C12, T6).
|
|
729
|
+
#notify(from, id) {
|
|
730
|
+
try {
|
|
731
|
+
this.transaction(() => {
|
|
732
|
+
const row = this.#q(SQL.call).get(from, id);
|
|
733
|
+
this.#results.get(from)?.[row.late === null ? 'result' : 'late']?.(callView(row));
|
|
734
|
+
this.#q(SQL.paidCall).run(from, id);
|
|
735
|
+
});
|
|
736
|
+
} catch (error) {
|
|
737
|
+
this.#tx.after(() => this.#onError(error));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* What a receipt means for the letter it answers, in the sender's transaction.
|
|
743
|
+
* @param {object} row
|
|
744
|
+
* @param {Receipt} receipt
|
|
745
|
+
* @param {boolean} [direct] the letter was delivered locally, in this same transaction
|
|
746
|
+
*/
|
|
747
|
+
#heard(row, receipt, direct = false) {
|
|
748
|
+
this.#q(SQL.dropLetter).run(row.seq);
|
|
749
|
+
const call = { from: row.call_from, id: row.call_id };
|
|
750
|
+
if (!receipt.ok) {
|
|
751
|
+
if (row.kind === 'call') this.#conclude(call, row.to_addr, failure(receipt.code, `${row.to_addr} answered ${receipt.code}`), true);
|
|
752
|
+
// A cancel in the place of a call letter not yet confirmed: that letter may have arrived all the same (C7).
|
|
753
|
+
else if (row.kind === 'cancel' && this.#q(SQL.call).get(call.from, call.id).state === 'sending')
|
|
754
|
+
this.#conclude(call, row.to_addr, failure(receipt.code, `${row.to_addr} answered ${receipt.code} to a cancel`, 'unknown'), true);
|
|
755
|
+
else this.#tx.after(() => this.#onError(new Error(`${row.to_addr} refused a ${row.kind} letter: ${receipt.code}`)));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (row.kind !== 'call' && row.kind !== 'cancel') return;
|
|
759
|
+
// A local callee has just run its own functions in this transaction; its outcome and its status come in their own
|
|
760
|
+
// letters (4, C6).
|
|
761
|
+
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
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Sends an open call again with `confirmed` (C5). It is not kept in the outbox: without a receipt it waits in memory
|
|
770
|
+
* for the next pump(). One to a local address is taken at once; there the callee runs no function of its own.
|
|
771
|
+
* @param {object} row
|
|
772
|
+
*/
|
|
773
|
+
#ask(row) {
|
|
774
|
+
const call = { from: row.from_addr, id: row.id };
|
|
775
|
+
const envelope = { v: 1, kind: 'call', from: row.from_addr, to: row.to_addr, id: row.id, op: row.op, input: parse(row.input),
|
|
776
|
+
digest: row.digest, awaited: row.awaited === 1, parent: parse(row.parent), confirmed: true };
|
|
777
|
+
if (this.#local(row.to_addr)) {
|
|
778
|
+
this.transaction(() => this.#reread(call, row.to_addr, this.#take(envelope)));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
this.#hand(row.to_addr, envelope, receipt => {
|
|
782
|
+
if (receipt === undefined || HOLD.has(receipt.code)) this.#rereads.set(JSON.stringify([call.from, call.id]), call);
|
|
783
|
+
else {
|
|
784
|
+
try {
|
|
785
|
+
this.transaction(() => this.#reread(call, row.to_addr, receipt));
|
|
786
|
+
} catch (error) {
|
|
787
|
+
this.#rereads.set(JSON.stringify([call.from, call.id]), call);
|
|
788
|
+
this.#onError(error);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* What the receipt of an inquiry means for the call (C5): an outcome, a newer status, or that the callee has no record.
|
|
796
|
+
* @param {Ref} call
|
|
797
|
+
* @param {string} to
|
|
798
|
+
* @param {Receipt} receipt
|
|
799
|
+
*/
|
|
800
|
+
#reread(call, to, receipt) {
|
|
801
|
+
if (!receipt.ok) {
|
|
802
|
+
if (receipt.code === 'unknown_call') {
|
|
803
|
+
this.#conclude(call, to, { outcome: 'failed', error: { code: 'lost', message: `${to} has no record of this call`, effects: 'unknown' } }, true);
|
|
804
|
+
} else this.#tx.after(() => this.#onError(new Error(`${to} answered ${receipt.code} to an inquiry about ${call.from}/${call.id}`)));
|
|
805
|
+
} else if (receipt.view.outcome) this.#conclude(call, to, receipt.view);
|
|
806
|
+
else this.#hear(call, to, receipt.view);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// After every outermost commit: the send loop goes on, the waiters of when() look again, and the host hears of a new
|
|
810
|
+
// earliest timer (T3, 8, 7).
|
|
811
|
+
#committed() {
|
|
812
|
+
this.#kick();
|
|
813
|
+
this.#waiters = this.#waiters.filter(waiter => !this.#met(waiter));
|
|
814
|
+
if (!this.#started || !this.#wake) return;
|
|
815
|
+
const due = this.#q(SQL.firstDue).get().due;
|
|
816
|
+
if (due !== this.#told) this.#tell(due);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Ends a wait of when() if it can: true when it is done with.
|
|
821
|
+
* @param {{ ref: Ref, what: string, resolve: (call: object) => void, reject: (error: Error) => void }} waiter
|
|
822
|
+
*/
|
|
823
|
+
#met({ ref, what, resolve, reject }) {
|
|
824
|
+
const row = this.#q(SQL.call).get(ref.from, ref.id);
|
|
825
|
+
if (row) {
|
|
826
|
+
if (what === 'accepted' ? row.state === 'sending' : !ENDED.has(row.state)) return false;
|
|
827
|
+
resolve(callView(row));
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
// A timer is still to become the call; one taken back never will.
|
|
831
|
+
if (this.#q(SQL.timer).get(ref.from, ref.id)) return false;
|
|
832
|
+
reject(new Error(`there is no call ${ref.from}/${ref.id}`));
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** @param {number | null} due */
|
|
837
|
+
#tell(due) {
|
|
838
|
+
this.#told = due;
|
|
839
|
+
try {
|
|
840
|
+
this.#wake(due);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
this.#onError(error);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
#kick() {
|
|
847
|
+
if (!this.#started || this.#closed) return;
|
|
848
|
+
if (this.#running) {
|
|
849
|
+
this.#again = true;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
this.#running = true;
|
|
853
|
+
queueMicrotask(() => this.#pass().catch(error => this.#onError(error)));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// One walk over the outbox in order; walks again when something committed meanwhile.
|
|
857
|
+
async #pass() {
|
|
858
|
+
let after = 0;
|
|
859
|
+
let handed = 0;
|
|
860
|
+
try {
|
|
861
|
+
for (;;) {
|
|
862
|
+
if (this.#closed) return;
|
|
863
|
+
const row = this.#q(SQL.next).get(after);
|
|
864
|
+
if (!row) {
|
|
865
|
+
if (!this.#again) break;
|
|
866
|
+
this.#again = false;
|
|
867
|
+
after = 0;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
after = row.pos;
|
|
871
|
+
if (this.#flying.has(row.seq) || this.#held.has(row.seq)) continue;
|
|
872
|
+
if (this.#local(row.to_addr)) this.#deliver(row);
|
|
873
|
+
else this.#post(row);
|
|
874
|
+
if (++handed % BATCH === 0) await new Promise(resolve => this.#schedule(resolve));
|
|
875
|
+
}
|
|
876
|
+
} finally {
|
|
877
|
+
// Whatever ended this walk, the next commit or pump() starts another.
|
|
878
|
+
this.#running = false;
|
|
879
|
+
this.#settled();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// A letter to a local address: take it, drop it from the outbox and apply the receipt, in one transaction.
|
|
884
|
+
#deliver(row) {
|
|
885
|
+
try {
|
|
886
|
+
const receipt = this.transaction(() => {
|
|
887
|
+
const receipt = this.#take(JSON.parse(row.envelope));
|
|
888
|
+
if (!HOLD.has(receipt.code)) this.#heard(row, receipt, true);
|
|
889
|
+
return receipt;
|
|
890
|
+
});
|
|
891
|
+
if (HOLD.has(receipt.code)) this.#held.add(row.seq);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
this.#held.add(row.seq);
|
|
894
|
+
this.#onError(error);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
#post(row) {
|
|
899
|
+
this.#flying.add(row.seq);
|
|
900
|
+
this.#hand(row.to_addr, JSON.parse(row.envelope), receipt => {
|
|
901
|
+
this.#flying.delete(row.seq);
|
|
902
|
+
this.#answered(row, receipt);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* @param {string} to
|
|
908
|
+
* @param {object} envelope
|
|
909
|
+
* @param {(receipt: Receipt | undefined) => void} answered gets undefined when send() rejected
|
|
910
|
+
*/
|
|
911
|
+
#hand(to, envelope, answered) {
|
|
912
|
+
this.#out += 1;
|
|
913
|
+
// A send() that throws instead of returning a promise counts as rejected.
|
|
914
|
+
new Promise(resolve => resolve(this.#send(to, envelope))).then(receipt => receipt, () => undefined).then(receipt => {
|
|
915
|
+
// A closed kernel no longer writes: a receipt that comes back now is void (R4).
|
|
916
|
+
if (this.#closed) return;
|
|
917
|
+
this.#out -= 1;
|
|
918
|
+
answered(receipt);
|
|
919
|
+
this.#settled();
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* @param {object} row
|
|
925
|
+
* @param {Receipt | undefined} receipt undefined when send() rejected
|
|
926
|
+
*/
|
|
927
|
+
#answered(row, receipt) {
|
|
928
|
+
if (receipt === undefined || HOLD.has(receipt.code)) {
|
|
929
|
+
this.#held.add(row.seq);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
this.transaction(() => this.#heard(row, receipt));
|
|
934
|
+
} catch (error) {
|
|
935
|
+
this.#held.add(row.seq);
|
|
936
|
+
this.#onError(error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
#settled() {
|
|
941
|
+
if (this.#running || this.#out > 0) return;
|
|
942
|
+
for (const resolve of this.#idle.splice(0)) resolve();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function failure(code, message, effects = 'none') {
|
|
947
|
+
return { outcome: 'failed', error: { code, message, effects } };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* A failure as its callee writes it (C11): `unknown` unless it says otherwise. An error that is no object, or effects
|
|
952
|
+
* of no known kind, is the handler's mistake.
|
|
953
|
+
* @param {unknown} [error]
|
|
954
|
+
*/
|
|
955
|
+
function written(error) {
|
|
956
|
+
if (error == null) return { effects: 'unknown' };
|
|
957
|
+
if (!isObject(error)) throw new Error(`the error of a failure must be an object, not ${JSON.stringify(error)}`);
|
|
958
|
+
const effects = error.effects ?? 'unknown';
|
|
959
|
+
if (!EFFECTS.has(effects)) throw new Error(`effects must be none, unknown or some, not ${JSON.stringify(effects)}`);
|
|
960
|
+
return { ...error, effects };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* A failure as its caller hears it (6, C11): what this kernel does not know reads as `unknown`, and the rest is kept.
|
|
965
|
+
* @param {unknown} error
|
|
966
|
+
*/
|
|
967
|
+
function heard(error) {
|
|
968
|
+
if (error == null) return { effects: 'unknown' };
|
|
969
|
+
if (!isObject(error)) return { effects: 'unknown', original: error };
|
|
970
|
+
return EFFECTS.has(error.effects) ? error : { ...error, effects: 'unknown' };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const isObject = value => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
974
|
+
|
|
975
|
+
/** @param {unknown} value */
|
|
976
|
+
function json(value) {
|
|
977
|
+
return value === undefined ? null : JSON.stringify(value);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** @param {string | null} text */
|
|
981
|
+
function parse(text) {
|
|
982
|
+
return text === null ? undefined : JSON.parse(text);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** @param {object} object */
|
|
986
|
+
function defined(object) {
|
|
987
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// The outcome fields of an ended row.
|
|
991
|
+
function outcomeOf(name, row) {
|
|
992
|
+
return { outcome: name, result: parse(row.result), error: parse(row.error), progress: parse(row.progress) };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// A run as its caller sees it in a receipt (6).
|
|
996
|
+
function view(row) {
|
|
997
|
+
return defined({ status: row.status, rev: row.rev, asked: row.asked, asking: parse(row.asking), progress: parse(row.progress),
|
|
998
|
+
...(ENDED.has(row.status) && outcomeOf(row.status, row)) });
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// A run as its handler's host sees it in runs().
|
|
1002
|
+
function runView(row) {
|
|
1003
|
+
return defined({ from: row.from_addr, id: row.id, to: row.to_addr, op: row.op, parent: parse(row.parent), awaited: row.awaited === 1,
|
|
1004
|
+
cancelAsked: row.cancel_asked === 1, status: row.status, rev: row.rev, asked: row.asked, asking: parse(row.asking), progress: parse(row.progress),
|
|
1005
|
+
...(row.root === 1 && { root: true }) });
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// The run that made a call or a timer (C9).
|
|
1009
|
+
function byOf(row) {
|
|
1010
|
+
return row.by_from === null ? undefined : { from: row.by_from, id: row.by_id };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// A timer as its host sees it, until it comes due and is a call (7).
|
|
1014
|
+
function timerView(row) {
|
|
1015
|
+
return defined({ from: row.from_addr, id: row.id, to: row.to_addr, op: row.op, state: 'scheduled', dueAt: row.due, memo: parse(row.memo),
|
|
1016
|
+
by: byOf(row), awaited: row.awaited === 1 });
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// A call as the caller's host sees it: with the last status it heard while open, with its outcome once ended.
|
|
1020
|
+
function callView(row) {
|
|
1021
|
+
return defined({ from: row.from_addr, id: row.id, to: row.to_addr, op: row.op, state: row.state, memo: parse(row.memo), by: byOf(row),
|
|
1022
|
+
awaited: row.awaited === 1,
|
|
1023
|
+
...(row.state === 'open' && { status: row.status, rev: row.rev, asked: row.asked, asking: parse(row.asking), progress: parse(row.progress) }),
|
|
1024
|
+
...(ENDED.has(row.state) && outcomeOf(row.state, row)), ...(row.local === 1 && { local: true }),
|
|
1025
|
+
...(row.late !== null && { late: parse(row.late) }) });
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// The digest of what makes two calls under one id the same call (C1). Key order is not content.
|
|
1029
|
+
function digestOf(value) {
|
|
1030
|
+
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function canonical(value) {
|
|
1034
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
1035
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
1036
|
+
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
|
|
1037
|
+
return `{${keys.map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
1038
|
+
}
|