@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/store.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { Node } from "./node.ts";
3
3
  import { type Session } from "./session.ts";
4
4
  import type { Warn } from "./topics.ts";
5
5
  import type { StoreChange, StoreHandle, StoreOptions, StoreStatus } from "./types.ts";
6
- import { AsyncQueue, type Deferred } from "./util.ts";
6
+ import { AsyncQueue, type Deferred, type StateValue } from "./util.ts";
7
7
  export declare const ENDED_DESTROYED = "destroyed";
8
8
  export declare const ENDED_CONNECTION_LOST = "connection_lost";
9
9
  export declare const ENDED_CLIENT = "client";
@@ -78,11 +78,19 @@ type Event = {
78
78
  epoch: string;
79
79
  version: number;
80
80
  reply: Deferred<void>;
81
- } | {
81
+ }
82
+ /** A write of handle `handle` (its parked writes are refused when it leaves). */
83
+ | {
82
84
  kind: "write";
85
+ handle: number;
83
86
  op: string;
84
87
  args: WriteOp;
85
88
  reply: Deferred<unknown>;
89
+ }
90
+ /** Handle `handle` was closed, others remain: its parked writes are refused. */
91
+ | {
92
+ kind: "leave";
93
+ handle: number;
86
94
  } | {
87
95
  kind: "writeDone";
88
96
  op: string;
@@ -107,6 +115,7 @@ export declare class Namespace {
107
115
  readonly mailbox: AsyncQueue<Event>;
108
116
  readonly exited: Deferred<void>;
109
117
  handles: number;
118
+ nextHandle: number;
110
119
  constructor(name: string, opts: StoreOptions, node: Node, warn: Warn);
111
120
  post(e: Event): boolean;
112
121
  request<T>(make: (reply: Deferred<T>) => Event): Promise<T>;
@@ -116,30 +125,42 @@ export declare function startNamespace(ns: Namespace, registry: Map<string, Name
116
125
  export declare class StoreHandleImpl implements StoreHandle {
117
126
  #private;
118
127
  private readonly ns;
128
+ /** The handle's own state, a state value: `get()` (the same object while unchanged), `onChange`, `changed()`, `watch()`. */
129
+ readonly status: StateValue<StoreStatus>;
119
130
  constructor(ns: Namespace);
120
131
  get name(): string;
121
- /** The whole value, what the runtime has; the same object while nothing changed (reference-stable for React). */
122
- get value(): Record<string, unknown>;
132
+ /** `get()`: the whole value, what the runtime has; the same object while nothing changed (reference-stable for React). `get(key)`: that key's value, `undefined` when absent. */
133
+ get(): Record<string, unknown>;
123
134
  get(key: string): unknown;
124
135
  get synced(): boolean;
125
- get status(): StoreStatus;
126
- /** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `value` shows it. */
136
+ /** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `get()` shows it. */
127
137
  set(keyOrValues: string | Record<string, unknown>, value?: unknown): Promise<void>;
128
138
  delete(key: string): Promise<void>;
129
139
  clear(): Promise<void>;
130
140
  destroy(): Promise<void>;
131
141
  /** `for await (const value of store.watch())`: the whole value after each change; `store.watch(key)`: that key's value each time it changes. Ends with the handle; leaving the loop removes the feed. */
132
142
  watch(key?: string): AsyncGenerator<unknown, void, undefined>;
133
- /** The framework bridge (React's `useStore`): called on every change of the value or the status. */
134
- subscribe(listener: () => void): () => void;
143
+ /** The whole value after its next change (`store_gone` once this handle is closed). */
144
+ changed(): Promise<Record<string, unknown>>;
145
+ /** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
146
+ onChange(cb: (value: Record<string, unknown>) => void): () => void;
135
147
  /** Waits for the namespace to be in (the first snapshot applied, or absent). */
136
- join(): Promise<void>;
148
+ ready(): Promise<void>;
137
149
  close(): void;
138
150
  [Symbol.asyncDispose](): Promise<void>;
139
151
  }
140
152
  /**
141
153
  * `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
142
- * it (no open written), with other options `invalid_input`; one exiting is waited out.
154
+ * it (no open written), with other options `invalid_input`; one exiting is waited out. Without a session
155
+ * (before the first, between two) the task waits for the next one, like after a drop; it resolves once in.
143
156
  */
144
157
  export declare function openStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): Promise<StoreHandleImpl>;
158
+ /**
159
+ * The synchronous form of `openStore`, for a handle the node itself holds (`node.tasks`): a handle on
160
+ * the namespace's task, joined if one runs, else started now; the value is in once `ready()`
161
+ * resolves. A task that is ending (its last handle closed) is waited out before the new one starts:
162
+ * the new one is in the registry at once (a `store()` meanwhile joins it), its requests wait in its
163
+ * mailbox.
164
+ */
165
+ export declare function attachStore(node: Node, registry: Map<string, Namespace>, name: string, opts: StoreOptions, warn: Warn): StoreHandleImpl;
145
166
  export {};
package/dist/store.js CHANGED
@@ -6,13 +6,14 @@
6
6
  // here. A refusal is asked again, paced, with a warning (R6); a transient reply paced, silently; an
7
7
  // incarnation that ends (writer gone, project switch, the owner restarted) replays the intent;
8
8
  // `destroyed` ends the handle. Writes name the epoch the mirror holds; the runtime checks ownership
9
- // (`not_owner`). Between incarnations (a session change, a replay) nothing is known: `store_gone`.
9
+ // (`not_owner`). Between incarnations (a session change, a replay) nothing is known: a write then is
10
+ // parked by the task and sent once the handle binds again (`store_gone` if it ends first).
10
11
  import * as Moq from "@moq/net";
11
- import { FLAGS, STORE_CONFIG, STORE_TRACK, SynapseError, decode as decodeBody, decodeHeader, moqPriority, storeBasePath, } from "@synapse-sdk/core";
12
+ import { FLAGS, STORE_TRACK, SynapseError, decode as decodeBody, decodeHeader, moqPriority, storeBasePath, storeRobotScoped, } from "@synapse-sdk/core";
12
13
  import { startOn } from "./calls.js";
13
14
  import { REFOLLOW_PACE_MS, holderOf, isTransient, refusalPaceMs } from "./channel.js";
14
15
  import { latestSubscription, waitActive } from "./session.js";
15
- import { AsyncQueue, TIMEOUT, deferred, withTimeout } from "./util.js";
16
+ import { AsyncQueue, StatusCell, TIMEOUT, deferred, withTimeout } from "./util.js";
16
17
  export const ENDED_DESTROYED = "destroyed";
17
18
  export const ENDED_CONNECTION_LOST = "connection_lost";
18
19
  export const ENDED_CLIENT = "client";
@@ -206,6 +207,7 @@ export class Namespace {
206
207
  mailbox = new AsyncQueue(Infinity);
207
208
  exited = deferred();
208
209
  handles = 0;
210
+ nextHandle = 0;
209
211
  constructor(name, opts, node, warn) {
210
212
  this.name = name;
211
213
  this.opts = opts;
@@ -224,7 +226,8 @@ export function startNamespace(ns, registry) { void run(ns, registry); }
224
226
  async function run(ns, registry) {
225
227
  const { name, opts, state, warn, node } = ns;
226
228
  const post = (e) => { ns.post(e); };
227
- const config = name === STORE_CONFIG;
229
+ /** A robot-scoped namespace belongs to the robot: a project switch leaves it as it is. */
230
+ const robotScoped = storeRobotScoped(name);
228
231
  let session;
229
232
  let phase = { kind: "idle" };
230
233
  const ph = () => phase;
@@ -238,6 +241,9 @@ async function run(ns, registry) {
238
241
  let unwatchProject;
239
242
  const joining = [];
240
243
  let versionWaiters = [];
244
+ /** Writes that came while no incarnation was bound (a replay, or no session yet), in arrival order: sent once the handle binds again, refused `store_gone` if it ends first. Each counts in `pending`. */
245
+ let parked = [];
246
+ const ABSENT = "the namespace is absent: open it with an access mode to create it";
241
247
  const leaveIncarnation = () => { for (const w of versionWaiters.splice(0))
242
248
  w.reply.reject(gone(name, `the handle left the incarnation before version ${w.version}`)); };
243
249
  const stopPhase = () => {
@@ -310,7 +316,7 @@ async function run(ns, registry) {
310
316
  state.reset();
311
317
  unwatchProject?.();
312
318
  unwatchProject = undefined;
313
- if (s && !config) {
319
+ if (s && !robotScoped) {
314
320
  const feed = s.project;
315
321
  const current = feed.get();
316
322
  if (known === undefined)
@@ -357,9 +363,12 @@ async function run(ns, registry) {
357
363
  const done = follow(ns, s, announced, access?.epoch, stop.signal);
358
364
  phase = { kind: "following", attempt, stop, done };
359
365
  void done.then(reason => post({ kind: "followEnded", attempt, reason }));
360
- if (!access)
366
+ // Absent: honestly in (nothing to load), and nothing to write to (the parked writes are refused).
367
+ if (!access) {
361
368
  for (const j of joining.splice(0))
362
369
  j.resolve();
370
+ refuseParked(ABSENT);
371
+ }
363
372
  return;
364
373
  }
365
374
  announced.close();
@@ -388,6 +397,9 @@ async function run(ns, registry) {
388
397
  if (state.loaded && state.epoch === epoch && state.version >= wanted)
389
398
  for (const j of joining.splice(0))
390
399
  j.resolve();
400
+ // The handle is bound again (the reader applied the snapshot that names the epoch): the writes parked meanwhile go out, in order, naming it.
401
+ if (state.ended === undefined && state.epoch !== undefined)
402
+ sendParked();
391
403
  };
392
404
  const join = (reply) => {
393
405
  if (phase.kind === "waitingProject") {
@@ -425,14 +437,53 @@ async function run(ns, registry) {
425
437
  again(REFOLLOW_PACE_MS);
426
438
  }
427
439
  };
428
- const write = (op, args, reply) => {
440
+ /**
441
+ * A write. Ended or absent: `store_gone`. Between incarnations (a replay, or no session yet) it is parked: a write is
442
+ * intent, sent once the handle binds again (the runtime decides it then), refused `store_gone` if the handle ends first;
443
+ * it has no deadline of its own. Bound: sent after any parked before it, naming the mirror's epoch (and claim).
444
+ */
445
+ const write = (handle, op, args, reply) => {
429
446
  if (state.ended !== undefined) {
430
447
  reply.reject(gone(name, `the handle ended (${state.ended})`));
431
448
  return;
432
449
  }
450
+ if (state.epoch === undefined) {
451
+ if (state.absent) {
452
+ reply.reject(gone(name, ABSENT));
453
+ return;
454
+ }
455
+ parked.push({ handle, op, args, reply });
456
+ state.setPending(state.pending + 1);
457
+ return;
458
+ }
459
+ sendParked();
460
+ send(op, args, reply);
461
+ };
462
+ /** The parked writes go out, in arrival order (the handle is bound). */
463
+ const sendParked = () => {
464
+ if (!parked.length)
465
+ return;
466
+ const ws = parked;
467
+ parked = [];
468
+ state.setPending(state.pending - ws.length);
469
+ for (const w of ws)
470
+ send(w.op, w.args, w.reply);
471
+ };
472
+ /** The parked writes are refused `store_gone` (`why`): all of them (the handle ended, or the namespace is absent), or those of handle `of` (that handle was closed, others stay open). */
473
+ const refuseParked = (why, of) => {
474
+ const ws = parked.filter(w => of === undefined || w.handle === of);
475
+ if (!ws.length)
476
+ return;
477
+ parked = parked.filter(w => !ws.includes(w));
478
+ state.setPending(state.pending - ws.length);
479
+ for (const w of ws)
480
+ w.reply.reject(gone(name, why));
481
+ };
482
+ /** One write on the wire, naming the bound epoch (and claim); answered by the broker (`writeDone`). */
483
+ const send = (op, args, reply) => {
433
484
  const epoch = state.epoch;
434
485
  if (epoch === undefined) {
435
- reply.reject(state.absent ? gone(name, "the namespace is absent: open it with an access mode to create it") : gone(name, "no incarnation is bound: the intent is being replayed"));
486
+ reply.reject(gone(name, "no incarnation is bound"));
436
487
  return;
437
488
  }
438
489
  if (!session) {
@@ -533,7 +584,10 @@ async function run(ns, registry) {
533
584
  waitVersion(ev.epoch, ev.version, ev.reply);
534
585
  break;
535
586
  case "write":
536
- write(ev.op, ev.args, ev.reply);
587
+ write(ev.handle, ev.op, ev.args, ev.reply);
588
+ break;
589
+ case "leave":
590
+ refuseParked(`the handle ended (${ENDED_CLIENT})`, ev.handle);
537
591
  break;
538
592
  case "writeDone":
539
593
  if (writeDone(ev.op, ev.result, ev.reply)) {
@@ -542,7 +596,7 @@ async function run(ns, registry) {
542
596
  }
543
597
  break;
544
598
  case "projectChanged":
545
- if (!config)
599
+ if (!robotScoped)
546
600
  projectChanged(ev.now);
547
601
  break;
548
602
  case "paced":
@@ -566,6 +620,7 @@ async function run(ns, registry) {
566
620
  for (const j of joining.splice(0))
567
621
  j.reject(e);
568
622
  leaveIncarnation();
623
+ refuseParked(`the handle ended (${state.ended ?? reason ?? ENDED_CLIENT})`);
569
624
  {
570
625
  const p = ph();
571
626
  if (p.kind === "following" || p.kind === "paced")
@@ -579,7 +634,7 @@ async function run(ns, registry) {
579
634
  if (ev.kind === "writeDone")
580
635
  writeDone(ev.op, ev.result, ev.reply);
581
636
  else if (ev.kind === "write")
582
- write(ev.op, ev.args, ev.reply);
637
+ write(ev.handle, ev.op, ev.args, ev.reply);
583
638
  else if (ev.kind === "opened") {
584
639
  const p = ph();
585
640
  if (p.kind === "opening" && p.o.attempt === ev.attempt) {
@@ -770,31 +825,39 @@ async function follow(ns, session, announced, want, signal) {
770
825
  export class StoreHandleImpl {
771
826
  ns;
772
827
  #closed = false;
828
+ /** This handle's id in its namespace: its parked writes are refused when it closes. */
829
+ #id;
773
830
  /** Resolved when this handle closes: its version waits end. */
774
831
  #left = deferred();
775
832
  /** This handle's change feeds: closed with it. */
776
833
  #feeds = new Set();
777
834
  #valueObj;
778
835
  #valueOf;
836
+ /** Told when this handle closes (its status says `ended: client` from then on, whatever the namespace does). */
837
+ #onClose = new Set();
838
+ /** The handle's own state, a state value: `get()` (the same object while unchanged), `onChange`, `changed()`, `watch()`. */
839
+ status;
840
+ #status;
779
841
  constructor(ns) {
780
842
  this.ns = ns;
781
843
  ns.handles++;
844
+ this.#id = ns.nextHandle++;
845
+ this.status = this.#status = new StatusCell(() => this.ns.state.status(this.ns.node.isConnected, this.#closed), l => { const un = this.ns.state.subscribe(l); this.#onClose.add(l); return () => { un(); this.#onClose.delete(l); }; }, () => this.#closed || this.ns.state.ended !== undefined);
782
846
  }
783
847
  #ended() { return gone(this.ns.name, `the handle ended (${ENDED_CLIENT})`); }
784
848
  get name() { return this.ns.name; }
785
- /** The whole value, what the runtime has; the same object while nothing changed (reference-stable for React). */
786
- get value() {
849
+ get(key) {
787
850
  const flat = this.ns.state.flat;
851
+ if (key !== undefined)
852
+ return flat.get(key);
788
853
  if (this.#valueOf !== flat) {
789
854
  this.#valueOf = flat;
790
855
  this.#valueObj = Object.fromEntries(flat);
791
856
  }
792
857
  return this.#valueObj;
793
858
  }
794
- get(key) { return this.ns.state.flat.get(key); }
795
- get synced() { return this.status.synced; }
796
- get status() { return this.ns.state.status(this.ns.node.isConnected, this.#closed); }
797
- /** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `value` shows it. */
859
+ get synced() { return this.status.get().synced; }
860
+ /** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `get()` shows it. */
798
861
  async set(keyOrValues, value) {
799
862
  const v = typeof keyOrValues === "string" ? await this.#write("put", { key: keyOrValues, value }) : await this.#write("put_many", { values: keyOrValues });
800
863
  await this.#reached(v);
@@ -802,11 +865,11 @@ export class StoreHandleImpl {
802
865
  async delete(key) { await this.#reached(await this.#write("delete", { key })); }
803
866
  async clear() { await this.#reached(await this.#write("clear", {})); }
804
867
  async destroy() { await this.#write("destroy", {}); }
805
- /** The wire half of a write: the runtime's answer, the version it assigned. */
868
+ /** The wire half of a write: the runtime's answer, the version it assigned (between incarnations the task holds it until the handle binds again). */
806
869
  async #write(op, args) {
807
870
  if (this.#closed)
808
871
  throw this.#ended();
809
- const r = await this.ns.request(reply => ({ kind: "write", op, args, reply }));
872
+ const r = await this.ns.request(reply => ({ kind: "write", handle: this.#id, op, args, reply }));
810
873
  const v = r?.version;
811
874
  return typeof v === "number" ? v : typeof v === "bigint" ? Number(v) : 0;
812
875
  }
@@ -829,7 +892,7 @@ export class StoreHandleImpl {
829
892
  try {
830
893
  for await (const batch of q) {
831
894
  if (key === undefined)
832
- yield this.value;
895
+ yield this.get();
833
896
  else if (batch.some(c => c.key === key))
834
897
  yield this.ns.state.flat.get(key);
835
898
  }
@@ -839,10 +902,25 @@ export class StoreHandleImpl {
839
902
  this.ns.state.unwatch(q);
840
903
  }
841
904
  }
842
- /** The framework bridge (React's `useStore`): called on every change of the value or the status. */
843
- subscribe(listener) { return this.ns.state.subscribe(listener); }
905
+ /** The whole value after its next change (`store_gone` once this handle is closed). */
906
+ changed() {
907
+ if (this.#closed)
908
+ return Promise.reject(this.#ended());
909
+ const from = this.get();
910
+ const d = deferred();
911
+ const un = this.ns.state.subscribe(() => { const v = this.get(); if (v !== from) {
912
+ un();
913
+ this.#onClose.delete(gone);
914
+ d.resolve(v);
915
+ } });
916
+ const gone = () => { un(); this.#onClose.delete(gone); d.reject(this.#ended()); };
917
+ this.#onClose.add(gone);
918
+ return d.promise;
919
+ }
920
+ /** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
921
+ onChange(cb) { return this.ns.state.subscribe(() => cb(this.get())); }
844
922
  /** Waits for the namespace to be in (the first snapshot applied, or absent). */
845
- join() { return this.ns.request(reply => ({ kind: "join", reply })); }
923
+ ready() { return this.ns.request(reply => ({ kind: "join", reply })); }
846
924
  close() {
847
925
  if (this.#closed)
848
926
  return;
@@ -851,18 +929,29 @@ export class StoreHandleImpl {
851
929
  for (const q of this.#feeds)
852
930
  this.ns.state.unwatch(q);
853
931
  this.#feeds.clear();
932
+ for (const l of [...this.#onClose]) {
933
+ try {
934
+ l();
935
+ }
936
+ catch (e) {
937
+ queueMicrotask(() => { throw e; });
938
+ }
939
+ }
940
+ this.#onClose.clear();
941
+ this.#status.dispose();
854
942
  if (--this.ns.handles === 0)
855
943
  this.ns.post({ kind: "close" });
944
+ else
945
+ this.ns.post({ kind: "leave", handle: this.#id });
856
946
  }
857
947
  async [Symbol.asyncDispose]() { this.close(); }
858
948
  }
859
949
  /**
860
950
  * `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
861
- * it (no open written), with other options `invalid_input`; one exiting is waited out.
951
+ * it (no open written), with other options `invalid_input`; one exiting is waited out. Without a session
952
+ * (before the first, between two) the task waits for the next one, like after a drop; it resolves once in.
862
953
  */
863
954
  export async function openStore(node, registry, name, opts, warn) {
864
- if (!node.currentSession())
865
- throw new SynapseError("not_connected");
866
955
  for (;;) {
867
956
  const live = registry.get(name);
868
957
  if (live) {
@@ -874,7 +963,7 @@ export async function openStore(node, registry, name, opts, warn) {
874
963
  throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
875
964
  const h = new StoreHandleImpl(live);
876
965
  try {
877
- await h.join();
966
+ await h.ready();
878
967
  }
879
968
  catch (e) {
880
969
  h.close();
@@ -887,7 +976,7 @@ export async function openStore(node, registry, name, opts, warn) {
887
976
  const h = new StoreHandleImpl(ns);
888
977
  startNamespace(ns, registry);
889
978
  try {
890
- await h.join();
979
+ await h.ready();
891
980
  }
892
981
  catch (e) {
893
982
  h.close();
@@ -896,3 +985,26 @@ export async function openStore(node, registry, name, opts, warn) {
896
985
  return h;
897
986
  }
898
987
  }
988
+ /**
989
+ * The synchronous form of `openStore`, for a handle the node itself holds (`node.tasks`): a handle on
990
+ * the namespace's task, joined if one runs, else started now; the value is in once `ready()`
991
+ * resolves. A task that is ending (its last handle closed) is waited out before the new one starts:
992
+ * the new one is in the registry at once (a `store()` meanwhile joins it), its requests wait in its
993
+ * mailbox.
994
+ */
995
+ export function attachStore(node, registry, name, opts, warn) {
996
+ const live = registry.get(name);
997
+ if (live && live.handles > 0) {
998
+ if (live.opts.access !== opts.access || live.opts.onDeath !== opts.onDeath)
999
+ throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
1000
+ return new StoreHandleImpl(live);
1001
+ }
1002
+ const ns = new Namespace(name, opts, node, warn);
1003
+ registry.set(name, ns);
1004
+ const h = new StoreHandleImpl(ns);
1005
+ if (live)
1006
+ void live.exited.promise.then(() => startNamespace(ns, registry));
1007
+ else
1008
+ startNamespace(ns, registry);
1009
+ return h;
1010
+ }
package/dist/synapse.d.ts CHANGED
@@ -1,45 +1,58 @@
1
- import { type Age, type ClockInfo, type ErrorBody, type LocalTime, type ProjectSnapshot, type ServiceContract, type TimeOffsets, type TopicContract } from "@synapse-sdk/core";
1
+ import { type Age, type ErrorBody, type ProjectSnapshot, type ServiceContract, type TopicContract } from "@synapse-sdk/core";
2
2
  import { Node } from "./node.ts";
3
3
  import { StoreHandleImpl } from "./store.ts";
4
4
  import { type Warn } from "./topics.ts";
5
- import type { CallEvent, CallOptions, Catalog, ConnectOptions, ConnectTarget, ConnectionState, Envelope, Info, Metrics, Notice, Publisher, Registration, Run, ServeHandler, ServeOptions, SceneHandle, StoreHandle, StoreOptions, SubscribeOptions, Subscription, View, ViewOptions } from "./types.ts";
6
- /** How long the first `connect()` keeps trying a retryable failure before `failed` (SDK §2: the first connect is bounded). */
7
- export declare const FIRST_CONNECT_GIVE_UP_MS = 30000;
5
+ import type { CallEvent, CallOptions, Catalog, ConnectOptions, ConnectTarget, ConnectionState, Envelope, Info, Metrics, Publisher, Registration, Run, ServeHandler, ServeOptions, SceneHandle, StoreHandle, StoreOptions, SubscribeOptions, Subscription, Tasks, View, ViewOptions } from "./types.ts";
6
+ import { type StateValue } from "./util.ts";
7
+ export { FIRST_CONNECT_GIVE_UP_MS } from "./node.ts";
8
8
  export declare class Synapse implements AsyncDisposable {
9
9
  #private;
10
- readonly opts: ConnectOptions;
11
10
  warn: Warn;
11
+ /**
12
+ * The node, usable at once and `idle` (no target) until `connect()`: constructing opens no connection. Where to connect
13
+ * (the url, its token, the certificate pin) is the target's, given to `connect()`. Throws `invalid_input` for options
14
+ * that can never work (a bad `name`).
15
+ */
12
16
  constructor(opts: ConnectOptions);
13
- /** `await Synapse.connect(opts)`: a node with a session, or the reason there is none. */
14
- static connect(opts: ConnectOptions): Promise<Synapse>;
15
- /** Connects (once; later calls return the same promise). Resolves once a session is established. */
16
- connect(): Promise<this>;
17
+ /**
18
+ * Connects the node to `target` (a string is the url), the one connect verb: the first connect, and how an HMI moves
19
+ * from one robot's runtime to another. The current session, if any, is torn down before `target` is dialed; every
20
+ * handle re-binds to the new session as across a reconnect. Identity fields `target` leaves out keep the node's.
21
+ *
22
+ * Operator intent is never lost under quick clicking: the newest call wins. The node works toward its latest target; a
23
+ * call superseded by a newer `connect()` or `disconnect()` resolves quietly. The target already declared declares
24
+ * nothing and settles with it, except from its own `failed`, where it is dialed again.
25
+ *
26
+ * Bounded (SDK §2): a retryable failure is tried again, paced by the reconnect policy, until `FIRST_CONNECT_GIVE_UP_MS`
27
+ * after the first attempt, a hard bound (an attempt still outstanding then is cut); `unauthorized` gives up at once.
28
+ * Resolves once a session to *this* target is connected (or a newer call superseded it). Rejects with this target's
29
+ * error when the node rests `failed` (still usable: `connect()` dials again), `invalid_input` at once for a malformed
30
+ * target, `not_connected` once the node has ended (`close()`).
31
+ *
32
+ * With no target: the declared target, if there is one (settles with it, as above), else `SYNAPSE_ENDPOINT` (Bun/Node;
33
+ * the runtime sets it for every task it starts); neither (or a browser) is `invalid_input` and the node stays `idle`.
34
+ *
35
+ * The outcome is handled: `node.connect(url);` without a handler is safe (`useConnection()` shows `failed`); `await` still rejects.
36
+ */
37
+ connect(target?: ConnectTarget | string): Promise<this>;
38
+ /**
39
+ * Disconnects: the session, if any, is torn down and the node rests `idle` (no target), not ended. Every handle is kept,
40
+ * exactly as between two sessions, and a later `connect()` dials again. A `connect()` still pursuing is superseded (it
41
+ * resolves quietly). Resolves once the session is gone; already `idle`: at once. `not_connected` once the node has ended.
42
+ */
43
+ disconnect(): Promise<void>;
44
+ /** The node's engine (the SDK's own modules and tests use it). */
17
45
  get node(): Node;
18
- get status(): ConnectionState;
19
- get connected(): boolean;
20
- /** Called on every change of the connection state; returns the unsubscribe. */
21
- watchStatus(cb: (s: ConnectionState) => void): () => void;
22
- /** Resolves on the next connected session (at once if connected); rejects once the node ended. */
23
- whenConnected(): Promise<void>;
24
- /** Declares a new target (broker, identity); resolves once connected to it (or rejects with why not). `null` disconnects for good. */
25
- setTarget(target: ConnectTarget | null): Promise<void>;
26
- /** Tells the runtime we are leaving, ends every handle, and resolves once the node has ended. */
27
- /** During the first `connect()` it ends that attempt: the node ends `closed` and `connect()` rejects `not_connected`. */
46
+ /** Where the node stands (`ConnectionState`), a state value: `get()` now, `onChange` for each change, `changed()` for the next one, `watch()` for each one in a loop. */
47
+ readonly status: StateValue<ConnectionState>;
48
+ /** Tells the runtime we are leaving, ends every handle, and resolves once the node has ended (`closed`, for good). A `connect()` in flight rejects `not_connected`. */
28
49
  close(): Promise<void>;
29
50
  [Symbol.asyncDispose](): Promise<void>;
30
- /** Resolves once the node has ended (closed, failed, or the policy gave up). */
31
- get ended(): Promise<void>;
32
51
  info(): Info;
33
52
  metrics(): Metrics;
34
- /** Broker notices addressed to this node (an error body on `res` without a call). */
35
- onNotice(cb: (n: ErrorBody) => void): () => void;
36
- /** `promoted`/`demoted` notices (WP §3.5). */
37
- onProjectNotice(cb: (n: Notice) => void): () => void;
53
+ /** Notices addressed to this node: `cb` per notice (an error body on `res` without a call, and a refused live override claim of this node's scene); returns the unsubscribe. */
54
+ notices(cb: (n: ErrorBody) => void): () => void;
38
55
  nowUs(): number;
39
- clock(): ClockInfo;
40
- timeOffsets(): TimeOffsets;
41
- /** A publisher stamp in our clock (WP §7.4), when the offsets allow it. */
42
- toLocalUs(tUs: number, dom: string): LocalTime | undefined;
43
56
  /** How old a message is now, in our clock. */
44
57
  ageUs(env: {
45
58
  tUs: number;
@@ -48,13 +61,20 @@ export declare class Synapse implements AsyncDisposable {
48
61
  /** A publisher of `topic`; resolves once the runtime admitted it on the current session (a refusal is asked again, paced, warned). */
49
62
  publisher<TIn, TOut>(topic: TopicContract<TIn, TOut>): Promise<Publisher<TIn>>;
50
63
  publisher<TIn = unknown>(topic: string): Promise<Publisher<TIn>>;
51
- /** A subscription to `topic` (every publisher the catalog lists but ourselves); its follow task starts now. */
64
+ /**
65
+ * A subscription to `topic` (every publisher the catalog lists but ourselves), the stream: read it with `next()` or
66
+ * `for await`. Its follow task starts now. With `cb`, the callback form: the SDK runs the read loop and calls `cb` with
67
+ * each batch `next()` returns (a throwing `cb` is rethrown later; the loop goes on); the same subscription comes back
68
+ * (`unsubscribe()` ends it, `stats` counts it).
69
+ */
52
70
  subscribe<TIn, TOut>(topic: TopicContract<TIn, TOut>, opts?: SubscribeOptions): Subscription<TOut>;
71
+ subscribe<TIn, TOut>(topic: TopicContract<TIn, TOut>, cb: (batch: Envelope<TOut>[]) => void, opts?: SubscribeOptions): Subscription<TOut>;
53
72
  subscribe<TOut = unknown>(topic: string, opts?: SubscribeOptions): Subscription<TOut>;
54
- /** The topic's latest value as a cell (SDK §4.3); subscribed while someone listens. */
73
+ subscribe<TOut = unknown>(topic: string, cb: (batch: Envelope<TOut>[]) => void, opts?: SubscribeOptions): Subscription<TOut>;
74
+ /** The topic's latest value as a view (SDK §4.3); subscribed while someone listens. */
55
75
  view<TIn, TOut, S = TOut>(topic: TopicContract<TIn, TOut>, opts?: ViewOptions<TOut, S>): View<S>;
56
76
  view<TOut = unknown, S = TOut>(topic: string, opts?: ViewOptions<TOut, S>): View<S>;
57
- /** Starts a call on the current session (SDK §5.2): `await` it for the result, or read `updates()` first. `not_connected` between sessions. */
77
+ /** Starts a call on the current session (SDK §5.2): `await` it for the result, or read `updates()` first. With no session (before the first, between two) the Run is already failed `not_connected`: nothing was sent. */
58
78
  call<TArgs, TInput, U, R>(svc: ServiceContract<TArgs, TInput, U, R>, args: TArgs, opts?: CallOptions<U>): Run<U, R>;
59
79
  call<U = unknown, R = unknown>(svc: string, args?: unknown, opts?: CallOptions<U>): Run<U, R>;
60
80
  /** The call's updates and result as one ordered stream; `break` cancels. */
@@ -65,25 +85,36 @@ export declare class Synapse implements AsyncDisposable {
65
85
  serve<I = unknown, U = unknown, R = unknown>(svc: string, handler: ServeHandler<I, U, R>, opts?: ServeOptions): Promise<Registration>;
66
86
  /** Opens namespace `name` (SDK §9); resolves once the mirror is in (or the namespace is known absent). */
67
87
  store(name: string, opts?: StoreOptions): Promise<StoreHandle>;
68
- /** The runtime's `config` namespace (operators write it). */
69
- config(opts?: StoreOptions): Promise<StoreHandle>;
70
88
  /** The active project's scene (PLAN v0.24 §5); resolves attached, or at once `detached` when no project is active. */
71
89
  scene(): Promise<SceneHandle>;
72
- /** The runtime's catalogs as the current session last read them; empty between sessions. */
73
- get catalog(): Catalog;
74
- /** Called when either catalog changes on the current session. */
75
- watchCatalog(cb: (c: Catalog) => void): () => void;
76
- /** The project state the current session last read (WP §3.5); `undefined` between sessions or before the first snapshot. */
77
- project(): ProjectSnapshot | undefined;
78
- watchProject(cb: (p: ProjectSnapshot | undefined) => void): () => void;
79
- /** `synapse/project/activate {id}`: activates `id` (or joins it when active); promoted to operator. */
90
+ /** The runtime's catalogs as the current session last read them (empty between sessions), a state value: `get()` now, `onChange` for each change, `changed()` for the next one, `watch()` for each one in a loop. */
91
+ readonly catalog: StateValue<Catalog>;
92
+ /** The project state the current session last read (WP §3.5), a state value; `get()` is `undefined` between sessions or before the first snapshot. */
93
+ readonly project: StateValue<ProjectSnapshot | undefined>;
94
+ /**
95
+ * `synapse/project/activate {id}`: activates `id` when no project is active, or joins it when it is the active one;
96
+ * `project_busy` while another project is active (no takeover: deactivate it first). A web node is promoted to
97
+ * operator and resolves once the session with the new role is connected; an hmi stays `hmi` and resolves on the answer.
98
+ */
80
99
  activateProject(id: string): Promise<ProjectSnapshot>;
81
- /** `synapse/project/deactivate {}`: every operator is demoted. */
100
+ /**
101
+ * `synapse/project/deactivate {}`: any operator or hmi may deactivate, except a project an hmi activated
102
+ * (`active.activated_by_hmi`), which only an hmi may (`not_operator`). Every web operator is demoted; when that
103
+ * demotes this node, resolves once the demoted session is connected. An hmi is never demoted.
104
+ */
82
105
  deactivateProject(): Promise<ProjectSnapshot>;
83
- /** `synapse/project/join {as: "operator"}`: promoted on the active project, or `project_inactive`. */
106
+ /** `synapse/project/join {as: "operator"}`: an operator of the active project, or `project_inactive`. A web node resolves once the promoted session is connected; an hmi on the answer. */
84
107
  joinProject(as?: "operator"): Promise<ProjectSnapshot>;
85
- /** `synapse/project/leave {}`: drops our standing (every session of our `user_id`). */
108
+ /**
109
+ * `synapse/project/leave {}`: drops our standing. For an hmi membership is per session: this session only (it stays
110
+ * `hmi`, never closed). For a web node it removes our `user_id`'s web sessions only; when that demotes this node,
111
+ * resolves once the demoted session is connected.
112
+ */
86
113
  leaveProject(): Promise<ProjectSnapshot>;
87
- getProject(): Promise<ProjectSnapshot>;
114
+ /**
115
+ * The robot's task declarations, what runs (`status`), what the runtime can run (`catalog`), and the verbs; the same
116
+ * object for the node's life. Each handle opens on its first touch and is the node's own (`node.close()` ends it).
117
+ */
118
+ get tasks(): Tasks;
88
119
  }
89
120
  export type { Envelope, StoreHandleImpl };