entangld 2.6.0 → 2.7.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/.github/workflows/mocha-tests.yml +1 -1
- package/API.md +754 -0
- package/README.md +212 -799
- package/docs/protocol.md +484 -0
- package/generate_docs.js +3 -3
- package/lib/Datastore.js +142 -19
- package/lib/Message.js +9 -10
- package/lib/Subscription.js +23 -0
- package/lib/error.js +15 -0
- package/package.json +1 -1
- package/test/signals.js +590 -0
- package/test/sockets.js +7 -2
- package/test/tests.js +10 -0
- package/test/timeouts.js +173 -0
- package/.gitmodules +0 -3
- package/README.hbs +0 -190
- package/tags +0 -244
package/test/signals.js
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
const assert = require("assert");
|
|
2
|
+
const { getEventListeners } = require("events");
|
|
3
|
+
const Entangld = require("../lib/Datastore.js");
|
|
4
|
+
const { EntangldError, EntangldTimeoutError } = require("../lib/error.js");
|
|
5
|
+
|
|
6
|
+
const listeners = (signal) => getEventListeners(signal, "abort").length;
|
|
7
|
+
|
|
8
|
+
// Resolve to the error a promise rejects with, or fail if it resolves
|
|
9
|
+
function rejection(promise) {
|
|
10
|
+
return promise.then(
|
|
11
|
+
(val) => assert.fail(`Expected a rejection, got ${val}`),
|
|
12
|
+
(err) => err
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Attach store b to store a. Messages from a are delivered only if allow(msg)
|
|
17
|
+
function linked_stores(allow = () => true) {
|
|
18
|
+
const a = new Entangld();
|
|
19
|
+
const b = new Entangld();
|
|
20
|
+
|
|
21
|
+
a.transmit((msg, store) => {
|
|
22
|
+
if (allow(msg)) store.receive(msg, a);
|
|
23
|
+
});
|
|
24
|
+
b.transmit((msg, store) => store.receive(msg, b));
|
|
25
|
+
|
|
26
|
+
a.attach("b", b);
|
|
27
|
+
b.attach("a", a);
|
|
28
|
+
|
|
29
|
+
return { a, b };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Signals", () => {
|
|
33
|
+
|
|
34
|
+
describe("get", () => {
|
|
35
|
+
|
|
36
|
+
it("rejects with the reason when a remote get is aborted", async () => {
|
|
37
|
+
const { a } = linked_stores(() => false);
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const reason = new Error("gave up");
|
|
40
|
+
|
|
41
|
+
const request = a.get("b.value", undefined, { signal: controller.signal });
|
|
42
|
+
controller.abort(reason);
|
|
43
|
+
|
|
44
|
+
assert.strictEqual(await rejection(request), reason);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("removes the request after aborting", async () => {
|
|
48
|
+
const { a } = linked_stores(() => false);
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
|
|
51
|
+
const request = a.get("b.value", undefined, { signal: controller.signal });
|
|
52
|
+
controller.abort();
|
|
53
|
+
await rejection(request);
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(Object.keys(a._requests).length, 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ignores a reply which arrives after aborting", async () => {
|
|
59
|
+
const { a, b } = linked_stores(() => false);
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const sent = [];
|
|
62
|
+
a.transmit((msg) => sent.push(msg));
|
|
63
|
+
|
|
64
|
+
const request = a.get("b.value", undefined, { signal: controller.signal });
|
|
65
|
+
controller.abort();
|
|
66
|
+
await rejection(request);
|
|
67
|
+
|
|
68
|
+
assert.doesNotThrow(() => {
|
|
69
|
+
a.receive({ type: "value", uuid: sent[0].uuid, value: 1 }, b);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("sends nothing if the signal has already aborted", async () => {
|
|
74
|
+
const sent = [];
|
|
75
|
+
const { a } = linked_stores((msg) => {
|
|
76
|
+
sent.push(msg);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
const before = sent.length;
|
|
80
|
+
const reason = new Error("gave up");
|
|
81
|
+
|
|
82
|
+
const err = await rejection(a.get("b.value", undefined, { signal: AbortSignal.abort(reason) }));
|
|
83
|
+
|
|
84
|
+
assert.strictEqual(err, reason);
|
|
85
|
+
assert.strictEqual(sent.length, before);
|
|
86
|
+
assert.strictEqual(Object.keys(a._requests).length, 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("rejects if a local function is aborted", async () => {
|
|
90
|
+
const a = new Entangld();
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const reason = new Error("gave up");
|
|
93
|
+
a.set("hang", () => new Promise(() => { }));
|
|
94
|
+
|
|
95
|
+
const request = a.get("hang", undefined, { signal: controller.signal });
|
|
96
|
+
controller.abort(reason);
|
|
97
|
+
|
|
98
|
+
assert.strictEqual(await rejection(request), reason);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("doesn't call a local function if the signal has already aborted", async () => {
|
|
102
|
+
const a = new Entangld();
|
|
103
|
+
let called = false;
|
|
104
|
+
a.set("f", () => {
|
|
105
|
+
called = true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await rejection(a.get("f", undefined, { signal: AbortSignal.abort() }));
|
|
109
|
+
|
|
110
|
+
assert.strictEqual(called, false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("resolves if the reply beats the abort", async () => {
|
|
114
|
+
const { a, b } = linked_stores();
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
b.set("value", 7);
|
|
117
|
+
|
|
118
|
+
const value = await a.get("b.value", undefined, { signal: controller.signal });
|
|
119
|
+
controller.abort();
|
|
120
|
+
|
|
121
|
+
assert.strictEqual(value, 7);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps signals off the wire", async () => {
|
|
125
|
+
const sent = [];
|
|
126
|
+
const { a, b } = linked_stores((msg) => {
|
|
127
|
+
sent.push(JSON.stringify(msg));
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
b.set("value", 7);
|
|
131
|
+
|
|
132
|
+
await a.get("b.value", undefined, { signal: new AbortController().signal });
|
|
133
|
+
|
|
134
|
+
assert(!sent.join("").includes("signal"));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("throws if signal isn't an AbortSignal", () => {
|
|
138
|
+
const a = new Entangld();
|
|
139
|
+
|
|
140
|
+
for (const signal of [null, 1, "signal", {}]) {
|
|
141
|
+
assert.throws(() => a.get("value", undefined, { signal }), TypeError);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("listeners", () => {
|
|
147
|
+
|
|
148
|
+
it("removes its listener when a request resolves", async () => {
|
|
149
|
+
const { a, b } = linked_stores();
|
|
150
|
+
const { signal } = new AbortController();
|
|
151
|
+
b.set("value", 7);
|
|
152
|
+
|
|
153
|
+
await a.get("b.value", undefined, { signal });
|
|
154
|
+
|
|
155
|
+
assert.strictEqual(listeners(signal), 0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("removes its listener when a local function rejects", async () => {
|
|
159
|
+
const a = new Entangld();
|
|
160
|
+
const { signal } = new AbortController();
|
|
161
|
+
a.set("bad", () => Promise.reject(new Error("boom")));
|
|
162
|
+
|
|
163
|
+
await rejection(a.get("bad", undefined, { signal }));
|
|
164
|
+
|
|
165
|
+
assert.strictEqual(listeners(signal), 0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("removes its listener when a request times out", async () => {
|
|
169
|
+
const { a } = linked_stores(() => false);
|
|
170
|
+
const { signal } = new AbortController();
|
|
171
|
+
|
|
172
|
+
await rejection(a.get("b.value", undefined, { signal, timeout_ms: 5 }));
|
|
173
|
+
|
|
174
|
+
assert.strictEqual(listeners(signal), 0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("with timeout_ms", () => {
|
|
179
|
+
|
|
180
|
+
it("times out if the signal doesn't abort", async () => {
|
|
181
|
+
const { a } = linked_stores(() => false);
|
|
182
|
+
const { signal } = new AbortController();
|
|
183
|
+
|
|
184
|
+
const err = await rejection(a.get("b.value", undefined, { signal, timeout_ms: 5 }));
|
|
185
|
+
|
|
186
|
+
assert(err instanceof EntangldTimeoutError);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("rejects with the abort reason if the signal aborts first", async () => {
|
|
190
|
+
const { a } = linked_stores(() => false);
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const reason = new Error("gave up");
|
|
193
|
+
|
|
194
|
+
const request = a.get("b.value", undefined, { signal: controller.signal, timeout_ms: 1000 });
|
|
195
|
+
controller.abort(reason);
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(await rejection(request), reason);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("cancels the timer after an abort", async () => {
|
|
201
|
+
const { a } = linked_stores(() => false);
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const { setTimeout: set_timer, clearTimeout: clear_timer } = global;
|
|
204
|
+
const cleared = [];
|
|
205
|
+
let timer;
|
|
206
|
+
|
|
207
|
+
global.setTimeout = (...args) => (timer = set_timer(...args));
|
|
208
|
+
global.clearTimeout = (id) => {
|
|
209
|
+
cleared.push(id);
|
|
210
|
+
clear_timer(id);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const request = a.get("b.value", undefined, { signal: controller.signal, timeout_ms: 1000 });
|
|
215
|
+
controller.abort();
|
|
216
|
+
await rejection(request);
|
|
217
|
+
} finally {
|
|
218
|
+
global.setTimeout = set_timer;
|
|
219
|
+
global.clearTimeout = clear_timer;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
assert(cleared.includes(timer));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("subscribe", () => {
|
|
227
|
+
|
|
228
|
+
it("stops calling back when a local subscription is aborted", async () => {
|
|
229
|
+
const a = new Entangld();
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
const seen = [];
|
|
232
|
+
|
|
233
|
+
a.subscribe("value", (path, value) => seen.push(value), { signal: controller.signal });
|
|
234
|
+
a.set("value", 1);
|
|
235
|
+
controller.abort();
|
|
236
|
+
a.set("value", 2);
|
|
237
|
+
|
|
238
|
+
assert.deepStrictEqual(seen, [1]);
|
|
239
|
+
assert.strictEqual(a.subscribed_to("value"), false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("removes the subscription from the remote store when aborted", () => {
|
|
243
|
+
const { a, b } = linked_stores();
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const seen = [];
|
|
246
|
+
|
|
247
|
+
a.subscribe("b.value", (path, value) => seen.push(value), { signal: controller.signal });
|
|
248
|
+
b.set("value", 1);
|
|
249
|
+
assert.strictEqual(b._subscriptions.length, 1);
|
|
250
|
+
|
|
251
|
+
controller.abort();
|
|
252
|
+
b.set("value", 2);
|
|
253
|
+
|
|
254
|
+
assert.deepStrictEqual(seen, [1]);
|
|
255
|
+
assert.strictEqual(a._subscriptions.length, 0);
|
|
256
|
+
assert.strictEqual(b._subscriptions.length, 0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("only removes the subscription which was aborted", () => {
|
|
260
|
+
const a = new Entangld();
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const seen = [];
|
|
263
|
+
|
|
264
|
+
a.subscribe("value", () => seen.push("signal"), { signal: controller.signal });
|
|
265
|
+
a.subscribe("value", () => seen.push("plain"));
|
|
266
|
+
controller.abort();
|
|
267
|
+
a.set("value", 1);
|
|
268
|
+
|
|
269
|
+
assert.deepStrictEqual(seen, ["plain"]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("still takes every as the third argument", () => {
|
|
273
|
+
for (const [every, calls] of [[2, 2], ["2", 2], [null, 4], [undefined, 4]]) {
|
|
274
|
+
const a = new Entangld();
|
|
275
|
+
const seen = [];
|
|
276
|
+
|
|
277
|
+
a.subscribe("value", (path, value) => seen.push(value), every);
|
|
278
|
+
for (const value of [1, 2, 3, 4]) a.set("value", value);
|
|
279
|
+
|
|
280
|
+
assert.strictEqual(seen.length, calls);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("takes an options object without a signal", () => {
|
|
285
|
+
const a = new Entangld();
|
|
286
|
+
const seen = [];
|
|
287
|
+
|
|
288
|
+
a.subscribe("value", (path, value) => seen.push(value), { every: 2 });
|
|
289
|
+
for (const value of [1, 2, 3, 4]) a.set("value", value);
|
|
290
|
+
|
|
291
|
+
assert.strictEqual(seen.length, 2);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("takes every and signal together", () => {
|
|
295
|
+
const a = new Entangld();
|
|
296
|
+
const { signal } = new AbortController();
|
|
297
|
+
const seen = [];
|
|
298
|
+
|
|
299
|
+
a.subscribe("value", (path, value) => seen.push(value), { every: 2, signal });
|
|
300
|
+
for (const value of [1, 2, 3, 4]) a.set("value", value);
|
|
301
|
+
|
|
302
|
+
assert.strictEqual(seen.length, 2);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("creates nothing if the signal has already aborted", () => {
|
|
306
|
+
const sent = [];
|
|
307
|
+
const { a } = linked_stores((msg) => {
|
|
308
|
+
sent.push(msg);
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
const before = sent.length;
|
|
312
|
+
|
|
313
|
+
const uuid = a.subscribe("b.value", () => { }, { signal: AbortSignal.abort() });
|
|
314
|
+
|
|
315
|
+
assert.strictEqual(typeof uuid, "string");
|
|
316
|
+
assert.strictEqual(a._subscriptions.length, 0);
|
|
317
|
+
assert.strictEqual(sent.length, before);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("unsubscribe throws after an abort", () => {
|
|
321
|
+
const a = new Entangld();
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
|
|
324
|
+
const uuid = a.subscribe("value", () => { }, { signal: controller.signal });
|
|
325
|
+
controller.abort();
|
|
326
|
+
|
|
327
|
+
assert.throws(() => a.unsubscribe(uuid), EntangldError);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("unsubscribe throws if the signal had already aborted", () => {
|
|
331
|
+
const a = new Entangld();
|
|
332
|
+
|
|
333
|
+
const uuid = a.subscribe("value", () => { }, { signal: AbortSignal.abort() });
|
|
334
|
+
|
|
335
|
+
assert.throws(() => a.unsubscribe(uuid), EntangldError);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("doesn't throw if aborted after unsubscribing", () => {
|
|
339
|
+
const a = new Entangld();
|
|
340
|
+
const controller = new AbortController();
|
|
341
|
+
|
|
342
|
+
const uuid = a.subscribe("value", () => { }, { signal: controller.signal });
|
|
343
|
+
a.unsubscribe(uuid);
|
|
344
|
+
|
|
345
|
+
assert.doesNotThrow(() => controller.abort());
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("removes its listener when aborted", () => {
|
|
349
|
+
const a = new Entangld();
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
|
|
352
|
+
a.subscribe("value", () => { }, { signal: controller.signal });
|
|
353
|
+
assert.strictEqual(listeners(controller.signal), 1);
|
|
354
|
+
controller.abort();
|
|
355
|
+
|
|
356
|
+
assert.strictEqual(listeners(controller.signal), 0);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("removes its listener on unsubscribe", () => {
|
|
360
|
+
const a = new Entangld();
|
|
361
|
+
const { signal } = new AbortController();
|
|
362
|
+
|
|
363
|
+
a.unsubscribe(a.subscribe("value", () => { }, { signal }));
|
|
364
|
+
|
|
365
|
+
assert.strictEqual(listeners(signal), 0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("removes its listener on unsubscribe_tree", () => {
|
|
369
|
+
const a = new Entangld();
|
|
370
|
+
const { signal } = new AbortController();
|
|
371
|
+
|
|
372
|
+
a.subscribe("tree.value", () => { }, { signal });
|
|
373
|
+
a.unsubscribe_tree("tree");
|
|
374
|
+
|
|
375
|
+
assert.strictEqual(listeners(signal), 0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("still works after the store is reattached", () => {
|
|
379
|
+
const { a, b } = linked_stores();
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
const { signal } = controller;
|
|
382
|
+
const seen = [];
|
|
383
|
+
|
|
384
|
+
const uuid = a.subscribe("b.value", (path, value) => seen.push(value), { signal });
|
|
385
|
+
|
|
386
|
+
const replacement = new Entangld();
|
|
387
|
+
replacement.transmit((msg, store) => store.receive(msg, replacement));
|
|
388
|
+
replacement.attach("a", a);
|
|
389
|
+
a.detach("b");
|
|
390
|
+
a.attach("b", replacement);
|
|
391
|
+
|
|
392
|
+
assert.strictEqual(a.subscriptions[0].uuid, uuid);
|
|
393
|
+
assert.strictEqual(listeners(signal), 1);
|
|
394
|
+
|
|
395
|
+
replacement.set("value", 1);
|
|
396
|
+
controller.abort();
|
|
397
|
+
replacement.set("value", 2);
|
|
398
|
+
|
|
399
|
+
assert.deepStrictEqual(seen, [1]);
|
|
400
|
+
assert.strictEqual(a._subscriptions.length, 0);
|
|
401
|
+
assert.strictEqual(replacement._subscriptions.length, 0);
|
|
402
|
+
assert.strictEqual(b._subscriptions.length, 0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("throws if signal isn't an AbortSignal", () => {
|
|
406
|
+
const a = new Entangld();
|
|
407
|
+
|
|
408
|
+
for (const signal of [null, 1, "signal", {}]) {
|
|
409
|
+
assert.throws(() => a.subscribe("value", () => { }, { signal }), TypeError);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("mixing abort and unsubscribe", () => {
|
|
415
|
+
|
|
416
|
+
// Count the unsubscribe messages store a sends
|
|
417
|
+
function counted_stores() {
|
|
418
|
+
const sent = [];
|
|
419
|
+
const { a, b } = linked_stores((msg) => {
|
|
420
|
+
sent.push(msg.type);
|
|
421
|
+
return true;
|
|
422
|
+
});
|
|
423
|
+
const unsubscribes = () => sent.filter(type => type === "unsubscribe").length;
|
|
424
|
+
|
|
425
|
+
return { a, b, unsubscribes };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
it("checking signal.aborted skips the unsubscribe after an abort", () => {
|
|
429
|
+
const a = new Entangld();
|
|
430
|
+
const controller = new AbortController();
|
|
431
|
+
const { signal } = controller;
|
|
432
|
+
|
|
433
|
+
const uuid = a.subscribe("value", () => { }, { signal });
|
|
434
|
+
controller.abort();
|
|
435
|
+
|
|
436
|
+
assert.doesNotThrow(() => {
|
|
437
|
+
if (!signal.aborted) a.unsubscribe(uuid);
|
|
438
|
+
});
|
|
439
|
+
assert.strictEqual(a._subscriptions.length, 0);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("checking signal.aborted still unsubscribes without an abort", () => {
|
|
443
|
+
const a = new Entangld();
|
|
444
|
+
const { signal } = new AbortController();
|
|
445
|
+
const seen = [];
|
|
446
|
+
|
|
447
|
+
const uuid = a.subscribe("value", (path, value) => seen.push(value), { signal });
|
|
448
|
+
if (!signal.aborted) a.unsubscribe(uuid);
|
|
449
|
+
a.set("value", 1);
|
|
450
|
+
|
|
451
|
+
assert.deepStrictEqual(seen, []);
|
|
452
|
+
assert.strictEqual(listeners(signal), 0);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("the unsubscribe error can be caught after an abort", () => {
|
|
456
|
+
const a = new Entangld();
|
|
457
|
+
const controller = new AbortController();
|
|
458
|
+
const reached = [];
|
|
459
|
+
|
|
460
|
+
const uuid = a.subscribe("value", () => { }, { signal: controller.signal });
|
|
461
|
+
controller.abort();
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
a.unsubscribe(uuid);
|
|
465
|
+
reached.push("returned");
|
|
466
|
+
} catch (err) {
|
|
467
|
+
assert(err instanceof EntangldError);
|
|
468
|
+
assert(err.message.includes(uuid));
|
|
469
|
+
reached.push("caught");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
assert.deepStrictEqual(reached, ["caught"]);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("unsubscribing by path throws after an abort", () => {
|
|
476
|
+
const a = new Entangld();
|
|
477
|
+
const controller = new AbortController();
|
|
478
|
+
|
|
479
|
+
a.subscribe("value", () => { }, { signal: controller.signal });
|
|
480
|
+
controller.abort();
|
|
481
|
+
|
|
482
|
+
assert.throws(() => a.unsubscribe("value"), EntangldError);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("unsubscribing by path removes what the abort left", () => {
|
|
486
|
+
const a = new Entangld();
|
|
487
|
+
const controller = new AbortController();
|
|
488
|
+
const seen = [];
|
|
489
|
+
|
|
490
|
+
a.subscribe("value", () => seen.push("signal"), { signal: controller.signal });
|
|
491
|
+
a.subscribe("value", () => seen.push("plain"));
|
|
492
|
+
controller.abort();
|
|
493
|
+
|
|
494
|
+
assert.strictEqual(a.unsubscribe("value"), 1);
|
|
495
|
+
a.set("value", 1);
|
|
496
|
+
|
|
497
|
+
assert.deepStrictEqual(seen, []);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("unsubscribing by path removes a signal's subscription too", () => {
|
|
501
|
+
const a = new Entangld();
|
|
502
|
+
const controller = new AbortController();
|
|
503
|
+
const { signal } = controller;
|
|
504
|
+
|
|
505
|
+
a.subscribe("value", () => { }, { signal });
|
|
506
|
+
a.subscribe("value", () => { });
|
|
507
|
+
|
|
508
|
+
assert.strictEqual(a.unsubscribe("value"), 2);
|
|
509
|
+
assert.strictEqual(listeners(signal), 0);
|
|
510
|
+
assert.doesNotThrow(() => controller.abort());
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("unsubscribing one leaves the others on a shared signal", () => {
|
|
514
|
+
const a = new Entangld();
|
|
515
|
+
const controller = new AbortController();
|
|
516
|
+
const { signal } = controller;
|
|
517
|
+
const seen = [];
|
|
518
|
+
|
|
519
|
+
const first = a.subscribe("one", () => seen.push("one"), { signal });
|
|
520
|
+
a.subscribe("two", () => seen.push("two"), { signal });
|
|
521
|
+
|
|
522
|
+
a.unsubscribe(first);
|
|
523
|
+
assert.strictEqual(listeners(signal), 1);
|
|
524
|
+
a.set("two", 1);
|
|
525
|
+
controller.abort();
|
|
526
|
+
a.set("two", 2);
|
|
527
|
+
|
|
528
|
+
assert.deepStrictEqual(seen, ["two"]);
|
|
529
|
+
assert.strictEqual(listeners(signal), 0);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("tells a remote store once if unsubscribed before an abort", () => {
|
|
533
|
+
const { a, b, unsubscribes } = counted_stores();
|
|
534
|
+
const controller = new AbortController();
|
|
535
|
+
|
|
536
|
+
const uuid = a.subscribe("b.value", () => { }, { signal: controller.signal });
|
|
537
|
+
a.unsubscribe(uuid);
|
|
538
|
+
controller.abort();
|
|
539
|
+
|
|
540
|
+
assert.strictEqual(unsubscribes(), 1);
|
|
541
|
+
assert.strictEqual(b._subscriptions.length, 0);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("tells a remote store once if aborted before unsubscribing", () => {
|
|
545
|
+
const { a, b, unsubscribes } = counted_stores();
|
|
546
|
+
const controller = new AbortController();
|
|
547
|
+
|
|
548
|
+
const uuid = a.subscribe("b.value", () => { }, { signal: controller.signal });
|
|
549
|
+
controller.abort();
|
|
550
|
+
|
|
551
|
+
assert.throws(() => a.unsubscribe(uuid), EntangldError);
|
|
552
|
+
assert.strictEqual(unsubscribes(), 1);
|
|
553
|
+
assert.strictEqual(b._subscriptions.length, 0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("a second abort changes nothing", () => {
|
|
557
|
+
const { a, unsubscribes } = counted_stores();
|
|
558
|
+
const controller = new AbortController();
|
|
559
|
+
|
|
560
|
+
a.subscribe("b.value", () => { }, { signal: controller.signal });
|
|
561
|
+
controller.abort();
|
|
562
|
+
controller.abort();
|
|
563
|
+
|
|
564
|
+
assert.strictEqual(unsubscribes(), 1);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe("call_rpc", () => {
|
|
569
|
+
|
|
570
|
+
it("rejects with the reason when aborted", async () => {
|
|
571
|
+
const { a } = linked_stores(() => false);
|
|
572
|
+
const controller = new AbortController();
|
|
573
|
+
const reason = new Error("gave up");
|
|
574
|
+
|
|
575
|
+
const request = a.call_rpc("b.rpc", [1], { signal: controller.signal });
|
|
576
|
+
controller.abort(reason);
|
|
577
|
+
|
|
578
|
+
assert.strictEqual(await rejection(request), reason);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("returns the result if the reply beats the abort", async () => {
|
|
582
|
+
const { a, b } = linked_stores();
|
|
583
|
+
b.set_rpc("double", (value) => value * 2);
|
|
584
|
+
|
|
585
|
+
const { signal } = new AbortController();
|
|
586
|
+
|
|
587
|
+
assert.strictEqual(await a.call_rpc("b.double", [4], { signal }), 8);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
});
|
package/test/sockets.js
CHANGED
|
@@ -122,7 +122,7 @@ describe("Sockets", function() {
|
|
|
122
122
|
});
|
|
123
123
|
while ( a._subscriptions.length < 1 ) await wait(1);
|
|
124
124
|
client.disconnect(); // kill the socket
|
|
125
|
-
await
|
|
125
|
+
await wait(5);
|
|
126
126
|
// Reconnect the client
|
|
127
127
|
sock_ref = null;
|
|
128
128
|
client = new sockhop.client({
|
|
@@ -134,6 +134,11 @@ describe("Sockets", function() {
|
|
|
134
134
|
while ( !sock_ref ) await wait(1);
|
|
135
135
|
while ( sock_ref.readyState !== "open" ) await wait(1);
|
|
136
136
|
|
|
137
|
+
// Wait for the resubscribe. Until it arrives, a only has the old
|
|
138
|
+
// subscription, which sends events to the dead client
|
|
139
|
+
const deadline = Date.now() + 1000;
|
|
140
|
+
while ( !a._subscriptions.some(sub => sub.upstream === client) && Date.now() < deadline ) await wait(1);
|
|
141
|
+
|
|
137
142
|
if ( a._subscriptions.length !== 1 ) {
|
|
138
143
|
// a still has the old subscription from before the the socket
|
|
139
144
|
// was destroyed
|
|
@@ -141,7 +146,7 @@ describe("Sockets", function() {
|
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
143
148
|
a.set("data",1); // Should trigger subscription, and call done
|
|
144
|
-
await wait(
|
|
149
|
+
while ( !flag && Date.now() < deadline ) await wait(1);
|
|
145
150
|
if ( !flag ) done(new Error("Never got resubscribe from reconnection event"));
|
|
146
151
|
});
|
|
147
152
|
});
|
package/test/tests.js
CHANGED
|
@@ -362,6 +362,16 @@ describe("Multiplexed stores",()=>{
|
|
|
362
362
|
});
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
+
it("Ignores a value reply with no matching request", ()=>{
|
|
366
|
+
|
|
367
|
+
var request_length = Object.keys(s._requests).length;
|
|
368
|
+
|
|
369
|
+
assert.doesNotThrow(()=>{
|
|
370
|
+
s.receive({ type: "value", uuid: "no-such-request", value: 1 }, a);
|
|
371
|
+
});
|
|
372
|
+
assert.strictEqual(request_length, Object.keys(s._requests).length);
|
|
373
|
+
});
|
|
374
|
+
|
|
365
375
|
it("Remote set single root value into child store", ()=>{
|
|
366
376
|
|
|
367
377
|
s.set("b.something",99);
|