@synapse-sdk/client 0.3.0 → 0.3.4
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/dist/calls.d.ts +7 -1
- package/dist/calls.js +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node.d.ts +62 -24
- package/dist/node.js +208 -68
- package/dist/scene.d.ts +150 -56
- package/dist/scene.js +995 -163
- package/dist/scene_frames.d.ts +2 -2
- package/dist/scene_frames.js +1 -1
- package/dist/session.d.ts +74 -11
- package/dist/session.js +279 -59
- package/dist/store.d.ts +31 -10
- package/dist/store.js +140 -28
- package/dist/synapse.d.ts +77 -46
- package/dist/synapse.js +193 -114
- package/dist/tasks.d.ts +37 -0
- package/dist/tasks.js +146 -0
- package/dist/topics.d.ts +23 -6
- package/dist/topics.js +59 -22
- package/dist/types.d.ts +405 -93
- package/dist/util.d.ts +43 -0
- package/dist/util.js +97 -1
- package/dist/view.d.ts +5 -3
- package/dist/view.js +21 -10
- package/package.json +2 -2
package/dist/node.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// The node (SDK §2, WP §10): the permanent thing. It owns this node's intents (the handle registries
|
|
2
|
-
// live on the handles' modules), its declared target, and ONE lifecycle
|
|
2
|
+
// live on the handles' modules), its declared target, and ONE lifecycle `Cell` (`Link`) that says where
|
|
3
3
|
// the node stands and, when connected, names the session. It holds no fact the broker told us: those
|
|
4
4
|
// are the current `Session`'s. One loop (`run`) is the only driver: it dials, attaches, waits, tears
|
|
5
|
-
// down, and answers `close()` and `
|
|
6
|
-
// message in their own mailbox (`monitor`), never by watching a
|
|
5
|
+
// down, and answers `close()` and every target change (`declare`) by reply. Handles are told about sessions as a
|
|
6
|
+
// message in their own mailbox (`monitor`), never by watching a `Cell`. Nothing on the wire is
|
|
7
7
|
// resumable: a new session means a fresh `req` track, fresh announces, fresh subscriptions, which
|
|
8
8
|
// the handles replay when told.
|
|
9
9
|
import { ClockSync, SynapseError, calibrateMonotonicClock, defaultDomId, isValidClientName, mintName, mintUserId, nowUs } from "@synapse-sdk/core";
|
|
10
10
|
import { closeOnSignals, forgetOnSignals } from "./signals.js";
|
|
11
|
-
import { Session, closeError, defaultMaxDatagram, discoverBrokerCertSha256, openSession } from "./session.js";
|
|
11
|
+
import { Session, closeError, defaultMaxDatagram, discoverBrokerCertSha256, makeDial, openSession, urlOrEndpoint } from "./session.js";
|
|
12
12
|
import { startOn } from "./calls.js";
|
|
13
13
|
import { AsyncQueue, Cell, deferred } from "./util.js";
|
|
14
14
|
// ------------------------------------------------------------------------------------- policy
|
|
@@ -27,6 +27,7 @@ export function withMaxAttempts(p, maxAttempts) {
|
|
|
27
27
|
export const defaultReconnectPolicy = exponentialPolicy(100, 10_000);
|
|
28
28
|
export function stateOf(l) {
|
|
29
29
|
switch (l.status) {
|
|
30
|
+
case "idle": return { status: "idle" };
|
|
30
31
|
case "connected": return { status: "connected", url: l.session.url, sinceMs: l.session.startedMs };
|
|
31
32
|
case "connecting": return { status: "connecting", attempt: l.attempt };
|
|
32
33
|
case "reconnecting": return { status: "reconnecting", attempt: l.attempt, reason: l.reason, error: l.error, nextRetryAtMs: l.nextRetryAtMs };
|
|
@@ -35,7 +36,14 @@ export function stateOf(l) {
|
|
|
35
36
|
case "failed": return { status: "failed", error: l.error };
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
+
/** How long the first pursuit of a declared target (`connect()`) keeps trying a retryable failure before `failed` (SDK §2: a hard bound; the same value in every SDK). */
|
|
40
|
+
export const FIRST_CONNECT_GIVE_UP_MS = 30_000;
|
|
41
|
+
/**
|
|
42
|
+
* The rules (SDK §2, WP §10.4) for the delay before attempt `attempt`. `unauthorized` gives up in every pursuit. The first
|
|
43
|
+
* connect retries only retryable errors and only until its deadline (the loop cuts the pause and the attempt at it; the
|
|
44
|
+
* delay is not cut here); with no policy it uses the default. Giving up is `failed` in every pursuit: the node rests
|
|
45
|
+
* until a new target.
|
|
46
|
+
*/
|
|
39
47
|
export function decide(pursuit, policy, err, attempt, nowMs) {
|
|
40
48
|
if (err.code === "unauthorized")
|
|
41
49
|
return { kind: "failed" };
|
|
@@ -43,43 +51,56 @@ export function decide(pursuit, policy, err, attempt, nowMs) {
|
|
|
43
51
|
if (pursuit.kind === "first") {
|
|
44
52
|
if (!err.retryable || nowMs >= pursuit.deadlineMs)
|
|
45
53
|
return { kind: "failed" };
|
|
46
|
-
|
|
47
|
-
delay = d === null ? null : Math.min(d, pursuit.deadlineMs - nowMs);
|
|
54
|
+
delay = (policy ?? defaultReconnectPolicy).nextRetryDelayMs({ attempt });
|
|
48
55
|
}
|
|
49
56
|
else
|
|
50
57
|
delay = policy ? policy.nextRetryDelayMs({ attempt }) : null;
|
|
51
58
|
if (delay !== null)
|
|
52
59
|
return { kind: "retry", delayMs: delay };
|
|
53
|
-
return
|
|
60
|
+
return { kind: "failed" };
|
|
61
|
+
}
|
|
62
|
+
/** The first connect's give-up when its bound cut an attempt: `last`, the previous attempt's error (when there was one), is its cause. */
|
|
63
|
+
function gaveUp(boundMs, last) {
|
|
64
|
+
return new SynapseError(last?.code ?? "connection_lost", { message: `the first connect gave up after ${boundMs} ms${last ? ` (last attempt: ${last.message})` : ""}`, ...(last ? { cause: last } : {}) });
|
|
54
65
|
}
|
|
55
66
|
// ---------------------------------------------------------------------------------------- node
|
|
56
67
|
export class Node {
|
|
57
|
-
link = new Cell({ status: "
|
|
68
|
+
link = new Cell({ status: "idle" });
|
|
58
69
|
target;
|
|
70
|
+
/** The node's time domain; final once `prepared` resolved (the loop dials, and publishers describe themselves, only after). */
|
|
59
71
|
dom;
|
|
72
|
+
/** Resolves once the time domain is final (the host's monotonic clock read, its domain id derived). */
|
|
73
|
+
prepared;
|
|
60
74
|
knobs;
|
|
61
75
|
policy;
|
|
76
|
+
/** Bun/Node: close on SIGINT/SIGTERM once a target is declared (not at construction: constructing opens nothing). */
|
|
77
|
+
signals;
|
|
62
78
|
/** Broker notices addressed to this node, across its sessions (fed by the loop from the current session's). */
|
|
63
79
|
notices = new Set();
|
|
64
80
|
projectNotices = new Set();
|
|
65
|
-
/** Resolves once the node has ended for good
|
|
81
|
+
/** Resolves once the node has ended for good: `close()`, and nothing else. */
|
|
66
82
|
ended = deferred();
|
|
67
83
|
#endedFlag = false;
|
|
68
|
-
/** The loop's mailbox: unbounded, so no `close()` or
|
|
84
|
+
/** The loop's mailbox: unbounded, so no `close()` or target change is ever dropped. */
|
|
69
85
|
#commands = new AsyncQueue(Infinity);
|
|
70
86
|
#monitors = new Set();
|
|
71
87
|
epochs = 0;
|
|
72
88
|
reconnects = 0;
|
|
73
89
|
callsSent = 0;
|
|
74
90
|
callsAnswered = 0;
|
|
75
|
-
|
|
91
|
+
/** `finalDom`: the time domain once known, when `dom` is only provisional (see `createNode`). */
|
|
92
|
+
constructor(target, dom, knobs, policy, opts = {}) {
|
|
76
93
|
this.target = new Cell(target);
|
|
77
94
|
this.dom = dom;
|
|
78
95
|
this.knobs = knobs;
|
|
79
96
|
this.policy = policy;
|
|
97
|
+
this.signals = opts.signals ?? false;
|
|
98
|
+
this.prepared = opts.finalDom ? opts.finalDom.then(d => { this.dom = d; }) : Promise.resolve();
|
|
80
99
|
}
|
|
81
100
|
get isEnded() { return this.#endedFlag; }
|
|
82
101
|
get isConnected() { return this.link.get().status === "connected"; }
|
|
102
|
+
/** Whether the node rests `failed` on target `generation` (a `failed` of an older target, not yet replaced by the loop, is not this target's). */
|
|
103
|
+
failedOn(generation) { const l = this.link.get(); return l.status === "failed" && l.generation === generation; }
|
|
83
104
|
currentSession() { const l = this.link.get(); return l.status === "connected" ? l.session : undefined; }
|
|
84
105
|
state() { return stateOf(this.link.get()); }
|
|
85
106
|
/** The loop's one write. A write that names a session or withdraws one tells every monitoring handle, in the same step. */
|
|
@@ -119,9 +140,10 @@ export class Node {
|
|
|
119
140
|
forgetOnSignals(this.#closable);
|
|
120
141
|
}
|
|
121
142
|
#closable = { close: () => this.close() };
|
|
122
|
-
/** Builds the freshly opened session completely (its epoch, its tasks running), then names it. */
|
|
123
|
-
attach(session) {
|
|
143
|
+
/** Builds the freshly opened session completely (its epoch, the target generation it was opened for, its tasks running), then names it. */
|
|
144
|
+
attach(session, generation) {
|
|
124
145
|
session.epoch = ++this.epochs;
|
|
146
|
+
session.generation = generation;
|
|
125
147
|
const sync = new ClockSync(this.dom.id, session.hello.dom, nowUs());
|
|
126
148
|
session.clock.set(sync.info());
|
|
127
149
|
session.onNotice = (n) => {
|
|
@@ -141,30 +163,130 @@ export class Node {
|
|
|
141
163
|
this.#commands.push({ kind: "close" });
|
|
142
164
|
await this.ended.promise;
|
|
143
165
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The one path of every target change (`connect()`, `disconnect()`): declares `dial` (`undefined`: no target) under the
|
|
168
|
+
* `target` `Cell`'s write, posts the command with its reply, and awaits it (the loop answers once the target is reached, given up
|
|
169
|
+
* on, applied as none, or superseded by a newer one: quietly). The target already declared declares nothing (except
|
|
170
|
+
* from `failed` on it, where it is dialed again) and settles with the node's state (`settled`); no target again is done at
|
|
171
|
+
* once. `giveUpAfterMs`: the bound of the dial's first pursuit.
|
|
172
|
+
*/
|
|
173
|
+
declare(dial, giveUpAfterMs = FIRST_CONNECT_GIVE_UP_MS) {
|
|
146
174
|
if (this.#endedFlag)
|
|
147
|
-
return Promise.reject(
|
|
175
|
+
return Promise.reject(notConnected());
|
|
148
176
|
const t = this.target.get();
|
|
149
|
-
if (sameDial(t.dial, dial))
|
|
150
|
-
return
|
|
177
|
+
if (sameDial(t.dial, dial) && !this.failedOn(t.generation))
|
|
178
|
+
return dial ? this.settled(t.generation) : this.idle(t.generation);
|
|
179
|
+
if (dial && this.signals)
|
|
180
|
+
closeOnSignals(this.#closable);
|
|
151
181
|
const next = { generation: t.generation + 1, dial, identity: dial ? dial.identity : t.identity };
|
|
152
182
|
this.target.set(next);
|
|
153
183
|
const reply = deferred();
|
|
154
|
-
this.#commands.push({ kind: "
|
|
184
|
+
this.#commands.push({ kind: "declare", target: next, reply, giveUpAfterMs });
|
|
155
185
|
return reply.promise;
|
|
156
186
|
}
|
|
157
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Settles with target `generation` by reading the node's state: resolves once connected on a session opened for it (a
|
|
189
|
+
* session of an earlier target still connected does not count) or once a newer target superseded it (quietly, as the
|
|
190
|
+
* loop answers a superseded call); rejects once `failed` on it (its error; a `failed` of an older target keeps it waiting) or ended (`not_connected`).
|
|
191
|
+
*/
|
|
192
|
+
settled(generation) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const check = () => {
|
|
195
|
+
const l = this.link.get();
|
|
196
|
+
if ((l.status === "connected" && l.session.generation === generation) || this.target.get().generation !== generation) {
|
|
197
|
+
resolve();
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (l.status === "failed" && l.generation === generation) {
|
|
201
|
+
reject(l.error);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (l.status === "closed") {
|
|
205
|
+
reject(notConnected());
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
};
|
|
210
|
+
if (check())
|
|
211
|
+
return;
|
|
212
|
+
const unLink = this.link.subscribe(() => { if (check()) {
|
|
213
|
+
unLink();
|
|
214
|
+
unTarget();
|
|
215
|
+
} });
|
|
216
|
+
const unTarget = this.target.subscribe(() => { if (check()) {
|
|
217
|
+
unLink();
|
|
218
|
+
unTarget();
|
|
219
|
+
} });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/** `disconnect()` again while the first is still being acted on: resolves once the node rests `idle` (or a newer target superseded it); `not_connected` once it ended. */
|
|
223
|
+
idle(generation) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const check = () => {
|
|
226
|
+
const l = this.link.get();
|
|
227
|
+
if (l.status === "idle" || this.target.get().generation !== generation) {
|
|
228
|
+
resolve();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (l.status === "closed") {
|
|
232
|
+
reject(notConnected());
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
};
|
|
237
|
+
if (check())
|
|
238
|
+
return;
|
|
239
|
+
const unLink = this.link.subscribe(() => { if (check()) {
|
|
240
|
+
unLink();
|
|
241
|
+
unTarget();
|
|
242
|
+
} });
|
|
243
|
+
const unTarget = this.target.subscribe(() => { if (check()) {
|
|
244
|
+
unLink();
|
|
245
|
+
unTarget();
|
|
246
|
+
} });
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** Resolves once a session other than `asked` is connected; rejects with the error once the node rests `failed`, `not_connected` once it ended or rests `idle` (disconnected meanwhile: no session will follow). */
|
|
250
|
+
switchedFrom(asked) {
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
const check = () => {
|
|
253
|
+
const l = this.link.get();
|
|
254
|
+
if (l.status === "connected" && l.session !== asked) {
|
|
255
|
+
resolve();
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (l.status === "failed") {
|
|
259
|
+
reject(l.error);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
if (l.status === "closed") {
|
|
263
|
+
reject(notConnected());
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
// A `disconnect()` during the switch: no session will follow.
|
|
267
|
+
if (l.status === "idle") {
|
|
268
|
+
reject(new SynapseError("not_connected", "disconnected before the promoted session was up"));
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
if (check())
|
|
274
|
+
return;
|
|
275
|
+
const un = this.link.subscribe(() => { if (check())
|
|
276
|
+
un(); });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** Starts the loop (once the time domain is final). `first` is a `connect()` call's reply. `signals`: close on SIGINT/SIGTERM (Bun/Node). */
|
|
158
280
|
run(stand, first, signals) {
|
|
159
281
|
if (signals)
|
|
160
282
|
closeOnSignals(this.#closable);
|
|
161
|
-
void runLoop(this, this.#commands, stand, first);
|
|
283
|
+
void this.prepared.then(() => runLoop(this, this.#commands, stand, first));
|
|
162
284
|
}
|
|
163
285
|
}
|
|
164
286
|
function sameDial(a, b) {
|
|
165
287
|
if (!a || !b)
|
|
166
288
|
return a === b;
|
|
167
|
-
return a.url === b.url && a.cert === b.cert && a.identity.type === b.identity.type && a.identity.name === b.identity.name && a.identity.user_id === b.identity.user_id && a.identity.credential === b.identity.credential;
|
|
289
|
+
return a.url === b.url && a.cert === b.cert && a.token === b.token && a.identity.type === b.identity.type && a.identity.name === b.identity.name && a.identity.user_id === b.identity.user_id && a.identity.credential === b.identity.credential;
|
|
168
290
|
}
|
|
169
291
|
const notConnected = () => new SynapseError("not_connected", "node closed");
|
|
170
292
|
/**
|
|
@@ -174,7 +296,7 @@ const notConnected = () => new SynapseError("not_connected", "node closed");
|
|
|
174
296
|
*/
|
|
175
297
|
async function runLoop(node, commands, stand, first) {
|
|
176
298
|
let target = node.target.get();
|
|
177
|
-
/** The `
|
|
299
|
+
/** The `connect`/`disconnect` calls waiting for the current target. */
|
|
178
300
|
let waiting = first ? [first] : [];
|
|
179
301
|
const replyAll = (r) => { for (const w of waiting)
|
|
180
302
|
r ? w.reject(r) : w.resolve(); waiting = []; };
|
|
@@ -198,7 +320,7 @@ async function runLoop(node, commands, stand, first) {
|
|
|
198
320
|
stand = { kind: "resting" };
|
|
199
321
|
continue;
|
|
200
322
|
}
|
|
201
|
-
const { delayMs } = stand;
|
|
323
|
+
const { delayMs, pursuit } = stand;
|
|
202
324
|
const ac = new AbortController();
|
|
203
325
|
const opened = (async () => {
|
|
204
326
|
if (delayMs > 0)
|
|
@@ -214,9 +336,18 @@ async function runLoop(node, commands, stand, first) {
|
|
|
214
336
|
return { kind: "opened", result: e instanceof SynapseError ? e : new SynapseError("connection_lost", { message: String(e), cause: e }) };
|
|
215
337
|
}
|
|
216
338
|
})();
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
|
|
339
|
+
// The first connect's bound cuts the pause and the attempt at its deadline: its give-up, with the previous
|
|
340
|
+
// attempt's error (attempt 1 has none) as the cause.
|
|
341
|
+
let cutTimer;
|
|
342
|
+
const racers = [nextCommand(), opened];
|
|
343
|
+
if (pursuit.kind === "first") {
|
|
344
|
+
const cut = { kind: "cut", error: gaveUp(pursuit.boundMs, stand.attempt > 1 ? stand.error : undefined) };
|
|
345
|
+
racers.push(new Promise(r => { cutTimer = setTimeout(() => r(cut), Math.max(0, pursuit.deadlineMs - performance.now())); }));
|
|
346
|
+
}
|
|
347
|
+
wake = await Promise.race(racers);
|
|
348
|
+
clearTimeout(cutTimer);
|
|
349
|
+
// The dial is abandoned (a command, or the bound): its pause ends now; a session it still opens is closed when it arrives.
|
|
350
|
+
if (wake.kind === "command" || wake.kind === "cut") {
|
|
220
351
|
ac.abort();
|
|
221
352
|
void opened.then(w => { if (w.kind === "opened" && w.result instanceof Session)
|
|
222
353
|
w.result.close(); });
|
|
@@ -243,7 +374,7 @@ async function runLoop(node, commands, stand, first) {
|
|
|
243
374
|
const close = queued.some(c => c.kind === "close");
|
|
244
375
|
let newest;
|
|
245
376
|
for (const c of queued) {
|
|
246
|
-
if (c.kind !== "
|
|
377
|
+
if (c.kind !== "declare")
|
|
247
378
|
continue;
|
|
248
379
|
if (close) {
|
|
249
380
|
waiting.push(c.reply);
|
|
@@ -252,7 +383,7 @@ async function runLoop(node, commands, stand, first) {
|
|
|
252
383
|
if (!newest || c.target.generation > newest.target.generation) {
|
|
253
384
|
if (newest)
|
|
254
385
|
newest.reply.resolve();
|
|
255
|
-
newest = { target: c.target, reply: c.reply };
|
|
386
|
+
newest = { target: c.target, reply: c.reply, giveUpAfterMs: c.giveUpAfterMs };
|
|
256
387
|
}
|
|
257
388
|
else
|
|
258
389
|
c.reply.resolve(); // superseded: quietly done
|
|
@@ -272,11 +403,13 @@ async function runLoop(node, commands, stand, first) {
|
|
|
272
403
|
target = newest.target;
|
|
273
404
|
attempts = 0;
|
|
274
405
|
if (target.dial) {
|
|
406
|
+
// Every dial is first pursued within its bound.
|
|
407
|
+
const pursuit = { kind: "first", deadlineMs: performance.now() + newest.giveUpAfterMs, boundMs: newest.giveUpAfterMs };
|
|
275
408
|
node.setLink({ status: "connecting", attempt: 1 });
|
|
276
|
-
stand = { kind: "dialing", pursuit
|
|
409
|
+
stand = { kind: "dialing", pursuit, attempt: 1, delayMs: 0, reason: "client", error: new SynapseError("connection_lost", "target changed") };
|
|
277
410
|
}
|
|
278
411
|
else {
|
|
279
|
-
node.setLink({ status: "
|
|
412
|
+
node.setLink({ status: "idle" });
|
|
280
413
|
replyAll(undefined);
|
|
281
414
|
stand = { kind: "resting" };
|
|
282
415
|
}
|
|
@@ -302,18 +435,24 @@ async function runLoop(node, commands, stand, first) {
|
|
|
302
435
|
stand = { kind: "dialing", pursuit: { kind: "reconnect" }, attempt: attempts, delayMs: delay, reason, error: err };
|
|
303
436
|
}
|
|
304
437
|
else {
|
|
305
|
-
|
|
306
|
-
node
|
|
307
|
-
|
|
308
|
-
return;
|
|
438
|
+
// The policy declined: the node rests `failed` with the drop's error until a new target (or `close()`).
|
|
439
|
+
await teardown(node, s, { status: "failed", error: err, generation: target.generation });
|
|
440
|
+
stand = { kind: "resting" };
|
|
309
441
|
}
|
|
310
442
|
continue;
|
|
311
443
|
}
|
|
312
|
-
// opened
|
|
313
444
|
if (stand.kind !== "dialing")
|
|
314
445
|
continue;
|
|
446
|
+
// the first connect's bound cut the attempt: given up
|
|
447
|
+
if (wake.kind === "cut") {
|
|
448
|
+
node.setLink({ status: "failed", error: wake.error, generation: target.generation });
|
|
449
|
+
replyAll(wake.error);
|
|
450
|
+
stand = { kind: "resting" };
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
// opened
|
|
315
454
|
if (wake.result instanceof Session) {
|
|
316
|
-
node.attach(wake.result);
|
|
455
|
+
node.attach(wake.result, target.generation);
|
|
317
456
|
replyAll(undefined); // the target is reached
|
|
318
457
|
stand = { kind: "live", session: wake.result };
|
|
319
458
|
continue;
|
|
@@ -329,21 +468,12 @@ async function runLoop(node, commands, stand, first) {
|
|
|
329
468
|
stand.error = e;
|
|
330
469
|
node.setLink(pursuit.kind === "first" ? { status: "connecting", attempt: stand.attempt } : { status: "reconnecting", attempt: stand.attempt, reason: stand.reason, error: e, nextRetryAtMs: performance.now() + next.delayMs });
|
|
331
470
|
}
|
|
332
|
-
else
|
|
333
|
-
|
|
471
|
+
else {
|
|
472
|
+
// Given up (any pursuit): `failed` is a resting state; a later `connect()` dials again.
|
|
473
|
+
node.setLink({ status: "failed", error: e, generation: target.generation });
|
|
334
474
|
replyAll(e);
|
|
335
475
|
stand = { kind: "resting" };
|
|
336
476
|
}
|
|
337
|
-
else if (next.kind === "failed") {
|
|
338
|
-
node.end({ status: "failed", error: e });
|
|
339
|
-
replyAll(e);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
node.end({ status: "closed", reason: stand.reason });
|
|
344
|
-
replyAll(notConnected());
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
477
|
}
|
|
348
478
|
}
|
|
349
479
|
/** Ends session `s`: the node stops naming it (handles are told, the status is true from that instant), then the session is torn down (calls failed, tasks awaited, close sent). */
|
|
@@ -367,12 +497,7 @@ export function resolveIdentity(o) {
|
|
|
367
497
|
}
|
|
368
498
|
/** Resolves a `ConnectTarget` against the node's current identity (fields left out are kept). */
|
|
369
499
|
export function resolveTarget(node, t) {
|
|
370
|
-
|
|
371
|
-
new URL(t.url);
|
|
372
|
-
}
|
|
373
|
-
catch (e) {
|
|
374
|
-
throw new SynapseError("invalid_input", { message: `broker url ${JSON.stringify(t.url)}: ${e instanceof Error ? e.message : String(e)}` });
|
|
375
|
-
}
|
|
500
|
+
checkUrl(t.url);
|
|
376
501
|
const cur = node.target.get().identity;
|
|
377
502
|
if (t.name !== undefined && !isValidClientName(t.name))
|
|
378
503
|
throw new SynapseError("invalid_input", `name ${JSON.stringify(t.name)} is not [a-z0-9_-]{1,64}`);
|
|
@@ -384,24 +509,39 @@ export function resolveTarget(node, t) {
|
|
|
384
509
|
const cred = t.credential ?? cur.credential;
|
|
385
510
|
if (cred)
|
|
386
511
|
identity.credential = cred;
|
|
387
|
-
return
|
|
512
|
+
return makeDial(t.url, identity, t.brokerCertSha256, t.token);
|
|
513
|
+
}
|
|
514
|
+
/** A given broker url that is not a URL is the caller's mistake: `invalid_input`. */
|
|
515
|
+
export function checkUrl(url) {
|
|
516
|
+
try {
|
|
517
|
+
new URL(url);
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
throw new SynapseError("invalid_input", { message: `broker url ${JSON.stringify(url)}: ${e instanceof Error ? e.message : String(e)}` });
|
|
521
|
+
}
|
|
388
522
|
}
|
|
389
523
|
/**
|
|
390
|
-
* Builds the node
|
|
391
|
-
*
|
|
392
|
-
*
|
|
524
|
+
* Builds the node, synchronously, with no target (`idle`), and starts its loop resting; nothing is
|
|
525
|
+
* dialed until `connect()` declares a target. Throws `invalid_input` for options that can never work
|
|
526
|
+
* (a bad name). The time domain is read in the background; the loop starts once it is final.
|
|
393
527
|
*/
|
|
394
|
-
export
|
|
528
|
+
export function createNode(opts) {
|
|
395
529
|
const identity = resolveIdentity(opts);
|
|
396
530
|
// In the host's domain (Bun/Node) `nowUs()` must be the kernel's monotonic clock, like every other process on the host (WP §7.3).
|
|
397
|
-
|
|
531
|
+
const provisional = { id: opts.domId ?? identity.name };
|
|
532
|
+
const finalDom = opts.domId ? undefined : (async () => {
|
|
398
533
|
await calibrateMonotonicClock();
|
|
399
|
-
|
|
534
|
+
return { id: (await defaultDomId()) ?? identity.name };
|
|
535
|
+
})();
|
|
400
536
|
const knobs = { transport: opts.transport ?? "auto", connectTimeoutMs: opts.connectTimeoutMs ?? 10_000, maxDatagram: opts.maxDatagram ?? defaultMaxDatagram(), project: opts.project };
|
|
401
537
|
const policy = opts.reconnect === undefined ? defaultReconnectPolicy : opts.reconnect;
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
538
|
+
const node = new Node({ generation: 0, dial: undefined, identity }, provisional, knobs, policy, { signals: opts.signals ?? true, finalDom });
|
|
539
|
+
node.run({ kind: "resting" }, undefined, false);
|
|
540
|
+
return node;
|
|
541
|
+
}
|
|
542
|
+
/** The dial `connect()` with no target declares when none is declared: `SYNAPSE_ENDPOINT` (Bun/Node; unset, or a browser: `invalid_input`) with the node's current identity. */
|
|
543
|
+
export function endpointDial(node) {
|
|
544
|
+
const url = urlOrEndpoint(undefined);
|
|
545
|
+
checkUrl(url);
|
|
546
|
+
return makeDial(url, node.target.get().identity, undefined, undefined);
|
|
407
547
|
}
|