@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 +12 -6
- package/dist/calls.js +35 -18
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/node.d.ts +62 -24
- package/dist/node.js +205 -66
- package/dist/scene.d.ts +166 -61
- package/dist/scene.js +1254 -343
- package/dist/scene_frames.d.ts +2 -2
- package/dist/scene_frames.js +1 -1
- package/dist/session.d.ts +3 -1
- package/dist/session.js +5 -3
- package/dist/store.d.ts +33 -14
- package/dist/store.js +209 -93
- package/dist/synapse.d.ts +73 -49
- package/dist/synapse.js +180 -116
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +24 -10
- package/dist/topics.d.ts +2 -0
- package/dist/topics.js +17 -1
- package/dist/types.d.ts +292 -110
- package/dist/util.d.ts +43 -0
- package/dist/util.js +97 -1
- package/dist/view.d.ts +5 -3
- package/dist/view.js +21 -10
- package/package.json +2 -2
package/dist/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:
|
|
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
|
-
|
|
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
|
|
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})
|
|
100
|
-
this.gap =
|
|
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 =
|
|
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 =
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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", () => {
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (
|
|
697
|
-
|
|
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
|
-
|
|
700
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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(
|
|
796
|
-
|
|
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.
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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.
|
|
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.
|
|
1009
|
+
await h.ready();
|
|
894
1010
|
}
|
|
895
1011
|
catch (e) {
|
|
896
1012
|
h.close();
|