@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/image-chain.js
CHANGED
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
// two trees above them and the root; every full block stays as it is. The
|
|
20
20
|
// chain part's objects live in host.db (`image_blocks`: only the current ones;
|
|
21
21
|
// a replaced object is deleted with the write that replaces it), the current
|
|
22
|
-
// root and `chain` tree in `host_settings` (`image`, `image_chain`).
|
|
22
|
+
// root and `chain` tree in `host_settings` (`image`, `image_chain`). While a
|
|
23
|
+
// boot reads the image (`hold()`, Router.createInstance), a write puts its new
|
|
24
|
+
// objects and settings but keeps the replaced ones: their deletes wait for the
|
|
25
|
+
// next write with no boot in flight, or the last boot's release.
|
|
23
26
|
//
|
|
24
27
|
// Where the headers come from: the host's headers feed (SKEIN_HEADERS_URL,
|
|
25
28
|
// feeds.ts), each one appended as it arrives; and its history, from the same
|
|
@@ -95,6 +98,10 @@ export class ImageChain {
|
|
|
95
98
|
stat;
|
|
96
99
|
/** #142, #152: the overlay images' static parts (images/host, a directory with no `bin/`), each merged over the default image once. */
|
|
97
100
|
merged = new Map();
|
|
101
|
+
/** Boots reading the image now (`hold`): while any, a write's drops wait in `pending`. */
|
|
102
|
+
holds = 0;
|
|
103
|
+
/** The objects writes replaced while a boot read the image: dropped once none does. */
|
|
104
|
+
pending = new Set();
|
|
98
105
|
/** Settles once the first sync (start) is done: what a creation waits for. */
|
|
99
106
|
ready = Promise.resolve();
|
|
100
107
|
constructor(o) {
|
|
@@ -132,6 +139,54 @@ export class ImageChain {
|
|
|
132
139
|
const h = this.height();
|
|
133
140
|
return h < 0 ? undefined : { height: h, hash: display(sha256d(this.last.subarray(this.last.length - SIZE))) };
|
|
134
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The height of a header held within `depth` of the tip (default two blocks' worth), by its hash
|
|
144
|
+
* (display hex); undefined when none there is it. An index over that window, rebuilt when the tip moves.
|
|
145
|
+
*/
|
|
146
|
+
find(hash, depth = 2 * PER_BLOCK) {
|
|
147
|
+
const t = this.tip();
|
|
148
|
+
if (!t)
|
|
149
|
+
return undefined;
|
|
150
|
+
if (this.window?.tip !== t.hash || this.window.depth !== depth) {
|
|
151
|
+
const map = new Map();
|
|
152
|
+
for (let h = t.height; h >= Math.max(0, t.height - depth); h--)
|
|
153
|
+
map.set(display(sha256d(this.at(h))), h);
|
|
154
|
+
this.window = { tip: t.hash, depth, map };
|
|
155
|
+
}
|
|
156
|
+
return this.window.map.get(hash);
|
|
157
|
+
}
|
|
158
|
+
window;
|
|
159
|
+
/**
|
|
160
|
+
* The chain part as objects (#156: what an empty host chain imports): the `chain` tree's CID and
|
|
161
|
+
* every object under it (the tree, `headers` and its blocks, `tip`), as host.db holds them.
|
|
162
|
+
*/
|
|
163
|
+
chainPart() {
|
|
164
|
+
const c = this.o.db.setting("image_chain");
|
|
165
|
+
if (!c)
|
|
166
|
+
return undefined;
|
|
167
|
+
const chain = Cid.parse(c);
|
|
168
|
+
const objects = [[chain, this.object(chain)]];
|
|
169
|
+
for (const e of parseTree(this.object(chain))) {
|
|
170
|
+
objects.push([e.cid, this.object(e.cid)]);
|
|
171
|
+
if (e.name === "headers")
|
|
172
|
+
for (const b of parseTree(this.object(e.cid)))
|
|
173
|
+
objects.push([b.cid, this.object(b.cid)]);
|
|
174
|
+
}
|
|
175
|
+
return { chain, objects };
|
|
176
|
+
}
|
|
177
|
+
/** The header held at a height (raw, a copy), or undefined. */
|
|
178
|
+
headerAt(height) {
|
|
179
|
+
return height >= 0 && height <= this.height() ? new Uint8Array(this.at(height)) : undefined;
|
|
180
|
+
}
|
|
181
|
+
/** The headers held from `from` to the tip, `run` at a time (copies; the chain read as it stands at each run). */
|
|
182
|
+
*runs(from, run) {
|
|
183
|
+
for (let h = Math.max(0, from); h <= this.height(); h += run) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (let i = h; i < h + run && i <= this.height(); i++)
|
|
186
|
+
out.push(new Uint8Array(this.at(i)));
|
|
187
|
+
yield out;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
135
190
|
/** The header at a height (raw), from its block. */
|
|
136
191
|
at(height) {
|
|
137
192
|
const b = Math.floor(height / PER_BLOCK);
|
|
@@ -204,13 +259,40 @@ export class ImageChain {
|
|
|
204
259
|
const oldRoot = this.o.db.setting("image");
|
|
205
260
|
if (oldRoot)
|
|
206
261
|
drops.add(oldRoot);
|
|
262
|
+
for (const d of this.pending)
|
|
263
|
+
drops.add(d);
|
|
207
264
|
for (const k of puts.keys())
|
|
208
265
|
drops.delete(k);
|
|
209
|
-
|
|
266
|
+
// A boot reading the image (hold): the replaced objects stay until it is done.
|
|
267
|
+
const now = this.holds === 0 ? drops : new Set();
|
|
268
|
+
this.pending = this.holds === 0 ? new Set() : drops;
|
|
269
|
+
this.o.db.writeImage(puts, now, { image: root.cid.toString(), image_chain: chain.cid.toString() });
|
|
210
270
|
this.blocks = blocks;
|
|
211
271
|
this.last = Buffer.from(last);
|
|
212
272
|
return true;
|
|
213
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* A boot reads the image (its root, then every object under it) from `source()` on: hold it from
|
|
276
|
+
* before `source()` until the walk is done. Until the last hold is released, a write keeps the
|
|
277
|
+
* objects it replaces (their deletes wait); the release returned (once) applies them when no
|
|
278
|
+
* other boot holds.
|
|
279
|
+
*/
|
|
280
|
+
hold() {
|
|
281
|
+
this.holds++;
|
|
282
|
+
let done = false;
|
|
283
|
+
return () => {
|
|
284
|
+
if (done)
|
|
285
|
+
return;
|
|
286
|
+
done = true;
|
|
287
|
+
if (--this.holds === 0 && this.pending.size)
|
|
288
|
+
void this.serial(() => {
|
|
289
|
+
if (this.holds !== 0 || !this.pending.size)
|
|
290
|
+
return;
|
|
291
|
+
this.o.db.writeImage(new Map(), this.pending, {});
|
|
292
|
+
this.pending = new Set();
|
|
293
|
+
}).catch((e) => this.say(`the image's replaced objects not dropped: ${e.message}`));
|
|
294
|
+
};
|
|
295
|
+
}
|
|
214
296
|
/** The static part: the image directory, scanned once (boot.ts imageDir: the apps it names beside it too). */
|
|
215
297
|
static() {
|
|
216
298
|
this.stat ??= (async () => {
|
package/dist/host/instances.d.ts
CHANGED
|
@@ -83,6 +83,25 @@ export declare class HostDb {
|
|
|
83
83
|
setStatus(handle: string, status: Status): boolean;
|
|
84
84
|
/** Remove the row (not its store or wallet); false if there was none. */
|
|
85
85
|
remove(handle: string): boolean;
|
|
86
|
+
/** A liveness beat kept (#138): the sender's row on `handle`'s `topic` replaced. */
|
|
87
|
+
putBeat(handle: string, topic: string, b: {
|
|
88
|
+
sender: string;
|
|
89
|
+
at: number;
|
|
90
|
+
from: string;
|
|
91
|
+
frame: Uint8Array;
|
|
92
|
+
}): void;
|
|
93
|
+
/** The liveness beats kept for `handle` (on `topic` only, when named). */
|
|
94
|
+
beatsOf(handle: string, topic?: string): Array<{
|
|
95
|
+
topic: string;
|
|
96
|
+
sender: string;
|
|
97
|
+
at: number;
|
|
98
|
+
from: string;
|
|
99
|
+
frame: Uint8Array;
|
|
100
|
+
}>;
|
|
101
|
+
/** Forget `handle`'s liveness beats: all, a topic's, or one sender's on a topic. */
|
|
102
|
+
dropBeats(handle: string, topic?: string, sender?: string): void;
|
|
103
|
+
/** Forget the liveness beats dated before `before` (ms). → how many rows went. */
|
|
104
|
+
pruneBeats(before: number): number;
|
|
86
105
|
/** The enabled mailbox instance for identity `owner` (hex). */
|
|
87
106
|
/** A beat kept from `peer` on `handle`'s overlay `topic` (sync): upserted with its addresses (kept when none are known now). */
|
|
88
107
|
seenPeer(handle: string, topic: string, peer: string, addrs: string[], now: number): void;
|
package/dist/host/instances.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
// the host last read it (asleep since when: the grace), the periods it ticked
|
|
21
21
|
// (each period's log record, which its tick commits to), and the payments its
|
|
22
22
|
// wallet made (the `payment` events). And overlay sync's: the peers each
|
|
23
|
-
// instance's overlay topics heard beats from (`topic_peers`).
|
|
23
|
+
// instance's overlay topics heard beats from (`topic_peers`). And the liveness
|
|
24
|
+
// tool's (#138): the beats it kept, reloaded at a start (`live_beats`).
|
|
24
25
|
import { DatabaseSync } from "node:sqlite";
|
|
25
26
|
const DDL = `
|
|
26
27
|
CREATE TABLE IF NOT EXISTS instances (
|
|
@@ -134,6 +135,17 @@ CREATE TABLE IF NOT EXISTS topic_peers (
|
|
|
134
135
|
addrs TEXT NOT NULL, -- JSON array of multiaddrs (may be empty)
|
|
135
136
|
last_seen INTEGER NOT NULL, -- ms: the last beat kept from it
|
|
136
137
|
PRIMARY KEY (handle, topic, peer_id)
|
|
138
|
+
) WITHOUT ROWID;
|
|
139
|
+
-- The liveness tool's beats (#138, liveness.ts): per instance and liveness topic, the latest beat kept from each
|
|
140
|
+
-- sender, the frame as received; a start feeds them through the tool again (signature and window re-checked).
|
|
141
|
+
CREATE TABLE IF NOT EXISTS live_beats (
|
|
142
|
+
handle TEXT NOT NULL,
|
|
143
|
+
topic TEXT NOT NULL, -- the liveness topic (e.g. <topic>-live)
|
|
144
|
+
sender TEXT NOT NULL, -- the beat's identity key (hex)
|
|
145
|
+
at INTEGER NOT NULL, -- ms: the beat's own time
|
|
146
|
+
peer_id TEXT NOT NULL, -- the libp2p peer that published it
|
|
147
|
+
frame BLOB NOT NULL, -- the beacon frame (p2p.ts beaconFrame)
|
|
148
|
+
PRIMARY KEY (handle, topic, sender)
|
|
137
149
|
) WITHOUT ROWID;`;
|
|
138
150
|
/**
|
|
139
151
|
* A handle: one hostname label (lower-case letters, digits and "-", at most
|
|
@@ -249,6 +261,31 @@ export class HostDb {
|
|
|
249
261
|
remove(handle) {
|
|
250
262
|
return Number(this.db.prepare("DELETE FROM instances WHERE handle = ?").run(handle).changes) > 0;
|
|
251
263
|
}
|
|
264
|
+
/** A liveness beat kept (#138): the sender's row on `handle`'s `topic` replaced. */
|
|
265
|
+
putBeat(handle, topic, b) {
|
|
266
|
+
this.db.prepare("INSERT INTO live_beats (handle, topic, sender, at, peer_id, frame) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (handle, topic, sender) DO UPDATE SET at = excluded.at, peer_id = excluded.peer_id, frame = excluded.frame")
|
|
267
|
+
.run(handle, topic, b.sender, b.at, b.from, b.frame);
|
|
268
|
+
}
|
|
269
|
+
/** The liveness beats kept for `handle` (on `topic` only, when named). */
|
|
270
|
+
beatsOf(handle, topic) {
|
|
271
|
+
const rows = topic === undefined
|
|
272
|
+
? this.db.prepare("SELECT topic, sender, at, peer_id, frame FROM live_beats WHERE handle = ? ORDER BY topic, sender").all(handle)
|
|
273
|
+
: this.db.prepare("SELECT topic, sender, at, peer_id, frame FROM live_beats WHERE handle = ? AND topic = ? ORDER BY sender").all(handle, topic);
|
|
274
|
+
return rows.map((r) => ({ topic: String(r.topic), sender: String(r.sender), at: Number(r.at), from: String(r.peer_id), frame: new Uint8Array(r.frame) }));
|
|
275
|
+
}
|
|
276
|
+
/** Forget `handle`'s liveness beats: all, a topic's, or one sender's on a topic. */
|
|
277
|
+
dropBeats(handle, topic, sender) {
|
|
278
|
+
if (topic === undefined)
|
|
279
|
+
this.db.prepare("DELETE FROM live_beats WHERE handle = ?").run(handle);
|
|
280
|
+
else if (sender === undefined)
|
|
281
|
+
this.db.prepare("DELETE FROM live_beats WHERE handle = ? AND topic = ?").run(handle, topic);
|
|
282
|
+
else
|
|
283
|
+
this.db.prepare("DELETE FROM live_beats WHERE handle = ? AND topic = ? AND sender = ?").run(handle, topic, sender);
|
|
284
|
+
}
|
|
285
|
+
/** Forget the liveness beats dated before `before` (ms). → how many rows went. */
|
|
286
|
+
pruneBeats(before) {
|
|
287
|
+
return Number(this.db.prepare("DELETE FROM live_beats WHERE at < ?").run(before).changes);
|
|
288
|
+
}
|
|
252
289
|
/** The enabled mailbox instance for identity `owner` (hex). */
|
|
253
290
|
/** A beat kept from `peer` on `handle`'s overlay `topic` (sync): upserted with its addresses (kept when none are known now). */
|
|
254
291
|
seenPeer(handle, topic, peer, addrs, now) {
|
package/dist/host/liveness.d.ts
CHANGED
|
@@ -31,6 +31,20 @@ export interface LiveBeat {
|
|
|
31
31
|
body: Uint8Array;
|
|
32
32
|
from: string;
|
|
33
33
|
}
|
|
34
|
+
/** A beat as the store keeps it: the frame as received (re-verified on reload), its time, sender (hex), and the peer that published it. */
|
|
35
|
+
export interface StoredBeat {
|
|
36
|
+
topic: string;
|
|
37
|
+
sender: string;
|
|
38
|
+
at: number;
|
|
39
|
+
from: string;
|
|
40
|
+
frame: Uint8Array;
|
|
41
|
+
}
|
|
42
|
+
/** Where the beats kept are persisted (host.db `live_beats`, instances.ts HostDb). */
|
|
43
|
+
export interface BeatStore {
|
|
44
|
+
putBeat(handle: string, topic: string, beat: Omit<StoredBeat, "topic">): void;
|
|
45
|
+
beatsOf(handle: string, topic?: string): StoredBeat[];
|
|
46
|
+
dropBeats(handle: string, topic?: string, sender?: string): void;
|
|
47
|
+
}
|
|
34
48
|
/** A message's verdict for GossipSub: a verified beat is accepted (forwarded), a bad one rejected, a stale one ignored. */
|
|
35
49
|
export type LiveVerdict = "accept" | "reject" | "ignore";
|
|
36
50
|
/** The tool's state: per instance, the apps' liveness and the beats kept per topic (in memory). */
|
|
@@ -41,7 +55,11 @@ export declare class Liveness {
|
|
|
41
55
|
private readonly now;
|
|
42
56
|
/** Told each beat newly kept (shruggr/skein#150: the overlay's sync reads a peer's tip anchor from it). */
|
|
43
57
|
kept?: (handle: string, topic: string, beat: LiveBeat) => void;
|
|
44
|
-
|
|
58
|
+
/** The store the kept beats are persisted in (absent: memory only). */
|
|
59
|
+
private readonly store?;
|
|
60
|
+
constructor(now?: () => number, store?: BeatStore);
|
|
61
|
+
/** A store write: its failure (a closed host.db at shutdown) changes nothing in memory nor any verdict. */
|
|
62
|
+
private persist;
|
|
45
63
|
/** `handle`'s apps keep this liveness now: the beats of topics none keeps any more are dropped. */
|
|
46
64
|
set(handle: string, lives: Live[]): void;
|
|
47
65
|
/** The topics `handle`'s apps keep liveness for (each once). */
|
|
@@ -60,6 +78,8 @@ export declare class Liveness {
|
|
|
60
78
|
read(handle: string, app: string, topic: string): LiveBeat[] | undefined;
|
|
61
79
|
/** Every beat live on `topic` at `handle` (newer than the longest window on it), whichever app keeps it. */
|
|
62
80
|
live(handle: string, topic: string): LiveBeat[];
|
|
63
|
-
/** Forget `handle`: its liveness and its beats. */
|
|
81
|
+
/** Forget `handle`: its liveness and its beats (the stored ones too). */
|
|
64
82
|
drop(handle: string): void;
|
|
83
|
+
/** Forget `handle` in memory only (the host stopping): the stored beats stay for the next start. */
|
|
84
|
+
forget(handle: string): void;
|
|
65
85
|
}
|
package/dist/host/liveness.js
CHANGED
|
@@ -19,8 +19,12 @@
|
|
|
19
19
|
// (identity key), with the peer that published it (`from`); the node's
|
|
20
20
|
// OWN beats on the topic are written into the same set (gossip does not
|
|
21
21
|
// echo them: P2PHost.beacons);
|
|
22
|
-
// - in memory
|
|
23
|
-
// the
|
|
22
|
+
// - in memory, and in the host's store when it gives one (host.db
|
|
23
|
+
// `live_beats`: the frame as received, its time, sender and peer): when
|
|
24
|
+
// the apps' liveness is set at a start, the frames kept for its topics go
|
|
25
|
+
// through `observe` again (signature re-checked, window re-applied; the
|
|
26
|
+
// ones that no longer count deleted); dropped at the app's unliveness or
|
|
27
|
+
// uninstall.
|
|
24
28
|
//
|
|
25
29
|
// The set is served to the app's page by the host itself, no program in
|
|
26
30
|
// between (Router: GET /<app>/.live/<topic> on the instance's origin).
|
|
@@ -73,7 +77,18 @@ export class Liveness {
|
|
|
73
77
|
now;
|
|
74
78
|
/** Told each beat newly kept (shruggr/skein#150: the overlay's sync reads a peer's tip anchor from it). */
|
|
75
79
|
kept;
|
|
76
|
-
|
|
80
|
+
/** The store the kept beats are persisted in (absent: memory only). */
|
|
81
|
+
store;
|
|
82
|
+
constructor(now = Date.now, store) { this.now = now; this.store = store; }
|
|
83
|
+
/** A store write: its failure (a closed host.db at shutdown) changes nothing in memory nor any verdict. */
|
|
84
|
+
persist(f) {
|
|
85
|
+
if (!this.store)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
f(this.store);
|
|
89
|
+
}
|
|
90
|
+
catch { /* memory stays right; the row is caught up at the next beat */ }
|
|
91
|
+
}
|
|
77
92
|
/** `handle`'s apps keep this liveness now: the beats of topics none keeps any more are dropped. */
|
|
78
93
|
set(handle, lives) {
|
|
79
94
|
if (!lives.length) {
|
|
@@ -86,6 +101,20 @@ export class Liveness {
|
|
|
86
101
|
for (const t of kept.keys())
|
|
87
102
|
if (!lives.some((l) => l.topic === t))
|
|
88
103
|
kept.delete(t);
|
|
104
|
+
if (!this.store)
|
|
105
|
+
return;
|
|
106
|
+
let rows = [];
|
|
107
|
+
this.persist((s) => { rows = s.beatsOf(handle); });
|
|
108
|
+
for (const t of new Set(rows.map((r) => r.topic)))
|
|
109
|
+
if (!lives.some((l) => l.topic === t))
|
|
110
|
+
this.persist((s) => s.dropBeats(handle, t));
|
|
111
|
+
// The beats kept before (a restart): each frame judged again by `observe`; one that no longer counts is deleted.
|
|
112
|
+
for (const r of rows) {
|
|
113
|
+
if (!lives.some((l) => l.topic === r.topic))
|
|
114
|
+
continue;
|
|
115
|
+
if (this.observe(handle, r.topic, r.frame, r.from) !== "accept")
|
|
116
|
+
this.persist((s) => s.dropBeats(handle, r.topic, r.sender));
|
|
117
|
+
}
|
|
89
118
|
}
|
|
90
119
|
/** The topics `handle`'s apps keep liveness for (each once). */
|
|
91
120
|
topics(handle) { return [...new Set((this.lives.get(handle) ?? []).map((l) => l.topic))]; }
|
|
@@ -123,12 +152,15 @@ export class Liveness {
|
|
|
123
152
|
if (!was || b.at > was.at) {
|
|
124
153
|
const beat = { sender, at: b.at, body: b.body, from };
|
|
125
154
|
bySender.set(sender, beat);
|
|
155
|
+
this.persist((s) => s.putBeat(handle, topic, { sender, at: b.at, from, frame }));
|
|
126
156
|
this.kept?.(handle, topic, beat);
|
|
127
157
|
}
|
|
128
158
|
// The stale ones go as the set is touched.
|
|
129
159
|
for (const [k, x] of bySender)
|
|
130
|
-
if (now - x.at >= window)
|
|
160
|
+
if (now - x.at >= window) {
|
|
131
161
|
bySender.delete(k);
|
|
162
|
+
this.persist((s) => s.dropBeats(handle, topic, k));
|
|
163
|
+
}
|
|
132
164
|
return "accept";
|
|
133
165
|
}
|
|
134
166
|
/** The beats live for `app`'s liveness on `topic` (newer than its window), newest first; undefined: the app keeps no liveness for the topic. */
|
|
@@ -147,8 +179,13 @@ export class Liveness {
|
|
|
147
179
|
const now = this.now();
|
|
148
180
|
return [...(this.beats.get(handle)?.get(topic)?.values() ?? [])].filter((b) => now - b.at < window);
|
|
149
181
|
}
|
|
150
|
-
/** Forget `handle`: its liveness and its beats. */
|
|
182
|
+
/** Forget `handle`: its liveness and its beats (the stored ones too). */
|
|
151
183
|
drop(handle) {
|
|
184
|
+
this.forget(handle);
|
|
185
|
+
this.persist((s) => s.dropBeats(handle));
|
|
186
|
+
}
|
|
187
|
+
/** Forget `handle` in memory only (the host stopping): the stored beats stay for the next start. */
|
|
188
|
+
forget(handle) {
|
|
152
189
|
this.lives.delete(handle);
|
|
153
190
|
this.beats.delete(handle);
|
|
154
191
|
}
|
package/dist/host/p2p.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Libp2p, PeerId } from "@libp2p/interface";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ComponentLogger } from "@libp2p/interface";
|
|
3
|
+
import { type KadDHT } from "@libp2p/kad-dht";
|
|
4
|
+
import { type Multiaddr } from "@multiformats/multiaddr";
|
|
5
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
3
6
|
import type { Store } from "../runtime/store.ts";
|
|
4
7
|
import { CID } from "multiformats/cid";
|
|
5
|
-
import { Liveness, type Live, type LiveBeat } from "./liveness.ts";
|
|
6
|
-
export type DhtMode = "off" | "client" | "server";
|
|
8
|
+
import { Liveness, type BeatStore, type Live, type LiveBeat } from "./liveness.ts";
|
|
9
|
+
export type DhtMode = "off" | "auto" | "client" | "server";
|
|
7
10
|
/** The host-wide settings: every node gets these. */
|
|
8
11
|
export interface P2PHostConfig {
|
|
9
12
|
listen: string[];
|
|
@@ -11,12 +14,20 @@ export interface P2PHostConfig {
|
|
|
11
14
|
dht: DhtMode;
|
|
12
15
|
relays: string[];
|
|
13
16
|
mdns: boolean;
|
|
17
|
+
/** UPnP/NAT-PMP port mapping (@libp2p/upnp-nat): SKEIN_LIBP2P_UPNP, on by default in hostP2PConfig; absent: off. */
|
|
18
|
+
upnp?: boolean;
|
|
14
19
|
/** PEM certificate and key for the WebSocket listener (WSS). */
|
|
15
20
|
tls?: {
|
|
16
21
|
cert: string;
|
|
17
22
|
key: string;
|
|
18
23
|
};
|
|
24
|
+
/** The network (SKEIN_NETWORK's names): the skein DHT's protocol names it (skeinDhtProtocol); default main. */
|
|
25
|
+
network?: string;
|
|
19
26
|
}
|
|
27
|
+
/** SKEIN_NETWORK's names as the networks' own (the Teranode block topic's, the skein DHT's). */
|
|
28
|
+
export declare const NETWORK_NAMES: Record<string, string>;
|
|
29
|
+
/** The skein DHT's protocol for a network: the overlay's provider records, never merged with another network's. */
|
|
30
|
+
export declare const skeinDhtProtocol: (network?: string) => string;
|
|
20
31
|
/** One instance's libp2p config (the genesis's `libp2p`): what the router subscribes and serves for it. */
|
|
21
32
|
export interface P2PInstanceConfig {
|
|
22
33
|
topics: string[];
|
|
@@ -24,8 +35,18 @@ export interface P2PInstanceConfig {
|
|
|
24
35
|
/** This instance's listen addresses (over the host's): a fixed port belongs to one node. */
|
|
25
36
|
listen?: string[];
|
|
26
37
|
}
|
|
27
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The default TCP port (2026-11-01): one fixed port on every install, so a user who opens one port knows
|
|
40
|
+
* which. 7534 is "SKEI" on a phone keypad; unassigned in the IANA registry (not IPFS's 4001, not
|
|
41
|
+
* Teranode's 9905/9915).
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_PORT = 7534;
|
|
44
|
+
/** The default listeners: TCP on every interface, IPv4 and IPv6, on DEFAULT_PORT; WebSocket on loopback, its port the OS's (plain WS off loopback: dev only). */
|
|
28
45
|
export declare const DEFAULT_LISTEN: string[];
|
|
46
|
+
/** Loopback only, ports chosen by the OS: the router's listeners when it is given no host settings (tests, an embedded router). */
|
|
47
|
+
export declare const LOOPBACK_LISTEN: string[];
|
|
48
|
+
/** `listen` with DEFAULT_PORT made an OS-chosen port (the fallback when the default port is taken). */
|
|
49
|
+
export declare const anyPort: (listen: string[]) => string[];
|
|
29
50
|
/** What the front door is called with for an inbound message or stream frame (#51). */
|
|
30
51
|
export interface InboundCall {
|
|
31
52
|
transport: "libp2p";
|
|
@@ -85,7 +106,16 @@ export interface P2POptions {
|
|
|
85
106
|
hostProtocols?: Record<string, (handle: string, remote: PeerId, frame: Uint8Array) => Promise<Uint8Array>>;
|
|
86
107
|
/** Each beat the liveness tool newly keeps from another peer (shruggr/skein#150). */
|
|
87
108
|
beat?(handle: string, topic: string, beat: LiveBeat): void;
|
|
109
|
+
/** Where the liveness tool persists the beats it keeps (host.db `live_beats`), reloaded when an instance's liveness is set. Absent: memory only. */
|
|
110
|
+
beats?: BeatStore;
|
|
111
|
+
/**
|
|
112
|
+
* The host node's peer key (#156): on a one-key host the key every instance's node uses (keyOf's
|
|
113
|
+
* answer for every handle), so the instances join the host node. Absent: a fresh key (a test's).
|
|
114
|
+
*/
|
|
115
|
+
hostKey?: PrivateKey;
|
|
88
116
|
}
|
|
117
|
+
/** A host topic's judge (hostTopic): each message's data and publisher → GossipSub's verdict. */
|
|
118
|
+
export type HostTopicHandler = (data: Uint8Array, from: PeerId) => "accept" | "reject" | "ignore";
|
|
89
119
|
/** A bsv secp256k1 key as a libp2p private key. */
|
|
90
120
|
export declare function libp2pKey(k: PrivateKey): import("@libp2p/interface").PrivateKey;
|
|
91
121
|
/** The peer ID of a key: the identity multihash of its protobuf (secp256k1, compressed). */
|
|
@@ -94,7 +124,20 @@ export declare function peerIdOf(k: PrivateKey): PeerId;
|
|
|
94
124
|
export declare function keyOfPeerId(id: string | Uint8Array): string | undefined;
|
|
95
125
|
/** A topic name's rendezvous CID: v1 raw over sha2-256 of the name (go-libp2p's nsToCid). */
|
|
96
126
|
export declare function topicCid(topic: string): Promise<CID>;
|
|
97
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* The network's overlay bootstrap peers (SKEIN_NETWORK's names): the hosted Open Exchange node per network,
|
|
129
|
+
* dialled beside SKEIN_LIBP2P_BOOTSTRAP's (unioned, as TERANODE_BOOTSTRAP is with SKEIN_TERANODE_BOOTSTRAP).
|
|
130
|
+
* A node whose own peer id is one of them skips it (discover()). Mainnet and regtest have none yet.
|
|
131
|
+
*/
|
|
132
|
+
export declare const OVERLAY_BOOTSTRAP: Record<string, string[]>;
|
|
133
|
+
/** SKEIN_LIBP2P_BOOTSTRAP over the network's defaults: unset, the network's; a list, unioned with them; `off`, none at all. */
|
|
134
|
+
export declare function overlayBootstrap(network: string, named: string | undefined): string[];
|
|
135
|
+
/**
|
|
136
|
+
* The bootstrap addresses a node dials now: not its own peer id (the hosted node is its network's default
|
|
137
|
+
* bootstrap), not a peer already connected; an address that does not parse is reported and skipped.
|
|
138
|
+
*/
|
|
139
|
+
export declare function bootstrapDials(bootstrap: string[], self: string, connected: Set<string>, bad?: (a: string) => void): string[];
|
|
140
|
+
/** The host-wide settings: the environment's SKEIN_LIBP2P_* (and SKEIN_NETWORK), else $SKEIN_HOME/host.env's. */
|
|
98
141
|
export declare function hostP2PConfig(vars: Record<string, string | undefined>, home?: string): P2PHostConfig;
|
|
99
142
|
/** The libp2p config a genesis carries (`libp2p: {topics?, protocols?, listen?}`), or undefined: no node. */
|
|
100
143
|
export declare function libp2pOf(g: Record<string, unknown> | null | undefined): P2PInstanceConfig | undefined;
|
|
@@ -214,7 +257,7 @@ export declare function foldBeacons(events: Iterable<BeaconEvent>, into?: Beacon
|
|
|
214
257
|
export declare const TOPIC_REQUIRED = "topic required";
|
|
215
258
|
export declare class P2PHost {
|
|
216
259
|
readonly o: P2POptions;
|
|
217
|
-
/** The liveness tool's state (#138): the apps' liveness and the beats kept
|
|
260
|
+
/** The liveness tool's state (#138): the apps' liveness and the beats kept (persisted in `o.beats` when given). */
|
|
218
261
|
readonly live: Liveness;
|
|
219
262
|
/** The nodes, by peer ID (#160: one per key). */
|
|
220
263
|
private nodes;
|
|
@@ -225,10 +268,35 @@ export declare class P2PHost {
|
|
|
225
268
|
/** Each key's declares, in order (one at a time). */
|
|
226
269
|
private starting;
|
|
227
270
|
private stopped;
|
|
271
|
+
/** The host node's key (hostKey, else a fresh one) and its peer ID. */
|
|
272
|
+
private readonly hostKey;
|
|
273
|
+
readonly hostId: string;
|
|
228
274
|
constructor(o: P2POptions);
|
|
229
275
|
private say;
|
|
230
|
-
/** A node's name in a log line: its one handle (as before #160), else its first. */
|
|
276
|
+
/** A node's name in a log line: its one handle (as before #160), else its first; the host node with none: the router. */
|
|
231
277
|
private nameOf;
|
|
278
|
+
/**
|
|
279
|
+
* The host node: the node of the host key, started (once) with no handles when no instance has
|
|
280
|
+
* started it yet; an instance on that key that starts later joins it (#160), one that started
|
|
281
|
+
* earlier made it.
|
|
282
|
+
*/
|
|
283
|
+
hostNode(): Promise<Libp2p>;
|
|
284
|
+
/** The host node's record, once it runs. */
|
|
285
|
+
private hostOf;
|
|
286
|
+
/**
|
|
287
|
+
* A host topic on the host node (#156: the Teranode block topic): subscribed, each message judged
|
|
288
|
+
* by `take` alone (its verdict GossipSub's), never the front door, never advertised on the skein
|
|
289
|
+
* DHT. Undefined `take`: the topic left.
|
|
290
|
+
*/
|
|
291
|
+
hostTopic(topic: string, take: HostTopicHandler | undefined): Promise<void>;
|
|
292
|
+
/** The peers the host node knows subscribe `topic` (GossipSub's view). */
|
|
293
|
+
hostSubscribers(topic: string): string[];
|
|
294
|
+
/** The host node's skein DHT's mode now (client | server; undefined: off or no host node yet). */
|
|
295
|
+
hostDhtMode(): "client" | "server" | undefined;
|
|
296
|
+
/** The host node's confirmed public addresses (AutoNAT's, or UPnP's once AutoNAT confirmed them): libp2p announces only confirmed ones; relay addresses aside. */
|
|
297
|
+
hostPublicAddrs(): string[];
|
|
298
|
+
/** The host node's DHT service, the skein DHT (undefined: SKEIN_LIBP2P_DHT=off, or no host node yet). */
|
|
299
|
+
hostDht(): KadDHT | undefined;
|
|
232
300
|
/** The node of an instance, if it has one. */
|
|
233
301
|
node(handle: string): Libp2p | undefined;
|
|
234
302
|
/** An instance's peer ID (derived: whether or not its node runs). */
|
|
@@ -255,7 +323,7 @@ export declare class P2PHost {
|
|
|
255
323
|
private subscribe;
|
|
256
324
|
/** The topics `handle` takes: its config's and its apps' liveness topics (#138). */
|
|
257
325
|
private takes;
|
|
258
|
-
/** Subscribe what the handles' configs and liveness tools (#138) take, unsubscribe the rest: the topics added and removed. */
|
|
326
|
+
/** Subscribe what the handles' configs and liveness tools (#138) take, and the host's own topics (#156), unsubscribe the rest: the topics added and removed. */
|
|
259
327
|
private resubscribe;
|
|
260
328
|
/** Handle the protocols the handles declare, unhandle those none declares any more (the host's own aside). */
|
|
261
329
|
private rehandle;
|
|
@@ -271,6 +339,7 @@ export declare class P2PHost {
|
|
|
271
339
|
private hostServed;
|
|
272
340
|
private handleHostProtocol;
|
|
273
341
|
private handleProtocol;
|
|
342
|
+
/** A node for `key` (its private key `pk`): with `handle` and its config, or the host node with none (#156). */
|
|
274
343
|
private start;
|
|
275
344
|
/** Keep the bootstrap peers connected; with the DHT on, advertise each topic and dial its providers. */
|
|
276
345
|
private discover;
|
|
@@ -335,3 +404,33 @@ export declare class P2PHost {
|
|
|
335
404
|
private drop;
|
|
336
405
|
stop(): Promise<void>;
|
|
337
406
|
}
|
|
407
|
+
/** A multiaddr on a loopback or private network (dev: the DHT keeps such addresses). */
|
|
408
|
+
export declare function isPrivateAddr(ma: Multiaddr): boolean;
|
|
409
|
+
/**
|
|
410
|
+
* kad-DHT's clientMode for a mode of the skein DHT (2026-11-01; @libp2p/kad-dht 16.4.5 kad-dht.js): `auto` leaves it
|
|
411
|
+
* unset — the DHT's own switch (a client, a server while the node has a confirmed public address);
|
|
412
|
+
* `client` true — only ever a client; `server` false — a server from its start (its start sets the mode),
|
|
413
|
+
* never switched back. An explicit false is NOT auto in this version: the switch listens only when unset.
|
|
414
|
+
*/
|
|
415
|
+
export declare function dhtClientMode(mode: Exclude<DhtMode, "off">): {
|
|
416
|
+
clientMode?: boolean;
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* The libp2p logger, with the UPnP service's outcome told once per distinct line (2026-11-01): the gateway
|
|
420
|
+
* search's result, the public IP found, each mapping made or refused, an error. The service reports its
|
|
421
|
+
* outcome nowhere else (its log only); every other component's log is the default, untouched.
|
|
422
|
+
*/
|
|
423
|
+
export declare function upnpTold(tell: (line: string) => void): ComponentLogger;
|
|
424
|
+
/** A node's DHT service, the skein DHT (#156), or undefined (SKEIN_LIBP2P_DHT=off). */
|
|
425
|
+
export declare function dhtOf(node: Libp2p): KadDHT | undefined;
|
|
426
|
+
/** The providers a DHT finds for `cid` (its PROVIDER events' peers), on that DHT alone. */
|
|
427
|
+
export declare function providersOf(dht: KadDHT, cid: CID, options?: {
|
|
428
|
+
signal?: AbortSignal;
|
|
429
|
+
}): AsyncIterable<{
|
|
430
|
+
id: PeerId;
|
|
431
|
+
multiaddrs: Multiaddr[];
|
|
432
|
+
}>;
|
|
433
|
+
/** Run a DHT operation's events to the end. */
|
|
434
|
+
export declare function drain(events: AsyncIterable<unknown>): Promise<void>;
|
|
435
|
+
/** A DHT's routing table size (undefined: no such DHT). */
|
|
436
|
+
export declare function tableSize(dht: KadDHT | undefined): number | undefined;
|