@dcasegr/skein 0.1.1 → 0.2.8
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 +41 -1
- package/dist/host/arc.d.ts +12 -2
- package/dist/host/arc.js +41 -13
- package/dist/host/cli.d.ts +5 -0
- package/dist/host/cli.js +37 -4
- package/dist/host/fake-teranode.d.ts +29 -0
- package/dist/host/fake-teranode.js +100 -0
- package/dist/host/feeds.d.ts +9 -1
- package/dist/host/feeds.js +30 -5
- package/dist/host/image-chain.d.ts +30 -0
- package/dist/host/image-chain.js +84 -2
- package/dist/host/instances.d.ts +19 -0
- package/dist/host/instances.js +38 -1
- package/dist/host/liveness.d.ts +22 -2
- package/dist/host/liveness.js +42 -5
- package/dist/host/p2p.d.ts +107 -8
- package/dist/host/p2p.js +298 -53
- package/dist/host/router.d.ts +83 -1
- package/dist/host/router.js +248 -33
- package/dist/host/signer.d.ts +6 -0
- package/dist/host/signer.js +8 -0
- package/dist/host/teranode.d.ts +132 -0
- package/dist/host/teranode.js +406 -0
- package/dist/host/testhost.d.ts +5 -0
- package/dist/host/testhost.js +8 -3
- package/dist/host/wasm-kernel.d.ts +9 -0
- package/dist/host/wasm-kernel.js +46 -0
- package/package.json +4 -1
package/dist/host/router.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { CID } from "multiformats/cid";
|
|
|
4
4
|
import type { Stamp } from "../runtime/syscalls.ts";
|
|
5
5
|
import { Broadcaster, type ArcConfig } from "./arc.ts";
|
|
6
6
|
import { Feeds, type FeedSpec } from "./feeds.ts";
|
|
7
|
+
import { TeranodeTracker } from "./teranode.ts";
|
|
7
8
|
import { Cron } from "./cron.ts";
|
|
8
9
|
import { type DispatchRow } from "../runtime/dispatch.ts";
|
|
9
10
|
import { type BootSource, type Booted } from "./boot.ts";
|
|
@@ -11,7 +12,7 @@ import { ImageChain } from "./image-chain.ts";
|
|
|
11
12
|
import { OverlaySync } from "./sync.ts";
|
|
12
13
|
import { type AddressSeed, type DispatchSpec, type Genesis2Config, type Libp2pSpec } from "./genesis.ts";
|
|
13
14
|
import { HANDLE, type HostDb, type InstanceRow } from "./instances.ts";
|
|
14
|
-
import { type KernelSurface } from "./kernel.ts";
|
|
15
|
+
import { type HostChainTip, type KernelSurface } from "./kernel.ts";
|
|
15
16
|
import { type Live } from "./liveness.ts";
|
|
16
17
|
import { EventStreams } from "./events.ts";
|
|
17
18
|
import { P2PHost, type Beacon, type InboundAnswer, type InboundCall, type P2PHostConfig, type Subscription } from "./p2p.ts";
|
|
@@ -53,6 +54,21 @@ export interface RouterOptions {
|
|
|
53
54
|
};
|
|
54
55
|
/** The host's headers feed (#102, SKEIN_HEADERS_URL): an SSE stream of block headers every enabled instance whose dispatch table takes events in box `chain` is subscribed to. */
|
|
55
56
|
headersFeed?: string;
|
|
57
|
+
/**
|
|
58
|
+
* #156: the host's headers tracker over the Teranode P2P network (teranode.ts): its bootstrap
|
|
59
|
+
* peers (the network's, SKEIN_TERANODE_BOOTSTRAP), its first-sync hub (the network's default,
|
|
60
|
+
* SKEIN_HEADERS_HUB), its key file ($SKEIN_HOME/teranode.key). Beside the headers feed or alone:
|
|
61
|
+
* both feed the same intake. Absent (SKEIN_TERANODE=off, or a network with neither): no tracker.
|
|
62
|
+
*/
|
|
63
|
+
teranode?: {
|
|
64
|
+
bootstrap: string[];
|
|
65
|
+
hub?: string;
|
|
66
|
+
peersFile?: string;
|
|
67
|
+
page?: number;
|
|
68
|
+
bulkPage?: number;
|
|
69
|
+
maxCrawl?: number;
|
|
70
|
+
discoveryMs?: number;
|
|
71
|
+
};
|
|
56
72
|
/** The host's Arcade (#58, #65, arc.ts): where the broadcast events go, the status subscription, the `status` provider. Absent: no broadcaster (a broadcast event is dropped). */
|
|
57
73
|
arc?: ArcConfig;
|
|
58
74
|
/** Tests: how the broadcaster reaches Arcade. */
|
|
@@ -111,6 +127,11 @@ export interface RouterOptions {
|
|
|
111
127
|
libp2p?: P2PHostConfig;
|
|
112
128
|
/** An instance's libp2p peer key (signer.ts peerKey). Absent: no libp2p host; the kernels' `libp2p` is refused. */
|
|
113
129
|
peerKeyFor?(handle: string): PrivateKey;
|
|
130
|
+
/**
|
|
131
|
+
* #156: the host node's peer key (P2PHost.hostNode: the Teranode tracker rides it; on a one-key
|
|
132
|
+
* host every instance's peer key, so they share it). Absent: a fresh key per start.
|
|
133
|
+
*/
|
|
134
|
+
hostPeerKey?: PrivateKey;
|
|
114
135
|
/**
|
|
115
136
|
* A provider's key (#70, signer.ts providerKey: the HTTP proxy `fetch`, the
|
|
116
137
|
* `waker`, `cron`, `libp2p`, `status`), which each new genesis's address
|
|
@@ -150,6 +171,17 @@ export interface RouterOptions {
|
|
|
150
171
|
* stock wiring takes, from `handle`, handed over after its step's commit. Absent: logged as ignored.
|
|
151
172
|
*/
|
|
152
173
|
appEvent?(handle: string, record: Record<string, unknown>): void;
|
|
174
|
+
/**
|
|
175
|
+
* An application host's reads (shruggr/open-exchange 0.11.0, the public read API): a GET for an
|
|
176
|
+
* instance (its hostname, `/@<handle>/`, or the root instance's hostname), asked AFTER the host's own
|
|
177
|
+
* reads there (the instance card, `/<app>/.live/<topic>`) and BEFORE the request is forwarded to the
|
|
178
|
+
* instance's program. `route` is the path at the instance (`/amm/pools`); `undefined`: not the host's,
|
|
179
|
+
* forwarded as before. An answer with `stream` is held open (RouterResponse). What the host needs
|
|
180
|
+
* for its reads is the Router's public surface: `dispatch` (in-process reads of the instances),
|
|
181
|
+
* `p2p.live.live(handle, topic)` (the beats kept), `events.subscribe(handle, query, send, close)` (an
|
|
182
|
+
* instance's emitted events), `o.db.list("enabled")` (the instances). Absent: nothing is asked.
|
|
183
|
+
*/
|
|
184
|
+
appRead?(handle: string, route: string, url: URL, req: RouterRequest): Promise<RouterResponse | undefined>;
|
|
153
185
|
}
|
|
154
186
|
interface Loaded {
|
|
155
187
|
row: InstanceRow;
|
|
@@ -180,6 +212,13 @@ export interface RouterResponse {
|
|
|
180
212
|
status: number;
|
|
181
213
|
headers: Record<string, string>;
|
|
182
214
|
body: Uint8Array;
|
|
215
|
+
/**
|
|
216
|
+
* A response held open (an event stream an `appRead` answers): on a socket (Router.handler) the
|
|
217
|
+
* status and headers are written, then `stream` is called with `send` (writes a chunk) and `end`
|
|
218
|
+
* (closes the response); the function it returns is called once when the client goes. `body` is
|
|
219
|
+
* not written. In process (Router.dispatch's caller) it is not called: the caller gets `body`.
|
|
220
|
+
*/
|
|
221
|
+
stream?(send: (chunk: string) => void, end: () => void): () => void;
|
|
183
222
|
}
|
|
184
223
|
export { HANDLE };
|
|
185
224
|
/** Handles no instance may take on this host (H3), besides the router origin's own first label (Router.reserved): `id` (the router's own in production) and `host` (the host skein's). */
|
|
@@ -262,6 +301,13 @@ export declare class Router {
|
|
|
262
301
|
private control?;
|
|
263
302
|
/** The default image (#89), with its chain part (#132): what createInstance boots a new skein from. */
|
|
264
303
|
readonly image: ImageChain;
|
|
304
|
+
/** #156: the headers tracker over the Teranode mesh (teranode.ts), when the host runs one. */
|
|
305
|
+
tracker?: TeranodeTracker;
|
|
306
|
+
/** #156: the libp2p the host node runs on: the instances' (`p2p`), else one for the tracker alone. */
|
|
307
|
+
hostP2p?: P2PHost;
|
|
308
|
+
private readonly p2pConfig;
|
|
309
|
+
/** #156: the header intake, one run at a time (the image chain, then the host chains). */
|
|
310
|
+
private intake;
|
|
265
311
|
/** #130: each billed instance as last read (its host row's terms, the kernel's billing state), and what this host metered since its last tick. */
|
|
266
312
|
private views;
|
|
267
313
|
private periods;
|
|
@@ -392,6 +438,33 @@ export declare class Router {
|
|
|
392
438
|
* libp2p arrivals, which do not queue here, need no queue.
|
|
393
439
|
*/
|
|
394
440
|
serial<T>(handle: string, f: () => Promise<T>): Promise<T>;
|
|
441
|
+
/** #156: the key the host's headers arrive under (feeds.ts): the feed's URL, else the tracker's own. */
|
|
442
|
+
headersKey(): string | undefined;
|
|
443
|
+
/** #156: the Teranode tracker started: its node on the mesh, its first sync begun (`tracker.ready`). */
|
|
444
|
+
private startTracker;
|
|
445
|
+
/**
|
|
446
|
+
* #156: a run of headers from either source (the feed's, `at` undefined: image-chain.ts places
|
|
447
|
+
* each; the tracker's, at height `at`): into the image's chain part, then every host chain synced
|
|
448
|
+
* from it. One run at a time.
|
|
449
|
+
*/
|
|
450
|
+
takeHeaders(raws: Uint8Array[], at?: number): Promise<void>;
|
|
451
|
+
/** #156: the host chains this host keeps once per database (its SQLite one, its RocksDB one, and those its rows name). */
|
|
452
|
+
private hostHolders;
|
|
453
|
+
/** #156: each host chain this host keeps (by database) and its tip (undefined: empty). */
|
|
454
|
+
hostChainTips(): Promise<Array<{
|
|
455
|
+
name: string;
|
|
456
|
+
tip: HostChainTip | undefined;
|
|
457
|
+
}>>;
|
|
458
|
+
/** #156: every host chain brought to the image chain's tip (from `from` when a run replaced from there). */
|
|
459
|
+
private syncHolders;
|
|
460
|
+
/**
|
|
461
|
+
* #156: a host chain brought to the image chain: an EMPTY one imports the image's chain part
|
|
462
|
+
* (`chain-import`, as an instance born with it would bring it: one frame) where its kernel can
|
|
463
|
+
* read it (the wasm kernel's SQLite database), else takes the image's headers from genesis in
|
|
464
|
+
* `headers` runs; one behind takes the headers it lacks; one the image does not hold at its
|
|
465
|
+
* height (a fork) from a little below it, or from `from`.
|
|
466
|
+
*/
|
|
467
|
+
private syncHolder;
|
|
395
468
|
/**
|
|
396
469
|
* The network the host chain is on: SKEIN_NETWORK (`network`), else the genesis defaults'
|
|
397
470
|
* `walletNetwork` (a test host's), else the kernel's default (main).
|
|
@@ -732,6 +805,15 @@ export declare class Router {
|
|
|
732
805
|
* in a wallet tracks them). Unsigned, anyone; mailbox instances are not listed.
|
|
733
806
|
*/
|
|
734
807
|
private skeinsHeldBy;
|
|
808
|
+
/**
|
|
809
|
+
* GET /.well-known/skein at an instance's origin (its hostname, `/@<handle>/`, or the root instance's
|
|
810
|
+
* hostname): `{handle, identity, origin, claimed}` — `claimed` whether its head `grants` names a root
|
|
811
|
+
* holder (an image instance nobody has claimed: false). Read from its store read-only, no kernel, as
|
|
812
|
+
* /skeins does. Unsigned, anyone; 404 for no instance. "Am I root" is /skeins?key=<identity>.
|
|
813
|
+
*/
|
|
814
|
+
private instanceCard;
|
|
815
|
+
/** The keys an instance's head `grants` names root (#143), its store read read-only; none when it has no store yet. */
|
|
816
|
+
private rootsOf;
|
|
735
817
|
/** A forwarded request, held among the in-flight ones (shutdown answers them 503). */
|
|
736
818
|
private held;
|
|
737
819
|
/**
|
package/dist/host/router.js
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
// With no host skein: 404 (tests may answer them with a fixture: `discovery`).
|
|
28
28
|
// GET /skeins?key=<hex> at the host's own origin: the skeins here whose head `grants` names the key
|
|
29
29
|
// among root's holders, [{handle, identity, url}] (no wallet token tracks them)
|
|
30
|
+
// GET /.well-known/skein at an instance's origin (its hostname, /@<handle>/, the root instance's): {handle,
|
|
31
|
+
// identity, origin, claimed} — claimed: its head `grants` names a root holder
|
|
30
32
|
// GET /.well-known/skein-host at every host name, an instance's too (#103): {origin, domain} — this
|
|
31
33
|
// router's origin and the handle domain (the onboarding app's
|
|
32
34
|
// config.onboard.domain), for a page an instance serves (the management
|
|
@@ -115,7 +117,8 @@ import { CID } from "multiformats/cid";
|
|
|
115
117
|
import { rootIdentity } from "../runtime/identity.js";
|
|
116
118
|
import { DEFAULTS, short, stampMs } from "../runtime/log.js";
|
|
117
119
|
import { ARC_ROUTE, Broadcaster } from "./arc.js";
|
|
118
|
-
import { DEFAULT_BOX, Feeds, feedsOf, txCid } from "./feeds.js";
|
|
120
|
+
import { BACKFILL_RUN, DEFAULT_BOX, Feeds, feedsOf, txCid } from "./feeds.js";
|
|
121
|
+
import { hashOf, TeranodeTracker } from "./teranode.js";
|
|
119
122
|
import { Cron, dbSchedules } from "./cron.js";
|
|
120
123
|
import { currentDispatch, takesEvent, takesMail } from "../runtime/dispatch.js";
|
|
121
124
|
import { isSet, parseRocksSpec, parseSqliteSpec, storeSpec } from "../runtime/index-store.js";
|
|
@@ -134,9 +137,9 @@ import { keyBytes, keyHex } from "./genesis.js";
|
|
|
134
137
|
import { HANDLE, pathOnly, TOKEN_HANDLE, topicsOf, validHandle } from "./instances.js";
|
|
135
138
|
import { signClaim } from "../client/raw.js";
|
|
136
139
|
import { DoorAnswered, Kernel, KernelHost } from "./kernel.js";
|
|
137
|
-
import { foldLiveness, livenessEvent, livenessOf } from "./liveness.js";
|
|
140
|
+
import { foldLiveness, LIVENESS_MAX_MS, livenessEvent, livenessOf } from "./liveness.js";
|
|
138
141
|
import { EventStreams, eventsQuery, sseMessage } from "./events.js";
|
|
139
|
-
import { beaconEvent, beaconsOf,
|
|
142
|
+
import { beaconEvent, beaconsOf, LOOPBACK_LISTEN, emittedEvents, foldBeacons, foldSubscriptions, libp2pConfig, P2PHost, subscribedTopics, subscriptionEvent, subscriptionsOf } from "./p2p.js";
|
|
140
143
|
import { peerIdFromMultihash, peerIdFromString } from "@libp2p/peer-id";
|
|
141
144
|
import { MESSAGE_KEY_ID, MESSAGE_PROTOCOL, Providers, tooLarge } from "./providers.js";
|
|
142
145
|
import { closeControl, listenControl } from "./control.js";
|
|
@@ -312,6 +315,13 @@ export class Router {
|
|
|
312
315
|
control;
|
|
313
316
|
/** The default image (#89), with its chain part (#132): what createInstance boots a new skein from. */
|
|
314
317
|
image;
|
|
318
|
+
/** #156: the headers tracker over the Teranode mesh (teranode.ts), when the host runs one. */
|
|
319
|
+
tracker;
|
|
320
|
+
/** #156: the libp2p the host node runs on: the instances' (`p2p`), else one for the tracker alone. */
|
|
321
|
+
hostP2p;
|
|
322
|
+
p2pConfig;
|
|
323
|
+
/** #156: the header intake, one run at a time (the image chain, then the host chains). */
|
|
324
|
+
intake = Promise.resolve();
|
|
315
325
|
/** #130: each billed instance as last read (its host row's terms, the kernel's billing state), and what this host metered since its last tick. */
|
|
316
326
|
views = new Map();
|
|
317
327
|
periods = new Map();
|
|
@@ -385,9 +395,12 @@ export class Router {
|
|
|
385
395
|
holds: async (h, txid) => await (await this.hydrate(h)).kernel.hasBlock(txCid(txid)),
|
|
386
396
|
});
|
|
387
397
|
}
|
|
398
|
+
// #156: the host's libp2p (one node per key; the host node among them) — for the instances, and for the tracker on its own.
|
|
399
|
+
const hostConfig = { ...(o.libp2p ?? { listen: LOOPBACK_LISTEN, bootstrap: [], dht: "off", relays: [], mdns: false }), network: this.chainNetwork() ?? "main" };
|
|
400
|
+
this.p2pConfig = hostConfig;
|
|
388
401
|
if (o.peerKeyFor) {
|
|
389
402
|
this.p2p = new P2PHost({
|
|
390
|
-
host: o.
|
|
403
|
+
host: hostConfig, ...(o.hostPeerKey ? { hostKey: o.hostPeerKey } : {}),
|
|
391
404
|
keyOf: o.peerKeyFor, discoveryMs: o.libp2pDiscoveryMs,
|
|
392
405
|
inbound: (h, c) => this.p2pInbound(h, c),
|
|
393
406
|
log: (s, l) => this.say(s, l),
|
|
@@ -395,8 +408,10 @@ export class Router {
|
|
|
395
408
|
sign: (h, data) => this.signAs(h, data),
|
|
396
409
|
identity: async (h) => { const id = this.o.db.get(h)?.identity; return id ? keyBytes(id) : undefined; },
|
|
397
410
|
beat: (h, topic, b) => { this.sync?.beat(h, topic, b); void this.rememberPeer(h, topic, b.from); },
|
|
411
|
+
beats: o.db,
|
|
398
412
|
});
|
|
399
413
|
const p2pHost = this.p2p;
|
|
414
|
+
this.hostP2p = p2pHost;
|
|
400
415
|
this.sync = new OverlaySync({
|
|
401
416
|
overlayOf: (h, liveTopic) => this.loaded.get(h)?.beacons.find((b) => b.topic === liveTopic)?.app,
|
|
402
417
|
local: (h, app, fn, query) => this.overlayRead(h, app, fn, query),
|
|
@@ -521,11 +536,28 @@ export class Router {
|
|
|
521
536
|
}
|
|
522
537
|
/** Hydrate every enabled row once (recovery at hydrate time: what a waiting thread awaits is handed to the providers again), then let them idle out. */
|
|
523
538
|
async start() {
|
|
539
|
+
// #132, #156: the host's headers — the feed's and the Teranode tracker's — into one intake: the image's chain part, then the host chains.
|
|
540
|
+
const key = this.headersKey();
|
|
541
|
+
if (key)
|
|
542
|
+
this.feeds.listen(key, (raws, at) => this.takeHeaders(raws, at));
|
|
524
543
|
// #132: the image's chain part, filled from the feed's history (from genesis once; then from near its tip), then grown by the feed.
|
|
525
544
|
if (this.o.headersFeed) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
545
|
+
const started = this.image.start();
|
|
546
|
+
void started.then(() => this.syncHolders()).catch((e) => this.say("router", `host chain: not filled from the image chain: ${e.message}`));
|
|
547
|
+
// With a feed, its history makes the first sync; the tracker follows the mesh once that is done.
|
|
548
|
+
if (this.o.teranode)
|
|
549
|
+
void started.then(() => this.startTracker()).catch((e) => this.say("router", `headers: the Teranode tracker not started: ${e.message}`));
|
|
550
|
+
}
|
|
551
|
+
else if (this.o.teranode) {
|
|
552
|
+
// #156: the tracker is the source: its first sync (a hub's, when the network has one) before any instance steps.
|
|
553
|
+
await this.startTracker().catch((e) => this.say("router", `headers: the Teranode tracker not started: ${e.message}`));
|
|
554
|
+
if (this.tracker?.o.hub)
|
|
555
|
+
await this.tracker.ready;
|
|
556
|
+
}
|
|
557
|
+
// #156: a host chain that is empty (or behind) filled from the image chain, before any instance is hydrated.
|
|
558
|
+
await this.syncHolders().catch((e) => this.say("router", `host chain: not filled from the image chain: ${e.message}`));
|
|
559
|
+
// #138: the stored liveness beats older than the longest window (a day) go before any instance reloads them.
|
|
560
|
+
this.o.db.pruneBeats(stampMs(this.now()) - LIVENESS_MAX_MS);
|
|
529
561
|
for (const row of this.o.db.list("enabled")) {
|
|
530
562
|
try {
|
|
531
563
|
await this.hydrate(row.handle);
|
|
@@ -588,7 +620,8 @@ export class Router {
|
|
|
588
620
|
this.control = undefined;
|
|
589
621
|
this.providers.stop();
|
|
590
622
|
this.events.stop();
|
|
591
|
-
await Promise.all([this.cron.stop(), this.feeds.stop(), this.arc?.stop(), this.
|
|
623
|
+
await Promise.all([this.cron.stop(), this.feeds.stop(), this.arc?.stop(), this.tracker?.stop()]);
|
|
624
|
+
await Promise.all([this.p2p?.stop(), this.hostP2p !== this.p2p ? this.hostP2p?.stop() : undefined]); // #156: the host node with them
|
|
592
625
|
// A kernel still hydrating: its load finishes (or fails) first, then it is stopped with the rest.
|
|
593
626
|
await Promise.all([...this.loading.values()].map((p) => p.catch(() => { }))); // a failed load was reported to whoever asked for it
|
|
594
627
|
await Promise.all([...this.loaded.values()].map((l) => l.kernel.stop()));
|
|
@@ -813,6 +846,128 @@ export class Router {
|
|
|
813
846
|
return next;
|
|
814
847
|
}
|
|
815
848
|
// ---------------------------------------------------------------- the host chain (#156)
|
|
849
|
+
/** #156: the key the host's headers arrive under (feeds.ts): the feed's URL, else the tracker's own. */
|
|
850
|
+
headersKey() {
|
|
851
|
+
return this.o.headersFeed ?? (this.o.teranode ? `teranode:${this.chainNetwork() ?? "main"}` : undefined);
|
|
852
|
+
}
|
|
853
|
+
/** #156: the Teranode tracker started: its node on the mesh, its first sync begun (`tracker.ready`). */
|
|
854
|
+
async startTracker() {
|
|
855
|
+
const t = this.o.teranode;
|
|
856
|
+
if (!t || this.tracker || this.stopped)
|
|
857
|
+
return;
|
|
858
|
+
const key = this.headersKey();
|
|
859
|
+
// #156: no instance libp2p (no peer keys: a test's host): the host's libp2p for the tracker alone.
|
|
860
|
+
this.hostP2p ??= new P2PHost({
|
|
861
|
+
host: this.p2pConfig, ...(this.o.hostPeerKey ? { hostKey: this.o.hostPeerKey } : {}), discoveryMs: this.o.libp2pDiscoveryMs,
|
|
862
|
+
keyOf: () => { throw new Error("no instance libp2p on this host"); },
|
|
863
|
+
inbound: async () => ({ verdict: "ignore" }),
|
|
864
|
+
log: (s, l) => this.say(s, l),
|
|
865
|
+
});
|
|
866
|
+
this.tracker = new TeranodeTracker({
|
|
867
|
+
network: this.chainNetwork() ?? "main", ...t, p2p: this.hostP2p,
|
|
868
|
+
chain: {
|
|
869
|
+
tip: () => this.image.tip(),
|
|
870
|
+
find: (hash) => this.image.find(hash),
|
|
871
|
+
// A long run (a first sync, a deep catch-up): into the image and the host chains, then each running instance its chain event; a short one as the feed's headers go.
|
|
872
|
+
take: async (at, raws) => {
|
|
873
|
+
if (raws.length <= 4 * BACKFILL_RUN)
|
|
874
|
+
return await this.feeds.inject(key, raws, at);
|
|
875
|
+
await this.takeHeaders(raws, at);
|
|
876
|
+
for (const h of [...this.loaded.keys()])
|
|
877
|
+
void this.chainBirth(h).catch((e) => this.say(h, `host chain: the chain event not sent: ${e.message}`));
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
log: (l) => this.say("router", l),
|
|
881
|
+
});
|
|
882
|
+
this.image.ready = this.tracker.ready;
|
|
883
|
+
await this.tracker.start();
|
|
884
|
+
this.image.ready = this.tracker.ready;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* #156: a run of headers from either source (the feed's, `at` undefined: image-chain.ts places
|
|
888
|
+
* each; the tracker's, at height `at`): into the image's chain part, then every host chain synced
|
|
889
|
+
* from it. One run at a time.
|
|
890
|
+
*/
|
|
891
|
+
takeHeaders(raws, at) {
|
|
892
|
+
const p = this.intake.then(async () => {
|
|
893
|
+
if (at === undefined)
|
|
894
|
+
await this.image.headers(raws);
|
|
895
|
+
else
|
|
896
|
+
await this.image.put(at, raws);
|
|
897
|
+
await this.syncHolders(at);
|
|
898
|
+
});
|
|
899
|
+
this.intake = p.catch(() => { });
|
|
900
|
+
return p;
|
|
901
|
+
}
|
|
902
|
+
/** #156: the host chains this host keeps once per database (its SQLite one, its RocksDB one, and those its rows name). */
|
|
903
|
+
hostHolders() {
|
|
904
|
+
const sq = new Set(), rocks = new Set();
|
|
905
|
+
if (this.o.sqliteStore)
|
|
906
|
+
sq.add(this.o.sqliteStore);
|
|
907
|
+
if (this.o.rocksStore)
|
|
908
|
+
rocks.add(this.o.rocksStore);
|
|
909
|
+
for (const row of this.o.db.list("enabled")) {
|
|
910
|
+
const spec = storeSpec(row);
|
|
911
|
+
const r = parseRocksSpec(spec), q = parseSqliteSpec(spec);
|
|
912
|
+
if (r)
|
|
913
|
+
rocks.add(r.dir);
|
|
914
|
+
if (q)
|
|
915
|
+
sq.add(q.db);
|
|
916
|
+
}
|
|
917
|
+
return [
|
|
918
|
+
...[...sq].map((db) => { const w = this.wasmHost(db); return { name: db, tip: () => w.chainTip(), headers: (raws) => w.chainHeaders(raws), importChain: w.importChain.bind(w) }; }),
|
|
919
|
+
...[...rocks].map((dir) => { const k = this.kernelHost(dir); return { name: dir, tip: () => k.chainTip(), headers: (raws) => k.chainHeaders(raws) }; }),
|
|
920
|
+
];
|
|
921
|
+
}
|
|
922
|
+
/** #156: each host chain this host keeps (by database) and its tip (undefined: empty). */
|
|
923
|
+
async hostChainTips() {
|
|
924
|
+
return await Promise.all(this.hostHolders().map(async (h) => ({ name: h.name, tip: await h.tip() })));
|
|
925
|
+
}
|
|
926
|
+
/** #156: every host chain brought to the image chain's tip (from `from` when a run replaced from there). */
|
|
927
|
+
async syncHolders(from) {
|
|
928
|
+
for (const h of this.hostHolders())
|
|
929
|
+
await this.syncHolder(h.name, h, from).catch((e) => this.say("router", `host chain ${h.name}: not synced from the image chain: ${e.message}`));
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* #156: a host chain brought to the image chain: an EMPTY one imports the image's chain part
|
|
933
|
+
* (`chain-import`, as an instance born with it would bring it: one frame) where its kernel can
|
|
934
|
+
* read it (the wasm kernel's SQLite database), else takes the image's headers from genesis in
|
|
935
|
+
* `headers` runs; one behind takes the headers it lacks; one the image does not hold at its
|
|
936
|
+
* height (a fork) from a little below it, or from `from`.
|
|
937
|
+
*/
|
|
938
|
+
async syncHolder(name, h, from) {
|
|
939
|
+
const top = this.image.height();
|
|
940
|
+
if (top < 0)
|
|
941
|
+
return;
|
|
942
|
+
const tip = await h.tip();
|
|
943
|
+
const t0 = Date.now();
|
|
944
|
+
if (!tip && h.importChain) {
|
|
945
|
+
const part = this.image.chainPart();
|
|
946
|
+
if (part) {
|
|
947
|
+
const got = await h.importChain(part.chain, part.objects);
|
|
948
|
+
this.say("router", `host chain: ${got.added ?? 0} headers imported from the image chain (tip ${got.height}, ${Date.now() - t0} ms)`);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
let start = 0;
|
|
953
|
+
if (tip) {
|
|
954
|
+
const held = this.image.headerAt(tip.height);
|
|
955
|
+
start = held && hashOf(held) === tip.hash ? tip.height + 1 : Math.max(0, tip.height - 6);
|
|
956
|
+
if (from !== undefined)
|
|
957
|
+
start = Math.min(start, from);
|
|
958
|
+
}
|
|
959
|
+
if (start > top)
|
|
960
|
+
return;
|
|
961
|
+
let n = 0;
|
|
962
|
+
for (const run of this.image.runs(start, BACKFILL_RUN)) {
|
|
963
|
+
await h.headers(run);
|
|
964
|
+
n += run.length;
|
|
965
|
+
if (top - start > 10 * BACKFILL_RUN && n % (50 * BACKFILL_RUN) < BACKFILL_RUN)
|
|
966
|
+
this.say("router", `host chain: ${start + n}/${top + 1} from the image chain`);
|
|
967
|
+
}
|
|
968
|
+
if (n > BACKFILL_RUN)
|
|
969
|
+
this.say("router", `host chain: ${n} headers from the image chain (to ${top}, ${Date.now() - t0} ms)`);
|
|
970
|
+
}
|
|
816
971
|
/**
|
|
817
972
|
* The network the host chain is on: SKEIN_NETWORK (`network`), else the genesis defaults'
|
|
818
973
|
* `walletNetwork` (a test host's), else the kernel's default (main).
|
|
@@ -953,6 +1108,9 @@ export class Router {
|
|
|
953
1108
|
await kernel.start();
|
|
954
1109
|
// #156: after the start (the genesis processed: its tree is the head `main`), before any event is admitted.
|
|
955
1110
|
await this.chainBootstrap(handle, kernel);
|
|
1111
|
+
// #156: a serve kernel's own host chain still empty (born before the image held a chain): from the image chain now.
|
|
1112
|
+
if (kernel instanceof Kernel)
|
|
1113
|
+
await this.syncHolder(handle, { tip: () => kernel.chainTip(), headers: (raws) => kernel.chainHeaders(raws) }).catch((e) => this.say(handle, `host chain: not synced from the image chain: ${e.message}`));
|
|
956
1114
|
await kernel.running(identity);
|
|
957
1115
|
void kernel.idle().catch(() => { }); // busy until what start resumed is done; a kernel that exits meanwhile is reported by its exit
|
|
958
1116
|
if (!row.identity)
|
|
@@ -1186,7 +1344,14 @@ export class Router {
|
|
|
1186
1344
|
// operator's key, `appConfig` merged.
|
|
1187
1345
|
// #162: any instance's app config at birth (a create's `config`), the host skein's owner at birth.
|
|
1188
1346
|
// #164: an owned instance's genesis names `owner` root (boot.ts then seeds no claim route), as the host skein's does.
|
|
1189
|
-
|
|
1347
|
+
// The image read from its root to the last object walked: a header taken meanwhile keeps the objects it replaces.
|
|
1348
|
+
const release = this.image.hold();
|
|
1349
|
+
try {
|
|
1350
|
+
await this.bootRow(handle, await this.namedImage(o.image, o.host), { image: true, manager: o.host, ...(owned ? { owner } : {}), ...(o.appConfig ? { appConfig: o.appConfig } : {}) });
|
|
1351
|
+
}
|
|
1352
|
+
finally {
|
|
1353
|
+
release();
|
|
1354
|
+
}
|
|
1190
1355
|
const l = await this.hydrate(handle);
|
|
1191
1356
|
if (claim) {
|
|
1192
1357
|
// #127: the owner's own message, forwarded as signed; the host signs nothing for the owner. #143: one the door
|
|
@@ -1397,7 +1562,7 @@ export class Router {
|
|
|
1397
1562
|
* handled and unhandled) or stopped. A failure is logged, not fatal.
|
|
1398
1563
|
*/
|
|
1399
1564
|
async syncDispatch(l, first = false) {
|
|
1400
|
-
if ((!this.p2p && !this.
|
|
1565
|
+
if ((!this.p2p && !this.headersKey() && !this.events.open(l.row.handle)) || l.kernel.gone)
|
|
1401
1566
|
return;
|
|
1402
1567
|
const d = await l.kernel.dispatch().catch((e) => { this.say(l.row.handle, `the dispatch table not read: ${e.message}`); return undefined; });
|
|
1403
1568
|
if (d === undefined)
|
|
@@ -1718,7 +1883,7 @@ export class Router {
|
|
|
1718
1883
|
* else not. One line when that changes.
|
|
1719
1884
|
*/
|
|
1720
1885
|
followHeaders(handle, rows) {
|
|
1721
|
-
const url = this.
|
|
1886
|
+
const url = this.headersKey();
|
|
1722
1887
|
if (!url)
|
|
1723
1888
|
return;
|
|
1724
1889
|
const want = this.o.db.get(handle)?.status === "enabled" && rows.some((r) => r.address === DEFAULT_BOX && takesEvent(r, DEFAULT_BOX));
|
|
@@ -1747,7 +1912,7 @@ export class Router {
|
|
|
1747
1912
|
}
|
|
1748
1913
|
/** Follow the host's headers feed for an instance whose status changed (#102): its table read again. */
|
|
1749
1914
|
async refollowHeaders(handle) {
|
|
1750
|
-
if (!this.
|
|
1915
|
+
if (!this.headersKey())
|
|
1751
1916
|
return;
|
|
1752
1917
|
const l = this.loaded.get(handle);
|
|
1753
1918
|
const d = l && !l.kernel.gone ? await l.kernel.dispatch().catch((e) => { this.say(handle, `the dispatch table not read: ${e.message}`); return undefined; }) : undefined;
|
|
@@ -2060,6 +2225,9 @@ export class Router {
|
|
|
2060
2225
|
return await this.held(this.forward(o.handle, url.pathname, url, req));
|
|
2061
2226
|
}
|
|
2062
2227
|
const serve = async (x) => {
|
|
2228
|
+
// The instance's own card, the host's read (no program): whether anyone holds root yet.
|
|
2229
|
+
if (req.method === "GET" && x.route === "/.well-known/skein")
|
|
2230
|
+
return await this.instanceCard(x.handle);
|
|
2063
2231
|
// #138: the liveness tool's read, the host's own (no program).
|
|
2064
2232
|
const live = req.method === "GET" ? /^\/([^/]+)\/\.live\/([^/]+)$/.exec(x.route) : null;
|
|
2065
2233
|
if (live)
|
|
@@ -2067,6 +2235,12 @@ export class Router {
|
|
|
2067
2235
|
// #148: a stream, served on a socket only (handler).
|
|
2068
2236
|
if (req.method === "GET" && EVENTS_ROUTE.test(x.route))
|
|
2069
2237
|
return json(400, { status: "error", code: "ERR_BAD_REQUEST", description: "an event stream (text/event-stream) is served on a connection of its own, not in process" });
|
|
2238
|
+
// An application host's own reads (appRead): before the instance's program; undefined falls through.
|
|
2239
|
+
if (req.method === "GET" && this.o.appRead) {
|
|
2240
|
+
const a = await this.o.appRead(x.handle, x.route, url, req);
|
|
2241
|
+
if (a)
|
|
2242
|
+
return a;
|
|
2243
|
+
}
|
|
2070
2244
|
return await this.held(this.forward(x.handle, x.route, url, req));
|
|
2071
2245
|
};
|
|
2072
2246
|
if (t)
|
|
@@ -2074,6 +2248,16 @@ export class Router {
|
|
|
2074
2248
|
const path = url.pathname;
|
|
2075
2249
|
if (req.method === "GET" && path === "/skeins")
|
|
2076
2250
|
return await this.skeinsHeldBy(url.searchParams.get("key") ?? "");
|
|
2251
|
+
if (path === `${ARC_ROUTE}/callback`)
|
|
2252
|
+
return await this.arcRequest(req, path);
|
|
2253
|
+
// SKEIN_ROOT_INSTANCE: a hostname that is no instance's serves the root instance (the standalone's one app at
|
|
2254
|
+
// localhost:<port>/) — every path but the router's own three above (.well-known/skein-host, /skeins, the ARC
|
|
2255
|
+
// callback). The BRC-169 set below is the host skein's only when no root instance takes the hostname: with one,
|
|
2256
|
+
// POST /.well-known/auth must shake hands with the root instance (its sessions are its own, #68) and
|
|
2257
|
+
// GET /manifest.json is the app's (its grouped permissions), as at an instance's own hostname.
|
|
2258
|
+
const root = this.o.rootInstance;
|
|
2259
|
+
if (root && this.o.db.get(root)?.status === "enabled")
|
|
2260
|
+
return await serve({ handle: root, route: path });
|
|
2077
2261
|
// #113: the host's own origin's BRC-169 requests are the host skein's (its onboarding app's routes).
|
|
2078
2262
|
const route = DISCOVERY[`${req.method} ${path}`] ?? (req.method === "GET" && path.startsWith(PAYMAIL_PREFIX) ? `/${ONBOARD_APP}${path}` : undefined);
|
|
2079
2263
|
if (route) {
|
|
@@ -2085,13 +2269,6 @@ export class Router {
|
|
|
2085
2269
|
return f;
|
|
2086
2270
|
return json(404, { status: "error", code: "ERR_NOT_FOUND", description: "this host has no host skein (skein-host init): no BRC-169 here" });
|
|
2087
2271
|
}
|
|
2088
|
-
if (path === `${ARC_ROUTE}/callback`)
|
|
2089
|
-
return await this.arcRequest(req, path);
|
|
2090
|
-
// SKEIN_ROOT_INSTANCE: a hostname that is no instance's serves the root instance (the standalone's one app at localhost:<port>/),
|
|
2091
|
-
// after the host's own paths above.
|
|
2092
|
-
const root = this.o.rootInstance;
|
|
2093
|
-
if (root && this.o.db.get(root)?.status === "enabled")
|
|
2094
|
-
return await serve({ handle: root, route: path });
|
|
2095
2272
|
return json(404, { status: "error", code: "ERR_NOT_FOUND", description: "no instance here: an instance is at http://<handle>.localhost:<port>/ or /@<handle>/" });
|
|
2096
2273
|
}
|
|
2097
2274
|
/**
|
|
@@ -2183,24 +2360,41 @@ export class Router {
|
|
|
2183
2360
|
return json(400, { status: "error", code: "ERR_BAD_REQUEST", description: "key: an identity key (33 bytes, hex)" });
|
|
2184
2361
|
const out = [];
|
|
2185
2362
|
for (const row of this.o.db.list("enabled")) {
|
|
2186
|
-
if (row.kind === "mailbox" || !row.identity
|
|
2363
|
+
if (row.kind === "mailbox" || !row.identity)
|
|
2187
2364
|
continue;
|
|
2188
|
-
|
|
2189
|
-
const s = openStoreFile(storeSpec(row), { readOnly: true });
|
|
2190
|
-
try {
|
|
2191
|
-
roots = await rootOf(s);
|
|
2192
|
-
}
|
|
2193
|
-
catch {
|
|
2194
|
-
roots = [];
|
|
2195
|
-
}
|
|
2196
|
-
finally {
|
|
2197
|
-
s.close();
|
|
2198
|
-
}
|
|
2199
|
-
if (roots.includes(key))
|
|
2365
|
+
if ((await this.rootsOf(row)).includes(key))
|
|
2200
2366
|
out.push({ handle: row.handle, identity: row.identity, url: this.originOf(row.handle) });
|
|
2201
2367
|
}
|
|
2202
2368
|
return json(200, out);
|
|
2203
2369
|
}
|
|
2370
|
+
/**
|
|
2371
|
+
* GET /.well-known/skein at an instance's origin (its hostname, `/@<handle>/`, or the root instance's
|
|
2372
|
+
* hostname): `{handle, identity, origin, claimed}` — `claimed` whether its head `grants` names a root
|
|
2373
|
+
* holder (an image instance nobody has claimed: false). Read from its store read-only, no kernel, as
|
|
2374
|
+
* /skeins does. Unsigned, anyone; 404 for no instance. "Am I root" is /skeins?key=<identity>.
|
|
2375
|
+
*/
|
|
2376
|
+
async instanceCard(handle) {
|
|
2377
|
+
const row = this.o.db.get(handle);
|
|
2378
|
+
if (!row?.identity)
|
|
2379
|
+
return json(404, { status: "error", code: "ERR_NOT_FOUND", description: `no instance ${handle} here` });
|
|
2380
|
+
return json(200, { handle, identity: row.identity, origin: this.originOf(handle), claimed: (await this.rootsOf(row)).length > 0 });
|
|
2381
|
+
}
|
|
2382
|
+
/** The keys an instance's head `grants` names root (#143), its store read read-only; none when it has no store yet. */
|
|
2383
|
+
async rootsOf(row) {
|
|
2384
|
+
const spec = storeSpec(row);
|
|
2385
|
+
if (!storeExists(spec))
|
|
2386
|
+
return [];
|
|
2387
|
+
const s = openStoreFile(spec, { readOnly: true });
|
|
2388
|
+
try {
|
|
2389
|
+
return await rootOf(s);
|
|
2390
|
+
}
|
|
2391
|
+
catch {
|
|
2392
|
+
return [];
|
|
2393
|
+
}
|
|
2394
|
+
finally {
|
|
2395
|
+
s.close();
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2204
2398
|
/** A forwarded request, held among the in-flight ones (shutdown answers them 503). */
|
|
2205
2399
|
async held(p) {
|
|
2206
2400
|
this.inflight.add(p);
|
|
@@ -2300,7 +2494,28 @@ export class Router {
|
|
|
2300
2494
|
req.on("end", () => {
|
|
2301
2495
|
const url = `http://${req.headers.host ?? `127.0.0.1:${this.port}`}${req.url ?? "/"}`;
|
|
2302
2496
|
this.dispatch({ method: req.method ?? "GET", url, headers: headerMap(req.headers), body: new Uint8Array(Buffer.concat(chunks)) })
|
|
2303
|
-
.then((r) => {
|
|
2497
|
+
.then((r) => {
|
|
2498
|
+
if (!r.stream) {
|
|
2499
|
+
res.writeHead(r.status, { ...cors, ...r.headers, "content-length": String(r.body.length) });
|
|
2500
|
+
res.end(r.body);
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
// A response held open (an appRead's event stream): written as it comes, until either side ends it.
|
|
2504
|
+
if (res.destroyed)
|
|
2505
|
+
return;
|
|
2506
|
+
res.writeHead(r.status, { ...cors, ...r.headers });
|
|
2507
|
+
res.flushHeaders();
|
|
2508
|
+
let forget;
|
|
2509
|
+
let gone = false;
|
|
2510
|
+
const leave = () => { if (gone)
|
|
2511
|
+
return; gone = true; forget?.(); };
|
|
2512
|
+
res.on("close", leave);
|
|
2513
|
+
forget = r.stream((chunk) => { if (!gone)
|
|
2514
|
+
res.write(chunk); }, () => { if (!gone)
|
|
2515
|
+
res.end(); });
|
|
2516
|
+
if (gone)
|
|
2517
|
+
forget();
|
|
2518
|
+
})
|
|
2304
2519
|
.catch((e) => { if (!res.headersSent) {
|
|
2305
2520
|
res.writeHead(500, { ...cors, "content-type": "application/json" });
|
|
2306
2521
|
res.end(JSON.stringify({ status: "error", code: "ERR_INTERNAL", description: e.message }));
|
package/dist/host/signer.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export declare class Signer {
|
|
|
32
32
|
* (p2p.ts peerIdOf).
|
|
33
33
|
*/
|
|
34
34
|
peerKey(id: string): PrivateKey;
|
|
35
|
+
/**
|
|
36
|
+
* The host node's peer key (#156): the key's child [2, "skein instance"], key ID `libp2p`, self —
|
|
37
|
+
* in one-key mode every instance's peer key (peerKey), so they share the host node; in master-key
|
|
38
|
+
* mode the same rule over the master (its own node beside the instances').
|
|
39
|
+
*/
|
|
40
|
+
hostPeerKey(): PrivateKey;
|
|
35
41
|
/** The instance's identity key (hex): what `skein-host add` records. */
|
|
36
42
|
identity(id: string): string;
|
|
37
43
|
/** The instance's signer: a ProtoWallet over its root key (a WalletInterface whose actions reject). */
|
package/dist/host/signer.js
CHANGED
|
@@ -137,6 +137,14 @@ export class Signer {
|
|
|
137
137
|
return this.deriver.derivePrivateKey(INSTANCE_PROTOCOL, "libp2p", "self");
|
|
138
138
|
return new KeyDeriver(this.instanceKey(id)).derivePrivateKey(INSTANCE_PROTOCOL, `libp2p:${id}`, "self");
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The host node's peer key (#156): the key's child [2, "skein instance"], key ID `libp2p`, self —
|
|
142
|
+
* in one-key mode every instance's peer key (peerKey), so they share the host node; in master-key
|
|
143
|
+
* mode the same rule over the master (its own node beside the instances').
|
|
144
|
+
*/
|
|
145
|
+
hostPeerKey() {
|
|
146
|
+
return this.deriver.derivePrivateKey(INSTANCE_PROTOCOL, "libp2p", "self");
|
|
147
|
+
}
|
|
140
148
|
/** The instance's identity key (hex): what `skein-host add` records. */
|
|
141
149
|
identity(id) {
|
|
142
150
|
return this.instanceKey(id).toPublicKey().toString();
|