@dcasegr/skein 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +522 -2
  2. package/bin/skein-host.js +22 -0
  3. package/bin/skein.js +8 -0
  4. package/dist/client/admin-cli.d.ts +10 -0
  5. package/dist/client/admin-cli.js +370 -0
  6. package/dist/client/admin.d.ts +139 -0
  7. package/dist/client/admin.js +256 -0
  8. package/dist/client/bundle.d.ts +9 -0
  9. package/dist/client/bundle.js +34 -0
  10. package/dist/client/cli.d.ts +44 -0
  11. package/dist/client/cli.js +306 -0
  12. package/dist/client/client.d.ts +161 -0
  13. package/dist/client/client.js +237 -0
  14. package/dist/client/config.d.ts +18 -0
  15. package/dist/client/config.js +31 -0
  16. package/dist/client/conversation.d.ts +62 -0
  17. package/dist/client/conversation.js +113 -0
  18. package/dist/client/raw.d.ts +94 -0
  19. package/dist/client/raw.js +154 -0
  20. package/dist/client/target.d.ts +34 -0
  21. package/dist/client/target.js +158 -0
  22. package/dist/dev/cli.d.ts +8 -0
  23. package/dist/dev/cli.js +248 -0
  24. package/dist/dev/scan.d.ts +15 -0
  25. package/dist/dev/scan.js +173 -0
  26. package/dist/envelope-cbor.d.ts +110 -0
  27. package/dist/envelope-cbor.js +160 -0
  28. package/dist/envelope.d.ts +38 -0
  29. package/dist/envelope.js +101 -0
  30. package/dist/host/arc.d.ts +131 -0
  31. package/dist/host/arc.js +425 -0
  32. package/dist/host/billing.d.ts +102 -0
  33. package/dist/host/billing.js +167 -0
  34. package/dist/host/boot.d.ts +246 -0
  35. package/dist/host/boot.js +643 -0
  36. package/dist/host/cli.d.ts +57 -0
  37. package/dist/host/cli.js +1253 -0
  38. package/dist/host/clock.d.ts +3 -0
  39. package/dist/host/clock.js +8 -0
  40. package/dist/host/control.d.ts +45 -0
  41. package/dist/host/control.js +170 -0
  42. package/dist/host/cron.d.ts +99 -0
  43. package/dist/host/cron.js +196 -0
  44. package/dist/host/deliver.d.ts +53 -0
  45. package/dist/host/deliver.js +251 -0
  46. package/dist/host/deploy.d.ts +29 -0
  47. package/dist/host/deploy.js +55 -0
  48. package/dist/host/door-answer.d.ts +24 -0
  49. package/dist/host/door-answer.js +21 -0
  50. package/dist/host/events.d.ts +89 -0
  51. package/dist/host/events.js +222 -0
  52. package/dist/host/fake-arcade.d.ts +40 -0
  53. package/dist/host/fake-arcade.js +149 -0
  54. package/dist/host/fake-discovery.d.ts +4 -0
  55. package/dist/host/fake-discovery.js +58 -0
  56. package/dist/host/feeds.d.ts +127 -0
  57. package/dist/host/feeds.js +422 -0
  58. package/dist/host/frontdoor.d.ts +92 -0
  59. package/dist/host/frontdoor.js +172 -0
  60. package/dist/host/genesis.d.ts +259 -0
  61. package/dist/host/genesis.js +353 -0
  62. package/dist/host/handles.d.ts +40 -0
  63. package/dist/host/handles.js +88 -0
  64. package/dist/host/hostenv.d.ts +30 -0
  65. package/dist/host/hostenv.js +83 -0
  66. package/dist/host/image-chain.d.ts +127 -0
  67. package/dist/host/image-chain.js +440 -0
  68. package/dist/host/install.d.ts +22 -0
  69. package/dist/host/install.js +139 -0
  70. package/dist/host/instances.d.ts +227 -0
  71. package/dist/host/instances.js +430 -0
  72. package/dist/host/kernel.d.ts +246 -0
  73. package/dist/host/kernel.js +603 -0
  74. package/dist/host/liveness.d.ts +65 -0
  75. package/dist/host/liveness.js +155 -0
  76. package/dist/host/manifest.d.ts +160 -0
  77. package/dist/host/manifest.js +597 -0
  78. package/dist/host/p2p.d.ts +337 -0
  79. package/dist/host/p2p.js +1002 -0
  80. package/dist/host/packet.d.ts +121 -0
  81. package/dist/host/packet.js +562 -0
  82. package/dist/host/plan.d.ts +154 -0
  83. package/dist/host/plan.js +418 -0
  84. package/dist/host/providers.d.ts +238 -0
  85. package/dist/host/providers.js +565 -0
  86. package/dist/host/roster.d.ts +72 -0
  87. package/dist/host/roster.js +172 -0
  88. package/dist/host/router.d.ts +773 -0
  89. package/dist/host/router.js +2389 -0
  90. package/dist/host/signer.d.ts +49 -0
  91. package/dist/host/signer.js +167 -0
  92. package/dist/host/store-sqlite.d.ts +39 -0
  93. package/dist/host/store-sqlite.js +170 -0
  94. package/dist/host/stores.d.ts +43 -0
  95. package/dist/host/stores.js +342 -0
  96. package/dist/host/sync.d.ts +121 -0
  97. package/dist/host/sync.js +467 -0
  98. package/dist/host/testbeef.d.ts +21 -0
  99. package/dist/host/testbeef.js +37 -0
  100. package/dist/host/testhost.d.ts +149 -0
  101. package/dist/host/testhost.js +211 -0
  102. package/dist/host/vcdiff.d.ts +9 -0
  103. package/dist/host/vcdiff.js +210 -0
  104. package/dist/host/wasm-kernel.d.ts +87 -0
  105. package/dist/host/wasm-kernel.js +296 -0
  106. package/dist/host/wasm-worker.d.ts +1 -0
  107. package/dist/host/wasm-worker.js +186 -0
  108. package/dist/peers/cron.d.ts +33 -0
  109. package/dist/peers/cron.js +75 -0
  110. package/dist/peers/infer-main.d.ts +2 -0
  111. package/dist/peers/infer-main.js +74 -0
  112. package/dist/peers/infer.d.ts +163 -0
  113. package/dist/peers/infer.js +367 -0
  114. package/dist/runtime/beef.d.ts +28 -0
  115. package/dist/runtime/beef.js +107 -0
  116. package/dist/runtime/bitcoin.d.ts +36 -0
  117. package/dist/runtime/bitcoin.js +141 -0
  118. package/dist/runtime/cid.d.ts +14 -0
  119. package/dist/runtime/cid.js +51 -0
  120. package/dist/runtime/dispatch.d.ts +92 -0
  121. package/dist/runtime/dispatch.js +142 -0
  122. package/dist/runtime/envelope.d.ts +60 -0
  123. package/dist/runtime/envelope.js +101 -0
  124. package/dist/runtime/heads.d.ts +25 -0
  125. package/dist/runtime/heads.js +37 -0
  126. package/dist/runtime/identity.d.ts +28 -0
  127. package/dist/runtime/identity.js +67 -0
  128. package/dist/runtime/index-store.d.ts +152 -0
  129. package/dist/runtime/index-store.js +715 -0
  130. package/dist/runtime/log.d.ts +62 -0
  131. package/dist/runtime/log.js +104 -0
  132. package/dist/runtime/programs.d.ts +30 -0
  133. package/dist/runtime/programs.js +61 -0
  134. package/dist/runtime/records.d.ts +135 -0
  135. package/dist/runtime/records.js +149 -0
  136. package/dist/runtime/sqlite.d.ts +33 -0
  137. package/dist/runtime/sqlite.js +604 -0
  138. package/dist/runtime/store.d.ts +141 -0
  139. package/dist/runtime/store.js +24 -0
  140. package/dist/runtime/syscalls.d.ts +9 -0
  141. package/dist/runtime/syscalls.js +41 -0
  142. package/dist/runtime/tree.d.ts +60 -0
  143. package/dist/runtime/tree.js +196 -0
  144. package/dist/runtime/types.d.ts +100 -0
  145. package/dist/runtime/types.js +7 -0
  146. package/dist/testapps.d.ts +101 -0
  147. package/dist/testapps.js +275 -0
  148. package/dist/testkit.d.ts +25 -0
  149. package/dist/testkit.js +48 -0
  150. package/dist/wallet.d.ts +35 -0
  151. package/dist/wallet.js +87 -0
  152. package/images/README.md +68 -0
  153. package/images/default/README.md +38 -0
  154. package/images/default/bin/frontdoor.cid +1 -0
  155. package/images/default/bin/frontdoor.json +5 -0
  156. package/images/default/bin/messagebox.cid +1 -0
  157. package/images/default/bin/messagebox.json +8 -0
  158. package/images/default/bin/wallet.cid +1 -0
  159. package/images/default/bin/wallet.json +14 -0
  160. package/images/default/etc/config.json +1 -0
  161. package/images/default/etc/dispatch.json +82 -0
  162. package/images/host/etc/apps.json +8 -0
  163. package/kernel-zig/README.md +1247 -0
  164. package/kernel-zig/web/client.js +65 -0
  165. package/kernel-zig/web/kernel.d.ts +59 -0
  166. package/kernel-zig/web/kernel.js +329 -0
  167. package/kernel-zig/web/store.js +67 -0
  168. package/kernel-zig/web/worker.js +79 -0
  169. package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
  170. package/package.json +77 -4
  171. package/wasm/README.md +35 -0
  172. package/wasm/frontdoor.wasm +0 -0
  173. package/wasm/messagebox.wasm +0 -0
  174. package/wasm/resolve.wasm +0 -0
  175. package/wasm/wallet.wasm +0 -0
  176. package/wasm/wire-probe.wasm +0 -0
