@dcasegr/skein 0.0.0-stage → 0.1.2
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 +525 -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 +782 -0
- package/dist/host/router.js +2411 -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,422 @@
|
|
|
1
|
+
// Router-held feeds (#33 part 2, #29's revision): long-lived subscriptions the
|
|
2
|
+
// router keeps on behalf of instances. A headers feed's headers go to the
|
|
3
|
+
// host chain (#156): the header chain the host keeps once, in its kernel
|
|
4
|
+
// process (`headers` frames, kernel.ts; a serve instance's in its own store);
|
|
5
|
+
// on a changed record the subscribed instance is sent the event
|
|
6
|
+
//
|
|
7
|
+
// chain {kind: "chain", root: <the host chain record>, height}
|
|
8
|
+
//
|
|
9
|
+
// in its box (Router.chainHeaders: one `headers` run per SSE event, the
|
|
10
|
+
// backfill's in runs of 2016; the per-instance `header` events are gone).
|
|
11
|
+
//
|
|
12
|
+
// (The broadcaster's proofs travel the same way, #65: {kind: "proof",
|
|
13
|
+
// subject, txid, path, …} in box `chain`, arc.ts; its other statuses are
|
|
14
|
+
// signed messages from the status provider. `statusOf` reads Arcade's JSON
|
|
15
|
+
// for it: {kind: "status", subject: <tx CID>, txid, txStatus, merklePath?:
|
|
16
|
+
// bytes, blockHeight?, blockHash?, extraInfo?}.)
|
|
17
|
+
//
|
|
18
|
+
// An instance declares its header feeds in its config (etc/config.json,
|
|
19
|
+
// `feeds`), which its genesis carries:
|
|
20
|
+
//
|
|
21
|
+
// {kind: "headers", url, box?} an SSE stream of block headers (ChainTracks-style):
|
|
22
|
+
// each event's data is the header's hex (160 digits), or
|
|
23
|
+
// JSON {header | raw | hex: "<hex>"}, or chaintracks' JSON
|
|
24
|
+
// {version, previousHash, merkleRoot, time, bits, nonce, height?, hash?}
|
|
25
|
+
// (Arcade's /chaintracks/v2/tip/stream; the hashes in display
|
|
26
|
+
// order: the 80 bytes serialized from the fields, and a `hash`
|
|
27
|
+
// that is not their sha256d is logged and dropped), or an array of them
|
|
28
|
+
//
|
|
29
|
+
// The host has one headers feed of its own besides (#102, SKEIN_HEADERS_URL;
|
|
30
|
+
// the Router's `headersFeed`): every enabled instance whose dispatch table
|
|
31
|
+
// takes events in box `chain` is subscribed to it, the others not; the router
|
|
32
|
+
// keeps that current as dispatch tables change (`host`). A genesis's `feeds`
|
|
33
|
+
// are in addition. The host listens to it itself too (#132, `listen`): every
|
|
34
|
+
// header it brings grows the default image's chain part (image-chain.ts).
|
|
35
|
+
//
|
|
36
|
+
// `box` defaults to "chain" (the wallet's sender-less subscription). Nothing
|
|
37
|
+
// here judges an item: the kernel's host chain tracker validates headers (and
|
|
38
|
+
// the chain app proofs). One SSE connection per URL, fanned out to every subscriber;
|
|
39
|
+
// reconnects with exponential backoff (and `Last-Event-ID`); each instance's
|
|
40
|
+
// queue of items not yet admitted holds at most `maxQueue` (the oldest go
|
|
41
|
+
// first, logged).
|
|
42
|
+
//
|
|
43
|
+
// The transaction half is the host's broadcaster (#58, #65, arc.ts): one
|
|
44
|
+
// Arcade subscription for the whole host over the same SSE client
|
|
45
|
+
// (SseStream), each proof and status routed to every instance that holds the
|
|
46
|
+
// transaction. The
|
|
47
|
+
// per-instance `arc-callback` feed is gone: a genesis that still declares one
|
|
48
|
+
// has it ignored.
|
|
49
|
+
import { Hash, Utils } from "@bsv/sdk";
|
|
50
|
+
import { CID } from "multiformats/cid";
|
|
51
|
+
import * as Digest from "multiformats/hashes/digest";
|
|
52
|
+
export const DEFAULT_BOX = "chain";
|
|
53
|
+
/** A transaction's CID: bitcoin-tx (0xb1), dbl-sha2-256 (0x56) over the txid in internal byte order. */
|
|
54
|
+
export function txCid(txid) {
|
|
55
|
+
return CID.createV1(0xb1, Digest.create(0x56, Uint8Array.from(Buffer.from(txid, "hex").reverse())));
|
|
56
|
+
}
|
|
57
|
+
/** The feeds a genesis declares (`feeds`), the malformed ones (and a pre-#58 `arc-callback`) left out. */
|
|
58
|
+
export function feedsOf(g) {
|
|
59
|
+
const fs = g?.feeds;
|
|
60
|
+
if (!Array.isArray(fs))
|
|
61
|
+
return [];
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const f of fs) {
|
|
64
|
+
const box = typeof f?.box === "string" && f.box ? f.box : undefined;
|
|
65
|
+
if (f?.kind === "headers" && typeof f.url === "string")
|
|
66
|
+
out.push({ kind: "headers", url: f.url, ...(box ? { box } : {}) });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The 80 bytes of a header given as chaintracks' JSON (Arcade's tip stream):
|
|
72
|
+
* version, previousHash, merkleRoot (display order: reversed into raw),
|
|
73
|
+
* time, bits, nonce — and, with `hash`, checked against their sha256d. Not
|
|
74
|
+
* that form: undefined. A `hash` that does not match: an Error saying so.
|
|
75
|
+
*/
|
|
76
|
+
export function headerOfFields(o) {
|
|
77
|
+
const u32 = (x) => typeof x === "number" && Number.isInteger(x) && x >= 0 && x <= 0xffffffff;
|
|
78
|
+
const h32 = (x) => typeof x === "string" && /^[0-9a-fA-F]{64}$/.test(x);
|
|
79
|
+
if (!u32(o.version) || !h32(o.previousHash) || !h32(o.merkleRoot) || !u32(o.time) || !u32(o.bits) || !u32(o.nonce))
|
|
80
|
+
return undefined;
|
|
81
|
+
const w = new Utils.Writer();
|
|
82
|
+
w.writeUInt32LE(o.version);
|
|
83
|
+
w.writeReverse(Utils.toArray(o.previousHash, "hex"));
|
|
84
|
+
w.writeReverse(Utils.toArray(o.merkleRoot, "hex"));
|
|
85
|
+
w.writeUInt32LE(o.time).writeUInt32LE(o.bits).writeUInt32LE(o.nonce);
|
|
86
|
+
const raw = w.toArray();
|
|
87
|
+
if (o.hash !== undefined) {
|
|
88
|
+
const hash = Utils.toHex(Hash.hash256(raw).reverse());
|
|
89
|
+
if (typeof o.hash !== "string" || o.hash.toLowerCase() !== hash)
|
|
90
|
+
return new Error(`header ${String(o.height ?? "?")}: its hash ${String(o.hash)} is not its fields' (${hash}): dropped`);
|
|
91
|
+
}
|
|
92
|
+
return Uint8Array.from(raw);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Header bytes out of one SSE event's data: hex, JSON with header/raw/hex,
|
|
96
|
+
* chaintracks' fields (headerOfFields), or an array of those. A header whose
|
|
97
|
+
* `hash` is not its fields' is left out, and `drop` told why.
|
|
98
|
+
*/
|
|
99
|
+
export function headersOf(data, drop) {
|
|
100
|
+
const hex = (s) => typeof s === "string" && /^[0-9a-fA-F]{160}$/.test(s) ? Uint8Array.from(Buffer.from(s, "hex")) : undefined;
|
|
101
|
+
const t = data.trim();
|
|
102
|
+
const direct = hex(t);
|
|
103
|
+
if (direct)
|
|
104
|
+
return [direct];
|
|
105
|
+
let v;
|
|
106
|
+
try {
|
|
107
|
+
v = JSON.parse(t);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
} // neither hex nor JSON: no header; the caller logs an event with none
|
|
112
|
+
const one = (x) => {
|
|
113
|
+
if (typeof x === "string")
|
|
114
|
+
return hex(x);
|
|
115
|
+
const o = x;
|
|
116
|
+
if (!o || typeof o !== "object")
|
|
117
|
+
return undefined;
|
|
118
|
+
const h = hex(o.header) ?? hex(o.raw) ?? hex(o.hex);
|
|
119
|
+
if (h)
|
|
120
|
+
return h;
|
|
121
|
+
const f = headerOfFields(o);
|
|
122
|
+
if (f instanceof Error) {
|
|
123
|
+
drop?.(f.message);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return f;
|
|
127
|
+
};
|
|
128
|
+
const list = Array.isArray(v) ? v : Array.isArray(v?.headers) ? v.headers : [v];
|
|
129
|
+
return list.map(one).filter((x) => !!x);
|
|
130
|
+
}
|
|
131
|
+
/** Arcade's (and ARC's) status JSON — an SSE event's data, a webhook's body — as a `status` event record, or why not. */
|
|
132
|
+
export function statusOf(v) {
|
|
133
|
+
const o = v;
|
|
134
|
+
if (!o || typeof o !== "object")
|
|
135
|
+
return "not a JSON object";
|
|
136
|
+
const txid = o.txid;
|
|
137
|
+
if (typeof txid !== "string" || !/^[0-9a-f]{64}$/i.test(txid))
|
|
138
|
+
return "no txid";
|
|
139
|
+
if (typeof o.txStatus !== "string" || !o.txStatus)
|
|
140
|
+
return "no txStatus";
|
|
141
|
+
const ev = { kind: "status", subject: txCid(txid.toLowerCase()), txid: txid.toLowerCase(), txStatus: o.txStatus };
|
|
142
|
+
if (typeof o.merklePath === "string" && o.merklePath) {
|
|
143
|
+
if (!/^([0-9a-f]{2})+$/i.test(o.merklePath))
|
|
144
|
+
return "merklePath is not hex";
|
|
145
|
+
ev.merklePath = Uint8Array.from(Buffer.from(o.merklePath, "hex"));
|
|
146
|
+
}
|
|
147
|
+
if (typeof o.blockHeight === "number")
|
|
148
|
+
ev.blockHeight = o.blockHeight;
|
|
149
|
+
if (typeof o.blockHash === "string" && o.blockHash)
|
|
150
|
+
ev.blockHash = o.blockHash;
|
|
151
|
+
if (typeof o.extraInfo === "string" && o.extraInfo)
|
|
152
|
+
ev.extraInfo = o.extraInfo;
|
|
153
|
+
return ev;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One SSE subscription held open (#33, #58): connects, parses the stream,
|
|
157
|
+
* hands each event to `onEvent` — awaited before the next one is read and
|
|
158
|
+
* before its id counts as seen, so a caller that remembers `lastId` after it
|
|
159
|
+
* never skips an event — and reconnects with backoff, sending the last id
|
|
160
|
+
* seen as `Last-Event-ID`.
|
|
161
|
+
*/
|
|
162
|
+
export class SseStream {
|
|
163
|
+
url;
|
|
164
|
+
lastId;
|
|
165
|
+
abort = new AbortController();
|
|
166
|
+
stopped = false;
|
|
167
|
+
o;
|
|
168
|
+
onEvent;
|
|
169
|
+
running;
|
|
170
|
+
constructor(url, onEvent, o = {}) {
|
|
171
|
+
this.url = url;
|
|
172
|
+
this.onEvent = onEvent;
|
|
173
|
+
this.o = o;
|
|
174
|
+
this.lastId = o.lastId;
|
|
175
|
+
}
|
|
176
|
+
start() { this.running ??= this.connect(); return this; }
|
|
177
|
+
/** Close the connection; no reconnect. Resolves once the loop has ended (an event being handled finishes first). */
|
|
178
|
+
async stop() { this.stopped = true; this.abort.abort(); await this.running; }
|
|
179
|
+
say(line) { this.o.log?.(line); }
|
|
180
|
+
async connect() {
|
|
181
|
+
const { min, max } = this.o.backoff ?? { min: 500, max: 30_000 };
|
|
182
|
+
let wait = min;
|
|
183
|
+
while (!this.stopped) {
|
|
184
|
+
try {
|
|
185
|
+
const res = await (this.o.fetch ?? fetch)(this.url, {
|
|
186
|
+
headers: { ...this.o.headers, accept: "text/event-stream", ...(this.lastId !== undefined ? { "last-event-id": this.lastId } : {}) },
|
|
187
|
+
signal: this.abort.signal,
|
|
188
|
+
});
|
|
189
|
+
if (!res.ok || !res.body)
|
|
190
|
+
throw new Error(`HTTP ${res.status}`);
|
|
191
|
+
this.say(`connected${this.lastId !== undefined ? ` (after event ${this.lastId})` : ""}`);
|
|
192
|
+
await this.read(res.body, () => { wait = min; });
|
|
193
|
+
if (this.stopped)
|
|
194
|
+
return;
|
|
195
|
+
this.say(`closed; reconnecting in ${wait} ms`);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
if (this.stopped)
|
|
199
|
+
return;
|
|
200
|
+
this.say(`${e.message}; reconnecting in ${wait} ms`);
|
|
201
|
+
}
|
|
202
|
+
await new Promise((r) => { const t = setTimeout(r, wait); this.abort.signal.addEventListener("abort", () => { clearTimeout(t); r(); }, { once: true }); });
|
|
203
|
+
wait = Math.min(max, wait * 2);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Parse the event stream (text/event-stream): `data:` lines until a blank line make one event; `id:` and `event:` go with it. */
|
|
207
|
+
async read(body, onData) {
|
|
208
|
+
const dec = new TextDecoder();
|
|
209
|
+
let buf = "";
|
|
210
|
+
let data = [];
|
|
211
|
+
let id;
|
|
212
|
+
let event;
|
|
213
|
+
const reader = body.getReader();
|
|
214
|
+
try {
|
|
215
|
+
for (;;) {
|
|
216
|
+
const { value, done } = await reader.read();
|
|
217
|
+
if (done)
|
|
218
|
+
return;
|
|
219
|
+
buf += dec.decode(value, { stream: true });
|
|
220
|
+
let nl;
|
|
221
|
+
while ((nl = buf.search(/\r\n|\n|\r/)) >= 0) {
|
|
222
|
+
const line = buf.slice(0, nl);
|
|
223
|
+
buf = buf.slice(nl + (buf.startsWith("\r\n", nl) ? 2 : 1));
|
|
224
|
+
if (line === "") {
|
|
225
|
+
if (data.length) {
|
|
226
|
+
onData();
|
|
227
|
+
try {
|
|
228
|
+
await this.onEvent({ data: data.join("\n"), ...(id !== undefined ? { id } : {}), ...(event !== undefined ? { event } : {}) });
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
this.say(`an event not taken: ${e.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (this.stopped)
|
|
235
|
+
return;
|
|
236
|
+
if (id !== undefined)
|
|
237
|
+
this.lastId = id;
|
|
238
|
+
data = [];
|
|
239
|
+
id = undefined;
|
|
240
|
+
event = undefined;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (line.startsWith(":"))
|
|
244
|
+
continue;
|
|
245
|
+
const c = line.indexOf(":");
|
|
246
|
+
const field = c < 0 ? line : line.slice(0, c);
|
|
247
|
+
const v = c < 0 ? "" : line.slice(c + 1).replace(/^ /, "");
|
|
248
|
+
if (field === "data")
|
|
249
|
+
data.push(v);
|
|
250
|
+
else if (field === "id")
|
|
251
|
+
id = v;
|
|
252
|
+
else if (field === "event")
|
|
253
|
+
event = v;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
reader.releaseLock?.();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Headers per backfill run (#141): one `headers` frame each, parents first (#156). */
|
|
263
|
+
export const BACKFILL_RUN = 2016;
|
|
264
|
+
export class Feeds {
|
|
265
|
+
o;
|
|
266
|
+
sse = new Map();
|
|
267
|
+
declared = new Map();
|
|
268
|
+
hosted = new Map();
|
|
269
|
+
listeners = new Map();
|
|
270
|
+
queues = new Map();
|
|
271
|
+
stopped = false;
|
|
272
|
+
constructor(o) { this.o = o; }
|
|
273
|
+
say(source, line) { this.o.log?.(source, line); }
|
|
274
|
+
/** `handle`'s feeds are these (replacing what it declared before). */
|
|
275
|
+
declare(handle, specs) {
|
|
276
|
+
if (this.stopped)
|
|
277
|
+
return;
|
|
278
|
+
this.declared.set(handle, specs);
|
|
279
|
+
this.subscribe(handle);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The host's own feed (#102) for `handle`: subscribed to it (`spec`), or not (undefined); a genesis's feeds are kept.
|
|
283
|
+
* `backfill` (#141): on subscribing, the headers the instance lacks up to the host's tip — read after the
|
|
284
|
+
* subscription is in place; until they are queued, the live items wait behind them (held), and a live header
|
|
285
|
+
* the backfill carries too is dropped. Admitted first, in runs of BACKFILL_RUN, then the live stream.
|
|
286
|
+
*/
|
|
287
|
+
host(handle, spec, backfill) {
|
|
288
|
+
if (this.stopped)
|
|
289
|
+
return Promise.resolve();
|
|
290
|
+
if (spec)
|
|
291
|
+
this.hosted.set(handle, spec);
|
|
292
|
+
else
|
|
293
|
+
this.hosted.delete(handle);
|
|
294
|
+
if (!spec || !backfill) {
|
|
295
|
+
this.subscribe(handle);
|
|
296
|
+
return Promise.resolve();
|
|
297
|
+
}
|
|
298
|
+
const q = this.queue(handle);
|
|
299
|
+
q.held = true;
|
|
300
|
+
this.subscribe(handle);
|
|
301
|
+
return backfill().then((raws) => {
|
|
302
|
+
if (!raws.length)
|
|
303
|
+
return;
|
|
304
|
+
const have = new Set(raws.map((r) => Buffer.from(r).toString("hex")));
|
|
305
|
+
q.items = q.items.map((x) => ({ ...x, raws: x.raws.filter((r) => !have.has(Buffer.from(r).toString("hex"))) })).filter((x) => x.raws.length > 0);
|
|
306
|
+
const runs = [];
|
|
307
|
+
for (let i = 0; i < raws.length; i += BACKFILL_RUN)
|
|
308
|
+
runs.push({ box: spec.box ?? DEFAULT_BOX, raws: raws.slice(i, i + BACKFILL_RUN) });
|
|
309
|
+
q.items.unshift(...runs);
|
|
310
|
+
this.say(handle, `feed: ${raws.length} header${raws.length === 1 ? "" : "s"} it lacks pushed first (the backfill), then the live stream`);
|
|
311
|
+
}).catch((e) => this.say(handle, `feed: the backfill not read: ${e.message}`)).finally(() => {
|
|
312
|
+
q.held = false;
|
|
313
|
+
if (!q.running && q.items.length)
|
|
314
|
+
void this.drain(handle, q);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
queue(handle) {
|
|
318
|
+
let q = this.queues.get(handle);
|
|
319
|
+
if (!q)
|
|
320
|
+
this.queues.set(handle, (q = { items: [], running: false, dropped: 0 }));
|
|
321
|
+
return q;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The host's own listener on a headers feed (#132: the default image's
|
|
325
|
+
* chain part grows by it): every header the stream brings, as well as the
|
|
326
|
+
* subscribers'. The connection stays open while a listener is on it.
|
|
327
|
+
*/
|
|
328
|
+
listen(url, fn) {
|
|
329
|
+
if (this.stopped)
|
|
330
|
+
return;
|
|
331
|
+
this.listeners.set(url, fn);
|
|
332
|
+
this.connect(url);
|
|
333
|
+
}
|
|
334
|
+
/** Whether `handle` is subscribed to the host's own feed. */
|
|
335
|
+
hosts(handle) { return this.hosted.has(handle); }
|
|
336
|
+
/** Subscribe `handle` to what it declared and the host's feed for it, and to nothing else. */
|
|
337
|
+
subscribe(handle) {
|
|
338
|
+
const h = this.hosted.get(handle);
|
|
339
|
+
const specs = [...(this.declared.get(handle) ?? []), ...(h ? [h] : [])];
|
|
340
|
+
for (const [url, s] of this.sse) {
|
|
341
|
+
if (s.subscribers.has(handle) && !specs.some((f) => f.kind === "headers" && f.url === url))
|
|
342
|
+
s.subscribers.delete(handle);
|
|
343
|
+
if (!s.subscribers.size && !this.listeners.has(url)) {
|
|
344
|
+
void s.stream.stop();
|
|
345
|
+
this.sse.delete(url);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const f of specs) {
|
|
349
|
+
if (f.kind !== "headers")
|
|
350
|
+
continue;
|
|
351
|
+
this.connect(f.url).subscribers.set(handle, f.box ?? DEFAULT_BOX);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** The one connection to `url`, opened if it is not. */
|
|
355
|
+
connect(url) {
|
|
356
|
+
let s = this.sse.get(url);
|
|
357
|
+
if (!s) {
|
|
358
|
+
const subscribers = new Map();
|
|
359
|
+
const stream = new SseStream(url, (ev) => this.dispatch(url, subscribers, ev.data), {
|
|
360
|
+
backoff: this.o.backoff, fetch: this.o.fetch, log: (l) => this.say("router", `feed ${url}: ${l}`),
|
|
361
|
+
});
|
|
362
|
+
s = { stream, subscribers };
|
|
363
|
+
this.sse.set(url, s);
|
|
364
|
+
stream.start();
|
|
365
|
+
}
|
|
366
|
+
return s;
|
|
367
|
+
}
|
|
368
|
+
/** The declared feeds of `handle`. */
|
|
369
|
+
of(handle) { return this.declared.get(handle) ?? []; }
|
|
370
|
+
/** Close every connection (no reconnect); resolves once their loops have ended. */
|
|
371
|
+
async stop() {
|
|
372
|
+
this.stopped = true;
|
|
373
|
+
const streams = [...this.sse.values()].map((s) => s.stream.stop());
|
|
374
|
+
this.sse.clear();
|
|
375
|
+
await Promise.all(streams);
|
|
376
|
+
}
|
|
377
|
+
/** Queue a run of headers for `handle` (bounded), and drain the queue serially. */
|
|
378
|
+
push(handle, box, raws) {
|
|
379
|
+
const q = this.queue(handle);
|
|
380
|
+
q.items.push({ box, raws });
|
|
381
|
+
const max = this.o.maxQueue ?? 1000;
|
|
382
|
+
if (q.items.length > max) {
|
|
383
|
+
const n = q.items.length - max;
|
|
384
|
+
q.items.splice(0, n);
|
|
385
|
+
q.dropped += n;
|
|
386
|
+
this.say(handle, `feed: queue full (${max}): dropped the ${n} oldest (${q.dropped} so far)`);
|
|
387
|
+
}
|
|
388
|
+
if (!q.running && !q.held)
|
|
389
|
+
void this.drain(handle, q);
|
|
390
|
+
}
|
|
391
|
+
/** How many items wait for `handle` (tests). */
|
|
392
|
+
pending(handle) { return this.queues.get(handle)?.items.length ?? 0; }
|
|
393
|
+
async drain(handle, q) {
|
|
394
|
+
q.running = true;
|
|
395
|
+
try {
|
|
396
|
+
while (q.items.length && !this.stopped && !q.held) {
|
|
397
|
+
const { box, raws } = q.items.shift();
|
|
398
|
+
try {
|
|
399
|
+
await this.o.headers(handle, box, raws);
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
this.say(handle, `feed: ${raws.length} header${raws.length === 1 ? "" : "s"} not taken: ${e.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
q.running = false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
dispatch(url, subscribers, data) {
|
|
411
|
+
let dropped = false;
|
|
412
|
+
const hs = headersOf(data, (why) => { dropped = true; this.say("router", `feed ${url}: ${why}`); });
|
|
413
|
+
if (!hs.length) {
|
|
414
|
+
if (!dropped)
|
|
415
|
+
this.say("router", `feed ${url}: an event with no header in it: ignored`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
this.listeners.get(url)?.(hs);
|
|
419
|
+
for (const [handle, box] of subscribers)
|
|
420
|
+
this.push(handle, box, hs);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
import type { Stamp } from "../runtime/syscalls.ts";
|
|
3
|
+
import { type DoorAnswer } from "./door-answer.ts";
|
|
4
|
+
import type { KernelSurface as Kernel, RequestAnswer } from "./kernel.ts";
|
|
5
|
+
/** A raw request as the host appends it: `path` as the client signed it, `route` what the routes table sees. */
|
|
6
|
+
export interface FrontRequest {
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
route?: string;
|
|
10
|
+
query: string;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
body: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
export interface FrontAnswer {
|
|
15
|
+
status: number;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
body: Uint8Array;
|
|
18
|
+
/** The request's entry and the thread it launched (absent: not processed within the wait). */
|
|
19
|
+
entry?: CID;
|
|
20
|
+
thread?: CID;
|
|
21
|
+
/** Fuel of the door's filters and the answering call, when the answer was the door's or a read (the steps' fuel is on their updates). */
|
|
22
|
+
fuel?: number;
|
|
23
|
+
/**
|
|
24
|
+
* The caller (hex) of an answer the host metered: a read after the request's thread (its
|
|
25
|
+
* answer's `read.caller`), or the principal the door's filters yielded for an answer it gave
|
|
26
|
+
* without an entry. Absent when the request named none.
|
|
27
|
+
*/
|
|
28
|
+
caller?: string;
|
|
29
|
+
}
|
|
30
|
+
/** How long a synchronous client waits on its request's thread by default (ms): SKEIN_ANSWER_WAIT_MS, else two minutes. */
|
|
31
|
+
export declare const ANSWER_WAIT_MS: number;
|
|
32
|
+
/** The Retry-After (s) a client that waited past the bound, or met a host shutting down, is told. */
|
|
33
|
+
export declare const RETRY_AFTER_S = 5;
|
|
34
|
+
/** 503 + Retry-After: the wait's bound passed (the thread goes on), or the host is shutting down. */
|
|
35
|
+
export declare function unavailable(description: string, extra?: Partial<FrontAnswer>): FrontAnswer;
|
|
36
|
+
/** Lower-cased header names, one value each (joined with ", "). */
|
|
37
|
+
export declare function headerMap(h: Record<string, string | string[] | undefined> | Headers): Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* Append a package as received (#68): one `request` entry naming it and its
|
|
40
|
+
* transport. The record goes with the frame, not into the store (#121): the
|
|
41
|
+
* kernel's door matches the route, runs its filters and the gate, and puts
|
|
42
|
+
* the package as it hands it back (a BEEF's bytes never stored), so the entry
|
|
43
|
+
* written — its CID, returned — may name another record than this. A package
|
|
44
|
+
* the door turns away or answers writes nothing (#143): DoorAnswered.
|
|
45
|
+
*/
|
|
46
|
+
export declare function appendRequest(k: Kernel, transport: string, record: Record<string, unknown>, time?: Stamp): Promise<CID>;
|
|
47
|
+
/** An HTTP request's record (log.zig: {kind: "http", method, path, route, query, headers, body}). */
|
|
48
|
+
export declare function httpRecord(req: FrontRequest): Record<string, unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* One HTTP request through the instance: appended, then waited on (#66)
|
|
51
|
+
* until its thread comes to rest or `waitMs` passes (503 + Retry-After).
|
|
52
|
+
* `stop`, when it settles first, answers 503 too (the host shutting down).
|
|
53
|
+
*/
|
|
54
|
+
export declare function frontDoor(k: Kernel, req: FrontRequest, o?: {
|
|
55
|
+
now?: Stamp;
|
|
56
|
+
waitMs?: number;
|
|
57
|
+
stop?: Promise<unknown>;
|
|
58
|
+
}): Promise<FrontAnswer>;
|
|
59
|
+
/**
|
|
60
|
+
* A request thread's answer as the HTTP response: its last step's, a read after it, or 500/503.
|
|
61
|
+
* `req`, the request as received: a door refusal of a signed one is answered by the front door.
|
|
62
|
+
*/
|
|
63
|
+
export declare function httpAnswer(k: Kernel, entry: CID, a: RequestAnswer, req?: FrontRequest): Promise<FrontAnswer>;
|
|
64
|
+
/**
|
|
65
|
+
* The door's answer without an entry (#143: a filter's rejection or answer — a read route's — or the
|
|
66
|
+
* gate's refusal), as the HTTP response: a signed request gets it signed on its session — a
|
|
67
|
+
* front-door call (fn "respond") with the request as received, at the request's time — and an
|
|
68
|
+
* unsigned one, or one whose session does not verify (unknown, expired: the stock client shakes
|
|
69
|
+
* hands again), gets it plain.
|
|
70
|
+
*/
|
|
71
|
+
export declare function doorAnswer(k: Kernel, d: DoorAnswer, req: FrontRequest, now?: Stamp): Promise<FrontAnswer>;
|
|
72
|
+
/** The BRC-104 handshake's path: a route of its own (the front door's fn "handshake"). */
|
|
73
|
+
export declare const HANDSHAKE_PATH = "/.well-known/auth";
|
|
74
|
+
/** Whether a request carries BRC-104 headers (x-bsv-auth-*): a signed request. */
|
|
75
|
+
export declare const isSigned: (headers: Record<string, string>) => boolean;
|
|
76
|
+
/**
|
|
77
|
+
* One HTTP request through the kernel's door (#143): its route's filters decide — admitted and
|
|
78
|
+
* waited on, or answered at once with nothing logged (a read route; a rejection; the gate's refusal).
|
|
79
|
+
*/
|
|
80
|
+
export declare function serveHttp(k: Kernel, req: FrontRequest, o?: {
|
|
81
|
+
now?: Stamp;
|
|
82
|
+
waitMs?: number;
|
|
83
|
+
stop?: Promise<unknown>;
|
|
84
|
+
}): Promise<FrontAnswer>;
|
|
85
|
+
/** A fetch over one instance's door (serveHttp), no socket: for a client in a test, or a URL of the host's own. */
|
|
86
|
+
export declare function frontDoorFetch(k: Kernel, o?: {
|
|
87
|
+
now?: () => Stamp;
|
|
88
|
+
route?: (path: string) => string;
|
|
89
|
+
onAnswer?: (a: FrontAnswer) => void;
|
|
90
|
+
waitMs?: number;
|
|
91
|
+
}): typeof fetch;
|
|
92
|
+
export declare const cidOf: (v: unknown) => CID;
|