@synapse-sdk/client 0.3.3 → 0.3.6

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 CHANGED
@@ -1,7 +1,7 @@
1
- import { type CallId, type ServiceContract } from "@synapse-sdk/core";
1
+ import { SynapseError, type CallId, type ServiceContract } from "@synapse-sdk/core";
2
2
  import type { Node } from "./node.ts";
3
3
  import { type RespFrame, type Session } from "./session.ts";
4
- import type { CallEvent, CallOptions, Run, RunStatus } from "./types.ts";
4
+ import type { CallOptions, Run, RunStatus } from "./types.ts";
5
5
  /** A fresh call id (WP §8.2): 16 random bytes. */
6
6
  export declare function newCallId(): CallId;
7
7
  export declare class RunImpl<U, R> implements Run<U, R> {
@@ -17,12 +17,14 @@ export declare class RunImpl<U, R> implements Run<U, R> {
17
17
  get status(): RunStatus;
18
18
  get result(): Promise<R>;
19
19
  then<A = R, B = never>(onFulfilled?: ((v: R) => A | PromiseLike<A>) | null, onRejected?: ((e: unknown) => B | PromiseLike<B>) | null): Promise<A | B>;
20
+ catch<B = never>(onRejected?: ((e: unknown) => B | PromiseLike<B>) | null): Promise<R | B>;
21
+ finally(onFinally?: (() => void) | null): Promise<R>;
20
22
  /** The session's demux hands frames here; `null` = the session ended with the call pending. */
21
23
  deliver(f: RespFrame | null): void;
22
24
  /** Starts the per-frame reader once the request is written (frames can arrive before `writeCall` returns). */
23
25
  start(): void;
24
- nextUpdate(): Promise<U | undefined>;
25
- updates(): AsyncIterable<U>;
26
+ next(): Promise<U | undefined>;
27
+ [Symbol.asyncIterator](): AsyncIterator<U>;
26
28
  /** Cooperative cancel (WP §8.4): tells the broker, then waits for its `canceled` terminal frame. `connection_lost` without sending once the session ended. */
27
29
  cancel(reason?: string): Promise<void>;
28
30
  /** Dropped with the call in flight: the runtime is told (best effort). */
@@ -33,5 +35,9 @@ export declare class RunImpl<U, R> implements Run<U, R> {
33
35
  * go on the session the attempt is for (`channel.ts`). `connection_lost` once it ended.
34
36
  */
35
37
  export declare function startOn<U, R>(node: Node, session: Session, svc: string, args: unknown, opts: CallOptions<U>, contract?: ServiceContract<unknown, unknown, U, R>): RunImpl<U, R>;
36
- /** `callStream`: the updates and the result as one ordered stream; `break` cancels. */
37
- export declare function callStream<U, R>(run: Run<U, R>): AsyncGenerator<CallEvent<U, R>, void, undefined>;
38
+ /**
39
+ * A call that was never sent (no session): a `Run` already `failed` with `error`. Its result is the
40
+ * error; it has no updates; `cancel()` has nothing to do. Like every Run, an unread result is no
41
+ * unhandled rejection.
42
+ */
43
+ export declare function failedRun<U, R>(error: SynapseError): Run<U, R>;
package/dist/calls.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // a frame on a later session (WP §8.4: ownership is checked, never inferred).
5
5
  import { FLAGS, SynapseError, TYPES, decode as decodeBody, encode as encodeBody, nowUs, toHex, validateSync } from "@synapse-sdk/core";
6
6
  import { outcomeUnknown } from "./session.js";
7
- import { AsyncQueue, deferred } from "./util.js";
7
+ import { AsyncQueue, deferred, rethrowLater } from "./util.js";
8
8
  /** A fresh call id (WP §8.2): 16 random bytes. */
9
9
  export function newCallId() { const b = new Uint8Array(16); crypto.getRandomValues(b); return b; }
10
10
  export class RunImpl {
@@ -42,6 +42,8 @@ export class RunImpl {
42
42
  get status() { return this.#status; }
43
43
  get result() { return this.#result.promise; }
44
44
  then(onFulfilled, onRejected) { return this.#result.promise.then(onFulfilled, onRejected); }
45
+ catch(onRejected) { return this.then(undefined, onRejected); }
46
+ finally(onFinally) { return this.#result.promise.finally(onFinally); }
45
47
  /** The session's demux hands frames here; `null` = the session ended with the call pending. */
46
48
  deliver(f) { this.#frames.push(f); }
47
49
  /** Starts the per-frame reader once the request is written (frames can arrive before `writeCall` returns). */
@@ -93,11 +95,17 @@ export class RunImpl {
93
95
  continue;
94
96
  u = r.value;
95
97
  }
96
- this.#updates.push(u);
97
- try {
98
- this.opts.onUpdate?.(u);
98
+ // The callback form takes the stream (as `subscribe(topic, cb)`): a throwing `onUpdate` is rethrown later and the reading goes on.
99
+ if (this.opts.onUpdate) {
100
+ try {
101
+ this.opts.onUpdate(u);
102
+ }
103
+ catch (e) {
104
+ rethrowLater(e);
105
+ }
99
106
  }
100
- catch { /* the caller's */ }
107
+ else
108
+ this.#updates.push(u);
101
109
  }
102
110
  }
103
111
  #updates = new AsyncQueue(4096);
@@ -129,8 +137,8 @@ export class RunImpl {
129
137
  this.#result.resolve(r.value);
130
138
  }
131
139
  }
132
- async nextUpdate() { return this.#updates.next(); }
133
- updates() { return this.#updates; }
140
+ next() { return this.#updates.next(); }
141
+ [Symbol.asyncIterator]() { return this.#updates[Symbol.asyncIterator](); }
134
142
  #sendCancel(reason) {
135
143
  if (!this.session.isLive)
136
144
  return;
@@ -178,15 +186,24 @@ export function startOn(node, session, svc, args, opts, contract) {
178
186
  run.start();
179
187
  return run;
180
188
  }
181
- /** `callStream`: the updates and the result as one ordered stream; `break` cancels. */
182
- export async function* callStream(run) {
183
- try {
184
- for await (const u of run.updates())
185
- yield { type: "update", data: u };
186
- yield { type: "result", data: await run.result };
187
- }
188
- finally {
189
- if (run.status === "pending" || run.status === "running")
190
- run.cancel("caller").catch(() => { });
191
- }
189
+ /**
190
+ * A call that was never sent (no session): a `Run` already `failed` with `error`. Its result is the
191
+ * error; it has no updates; `cancel()` has nothing to do. Like every Run, an unread result is no
192
+ * unhandled rejection.
193
+ */
194
+ export function failedRun(error) {
195
+ const result = Promise.reject(error);
196
+ result.catch(() => { });
197
+ return {
198
+ id: "",
199
+ status: "failed",
200
+ result,
201
+ then: (onFulfilled, onRejected) => result.then(onFulfilled, onRejected),
202
+ catch: onRejected => result.catch(onRejected),
203
+ finally: onFinally => result.finally(onFinally),
204
+ next: async () => undefined,
205
+ [Symbol.asyncIterator]: async function* () { },
206
+ cancel: async () => { },
207
+ [Symbol.asyncDispose]: async () => { },
208
+ };
192
209
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  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
- export { callStream } from "./calls.ts";
5
4
  export { isTasksRefusal } from "./tasks.ts";
6
5
  export { setMoqDebug } from "./moq_log.ts";
7
6
  export type * from "./types.ts";
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  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
- export { callStream } from "./calls.js";
6
5
  export { isTasksRefusal } from "./tasks.js";
7
6
  export { setMoqDebug } from "./moq_log.js";
8
7
  export { brokerCertHelperUrl, discoverBrokerCertSha256, ensureWebTransport } from "./session.js";
package/dist/node.d.ts CHANGED
@@ -9,14 +9,16 @@ export declare function fixedPolicy(ms: number): ReconnectPolicy;
9
9
  /** Stops after `maxAttempts` attempts in one phase. */
10
10
  export declare function withMaxAttempts(p: ReconnectPolicy, maxAttempts: number): ReconnectPolicy;
11
11
  export declare const defaultReconnectPolicy: ReconnectPolicy;
12
- /** The node's declared target: bumped `generation` per change; `dial` undefined = deliberately disconnected. */
12
+ /** The node's declared target: bumped `generation` per change; `dial` undefined = no target (the node is `idle`). */
13
13
  export interface Target {
14
14
  generation: number;
15
15
  dial: Dial | undefined;
16
16
  identity: Identity;
17
17
  }
18
- /** The one lifecycle cell: `ConnectionState` with the session on `connected`. State, not history. */
18
+ /** The one lifecycle value (held in a `Cell`): `ConnectionState` with the session on `connected`. State, not history. */
19
19
  export type Link = {
20
+ status: "idle";
21
+ } | {
20
22
  status: "connecting";
21
23
  attempt: number;
22
24
  } | {
@@ -36,6 +38,7 @@ export type Link = {
36
38
  } | {
37
39
  status: "failed";
38
40
  error: SynapseError;
41
+ generation: number;
39
42
  };
40
43
  export declare function stateOf(l: Link): ConnectionState;
41
44
  /** What a handle task is told about the node's session, in its own mailbox, in order with its other events. */
@@ -47,16 +50,20 @@ export type SessionEvent = {
47
50
  } | {
48
51
  kind: "nodeEnded";
49
52
  };
50
- /** Where the loop replies to a `connect`/`setTarget` call waiting for a target generation. */
53
+ /** 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). */
54
+ export declare const FIRST_CONNECT_GIVE_UP_MS = 30000;
55
+ /** Where the loop replies to a `connect`/`disconnect` call waiting for a target generation. */
51
56
  type Reply = Deferred<void>;
52
- /** Why the loop is dialing. */
57
+ /**
58
+ * Why the loop is dialing: the first pursuit of a declared target (`connect()`), every attempt (its pause included) cut
59
+ * at `deadlineMs` (`boundMs` after the first attempt), or a dropped session (paced by the policy).
60
+ */
53
61
  type Pursuit = {
54
62
  kind: "first";
55
63
  deadlineMs: number;
64
+ boundMs: number;
56
65
  } | {
57
66
  kind: "reconnect";
58
- } | {
59
- kind: "target";
60
67
  };
61
68
  type Stand = {
62
69
  kind: "live";
@@ -76,30 +83,44 @@ type Next = {
76
83
  delayMs: number;
77
84
  } | {
78
85
  kind: "failed";
79
- } | {
80
- kind: "closed";
81
86
  };
82
- /** The rules (SDK §2, WP §10.4) for the delay before attempt `attempt`. */
87
+ /**
88
+ * The rules (SDK §2, WP §10.4) for the delay before attempt `attempt`. `unauthorized` gives up in every pursuit. The first
89
+ * connect retries only retryable errors and only until its deadline (the loop cuts the pause and the attempt at it; the
90
+ * delay is not cut here); with no policy it uses the default. Giving up is `failed` in every pursuit: the node rests
91
+ * until a new target.
92
+ */
83
93
  export declare function decide(pursuit: Pursuit, policy: ReconnectPolicy | null, err: SynapseError, attempt: number, nowMs: number): Next;
84
94
  export declare class Node {
85
95
  #private;
86
96
  readonly link: Cell<Link>;
87
97
  readonly target: Cell<Target>;
88
- readonly dom: TimeDomain;
98
+ /** The node's time domain; final once `prepared` resolved (the loop dials, and publishers describe themselves, only after). */
99
+ dom: TimeDomain;
100
+ /** Resolves once the time domain is final (the host's monotonic clock read, its domain id derived). */
101
+ readonly prepared: Promise<void>;
89
102
  readonly knobs: SessionKnobs;
90
103
  readonly policy: ReconnectPolicy | null;
104
+ /** Bun/Node: close on SIGINT/SIGTERM once a target is declared (not at construction: constructing opens nothing). */
105
+ readonly signals: boolean;
91
106
  /** Broker notices addressed to this node, across its sessions (fed by the loop from the current session's). */
92
107
  readonly notices: Set<(n: ErrorBody) => void>;
93
108
  readonly projectNotices: Set<(n: Notice) => void>;
94
- /** Resolves once the node has ended for good (`close()`, the first connect failing, the policy giving up). */
109
+ /** Resolves once the node has ended for good: `close()`, and nothing else. */
95
110
  readonly ended: Deferred<void>;
96
111
  epochs: number;
97
112
  reconnects: number;
98
113
  callsSent: number;
99
114
  callsAnswered: number;
100
- constructor(target: Target, dom: TimeDomain, knobs: SessionKnobs, policy: ReconnectPolicy | null);
115
+ /** `finalDom`: the time domain once known, when `dom` is only provisional (see `createNode`). */
116
+ constructor(target: Target, dom: TimeDomain, knobs: SessionKnobs, policy: ReconnectPolicy | null, opts?: {
117
+ signals?: boolean;
118
+ finalDom?: Promise<TimeDomain>;
119
+ });
101
120
  get isEnded(): boolean;
102
121
  get isConnected(): boolean;
122
+ /** 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). */
123
+ failedOn(generation: number): boolean;
103
124
  currentSession(): Session | undefined;
104
125
  state(): ConnectionState;
105
126
  /** The loop's one write. A write that names a session or withdraws one tells every monitoring handle, in the same step. */
@@ -115,13 +136,29 @@ export declare class Node {
115
136
  };
116
137
  /** The node ends for good in `link`: handles are told, feeds end. */
117
138
  end(link: Link): void;
118
- /** Builds the freshly opened session completely (its epoch, its tasks running), then names it. */
119
- attach(session: Session): Session;
139
+ /** Builds the freshly opened session completely (its epoch, the target generation it was opened for, its tasks running), then names it. */
140
+ attach(session: Session, generation: number): Session;
120
141
  /** `close()`: posts the command; resolves once the node has ended. */
121
142
  close(): Promise<void>;
122
- /** `setTarget()`: declares the target under the cell's write, posts the command with its reply, and awaits it. */
123
- setTarget(dial: Dial | undefined): Promise<void>;
124
- /** Starts the loop. `first` is the `connect()` call's reply. `signals`: close on SIGINT/SIGTERM (Bun/Node). */
143
+ /**
144
+ * The one path of every target change (`connect()`, `disconnect()`): declares `dial` (`undefined`: no target) under the
145
+ * `target` `Cell`'s write, posts the command with its reply, and awaits it (the loop answers once the target is reached, given up
146
+ * on, applied as none, or superseded by a newer one: quietly). The target already declared declares nothing (except
147
+ * from `failed` on it, where it is dialed again) and settles with the node's state (`settled`); no target again is done at
148
+ * once. `giveUpAfterMs`: the bound of the dial's first pursuit.
149
+ */
150
+ declare(dial: Dial | undefined, giveUpAfterMs?: number): Promise<void>;
151
+ /**
152
+ * Settles with target `generation` by reading the node's state: resolves once connected on a session opened for it (a
153
+ * session of an earlier target still connected does not count) or once a newer target superseded it (quietly, as the
154
+ * 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`).
155
+ */
156
+ settled(generation: number): Promise<void>;
157
+ /** `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. */
158
+ idle(generation: number): Promise<void>;
159
+ /** 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). */
160
+ switchedFrom(asked: Session): Promise<void>;
161
+ /** Starts the loop (once the time domain is final). `first` is a `connect()` call's reply. `signals`: close on SIGINT/SIGTERM (Bun/Node). */
125
162
  run(stand: Stand, first: Reply | undefined, signals: boolean): void;
126
163
  }
127
164
  /** Resolves the identity a node declares (`resolve_identity`): a missing name is minted, a `web`/`hmi` node without a `user_id` mints one. */
@@ -133,13 +170,14 @@ export declare function resolveIdentity(o: {
133
170
  }): Identity;
134
171
  /** Resolves a `ConnectTarget` against the node's current identity (fields left out are kept). */
135
172
  export declare function resolveTarget(node: Node, t: ConnectTarget): Dial;
173
+ /** A given broker url that is not a URL is the caller's mistake: `invalid_input`. */
174
+ export declare function checkUrl(url: string): void;
136
175
  /**
137
- * Builds the node and starts its loop in the first pursuit. Resolves as soon as the loop runs, with
138
- * the node (so a `close()` can reach it during the first dial) and `connected`: the loop's reply to
139
- * the first connect, a session established or why there is none (`not_connected` after a `close()`).
176
+ * Builds the node, synchronously, with no target (`idle`), and starts its loop resting; nothing is
177
+ * dialed until `connect()` declares a target. Throws `invalid_input` for options that can never work
178
+ * (a bad name). The time domain is read in the background; the loop starts once it is final.
140
179
  */
141
- export declare function startNode(opts: ConnectOptions, giveUpAfterMs: number): Promise<{
142
- node: Node;
143
- connected: Promise<void>;
144
- }>;
180
+ export declare function createNode(opts: ConnectOptions): Node;
181
+ /** 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. */
182
+ export declare function endpointDial(node: Node): Dial;
145
183
  export {};