@@ -0,0 +1,3 @@
1
+ import { type Stamp } from "../runtime/syscalls.ts";
2
+ /** Wall time now, ms resolution, as a stamp. */
3
+ export declare function now(): Stamp;
@@ -0,0 +1,8 @@
1
+ // The host's clock: the one place the node host reads wall time for a log
2
+ // entry's stamp (a genesis, an admission). Nothing inside the kernel reads a
3
+ // clock; everything there derives time from these stamps.
4
+ import { msStamp } from "../runtime/syscalls.js";
5
+ /** Wall time now, ms resolution, as a stamp. */
6
+ export function now() {
7
+ return msStamp(Date.now());
8
+ }
@@ -0,0 +1,45 @@
1
+ import { type Server } from "node:net";
2
+ export declare const CONTROL_SOCKET = "host.sock";
3
+ export type ControlRequest = {
4
+ op: "event";
5
+ handle: string;
6
+ box: string;
7
+ event: Record<string, unknown>;
8
+ } | {
9
+ op: "reclaim";
10
+ handle: string;
11
+ } | {
12
+ op: "message";
13
+ handle: string;
14
+ message: string;
15
+ body: string;
16
+ };
17
+ export type ControlAnswer = {
18
+ ok: true;
19
+ entry: string;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ /** What the socket's owner does with a request (the router: cronEvent). */
25
+ export interface ControlHandler {
26
+ event(handle: string, box: string, event: Record<string, unknown>): Promise<string>;
27
+ /** #130: `skein-host reclaim --yes` (Router.reclaim); absent: refused. */
28
+ reclaim?(handle: string): Promise<void>;
29
+ /** #142: a signed message for `handle` (its mail record's dag-cbor and its body), appended as a `local` request (Router.messageLocal); absent: refused. */
30
+ message?(handle: string, message: Uint8Array, body: Uint8Array): Promise<string>;
31
+ }
32
+ /** The longest request line: ours to choose (#142) — a message carries a module whole (base64), so 64 MiB. */
33
+ export declare const MAX_LINE: number;
34
+ /**
35
+ * Listen on `path`: a socket left by a router that is gone is replaced; one
36
+ * that answers is another router's, and this one refuses to start on it.
37
+ */
38
+ export declare function listenControl(path: string, h: ControlHandler): Promise<Server>;
39
+ /** Close the socket and remove its file. */
40
+ export declare function closeControl(server: Server, path: string): Promise<void>;
41
+ /**
42
+ * Send one request to the router on `path`. Undefined if no router answers
43
+ * there (no socket, or one left by a router that is gone); its answer otherwise.
44
+ */
45
+ export declare function controlRequest(path: string, req: ControlRequest, timeoutMs?: number): Promise<ControlAnswer | undefined>;
@@ -0,0 +1,170 @@
1
+ // The router's control socket (#60 follow-up): a Unix socket at
2
+ // $SKEIN_HOME/host.sock, mode 0600 — owner-only by file permissions, and no
3
+ // HTTP route leads to it. A running router listens on it so that a local
4
+ // command can reach the kernels it already holds: a store has one kernel, and
5
+ // a second process opening it would fork the log.
6
+ //
7
+ // The protocol is one JSON line each way, and it carries one request:
8
+ //
9
+ // {"op": "event", "handle": h, "box": b, "event": {…}} → {"ok": true, "entry": "<cid>"} | {"ok": false, "error": "…"}
10
+ // {"op": "reclaim", "handle": h} → {"ok": true, "entry": ""} | {"ok": false, "error": "…"}
11
+ // {"op": "message", "handle": h, "message": "<base64>", "body": "<base64>"}
12
+ // → {"ok": true, "entry": "<cid>"} | {"ok": false, "error": "…"}
13
+ //
14
+ // `event` is `skein-host event` (cli.ts): the router sends it as a message
15
+ // from its cron provider, a tick due now (Router.cronEvent, #69).
16
+ // `message` (#142) is a message the client `skein` signed in its own process
17
+ // (src/client/raw.ts signMessage: the operator's key), delivered on the host
18
+ // machine: `message` its mail record's dag-cbor, `body` the body's bytes. The
19
+ // router appends it as received, a `local` request (Router.messageLocal →
20
+ // appendLocal, the path a forwarded claim takes): the front door checks its
21
+ // signature and admits it by the dispatch table, as any signed message. The
22
+ // router signs nothing; the answer is the entry, once the front door has
23
+ // judged it (a refusal is an error).
24
+ import { chmodSync, existsSync, unlinkSync } from "node:fs";
25
+ import { createConnection, createServer } from "node:net";
26
+ export const CONTROL_SOCKET = "host.sock";
27
+ /** The longest request line: ours to choose (#142) — a message carries a module whole (base64), so 64 MiB. */
28
+ export const MAX_LINE = 64 << 20;
29
+ /** A request line, checked. */
30
+ function requestOf(line) {
31
+ let r;
32
+ try {
33
+ r = JSON.parse(line);
34
+ }
35
+ catch {
36
+ throw new Error("not JSON");
37
+ }
38
+ const o = r;
39
+ if (o && typeof o === "object" && o.op === "message") {
40
+ if (typeof o.handle !== "string" || !o.handle)
41
+ throw new Error("a message names its instance (handle)");
42
+ if (typeof o.message !== "string" || typeof o.body !== "string")
43
+ throw new Error("a message is {message, body}, each base64");
44
+ return o;
45
+ }
46
+ if (o && typeof o === "object" && o.op === "reclaim") {
47
+ if (typeof o.handle !== "string" || !o.handle)
48
+ throw new Error("a reclaim names its instance (handle)");
49
+ return o;
50
+ }
51
+ if (!o || typeof o !== "object" || o.op !== "event")
52
+ throw new Error("want {op: \"event\", handle, box, event}, {op: \"reclaim\", handle} or {op: \"message\", handle, message, body}");
53
+ if (typeof o.handle !== "string" || !o.handle || typeof o.box !== "string" || !o.box)
54
+ throw new Error("an event names its instance (handle) and its box");
55
+ if (!o.event || typeof o.event !== "object" || Array.isArray(o.event))
56
+ throw new Error("the event is a JSON object");
57
+ return o;
58
+ }
59
+ /**
60
+ * Listen on `path`: a socket left by a router that is gone is replaced; one
61
+ * that answers is another router's, and this one refuses to start on it.
62
+ */
63
+ export async function listenControl(path, h) {
64
+ if (existsSync(path)) {
65
+ if (await answers(path))
66
+ throw new Error(`${path}: another router answers on it`);
67
+ unlinkSync(path);
68
+ }
69
+ const server = createServer((s) => serveOne(s, h));
70
+ await new Promise((resolve, reject) => {
71
+ server.once("error", reject);
72
+ server.listen(path, () => { server.off("error", reject); resolve(); });
73
+ });
74
+ chmodSync(path, 0o600);
75
+ return server;
76
+ }
77
+ /** Close the socket and remove its file. */
78
+ export async function closeControl(server, path) {
79
+ await new Promise((r) => server.close(() => r()));
80
+ try {
81
+ unlinkSync(path);
82
+ }
83
+ catch { /* gone already */ }
84
+ }
85
+ function serveOne(s, h) {
86
+ let buf = "";
87
+ let done = false;
88
+ const reply = (a) => { if (done)
89
+ return; done = true; s.end(`${JSON.stringify(a)}\n`); };
90
+ s.setEncoding("utf8");
91
+ s.on("error", () => { });
92
+ s.on("data", (d) => {
93
+ if (done)
94
+ return;
95
+ buf += d;
96
+ const nl = buf.indexOf("\n");
97
+ if (nl < 0) {
98
+ if (buf.length > MAX_LINE)
99
+ reply({ ok: false, error: "request too long" });
100
+ return;
101
+ }
102
+ let req;
103
+ try {
104
+ req = requestOf(buf.slice(0, nl));
105
+ }
106
+ catch (e) {
107
+ reply({ ok: false, error: e.message });
108
+ return;
109
+ }
110
+ if (req.op === "message") {
111
+ if (!h.message) {
112
+ reply({ ok: false, error: "this router takes no messages here" });
113
+ return;
114
+ }
115
+ h.message(req.handle, Buffer.from(req.message, "base64"), Buffer.from(req.body, "base64")).then((entry) => reply({ ok: true, entry }), (e) => reply({ ok: false, error: e.message }));
116
+ return;
117
+ }
118
+ if (req.op === "reclaim") {
119
+ if (!h.reclaim) {
120
+ reply({ ok: false, error: "this router reclaims nothing" });
121
+ return;
122
+ }
123
+ h.reclaim(req.handle).then(() => reply({ ok: true, entry: "" }), (e) => reply({ ok: false, error: e.message }));
124
+ return;
125
+ }
126
+ h.event(req.handle, req.box, req.event).then((entry) => reply({ ok: true, entry }), (e) => reply({ ok: false, error: e.message }));
127
+ });
128
+ }
129
+ /** Whether something answers on the socket at `path` (connects). */
130
+ function answers(path) {
131
+ return new Promise((resolve) => {
132
+ const c = createConnection(path);
133
+ c.once("connect", () => { c.destroy(); resolve(true); });
134
+ c.once("error", () => resolve(false));
135
+ });
136
+ }
137
+ /**
138
+ * Send one request to the router on `path`. Undefined if no router answers
139
+ * there (no socket, or one left by a router that is gone); its answer otherwise.
140
+ */
141
+ export function controlRequest(path, req, timeoutMs = 60_000) {
142
+ if (!existsSync(path))
143
+ return Promise.resolve(undefined);
144
+ return new Promise((resolve, reject) => {
145
+ const c = createConnection(path);
146
+ let buf = "";
147
+ let connected = false;
148
+ const t = setTimeout(() => { c.destroy(); reject(new Error(`${path}: no answer in ${timeoutMs} ms`)); }, timeoutMs);
149
+ c.setEncoding("utf8");
150
+ c.once("connect", () => { connected = true; c.write(`${JSON.stringify(req)}\n`); });
151
+ c.on("data", (d) => { buf += d; });
152
+ c.once("end", () => {
153
+ clearTimeout(t);
154
+ try {
155
+ resolve(JSON.parse(buf.split("\n")[0]));
156
+ }
157
+ catch {
158
+ reject(new Error(`${path}: a malformed answer`));
159
+ }
160
+ });
161
+ c.once("error", (e) => {
162
+ clearTimeout(t);
163
+ // Not there, or nobody listening: no router answers on it.
164
+ if (!connected)
165
+ resolve(undefined);
166
+ else
167
+ reject(e);
168
+ });
169
+ });
170
+ }
@@ -0,0 +1,99 @@
1
+ /** A tick request's schedule: every `every` ms, or once at `at`, into `box`. */
2
+ export interface TickSpec {
3
+ every?: number;
4
+ at?: number;
5
+ box: string;
6
+ body?: Record<string, unknown>;
7
+ }
8
+ /** One schedule: whose (the instance the ticks go to, its key), its name and spec, the request that made it, the next tick. */
9
+ export interface Schedule extends TickSpec {
10
+ instance: string;
11
+ recipient: string;
12
+ name: string;
13
+ request: string;
14
+ next: number;
15
+ }
16
+ /** A request the cron provider takes (box `cron`), checked; an error's text otherwise. */
17
+ export type CronRequest = {
18
+ fn: "tick";
19
+ name: string;
20
+ spec: TickSpec;
21
+ } | {
22
+ fn: "stop";
23
+ name: string;
24
+ };
25
+ export declare const CRON_SHAPE = "the cron provider takes {fn: \"tick\", every: <ms> | at: <ms since the epoch>, box, body?: {\u2026}, name} or {fn: \"stop\", name} in box \"cron\"";
26
+ /** A body as the cron provider reads it. */
27
+ export declare function cronRequestOf(b: unknown): CronRequest | string;
28
+ /** A tick's body. */
29
+ export declare function tickBody(s: Pick<Schedule, "name" | "body">, due: number): Record<string, unknown>;
30
+ /** Where schedules are kept (the reference host: host.db; a test or a remote service: memory). */
31
+ export interface ScheduleStore {
32
+ all(): Schedule[];
33
+ save(s: Schedule): void;
34
+ drop(instance: string, name: string): void;
35
+ }
36
+ /** A store in memory. */
37
+ export declare function memorySchedules(): ScheduleStore;
38
+ /** host.db's `cron_schedule` as a ScheduleStore (the spec as dag-cbor). */
39
+ export declare function dbSchedules(db: {
40
+ saveSchedule(s: {
41
+ instance: string;
42
+ name: string;
43
+ recipient: string;
44
+ spec: Uint8Array;
45
+ request: string;
46
+ next: number;
47
+ }): void;
48
+ dropSchedule(instance: string, name: string): boolean;
49
+ schedules(): Array<{
50
+ instance: string;
51
+ name: string;
52
+ recipient: string;
53
+ spec: Uint8Array;
54
+ request: string;
55
+ next: number;
56
+ }>;
57
+ }): ScheduleStore;
58
+ export interface CronOptions {
59
+ /** The clock (ms). */
60
+ now(): number;
61
+ /** Send one tick: a signed message from the provider to `s.recipient` in `s.box`. Throws if it could not go (an `at` tick is tried again). */
62
+ tick(s: Schedule, body: Record<string, unknown>): Promise<void>;
63
+ /** Whether the instance should be woken for a tick (the reference host: running, or something in it subscribes the box); default yes. */
64
+ wake?(s: Schedule): Promise<boolean>;
65
+ /** Where the schedules are kept (default memory). */
66
+ store?: ScheduleStore;
67
+ log?(source: string, line: string): void;
68
+ /** An `at` tick that could not go is tried again this much later (ms; default 60 000). */
69
+ retryMs?: number;
70
+ }
71
+ export declare class Cron {
72
+ readonly o: CronOptions;
73
+ private readonly store;
74
+ private timer?;
75
+ private ticking;
76
+ private stopped;
77
+ /** Whether this one sends ticks (start): a schedule asked of one that does not is kept for one that does. */
78
+ private started;
79
+ constructor(o: CronOptions);
80
+ private say;
81
+ /** Take up the kept schedules (a host start): each `every` one ticks once now (late), each `at` one at its time. */
82
+ start(): void;
83
+ /**
84
+ * A request from `instance` (whose key is `recipient`), the message
85
+ * `request`: → the answer body (beside `replyTo`). A tick request replaces
86
+ * the schedule of its name; stop ends it.
87
+ */
88
+ request(instance: string, recipient: string, request: string, body: unknown): Record<string, unknown>;
89
+ /** `instance`'s schedules and when each ticks next (the host page, tests). */
90
+ of(instance: string): Schedule[];
91
+ /** Drop every schedule of an instance that is gone. */
92
+ forget(instance: string): void;
93
+ /** Send every tick that is due (one pass at a time); then wait for the next. */
94
+ tick(): Promise<void>;
95
+ /** No more ticks; resolves once a pass under way has finished. */
96
+ stop(): Promise<void>;
97
+ private schedule;
98
+ private fire;
99
+ }
@@ -0,0 +1,196 @@
1
+ // The cron provider (#69): scheduling is a message to a provider. A program
2
+ // that wants to be ticked emits, to the cron provider's key (#126: the
3
+ // address book's entry reached at `local` `cron`, or a key its owner gives
4
+ // it), box `cron`:
5
+ //
6
+ // {fn: "tick", every: <ms> | at: <ms since the epoch>, box, body?: {…}, name}
7
+ // {fn: "stop", name}
8
+ //
9
+ // and the provider answers it ({replyTo, name, next: <ms>} | {replyTo, name,
10
+ // stopped: true | false} | {replyTo, error}), then sends each tick as a signed
11
+ // message from its own identity into the named box of the instance that
12
+ // asked:
13
+ //
14
+ // {...body, kind: body.kind ?? "cron", name, due: <ms>}
15
+ //
16
+ // routed there by the instance's subscription to that box (sender the cron
17
+ // provider, or anyone). `due` is when the tick was scheduled; the message's
18
+ // entry stamp is when it arrived. A tick request replaces the instance's
19
+ // schedule of the same name; `stop` ends it. An `every` schedule ticks at
20
+ // once, then every `every` ms on that grid; a tick the provider was too
21
+ // late for is not made up (one tick, then the next grid point after now). An
22
+ // `at` schedule ticks once — at `at`, or at once if that is past — and is
23
+ // then gone. The schedules are the provider's state, kept across restarts
24
+ // (the reference host: host.db `cron_schedule`); at a host start every
25
+ // `every` schedule ticks once (a restart is a late tick), each `at` one at
26
+ // its time.
27
+ //
28
+ // This module is the schedule itself (`Cron`): the provider on the reference
29
+ // host (providers.ts, its ticks appended as `local` requests) and a remote
30
+ // cron service (src/peers/cron.ts, its ticks delivered to the instance's
31
+ // messagebox over BRC-103/104) both run it. Which one an instance talks to
32
+ // is its address book's business: the same key, `local` or `mailbox`.
33
+ import * as dagCbor from "@ipld/dag-cbor";
34
+ export const CRON_SHAPE = "the cron provider takes {fn: \"tick\", every: <ms> | at: <ms since the epoch>, box, body?: {…}, name} or {fn: \"stop\", name} in box \"cron\"";
35
+ /** A body as the cron provider reads it. */
36
+ export function cronRequestOf(b) {
37
+ const o = b;
38
+ if (!o || typeof o !== "object" || Array.isArray(o) || o instanceof Uint8Array)
39
+ return CRON_SHAPE;
40
+ if (typeof o.name !== "string" || !o.name)
41
+ return `${CRON_SHAPE}: a request names its schedule (name)`;
42
+ if (o.fn === "stop")
43
+ return { fn: "stop", name: o.name };
44
+ if (o.fn !== "tick")
45
+ return CRON_SHAPE;
46
+ if (typeof o.box !== "string" || !o.box || o.box.startsWith(":"))
47
+ return `${CRON_SHAPE}: box is a box name (not empty, not reserved)`;
48
+ if ((o.every === undefined) === (o.at === undefined))
49
+ return `${CRON_SHAPE}: one of every and at`;
50
+ if (o.every !== undefined && !(Number.isSafeInteger(o.every) && o.every > 0))
51
+ return `${CRON_SHAPE}: every is a positive whole number of ms`;
52
+ if (o.at !== undefined && !(Number.isSafeInteger(o.at) && o.at >= 0))
53
+ return `${CRON_SHAPE}: at is a whole number of ms since the epoch`;
54
+ if (o.body !== undefined && (!o.body || typeof o.body !== "object" || Array.isArray(o.body) || o.body instanceof Uint8Array))
55
+ return `${CRON_SHAPE}: body is a map`;
56
+ return { fn: "tick", name: o.name, spec: { box: o.box, ...(o.every !== undefined ? { every: o.every } : { at: o.at }), ...(o.body !== undefined ? { body: o.body } : {}) } };
57
+ }
58
+ /** A tick's body. */
59
+ export function tickBody(s, due) {
60
+ const body = s.body ?? {};
61
+ return { ...body, kind: body.kind ?? "cron", name: s.name, due };
62
+ }
63
+ /** How a schedule is described in log lines. */
64
+ const label = (s) => `${s.name} (${s.box} ${s.every !== undefined ? `every ${s.every} ms` : `at ${new Date(s.at).toISOString()}`})`;
65
+ /** A store in memory. */
66
+ export function memorySchedules() {
67
+ const m = new Map();
68
+ return {
69
+ all: () => [...m.values()],
70
+ save: (s) => { m.set(`${s.instance}\0${s.name}`, { ...s }); },
71
+ drop: (i, n) => { m.delete(`${i}\0${n}`); },
72
+ };
73
+ }
74
+ /** host.db's `cron_schedule` as a ScheduleStore (the spec as dag-cbor). */
75
+ export function dbSchedules(db) {
76
+ return {
77
+ all: () => db.schedules().map((r) => ({ instance: r.instance, name: r.name, recipient: r.recipient, request: r.request, next: r.next, ...dagCbor.decode(r.spec) })),
78
+ save: (s) => db.saveSchedule({ instance: s.instance, name: s.name, recipient: s.recipient, request: s.request, next: s.next, spec: dagCbor.encode({ box: s.box, ...(s.every !== undefined ? { every: s.every } : { at: s.at }), ...(s.body !== undefined ? { body: s.body } : {}) }) }),
79
+ drop: (i, n) => { db.dropSchedule(i, n); },
80
+ };
81
+ }
82
+ /** setTimeout's longest delay; a tick further off is looked at again then. */
83
+ const MAX_DELAY = 2 ** 31 - 1;
84
+ export class Cron {
85
+ o;
86
+ store;
87
+ timer;
88
+ ticking = Promise.resolve();
89
+ stopped = false;
90
+ /** Whether this one sends ticks (start): a schedule asked of one that does not is kept for one that does. */
91
+ started = false;
92
+ constructor(o) {
93
+ this.o = o;
94
+ this.store = o.store ?? memorySchedules();
95
+ }
96
+ say(source, line) { this.o.log?.(source, line); }
97
+ /** Take up the kept schedules (a host start): each `every` one ticks once now (late), each `at` one at its time. */
98
+ start() {
99
+ this.started = true;
100
+ const now = this.o.now();
101
+ for (const s of this.store.all())
102
+ if (s.every !== undefined && s.next > now)
103
+ this.store.save({ ...s, next: now });
104
+ this.schedule();
105
+ }
106
+ /**
107
+ * A request from `instance` (whose key is `recipient`), the message
108
+ * `request`: → the answer body (beside `replyTo`). A tick request replaces
109
+ * the schedule of its name; stop ends it.
110
+ */
111
+ request(instance, recipient, request, body) {
112
+ if (this.stopped)
113
+ return { error: "the cron provider is stopping" };
114
+ const r = cronRequestOf(body);
115
+ if (typeof r === "string")
116
+ return { error: r };
117
+ if (r.fn === "stop") {
118
+ const had = this.store.all().some((s) => s.instance === instance && s.name === r.name);
119
+ this.store.drop(instance, r.name);
120
+ this.say(instance, `cron: ${r.name} stopped${had ? "" : " (it was not scheduled)"}`);
121
+ this.schedule();
122
+ return { name: r.name, stopped: had };
123
+ }
124
+ const next = r.spec.at !== undefined ? r.spec.at : this.o.now();
125
+ const s = { instance, recipient, name: r.name, request, next, ...r.spec };
126
+ this.store.save(s);
127
+ this.say(instance, `cron: ${label(s)} scheduled, next ${new Date(next).toISOString()}`);
128
+ this.schedule();
129
+ return { name: r.name, next };
130
+ }
131
+ /** `instance`'s schedules and when each ticks next (the host page, tests). */
132
+ of(instance) {
133
+ return this.store.all().filter((s) => s.instance === instance);
134
+ }
135
+ /** Drop every schedule of an instance that is gone. */
136
+ forget(instance) {
137
+ for (const s of this.of(instance))
138
+ this.store.drop(instance, s.name);
139
+ this.schedule();
140
+ }
141
+ /** Send every tick that is due (one pass at a time); then wait for the next. */
142
+ tick() {
143
+ this.ticking = this.ticking.then(() => this.fire()).catch((e) => this.say("router", `cron: ${e.message}`));
144
+ return this.ticking;
145
+ }
146
+ /** No more ticks; resolves once a pass under way has finished. */
147
+ async stop() {
148
+ this.stopped = true;
149
+ clearTimeout(this.timer);
150
+ this.timer = undefined;
151
+ await this.ticking;
152
+ }
153
+ schedule() {
154
+ clearTimeout(this.timer);
155
+ this.timer = undefined;
156
+ if (this.stopped || !this.started)
157
+ return;
158
+ let next = Infinity;
159
+ for (const s of this.store.all())
160
+ next = Math.min(next, s.next);
161
+ if (next === Infinity)
162
+ return;
163
+ this.timer = setTimeout(() => void this.tick(), Math.min(MAX_DELAY, Math.max(0, next - this.o.now()) + 1));
164
+ }
165
+ async fire() {
166
+ if (this.stopped)
167
+ return;
168
+ for (const s of this.store.all()) {
169
+ if (this.stopped)
170
+ return;
171
+ const now = this.o.now();
172
+ if (s.next > now)
173
+ continue;
174
+ const due = s.next;
175
+ // The next tick first: the grid point after now (one tick however late this one is); an `at` one is done.
176
+ if (s.every !== undefined)
177
+ this.store.save({ ...s, next: due + (Math.floor((now - due) / s.every) + 1) * s.every });
178
+ else
179
+ this.store.drop(s.instance, s.name);
180
+ if (this.o.wake && !(await this.o.wake(s).catch((e) => { this.say(s.instance, `cron: ${label(s)}: the instance could not be woken: ${e.message}`); return false; }))) {
181
+ this.say(s.instance, `cron: ${label(s)} due: stopped, and nothing in it subscribes ${s.box}: not woken`);
182
+ continue;
183
+ }
184
+ try {
185
+ await this.o.tick(s, tickBody(s, due));
186
+ this.say(s.instance, `cron: ${label(s)} → ${s.box}`);
187
+ }
188
+ catch (e) {
189
+ this.say(s.instance, `cron: ${label(s)}: not sent: ${e.message}`);
190
+ if (s.at !== undefined && !this.stopped)
191
+ this.store.save({ ...s, next: now + (this.o.retryMs ?? 60_000) });
192
+ }
193
+ }
194
+ this.schedule();
195
+ }
196
+ }
@@ -0,0 +1,53 @@
1
+ import type { WalletInterface } from "@bsv/sdk";
2
+ import type { CID } from "multiformats/cid";
3
+ import type { HttpRequest, HttpResponse, MailRecord } from "./providers.ts";
4
+ export declare const AUTH_PROTOCOL: [2, string];
5
+ /** The `deliver` event a step records (#157). */
6
+ export interface DeliverEvent {
7
+ kind: "event";
8
+ event: "deliver";
9
+ transport: string;
10
+ url: string;
11
+ message: CID;
12
+ nonce: Uint8Array;
13
+ app?: string;
14
+ }
15
+ /** The answer the instance admits (box `deliver`). */
16
+ export interface DeliveryAnswer {
17
+ kind: "delivery";
18
+ replyTo: CID;
19
+ peer: Uint8Array;
20
+ nonce: string;
21
+ yourNonce: string;
22
+ signature: Uint8Array;
23
+ status: number;
24
+ headers?: Record<string, string>;
25
+ body: Uint8Array;
26
+ [k: string]: unknown;
27
+ }
28
+ /** The payload a request signs (authfetch.zig requestPayload). */
29
+ export declare function requestPayload(rid: Uint8Array, method: string, path: string, query: string, headers: Record<string, string>, body: Uint8Array): Uint8Array;
30
+ /** The answer's signed headers: x-bsv-* but x-bsv-auth-*, and authorization; lower-cased. */
31
+ export declare function signedResponseHeaders(h: Record<string, string>): Record<string, string>;
32
+ /** The payload a response signs (authfetch.zig responsePayload): request id, status, the signed headers sorted, the body. */
33
+ export declare function responsePayload(rid: Uint8Array, status: number, headers: Record<string, string>, body: Uint8Array): Uint8Array;
34
+ export interface DelivererOptions {
35
+ /** One HTTP exchange (the router's: this host's own URLs in process). */
36
+ http(req: HttpRequest, from: string): Promise<HttpResponse>;
37
+ log?(source: string, line: string): void;
38
+ }
39
+ export declare class Deliverer {
40
+ readonly o: DelivererOptions;
41
+ /** BRC-103 sessions, per instance and server base URL, in memory. */
42
+ private sessions;
43
+ constructor(o: DelivererOptions);
44
+ /**
45
+ * Carry one `deliver` event for instance `handle` (its wallet: the instance's key), its mail record
46
+ * and the body's dag-cbor bytes: the answer to admit, or undefined (no answer that stands).
47
+ */
48
+ carry(handle: string, wallet: WalletInterface, eventCid: CID, ev: DeliverEvent, mail: MailRecord, body: Uint8Array): Promise<DeliveryAnswer | undefined>;
49
+ /** BRC-103: our nonce to <base>/.well-known/auth; the answer's signature over both nonces checked. */
50
+ private handshake;
51
+ /** BRC-104: the request signed on the session (request id and nonce = the event's nonce). Undefined: no answer. */
52
+ private send;
53
+ }