@synapse-sdk/client 0.3.0 → 0.3.3
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node.js +5 -4
- package/dist/session.d.ts +71 -10
- package/dist/session.js +275 -57
- package/dist/store.d.ts +10 -0
- package/dist/store.js +30 -4
- package/dist/synapse.d.ts +6 -2
- package/dist/synapse.js +18 -6
- package/dist/tasks.d.ts +37 -0
- package/dist/tasks.js +132 -0
- package/dist/topics.d.ts +21 -6
- package/dist/topics.js +42 -21
- package/dist/types.d.ts +153 -12
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "@synapse-sdk/core";
|
|
|
2
2
|
export { Synapse, FIRST_CONNECT_GIVE_UP_MS } from "./synapse.ts";
|
|
3
3
|
export { defaultReconnectPolicy, exponentialPolicy, fixedPolicy, withMaxAttempts } from "./node.ts";
|
|
4
4
|
export { callStream } from "./calls.ts";
|
|
5
|
+
export { isTasksRefusal } from "./tasks.ts";
|
|
5
6
|
export { setMoqDebug } from "./moq_log.ts";
|
|
6
7
|
export type * from "./types.ts";
|
|
7
8
|
export { brokerCertHelperUrl, discoverBrokerCertSha256, ensureWebTransport } from "./session.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from "@synapse-sdk/core";
|
|
|
3
3
|
export { Synapse, FIRST_CONNECT_GIVE_UP_MS } from "./synapse.js";
|
|
4
4
|
export { defaultReconnectPolicy, exponentialPolicy, fixedPolicy, withMaxAttempts } from "./node.js";
|
|
5
5
|
export { callStream } from "./calls.js";
|
|
6
|
+
export { isTasksRefusal } from "./tasks.js";
|
|
6
7
|
export { setMoqDebug } from "./moq_log.js";
|
|
7
8
|
export { brokerCertHelperUrl, discoverBrokerCertSha256, ensureWebTransport } from "./session.js";
|
|
8
9
|
export { replaceEqualDeep, shallowEqual } from "./util.js";
|
package/dist/node.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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
|
|
@@ -164,7 +164,7 @@ export class Node {
|
|
|
164
164
|
function sameDial(a, b) {
|
|
165
165
|
if (!a || !b)
|
|
166
166
|
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;
|
|
167
|
+
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
168
|
}
|
|
169
169
|
const notConnected = () => new SynapseError("not_connected", "node closed");
|
|
170
170
|
/**
|
|
@@ -384,7 +384,7 @@ export function resolveTarget(node, t) {
|
|
|
384
384
|
const cred = t.credential ?? cur.credential;
|
|
385
385
|
if (cred)
|
|
386
386
|
identity.credential = cred;
|
|
387
|
-
return
|
|
387
|
+
return makeDial(t.url, identity, t.brokerCertSha256, t.token);
|
|
388
388
|
}
|
|
389
389
|
/**
|
|
390
390
|
* Builds the node and starts its loop in the first pursuit. Resolves as soon as the loop runs, with
|
|
@@ -392,6 +392,7 @@ export function resolveTarget(node, t) {
|
|
|
392
392
|
* the first connect, a session established or why there is none (`not_connected` after a `close()`).
|
|
393
393
|
*/
|
|
394
394
|
export async function startNode(opts, giveUpAfterMs) {
|
|
395
|
+
const url = urlOrEndpoint(opts.url);
|
|
395
396
|
const identity = resolveIdentity(opts);
|
|
396
397
|
// 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
398
|
if (!opts.domId)
|
|
@@ -399,7 +400,7 @@ export async function startNode(opts, giveUpAfterMs) {
|
|
|
399
400
|
const domId = opts.domId ?? (await defaultDomId()) ?? identity.name;
|
|
400
401
|
const knobs = { transport: opts.transport ?? "auto", connectTimeoutMs: opts.connectTimeoutMs ?? 10_000, maxDatagram: opts.maxDatagram ?? defaultMaxDatagram(), project: opts.project };
|
|
401
402
|
const policy = opts.reconnect === undefined ? defaultReconnectPolicy : opts.reconnect;
|
|
402
|
-
const dial =
|
|
403
|
+
const dial = makeDial(url, identity, opts.brokerCertSha256, opts.token);
|
|
403
404
|
const node = new Node({ generation: 0, dial, identity }, { id: domId }, knobs, policy);
|
|
404
405
|
const first = deferred();
|
|
405
406
|
node.run({ kind: "dialing", pursuit: { kind: "first", deadlineMs: performance.now() + giveUpAfterMs }, attempt: 1, delayMs: 0, reason: "", error: new SynapseError("connection_lost") }, first, opts.signals ?? true);
|
package/dist/session.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Moq from "@moq/net";
|
|
2
|
-
import { ClockSync, SynapseError, type CallId, type ClockInfo, type ErrorBody, type Header, type Hello, type Identity, type ProjectSnapshot, type Role, type ServicesSnapshot, type TimeOffsets, type TopicsSnapshot } from "@synapse-sdk/core";
|
|
2
|
+
import { ClockSync, SynapseError, type CallId, type ClockInfo, type ConnectRequest, type ErrorBody, type Header, type Hello, type Identity, type ProjectSnapshot, type Role, type ServicesSnapshot, type TimeOffsets, type TopicsSnapshot } from "@synapse-sdk/core";
|
|
3
3
|
import { type MoqLogScope } from "./moq_log.ts";
|
|
4
4
|
import { Cell, deferred } from "./util.ts";
|
|
5
5
|
import type { Notice } from "./types.ts";
|
|
@@ -91,20 +91,51 @@ export declare function describeError(e: unknown): string;
|
|
|
91
91
|
export declare function closeError(err: Error | null): SynapseError;
|
|
92
92
|
/** `https://host:port/<root>` → `http://host:port/certificate.sha256`, the broker's plain-HTTP helper. */
|
|
93
93
|
export declare function brokerCertHelperUrl(brokerUrl: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* The runtime's URL when the caller gave none: `SYNAPSE_ENDPOINT` (Bun/Node; the runtime sets it for every task it starts).
|
|
96
|
+
* A browser has no environment: there, as with neither, `invalid_input`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function urlOrEndpoint(url: string | undefined): string;
|
|
94
99
|
/** The certificate pin when the caller gave none (SDK §13): `SYNAPSE_BROKER_CERT_SHA256` (Bun/Node), then the helper, then `undefined` = unpinned. Per attempt. */
|
|
95
100
|
export declare function discoverBrokerCertSha256(brokerUrl: string, opts?: {
|
|
96
101
|
timeoutMs?: number;
|
|
97
102
|
signal?: AbortSignal;
|
|
98
103
|
}): Promise<string | undefined>;
|
|
104
|
+
/**
|
|
105
|
+
* `token_from_url`: `url` without its `jwt=` query parameters, and the last non-empty one (as the relays
|
|
106
|
+
* read it). A URL without `jwt=`, or one that does not parse, is returned as it is (the dial reports it).
|
|
107
|
+
*/
|
|
108
|
+
export declare function tokenFromUrl(url: string): {
|
|
109
|
+
url: string;
|
|
110
|
+
token: string | undefined;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* `Dial::new`: the one place a user's target becomes a dial. A `jwt=` in the URL is the token (the form
|
|
114
|
+
* the relays read), moved out of the URL here so no URL kept or printed afterwards holds it; a `token`
|
|
115
|
+
* given as such wins over one in the URL.
|
|
116
|
+
*/
|
|
117
|
+
export declare function makeDial(url: string, identity: Identity, cert: string | undefined, token: string | undefined): Dial;
|
|
118
|
+
/** `root_runtime_id`: a root names a runtime when it is exactly `runtimes/<runtime id>` (a canonical lowercase UUID). */
|
|
119
|
+
export declare function isRuntimeRoot(root: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* `with_root`: the URL to dial for `url`. `url` itself when it names a path (a runtime's root, or a
|
|
122
|
+
* relay's `runtimes/<runtime_id>`); else `url` with the root the runtime names at its plain-HTTP helper
|
|
123
|
+
* (`http://<host>:<port>/root`, the port of `url`, 443 when it has none), asked at every attempt. A
|
|
124
|
+
* helper that cannot be reached, answers non-2xx or answers incompletely is `connection_lost` (the
|
|
125
|
+
* runtime may be coming back); an answer that is not a root (`runtimes/<runtime id>` exactly; a relay has
|
|
126
|
+
* none: it serves many runtimes) is `invalid_input`, naming the fix. `signal` abandons the ask (its reason).
|
|
127
|
+
*/
|
|
128
|
+
export declare function withRoot(url: string, signal?: AbortSignal): Promise<string>;
|
|
99
129
|
export interface RespFrame {
|
|
100
130
|
header: Header;
|
|
101
131
|
body: Uint8Array;
|
|
102
132
|
}
|
|
103
|
-
/** What one attempt dials (`Dial`). */
|
|
133
|
+
/** What one attempt dials (`Dial`): built by `makeDial` only, so `url` never holds the token. */
|
|
104
134
|
export interface Dial {
|
|
105
135
|
url: string;
|
|
106
136
|
identity: Identity;
|
|
107
137
|
cert: string | undefined;
|
|
138
|
+
token: string | undefined;
|
|
108
139
|
}
|
|
109
140
|
export interface SessionKnobs {
|
|
110
141
|
transport: "auto" | "webtransport" | "websocket";
|
|
@@ -147,7 +178,7 @@ export declare class Session {
|
|
|
147
178
|
onNotice: ((n: BrokerNotice) => void) | undefined;
|
|
148
179
|
/** Settles with the close's error once the connection closed. */
|
|
149
180
|
readonly closed: Promise<Error | null>;
|
|
150
|
-
constructor(url: string, conn: Moq.Connection.Established, pub: Moq.Origin.Producer, sub: Moq.Origin.Producer, hello: Hello, maxDatagram: number, log: MoqLogScope, reqBc: Moq.Broadcast.Producer, res: Moq.Broadcast.Consumer, resSub: Moq.Track.Subscriber, dom: string);
|
|
181
|
+
constructor(url: string, conn: Moq.Connection.Established, pub: Moq.Origin.Producer, sub: Moq.Origin.Producer, hello: Hello, maxDatagram: number, log: MoqLogScope, connect: ConnectTrack, reqBc: Moq.Broadcast.Producer, res: Moq.Broadcast.Consumer, resSub: Moq.Track.Subscriber, dom: string);
|
|
151
182
|
get prefix(): string;
|
|
152
183
|
get role(): Role;
|
|
153
184
|
nowUs(): number;
|
|
@@ -169,18 +200,48 @@ export declare class Session {
|
|
|
169
200
|
start(sync: ClockSync, startCall: (svc: string, args: unknown) => Promise<unknown>): void;
|
|
170
201
|
/** Stops and awaits the tasks (bounded), then closes the session: track finished, MoQ close sent. */
|
|
171
202
|
teardown(graceMs?: number): Promise<void>;
|
|
172
|
-
/** The synchronous end: fail its calls, finish our track, close the connection. Idempotent. */
|
|
203
|
+
/** The synchronous end: withdraw our connect request (the runtime ends the session), fail its calls, finish our track, close the connection. Idempotent. */
|
|
173
204
|
close(): void;
|
|
174
205
|
createBroadcast(path: string): Moq.Broadcast.Producer;
|
|
175
206
|
request(path: string): Moq.Origin.Requesting;
|
|
176
207
|
}
|
|
208
|
+
/** What a connect attempt that got no answer reports (PLAN v0.26 §4 step 1): the request may have gone nowhere. */
|
|
209
|
+
export declare const NO_ANSWER = "no runtime answered under this root (a relay drops out-of-scope publishes silently: check the token)";
|
|
210
|
+
/** A session id for one connect attempt (PLAN v0.26 §5.1): 8 lowercase hex characters, 32 random bits. */
|
|
211
|
+
export declare function mintSid(): string;
|
|
212
|
+
/** Our connect request broadcast `client/<name>/<sid>/connect` and its `hello` track: the session lasts as long as it is announced. */
|
|
213
|
+
export interface ConnectTrack {
|
|
214
|
+
bc: Moq.Broadcast.Producer;
|
|
215
|
+
track: CallWriter;
|
|
216
|
+
}
|
|
217
|
+
/** What the runtime answered a connect request with: the hello, and our `res` track at the path it grants (`hello.res`). */
|
|
218
|
+
export interface Answered {
|
|
219
|
+
connect: ConnectTrack;
|
|
220
|
+
hello: Hello;
|
|
221
|
+
res: Moq.Broadcast.Consumer;
|
|
222
|
+
resSub: Moq.Track.Subscriber;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Asks the runtime for a session over an established connection (PLAN v0.26 §3.1): mint a `sid` for
|
|
226
|
+
* this attempt, write our connect request (`RPC_CALL`, `END`) on track `hello` of
|
|
227
|
+
* `client/<name>/<sid>/connect` and announce it on `pub`, then wait on `sub` for the answer on
|
|
228
|
+
* `client/<name>/<sid>/hello`: the hello (our prefix and role), or an error frame (`END|ERR`, the
|
|
229
|
+
* runtime's refusal, which is the attempt's error as it is: `admission_refused` when a live session
|
|
230
|
+
* holds the name, `invalid_input`, ...). Then our `res` track at the path the hello grants (`hello.res`,
|
|
231
|
+
* `runtime/rpc/@<epoch>`, PLAN v0.26 §7.2). No answer within
|
|
232
|
+
* `timeoutMs` is `not_connected` (`NO_ANSWER`). On any failure the request is withdrawn. `signal`
|
|
233
|
+
* abandons the wait (its reason is the error).
|
|
234
|
+
*/
|
|
235
|
+
export declare function requestSession(pub: Moq.Origin.Producer, sub: Moq.Origin.Producer, request: ConnectRequest, timeoutMs: number, log: MoqLogScope, signal: AbortSignal): Promise<Answered>;
|
|
177
236
|
/**
|
|
178
|
-
* Opens one session to `dial
|
|
179
|
-
* the
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* (`
|
|
237
|
+
* Opens one session to `dial` (PLAN v0.26 §3.1): the path to dial is the URL's own or, for a bare
|
|
238
|
+
* `https://host:port`, the root the runtime names (`withRoot`, asked at every attempt); dial with our name
|
|
239
|
+
* (the relay level's scope) and the token (`jwt=`, through a relay) as the only query parameters; ask the runtime for a session (`requestSession`: our identity and project
|
|
240
|
+
* intent go in the connect request), then announce our `req` track on the path the hello grants (`hello.rpc`).
|
|
241
|
+
* A relay's refusal of the connection shows up as the session closing: an observed close wins, and a
|
|
242
|
+
* transport failure of a step is settled against the close (`CLOSE_GRACE_MS`). `signal` abandons the
|
|
243
|
+
* dial: every wait stops at once and the half-open connection is closed (`canceled`); the connection
|
|
244
|
+
* not established within the connect timeout is `connection_lost`.
|
|
184
245
|
*/
|
|
185
246
|
export declare function openSession(knobs: SessionKnobs, dial: Dial, dom: string, signal?: AbortSignal): Promise<Session>;
|
|
186
247
|
/** The error a call pending on a session that ended reports (WP §8.4: the request was written, so it may have run). */
|
package/dist/session.js
CHANGED
|
@@ -13,7 +13,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
13
13
|
// watchers, the clock probe), which write only into it. A task that loses its track records why and
|
|
14
14
|
// closes the connection, nothing else: the node's loop (`node.ts`) sees the close and does the rest.
|
|
15
15
|
import * as Moq from "@moq/net";
|
|
16
|
-
import { ADMISSION_REFUSED_CLOSE_CODE, ClockSync, FLAGS, STREAM_GROUP_SPAN_MS, STREAM_SUBSCRIBE_MAX_AGE_MS, SynapseError, TYPES, decode as decodeBody, decodeHeader, frame, headerLength, helloPath,
|
|
16
|
+
import { ADMISSION_REFUSED_CLOSE_CODE, ClockSync, FLAGS, STREAM_GROUP_SPAN_MS, STREAM_SUBSCRIBE_MAX_AGE_MS, SynapseError, TYPES, decode as decodeBody, decodeHeader, frame, headerLength, helloPath, SID_LEN, connectPath, encodeConnectRequest, isClientType, isRole, moqPriority, normalizeProjectSnapshot, normalizeServicesSnapshot, normalizeTimeOffsets, normalizeTopicsSnapshot, nowUs, sessionPrefix, toHex, } from "@synapse-sdk/core";
|
|
17
17
|
import { moqLogScope } from "./moq_log.js";
|
|
18
18
|
import { Cell, deferred, withTimeout, TIMEOUT } from "./util.js";
|
|
19
19
|
// ------------------------------------------------------------------------------ moq plumbing
|
|
@@ -334,6 +334,18 @@ export function closeError(err) {
|
|
|
334
334
|
}
|
|
335
335
|
/** `https://host:port/<root>` → `http://host:port/certificate.sha256`, the broker's plain-HTTP helper. */
|
|
336
336
|
export function brokerCertHelperUrl(brokerUrl) { const u = new URL(brokerUrl); return `http://${u.host}/certificate.sha256`; }
|
|
337
|
+
/**
|
|
338
|
+
* The runtime's URL when the caller gave none: `SYNAPSE_ENDPOINT` (Bun/Node; the runtime sets it for every task it starts).
|
|
339
|
+
* A browser has no environment: there, as with neither, `invalid_input`.
|
|
340
|
+
*/
|
|
341
|
+
export function urlOrEndpoint(url) {
|
|
342
|
+
if (url !== undefined)
|
|
343
|
+
return url;
|
|
344
|
+
const env = globalThis.process?.env?.SYNAPSE_ENDPOINT?.trim();
|
|
345
|
+
if (env)
|
|
346
|
+
return env;
|
|
347
|
+
throw new SynapseError("invalid_input", { message: "no url given and SYNAPSE_ENDPOINT is not set", source: "sdk" });
|
|
348
|
+
}
|
|
337
349
|
const SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
338
350
|
/** The certificate pin when the caller gave none (SDK §13): `SYNAPSE_BROKER_CERT_SHA256` (Bun/Node), then the helper, then `undefined` = unpinned. Per attempt. */
|
|
339
351
|
export async function discoverBrokerCertSha256(brokerUrl, opts = {}) {
|
|
@@ -352,6 +364,76 @@ export async function discoverBrokerCertSha256(brokerUrl, opts = {}) {
|
|
|
352
364
|
return undefined;
|
|
353
365
|
}
|
|
354
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* `token_from_url`: `url` without its `jwt=` query parameters, and the last non-empty one (as the relays
|
|
369
|
+
* read it). A URL without `jwt=`, or one that does not parse, is returned as it is (the dial reports it).
|
|
370
|
+
*/
|
|
371
|
+
export function tokenFromUrl(url) {
|
|
372
|
+
let u;
|
|
373
|
+
try {
|
|
374
|
+
u = new URL(url);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return { url, token: undefined };
|
|
378
|
+
}
|
|
379
|
+
if (!u.searchParams.has("jwt"))
|
|
380
|
+
return { url, token: undefined };
|
|
381
|
+
const token = u.searchParams.getAll("jwt").filter(v => v !== "").at(-1);
|
|
382
|
+
u.searchParams.delete("jwt");
|
|
383
|
+
return { url: u.toString(), token };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* `Dial::new`: the one place a user's target becomes a dial. A `jwt=` in the URL is the token (the form
|
|
387
|
+
* the relays read), moved out of the URL here so no URL kept or printed afterwards holds it; a `token`
|
|
388
|
+
* given as such wins over one in the URL.
|
|
389
|
+
*/
|
|
390
|
+
export function makeDial(url, identity, cert, token) {
|
|
391
|
+
const t = tokenFromUrl(url);
|
|
392
|
+
return { url: t.url, identity, cert, token: token ?? t.token };
|
|
393
|
+
}
|
|
394
|
+
/** `root_runtime_id`: a root names a runtime when it is exactly `runtimes/<runtime id>` (a canonical lowercase UUID). */
|
|
395
|
+
export function isRuntimeRoot(root) { return /^runtimes\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(root); }
|
|
396
|
+
/**
|
|
397
|
+
* `with_root`: the URL to dial for `url`. `url` itself when it names a path (a runtime's root, or a
|
|
398
|
+
* relay's `runtimes/<runtime_id>`); else `url` with the root the runtime names at its plain-HTTP helper
|
|
399
|
+
* (`http://<host>:<port>/root`, the port of `url`, 443 when it has none), asked at every attempt. A
|
|
400
|
+
* helper that cannot be reached, answers non-2xx or answers incompletely is `connection_lost` (the
|
|
401
|
+
* runtime may be coming back); an answer that is not a root (`runtimes/<runtime id>` exactly; a relay has
|
|
402
|
+
* none: it serves many runtimes) is `invalid_input`, naming the fix. `signal` abandons the ask (its reason).
|
|
403
|
+
*/
|
|
404
|
+
export async function withRoot(url, signal) {
|
|
405
|
+
let u;
|
|
406
|
+
try {
|
|
407
|
+
u = new URL(url);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
throw new SynapseError("invalid_input", { message: `broker url ${JSON.stringify(url)}: ${describeError(e)}`, source: "sdk" });
|
|
411
|
+
}
|
|
412
|
+
if (u.pathname !== "" && u.pathname !== "/")
|
|
413
|
+
return url;
|
|
414
|
+
const addr = `${u.hostname}:${u.port || (u.protocol === "http:" ? "80" : "443")}`;
|
|
415
|
+
const timeout = AbortSignal.timeout(2000);
|
|
416
|
+
const lost = (why) => new SynapseError("connection_lost", { message: `root helper at ${addr}: ${why}`, source: "transport" });
|
|
417
|
+
let root;
|
|
418
|
+
try {
|
|
419
|
+
const res = await fetch(`http://${addr}/root`, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
|
|
420
|
+
if (!res.ok)
|
|
421
|
+
throw lost(`answered ${res.status} ${res.statusText}`.trim());
|
|
422
|
+
root = (await res.text()).trim();
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
if (signal?.aborted)
|
|
426
|
+
throw signal.reason;
|
|
427
|
+
if (e instanceof SynapseError)
|
|
428
|
+
throw e;
|
|
429
|
+
throw timeout.aborted ? new SynapseError("connection_lost", { message: `root helper at ${addr} did not answer`, source: "transport" }) : lost(describeError(e));
|
|
430
|
+
}
|
|
431
|
+
if (!isRuntimeRoot(root)) {
|
|
432
|
+
throw new SynapseError("invalid_input", { message: `${url} names no runtime (its /root helper returned ${JSON.stringify(root.slice(0, 80))}); give the full path, e.g. https://relay:4443/runtimes/<runtime_id>`, source: "sdk" });
|
|
433
|
+
}
|
|
434
|
+
u.pathname = `/${root}`;
|
|
435
|
+
return u.toString();
|
|
436
|
+
}
|
|
355
437
|
/** The key of a call in the session's pending map. */
|
|
356
438
|
export const callKey = (id) => toHex(id);
|
|
357
439
|
export class Session {
|
|
@@ -385,7 +467,7 @@ export class Session {
|
|
|
385
467
|
#taskDone = [];
|
|
386
468
|
/** Settles with the close's error once the connection closed. */
|
|
387
469
|
closed;
|
|
388
|
-
constructor(url, conn, pub, sub, hello, maxDatagram, log, reqBc, res, resSub, dom) {
|
|
470
|
+
constructor(url, conn, pub, sub, hello, maxDatagram, log, connect, reqBc, res, resSub, dom) {
|
|
389
471
|
this.url = url;
|
|
390
472
|
this.conn = conn;
|
|
391
473
|
this.pub = pub;
|
|
@@ -393,6 +475,7 @@ export class Session {
|
|
|
393
475
|
this.hello = hello;
|
|
394
476
|
this.maxDatagram = maxDatagram;
|
|
395
477
|
this.log = log;
|
|
478
|
+
this.#connect = connect;
|
|
396
479
|
this.#reqBc = reqBc;
|
|
397
480
|
this.#role = hello.role;
|
|
398
481
|
this.transport = conn.transport;
|
|
@@ -403,6 +486,8 @@ export class Session {
|
|
|
403
486
|
this.#resSub = resSub;
|
|
404
487
|
this.#res = res;
|
|
405
488
|
}
|
|
489
|
+
/** Our connect request `client/<name>/<sid>/connect` and its track: the session lasts as long as it is announced (PLAN v0.26 §3.1); a clean close withdraws it first. */
|
|
490
|
+
#connect;
|
|
406
491
|
#reqBc;
|
|
407
492
|
#resSub;
|
|
408
493
|
#res;
|
|
@@ -581,8 +666,9 @@ export class Session {
|
|
|
581
666
|
this.close();
|
|
582
667
|
await withTimeout(this.closed, graceMs);
|
|
583
668
|
}
|
|
584
|
-
/** The synchronous end: fail its calls, finish our track, close the connection. Idempotent. */
|
|
669
|
+
/** The synchronous end: withdraw our connect request (the runtime ends the session), fail its calls, finish our track, close the connection. Idempotent. */
|
|
585
670
|
close() {
|
|
671
|
+
withdraw(this.#connect);
|
|
586
672
|
this.#admitting = false;
|
|
587
673
|
this.#tasks.abort();
|
|
588
674
|
this.log.closing();
|
|
@@ -621,16 +707,145 @@ export class Session {
|
|
|
621
707
|
function certOption(hash) {
|
|
622
708
|
return hash ? { serverCertificateHashes: [{ algorithm: "sha-256", value: hash.trim() }] } : undefined;
|
|
623
709
|
}
|
|
710
|
+
/** What a connect attempt that got no answer reports (PLAN v0.26 §4 step 1): the request may have gone nowhere. */
|
|
711
|
+
export const NO_ANSWER = "no runtime answered under this root (a relay drops out-of-scope publishes silently: check the token)";
|
|
712
|
+
/** A session id for one connect attempt (PLAN v0.26 §5.1): 8 lowercase hex characters, 32 random bits. */
|
|
713
|
+
export function mintSid() { return toHex(crypto.getRandomValues(new Uint8Array(SID_LEN / 2))); }
|
|
714
|
+
/** Withdraws a connect request: the track finished, the broadcast un-announced and closed. Idempotent. */
|
|
715
|
+
function withdraw(c) {
|
|
716
|
+
try {
|
|
717
|
+
c.track.close();
|
|
718
|
+
}
|
|
719
|
+
catch { /* closed */ }
|
|
720
|
+
try {
|
|
721
|
+
c.bc.close();
|
|
722
|
+
}
|
|
723
|
+
catch { /* closed */ }
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Asks the runtime for a session over an established connection (PLAN v0.26 §3.1): mint a `sid` for
|
|
727
|
+
* this attempt, write our connect request (`RPC_CALL`, `END`) on track `hello` of
|
|
728
|
+
* `client/<name>/<sid>/connect` and announce it on `pub`, then wait on `sub` for the answer on
|
|
729
|
+
* `client/<name>/<sid>/hello`: the hello (our prefix and role), or an error frame (`END|ERR`, the
|
|
730
|
+
* runtime's refusal, which is the attempt's error as it is: `admission_refused` when a live session
|
|
731
|
+
* holds the name, `invalid_input`, ...). Then our `res` track at the path the hello grants (`hello.res`,
|
|
732
|
+
* `runtime/rpc/@<epoch>`, PLAN v0.26 §7.2). No answer within
|
|
733
|
+
* `timeoutMs` is `not_connected` (`NO_ANSWER`). On any failure the request is withdrawn. `signal`
|
|
734
|
+
* abandons the wait (its reason is the error).
|
|
735
|
+
*/
|
|
736
|
+
export async function requestSession(pub, sub, request, timeoutMs, log, signal) {
|
|
737
|
+
const { name } = request;
|
|
738
|
+
const sid = mintSid();
|
|
739
|
+
// Our connect request, written before it is announced (the runtime reads it once it sees it).
|
|
740
|
+
const path = connectPath(name, sid);
|
|
741
|
+
const bc = pub.createBroadcast(Moq.Path.from(path));
|
|
742
|
+
const connect = { bc, track: CallWriter.create(bc, "hello") };
|
|
743
|
+
connect.track.write({ flags: FLAGS.END, type: TYPES.RPC_CALL, seq: 0, tUs: nowUs() }, encodeConnectRequest(request));
|
|
744
|
+
bc.announce();
|
|
745
|
+
log.announced(path);
|
|
746
|
+
const noAnswer = () => new SynapseError("not_connected", { message: NO_ANSWER, source: "sdk" });
|
|
747
|
+
const stopped = (e) => { throw signal.aborted ? signal.reason : e; };
|
|
748
|
+
const opened = [];
|
|
749
|
+
try {
|
|
750
|
+
const hp = helloPath(name, sid);
|
|
751
|
+
log.requested(hp);
|
|
752
|
+
// Routed only: wait for the runtime to announce its answer (a blind subscription before it exists is reset as unroutable).
|
|
753
|
+
const helloReq = sub.request(Moq.Path.from(hp), { announced: true });
|
|
754
|
+
opened.push(helloReq);
|
|
755
|
+
const helloBc = await waitActive(helloReq, timeoutMs, signal).catch(() => stopped(noAnswer()));
|
|
756
|
+
const helloSub = helloBc.track("hello").subscribe(latestSubscription(moqPriority(0)));
|
|
757
|
+
opened.push(helloSub);
|
|
758
|
+
const read = async () => {
|
|
759
|
+
const g = await helloSub.recvGroup();
|
|
760
|
+
if (!g)
|
|
761
|
+
throw new SynapseError("connection_lost", "hello track ended");
|
|
762
|
+
const f = await g.readFrame();
|
|
763
|
+
if (!f)
|
|
764
|
+
throw new SynapseError("connection_lost", "empty hello");
|
|
765
|
+
return f.payload;
|
|
766
|
+
};
|
|
767
|
+
const abort = new Promise((_, reject) => { if (signal.aborted)
|
|
768
|
+
reject(signal.reason); signal.addEventListener("abort", () => reject(signal.reason), { once: true }); });
|
|
769
|
+
abort.catch(() => { });
|
|
770
|
+
const reading = read();
|
|
771
|
+
reading.catch(() => { });
|
|
772
|
+
const payload = await Promise.race([withTimeout(reading, timeoutMs), abort]).catch(e => stopped(e instanceof SynapseError ? e : new SynapseError("connection_lost", { message: `hello: ${describeError(e)}`, source: "transport", cause: e })));
|
|
773
|
+
if (payload === TIMEOUT)
|
|
774
|
+
throw noAnswer();
|
|
775
|
+
const hello = readAnswer(payload, name, sid);
|
|
776
|
+
// Our `res` track, at the path the hello grants (the runtime publishes it before it writes the hello).
|
|
777
|
+
const rp = hello.res;
|
|
778
|
+
log.requested(rp);
|
|
779
|
+
const resReq = sub.request(Moq.Path.from(rp), { announced: true });
|
|
780
|
+
opened.push(resReq);
|
|
781
|
+
const res = await waitActive(resReq, timeoutMs, signal).catch(() => stopped(new SynapseError("connection_lost", { message: "broker never announced our response track", source: "transport" })));
|
|
782
|
+
const resSub = res.track("res").subscribe(callTrackSubscription(moqPriority(0)));
|
|
783
|
+
helloSub.close();
|
|
784
|
+
helloReq.close();
|
|
785
|
+
return { connect, hello, res, resSub };
|
|
786
|
+
}
|
|
787
|
+
catch (e) {
|
|
788
|
+
for (const o of opened) {
|
|
789
|
+
try {
|
|
790
|
+
o.close();
|
|
791
|
+
}
|
|
792
|
+
catch { /* closed */ }
|
|
793
|
+
}
|
|
794
|
+
withdraw(connect);
|
|
795
|
+
throw e;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/** The runtime's answer frame: the hello for `client/<name>/<sid>`, or its refusal as the error. */
|
|
799
|
+
function readAnswer(payload, name, sid) {
|
|
800
|
+
let header, hlen, body;
|
|
801
|
+
try {
|
|
802
|
+
({ header, hlen } = decodeHeader(payload));
|
|
803
|
+
body = decodeBody(payload.subarray(hlen));
|
|
804
|
+
}
|
|
805
|
+
catch (e) {
|
|
806
|
+
throw new SynapseError("invalid_input", { message: `the runtime's answer is not one this SDK understands (${describeError(e)})`, source: "sdk" });
|
|
807
|
+
}
|
|
808
|
+
if (header.type !== TYPES.RPC_RESP)
|
|
809
|
+
throw new SynapseError("invalid_input", { message: "hello frame is not a hello", source: "sdk" });
|
|
810
|
+
// The runtime's refusal: its error body is the attempt's error.
|
|
811
|
+
if (header.flags & FLAGS.ERR) {
|
|
812
|
+
const b = body;
|
|
813
|
+
if (!b || typeof b !== "object" || typeof b.code !== "string")
|
|
814
|
+
throw new SynapseError("invalid_input", { message: "the runtime's refusal is not one this SDK understands", source: "sdk" });
|
|
815
|
+
throw SynapseError.fromBody(b, { source: "broker" });
|
|
816
|
+
}
|
|
817
|
+
const h = body;
|
|
818
|
+
// A hello this SDK cannot read is a runtime/SDK version mismatch: said once, not retried.
|
|
819
|
+
if (!h || typeof h !== "object" || h.hello !== true || typeof h.name !== "string" || typeof h.prefix !== "string" || typeof h.rpc !== "string" || typeof h.res !== "string" || !isRole(h.role) || !isClientType(h.type) || typeof h.dom !== "string" || typeof h.broker_version !== "string") {
|
|
820
|
+
throw new SynapseError("invalid_input", { message: "the runtime's hello is not one this SDK understands; runtime and SDK versions differ", source: "sdk" });
|
|
821
|
+
}
|
|
822
|
+
const hello = { ...h, robot: typeof h.robot === "string" ? h.robot : "", runtime_name: typeof h.runtime_name === "string" ? h.runtime_name : "", runtime_id: typeof h.runtime_id === "string" ? h.runtime_id : "" };
|
|
823
|
+
if (hello.prefix !== sessionPrefix(name, sid))
|
|
824
|
+
throw new SynapseError("invalid_input", { message: `hello prefix ${JSON.stringify(hello.prefix)} is not client/${name}/${sid}`, source: "sdk" });
|
|
825
|
+
// The request path the hello grants is under our prefix: `<prefix>/rpc/@<epoch>` (PLAN v0.26 §7.1).
|
|
826
|
+
const granted = `${hello.prefix}/rpc/@`;
|
|
827
|
+
if (!hello.rpc.startsWith(granted) || hello.rpc.length === granted.length)
|
|
828
|
+
throw new SynapseError("invalid_input", { message: `hello rpc ${JSON.stringify(hello.rpc)} is not under ${hello.prefix}/rpc/`, source: "sdk" });
|
|
829
|
+
// The reply path it grants is under `runtime/rpc/` (`runtime/rpc/@<epoch>`, PLAN v0.26 §7.2).
|
|
830
|
+
if (!hello.res.startsWith("runtime/rpc/") || hello.res.length === "runtime/rpc/".length)
|
|
831
|
+
throw new SynapseError("invalid_input", { message: `hello res ${JSON.stringify(hello.res)} is not under runtime/rpc/`, source: "sdk" });
|
|
832
|
+
return hello;
|
|
833
|
+
}
|
|
834
|
+
/** How long a failed pre-hello step waits for the session's close to be observed (Rust `CLOSE_GRACE`). */
|
|
835
|
+
const CLOSE_GRACE_MS = 1000;
|
|
624
836
|
/**
|
|
625
|
-
* Opens one session to `dial
|
|
626
|
-
* the
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
* (`
|
|
837
|
+
* Opens one session to `dial` (PLAN v0.26 §3.1): the path to dial is the URL's own or, for a bare
|
|
838
|
+
* `https://host:port`, the root the runtime names (`withRoot`, asked at every attempt); dial with our name
|
|
839
|
+
* (the relay level's scope) and the token (`jwt=`, through a relay) as the only query parameters; ask the runtime for a session (`requestSession`: our identity and project
|
|
840
|
+
* intent go in the connect request), then announce our `req` track on the path the hello grants (`hello.rpc`).
|
|
841
|
+
* A relay's refusal of the connection shows up as the session closing: an observed close wins, and a
|
|
842
|
+
* transport failure of a step is settled against the close (`CLOSE_GRACE_MS`). `signal` abandons the
|
|
843
|
+
* dial: every wait stops at once and the half-open connection is closed (`canceled`); the connection
|
|
844
|
+
* not established within the connect timeout is `connection_lost`.
|
|
631
845
|
*/
|
|
632
846
|
export async function openSession(knobs, dial, dom, signal) {
|
|
633
|
-
const
|
|
847
|
+
const { identity } = dial;
|
|
848
|
+
const name = identity.name;
|
|
634
849
|
if (!name)
|
|
635
850
|
throw new SynapseError("internal", "identity without a name");
|
|
636
851
|
if (knobs.transport !== "websocket")
|
|
@@ -638,18 +853,21 @@ export async function openSession(knobs, dial, dom, signal) {
|
|
|
638
853
|
const abandoned = () => new SynapseError("canceled", { message: "the dial was abandoned", source: "sdk" });
|
|
639
854
|
if (signal?.aborted)
|
|
640
855
|
throw abandoned();
|
|
856
|
+
// The path to dial: the URL's own, or (none given) the runtime's root, asked of it now (a runtime started again elsewhere on this address may be another runtime).
|
|
857
|
+
const base = await withRoot(dial.url, signal).catch((e) => { throw signal?.aborted ? abandoned() : e; });
|
|
641
858
|
const pub = new Moq.Origin.Producer();
|
|
642
859
|
const sub = new Moq.Origin.Producer();
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
860
|
+
// Only the name and the token travel in the URL: the relay level's scope (`client/<name>/**`) and, through a relay, the token that
|
|
861
|
+
// grants it (`jwt=`, PLAN v0.26 §3.5). Who we are goes to the runtime in the connect request.
|
|
862
|
+
const url = new URL(base);
|
|
863
|
+
url.searchParams.append("name", name);
|
|
864
|
+
if (dial.token !== undefined)
|
|
865
|
+
url.searchParams.append("jwt", dial.token);
|
|
866
|
+
// One controller for the dial: the connect timeout (until the connection is established) and the caller's `signal`.
|
|
646
867
|
const ac = new AbortController();
|
|
647
868
|
const timer = setTimeout(() => ac.abort(new SynapseError("connection_lost", "connect timed out")), knobs.connectTimeoutMs);
|
|
648
869
|
const onAbandon = () => ac.abort(abandoned());
|
|
649
870
|
signal?.addEventListener("abort", onAbandon, { once: true });
|
|
650
|
-
// Settles (rejects) with the reason once the dial is stopped.
|
|
651
|
-
const stopped = new Promise((_, reject) => ac.signal.addEventListener("abort", () => reject(ac.signal.reason), { once: true }));
|
|
652
|
-
stopped.catch(() => { });
|
|
653
871
|
const log = moqLogScope();
|
|
654
872
|
const abandon = () => { signal?.removeEventListener("abort", onAbandon); log.closing(); log.release(); };
|
|
655
873
|
let conn;
|
|
@@ -676,48 +894,30 @@ export async function openSession(knobs, dial, dom, signal) {
|
|
|
676
894
|
throw e;
|
|
677
895
|
throw closeError(e instanceof Error ? e : new Error(describeError(e)));
|
|
678
896
|
}
|
|
897
|
+
clearTimeout(timer);
|
|
898
|
+
const request = { type: identity.type, name, user_id: identity.user_id, credential: identity.credential, project: knobs.project, dom };
|
|
899
|
+
// The handshake's own controller: the caller's `signal`, or the close winning the race below.
|
|
900
|
+
const hs = new AbortController();
|
|
901
|
+
const onStop = () => hs.abort(ac.signal.reason);
|
|
902
|
+
if (ac.signal.aborted)
|
|
903
|
+
onStop();
|
|
904
|
+
else
|
|
905
|
+
ac.signal.addEventListener("abort", onStop, { once: true });
|
|
906
|
+
// A close is the single source of truth for why a pre-hello session ended: an observed close wins the race.
|
|
907
|
+
let closed = false;
|
|
908
|
+
const closedEarly = conn.closed.then((err) => { closed = true; throw closeError(err); });
|
|
909
|
+
closedEarly.catch(() => { });
|
|
910
|
+
let answered;
|
|
679
911
|
try {
|
|
680
|
-
|
|
681
|
-
const closedEarly = Promise.race([conn.closed.then(err => { throw closeError(err); }), stopped]);
|
|
682
|
-
closedEarly.catch(() => { });
|
|
683
|
-
const helloReq = sub.request(Moq.Path.from(helloPath(name)));
|
|
684
|
-
log.requested(helloPath(name));
|
|
685
|
-
const helloBc = await Promise.race([waitActive(helloReq, knobs.connectTimeoutMs, ac.signal), closedEarly]);
|
|
686
|
-
const helloSub = helloBc.track("hello").subscribe(latestSubscription(moqPriority(0)));
|
|
687
|
-
const g = await Promise.race([withTimeout(helloSub.recvGroup(), knobs.connectTimeoutMs), closedEarly]);
|
|
688
|
-
if (g === TIMEOUT || !g)
|
|
689
|
-
throw new SynapseError("connection_lost", "no hello from the broker");
|
|
690
|
-
const first = await Promise.race([withTimeout(g.readFrame(), knobs.connectTimeoutMs), closedEarly]);
|
|
691
|
-
if (first === TIMEOUT || !first)
|
|
692
|
-
throw new SynapseError("connection_lost", "empty hello");
|
|
693
|
-
helloSub.close();
|
|
694
|
-
helloReq.close();
|
|
695
|
-
const { header, hlen } = decodeHeader(first.payload);
|
|
696
|
-
if (header.type !== TYPES.RPC_RESP || header.flags & FLAGS.ERR)
|
|
697
|
-
throw new SynapseError("invalid_input", "hello frame is not a hello");
|
|
698
|
-
const hello = decodeBody(first.payload.subarray(hlen));
|
|
699
|
-
if (!hello || hello.hello !== true || typeof hello.prefix !== "string" || !isRole(hello.role) || !isClientType(hello.type) || typeof hello.dom !== "string")
|
|
700
|
-
throw new SynapseError("invalid_input", "malformed hello");
|
|
701
|
-
const sp = parseSessionPath(rpcPath(hello.prefix));
|
|
702
|
-
if (!sp || sp.name !== name)
|
|
703
|
-
throw new SynapseError("invalid_input", `hello prefix ${hello.prefix} is not client/${name}/<sid>`);
|
|
704
|
-
// The broker's response track for this session.
|
|
705
|
-
const resPath = `runtime/rpc/${sp.sid}`;
|
|
706
|
-
log.requested(resPath);
|
|
707
|
-
const resReq = sub.request(Moq.Path.from(resPath));
|
|
708
|
-
const res = await Promise.race([waitActive(resReq, knobs.connectTimeoutMs, ac.signal), closedEarly]);
|
|
709
|
-
const resSub = res.track("res").subscribe(callTrackSubscription(moqPriority(0)));
|
|
710
|
-
// Our request track (WP §8.2), under the prefix the hello names: the only place we may publish.
|
|
711
|
-
const reqBc = pub.createBroadcast(Moq.Path.from(rpcPath(hello.prefix)));
|
|
712
|
-
log.announced(rpcPath(hello.prefix));
|
|
713
|
-
const session = new Session(dial.url, conn, pub, sub, hello, knobs.maxDatagram, log, reqBc, res, resSub, dom);
|
|
714
|
-
reqBc.announce();
|
|
715
|
-
clearTimeout(timer);
|
|
716
|
-
signal?.removeEventListener("abort", onAbandon);
|
|
717
|
-
return session;
|
|
912
|
+
answered = await Promise.race([closedEarly, requestSession(pub, sub, request, knobs.connectTimeoutMs, log, hs.signal)]);
|
|
718
913
|
}
|
|
719
914
|
catch (e) {
|
|
720
|
-
|
|
915
|
+
hs.abort(e);
|
|
916
|
+
let err = e instanceof SynapseError ? e : closeError(e instanceof Error ? e : new Error(describeError(e)));
|
|
917
|
+
// A step that tripped over a closing connection before its close was observed: the close decides.
|
|
918
|
+
if (!closed && err.code === "connection_lost")
|
|
919
|
+
err = await settlePreHello(err, conn.closed);
|
|
920
|
+
ac.signal.removeEventListener("abort", onStop);
|
|
721
921
|
abandon();
|
|
722
922
|
try {
|
|
723
923
|
conn.close();
|
|
@@ -725,8 +925,26 @@ export async function openSession(knobs, dial, dom, signal) {
|
|
|
725
925
|
catch { /* ignore */ }
|
|
726
926
|
pub.close();
|
|
727
927
|
sub.close();
|
|
728
|
-
throw
|
|
729
|
-
}
|
|
928
|
+
throw err;
|
|
929
|
+
}
|
|
930
|
+
ac.signal.removeEventListener("abort", onStop);
|
|
931
|
+
signal?.removeEventListener("abort", onAbandon);
|
|
932
|
+
// Our request track (WP §8.2), on the path the hello grants (`<prefix>/rpc/@<epoch>`, PLAN v0.26 §7.1): the broadcast and its
|
|
933
|
+
// `req` track created (in the `Session`), then announced, so the runtime finds `req` as soon as it sees the broadcast.
|
|
934
|
+
const { hello, connect, res, resSub } = answered;
|
|
935
|
+
const reqBc = pub.createBroadcast(Moq.Path.from(hello.rpc));
|
|
936
|
+
log.announced(hello.rpc);
|
|
937
|
+
const session = new Session(base, conn, pub, sub, hello, knobs.maxDatagram, log, connect, reqBc, res, resSub, dom);
|
|
938
|
+
reqBc.announce();
|
|
939
|
+
return session;
|
|
940
|
+
}
|
|
941
|
+
/** `settle_pre_hello`: a pre-hello step failed with `err`; if the session closes within the grace that close is the error (a plain transport loss keeps `err`'s text alongside), otherwise `err`. */
|
|
942
|
+
async function settlePreHello(err, closed) {
|
|
943
|
+
const c = await withTimeout(closed, CLOSE_GRACE_MS);
|
|
944
|
+
if (c === TIMEOUT)
|
|
945
|
+
return err;
|
|
946
|
+
const e = closeError(c);
|
|
947
|
+
return e.code === "connection_lost" ? new SynapseError("connection_lost", { message: `${e.message} (${err.message})`, source: e.source, cause: e.cause }) : e;
|
|
730
948
|
}
|
|
731
949
|
/** The error a call pending on a session that ended reports (WP §8.4: the request was written, so it may have run). */
|
|
732
950
|
/** `data` is `{ outcome: "unknown" }`, with `reason` when the session's close had one (Rust `outcome_unknown_data`). */
|
package/dist/store.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ export declare class StoreHandleImpl implements StoreHandle {
|
|
|
134
134
|
subscribe(listener: () => void): () => void;
|
|
135
135
|
/** Waits for the namespace to be in (the first snapshot applied, or absent). */
|
|
136
136
|
join(): Promise<void>;
|
|
137
|
+
/** `join()` under the SDK's word (Publisher and Registration have `ready()` too). */
|
|
138
|
+
ready(): Promise<void>;
|
|
137
139
|
close(): void;
|
|
138
140
|
[Symbol.asyncDispose](): Promise<void>;
|
|
139
141
|
}
|
|
@@ -142,4 +144,12 @@ export declare class StoreHandleImpl implements StoreHandle {
|
|
|
142
144
|
* it (no open written), with other options `invalid_input`; one exiting is waited out.
|
|
143
145
|
*/
|
|
144
146
|
export declare function openStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): Promise<StoreHandleImpl>;
|
|
147
|
+
/**
|
|
148
|
+
* The synchronous form of `openStore`, for a handle the node itself holds (`node.tasks`): a handle on
|
|
149
|
+
* the namespace's task, joined if one runs, else started now; the value is in once `ready()`
|
|
150
|
+
* resolves. A task that is ending (its last handle closed) is waited out before the new one starts:
|
|
151
|
+
* the new one is in the registry at once (a `store()` meanwhile joins it), its requests wait in its
|
|
152
|
+
* mailbox.
|
|
153
|
+
*/
|
|
154
|
+
export declare function attachStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): StoreHandleImpl;
|
|
145
155
|
export {};
|