@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/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
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";
@@ -20,7 +21,7 @@ const ENDED_STORE_GONE = "store_gone";
20
21
  /** A watcher this many batches behind is ended, never grown. */
21
22
  export const STORE_FEED_CAP = 256;
22
23
  /** How long an incarnation the open's reply named has to show up on the announcement cursor (no "live" marker in @moq/net's JS cursor). */
23
- const ANNOUNCE_WITHIN_MS = 1000;
24
+ export const ANNOUNCE_WITHIN_MS = 1000;
24
25
  const isFinal = (reason) => reason === ENDED_DESTROYED;
25
26
  const gone = (name, why) => new SynapseError("store_gone", `store ${JSON.stringify(name)}: ${why}`);
26
27
  // ------------------------------------------------------------------------------------- state
@@ -38,7 +39,8 @@ export class State {
38
39
  onDeath;
39
40
  absent = false;
40
41
  loaded = false;
41
- gap = false;
42
+ /** A diff skipped a version (not applied; the track is read again from its snapshot): the highest version seen then. Cleared once the copy is back at it (PLAN v0.34 §2.2). */
43
+ gap;
42
44
  pending = 0;
43
45
  ended;
44
46
  #listeners = new Set();
@@ -91,13 +93,22 @@ export class State {
91
93
  this.onDeath = s.on_death;
92
94
  this.absent = false;
93
95
  this.loaded = true;
94
- this.gap = false;
96
+ this.#heal();
95
97
  this.#emit(records);
96
98
  }
99
+ /** A gap is over once the copy is back at the highest version seen when it opened. */
100
+ #heal() { if (this.gap !== undefined && this.version >= this.gap)
101
+ this.gap = undefined; }
102
+ /**
103
+ * One diff, applied when it is the next version. Otherwise it is not applied (`false`): a gap, `synced` false until the
104
+ * copy is back at the version seen; the reader reads the track again from its snapshot (PLAN v0.34 §2.2).
105
+ */
97
106
  applyDiff(d, warn) {
98
107
  if (d.version !== this.version + 1) {
99
- warn(`store: diff out of sequence (have ${this.version}, got ${d.version}); not synced until the next snapshot`);
100
- this.gap = true;
108
+ warn(`store: diff out of sequence (have ${this.version}, got ${d.version}): not applied; reading the track again from its snapshot`);
109
+ this.gap = Math.max(this.gap ?? d.version, d.version);
110
+ this.#notify();
111
+ return false;
101
112
  }
102
113
  const records = [];
103
114
  const flat = new Map(this.flat);
@@ -118,7 +129,9 @@ export class State {
118
129
  }
119
130
  this.flat = flat;
120
131
  this.version = d.version;
132
+ this.#heal();
121
133
  this.#emit(records);
134
+ return true;
122
135
  }
123
136
  #clear() {
124
137
  const records = [...this.rows].map(([key, r]) => ({ kind: "delete", key, previousValue: r.v, by: undefined }));
