@arcote.tech/arc-host 0.7.15 → 0.7.16
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/index.js +48 -122
- package/dist/index.js.map +4 -4
- package/dist/src/create-server.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/index.d.ts +1 -1
- package/dist/src/middleware/index.d.ts.map +1 -1
- package/dist/src/middleware/live-query.integration.test.d.ts +2 -0
- package/dist/src/middleware/live-query.integration.test.d.ts.map +1 -0
- package/dist/src/middleware/ws.d.ts +1 -30
- package/dist/src/middleware/ws.d.ts.map +1 -1
- package/dist/src/types.d.ts +16 -14
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/create-server.ts +0 -8
- package/src/index.ts +1 -2
- package/src/middleware/index.ts +1 -2
- package/src/middleware/live-query.integration.test.ts +432 -0
- package/src/middleware/ws.ts +63 -186
- package/src/types.ts +13 -15
- package/dist/src/middleware/view-subscription.integration.test.d.ts +0 -2
- package/dist/src/middleware/view-subscription.integration.test.d.ts.map +0 -1
- package/dist/src/middleware/ws.test.d.ts +0 -2
- package/dist/src/middleware/ws.test.d.ts.map +0 -1
- package/src/middleware/view-subscription.integration.test.ts +0 -307
- package/src/middleware/ws.test.ts +0 -95
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
MasterDataStorage,
|
|
4
|
+
aggregate,
|
|
5
|
+
applyQueryChanges,
|
|
6
|
+
boolean,
|
|
7
|
+
context,
|
|
8
|
+
id,
|
|
9
|
+
string,
|
|
10
|
+
token,
|
|
11
|
+
} from "@arcote.tech/arc";
|
|
12
|
+
import type { ConnectedClient } from "../types";
|
|
13
|
+
import { cleanupClientSubs, querySubscriptionHandler } from "./ws";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Test rig: real MasterDataStorage + a real aggregate with custom
|
|
17
|
+
// clientQuery handlers (the NDT-shaped case: handler runs $query.find with
|
|
18
|
+
// protectBy restrictions) + the real querySubscriptionHandler. The fake
|
|
19
|
+
// connection manager collects outgoing messages.
|
|
20
|
+
//
|
|
21
|
+
// Covers the full server pipeline: subscribe-query → LiveQuery tracks the
|
|
22
|
+
// handler's inner finds → store commit → resolveQueryChange → re-execute
|
|
23
|
+
// from cache → diff → query-changes / query-snapshot.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Persistent in-memory DatabaseAdapter (shared tables across transactions). */
|
|
27
|
+
function memoryAdapter() {
|
|
28
|
+
const tables = new Map<string, Map<string, any>>();
|
|
29
|
+
const table = (s: string) => {
|
|
30
|
+
if (!tables.has(s)) tables.set(s, new Map());
|
|
31
|
+
return tables.get(s)!;
|
|
32
|
+
};
|
|
33
|
+
const matches = (row: any, where?: Record<string, any>) =>
|
|
34
|
+
!where || Object.entries(where).every(([k, v]) => row[k] === v);
|
|
35
|
+
const tx = {
|
|
36
|
+
async find(store: string, options: any) {
|
|
37
|
+
let rows = Array.from(table(store).values()).filter((r) =>
|
|
38
|
+
matches(r, options?.where),
|
|
39
|
+
);
|
|
40
|
+
if (options?.orderBy) {
|
|
41
|
+
const entries = Object.entries(options.orderBy) as [string, string][];
|
|
42
|
+
rows = [...rows].sort((a, b) => {
|
|
43
|
+
for (const [k, dir] of entries) {
|
|
44
|
+
if (a[k] < b[k]) return dir === "asc" ? -1 : 1;
|
|
45
|
+
if (a[k] > b[k]) return dir === "asc" ? 1 : -1;
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (options?.limit !== undefined) rows = rows.slice(0, options.limit);
|
|
51
|
+
return rows;
|
|
52
|
+
},
|
|
53
|
+
async set(store: string, data: any) {
|
|
54
|
+
table(store).set(data._id, structuredClone(data));
|
|
55
|
+
},
|
|
56
|
+
async remove(store: string, id: any) {
|
|
57
|
+
table(store).delete(String(id));
|
|
58
|
+
},
|
|
59
|
+
async commit() {},
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
readWriteTransaction: () => tx,
|
|
63
|
+
readTransaction: () => tx,
|
|
64
|
+
executeReinitTables: async () => {},
|
|
65
|
+
} as any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const wsToken = token("workspace", {});
|
|
69
|
+
const taskId = id("task");
|
|
70
|
+
|
|
71
|
+
const tasks = aggregate("tasks", taskId, {
|
|
72
|
+
title: string(),
|
|
73
|
+
workspaceId: string(),
|
|
74
|
+
done: boolean(),
|
|
75
|
+
})
|
|
76
|
+
.clientQuery("getAll", (fn: any) =>
|
|
77
|
+
fn.handle(async (ctx: any) =>
|
|
78
|
+
ctx.$query.find({ orderBy: { title: "asc" } }),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
.clientQuery("getDone", (fn: any) =>
|
|
82
|
+
fn.handle(async (ctx: any) =>
|
|
83
|
+
ctx.$query.find({ where: { done: true }, orderBy: { title: "asc" } }),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
.clientQuery("getTop2", (fn: any) =>
|
|
87
|
+
fn.handle(async (ctx: any) =>
|
|
88
|
+
ctx.$query.find({ orderBy: { title: "asc" }, limit: 2 }),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
.clientQuery("countAll", (fn: any) =>
|
|
92
|
+
fn.handle(async (ctx: any) => {
|
|
93
|
+
const rows = await ctx.$query.find({});
|
|
94
|
+
return { count: rows.length };
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
.protectBy(wsToken as any, (p: any) => ({ workspaceId: p.workspaceId }));
|
|
98
|
+
|
|
99
|
+
function fakeJwt(tokenName: string, params: Record<string, any>): string {
|
|
100
|
+
const b64 = (o: any) => Buffer.from(JSON.stringify(o)).toString("base64");
|
|
101
|
+
return `${b64({ alg: "none" })}.${b64({ tokenName, params })}.sig`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fakeClient(
|
|
105
|
+
clientId: string,
|
|
106
|
+
scope: string,
|
|
107
|
+
params: Record<string, any>,
|
|
108
|
+
): ConnectedClient {
|
|
109
|
+
return {
|
|
110
|
+
id: clientId,
|
|
111
|
+
scopeTokens: new Map([
|
|
112
|
+
[
|
|
113
|
+
scope,
|
|
114
|
+
{
|
|
115
|
+
decoded: { tokenType: "workspace", params },
|
|
116
|
+
raw: fakeJwt("workspace", params),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
]),
|
|
120
|
+
lastHostEventId: null,
|
|
121
|
+
ws: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Microtask + async re-execute settle. */
|
|
126
|
+
const settle = () => new Promise((r) => setTimeout(r, 10));
|
|
127
|
+
|
|
128
|
+
function rig() {
|
|
129
|
+
const storage = new MasterDataStorage(memoryAdapter());
|
|
130
|
+
const testContext = context([tasks as any]);
|
|
131
|
+
const fakeModel = {
|
|
132
|
+
context: testContext,
|
|
133
|
+
getAdapters: () => ({ dataStorage: storage }),
|
|
134
|
+
getEnvironment: () => "server" as const,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const sent: Array<{ clientId: string; message: any }> = [];
|
|
138
|
+
const wsCtx = {
|
|
139
|
+
contextHandler: { getModel: () => fakeModel },
|
|
140
|
+
connectionManager: {
|
|
141
|
+
sendToClient: (clientId: string, message: any) => {
|
|
142
|
+
sent.push({ clientId, message });
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
verifyToken: () => null,
|
|
147
|
+
} as any;
|
|
148
|
+
|
|
149
|
+
const handler = querySubscriptionHandler();
|
|
150
|
+
|
|
151
|
+
const subscribe = (
|
|
152
|
+
client: ConnectedClient,
|
|
153
|
+
subscriptionId: string,
|
|
154
|
+
method: string,
|
|
155
|
+
scope = "workspace",
|
|
156
|
+
) =>
|
|
157
|
+
handler(
|
|
158
|
+
client,
|
|
159
|
+
{
|
|
160
|
+
type: "subscribe-query",
|
|
161
|
+
subscriptionId,
|
|
162
|
+
descriptor: { element: "tasks", method, args: [] },
|
|
163
|
+
scope,
|
|
164
|
+
},
|
|
165
|
+
wsCtx,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const seed = (rows: any[]) =>
|
|
169
|
+
storage
|
|
170
|
+
.getStore<any>("tasks")
|
|
171
|
+
.applyChanges(rows.map((data) => ({ type: "set" as const, data })));
|
|
172
|
+
|
|
173
|
+
const messagesFor = (clientId: string, type?: string) =>
|
|
174
|
+
sent
|
|
175
|
+
.filter((s) => s.clientId === clientId)
|
|
176
|
+
.map((s) => s.message)
|
|
177
|
+
.filter((m) => (type ? m.type === type : true));
|
|
178
|
+
|
|
179
|
+
return { storage, wsCtx, handler, subscribe, seed, sent, messagesFor };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const row = (id: string, title: string, workspaceId: string, done = false) => ({
|
|
183
|
+
_id: id,
|
|
184
|
+
title,
|
|
185
|
+
workspaceId,
|
|
186
|
+
done,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
let seq = 0;
|
|
190
|
+
const uid = (p: string) => `${p}_${++seq}`;
|
|
191
|
+
|
|
192
|
+
describe("live query subscriptions — end-to-end host pipeline", () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
for (let i = 0; i <= seq; i++) {
|
|
195
|
+
cleanupClientSubs(`A_${i}`);
|
|
196
|
+
cleanupClientSubs(`B_${i}`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("snapshot contains the QUERY result for the subscriber's scope only", async () => {
|
|
201
|
+
const r = rig();
|
|
202
|
+
await r.seed([
|
|
203
|
+
row("t1", "Beta", "w1"),
|
|
204
|
+
row("t2", "Alpha", "w1"),
|
|
205
|
+
row("t3", "Other", "w2"),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
209
|
+
await r.subscribe(a, "s1", "getAll");
|
|
210
|
+
await settle();
|
|
211
|
+
|
|
212
|
+
const snapshots = r.messagesFor(a.id, "query-snapshot");
|
|
213
|
+
expect(snapshots).toHaveLength(1);
|
|
214
|
+
expect(snapshots[0].subscriptionId).toBe("s1");
|
|
215
|
+
// orderBy from the HANDLER (title asc), restrictions from protectBy
|
|
216
|
+
expect(snapshots[0].result.map((t: any) => t._id)).toEqual(["t2", "t1"]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("matching insert → query-changes with the correct position", async () => {
|
|
220
|
+
const r = rig();
|
|
221
|
+
await r.seed([row("t1", "Beta", "w1"), row("t2", "Delta", "w1")]);
|
|
222
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
223
|
+
await r.subscribe(a, "s1", "getAll");
|
|
224
|
+
await settle();
|
|
225
|
+
r.sent.length = 0;
|
|
226
|
+
|
|
227
|
+
await r.storage
|
|
228
|
+
.getStore<any>("tasks")
|
|
229
|
+
.applyChanges([{ type: "set", data: row("t3", "Charlie", "w1") }]);
|
|
230
|
+
await settle();
|
|
231
|
+
|
|
232
|
+
const changes = r.messagesFor(a.id, "query-changes");
|
|
233
|
+
expect(changes).toHaveLength(1);
|
|
234
|
+
expect(changes[0].changes).toEqual([
|
|
235
|
+
{
|
|
236
|
+
type: "set",
|
|
237
|
+
id: "t3",
|
|
238
|
+
item: row("t3", "Charlie", "w1"),
|
|
239
|
+
index: 1, // Beta, Charlie, Delta
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("tenant isolation: a change in another workspace produces NO message", async () => {
|
|
245
|
+
const r = rig();
|
|
246
|
+
await r.seed([row("t1", "Beta", "w1")]);
|
|
247
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
248
|
+
await r.subscribe(a, "s1", "getAll");
|
|
249
|
+
await settle();
|
|
250
|
+
r.sent.length = 0;
|
|
251
|
+
|
|
252
|
+
await r.storage
|
|
253
|
+
.getStore<any>("tasks")
|
|
254
|
+
.applyChanges([{ type: "set", data: row("t9", "Foreign", "w2") }]);
|
|
255
|
+
await settle();
|
|
256
|
+
|
|
257
|
+
expect(r.messagesFor(a.id)).toHaveLength(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("item leaving the scope → delete for the old tenant, set for the new one", async () => {
|
|
261
|
+
const r = rig();
|
|
262
|
+
await r.seed([row("t1", "Task", "w1"), row("t2", "Keep", "w1")]);
|
|
263
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
264
|
+
const b = fakeClient(uid("B"), "workspace", { workspaceId: "w2" });
|
|
265
|
+
await r.subscribe(a, "sa", "getAll");
|
|
266
|
+
await r.subscribe(b, "sb", "getAll");
|
|
267
|
+
await settle();
|
|
268
|
+
r.sent.length = 0;
|
|
269
|
+
|
|
270
|
+
// Move t1 from w1 to w2 (modify → full-item set event)
|
|
271
|
+
await r.storage
|
|
272
|
+
.getStore<any>("tasks")
|
|
273
|
+
.applyChanges([{ type: "modify", id: "t1", data: { workspaceId: "w2" } }]);
|
|
274
|
+
await settle();
|
|
275
|
+
|
|
276
|
+
const toA = r.messagesFor(a.id, "query-changes");
|
|
277
|
+
expect(toA).toHaveLength(1);
|
|
278
|
+
expect(toA[0].changes).toEqual([{ type: "delete", id: "t1" }]);
|
|
279
|
+
|
|
280
|
+
const toB = r.messagesFor(b.id, "query-changes");
|
|
281
|
+
expect(toB).toHaveLength(1);
|
|
282
|
+
expect(toB[0].changes).toEqual([
|
|
283
|
+
{ type: "set", id: "t1", item: row("t1", "Task", "w2"), index: 0 },
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("where inside the HANDLER: toggling done moves items in/out of the result", async () => {
|
|
288
|
+
const r = rig();
|
|
289
|
+
await r.seed([row("t1", "A", "w1", true), row("t2", "B", "w1", false)]);
|
|
290
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
291
|
+
await r.subscribe(a, "s1", "getDone");
|
|
292
|
+
await settle();
|
|
293
|
+
|
|
294
|
+
expect(r.messagesFor(a.id, "query-snapshot")[0].result).toEqual([
|
|
295
|
+
row("t1", "A", "w1", true),
|
|
296
|
+
]);
|
|
297
|
+
r.sent.length = 0;
|
|
298
|
+
|
|
299
|
+
// t2 becomes done → enters the result
|
|
300
|
+
await r.storage
|
|
301
|
+
.getStore<any>("tasks")
|
|
302
|
+
.applyChanges([{ type: "modify", id: "t2", data: { done: true } }]);
|
|
303
|
+
await settle();
|
|
304
|
+
expect(r.messagesFor(a.id, "query-changes")[0].changes).toEqual([
|
|
305
|
+
{ type: "set", id: "t2", item: row("t2", "B", "w1", true), index: 1 },
|
|
306
|
+
]);
|
|
307
|
+
r.sent.length = 0;
|
|
308
|
+
|
|
309
|
+
// t1 becomes undone → leaves the result
|
|
310
|
+
await r.storage
|
|
311
|
+
.getStore<any>("tasks")
|
|
312
|
+
.applyChanges([{ type: "modify", id: "t1", data: { done: false } }]);
|
|
313
|
+
await settle();
|
|
314
|
+
expect(r.messagesFor(a.id, "query-changes")[0].changes).toEqual([
|
|
315
|
+
{ type: "delete", id: "t1" },
|
|
316
|
+
]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("limit underflow: removing from a full top-N refills from the store", async () => {
|
|
320
|
+
const r = rig();
|
|
321
|
+
await r.seed([
|
|
322
|
+
row("t1", "A", "w1"),
|
|
323
|
+
row("t2", "B", "w1"),
|
|
324
|
+
row("t3", "C", "w1"),
|
|
325
|
+
]);
|
|
326
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
327
|
+
await r.subscribe(a, "s1", "getTop2");
|
|
328
|
+
await settle();
|
|
329
|
+
|
|
330
|
+
expect(
|
|
331
|
+
r.messagesFor(a.id, "query-snapshot")[0].result.map((t: any) => t._id),
|
|
332
|
+
).toEqual(["t1", "t2"]);
|
|
333
|
+
r.sent.length = 0;
|
|
334
|
+
|
|
335
|
+
// Remove t1 from the full top-2 → t3 must be pulled in from the store.
|
|
336
|
+
// The refill may arrive as a snapshot or as deltas — verify the FINAL
|
|
337
|
+
// result the client would hold either way.
|
|
338
|
+
await r.storage
|
|
339
|
+
.getStore<any>("tasks")
|
|
340
|
+
.applyChanges([{ type: "delete", id: "t1" }]);
|
|
341
|
+
await settle();
|
|
342
|
+
|
|
343
|
+
const all = r.messagesFor(a.id);
|
|
344
|
+
expect(all).toHaveLength(1);
|
|
345
|
+
const result =
|
|
346
|
+
all[0].type === "query-snapshot"
|
|
347
|
+
? all[0].result
|
|
348
|
+
: applyQueryChanges(
|
|
349
|
+
[row("t1", "A", "w1"), row("t2", "B", "w1")],
|
|
350
|
+
all[0].changes,
|
|
351
|
+
);
|
|
352
|
+
expect(result.map((t: any) => t._id)).toEqual(["t2", "t3"]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("non-list result (count) → full snapshot on every change", async () => {
|
|
356
|
+
const r = rig();
|
|
357
|
+
await r.seed([row("t1", "A", "w1")]);
|
|
358
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
359
|
+
await r.subscribe(a, "s1", "countAll");
|
|
360
|
+
await settle();
|
|
361
|
+
|
|
362
|
+
expect(r.messagesFor(a.id, "query-snapshot")[0].result).toEqual({
|
|
363
|
+
count: 1,
|
|
364
|
+
});
|
|
365
|
+
r.sent.length = 0;
|
|
366
|
+
|
|
367
|
+
await r.storage
|
|
368
|
+
.getStore<any>("tasks")
|
|
369
|
+
.applyChanges([{ type: "set", data: row("t2", "B", "w1") }]);
|
|
370
|
+
await settle();
|
|
371
|
+
|
|
372
|
+
const snapshots = r.messagesFor(a.id, "query-snapshot");
|
|
373
|
+
expect(snapshots).toHaveLength(1);
|
|
374
|
+
expect(snapshots[0].result).toEqual({ count: 2 });
|
|
375
|
+
expect(r.messagesFor(a.id, "query-changes")).toHaveLength(0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("unsubscribe-query stops deltas", async () => {
|
|
379
|
+
const r = rig();
|
|
380
|
+
await r.seed([row("t1", "A", "w1")]);
|
|
381
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
382
|
+
await r.subscribe(a, "s1", "getAll");
|
|
383
|
+
await settle();
|
|
384
|
+
await r.handler(
|
|
385
|
+
a,
|
|
386
|
+
{ type: "unsubscribe-query", subscriptionId: "s1" },
|
|
387
|
+
r.wsCtx,
|
|
388
|
+
);
|
|
389
|
+
r.sent.length = 0;
|
|
390
|
+
|
|
391
|
+
await r.storage
|
|
392
|
+
.getStore<any>("tasks")
|
|
393
|
+
.applyChanges([{ type: "set", data: row("t2", "B", "w1") }]);
|
|
394
|
+
await settle();
|
|
395
|
+
expect(r.messagesFor(a.id)).toHaveLength(0);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("disconnect cleanup stops deltas", async () => {
|
|
399
|
+
const r = rig();
|
|
400
|
+
await r.seed([row("t1", "A", "w1")]);
|
|
401
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
402
|
+
await r.subscribe(a, "s1", "getAll");
|
|
403
|
+
await settle();
|
|
404
|
+
cleanupClientSubs(a.id);
|
|
405
|
+
r.sent.length = 0;
|
|
406
|
+
|
|
407
|
+
await r.storage
|
|
408
|
+
.getStore<any>("tasks")
|
|
409
|
+
.applyChanges([{ type: "set", data: row("t2", "B", "w1") }]);
|
|
410
|
+
await settle();
|
|
411
|
+
expect(r.messagesFor(a.id)).toHaveLength(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("re-subscribe with the same id (reconnect resend) replaces the old subscription", async () => {
|
|
415
|
+
const r = rig();
|
|
416
|
+
await r.seed([row("t1", "A", "w1")]);
|
|
417
|
+
const a = fakeClient(uid("A"), "workspace", { workspaceId: "w1" });
|
|
418
|
+
await r.subscribe(a, "s1", "getAll");
|
|
419
|
+
await settle();
|
|
420
|
+
await r.subscribe(a, "s1", "getAll"); // resend after reconnect
|
|
421
|
+
await settle();
|
|
422
|
+
r.sent.length = 0;
|
|
423
|
+
|
|
424
|
+
await r.storage
|
|
425
|
+
.getStore<any>("tasks")
|
|
426
|
+
.applyChanges([{ type: "set", data: row("t2", "B", "w1") }]);
|
|
427
|
+
await settle();
|
|
428
|
+
|
|
429
|
+
// Exactly ONE delta (old subscription was stopped, not duplicated)
|
|
430
|
+
expect(r.messagesFor(a.id, "query-changes")).toHaveLength(1);
|
|
431
|
+
});
|
|
432
|
+
});
|