@synapse-sdk/client 0.1.0 → 0.3.0
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/README.md +1 -111
- package/dist/calls.d.ts +37 -0
- package/dist/calls.js +192 -0
- package/dist/channel.d.ts +84 -0
- package/dist/channel.js +215 -0
- package/dist/fec.d.ts +85 -0
- package/dist/fec.js +281 -0
- package/dist/index.d.ts +10 -74
- package/dist/index.js +10 -15
- package/dist/moq_log.d.ts +23 -0
- package/dist/moq_log.js +176 -0
- package/dist/node.d.ts +145 -0
- package/dist/node.js +407 -0
- package/dist/scene.d.ts +299 -0
- package/dist/scene.js +1531 -0
- package/dist/scene_frames.d.ts +51 -0
- package/dist/scene_frames.js +215 -0
- package/dist/serve.d.ts +46 -0
- package/dist/serve.js +281 -0
- package/dist/session.d.ts +189 -0
- package/dist/session.js +745 -0
- package/dist/signals.d.ts +10 -0
- package/dist/signals.js +63 -0
- package/dist/store.d.ts +145 -0
- package/dist/store.js +898 -0
- package/dist/synapse.d.ts +89 -0
- package/dist/synapse.js +256 -0
- package/dist/topics.d.ts +136 -0
- package/dist/topics.js +938 -0
- package/dist/types.d.ts +679 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +67 -0
- package/dist/util.js +227 -0
- package/dist/view.d.ts +19 -0
- package/dist/view.js +145 -0
- package/package.json +18 -39
- package/dist/node-CSSIXG4k.js +0 -1501
- package/dist/node-DbD7G0Nx.d.ts +0 -466
- package/dist/react.d.ts +0 -63
- package/dist/react.js +0 -141
package/dist/store.js
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
// The store (SDK §9, PLAN v0.20 §7), the JS spelling of the Rust client's `store.rs`: one task per
|
|
2
|
+
// namespace per node, told about sessions by the node's loop (`Node.monitor`), driven by one mailbox.
|
|
3
|
+
// Its intent is the open (`synapse/store/open` with the handle's access mode); admitted, it follows the
|
|
4
|
+
// incarnation the reply names (`runtime/store/<ns>/@<epoch>`, or `runtime/config/@<epoch>`), track
|
|
5
|
+
// `data`: a snapshot, then diffs, then a final frame. The mirror is the runtime's: nothing is decided
|
|
6
|
+
// here. A refusal is asked again, paced, with a warning (R6); a transient reply paced, silently; an
|
|
7
|
+
// incarnation that ends (writer gone, project switch, the owner restarted) replays the intent;
|
|
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`.
|
|
10
|
+
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 { startOn } from "./calls.js";
|
|
13
|
+
import { REFOLLOW_PACE_MS, holderOf, isTransient, refusalPaceMs } from "./channel.js";
|
|
14
|
+
import { latestSubscription, waitActive } from "./session.js";
|
|
15
|
+
import { AsyncQueue, TIMEOUT, deferred, withTimeout } from "./util.js";
|
|
16
|
+
export const ENDED_DESTROYED = "destroyed";
|
|
17
|
+
export const ENDED_CONNECTION_LOST = "connection_lost";
|
|
18
|
+
export const ENDED_CLIENT = "client";
|
|
19
|
+
const ENDED_STORE_GONE = "store_gone";
|
|
20
|
+
/** A watcher this many batches behind is ended, never grown. */
|
|
21
|
+
export const STORE_FEED_CAP = 256;
|
|
22
|
+
/** 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
|
+
const isFinal = (reason) => reason === ENDED_DESTROYED;
|
|
25
|
+
const gone = (name, why) => new SynapseError("store_gone", `store ${JSON.stringify(name)}: ${why}`);
|
|
26
|
+
// ------------------------------------------------------------------------------------- state
|
|
27
|
+
/** The mirror and what the runtime said about the namespace; written by the task and its reader only. */
|
|
28
|
+
export class State {
|
|
29
|
+
epoch;
|
|
30
|
+
version = 0;
|
|
31
|
+
rows = new Map();
|
|
32
|
+
flat = new Map();
|
|
33
|
+
mode;
|
|
34
|
+
owner;
|
|
35
|
+
mine = false;
|
|
36
|
+
claim;
|
|
37
|
+
minRole;
|
|
38
|
+
onDeath;
|
|
39
|
+
absent = false;
|
|
40
|
+
loaded = false;
|
|
41
|
+
gap = false;
|
|
42
|
+
pending = 0;
|
|
43
|
+
ended;
|
|
44
|
+
#listeners = new Set();
|
|
45
|
+
#watchers = new Set();
|
|
46
|
+
subscribe(l) { this.#listeners.add(l); return () => { this.#listeners.delete(l); }; }
|
|
47
|
+
#notify() { for (const l of [...this.#listeners]) {
|
|
48
|
+
try {
|
|
49
|
+
l();
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
queueMicrotask(() => { throw e; });
|
|
53
|
+
}
|
|
54
|
+
} }
|
|
55
|
+
watch() {
|
|
56
|
+
const q = new AsyncQueue(STORE_FEED_CAP, () => { void q.close(true); this.#watchers.delete(q); });
|
|
57
|
+
if (this.ended !== undefined)
|
|
58
|
+
void q.close();
|
|
59
|
+
else
|
|
60
|
+
this.#watchers.add(q);
|
|
61
|
+
return q;
|
|
62
|
+
}
|
|
63
|
+
/** A feed its reader has left: it is closed and gets nothing more. */
|
|
64
|
+
unwatch(q) { this.#watchers.delete(q); void q.close(); }
|
|
65
|
+
#emit(records) {
|
|
66
|
+
if (records.length)
|
|
67
|
+
for (const w of [...this.#watchers])
|
|
68
|
+
w.push(records);
|
|
69
|
+
this.#notify();
|
|
70
|
+
}
|
|
71
|
+
applySnapshot(s) {
|
|
72
|
+
const records = [];
|
|
73
|
+
const rows = new Map(Object.entries(s.rows));
|
|
74
|
+
for (const [k, row] of rows) {
|
|
75
|
+
const old = this.rows.get(k);
|
|
76
|
+
if (!old)
|
|
77
|
+
records.push({ kind: "put", key: k, value: row.v, previousValue: undefined, by: undefined });
|
|
78
|
+
else if (!sameValue(old.v, row.v))
|
|
79
|
+
records.push({ kind: "put", key: k, value: row.v, previousValue: old.v, by: undefined });
|
|
80
|
+
}
|
|
81
|
+
for (const [k, old] of this.rows)
|
|
82
|
+
if (!rows.has(k))
|
|
83
|
+
records.push({ kind: "delete", key: k, previousValue: old.v, by: undefined });
|
|
84
|
+
this.rows = rows;
|
|
85
|
+
this.flat = new Map([...rows].map(([k, r]) => [k, r.v]));
|
|
86
|
+
this.version = s.version;
|
|
87
|
+
this.epoch = s.epoch;
|
|
88
|
+
this.mode = s.mode;
|
|
89
|
+
this.owner = s.owner ?? undefined;
|
|
90
|
+
this.minRole = s.min_role;
|
|
91
|
+
this.onDeath = s.on_death;
|
|
92
|
+
this.absent = false;
|
|
93
|
+
this.loaded = true;
|
|
94
|
+
this.gap = false;
|
|
95
|
+
this.#emit(records);
|
|
96
|
+
}
|
|
97
|
+
applyDiff(d, warn) {
|
|
98
|
+
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;
|
|
101
|
+
}
|
|
102
|
+
const records = [];
|
|
103
|
+
const flat = new Map(this.flat);
|
|
104
|
+
for (const [k, row] of Object.entries(d.changed)) {
|
|
105
|
+
const prev = this.rows.get(k);
|
|
106
|
+
if (!prev || !sameValue(prev.v, row.v))
|
|
107
|
+
records.push({ kind: "put", key: k, value: row.v, previousValue: prev?.v, by: d.by });
|
|
108
|
+
flat.set(k, row.v);
|
|
109
|
+
this.rows.set(k, row);
|
|
110
|
+
}
|
|
111
|
+
for (const k of d.removed) {
|
|
112
|
+
flat.delete(k);
|
|
113
|
+
const old = this.rows.get(k);
|
|
114
|
+
if (old) {
|
|
115
|
+
this.rows.delete(k);
|
|
116
|
+
records.push({ kind: "delete", key: k, previousValue: old.v, by: d.by });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
this.flat = flat;
|
|
120
|
+
this.version = d.version;
|
|
121
|
+
this.#emit(records);
|
|
122
|
+
}
|
|
123
|
+
#clear() {
|
|
124
|
+
const records = [...this.rows].map(([key, r]) => ({ kind: "delete", key, previousValue: r.v, by: undefined }));
|
|
125
|
+
this.rows = new Map();
|
|
126
|
+
this.flat = new Map();
|
|
127
|
+
this.#emit(records);
|
|
128
|
+
}
|
|
129
|
+
/** Nothing of the incarnation is known until the runtime answers the next open. */
|
|
130
|
+
reset() {
|
|
131
|
+
if (this.ended !== undefined)
|
|
132
|
+
return;
|
|
133
|
+
this.#clear();
|
|
134
|
+
this.epoch = undefined;
|
|
135
|
+
this.version = 0;
|
|
136
|
+
this.loaded = false;
|
|
137
|
+
this.gap = false;
|
|
138
|
+
this.mode = undefined;
|
|
139
|
+
this.owner = undefined;
|
|
140
|
+
this.mine = false;
|
|
141
|
+
this.claim = undefined;
|
|
142
|
+
this.minRole = undefined;
|
|
143
|
+
this.onDeath = undefined;
|
|
144
|
+
this.absent = false;
|
|
145
|
+
this.#notify();
|
|
146
|
+
}
|
|
147
|
+
opened(a) {
|
|
148
|
+
if (this.ended !== undefined)
|
|
149
|
+
return;
|
|
150
|
+
this.epoch = undefined;
|
|
151
|
+
this.version = 0;
|
|
152
|
+
this.loaded = false;
|
|
153
|
+
this.gap = false;
|
|
154
|
+
this.mode = a?.mode;
|
|
155
|
+
this.owner = a?.owner ?? undefined;
|
|
156
|
+
this.mine = a?.mine ?? false;
|
|
157
|
+
this.claim = a?.claim;
|
|
158
|
+
this.minRole = a?.min_role;
|
|
159
|
+
this.onDeath = a?.on_death;
|
|
160
|
+
this.absent = a === undefined;
|
|
161
|
+
this.#notify();
|
|
162
|
+
}
|
|
163
|
+
end(reason) {
|
|
164
|
+
if (this.ended !== undefined)
|
|
165
|
+
return;
|
|
166
|
+
this.#clear();
|
|
167
|
+
this.ended = reason;
|
|
168
|
+
this.absent = false;
|
|
169
|
+
for (const w of this.#watchers)
|
|
170
|
+
void w.close();
|
|
171
|
+
this.#watchers.clear();
|
|
172
|
+
this.#notify();
|
|
173
|
+
}
|
|
174
|
+
setPending(n) { this.pending = n; this.#notify(); }
|
|
175
|
+
/** The status a handle reports: a closed handle (`closed`) has ended (`client`) whatever the namespace does. */
|
|
176
|
+
status(nodeConnected, closed = false) {
|
|
177
|
+
const ended = closed ? ENDED_CLIENT : this.ended;
|
|
178
|
+
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 };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function sameValue(a, b) {
|
|
183
|
+
if (a === b)
|
|
184
|
+
return true;
|
|
185
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array)
|
|
186
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
187
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b)
|
|
188
|
+
return false;
|
|
189
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
190
|
+
return false;
|
|
191
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
192
|
+
if (ka.length !== kb.length)
|
|
193
|
+
return false;
|
|
194
|
+
for (const k of ka)
|
|
195
|
+
if (!sameValue(a[k], b[k]))
|
|
196
|
+
return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/** The namespace shared by its handles and its task. */
|
|
200
|
+
export class Namespace {
|
|
201
|
+
name;
|
|
202
|
+
opts;
|
|
203
|
+
node;
|
|
204
|
+
warn;
|
|
205
|
+
state = new State();
|
|
206
|
+
mailbox = new AsyncQueue(Infinity);
|
|
207
|
+
exited = deferred();
|
|
208
|
+
handles = 0;
|
|
209
|
+
constructor(name, opts, node, warn) {
|
|
210
|
+
this.name = name;
|
|
211
|
+
this.opts = opts;
|
|
212
|
+
this.node = node;
|
|
213
|
+
this.warn = warn;
|
|
214
|
+
}
|
|
215
|
+
post(e) { return this.mailbox.push(e); }
|
|
216
|
+
request(make) {
|
|
217
|
+
const reply = deferred();
|
|
218
|
+
if (!this.post(make(reply)))
|
|
219
|
+
return Promise.reject(gone(this.name, "the handle ended"));
|
|
220
|
+
return reply.promise;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export function startNamespace(ns, registry) { void run(ns, registry); }
|
|
224
|
+
async function run(ns, registry) {
|
|
225
|
+
const { name, opts, state, warn, node } = ns;
|
|
226
|
+
const post = (e) => { ns.post(e); };
|
|
227
|
+
const config = name === STORE_CONFIG;
|
|
228
|
+
let session;
|
|
229
|
+
let phase = { kind: "idle" };
|
|
230
|
+
const ph = () => phase;
|
|
231
|
+
let attempts = 0;
|
|
232
|
+
let refusals = 0;
|
|
233
|
+
let destroying = 0;
|
|
234
|
+
/** The version the open reply named: the value is "in" (a `join` answered) once the mirror has reached it. */
|
|
235
|
+
let wanted = 0;
|
|
236
|
+
/** The active project as last known (project namespaces): `undefined` = no snapshot yet. */
|
|
237
|
+
let known;
|
|
238
|
+
let unwatchProject;
|
|
239
|
+
const joining = [];
|
|
240
|
+
let versionWaiters = [];
|
|
241
|
+
const leaveIncarnation = () => { for (const w of versionWaiters.splice(0))
|
|
242
|
+
w.reply.reject(gone(name, `the handle left the incarnation before version ${w.version}`)); };
|
|
243
|
+
const stopPhase = () => {
|
|
244
|
+
if (phase.kind === "following")
|
|
245
|
+
phase.stop.abort();
|
|
246
|
+
else if (phase.kind === "paced")
|
|
247
|
+
clearTimeout(phase.timer);
|
|
248
|
+
else if (phase.kind === "opening")
|
|
249
|
+
phase.o.announced.close();
|
|
250
|
+
};
|
|
251
|
+
const idle = () => { stopPhase(); phase = { kind: "idle" }; };
|
|
252
|
+
/** Writes the open on the task's session, from `idle`, when nothing holds it back. */
|
|
253
|
+
const open = () => {
|
|
254
|
+
if (phase.kind !== "idle" || destroying > 0 || state.ended !== undefined || !session)
|
|
255
|
+
return;
|
|
256
|
+
const s = session;
|
|
257
|
+
leaveIncarnation();
|
|
258
|
+
attempts += 1;
|
|
259
|
+
const attempt = attempts;
|
|
260
|
+
let announced;
|
|
261
|
+
try {
|
|
262
|
+
announced = s.sub.announced(Moq.Path.Pattern.parse(`${storeBasePath(name)}/*`));
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
warn(`store ${name}: announcement cursor refused: ${e instanceof Error ? e.message : String(e)}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const args = { namespace: name };
|
|
269
|
+
if (opts.access)
|
|
270
|
+
args.access = opts.access;
|
|
271
|
+
if (opts.onDeath)
|
|
272
|
+
args.on_death = opts.onDeath;
|
|
273
|
+
try {
|
|
274
|
+
const run = startOn(node, s, "synapse/store/open", args, {});
|
|
275
|
+
phase = { kind: "opening", o: { attempt, session: s, announced, again: undefined } };
|
|
276
|
+
void run.result.then(v => post({ kind: "opened", attempt, result: { ok: v } }), (e) => post({ kind: "opened", attempt, result: { err: e instanceof SynapseError ? e : new SynapseError("internal", String(e)) } }));
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
announced.close(); /* the session is going: its end is on its way */
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/** The intent is asked again after `paceMs`: the mirror resets. With an open outstanding, once that is answered. */
|
|
283
|
+
const again = (paceMs) => {
|
|
284
|
+
state.reset();
|
|
285
|
+
if (phase.kind === "opening") {
|
|
286
|
+
phase.o.again = paceMs;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
stopPhase();
|
|
290
|
+
const a = attempts;
|
|
291
|
+
phase = { kind: "paced", timer: setTimeout(() => post({ kind: "paced", attempt: a }), paceMs) };
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* The node's session is now `s` (or none). The reader of the previous session is stopped and its end
|
|
295
|
+
* awaited before the mirror resets, so nothing of it lands on the next incarnation; a final frame it
|
|
296
|
+
* had already been handed is the runtime's statement: `destroyed` ends the handle, returned as the
|
|
297
|
+
* reason (no open is written).
|
|
298
|
+
*/
|
|
299
|
+
const sessionChanged = async (s) => {
|
|
300
|
+
if (phase.kind === "following") {
|
|
301
|
+
const { stop, done } = phase;
|
|
302
|
+
stop.abort();
|
|
303
|
+
const r = await done;
|
|
304
|
+
if (r !== undefined && isFinal(r))
|
|
305
|
+
return r;
|
|
306
|
+
}
|
|
307
|
+
session = s;
|
|
308
|
+
idle();
|
|
309
|
+
leaveIncarnation();
|
|
310
|
+
state.reset();
|
|
311
|
+
unwatchProject?.();
|
|
312
|
+
unwatchProject = undefined;
|
|
313
|
+
if (s && !config) {
|
|
314
|
+
const feed = s.project;
|
|
315
|
+
const current = feed.get();
|
|
316
|
+
if (known === undefined)
|
|
317
|
+
known = current ? current.active : undefined;
|
|
318
|
+
else if (current)
|
|
319
|
+
post({ kind: "projectChanged", now: current.active });
|
|
320
|
+
unwatchProject = feed.subscribe(p => { if (p)
|
|
321
|
+
post({ kind: "projectChanged", now: p.active }); });
|
|
322
|
+
}
|
|
323
|
+
open();
|
|
324
|
+
return undefined;
|
|
325
|
+
};
|
|
326
|
+
const opened = (attempt, result) => {
|
|
327
|
+
if (phase.kind !== "opening" || phase.o.attempt !== attempt)
|
|
328
|
+
return;
|
|
329
|
+
const { announced, again: due, session: s } = phase.o;
|
|
330
|
+
phase = { kind: "idle" };
|
|
331
|
+
if ("err" in result && (result.err.code === "connection_lost" || result.err.code === "not_connected")) {
|
|
332
|
+
announced.close();
|
|
333
|
+
session = undefined;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (due !== undefined) {
|
|
337
|
+
announced.close();
|
|
338
|
+
const a = attempts;
|
|
339
|
+
phase = { kind: "paced", timer: setTimeout(() => post({ kind: "paced", attempt: a }), due) };
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if ("ok" in result) {
|
|
343
|
+
let access;
|
|
344
|
+
try {
|
|
345
|
+
access = parseOpen(result.ok);
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
announced.close();
|
|
349
|
+
warn(`store ${name}: open reply not understood (${e instanceof Error ? e.message : String(e)}); asked again`);
|
|
350
|
+
again(REFOLLOW_PACE_MS);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
refusals = 0;
|
|
354
|
+
state.opened(access);
|
|
355
|
+
wanted = access?.version ?? 0;
|
|
356
|
+
const stop = new AbortController();
|
|
357
|
+
const done = follow(ns, s, announced, access?.epoch, stop.signal);
|
|
358
|
+
phase = { kind: "following", attempt, stop, done };
|
|
359
|
+
void done.then(reason => post({ kind: "followEnded", attempt, reason }));
|
|
360
|
+
if (!access)
|
|
361
|
+
for (const j of joining.splice(0))
|
|
362
|
+
j.resolve();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
announced.close();
|
|
366
|
+
const e = result.err;
|
|
367
|
+
if (isTransient(e)) {
|
|
368
|
+
again(REFOLLOW_PACE_MS);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (e.code === "project_inactive") {
|
|
372
|
+
for (const j of joining.splice(0))
|
|
373
|
+
j.reject(e);
|
|
374
|
+
phase = { kind: "waitingProject", before: known };
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
refusals += 1;
|
|
378
|
+
const pace = refusalPaceMs(refusals);
|
|
379
|
+
warn(`store ${name}: open refused by the runtime (${e.code}${e.message ? `: ${e.message}` : ""}; holder ${holderOf(e) ?? "-"}; attempt ${refusals}; again in ${pace} ms)`);
|
|
380
|
+
again(pace);
|
|
381
|
+
};
|
|
382
|
+
const applied = (epoch, version) => {
|
|
383
|
+
const reached = versionWaiters.filter(w => w.epoch === epoch && w.version <= version);
|
|
384
|
+
versionWaiters = versionWaiters.filter(w => !reached.includes(w));
|
|
385
|
+
for (const w of reached)
|
|
386
|
+
w.reply.resolve();
|
|
387
|
+
// In = the mirror holds what the runtime named at the open (its version), not just a first snapshot.
|
|
388
|
+
if (state.loaded && state.epoch === epoch && state.version >= wanted)
|
|
389
|
+
for (const j of joining.splice(0))
|
|
390
|
+
j.resolve();
|
|
391
|
+
};
|
|
392
|
+
const join = (reply) => {
|
|
393
|
+
if (phase.kind === "waitingProject") {
|
|
394
|
+
reply.reject(new SynapseError("project_inactive"));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (state.absent || (state.loaded && state.version >= wanted))
|
|
398
|
+
reply.resolve();
|
|
399
|
+
else
|
|
400
|
+
joining.push(reply);
|
|
401
|
+
};
|
|
402
|
+
const waitVersion = (epoch, version, reply) => {
|
|
403
|
+
if (state.ended !== undefined || state.epoch !== epoch) {
|
|
404
|
+
reply.reject(gone(name, `the handle left the incarnation before version ${version}`));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (state.version >= version) {
|
|
408
|
+
reply.resolve();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
versionWaiters.push({ epoch, version, reply });
|
|
412
|
+
};
|
|
413
|
+
const projectChanged = (now) => {
|
|
414
|
+
if (phase.kind === "waitingProject") {
|
|
415
|
+
if (!sameProject(now, phase.before)) {
|
|
416
|
+
known = now;
|
|
417
|
+
phase = { kind: "idle" };
|
|
418
|
+
open();
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (known === undefined || !sameProject(known, now)) {
|
|
423
|
+
known = now;
|
|
424
|
+
if (phase.kind === "opening" || phase.kind === "following")
|
|
425
|
+
again(REFOLLOW_PACE_MS);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const write = (op, args, reply) => {
|
|
429
|
+
if (state.ended !== undefined) {
|
|
430
|
+
reply.reject(gone(name, `the handle ended (${state.ended})`));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const epoch = state.epoch;
|
|
434
|
+
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"));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (!session) {
|
|
439
|
+
reply.reject(new SynapseError("not_connected"));
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const w = { namespace: name, epoch };
|
|
443
|
+
if (state.claim !== undefined)
|
|
444
|
+
w.claim = state.claim;
|
|
445
|
+
if (args.key !== undefined)
|
|
446
|
+
w.key = args.key;
|
|
447
|
+
if (args.value !== undefined)
|
|
448
|
+
w.value = args.value;
|
|
449
|
+
if (args.values !== undefined)
|
|
450
|
+
w.values = args.values;
|
|
451
|
+
let run;
|
|
452
|
+
try {
|
|
453
|
+
run = startOn(node, session, `synapse/store/${op}`, w, {});
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
reply.reject(e instanceof SynapseError ? e : new SynapseError("internal", String(e)));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (op === "destroy")
|
|
460
|
+
destroying += 1;
|
|
461
|
+
state.setPending(state.pending + 1);
|
|
462
|
+
void run.result.then(v => post({ kind: "writeDone", op, result: { ok: v }, reply }), (e) => post({ kind: "writeDone", op, result: { err: e instanceof SynapseError ? e : new SynapseError("internal", String(e)) }, reply }));
|
|
463
|
+
};
|
|
464
|
+
/** `true` when it ends the handle (a confirmed destroy). */
|
|
465
|
+
const writeDone = (op, result, reply) => {
|
|
466
|
+
state.setPending(state.pending - 1);
|
|
467
|
+
const destroyed = op === "destroy" && "ok" in result;
|
|
468
|
+
if (op === "destroy")
|
|
469
|
+
destroying -= 1;
|
|
470
|
+
if (destroyed)
|
|
471
|
+
state.end(ENDED_DESTROYED);
|
|
472
|
+
if ("ok" in result)
|
|
473
|
+
reply.resolve(result.ok);
|
|
474
|
+
else
|
|
475
|
+
reply.reject(result.err);
|
|
476
|
+
if (!destroyed)
|
|
477
|
+
open();
|
|
478
|
+
return destroyed;
|
|
479
|
+
};
|
|
480
|
+
const endingError = () => state.ended === undefined || state.ended === ENDED_CONNECTION_LOST
|
|
481
|
+
? new SynapseError("connection_lost", `store ${JSON.stringify(name)}: the node ended before the namespace was read`)
|
|
482
|
+
: gone(name, `the handle ended before the namespace was read (${state.ended})`);
|
|
483
|
+
const { session: first, unmonitor } = node.monitor((e) => ns.post(e.kind === "started" ? { kind: "session", session: e.session } : e.kind === "ended" ? { kind: "sessionEnded" } : { kind: "nodeEnded" }));
|
|
484
|
+
let reason = null; // null: no reason (a destroy ended it)
|
|
485
|
+
try {
|
|
486
|
+
await sessionChanged(first); // no reader yet: it cannot end the handle
|
|
487
|
+
loop: for (;;) {
|
|
488
|
+
const ev = await ns.mailbox.next();
|
|
489
|
+
if (!ev) {
|
|
490
|
+
reason = undefined;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
switch (ev.kind) {
|
|
494
|
+
case "session":
|
|
495
|
+
case "sessionEnded": {
|
|
496
|
+
const r = await sessionChanged(ev.kind === "session" ? ev.session : undefined);
|
|
497
|
+
if (r !== undefined) {
|
|
498
|
+
reason = r;
|
|
499
|
+
break loop;
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case "nodeEnded":
|
|
504
|
+
reason = ENDED_CONNECTION_LOST;
|
|
505
|
+
break loop;
|
|
506
|
+
case "opened":
|
|
507
|
+
opened(ev.attempt, ev.result);
|
|
508
|
+
break;
|
|
509
|
+
case "applied":
|
|
510
|
+
applied(ev.epoch, ev.version);
|
|
511
|
+
break;
|
|
512
|
+
case "followEnded": {
|
|
513
|
+
// A final frame is the runtime's statement about the namespace: `destroyed` ends the handle,
|
|
514
|
+
// whichever reader (current or already stopped) was handed it; every other end of the current
|
|
515
|
+
// reader replays the intent, and of a stopped one is already dealt with.
|
|
516
|
+
if (ev.reason !== undefined && isFinal(ev.reason)) {
|
|
517
|
+
reason = ev.reason;
|
|
518
|
+
break loop;
|
|
519
|
+
}
|
|
520
|
+
{
|
|
521
|
+
const p = ph();
|
|
522
|
+
if (p.kind !== "following" || p.attempt !== ev.attempt)
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
phase = { kind: "idle" };
|
|
526
|
+
again(REFOLLOW_PACE_MS);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "join":
|
|
530
|
+
join(ev.reply);
|
|
531
|
+
break;
|
|
532
|
+
case "waitVersion":
|
|
533
|
+
waitVersion(ev.epoch, ev.version, ev.reply);
|
|
534
|
+
break;
|
|
535
|
+
case "write":
|
|
536
|
+
write(ev.op, ev.args, ev.reply);
|
|
537
|
+
break;
|
|
538
|
+
case "writeDone":
|
|
539
|
+
if (writeDone(ev.op, ev.result, ev.reply)) {
|
|
540
|
+
reason = undefined;
|
|
541
|
+
break loop;
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
case "projectChanged":
|
|
545
|
+
if (!config)
|
|
546
|
+
projectChanged(ev.now);
|
|
547
|
+
break;
|
|
548
|
+
case "paced":
|
|
549
|
+
if (ph().kind === "paced" && ev.attempt === attempts) {
|
|
550
|
+
phase = { kind: "idle" };
|
|
551
|
+
open();
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
case "close":
|
|
555
|
+
reason = ENDED_CLIENT;
|
|
556
|
+
break loop;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
unmonitor();
|
|
562
|
+
unwatchProject?.();
|
|
563
|
+
if (reason !== null && reason !== undefined)
|
|
564
|
+
state.end(reason);
|
|
565
|
+
const e = endingError();
|
|
566
|
+
for (const j of joining.splice(0))
|
|
567
|
+
j.reject(e);
|
|
568
|
+
leaveIncarnation();
|
|
569
|
+
{
|
|
570
|
+
const p = ph();
|
|
571
|
+
if (p.kind === "following" || p.kind === "paced")
|
|
572
|
+
idle();
|
|
573
|
+
}
|
|
574
|
+
// Every write in flight is answered to its caller before the task ends; an open in flight is waited out.
|
|
575
|
+
while (state.pending > 0 || ph().kind === "opening") {
|
|
576
|
+
const ev = await ns.mailbox.next();
|
|
577
|
+
if (!ev)
|
|
578
|
+
break;
|
|
579
|
+
if (ev.kind === "writeDone")
|
|
580
|
+
writeDone(ev.op, ev.result, ev.reply);
|
|
581
|
+
else if (ev.kind === "write")
|
|
582
|
+
write(ev.op, ev.args, ev.reply);
|
|
583
|
+
else if (ev.kind === "opened") {
|
|
584
|
+
const p = ph();
|
|
585
|
+
if (p.kind === "opening" && p.o.attempt === ev.attempt) {
|
|
586
|
+
p.o.announced.close();
|
|
587
|
+
phase = { kind: "idle" };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else if (ev.kind === "join")
|
|
591
|
+
ev.reply.reject(e);
|
|
592
|
+
else if (ev.kind === "waitVersion")
|
|
593
|
+
ev.reply.reject(gone(name, "the handle ended"));
|
|
594
|
+
}
|
|
595
|
+
if (state.ended === undefined)
|
|
596
|
+
state.end(reason ?? ENDED_CLIENT);
|
|
597
|
+
// The mailbox takes nothing more (a request now fails at once); what is still queued is answered.
|
|
598
|
+
void ns.mailbox.close();
|
|
599
|
+
for (let ev = ns.mailbox.tryNext(); ev; ev = ns.mailbox.tryNext()) {
|
|
600
|
+
if (ev.kind === "write")
|
|
601
|
+
ev.reply.reject(gone(name, `the handle ended (${state.ended})`));
|
|
602
|
+
else if (ev.kind === "join")
|
|
603
|
+
ev.reply.reject(e);
|
|
604
|
+
else if (ev.kind === "waitVersion")
|
|
605
|
+
ev.reply.reject(gone(name, "the handle ended"));
|
|
606
|
+
}
|
|
607
|
+
if (registry.get(name) === ns)
|
|
608
|
+
registry.delete(name);
|
|
609
|
+
ns.exited.resolve();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const sameProject = (a, b) => (a === b) || (!!a && !!b && a.id === b.id && a.since_us === b.since_us);
|
|
613
|
+
/** An open's reply: `undefined` = absent. */
|
|
614
|
+
function parseOpen(reply) {
|
|
615
|
+
if (!reply || typeof reply !== "object")
|
|
616
|
+
throw new Error("not a map");
|
|
617
|
+
const r = reply;
|
|
618
|
+
if (r.absent === true)
|
|
619
|
+
return undefined;
|
|
620
|
+
if (typeof r.epoch !== "string" || typeof r.mode !== "string")
|
|
621
|
+
throw new Error("no epoch/mode");
|
|
622
|
+
return r;
|
|
623
|
+
}
|
|
624
|
+
/** The epoch of an announced prefix under the namespace's base path (`<base>/@<epoch>`). */
|
|
625
|
+
function announcedEpoch(base, prefix) {
|
|
626
|
+
if (!prefix.startsWith(`${base}/@`))
|
|
627
|
+
return undefined;
|
|
628
|
+
const e = prefix.slice(base.length + 2);
|
|
629
|
+
return e && !e.includes("/") ? e : undefined;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Reads the incarnation the open bound (`want`; `undefined`: absent, the first one announced), writing
|
|
633
|
+
* the mirror and telling the task of each frame applied; returns how the track ended (`undefined`:
|
|
634
|
+
* no final frame — the session, or the broker, went; the task replays).
|
|
635
|
+
*/
|
|
636
|
+
async function follow(ns, session, announced, want, signal) {
|
|
637
|
+
const { state, warn, name } = ns;
|
|
638
|
+
const base = storeBasePath(name);
|
|
639
|
+
const onAbort = () => announced.close();
|
|
640
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
641
|
+
let sub;
|
|
642
|
+
try {
|
|
643
|
+
// The incarnation: ours, or, absent, the first announced. One the reply named has a bound to show up on the cursor (an announce/retract pair the cursor never saw is gone for good): then the runtime is asked again.
|
|
644
|
+
let epoch;
|
|
645
|
+
const deadline = want !== undefined ? setTimeout(() => announced.close(), ANNOUNCE_WITHIN_MS) : undefined;
|
|
646
|
+
try {
|
|
647
|
+
for (;;) {
|
|
648
|
+
const u = await announced.next();
|
|
649
|
+
if (!u)
|
|
650
|
+
return undefined;
|
|
651
|
+
if (u.kind === "retracted")
|
|
652
|
+
continue;
|
|
653
|
+
const e = announcedEpoch(base, String(u.prefix));
|
|
654
|
+
if (e !== undefined && (want === undefined || want === e)) {
|
|
655
|
+
epoch = e;
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
if (deadline !== undefined)
|
|
662
|
+
clearTimeout(deadline);
|
|
663
|
+
}
|
|
664
|
+
// Resolving the broadcast races an end of this epoch already on the cursor.
|
|
665
|
+
const req = session.request(`${base}/@${epoch}`);
|
|
666
|
+
const retracted = (async () => { for (;;) {
|
|
667
|
+
const u = await announced.next();
|
|
668
|
+
if (!u)
|
|
669
|
+
return false;
|
|
670
|
+
if (u.kind === "retracted" && announcedEpoch(base, String(u.prefix)) === epoch)
|
|
671
|
+
return true;
|
|
672
|
+
} })();
|
|
673
|
+
let bc;
|
|
674
|
+
try {
|
|
675
|
+
const r = await Promise.race([waitActive(req, 10_000, signal).then(b => ({ bc: b })), retracted.then(r => ({ gone: r }))]);
|
|
676
|
+
if ("gone" in r) {
|
|
677
|
+
req.close();
|
|
678
|
+
return r.gone ? ENDED_STORE_GONE : undefined;
|
|
679
|
+
}
|
|
680
|
+
bc = r.bc;
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
req.close();
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
announced.close();
|
|
687
|
+
sub = bc.track(STORE_TRACK).subscribe(latestSubscription(moqPriority(1)));
|
|
688
|
+
const s = sub;
|
|
689
|
+
// Closing the subscriber ends `recvGroup`, not a read on the group it already handed us: that group is closed too.
|
|
690
|
+
let g;
|
|
691
|
+
signal.addEventListener("abort", () => { s.close(); g?.close(); }, { once: true });
|
|
692
|
+
// The first group read is the open one, from its start: a snapshot, then the diffs.
|
|
693
|
+
for (;;) {
|
|
694
|
+
g = await s.recvGroup();
|
|
695
|
+
if (!g)
|
|
696
|
+
break;
|
|
697
|
+
for (;;) {
|
|
698
|
+
let f;
|
|
699
|
+
try {
|
|
700
|
+
f = await g.readFrame();
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
if (!f)
|
|
706
|
+
break;
|
|
707
|
+
let header, hlen;
|
|
708
|
+
try {
|
|
709
|
+
({ header, hlen } = decodeHeader(f.payload));
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
warn(`store ${name}: frame not decodable`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
let body;
|
|
716
|
+
try {
|
|
717
|
+
body = decodeBody(f.payload.subarray(hlen));
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
warn(`store ${name}: frame body not decodable`);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (header.flags & FLAGS.END) {
|
|
724
|
+
const fin = body;
|
|
725
|
+
return fin && typeof fin.reason === "string" ? fin.reason : ENDED_STORE_GONE;
|
|
726
|
+
}
|
|
727
|
+
// A stopped reader no longer writes the mirror: it belongs to the next incarnation (or to none).
|
|
728
|
+
if (header.flags & FLAGS.KEY) {
|
|
729
|
+
const snap = body;
|
|
730
|
+
if (!snap || snap.epoch !== epoch) {
|
|
731
|
+
warn(`store ${name}: snapshot of another epoch dropped`);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (signal.aborted)
|
|
735
|
+
continue;
|
|
736
|
+
if (state.ended === undefined)
|
|
737
|
+
state.applySnapshot(snap);
|
|
738
|
+
ns.post({ kind: "applied", epoch, version: snap.version });
|
|
739
|
+
}
|
|
740
|
+
else if (header.flags & FLAGS.DELTA) {
|
|
741
|
+
const diff = body;
|
|
742
|
+
if (!diff || typeof diff.version !== "number") {
|
|
743
|
+
warn(`store ${name}: diff not decodable`);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
if (signal.aborted)
|
|
747
|
+
continue;
|
|
748
|
+
if (state.ended === undefined && state.loaded)
|
|
749
|
+
state.applyDiff(diff, warn);
|
|
750
|
+
ns.post({ kind: "applied", epoch, version: diff.version });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return undefined; // the track ended with no final frame: the session (or the broker) went
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
finally {
|
|
760
|
+
signal.removeEventListener("abort", onAbort);
|
|
761
|
+
try {
|
|
762
|
+
sub?.close();
|
|
763
|
+
}
|
|
764
|
+
catch { /* closed */ }
|
|
765
|
+
announced.close();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// ------------------------------------------------------------------------------------ handle
|
|
769
|
+
/** The application's handle on a namespace: the last one closed ends the task. */
|
|
770
|
+
export class StoreHandleImpl {
|
|
771
|
+
ns;
|
|
772
|
+
#closed = false;
|
|
773
|
+
/** Resolved when this handle closes: its version waits end. */
|
|
774
|
+
#left = deferred();
|
|
775
|
+
/** This handle's change feeds: closed with it. */
|
|
776
|
+
#feeds = new Set();
|
|
777
|
+
#valueObj;
|
|
778
|
+
#valueOf;
|
|
779
|
+
constructor(ns) {
|
|
780
|
+
this.ns = ns;
|
|
781
|
+
ns.handles++;
|
|
782
|
+
}
|
|
783
|
+
#ended() { return gone(this.ns.name, `the handle ended (${ENDED_CLIENT})`); }
|
|
784
|
+
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() {
|
|
787
|
+
const flat = this.ns.state.flat;
|
|
788
|
+
if (this.#valueOf !== flat) {
|
|
789
|
+
this.#valueOf = flat;
|
|
790
|
+
this.#valueObj = Object.fromEntries(flat);
|
|
791
|
+
}
|
|
792
|
+
return this.#valueObj;
|
|
793
|
+
}
|
|
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. */
|
|
798
|
+
async set(keyOrValues, value) {
|
|
799
|
+
const v = typeof keyOrValues === "string" ? await this.#write("put", { key: keyOrValues, value }) : await this.#write("put_many", { values: keyOrValues });
|
|
800
|
+
await this.#reached(v);
|
|
801
|
+
}
|
|
802
|
+
async delete(key) { await this.#reached(await this.#write("delete", { key })); }
|
|
803
|
+
async clear() { await this.#reached(await this.#write("clear", {})); }
|
|
804
|
+
async destroy() { await this.#write("destroy", {}); }
|
|
805
|
+
/** The wire half of a write: the runtime's answer, the version it assigned. */
|
|
806
|
+
async #write(op, args) {
|
|
807
|
+
if (this.#closed)
|
|
808
|
+
throw this.#ended();
|
|
809
|
+
const r = await this.ns.request(reply => ({ kind: "write", op, args, reply }));
|
|
810
|
+
const v = r?.version;
|
|
811
|
+
return typeof v === "number" ? v : typeof v === "bigint" ? Number(v) : 0;
|
|
812
|
+
}
|
|
813
|
+
/** The echo half: the mirror has applied `version` of the bound incarnation (`store_gone` if it changes first). */
|
|
814
|
+
#reached(version) {
|
|
815
|
+
const epoch = this.ns.state.epoch;
|
|
816
|
+
if (this.#closed)
|
|
817
|
+
return Promise.reject(this.#ended());
|
|
818
|
+
if (epoch === undefined)
|
|
819
|
+
return Promise.reject(gone(this.ns.name, `no incarnation is bound (the write of version ${version} may have landed on the previous one)`));
|
|
820
|
+
const waited = this.ns.request(reply => ({ kind: "waitVersion", epoch, version, reply }));
|
|
821
|
+
return Promise.race([waited, this.#left.promise.then(() => { throw this.#ended(); })]);
|
|
822
|
+
}
|
|
823
|
+
/** `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. */
|
|
824
|
+
async *watch(key) {
|
|
825
|
+
if (this.#closed)
|
|
826
|
+
return;
|
|
827
|
+
const q = this.ns.state.watch();
|
|
828
|
+
this.#feeds.add(q);
|
|
829
|
+
try {
|
|
830
|
+
for await (const batch of q) {
|
|
831
|
+
if (key === undefined)
|
|
832
|
+
yield this.value;
|
|
833
|
+
else if (batch.some(c => c.key === key))
|
|
834
|
+
yield this.ns.state.flat.get(key);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
finally {
|
|
838
|
+
this.#feeds.delete(q);
|
|
839
|
+
this.ns.state.unwatch(q);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
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); }
|
|
844
|
+
/** Waits for the namespace to be in (the first snapshot applied, or absent). */
|
|
845
|
+
join() { return this.ns.request(reply => ({ kind: "join", reply })); }
|
|
846
|
+
close() {
|
|
847
|
+
if (this.#closed)
|
|
848
|
+
return;
|
|
849
|
+
this.#closed = true;
|
|
850
|
+
this.#left.resolve();
|
|
851
|
+
for (const q of this.#feeds)
|
|
852
|
+
this.ns.state.unwatch(q);
|
|
853
|
+
this.#feeds.clear();
|
|
854
|
+
if (--this.ns.handles === 0)
|
|
855
|
+
this.ns.post({ kind: "close" });
|
|
856
|
+
}
|
|
857
|
+
async [Symbol.asyncDispose]() { this.close(); }
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* `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.
|
|
862
|
+
*/
|
|
863
|
+
export async function openStore(node, registry, name, opts, warn) {
|
|
864
|
+
if (!node.currentSession())
|
|
865
|
+
throw new SynapseError("not_connected");
|
|
866
|
+
for (;;) {
|
|
867
|
+
const live = registry.get(name);
|
|
868
|
+
if (live) {
|
|
869
|
+
if (live.handles === 0) {
|
|
870
|
+
await live.exited.promise;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (live.opts.access !== opts.access || live.opts.onDeath !== opts.onDeath)
|
|
874
|
+
throw new SynapseError("invalid_input", `store ${JSON.stringify(name)}: this node already has it open with other options`);
|
|
875
|
+
const h = new StoreHandleImpl(live);
|
|
876
|
+
try {
|
|
877
|
+
await h.join();
|
|
878
|
+
}
|
|
879
|
+
catch (e) {
|
|
880
|
+
h.close();
|
|
881
|
+
throw e;
|
|
882
|
+
}
|
|
883
|
+
return h;
|
|
884
|
+
}
|
|
885
|
+
const ns = new Namespace(name, opts, node, warn);
|
|
886
|
+
registry.set(name, ns);
|
|
887
|
+
const h = new StoreHandleImpl(ns);
|
|
888
|
+
startNamespace(ns, registry);
|
|
889
|
+
try {
|
|
890
|
+
await h.join();
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
h.close();
|
|
894
|
+
throw e;
|
|
895
|
+
}
|
|
896
|
+
return h;
|
|
897
|
+
}
|
|
898
|
+
}
|