@@ -134,7 +147,7 @@ export class State {
134
147
  this.epoch = undefined;
135
148
  this.version = 0;
136
149
  this.loaded = false;
137
- this.gap = false;
150
+ this.gap = undefined;
138
151
  this.mode = undefined;
139
152
  this.owner = undefined;
140
153
  this.mine = false;
@@ -150,7 +163,7 @@ export class State {
150
163
  this.epoch = undefined;
151
164
  this.version = 0;
152
165
  this.loaded = false;
153
- this.gap = false;
166
+ this.gap = undefined;
154
167
  this.mode = a?.mode;
155
168
  this.owner = a?.owner ?? undefined;
156
169
  this.mine = a?.mine ?? false;
@@ -176,7 +189,7 @@ export class State {
176
189
  status(nodeConnected, closed = false) {
177
190
  const ended = closed ? ENDED_CLIENT : this.ended;
178
191
  const connected = ended === undefined && nodeConnected;
179
- return { synced: connected && this.pending === 0 && !this.gap && (this.loaded || this.absent), connected, pending: this.pending, mode: this.mode, owner: this.owner, mine: this.mine, minRole: this.minRole, onDeath: this.onDeath, absent: this.absent, version: this.version, epoch: this.epoch, ended };
192
+ return { synced: connected && this.pending === 0 && this.gap === undefined && (this.loaded || this.absent), connected, pending: this.pending, mode: this.mode, owner: this.owner, mine: this.mine, minRole: this.minRole, onDeath: this.onDeath, absent: this.absent, version: this.version, epoch: this.epoch, ended };
180
193
  }
181
194
  }
182
195
  function sameValue(a, b) {
@@ -206,6 +219,7 @@ export class Namespace {
206
219
  mailbox = new AsyncQueue(Infinity);
207
220
  exited = deferred();
208
221
  handles = 0;
222
+ nextHandle = 0;
209
223
  constructor(name, opts, node, warn) {
210
224
  this.name = name;
211
225
  this.opts = opts;
@@ -239,6 +253,9 @@ async function run(ns, registry) {
239
253
  let unwatchProject;
240
254
  const joining = [];
241
255
  let versionWaiters = [];
256
+ /** 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`. */
257
+ let parked = [];
258
+ const ABSENT = "the namespace is absent: open it with an access mode to create it";
242
259
  const leaveIncarnation = () => { for (const w of versionWaiters.splice(0))
243
260
  w.reply.reject(gone(name, `the handle left the incarnation before version ${w.version}`)); };
244
261
  const stopPhase = () => {
@@ -358,9 +375,12 @@ async function run(ns, registry) {
358
375
  const done = follow(ns, s, announced, access?.epoch, stop.signal);
359
376
  phase = { kind: "following", attempt, stop, done };
360
377
  void done.then(reason => post({ kind: "followEnded", attempt, reason }));
361
- if (!access)
378
+ // Absent: honestly in (nothing to load), and nothing to write to (the parked writes are refused).
379
+ if (!access) {
362
380
  for (const j of joining.splice(0))
363
381
  j.resolve();
382
+ refuseParked(ABSENT);
383
+ }
364
384
  return;
365
385
  }
366
386
  announced.close();
@@ -389,6 +409,9 @@ async function run(ns, registry) {
389
409
  if (state.loaded && state.epoch === epoch && state.version >= wanted)
390
410
  for (const j of joining.splice(0))
391
411
  j.resolve();
412
+ // The handle is bound again (the reader applied the snapshot that names the epoch): the writes parked meanwhile go out, in order, naming it.
413
+ if (state.ended === undefined && state.epoch !== undefined)
414
+ sendParked();
392
415
  };
393
416
  const join = (reply) => {
394
417
  if (phase.kind === "waitingProject") {
@@ -426,14 +449,53 @@ async function run(ns, registry) {
426
449
  again(REFOLLOW_PACE_MS);
427
450
  }
428
451
  };
429
- const write = (op, args, reply) => {
452
+ /**
453
+ * A write. Ended or absent: `store_gone`. Between incarnations (a replay, or no session yet) it is parked: a write is
454
+ * intent, sent once the handle binds again (the runtime decides it then), refused `store_gone` if the handle ends first;
455
+ * it has no deadline of its own. Bound: sent after any parked before it, naming the mirror's epoch (and claim).
456
+ */
457
+ const write = (handle, op, args, reply) => {
430
458
  if (state.ended !== undefined) {
431
459
  reply.reject(gone(name, `the handle ended (${state.ended})`));
432
460
  return;
433
461
  }
462
+ if (state.epoch === undefined) {
463
+ if (state.absent) {
464
+ reply.reject(gone(name, ABSENT));
465
+ return;
466
+ }
467
+ parked.push({ handle, op, args, reply });
468
+ state.setPending(state.pending + 1);
469
+ return;
470
+ }
471
+ sendParked();
472
+ send(op, args, reply);
473
+ };
474
+ /** The parked writes go out, in arrival order (the handle is bound). */
475
+ const sendParked = () => {
476
+ if (!parked.length)
477
+ return;
478
+ const ws = parked;
479
+ parked = [];
480
+ state.setPending(state.pending - ws.length);
481
+ for (const w of ws)
482
+ send(w.op, w.args, w.reply);
483
+ };
484
+ /** 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). */
485
+ const refuseParked = (why, of) => {
486
+ const ws = parked.filter(w => of === undefined || w.handle === of);
487
+ if (!ws.length)
488
+ return;
489
+ parked = parked.filter(w => !ws.includes(w));
490
+ state.setPending(state.pending - ws.length);
491
+ for (const w of ws)
492
+ w.reply.reject(gone(name, why));
493
+ };
494
+ /** One write on the wire, naming the bound epoch (and claim); answered by the broker (`writeDone`). */
495
+ const send = (op, args, reply) => {
434
496
  const epoch = state.epoch;
435
497
  if (epoch === undefined) {
436
- 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"));
498
+ reply.reject(gone(name, "no incarnation is bound"));
437
499
  return;
438
500
  }
439
501
  if (!session) {
@@ -534,7 +596,10 @@ async function run(ns, registry) {
534
596
  waitVersion(ev.epoch, ev.version, ev.reply);
535
597
  break;
536
598
  case "write":
537
- write(ev.op, ev.args, ev.reply);
599
+ write(ev.handle, ev.op, ev.args, ev.reply);
600
+ break;
601
+ case "leave":
602
+ refuseParked(`the handle ended (${ENDED_CLIENT})`, ev.handle);
538
603
  break;
539
604
  case "writeDone":
540
605
  if (writeDone(ev.op, ev.result, ev.reply)) {
@@ -567,6 +632,7 @@ async function run(ns, registry) {
567
632
  for (const j of joining.splice(0))
568
633
  j.reject(e);
569
634
  leaveIncarnation();
635
+ refuseParked(`the handle ended (${state.ended ?? reason ?? ENDED_CLIENT})`);
570
636
  {
571
637
  const p = ph();
572
638
  if (p.kind === "following" || p.kind === "paced")
@@ -580,7 +646,7 @@ async function run(ns, registry) {
580
646
  if (ev.kind === "writeDone")
581
647
  writeDone(ev.op, ev.result, ev.reply);
582
648
  else if (ev.kind === "write")
583
- write(ev.op, ev.args, ev.reply);
649
+ write(ev.handle, ev.op, ev.args, ev.reply);
584
650
  else if (ev.kind === "opened") {
585
651
  const p = ph();
586
652
  if (p.kind === "opening" && p.o.attempt === ev.attempt) {
@@ -632,7 +698,8 @@ function announcedEpoch(base, prefix) {
632
698
  /**
633
699
  * Reads the incarnation the open bound (`want`; `undefined`: absent, the first one announced), writing
634
700
  * the mirror and telling the task of each frame applied; returns how the track ended (`undefined`:
635
- * no final frame — the session, or the broker, went; the task replays).
701
+ * no final frame — the session, or the broker, went; the task replays). A diff out of sequence is not applied: the
702
+ * subscription is let go and the track subscribed again, which delivers the open group from its snapshot (PLAN v0.34 §2.2).
636
703
  */
637
704
  async function follow(ns, session, announced, want, signal) {
638
705
  const { state, warn, name } = ns;
@@ -685,74 +752,86 @@ async function follow(ns, session, announced, want, signal) {
685
752
  return undefined;
686
753
  }
687
754
  announced.close();
688
- sub = bc.track(STORE_TRACK).subscribe(latestSubscription(moqPriority(1)));
689
- const s = sub;
755
+ const track = bc.track(STORE_TRACK);
690
756
  // Closing the subscriber ends `recvGroup`, not a read on the group it already handed us: that group is closed too.
691
757
  let g;
692
- signal.addEventListener("abort", () => { s.close(); g?.close(); }, { once: true });
693
- // The first group read is the open one, from its start: a snapshot, then the diffs.
694
- for (;;) {
695
- g = await s.recvGroup();
696
- if (!g)
697
- break;
758
+ signal.addEventListener("abort", () => { sub?.close(); g?.close(); }, { once: true });
759
+ // A gap: this subscription is let go and the track subscribed again, which delivers the open group from its
760
+ // snapshot (PLAN v0.34 §2.2; no counter, no timer).
761
+ subscribe: for (;;) {
762
+ if (signal.aborted)
763
+ return undefined;
764
+ try {
765
+ sub?.close();
766
+ }
767
+ catch { /* closed */ }
768
+ const s = sub = track.subscribe(latestSubscription(moqPriority(1)));
769
+ // The first group read is the open one, from its start: a snapshot, then the diffs.
698
770
  for (;;) {
699
- let f;
700
- try {
701
- f = await g.readFrame();
702
- }
703
- catch {
704
- break;
705
- }
706
- if (!f)
771
+ g = await s.recvGroup();
772
+ if (!g)
707
773
  break;
708
- let header, hlen;
709
- try {
710
- ({ header, hlen } = decodeHeader(f.payload));
711
- }
712
- catch {
713
- warn(`store ${name}: frame not decodable`);
714
- continue;
715
- }
716
- let body;
717
- try {
718
- body = decodeBody(f.payload.subarray(hlen));
719
- }
720
- catch {
721
- warn(`store ${name}: frame body not decodable`);
722
- continue;
723
- }
724
- if (header.flags & FLAGS.END) {
725
- const fin = body;
726
- return fin && typeof fin.reason === "string" ? fin.reason : ENDED_STORE_GONE;
727
- }
728
- // A stopped reader no longer writes the mirror: it belongs to the next incarnation (or to none).
729
- if (header.flags & FLAGS.KEY) {
730
- const snap = body;
731
- if (!snap || snap.epoch !== epoch) {
732
- warn(`store ${name}: snapshot of another epoch dropped`);
733
- continue;
774
+ for (;;) {
775
+ let f;
776
+ try {
777
+ f = await g.readFrame();
734
778
  }
735
- if (signal.aborted)
736
- continue;
737
- if (state.ended === undefined)
738
- state.applySnapshot(snap);
739
- ns.post({ kind: "applied", epoch, version: snap.version });
740
- }
741
- else if (header.flags & FLAGS.DELTA) {
742
- const diff = body;
743
- if (!diff || typeof diff.version !== "number") {
744
- warn(`store ${name}: diff not decodable`);
779
+ catch {
780
+ break;
781
+ }
782
+ if (!f)
783
+ break;
784
+ let header, hlen;
785
+ try {
786
+ ({ header, hlen } = decodeHeader(f.payload));
787
+ }
788
+ catch {
789
+ warn(`store ${name}: frame not decodable`);
745
790
  continue;
746
791
  }
747
- if (signal.aborted)
792
+ let body;
793
+ try {
794
+ body = decodeBody(f.payload.subarray(hlen));
795
+ }
796
+ catch {
797
+ warn(`store ${name}: frame body not decodable`);
748
798
  continue;
749
- if (state.ended === undefined && state.loaded)
750
- state.applyDiff(diff, warn);
751
- ns.post({ kind: "applied", epoch, version: diff.version });
799
+ }
800
+ if (header.flags & FLAGS.END) {
801
+ const fin = body;
802
+ return fin && typeof fin.reason === "string" ? fin.reason : ENDED_STORE_GONE;
803
+ }
804
+ // A stopped reader no longer writes the mirror: it belongs to the next incarnation (or to none).
805
+ if (header.flags & FLAGS.KEY) {
806
+ const snap = body;
807
+ if (!snap || snap.epoch !== epoch) {
808
+ warn(`store ${name}: snapshot of another epoch dropped`);
809
+ continue;
810
+ }
811
+ if (signal.aborted)
812
+ continue;
813
+ if (state.ended === undefined)
814
+ state.applySnapshot(snap);
815
+ ns.post({ kind: "applied", epoch, version: snap.version });
816
+ }
817
+ else if (header.flags & FLAGS.DELTA) {
818
+ const diff = body;
819
+ if (!diff || typeof diff.version !== "number") {
820
+ warn(`store ${name}: diff not decodable`);
821
+ continue;
822
+ }
823
+ if (signal.aborted)
824
+ continue;
825
+ if (state.ended === undefined && state.loaded && !state.applyDiff(diff, warn)) {
826
+ g.close();
827
+ continue subscribe;
828
+ }
829
+ ns.post({ kind: "applied", epoch, version: diff.version });
830
+ }
752
831
  }
753
832
  }
833
+ return undefined; // the track ended with no final frame: the session (or the broker) went
754
834
  }
755
- return undefined; // the track ended with no final frame: the session (or the broker) went
756
835
  }
757
836
  catch {
758
837
  return undefined;
@@ -771,31 +850,39 @@ async function follow(ns, session, announced, want, signal) {
771
850
  export class StoreHandleImpl {
772
851
  ns;
773
852
  #closed = false;
853
+ /** This handle's id in its namespace: its parked writes are refused when it closes. */
854
+ #id;
774
855
  /** Resolved when this handle closes: its version waits end. */
775
856
  #left = deferred();
776
857
  /** This handle's change feeds: closed with it. */
777
858
  #feeds = new Set();
778
859
  #valueObj;
779
860
  #valueOf;
861
+ /** Told when this handle closes (its status says `ended: client` from then on, whatever the namespace does). */
862
+ #onClose = new Set();
863
+ /** The handle's own state, a state value: `get()` (the same object while unchanged), `onChange`, `changed()`, `watch()`. */
864
+ status;
865
+ #status;
780
866
  constructor(ns) {
781
867
  this.ns = ns;
782
868
  ns.handles++;
869
+ this.#id = ns.nextHandle++;
870
+ 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);
783
871
  }
784
872
  #ended() { return gone(this.ns.name, `the handle ended (${ENDED_CLIENT})`); }
785
873
  get name() { return this.ns.name; }
786
- /** The whole value, what the runtime has; the same object while nothing changed (reference-stable for React). */
787
- get value() {
874
+ get(key) {
788
875
  const flat = this.ns.state.flat;
876
+ if (key !== undefined)
877
+ return flat.get(key);
789
878
  if (this.#valueOf !== flat) {
790
879
  this.#valueOf = flat;
791
880
  this.#valueObj = Object.fromEntries(flat);
792
881
  }
793
882
  return this.#valueObj;
794
883
  }
795
- get(key) { return this.ns.state.flat.get(key); }
796
- get synced() { return this.status.synced; }
797
- get status() { return this.ns.state.status(this.ns.node.isConnected, this.#closed); }
798
- /** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `value` shows it. */
884
+ get synced() { return this.status.get().synced; }
885
+ /** `set(key, value)` or `set({ ...several keys })` (one atomic write); resolves once `get()` shows it. */
799
886
  async set(keyOrValues, value) {
800
887
  const v = typeof keyOrValues === "string" ? await this.#write("put", { key: keyOrValues, value }) : await this.#write("put_many", { values: keyOrValues });
801
888
  await this.#reached(v);
@@ -803,11 +890,11 @@ export class StoreHandleImpl {
803
890
  async delete(key) { await this.#reached(await this.#write("delete", { key })); }
804
891
  async clear() { await this.#reached(await this.#write("clear", {})); }
805
892
  async destroy() { await this.#write("destroy", {}); }
806
- /** The wire half of a write: the runtime's answer, the version it assigned. */
893
+ /** 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). */
807
894
  async #write(op, args) {
808
895
  if (this.#closed)
809
896
  throw this.#ended();
810
- const r = await this.ns.request(reply => ({ kind: "write", op, args, reply }));
897
+ const r = await this.ns.request(reply => ({ kind: "write", handle: this.#id, op, args, reply }));
811
898
  const v = r?.version;
812
899
  return typeof v === "number" ? v : typeof v === "bigint" ? Number(v) : 0;
813
900
  }
@@ -830,7 +917,7 @@ export class StoreHandleImpl {
830
917
  try {
831
918
  for await (const batch of q) {
832
919
  if (key === undefined)
833
- yield this.value;
920
+ yield this.get();
834
921
  else if (batch.some(c => c.key === key))
835
922
  yield this.ns.state.flat.get(key);
836
923
  }
@@ -840,12 +927,30 @@ export class StoreHandleImpl {
840
927
  this.ns.state.unwatch(q);
841
928
  }
842
929
  }
843
- /** The framework bridge (React's `useStore`): called on every change of the value or the status. */
844
- subscribe(listener) { return this.ns.state.subscribe(listener); }
845
- /** Waits for the namespace to be in (the first snapshot applied, or absent). */
846
- join() { return this.ns.request(reply => ({ kind: "join", reply })); }
847
- /** `join()` under the SDK's word (Publisher and Registration have `ready()` too). */
848
- ready() { return this.join(); }
930
+ changed(key) {
931
+ if (this.#closed)
932
+ return Promise.reject(this.#ended());
933
+ if (key !== undefined) {
934
+ // the first step of `watch(key)`, started now (its feed opens at the first `next()`), then left
935
+ const it = this.watch(key)[Symbol.asyncIterator]();
936
+ return it.next().then(r => { void it.return(undefined); if (r.done)
937
+ throw this.#ended(); return r.value; });
938
+ }
939
+ const from = this.get();
940
+ const d = deferred();
941
+ const un = this.ns.state.subscribe(() => { const v = this.get(); if (v !== from) {
942
+ un();
943
+ this.#onClose.delete(gone);
944
+ d.resolve(v);
945
+ } });
946
+ const gone = () => { un(); this.#onClose.delete(gone); d.reject(this.#ended()); };
947
+ this.#onClose.add(gone);
948
+ return d.promise;
949
+ }
950
+ /** `cb` with the whole value on every change of the value or the status (the framework bridge, React's `useStore`); returns the remover. */
951
+ onChange(cb) { return this.ns.state.subscribe(() => cb(this.get())); }
952
+ /** Waits for the namespace to be in: the value current as of the open (the version the runtime answered applied), or the namespace absent. */
953
+ ready() { return this.ns.request(reply => ({ kind: "join", reply })); }
849
954
  close() {
850
955
  if (this.#closed)
851
956
  return;
@@ -854,18 +959,29 @@ export class StoreHandleImpl {
854
959
  for (const q of this.#feeds)
855
960
  this.ns.state.unwatch(q);
856
961
  this.#feeds.clear();
962
+ for (const l of [...this.#onClose]) {
963
+ try {
964
+ l();
965
+ }
966
+ catch (e) {
967
+ queueMicrotask(() => { throw e; });
968
+ }
969
+ }
970
+ this.#onClose.clear();
971
+ this.#status.dispose();
857
972
  if (--this.ns.handles === 0)
858
973
  this.ns.post({ kind: "close" });
974
+ else
975
+ this.ns.post({ kind: "leave", handle: this.#id });
859
976
  }
860
977
  async [Symbol.asyncDispose]() { this.close(); }
861
978
  }
862
979
  /**
863
980
  * `store(name, opts)`: one task per namespace per node; a second `store` with the same options joins
864
- * it (no open written), with other options `invalid_input`; one exiting is waited out.
981
+ * it (no open written), with other options `invalid_input`; one exiting is waited out. Without a session
982
+ * (before the first, between two) the task waits for the next one, like after a drop; it resolves once in.
865
983
  */
866
984
  export async function openStore(node, registry, name, opts, warn) {
867
- if (!node.currentSession())
868
- throw new SynapseError("not_connected");
869
985
  for (;;) {
870
986
  const live = registry.get(name);
871
987
  if (live) {
@@ -877,7 +993,7 @@ export async function openStore(node, registry, name, opts, warn) {
877
993
  throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
878
994
  const h = new StoreHandleImpl(live);
879
995
  try {
880
- await h.join();
996
+ await h.ready();
881
997
  }
882
998
  catch (e) {
883
999
  h.close();
@@ -890,7 +1006,7 @@ export async function openStore(node, registry, name, opts, warn) {
890
1006
  const h = new StoreHandleImpl(ns);
891
1007
  startNamespace(ns, registry);
892
1008
  try {
893
- await h.join();
1009
+ await h.ready();
894
1010
  }
895
1011
  catch (e) {
896
1012
  h.close();