@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/schema.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The kernel's tables and every statement it runs on them. Keeping the statements in one place lets a test check the
|
|
2
|
+
// query plan of each one: none of them may scan a table.
|
|
3
|
+
|
|
4
|
+
// Entry n upgrades the schema from version n to n + 1. Migrations only go forward: the first entry is the 0.1.0 baseline,
|
|
5
|
+
// and no entry changes once released, since a database already past it would never run it again. A changed table adds
|
|
6
|
+
// an entry at the end.
|
|
7
|
+
export const MIGRATIONS = [
|
|
8
|
+
`CREATE TABLE kernel_calls (
|
|
9
|
+
from_addr TEXT NOT NULL,
|
|
10
|
+
id TEXT NOT NULL,
|
|
11
|
+
to_addr TEXT NOT NULL,
|
|
12
|
+
op TEXT NOT NULL,
|
|
13
|
+
digest TEXT NOT NULL,
|
|
14
|
+
input TEXT NOT NULL,
|
|
15
|
+
parent TEXT NOT NULL,
|
|
16
|
+
lane TEXT,
|
|
17
|
+
by_from TEXT,
|
|
18
|
+
by_id TEXT,
|
|
19
|
+
awaited INTEGER NOT NULL,
|
|
20
|
+
memo TEXT,
|
|
21
|
+
state TEXT NOT NULL,
|
|
22
|
+
status TEXT,
|
|
23
|
+
rev INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
asked INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
asking TEXT,
|
|
26
|
+
result TEXT,
|
|
27
|
+
error TEXT,
|
|
28
|
+
progress TEXT,
|
|
29
|
+
owed INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
local INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
abandoned INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
late TEXT,
|
|
33
|
+
PRIMARY KEY (from_addr, id)
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX kernel_calls_owed ON kernel_calls (owed) WHERE owed = 1;
|
|
36
|
+
CREATE INDEX kernel_calls_open ON kernel_calls (state) WHERE state = 'open';
|
|
37
|
+
CREATE INDEX kernel_calls_state ON kernel_calls (from_addr, state);
|
|
38
|
+
CREATE INDEX kernel_calls_by ON kernel_calls (by_from, by_id);
|
|
39
|
+
CREATE TABLE kernel_runs (
|
|
40
|
+
from_addr TEXT NOT NULL,
|
|
41
|
+
id TEXT NOT NULL,
|
|
42
|
+
to_addr TEXT NOT NULL,
|
|
43
|
+
op TEXT,
|
|
44
|
+
digest TEXT,
|
|
45
|
+
parent TEXT,
|
|
46
|
+
awaited INTEGER NOT NULL,
|
|
47
|
+
root INTEGER NOT NULL,
|
|
48
|
+
cancel_asked INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
status TEXT NOT NULL,
|
|
50
|
+
rev INTEGER NOT NULL,
|
|
51
|
+
asked INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
asking TEXT,
|
|
53
|
+
result TEXT,
|
|
54
|
+
error TEXT,
|
|
55
|
+
progress TEXT,
|
|
56
|
+
PRIMARY KEY (from_addr, id)
|
|
57
|
+
);
|
|
58
|
+
CREATE INDEX kernel_runs_status ON kernel_runs (to_addr, status);
|
|
59
|
+
CREATE TABLE kernel_answers (
|
|
60
|
+
from_addr TEXT NOT NULL,
|
|
61
|
+
id TEXT NOT NULL,
|
|
62
|
+
run_from TEXT NOT NULL,
|
|
63
|
+
run_id TEXT NOT NULL,
|
|
64
|
+
asked INTEGER NOT NULL,
|
|
65
|
+
PRIMARY KEY (from_addr, id)
|
|
66
|
+
);
|
|
67
|
+
CREATE UNIQUE INDEX kernel_answers_question ON kernel_answers (run_from, run_id, asked);
|
|
68
|
+
CREATE TABLE kernel_timers (
|
|
69
|
+
from_addr TEXT NOT NULL,
|
|
70
|
+
id TEXT NOT NULL,
|
|
71
|
+
to_addr TEXT NOT NULL,
|
|
72
|
+
op TEXT NOT NULL,
|
|
73
|
+
digest TEXT NOT NULL,
|
|
74
|
+
input TEXT NOT NULL,
|
|
75
|
+
parent TEXT NOT NULL,
|
|
76
|
+
lane TEXT,
|
|
77
|
+
by_from TEXT,
|
|
78
|
+
by_id TEXT,
|
|
79
|
+
awaited INTEGER NOT NULL,
|
|
80
|
+
memo TEXT,
|
|
81
|
+
due INTEGER NOT NULL,
|
|
82
|
+
PRIMARY KEY (from_addr, id)
|
|
83
|
+
);
|
|
84
|
+
CREATE INDEX kernel_timers_due ON kernel_timers (due);
|
|
85
|
+
CREATE INDEX kernel_timers_by ON kernel_timers (by_from, by_id);
|
|
86
|
+
CREATE TABLE kernel_outbox (
|
|
87
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
88
|
+
pos INTEGER NOT NULL,
|
|
89
|
+
from_addr TEXT NOT NULL,
|
|
90
|
+
to_addr TEXT NOT NULL,
|
|
91
|
+
lane TEXT,
|
|
92
|
+
kind TEXT NOT NULL,
|
|
93
|
+
call_from TEXT NOT NULL,
|
|
94
|
+
call_id TEXT NOT NULL,
|
|
95
|
+
envelope TEXT NOT NULL
|
|
96
|
+
);
|
|
97
|
+
CREATE UNIQUE INDEX kernel_outbox_pos ON kernel_outbox (pos);
|
|
98
|
+
CREATE INDEX kernel_outbox_lane ON kernel_outbox (lane, pos);
|
|
99
|
+
CREATE INDEX kernel_outbox_call ON kernel_outbox (call_from, call_id);
|
|
100
|
+
CREATE INDEX kernel_outbox_from ON kernel_outbox (from_addr, pos);`,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export const META = 'CREATE TABLE IF NOT EXISTS kernel_meta (key TEXT PRIMARY KEY, value INTEGER NOT NULL)';
|
|
104
|
+
|
|
105
|
+
const UNENDED_CALL = "state IN ('sending', 'open')";
|
|
106
|
+
const UNENDED_RUN = "status IN ('working', 'input-required')";
|
|
107
|
+
|
|
108
|
+
export const SQL = {
|
|
109
|
+
version: "SELECT value FROM kernel_meta WHERE key = 'version'",
|
|
110
|
+
setVersion: "INSERT INTO kernel_meta (key, value) VALUES ('version', ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value",
|
|
111
|
+
|
|
112
|
+
call: 'SELECT * FROM kernel_calls WHERE from_addr = ? AND id = ?',
|
|
113
|
+
addCall: `INSERT INTO kernel_calls (from_addr, id, to_addr, op, digest, input, parent, lane, by_from, by_id, awaited, memo, state)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'sending')`,
|
|
115
|
+
// A receipt says the callee took the call; the run it took is working at version 1 (C6).
|
|
116
|
+
openCall: "UPDATE kernel_calls SET state = 'open', status = 'working', rev = 1 WHERE from_addr = ? AND id = ? AND state = 'sending'",
|
|
117
|
+
hearCall: "UPDATE kernel_calls SET state = 'open', status = ?, rev = ?, asked = ?, asking = ?, progress = ? WHERE from_addr = ? AND id = ? RETURNING *",
|
|
118
|
+
endCall: 'UPDATE kernel_calls SET state = ?, result = ?, error = ?, progress = ?, owed = 1, local = ? WHERE from_addr = ? AND id = ?',
|
|
119
|
+
paidCall: 'UPDATE kernel_calls SET owed = 0 WHERE from_addr = ? AND id = ?',
|
|
120
|
+
owedCalls: 'SELECT from_addr, id FROM kernel_calls WHERE owed = 1',
|
|
121
|
+
openCalls: "SELECT * FROM kernel_calls WHERE state = 'open'",
|
|
122
|
+
detach: 'UPDATE kernel_calls SET awaited = 0 WHERE from_addr = ? AND id = ?',
|
|
123
|
+
// The caller ends the call itself and takes no result from it; a late outcome is kept apart (C12).
|
|
124
|
+
abandonCall: "UPDATE kernel_calls SET state = 'failed', error = ?, local = 1, abandoned = 1 WHERE from_addr = ? AND id = ?",
|
|
125
|
+
lateCall: 'UPDATE kernel_calls SET late = ?, owed = 1 WHERE from_addr = ? AND id = ?',
|
|
126
|
+
awaiting: `SELECT * FROM kernel_calls WHERE by_from = ? AND by_id = ? AND awaited = 1 AND ${UNENDED_CALL}`,
|
|
127
|
+
calls: `SELECT * FROM kernel_calls WHERE from_addr = ? AND ${UNENDED_CALL} AND state = coalesce(?, state)`,
|
|
128
|
+
|
|
129
|
+
run: 'SELECT * FROM kernel_runs WHERE from_addr = ? AND id = ?',
|
|
130
|
+
addRun: `INSERT INTO kernel_runs (from_addr, id, to_addr, op, digest, parent, awaited, root, status, rev)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'working', 1)`,
|
|
132
|
+
dropRun: 'DELETE FROM kernel_runs WHERE from_addr = ? AND id = ?',
|
|
133
|
+
haltRun: 'UPDATE kernel_runs SET cancel_asked = 1 WHERE from_addr = ? AND id = ?',
|
|
134
|
+
updateRun: 'UPDATE kernel_runs SET status = ?, asked = ?, asking = ?, progress = ?, rev = rev + 1 WHERE from_addr = ? AND id = ? RETURNING *',
|
|
135
|
+
// Without a progress of its own, an outcome carries the run's last one (C4).
|
|
136
|
+
endRun: `UPDATE kernel_runs SET status = ?, result = ?, error = ?, progress = coalesce(?, progress), rev = rev + 1
|
|
137
|
+
WHERE from_addr = ? AND id = ? RETURNING to_addr, root, progress`,
|
|
138
|
+
runs: `SELECT * FROM kernel_runs WHERE to_addr = ? AND ${UNENDED_RUN} AND status = coalesce(?, status)`,
|
|
139
|
+
|
|
140
|
+
timer: 'SELECT * FROM kernel_timers WHERE from_addr = ? AND id = ?',
|
|
141
|
+
timers: 'SELECT * FROM kernel_timers WHERE from_addr = ?',
|
|
142
|
+
addTimer: `INSERT INTO kernel_timers (from_addr, id, to_addr, op, digest, input, parent, lane, by_from, by_id, awaited, memo, due)
|
|
143
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
144
|
+
dropTimer: 'DELETE FROM kernel_timers WHERE from_addr = ? AND id = ?',
|
|
145
|
+
detachTimer: 'UPDATE kernel_timers SET awaited = 0 WHERE from_addr = ? AND id = ?',
|
|
146
|
+
// Timers due at the same time come due in the order they were registered (7).
|
|
147
|
+
dueTimers: 'SELECT * FROM kernel_timers WHERE due <= ? ORDER BY due, rowid',
|
|
148
|
+
firstDue: 'SELECT min(due) AS due FROM kernel_timers',
|
|
149
|
+
awaitedTimer: 'SELECT 1 FROM kernel_timers WHERE by_from = ? AND by_id = ? AND awaited = 1',
|
|
150
|
+
dropRunTimers: 'DELETE FROM kernel_timers WHERE by_from = ? AND by_id = ?',
|
|
151
|
+
|
|
152
|
+
// Only the number of an answer taken is kept, and which question it answered (C8, S8).
|
|
153
|
+
answer: 'SELECT 1 FROM kernel_answers WHERE from_addr = ? AND id = ?',
|
|
154
|
+
addAnswer: 'INSERT INTO kernel_answers (from_addr, id, run_from, run_id, asked) VALUES (?, ?, ?, ?, ?)',
|
|
155
|
+
|
|
156
|
+
// A letter is identified by `seq` and ordered by `pos`: a letter that replaces another takes the other's `pos` (R3).
|
|
157
|
+
owe: `INSERT INTO kernel_outbox (pos, from_addr, to_addr, lane, kind, call_from, call_id, envelope)
|
|
158
|
+
VALUES (coalesce(?, (SELECT coalesce(max(pos), 0) + 1 FROM kernel_outbox)), ?, ?, ?, ?, ?, ?, ?)`,
|
|
159
|
+
// The first letter after `pos` that may go now: one without a lane, or the oldest of its lane (C10).
|
|
160
|
+
next: `SELECT * FROM kernel_outbox AS o
|
|
161
|
+
WHERE pos > ? AND (lane IS NULL OR pos = (SELECT min(pos) FROM kernel_outbox WHERE lane = o.lane))
|
|
162
|
+
ORDER BY pos LIMIT 1`,
|
|
163
|
+
dropLetter: 'DELETE FROM kernel_outbox WHERE seq = ?',
|
|
164
|
+
dropCallLetter: "DELETE FROM kernel_outbox WHERE call_from = ? AND call_id = ? AND kind = 'call' RETURNING pos",
|
|
165
|
+
cancelLetter: "SELECT 1 FROM kernel_outbox WHERE call_from = ? AND call_id = ? AND kind = 'cancel'",
|
|
166
|
+
dropCancelLetter: "DELETE FROM kernel_outbox WHERE call_from = ? AND call_id = ? AND kind = 'cancel'",
|
|
167
|
+
dropStatusLetter: "DELETE FROM kernel_outbox WHERE call_from = ? AND call_id = ? AND kind = 'status' RETURNING pos",
|
|
168
|
+
letters: 'SELECT kind, to_addr, call_from, call_id FROM kernel_outbox WHERE from_addr = ? ORDER BY pos',
|
|
169
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transactions and the output gate (T1, T3, T5). Effects registered while a transaction is open wait for the outermost
|
|
3
|
+
* COMMIT; a rollback at any level drops what that level registered.
|
|
4
|
+
*/
|
|
5
|
+
export class Transactions {
|
|
6
|
+
#db;
|
|
7
|
+
#report;
|
|
8
|
+
#committed;
|
|
9
|
+
/** @type {Array<Array<() => void>>} effects registered per open level, outermost first */
|
|
10
|
+
#levels = [];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import('node:sqlite').DatabaseSync} db
|
|
14
|
+
* @param {(error: unknown) => void} report
|
|
15
|
+
* @param {() => void} committed runs after every outermost commit, once its effects have run
|
|
16
|
+
*/
|
|
17
|
+
constructor(db, report, committed) {
|
|
18
|
+
this.#db = db;
|
|
19
|
+
this.#report = report;
|
|
20
|
+
this.#committed = committed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get open() {
|
|
24
|
+
return this.#levels.length > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @template T
|
|
29
|
+
* @param {() => T} fn
|
|
30
|
+
* @returns {T}
|
|
31
|
+
*/
|
|
32
|
+
run(fn) {
|
|
33
|
+
const depth = this.#levels.length;
|
|
34
|
+
const name = `kernel_${depth}`;
|
|
35
|
+
this.#db.exec(depth === 0 ? 'BEGIN IMMEDIATE' : `SAVEPOINT ${name}`);
|
|
36
|
+
this.#levels.push([]);
|
|
37
|
+
let value;
|
|
38
|
+
try {
|
|
39
|
+
value = fn();
|
|
40
|
+
if (typeof value?.then === 'function') throw new Error('a transaction function must be synchronous');
|
|
41
|
+
} catch (error) {
|
|
42
|
+
this.#levels.pop();
|
|
43
|
+
this.#db.exec(depth === 0 ? 'ROLLBACK' : `ROLLBACK TO ${name}; RELEASE ${name}`);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const effects = this.#levels.pop();
|
|
47
|
+
if (depth > 0) {
|
|
48
|
+
this.#db.exec(`RELEASE ${name}`);
|
|
49
|
+
this.#levels[depth - 1].push(...effects);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
this.#db.exec('COMMIT');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// A failed COMMIT may leave the transaction open; nothing of it may survive, and nothing is released (T5).
|
|
56
|
+
if (this.#db.isTransaction) this.#db.exec('ROLLBACK');
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
for (const effect of effects) {
|
|
60
|
+
try {
|
|
61
|
+
effect();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.#report(error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.#committed();
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {() => void} fn */
|
|
71
|
+
after(fn) {
|
|
72
|
+
if (!this.open) throw new Error('afterCommit() must be called inside kernel.transaction()');
|
|
73
|
+
this.#levels.at(-1).push(fn);
|
|
74
|
+
}
|
|
75
|
+
}
|