@dcasegr/skein 0.0.0-stage → 0.1.1
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 +522 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +773 -0
- package/dist/host/router.js +2389 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
// The Store as one SQLite file (node:sqlite). `blocks` is the record; every
|
|
2
|
+
// other table except `handles` is an index derived from it and can be dropped
|
|
3
|
+
// and rebuilt (edges.rebuild). node:sqlite is synchronous, so each method body
|
|
4
|
+
// runs to completion without yielding; BEGIN IMMEDIATE covers other processes
|
|
5
|
+
// sharing the file.
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { CID, decode, encode, fmt, fromBytes, isCID, parse } from "./cid.js";
|
|
8
|
+
import { checkExtends, NotFound, Rejected } from "./store.js";
|
|
9
|
+
import { verifyMessageSync } from "./records.js";
|
|
10
|
+
const DDL = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS blocks (
|
|
12
|
+
cid BLOB PRIMARY KEY,
|
|
13
|
+
bytes BLOB NOT NULL
|
|
14
|
+
) WITHOUT ROWID;
|
|
15
|
+
|
|
16
|
+
-- One row per origin. Columns below tip are derived from the origin block and
|
|
17
|
+
-- the tip block only, so a row can always be recomputed.
|
|
18
|
+
CREATE TABLE IF NOT EXISTS chains (
|
|
19
|
+
origin BLOB PRIMARY KEY REFERENCES blocks(cid),
|
|
20
|
+
tip BLOB NOT NULL REFERENCES blocks(cid),
|
|
21
|
+
seq INTEGER NOT NULL, -- tip's seq; 0 = no updates yet
|
|
22
|
+
kind TEXT, -- origin.kind
|
|
23
|
+
thread BLOB, -- nodes: origin.thread
|
|
24
|
+
launched_by BLOB, -- origin.launchedBy
|
|
25
|
+
at INTEGER, -- origin.at
|
|
26
|
+
state TEXT, -- threads: tip.state (NULL until the first update)
|
|
27
|
+
until INTEGER, -- threads: tip.until
|
|
28
|
+
waiting_on TEXT, -- threads: tip.waitingOn as a JSON array of CID strings
|
|
29
|
+
tip_at INTEGER, -- tip.at (origin.at until the first update): recent activity
|
|
30
|
+
program BLOB, -- threads: origin.program
|
|
31
|
+
waiting_from TEXT, -- threads: tip.waitingFrom (an identity)
|
|
32
|
+
awaits TEXT -- threads: tip.awaits (emitted envelopes awaiting a reply) as a JSON array of CID strings
|
|
33
|
+
) WITHOUT ROWID;
|
|
34
|
+
CREATE INDEX IF NOT EXISTS chains_kind_at ON chains(kind, at);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS chains_thread ON chains(thread, at) WHERE thread IS NOT NULL;
|
|
36
|
+
CREATE INDEX IF NOT EXISTS chains_state ON chains(state) WHERE state IS NOT NULL;
|
|
37
|
+
CREATE INDEX IF NOT EXISTS chains_until ON chains(until) WHERE state = 'waiting';
|
|
38
|
+
-- Indexes on migrated columns are created after the migration below.
|
|
39
|
+
|
|
40
|
+
-- Every update's position. UNIQUE(origin, seq) is the backstop against forks.
|
|
41
|
+
CREATE TABLE IF NOT EXISTS updates (
|
|
42
|
+
cid BLOB PRIMARY KEY REFERENCES blocks(cid),
|
|
43
|
+
origin BLOB NOT NULL REFERENCES chains(origin) ON DELETE CASCADE,
|
|
44
|
+
seq INTEGER NOT NULL,
|
|
45
|
+
UNIQUE (origin, seq)
|
|
46
|
+
) WITHOUT ROWID;
|
|
47
|
+
|
|
48
|
+
-- Pointers out of a chain, keyed by the origin whatever block of the chain
|
|
49
|
+
-- holds them: (seq, ord) says which block and where in it, which keeps a
|
|
50
|
+
-- rebuild byte-identical regardless of walk order. "to" is a CID string or a
|
|
51
|
+
-- URL (URLs always contain ':', CID strings never do).
|
|
52
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
53
|
+
"from" BLOB NOT NULL REFERENCES chains(origin) ON DELETE CASCADE,
|
|
54
|
+
seq INTEGER NOT NULL,
|
|
55
|
+
ord INTEGER NOT NULL,
|
|
56
|
+
"to" TEXT NOT NULL,
|
|
57
|
+
rel TEXT NOT NULL,
|
|
58
|
+
locator TEXT,
|
|
59
|
+
PRIMARY KEY ("from", seq, ord)
|
|
60
|
+
) WITHOUT ROWID;
|
|
61
|
+
CREATE INDEX IF NOT EXISTS edges_to ON edges("to", rel);
|
|
62
|
+
|
|
63
|
+
-- Signed messages. Each is one record, not a chain. Only verified messages
|
|
64
|
+
-- get a row; UNIQUE("from", seq) refuses a sender's second message at a seq.
|
|
65
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
66
|
+
cid BLOB PRIMARY KEY REFERENCES blocks(cid),
|
|
67
|
+
"from" TEXT NOT NULL,
|
|
68
|
+
"to" TEXT,
|
|
69
|
+
seq INTEGER NOT NULL,
|
|
70
|
+
at INTEGER NOT NULL,
|
|
71
|
+
UNIQUE ("from", seq)
|
|
72
|
+
) WITHOUT ROWID;
|
|
73
|
+
CREATE INDEX IF NOT EXISTS messages_at ON messages(at);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS messages_from_at ON messages("from", at);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS messages_to_at ON messages("to", at) WHERE "to" IS NOT NULL;
|
|
76
|
+
|
|
77
|
+
-- Not derived and not rebuilt: program bookkeeping, overwritten freely.
|
|
78
|
+
CREATE TABLE IF NOT EXISTS handles (
|
|
79
|
+
thread BLOB PRIMARY KEY,
|
|
80
|
+
json TEXT NOT NULL
|
|
81
|
+
) WITHOUT ROWID;
|
|
82
|
+
|
|
83
|
+
-- Not derived and not rebuilt: the input log. Each row is one signed
|
|
84
|
+
-- log-entry record (also in blocks); admission order is not recoverable from
|
|
85
|
+
-- the records. (An older file's message-based "log" table is left unread.)
|
|
86
|
+
-- The envelope column is the record the entry is unique by: an admitted envelope, or
|
|
87
|
+
-- an outcome's emit (one outcome per emit). They are different records, so
|
|
88
|
+
-- one column serves both.
|
|
89
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
90
|
+
n INTEGER PRIMARY KEY,
|
|
91
|
+
cid BLOB NOT NULL UNIQUE,
|
|
92
|
+
envelope BLOB UNIQUE
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
-- Not derived: the scheduler's cursor and similar single values.
|
|
96
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
97
|
+
key TEXT PRIMARY KEY,
|
|
98
|
+
value INTEGER NOT NULL
|
|
99
|
+
) WITHOUT ROWID;
|
|
100
|
+
`;
|
|
101
|
+
/**
|
|
102
|
+
* `readOnly`: open an existing file for reading only (the explorer beside a
|
|
103
|
+
* live runtime): no schema, no migration, no journal change; writes throw.
|
|
104
|
+
*/
|
|
105
|
+
export function openStore(path, o = {}) {
|
|
106
|
+
const db = new DatabaseSync(path, { readOnly: o.readOnly ?? false });
|
|
107
|
+
let stale = false;
|
|
108
|
+
if (o.readOnly)
|
|
109
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
110
|
+
else {
|
|
111
|
+
db.exec(`
|
|
112
|
+
PRAGMA journal_mode = WAL;
|
|
113
|
+
PRAGMA synchronous = NORMAL;
|
|
114
|
+
PRAGMA foreign_keys = ON;
|
|
115
|
+
PRAGMA busy_timeout = 5000;
|
|
116
|
+
`);
|
|
117
|
+
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
|
|
118
|
+
db.exec(DDL);
|
|
119
|
+
// Older files: add missing derived columns, then rebuild once the store
|
|
120
|
+
// exists. A new messages table beside old chains may owe rows to old blocks.
|
|
121
|
+
const cols = new Set(db.prepare("PRAGMA table_info(chains)").all().map((c) => c.name));
|
|
122
|
+
const added = [["tip_at", "INTEGER"], ["program", "BLOB"], ["waiting_from", "TEXT"], ["awaits", "TEXT"]].filter(([c]) => !cols.has(c));
|
|
123
|
+
for (const [c, type] of added)
|
|
124
|
+
db.exec(`ALTER TABLE chains ADD COLUMN ${c} ${type}`);
|
|
125
|
+
stale = added.length > 0 || (tables.has("chains") && !tables.has("messages"));
|
|
126
|
+
db.exec(`
|
|
127
|
+
CREATE INDEX IF NOT EXISTS chains_tip_at ON chains(kind, tip_at);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS chains_program ON chains(program) WHERE program IS NOT NULL;
|
|
129
|
+
CREATE INDEX IF NOT EXISTS chains_waiting_from ON chains(waiting_from) WHERE waiting_from IS NOT NULL;
|
|
130
|
+
CREATE INDEX IF NOT EXISTS chains_awaits ON chains(kind) WHERE awaits IS NOT NULL;
|
|
131
|
+
`);
|
|
132
|
+
}
|
|
133
|
+
const q = {
|
|
134
|
+
begin: db.prepare("BEGIN IMMEDIATE"),
|
|
135
|
+
commit: db.prepare("COMMIT"),
|
|
136
|
+
rollback: db.prepare("ROLLBACK"),
|
|
137
|
+
blockPut: db.prepare("INSERT OR IGNORE INTO blocks (cid, bytes) VALUES (?, ?)"),
|
|
138
|
+
blockGet: db.prepare("SELECT bytes FROM blocks WHERE cid = ?"),
|
|
139
|
+
blockHas: db.prepare("SELECT 1 AS x FROM blocks WHERE cid = ?"),
|
|
140
|
+
blockAll: db.prepare("SELECT cid, bytes FROM blocks ORDER BY cid"),
|
|
141
|
+
chainIns: db.prepare(`INSERT OR IGNORE INTO chains
|
|
142
|
+
(origin, tip, seq, kind, thread, launched_by, at, tip_at, program)
|
|
143
|
+
VALUES (:origin, :origin, 0, :kind, :thread, :launched_by, :at, :at, :program)`),
|
|
144
|
+
chainGet: db.prepare("SELECT tip, seq, kind FROM chains WHERE origin = ?"),
|
|
145
|
+
chainMove: db.prepare(`UPDATE chains SET tip = :tip, seq = :seq,
|
|
146
|
+
state = :state, until = :until, waiting_on = :waiting_on, tip_at = :tip_at, waiting_from = :waiting_from, awaits = :awaits
|
|
147
|
+
WHERE origin = :origin`),
|
|
148
|
+
updateIns: db.prepare("INSERT INTO updates (cid, origin, seq) VALUES (?, ?, ?)"),
|
|
149
|
+
origins: db.prepare("SELECT origin FROM chains"),
|
|
150
|
+
updateOrigin: db.prepare("SELECT origin FROM updates WHERE cid = ?"),
|
|
151
|
+
updatesOf: db.prepare("SELECT cid FROM updates WHERE origin = ? ORDER BY seq"),
|
|
152
|
+
lastUpdate: db.prepare("SELECT cid, seq FROM updates WHERE origin = ? ORDER BY seq DESC LIMIT 1"),
|
|
153
|
+
edgeIns: db.prepare(`INSERT OR IGNORE INTO edges ("from", seq, ord, "to", rel, locator)
|
|
154
|
+
VALUES (?, ?, ?, ?, ?, ?)`),
|
|
155
|
+
// Same pointer from several blocks of one chain (waitingOn repeated across
|
|
156
|
+
// updates) collapses to one ref, in order of first appearance.
|
|
157
|
+
refsFrom: db.prepare(`SELECT "to", rel, locator FROM edges WHERE "from" = ?
|
|
158
|
+
GROUP BY "to", rel, locator ORDER BY MIN(seq * 1048576 + ord)`),
|
|
159
|
+
refsTo: db.prepare(`SELECT e."from", e.rel, e.locator FROM edges e
|
|
160
|
+
JOIN chains c ON c.origin = e."from" WHERE e."to" = ?
|
|
161
|
+
GROUP BY e."from", e.rel, e.locator ORDER BY MAX(c.at) DESC, e."from" DESC`),
|
|
162
|
+
// One statement for every Filter combination: NULL means "don't narrow".
|
|
163
|
+
query: db.prepare(`SELECT origin FROM chains WHERE
|
|
164
|
+
(:kind IS NULL OR kind = :kind)
|
|
165
|
+
AND (:thread IS NULL OR thread = :thread)
|
|
166
|
+
AND (:program IS NULL OR program = :program)
|
|
167
|
+
AND (:states IS NULL OR state IN (SELECT value FROM json_each(:states)))
|
|
168
|
+
AND (:parentless IS NULL OR (kind = 'thread' AND (launched_by IS NULL) = :parentless))
|
|
169
|
+
AND (:since IS NULL OR at >= :since)
|
|
170
|
+
AND (:before IS NULL OR at < :before)
|
|
171
|
+
ORDER BY CASE WHEN :tipAt THEN tip_at ELSE at END DESC, origin DESC LIMIT :limit`),
|
|
172
|
+
messageQuery: db.prepare(`SELECT cid FROM messages WHERE
|
|
173
|
+
(:from IS NULL OR "from" = :from)
|
|
174
|
+
AND (:to IS NULL OR "to" = :to)
|
|
175
|
+
AND (:since IS NULL OR at >= :since)
|
|
176
|
+
AND (:before IS NULL OR at < :before)
|
|
177
|
+
ORDER BY at DESC, cid DESC LIMIT :limit`),
|
|
178
|
+
messageIns: db.prepare(`INSERT INTO messages (cid, "from", "to", seq, at) VALUES (:cid, :from, :to, :seq, :at)`),
|
|
179
|
+
messageAt: db.prepare(`SELECT cid FROM messages WHERE "from" = ? AND seq = ?`),
|
|
180
|
+
resting: db.prepare(`SELECT origin FROM chains
|
|
181
|
+
WHERE kind = 'thread' AND (state IS NULL OR state <> 'finished')
|
|
182
|
+
AND (:states IS NULL OR state IN (SELECT value FROM json_each(:states)))
|
|
183
|
+
ORDER BY at, origin LIMIT :limit`),
|
|
184
|
+
due: db.prepare(`SELECT origin FROM chains
|
|
185
|
+
WHERE state = 'waiting' AND kind = 'thread' AND until <= ? ORDER BY until, origin`),
|
|
186
|
+
waitersOn: db.prepare(`SELECT origin FROM chains
|
|
187
|
+
WHERE kind = 'thread' AND waiting_on IS NOT NULL
|
|
188
|
+
AND EXISTS (SELECT 1 FROM json_each(waiting_on) WHERE value = ?) ORDER BY at, origin`),
|
|
189
|
+
waitingFrom: db.prepare(`SELECT origin FROM chains
|
|
190
|
+
WHERE kind = 'thread' AND waiting_from = ? ORDER BY at, origin`),
|
|
191
|
+
awaiting: db.prepare(`SELECT origin FROM chains
|
|
192
|
+
WHERE kind = 'thread' AND awaits IS NOT NULL
|
|
193
|
+
AND EXISTS (SELECT 1 FROM json_each(awaits) WHERE value = ?) ORDER BY at, origin`),
|
|
194
|
+
handleSet: db.prepare(`INSERT INTO handles (thread, json) VALUES (?, ?)
|
|
195
|
+
ON CONFLICT (thread) DO UPDATE SET json = excluded.json`),
|
|
196
|
+
handleGet: db.prepare("SELECT json FROM handles WHERE thread = ?"),
|
|
197
|
+
handleClear: db.prepare("DELETE FROM handles WHERE thread = ?"),
|
|
198
|
+
wipeEdges: db.prepare("DELETE FROM edges"),
|
|
199
|
+
wipeUpdates: db.prepare("DELETE FROM updates"),
|
|
200
|
+
wipeChains: db.prepare("DELETE FROM chains"),
|
|
201
|
+
wipeMessages: db.prepare("DELETE FROM messages"),
|
|
202
|
+
logTip: db.prepare("SELECT n, cid FROM entries ORDER BY n DESC LIMIT 1"),
|
|
203
|
+
logOf: db.prepare("SELECT cid FROM entries WHERE envelope = ?"),
|
|
204
|
+
logIns: db.prepare("INSERT INTO entries (n, cid, envelope) VALUES (?, ?, ?)"),
|
|
205
|
+
logFrom: db.prepare("SELECT cid FROM entries WHERE n >= ? ORDER BY n"),
|
|
206
|
+
metaGet: db.prepare("SELECT value FROM meta WHERE key = ?"),
|
|
207
|
+
metaSet: db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value"),
|
|
208
|
+
};
|
|
209
|
+
function tx(fn) {
|
|
210
|
+
q.begin.run();
|
|
211
|
+
try {
|
|
212
|
+
const r = fn();
|
|
213
|
+
q.commit.run();
|
|
214
|
+
return r;
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
if (db.isTransaction)
|
|
218
|
+
q.rollback.run();
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function put(value) {
|
|
223
|
+
const { cid, bytes } = encode(value);
|
|
224
|
+
q.blockPut.run(cid.bytes, bytes);
|
|
225
|
+
return cid;
|
|
226
|
+
}
|
|
227
|
+
function getBlock(cid) {
|
|
228
|
+
const row = q.blockGet.get(cid.bytes);
|
|
229
|
+
if (!row)
|
|
230
|
+
throw new NotFound(fmt(cid));
|
|
231
|
+
return decode(row.bytes);
|
|
232
|
+
}
|
|
233
|
+
function chainRow(origin) {
|
|
234
|
+
const row = q.chainGet.get(origin.bytes);
|
|
235
|
+
if (!row)
|
|
236
|
+
throw new NotFound(fmt(origin));
|
|
237
|
+
return row;
|
|
238
|
+
}
|
|
239
|
+
function writeEdges(from, seq, edges) {
|
|
240
|
+
edges.forEach((e, i) => q.edgeIns.run(from.bytes, seq, i, e.to, e.rel, e.locator));
|
|
241
|
+
}
|
|
242
|
+
// Register an origin with tip = itself. Returns false if already registered.
|
|
243
|
+
function register(cid, block) {
|
|
244
|
+
const r = q.chainIns.run(originMeta(cid, block));
|
|
245
|
+
if (r.changes === 0)
|
|
246
|
+
return false;
|
|
247
|
+
writeEdges(cid, 0, originEdges(block));
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
function move(origin, kind, tip, seq, block) {
|
|
251
|
+
q.chainMove.run({ origin: origin.bytes, tip: tip.bytes, seq, ...tipMeta(kind, block) });
|
|
252
|
+
}
|
|
253
|
+
function indexMessage(cid, m) {
|
|
254
|
+
q.messageIns.run({ cid: cid.bytes, from: m.from, to: m.to ?? null, seq: m.seq, at: m.at });
|
|
255
|
+
}
|
|
256
|
+
function* rows(stmt, p) {
|
|
257
|
+
// all() rather than iterate(): callers append while they walk the results.
|
|
258
|
+
for (const r of stmt.all(p))
|
|
259
|
+
yield fromBytes(r.origin);
|
|
260
|
+
}
|
|
261
|
+
const store = {
|
|
262
|
+
async put(value) { return put(value); },
|
|
263
|
+
async get(cid) { return getBlock(cid); },
|
|
264
|
+
async has(cid) { return q.blockHas.get(cid.bytes) !== undefined; },
|
|
265
|
+
async bytes(cid) {
|
|
266
|
+
const row = q.blockGet.get(cid.bytes);
|
|
267
|
+
if (!row)
|
|
268
|
+
throw new NotFound(fmt(cid));
|
|
269
|
+
return row.bytes;
|
|
270
|
+
},
|
|
271
|
+
async putBlock(cid, data) { q.blockPut.run(cid.bytes, data); },
|
|
272
|
+
async putMessage(m) {
|
|
273
|
+
if (!verifyMessageSync(m))
|
|
274
|
+
throw new Rejected("bad-signature", "message: bad shape or signature");
|
|
275
|
+
return tx(() => {
|
|
276
|
+
const { cid } = encode(m);
|
|
277
|
+
const at = q.messageAt.get(m.from, m.seq);
|
|
278
|
+
if (at) {
|
|
279
|
+
if (fromBytes(at.cid).equals(cid))
|
|
280
|
+
return cid; // same record again: idempotent, like put
|
|
281
|
+
throw new Rejected("duplicate-seq", `message: ${m.from} already sent seq ${m.seq}`);
|
|
282
|
+
}
|
|
283
|
+
put(m);
|
|
284
|
+
indexMessage(cid, m);
|
|
285
|
+
return cid;
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
async findByPrefix(prefix) {
|
|
289
|
+
// Origins are stored binary, so the match is on the formatted string; a full scan is fine at this scale.
|
|
290
|
+
const out = [];
|
|
291
|
+
for (const r of q.origins.iterate()) {
|
|
292
|
+
const cid = fromBytes(r.origin);
|
|
293
|
+
if (fmt(cid).startsWith(prefix))
|
|
294
|
+
out.push(cid);
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
},
|
|
298
|
+
chains: {
|
|
299
|
+
async open(origin) {
|
|
300
|
+
return tx(() => {
|
|
301
|
+
const cid = put(origin);
|
|
302
|
+
register(cid, origin);
|
|
303
|
+
return cid;
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
async append(origin, body) {
|
|
307
|
+
return tx(() => {
|
|
308
|
+
const row = chainRow(origin);
|
|
309
|
+
const seq = row.seq + 1;
|
|
310
|
+
// The chain fields are the store's to set; a caller's copies are overridden. `at` is the caller's: no clock here.
|
|
311
|
+
if (typeof body.at !== "number")
|
|
312
|
+
throw new TypeError("append: body.at (log time) is required");
|
|
313
|
+
const update = { ...body, origin, prev: fromBytes(row.tip), seq };
|
|
314
|
+
const cid = put(update);
|
|
315
|
+
q.updateIns.run(cid.bytes, origin.bytes, seq);
|
|
316
|
+
writeEdges(origin, seq, updateEdges(update));
|
|
317
|
+
move(origin, row.kind, cid, seq, update);
|
|
318
|
+
return cid;
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
async tip(origin) {
|
|
322
|
+
return fromBytes(chainRow(origin).tip);
|
|
323
|
+
},
|
|
324
|
+
async *history(origin) {
|
|
325
|
+
chainRow(origin);
|
|
326
|
+
const ups = q.updatesOf.all(origin.bytes);
|
|
327
|
+
yield origin;
|
|
328
|
+
for (const u of ups)
|
|
329
|
+
yield fromBytes(u.cid);
|
|
330
|
+
},
|
|
331
|
+
async originOf(cid) {
|
|
332
|
+
if (q.chainGet.get(cid.bytes))
|
|
333
|
+
return cid;
|
|
334
|
+
const u = q.updateOrigin.get(cid.bytes);
|
|
335
|
+
if (u)
|
|
336
|
+
return fromBytes(u.origin);
|
|
337
|
+
throw new NotFound(fmt(cid));
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
log: {
|
|
341
|
+
async append(entry) {
|
|
342
|
+
return tx(() => {
|
|
343
|
+
if (entry.envelope && q.logOf.get(entry.envelope.bytes))
|
|
344
|
+
throw new Rejected("duplicate-envelope", `log: envelope ${fmt(entry.envelope)} is already admitted`);
|
|
345
|
+
if (entry.outcome && q.logOf.get(entry.outcome.emit.bytes))
|
|
346
|
+
throw new Rejected("duplicate-outcome", `log: emit ${fmt(entry.outcome.emit)} already has an outcome`);
|
|
347
|
+
const last = q.logTip.get();
|
|
348
|
+
const tipCid = last ? fromBytes(last.cid) : undefined;
|
|
349
|
+
checkExtends(entry, tipCid, tipCid ? getBlock(tipCid) : undefined);
|
|
350
|
+
const cid = put(entry);
|
|
351
|
+
q.logIns.run(entry.n, cid.bytes, (entry.envelope ?? entry.outcome?.emit)?.bytes ?? null);
|
|
352
|
+
return cid;
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
async byEnvelope(envelope) {
|
|
356
|
+
const r = q.logOf.get(envelope.bytes);
|
|
357
|
+
const cid = r ? fromBytes(r.cid) : undefined;
|
|
358
|
+
return cid && getBlock(cid).envelope ? cid : undefined;
|
|
359
|
+
},
|
|
360
|
+
async outcomeOf(emit) {
|
|
361
|
+
const r = q.logOf.get(emit.bytes);
|
|
362
|
+
const cid = r ? fromBytes(r.cid) : undefined;
|
|
363
|
+
return cid && getBlock(cid).outcome ? cid : undefined;
|
|
364
|
+
},
|
|
365
|
+
async tip() {
|
|
366
|
+
const last = q.logTip.get();
|
|
367
|
+
return last ? fromBytes(last.cid) : undefined;
|
|
368
|
+
},
|
|
369
|
+
async *entries(from = 0) {
|
|
370
|
+
for (const r of q.logFrom.all(from)) {
|
|
371
|
+
const cid = fromBytes(r.cid);
|
|
372
|
+
yield { cid, entry: getBlock(cid) };
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
edges: {
|
|
377
|
+
async refsFrom(cid) {
|
|
378
|
+
return q.refsFrom.all(cid.bytes).map((r) => toRef(r.to, r.rel, r.locator));
|
|
379
|
+
},
|
|
380
|
+
async refsTo(cid) {
|
|
381
|
+
return q.refsTo.all(fmt(cid)).map((r) => ({
|
|
382
|
+
...toRef(cid, r.rel, r.locator),
|
|
383
|
+
from: fromBytes(r.from),
|
|
384
|
+
}));
|
|
385
|
+
},
|
|
386
|
+
async *query(f) {
|
|
387
|
+
if (f.kind === "message" || (f.kind === undefined && (f.from !== undefined || f.to !== undefined))) {
|
|
388
|
+
const ms = q.messageQuery.all({
|
|
389
|
+
from: f.from ?? null, to: f.to ?? null, since: f.since ?? null, before: f.before ?? null, limit: f.limit ?? -1,
|
|
390
|
+
});
|
|
391
|
+
for (const r of ms)
|
|
392
|
+
yield fromBytes(r.cid);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
yield* rows(q.query, {
|
|
396
|
+
kind: f.kind ?? null,
|
|
397
|
+
thread: f.thread?.bytes ?? null,
|
|
398
|
+
program: f.program?.bytes ?? null,
|
|
399
|
+
states: f.state ? JSON.stringify(f.state) : null,
|
|
400
|
+
parentless: f.parentless === undefined ? null : f.parentless ? 1 : 0,
|
|
401
|
+
since: f.since ?? null,
|
|
402
|
+
before: f.before ?? null,
|
|
403
|
+
tipAt: f.orderBy === "tipAt" ? 1 : 0,
|
|
404
|
+
limit: f.limit ?? -1,
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
async rebuild() {
|
|
408
|
+
tx(() => {
|
|
409
|
+
q.wipeEdges.run();
|
|
410
|
+
q.wipeUpdates.run();
|
|
411
|
+
q.wipeChains.run();
|
|
412
|
+
q.wipeMessages.run();
|
|
413
|
+
// Origins are thread/node blocks plus anything an update names as
|
|
414
|
+
// its origin. A chain of another kind opened but never appended to
|
|
415
|
+
// is indistinguishable from a plain block and is not recovered.
|
|
416
|
+
const origins = new Map();
|
|
417
|
+
const named = new Set();
|
|
418
|
+
const ups = [];
|
|
419
|
+
for (const r of q.blockAll.iterate()) {
|
|
420
|
+
const cid = fromBytes(r.cid);
|
|
421
|
+
if (cid.code === RAW_CODE || cid.code === GIT_RAW_CODE)
|
|
422
|
+
continue; // wasm modules, git blobs/trees: not dag-cbor
|
|
423
|
+
const block = decode(r.bytes);
|
|
424
|
+
if (!isObj(block))
|
|
425
|
+
continue;
|
|
426
|
+
if (block.kind === "message") {
|
|
427
|
+
// Re-verified: put() can store an unsigned look-alike. Plain INSERT: two
|
|
428
|
+
// valid messages at one (from, seq) is equivocation, and should fail loudly.
|
|
429
|
+
if (verifyMessageSync(block))
|
|
430
|
+
indexMessage(cid, block);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (isUpdate(block)) {
|
|
434
|
+
ups.push({ cid, origin: block.origin, seq: block.seq, edges: updateEdges(block) });
|
|
435
|
+
named.add(fmt(block.origin));
|
|
436
|
+
}
|
|
437
|
+
else if (block.kind === "thread" || block.kind === "node") {
|
|
438
|
+
origins.set(fmt(cid), { cid, block });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const s of named) {
|
|
442
|
+
if (origins.has(s))
|
|
443
|
+
continue;
|
|
444
|
+
const cid = parse(s);
|
|
445
|
+
const row = q.blockGet.get(cid.bytes);
|
|
446
|
+
if (!row)
|
|
447
|
+
continue; // update whose origin we never stored: orphan, skip
|
|
448
|
+
const block = decode(row.bytes);
|
|
449
|
+
if (isObj(block) && !isUpdate(block))
|
|
450
|
+
origins.set(s, { cid, block });
|
|
451
|
+
}
|
|
452
|
+
for (const { cid, block } of origins.values())
|
|
453
|
+
register(cid, block);
|
|
454
|
+
for (const u of ups) {
|
|
455
|
+
if (!origins.has(fmt(u.origin)))
|
|
456
|
+
continue;
|
|
457
|
+
// Plain INSERT: two updates claiming one seq is a fork, and should fail loudly.
|
|
458
|
+
q.updateIns.run(u.cid.bytes, u.origin.bytes, u.seq);
|
|
459
|
+
writeEdges(u.origin, u.seq, u.edges);
|
|
460
|
+
}
|
|
461
|
+
for (const { cid, block } of origins.values()) {
|
|
462
|
+
const last = q.lastUpdate.get(cid.bytes);
|
|
463
|
+
if (!last)
|
|
464
|
+
continue;
|
|
465
|
+
const tip = fromBytes(last.cid);
|
|
466
|
+
move(cid, block.kind, tip, last.seq, getBlock(tip));
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
live: {
|
|
472
|
+
handles: {
|
|
473
|
+
async set(thread, handle) { q.handleSet.run(thread.bytes, JSON.stringify(handle)); },
|
|
474
|
+
async get(thread) {
|
|
475
|
+
const r = q.handleGet.get(thread.bytes);
|
|
476
|
+
return r ? JSON.parse(r.json) : undefined;
|
|
477
|
+
},
|
|
478
|
+
async clear(thread) { q.handleClear.run(thread.bytes); },
|
|
479
|
+
},
|
|
480
|
+
async *resting(f = {}) {
|
|
481
|
+
yield* rows(q.resting, {
|
|
482
|
+
states: f.state ? JSON.stringify(f.state) : null,
|
|
483
|
+
limit: f.limit ?? -1,
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
async *due(now) {
|
|
487
|
+
for (const r of q.due.all(now))
|
|
488
|
+
yield fromBytes(r.origin);
|
|
489
|
+
},
|
|
490
|
+
async *waitersOn(thread) {
|
|
491
|
+
for (const r of q.waitersOn.all(fmt(thread)))
|
|
492
|
+
yield fromBytes(r.origin);
|
|
493
|
+
},
|
|
494
|
+
async *waitingFrom(identity) {
|
|
495
|
+
for (const r of q.waitingFrom.all(identity))
|
|
496
|
+
yield fromBytes(r.origin);
|
|
497
|
+
},
|
|
498
|
+
async *awaiting(envelope) {
|
|
499
|
+
for (const r of q.awaiting.all(fmt(envelope)))
|
|
500
|
+
yield fromBytes(r.origin);
|
|
501
|
+
},
|
|
502
|
+
cursor: {
|
|
503
|
+
async get() { return q.metaGet.get("cursor")?.value ?? 0; },
|
|
504
|
+
async set(n) { q.metaSet.run("cursor", n); },
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
async close() { db.close(); },
|
|
508
|
+
};
|
|
509
|
+
if (stale)
|
|
510
|
+
void store.edges.rebuild(); // synchronous body: done before we return
|
|
511
|
+
return store;
|
|
512
|
+
}
|
|
513
|
+
// ---------------------------------------------------------------- derivation
|
|
514
|
+
// Pure functions of blocks. open/append and rebuild share them, which is what
|
|
515
|
+
// makes the index reproducible.
|
|
516
|
+
const RAW_CODE = 0x55, GIT_RAW_CODE = 0x78;
|
|
517
|
+
function isObj(v) {
|
|
518
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && !isCID(v);
|
|
519
|
+
}
|
|
520
|
+
function isUpdate(b) {
|
|
521
|
+
return isCID(b.origin) && isCID(b.prev) && Number.isInteger(b.seq) && b.seq >= 1;
|
|
522
|
+
}
|
|
523
|
+
const str = (v) => (typeof v === "string" ? v : null);
|
|
524
|
+
const num = (v) => (typeof v === "number" ? v : null);
|
|
525
|
+
const cidBytes = (v) => (isCID(v) ? v.bytes : null);
|
|
526
|
+
function originMeta(cid, b) {
|
|
527
|
+
return {
|
|
528
|
+
origin: cid.bytes,
|
|
529
|
+
kind: str(b.kind),
|
|
530
|
+
thread: b.kind === "node" ? cidBytes(b.thread) : null,
|
|
531
|
+
launched_by: cidBytes(b.launchedBy),
|
|
532
|
+
at: num(b.at),
|
|
533
|
+
program: b.kind === "thread" ? cidBytes(b.program) : null,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function tipMeta(kind, tip) {
|
|
537
|
+
const tip_at = num(tip.at);
|
|
538
|
+
if (kind !== "thread")
|
|
539
|
+
return { state: null, until: null, waiting_on: null, tip_at, waiting_from: null, awaits: null };
|
|
540
|
+
const w = Array.isArray(tip.waitingOn) ? tip.waitingOn.filter(isCID).map(fmt) : null;
|
|
541
|
+
const a = Array.isArray(tip.awaits) ? tip.awaits.filter(isCID).map(fmt) : null;
|
|
542
|
+
return { state: str(tip.state), until: num(tip.until), waiting_on: w ? JSON.stringify(w) : null, tip_at, waiting_from: str(tip.waitingFrom), awaits: a?.length ? JSON.stringify(a) : null };
|
|
543
|
+
}
|
|
544
|
+
function edge(to, rel, locator) {
|
|
545
|
+
if (isCID(to))
|
|
546
|
+
return [{ to: fmt(to), rel, locator: str(locator) }];
|
|
547
|
+
if (typeof to === "string")
|
|
548
|
+
return [{ to, rel, locator: str(locator) }];
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
export function originEdges(b) {
|
|
552
|
+
const out = [];
|
|
553
|
+
if (Array.isArray(b.refs)) {
|
|
554
|
+
for (const r of b.refs)
|
|
555
|
+
if (isObj(r) && typeof r.rel === "string")
|
|
556
|
+
out.push(...edge(r.to, r.rel, r.locator));
|
|
557
|
+
}
|
|
558
|
+
out.push(...edge(b.launchedBy, "launched-by"));
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
export function updateEdges(u) {
|
|
562
|
+
const out = [];
|
|
563
|
+
const rest = isObj(u.rest) ? u.rest : {};
|
|
564
|
+
for (const w of [u.waitingOn, rest.waitingOn]) {
|
|
565
|
+
if (Array.isArray(w))
|
|
566
|
+
for (const t of w)
|
|
567
|
+
out.push(...edge(t, "depends-on"));
|
|
568
|
+
}
|
|
569
|
+
out.push(...edge(u.resolution, "resolves"));
|
|
570
|
+
if (isObj(u.emit) && u.emit.type === "launched")
|
|
571
|
+
out.push(...edge(u.emit.thread, "launched"));
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The edges an update's kept records declare (#37, the Zig kernel's
|
|
576
|
+
* index.zig `updateEdgesWithKept`): each kept record with `refs: [{to, rel,
|
|
577
|
+
* locator?}]` gives an edge with the record's own rel, after the update's own
|
|
578
|
+
* edges. `kept` is the update's kept records, resolved (undefined: not held
|
|
579
|
+
* or not a dag-cbor map). Readers of a Zig store use it.
|
|
580
|
+
*/
|
|
581
|
+
export function keptEdges(kept) {
|
|
582
|
+
const out = [];
|
|
583
|
+
for (const r of kept) {
|
|
584
|
+
if (!isObj(r) || !Array.isArray(r.refs))
|
|
585
|
+
continue;
|
|
586
|
+
for (const x of r.refs)
|
|
587
|
+
if (isObj(x) && typeof x.rel === "string")
|
|
588
|
+
out.push(...edge(x.to, x.rel, x.locator));
|
|
589
|
+
}
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
export function toRef(to, rel, locator) {
|
|
593
|
+
let target = to;
|
|
594
|
+
if (typeof to === "string" && !to.includes(":")) {
|
|
595
|
+
try {
|
|
596
|
+
target = parse(to);
|
|
597
|
+
}
|
|
598
|
+
catch { /* not a CID after all; hand back the string */ }
|
|
599
|
+
}
|
|
600
|
+
const ref = { to: target, rel };
|
|
601
|
+
if (typeof locator === "string")
|
|
602
|
+
ref.locator = locator;
|
|
603
|
+
return ref;
|
|
604
|
+
}
|