jazz-tools 0.18.2 → 0.18.4
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/.turbo/turbo-build.log +43 -41
- package/CHANGELOG.md +20 -0
- package/dist/{chunk-IERUTUXB.js → chunk-LHQQZH7I.js} +121 -36
- package/dist/chunk-LHQQZH7I.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/media/{chunk-KR2V6X2N.js → chunk-W3S526L3.js} +96 -93
- package/dist/media/chunk-W3S526L3.js.map +1 -0
- package/dist/media/create-image/browser.d.ts +43 -0
- package/dist/media/create-image/browser.d.ts.map +1 -0
- package/dist/media/create-image/react-native.d.ts +37 -0
- package/dist/media/create-image/react-native.d.ts.map +1 -0
- package/dist/media/create-image/server.d.ts +34 -0
- package/dist/media/create-image/server.d.ts.map +1 -0
- package/dist/media/create-image/server.test.d.ts +2 -0
- package/dist/media/create-image/server.test.d.ts.map +1 -0
- package/dist/media/{create-image.d.ts → create-image-factory.d.ts} +8 -7
- package/dist/media/create-image-factory.d.ts.map +1 -0
- package/dist/media/create-image-factory.test.d.ts +2 -0
- package/dist/media/create-image-factory.test.d.ts.map +1 -0
- package/dist/media/exports.d.ts +3 -0
- package/dist/media/exports.d.ts.map +1 -0
- package/dist/media/index.browser.d.ts +2 -14
- package/dist/media/index.browser.d.ts.map +1 -1
- package/dist/media/index.browser.js +11 -20
- package/dist/media/index.browser.js.map +1 -1
- package/dist/media/index.d.ts +12 -4
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/index.native.d.ts +2 -16
- package/dist/media/index.native.d.ts.map +1 -1
- package/dist/media/index.native.js +23 -42
- package/dist/media/index.native.js.map +1 -1
- package/dist/media/index.server.d.ts +3 -0
- package/dist/media/index.server.d.ts.map +1 -0
- package/dist/media/index.server.js +103 -0
- package/dist/media/index.server.js.map +1 -0
- package/dist/react/index.js +7 -7
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/index.js +120 -35
- package/dist/react-core/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/coFeed.d.ts +12 -0
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +19 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +61 -11
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts +2 -0
- package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts.map +1 -0
- package/dist/tools/testing.d.ts.map +1 -1
- package/package.json +27 -11
- package/src/media/create-image/browser.ts +161 -0
- package/src/media/create-image/react-native.ts +158 -0
- package/src/media/create-image/server.test.ts +74 -0
- package/src/media/create-image/server.ts +181 -0
- package/src/media/{create-image.test.ts → create-image-factory.test.ts} +1 -1
- package/src/media/{create-image.ts → create-image-factory.ts} +22 -12
- package/src/media/exports.ts +2 -0
- package/src/media/index.browser.ts +2 -150
- package/src/media/index.native.ts +2 -166
- package/src/media/index.server.ts +2 -0
- package/src/media/index.ts +16 -8
- package/src/tools/coValues/account.ts +3 -1
- package/src/tools/coValues/coFeed.ts +5 -0
- package/src/tools/coValues/coMap.ts +3 -1
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +19 -0
- package/src/tools/subscribe/CoValueCoreSubscription.test.ts +1000 -0
- package/src/tools/subscribe/CoValueCoreSubscription.ts +179 -43
- package/src/tools/tests/account.test.ts +12 -0
- package/src/tools/tests/coFeed.test.ts +25 -0
- package/src/tools/tests/coList.test.ts +20 -0
- package/src/tools/tests/coMap.record.test.ts +1 -0
- package/src/tools/tests/coMap.test.ts +12 -2
- package/tsup.config.ts +1 -0
- package/dist/chunk-IERUTUXB.js.map +0 -1
- package/dist/media/chunk-KR2V6X2N.js.map +0 -1
- package/dist/media/create-image.d.ts.map +0 -1
- package/dist/media/create-image.test.d.ts +0 -2
- package/dist/media/create-image.test.d.ts.map +0 -1
@@ -0,0 +1,1000 @@
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
2
|
+
import { Group, co, z } from "../exports.js";
|
3
|
+
import { CoValueCoreSubscription } from "./CoValueCoreSubscription.js";
|
4
|
+
import {
|
5
|
+
createJazzTestAccount,
|
6
|
+
getPeerConnectedToTestSyncServer,
|
7
|
+
setupJazzTestSync,
|
8
|
+
} from "../testing.js";
|
9
|
+
import { waitFor } from "../tests/utils.js";
|
10
|
+
|
11
|
+
beforeEach(async () => {
|
12
|
+
await setupJazzTestSync();
|
13
|
+
|
14
|
+
// Create a test account for each test
|
15
|
+
await createJazzTestAccount({
|
16
|
+
isCurrentActiveAccount: true,
|
17
|
+
creationProps: { name: "Hermes Puggington" },
|
18
|
+
});
|
19
|
+
});
|
20
|
+
|
21
|
+
describe("CoValueCoreSubscription", async () => {
|
22
|
+
/**
|
23
|
+
* Tests scenarios where the CoValue is immediately available
|
24
|
+
* (already loaded in memory, no async loading required)
|
25
|
+
*/
|
26
|
+
describe("immediate availability scenarios", () => {
|
27
|
+
test("should emit immediately when CoValue is available and no branch requested", async () => {
|
28
|
+
const Person = co.map({
|
29
|
+
name: z.string(),
|
30
|
+
age: z.number(),
|
31
|
+
});
|
32
|
+
type Person = co.loaded<typeof Person>;
|
33
|
+
|
34
|
+
// Create a person that's immediately available
|
35
|
+
const person = Person.create({ name: "John", age: 30 });
|
36
|
+
let lastResult: any = null;
|
37
|
+
const listener = vi.fn();
|
38
|
+
|
39
|
+
// Subscribe to the person without requesting a specific branch
|
40
|
+
const subscription = new CoValueCoreSubscription(
|
41
|
+
person.$jazz.localNode,
|
42
|
+
person.$jazz.id,
|
43
|
+
(result) => {
|
44
|
+
lastResult = result;
|
45
|
+
listener(result);
|
46
|
+
},
|
47
|
+
);
|
48
|
+
|
49
|
+
// Should immediately call the listener since CoValue is available
|
50
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
51
|
+
expect(lastResult.get("name")).toEqual("John");
|
52
|
+
|
53
|
+
subscription.unsubscribe();
|
54
|
+
});
|
55
|
+
|
56
|
+
test("should subscribe to branch when CoValue is available and branch is requested and available", async () => {
|
57
|
+
const Person = co.map({
|
58
|
+
name: z.string(),
|
59
|
+
age: z.number(),
|
60
|
+
});
|
61
|
+
|
62
|
+
// Create a person that's immediately available
|
63
|
+
const person = Person.create({ name: "John", age: 30 });
|
64
|
+
let lastResult: any = null;
|
65
|
+
const listener = vi.fn();
|
66
|
+
|
67
|
+
// Create a branch on the person with modified data
|
68
|
+
const branch = person.$jazz.raw.core.createBranch(
|
69
|
+
"main",
|
70
|
+
person.$jazz.owner.$jazz.raw.id,
|
71
|
+
);
|
72
|
+
|
73
|
+
// @ts-ignore Update the person name in the branch
|
74
|
+
branch.getCurrentContent().set("name", "Jane");
|
75
|
+
|
76
|
+
// Subscribe to the specific branch
|
77
|
+
const subscription = new CoValueCoreSubscription(
|
78
|
+
person.$jazz.localNode,
|
79
|
+
person.$jazz.id,
|
80
|
+
(result) => {
|
81
|
+
lastResult = result;
|
82
|
+
listener(result);
|
83
|
+
},
|
84
|
+
false,
|
85
|
+
{ name: "main", owner: person.$jazz.owner },
|
86
|
+
);
|
87
|
+
|
88
|
+
// Should immediately call the listener with branch data
|
89
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
90
|
+
expect(lastResult.get("name")).toEqual("Jane");
|
91
|
+
|
92
|
+
subscription.unsubscribe();
|
93
|
+
});
|
94
|
+
|
95
|
+
test("should fall through to loading when CoValue is available but branch is not available", async () => {
|
96
|
+
const Person = co.map({
|
97
|
+
name: z.string(),
|
98
|
+
age: z.number(),
|
99
|
+
});
|
100
|
+
|
101
|
+
// Create a person that's immediately available
|
102
|
+
const person = Person.create({ name: "John", age: 30 });
|
103
|
+
let lastResult: any = null;
|
104
|
+
const listener = vi.fn();
|
105
|
+
|
106
|
+
// Request a branch that doesn't exist yet
|
107
|
+
const subscription = new CoValueCoreSubscription(
|
108
|
+
person.$jazz.localNode,
|
109
|
+
person.$jazz.id,
|
110
|
+
(result) => {
|
111
|
+
lastResult = result;
|
112
|
+
listener(result);
|
113
|
+
},
|
114
|
+
false,
|
115
|
+
{ name: "main" },
|
116
|
+
);
|
117
|
+
|
118
|
+
// Should not call listener immediately since branch isn't available
|
119
|
+
expect(listener).not.toHaveBeenCalled();
|
120
|
+
|
121
|
+
// Wait for the branch to be created and loaded
|
122
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
123
|
+
|
124
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
125
|
+
|
126
|
+
// Should return the branch, that contains the source data
|
127
|
+
expect(lastResult.get("name")).toEqual("John");
|
128
|
+
expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
|
129
|
+
|
130
|
+
subscription.unsubscribe();
|
131
|
+
});
|
132
|
+
});
|
133
|
+
|
134
|
+
/**
|
135
|
+
* Tests scenarios where the CoValue needs to be loaded asynchronously
|
136
|
+
* (not currently in memory, requires network/sync operations)
|
137
|
+
*/
|
138
|
+
describe("loading scenarios", () => {
|
139
|
+
test("should emit in async when CoValue is available and no branch requested", async () => {
|
140
|
+
const Person = co.map({
|
141
|
+
name: z.string(),
|
142
|
+
age: z.number(),
|
143
|
+
});
|
144
|
+
const bob = await createJazzTestAccount();
|
145
|
+
|
146
|
+
// Create a person on a different account that bob doesn't have access to yet
|
147
|
+
// The sync is delayed by a queueMicrotask, making the load async
|
148
|
+
const person = Person.create(
|
149
|
+
{ name: "John", age: 30 },
|
150
|
+
Group.create().makePublic("writer"),
|
151
|
+
);
|
152
|
+
|
153
|
+
let lastResult: any = null;
|
154
|
+
const listener = vi.fn();
|
155
|
+
|
156
|
+
// Subscribe to a CoValue that needs to be loaded
|
157
|
+
const subscription = new CoValueCoreSubscription(
|
158
|
+
bob.$jazz.localNode,
|
159
|
+
person.$jazz.id,
|
160
|
+
(result) => {
|
161
|
+
lastResult = result;
|
162
|
+
listener(result);
|
163
|
+
},
|
164
|
+
);
|
165
|
+
|
166
|
+
// Should not call listener immediately since CoValue needs to be loaded
|
167
|
+
expect(listener).not.toHaveBeenCalled();
|
168
|
+
|
169
|
+
// Wait for the async loading to complete
|
170
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
171
|
+
|
172
|
+
// Should call listener with the loaded value
|
173
|
+
expect(lastResult.get("name")).toEqual("John");
|
174
|
+
expect(lastResult.id).toBe(person.$jazz.id);
|
175
|
+
|
176
|
+
subscription.unsubscribe();
|
177
|
+
});
|
178
|
+
|
179
|
+
test("should handle loading when CoValue is not available and branch is requested", async () => {
|
180
|
+
const Person = co.map({
|
181
|
+
name: z.string(),
|
182
|
+
age: z.number(),
|
183
|
+
});
|
184
|
+
const bob = await createJazzTestAccount();
|
185
|
+
|
186
|
+
// Create a person on a different account that bob doesn't have access to yet
|
187
|
+
const person = Person.create(
|
188
|
+
{ name: "John", age: 30 },
|
189
|
+
Group.create().makePublic("writer"),
|
190
|
+
);
|
191
|
+
|
192
|
+
let lastResult: any = null;
|
193
|
+
const listener = vi.fn();
|
194
|
+
|
195
|
+
// Request both the CoValue and a specific branch
|
196
|
+
const subscription = new CoValueCoreSubscription(
|
197
|
+
bob.$jazz.localNode,
|
198
|
+
person.$jazz.id,
|
199
|
+
(result) => {
|
200
|
+
lastResult = result;
|
201
|
+
listener(result);
|
202
|
+
},
|
203
|
+
false,
|
204
|
+
{ name: "main" },
|
205
|
+
);
|
206
|
+
|
207
|
+
// Should not call listener immediately since both CoValue and branch need to be loaded
|
208
|
+
expect(listener).not.toHaveBeenCalled();
|
209
|
+
|
210
|
+
// Wait for the async loading to complete
|
211
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
212
|
+
|
213
|
+
// Should return the branch, that contains the source data
|
214
|
+
expect(lastResult.get("name")).toEqual("John");
|
215
|
+
expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
|
216
|
+
|
217
|
+
subscription.unsubscribe();
|
218
|
+
});
|
219
|
+
});
|
220
|
+
|
221
|
+
/**
|
222
|
+
* Tests scenarios involving branch checkout operations
|
223
|
+
* (creating, accessing, and working with different branches of CoValues)
|
224
|
+
*/
|
225
|
+
describe("branch checkout scenarios", () => {
|
226
|
+
test("should handle successful branch checkout when source is not available", async () => {
|
227
|
+
const Person = co.map({
|
228
|
+
name: z.string(),
|
229
|
+
age: z.number(),
|
230
|
+
});
|
231
|
+
const bob = await createJazzTestAccount();
|
232
|
+
|
233
|
+
// Create a person on a different account that bob doesn't have access to yet
|
234
|
+
const person = Person.create(
|
235
|
+
{ name: "John", age: 30 },
|
236
|
+
Group.create().makePublic("writer"),
|
237
|
+
);
|
238
|
+
|
239
|
+
// Create a branch on the person with modified data
|
240
|
+
const branch = person.$jazz.raw.core.createBranch(
|
241
|
+
"main",
|
242
|
+
person.$jazz.owner.$jazz.raw.id,
|
243
|
+
);
|
244
|
+
|
245
|
+
// @ts-ignore Update the person name in the branch
|
246
|
+
branch.getCurrentContent().set("name", "Jane");
|
247
|
+
|
248
|
+
let lastResult: any = null;
|
249
|
+
const listener = vi.fn();
|
250
|
+
|
251
|
+
// Subscribe to the specific branch
|
252
|
+
const subscription = new CoValueCoreSubscription(
|
253
|
+
bob.$jazz.localNode,
|
254
|
+
person.$jazz.id,
|
255
|
+
(result) => {
|
256
|
+
lastResult = result;
|
257
|
+
listener(result);
|
258
|
+
},
|
259
|
+
false,
|
260
|
+
{ name: "main", owner: person.$jazz.owner },
|
261
|
+
);
|
262
|
+
|
263
|
+
// Should not call listener immediately since source isn't available
|
264
|
+
expect(listener).not.toHaveBeenCalled();
|
265
|
+
|
266
|
+
// Wait for the branch checkout to complete
|
267
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
268
|
+
|
269
|
+
// Should return the branch data
|
270
|
+
await waitFor(() => expect(lastResult.get("name")).toEqual("Jane"));
|
271
|
+
expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
|
272
|
+
|
273
|
+
subscription.unsubscribe();
|
274
|
+
});
|
275
|
+
|
276
|
+
test("should create a private branch when a custom owner id is provided", async () => {
|
277
|
+
const Person = co.map({
|
278
|
+
name: z.string(),
|
279
|
+
age: z.number(),
|
280
|
+
});
|
281
|
+
const bob = await createJazzTestAccount();
|
282
|
+
|
283
|
+
// Create a person on a different account that bob doesn't have access to yet
|
284
|
+
const person = Person.create(
|
285
|
+
{ name: "John", age: 30 },
|
286
|
+
Group.create().makePublic(), // Only read access
|
287
|
+
);
|
288
|
+
|
289
|
+
// Create a branch on the person using the current owner id
|
290
|
+
const branch = person.$jazz.raw.core.createBranch(
|
291
|
+
"main",
|
292
|
+
person.$jazz.owner.$jazz.raw.id,
|
293
|
+
);
|
294
|
+
|
295
|
+
// @ts-ignore Update the person name in the branch
|
296
|
+
branch.getCurrentContent().set("name", "Jane");
|
297
|
+
|
298
|
+
let lastResult: any = null;
|
299
|
+
const listener = vi.fn();
|
300
|
+
|
301
|
+
// Wait for the branch to sync before subscribing
|
302
|
+
await branch.waitForSync();
|
303
|
+
|
304
|
+
// Subscribe with bob's ID as the owner, creating a private branch
|
305
|
+
const subscription = new CoValueCoreSubscription(
|
306
|
+
bob.$jazz.localNode,
|
307
|
+
person.$jazz.id,
|
308
|
+
(result) => {
|
309
|
+
lastResult = result;
|
310
|
+
listener(result);
|
311
|
+
},
|
312
|
+
false,
|
313
|
+
{ name: "main", owner: bob },
|
314
|
+
);
|
315
|
+
|
316
|
+
// Should not call listener immediately since private branch needs to be created
|
317
|
+
expect(listener).not.toHaveBeenCalled();
|
318
|
+
|
319
|
+
// Wait for the private branch creation to complete
|
320
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
321
|
+
|
322
|
+
// Should return the source data (not branch data) since it's a private branch
|
323
|
+
expect(lastResult.get("name")).toEqual("John");
|
324
|
+
expect(lastResult.core.getGroup().id).toBe(bob.$jazz.id); // Should be owned by bob
|
325
|
+
expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
|
326
|
+
expect(lastResult.id).not.toBe(branch.id); // Should not be the original branch
|
327
|
+
|
328
|
+
// Should have write access to the private branch, even though source gives only read access
|
329
|
+
lastResult.set("name", "Guido");
|
330
|
+
expect(lastResult.get("name")).toEqual("Guido");
|
331
|
+
|
332
|
+
subscription.unsubscribe();
|
333
|
+
});
|
334
|
+
});
|
335
|
+
|
336
|
+
describe("error handling scenarios", () => {
|
337
|
+
test("should handle return unavailable when the id is invalid", async () => {
|
338
|
+
const bob = await createJazzTestAccount();
|
339
|
+
const invalidId = "invalid-co-value-id";
|
340
|
+
|
341
|
+
let lastResult: any = null;
|
342
|
+
const listener = vi.fn();
|
343
|
+
|
344
|
+
// Try to subscribe to an invalid CoValue ID
|
345
|
+
const subscription = new CoValueCoreSubscription(
|
346
|
+
bob.$jazz.localNode,
|
347
|
+
invalidId,
|
348
|
+
(result) => {
|
349
|
+
lastResult = result;
|
350
|
+
listener(result);
|
351
|
+
},
|
352
|
+
);
|
353
|
+
|
354
|
+
// Should not call listener immediately since ID is invalid
|
355
|
+
expect(listener).not.toHaveBeenCalled();
|
356
|
+
|
357
|
+
// Wait for the error handling to complete
|
358
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
359
|
+
|
360
|
+
// Should report unavailable when loading fails
|
361
|
+
expect(lastResult).toBe("unavailable");
|
362
|
+
|
363
|
+
subscription.unsubscribe();
|
364
|
+
});
|
365
|
+
|
366
|
+
test("should handle return unavailable when the id is invalid and a branch is requested", async () => {
|
367
|
+
const bob = await createJazzTestAccount();
|
368
|
+
const invalidId = "invalid-co-value-id";
|
369
|
+
|
370
|
+
let lastResult: any = null;
|
371
|
+
const listener = vi.fn();
|
372
|
+
|
373
|
+
// Try to subscribe to an invalid CoValue ID with branch request
|
374
|
+
const subscription = new CoValueCoreSubscription(
|
375
|
+
bob.$jazz.localNode,
|
376
|
+
invalidId,
|
377
|
+
(result) => {
|
378
|
+
lastResult = result;
|
379
|
+
listener(result);
|
380
|
+
},
|
381
|
+
false,
|
382
|
+
{ name: "main", owner: bob },
|
383
|
+
);
|
384
|
+
|
385
|
+
// Should not call listener immediately since ID is invalid
|
386
|
+
expect(listener).not.toHaveBeenCalled();
|
387
|
+
|
388
|
+
// Wait for the error handling to complete
|
389
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
390
|
+
|
391
|
+
// Should report unavailable when loading fails
|
392
|
+
expect(lastResult).toBe("unavailable");
|
393
|
+
|
394
|
+
subscription.unsubscribe();
|
395
|
+
});
|
396
|
+
|
397
|
+
test("should handle return unavailable when the owner is unavailable", async () => {
|
398
|
+
const Person = co.map({
|
399
|
+
name: z.string(),
|
400
|
+
age: z.number(),
|
401
|
+
});
|
402
|
+
|
403
|
+
const alice = await createJazzTestAccount();
|
404
|
+
|
405
|
+
// Disconnect all peers to not sync the unavailable group
|
406
|
+
alice.$jazz.localNode.syncManager
|
407
|
+
.getServerPeers(alice.$jazz.raw.id)
|
408
|
+
.forEach((peer) => peer.gracefulShutdown());
|
409
|
+
|
410
|
+
const unavailableGroup = Group.create(alice).makePublic("writer");
|
411
|
+
|
412
|
+
const bob = await createJazzTestAccount();
|
413
|
+
|
414
|
+
// Create a person that bob can access
|
415
|
+
const person = Person.create(
|
416
|
+
{ name: "John", age: 30 },
|
417
|
+
Group.create().makePublic("writer"),
|
418
|
+
);
|
419
|
+
let lastResult: any = null;
|
420
|
+
const listener = vi.fn();
|
421
|
+
|
422
|
+
// Try to subscribe with an invalid owner ID
|
423
|
+
const subscription = new CoValueCoreSubscription(
|
424
|
+
bob.$jazz.localNode,
|
425
|
+
person.$jazz.id,
|
426
|
+
(result) => {
|
427
|
+
lastResult = result;
|
428
|
+
listener(result);
|
429
|
+
},
|
430
|
+
true,
|
431
|
+
{ name: "main", owner: unavailableGroup },
|
432
|
+
);
|
433
|
+
|
434
|
+
// Should not call listener immediately since owner is unavailable
|
435
|
+
expect(listener).not.toHaveBeenCalled();
|
436
|
+
|
437
|
+
// Wait for the error handling to complete
|
438
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
439
|
+
|
440
|
+
// Should report unavailable when loading fails
|
441
|
+
expect(lastResult).toBe("unavailable");
|
442
|
+
|
443
|
+
subscription.unsubscribe();
|
444
|
+
});
|
445
|
+
});
|
446
|
+
|
447
|
+
/**
|
448
|
+
* Tests scenarios where CoValues transition from unavailable to available
|
449
|
+
*/
|
450
|
+
describe("resolving an unavailable covalue", () => {
|
451
|
+
test("should handle state changes when source becomes available", async () => {
|
452
|
+
const Person = co.map({
|
453
|
+
name: z.string(),
|
454
|
+
age: z.number(),
|
455
|
+
});
|
456
|
+
const bob = await createJazzTestAccount();
|
457
|
+
|
458
|
+
// Create a person that bob can access
|
459
|
+
const person = Person.create(
|
460
|
+
{ name: "John", age: 30 },
|
461
|
+
Group.create().makePublic("writer"),
|
462
|
+
);
|
463
|
+
|
464
|
+
// Disconnect all peers to make the CoValue unavailable
|
465
|
+
bob.$jazz.localNode.syncManager
|
466
|
+
.getServerPeers(person.$jazz.raw.id)
|
467
|
+
.forEach((peer) => peer.gracefulShutdown());
|
468
|
+
|
469
|
+
let lastResult: any = null;
|
470
|
+
const listener = vi.fn();
|
471
|
+
|
472
|
+
// Subscribe to the now-unavailable CoValue with branch request
|
473
|
+
const subscription = new CoValueCoreSubscription(
|
474
|
+
bob.$jazz.localNode,
|
475
|
+
person.$jazz.id,
|
476
|
+
(result) => {
|
477
|
+
lastResult = result;
|
478
|
+
listener(result);
|
479
|
+
},
|
480
|
+
false,
|
481
|
+
{ name: "main" },
|
482
|
+
);
|
483
|
+
|
484
|
+
// Wait for the initial unavailable state
|
485
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
486
|
+
|
487
|
+
// Clear the listener to track new calls
|
488
|
+
listener.mockClear();
|
489
|
+
|
490
|
+
// Reconnect to make the CoValue available again
|
491
|
+
bob.$jazz.localNode.syncManager.addPeer(
|
492
|
+
getPeerConnectedToTestSyncServer(),
|
493
|
+
);
|
494
|
+
|
495
|
+
// Wait for the CoValue to become available
|
496
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
497
|
+
|
498
|
+
// Should return the source data when branch isn't available
|
499
|
+
expect(lastResult.get("name")).toEqual("John");
|
500
|
+
expect(lastResult.id).not.toBe(person.$jazz.id); // Should be a different instance
|
501
|
+
|
502
|
+
subscription.unsubscribe();
|
503
|
+
});
|
504
|
+
|
505
|
+
test("should handle state changes when source becomes available and no branch requested", async () => {
|
506
|
+
const Person = co.map({
|
507
|
+
name: z.string(),
|
508
|
+
age: z.number(),
|
509
|
+
});
|
510
|
+
const bob = await createJazzTestAccount();
|
511
|
+
|
512
|
+
// Create a person that bob can access
|
513
|
+
const person = Person.create(
|
514
|
+
{ name: "John", age: 30 },
|
515
|
+
Group.create().makePublic("writer"),
|
516
|
+
);
|
517
|
+
|
518
|
+
// Disconnect all peers to make the CoValue unavailable
|
519
|
+
bob.$jazz.localNode.syncManager
|
520
|
+
.getServerPeers(person.$jazz.raw.id)
|
521
|
+
.forEach((peer) => peer.gracefulShutdown());
|
522
|
+
|
523
|
+
let lastResult: any = null;
|
524
|
+
const listener = vi.fn();
|
525
|
+
|
526
|
+
// Subscribe to the now-unavailable CoValue without branch request
|
527
|
+
const subscription = new CoValueCoreSubscription(
|
528
|
+
bob.$jazz.localNode,
|
529
|
+
person.$jazz.id,
|
530
|
+
(result) => {
|
531
|
+
lastResult = result;
|
532
|
+
listener(result);
|
533
|
+
},
|
534
|
+
false,
|
535
|
+
);
|
536
|
+
|
537
|
+
// Wait for the initial unavailable state
|
538
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
539
|
+
|
540
|
+
// Clear the listener to track new calls
|
541
|
+
listener.mockClear();
|
542
|
+
|
543
|
+
// Reconnect to make the CoValue available again
|
544
|
+
bob.$jazz.localNode.syncManager.addPeer(
|
545
|
+
getPeerConnectedToTestSyncServer(),
|
546
|
+
);
|
547
|
+
|
548
|
+
// Wait for the CoValue to become available
|
549
|
+
await waitFor(() => expect(listener).toHaveBeenCalled());
|
550
|
+
|
551
|
+
// Should return the original CoValue when no branch is requested
|
552
|
+
expect(lastResult.get("name")).toEqual("John");
|
553
|
+
expect(lastResult.id).toBe(person.$jazz.id);
|
554
|
+
|
555
|
+
subscription.unsubscribe();
|
556
|
+
});
|
557
|
+
});
|
558
|
+
|
559
|
+
/**
|
560
|
+
* Tests unsubscribe behavior in various scenarios
|
561
|
+
* (immediate unsubscribe, multiple calls, during async operations)
|
562
|
+
*/
|
563
|
+
describe("unsubscribe scenarios", () => {
|
564
|
+
test("should properly unsubscribe when called", async () => {
|
565
|
+
const Person = co.map({
|
566
|
+
name: z.string(),
|
567
|
+
age: z.number(),
|
568
|
+
});
|
569
|
+
|
570
|
+
// Create a person that's immediately available
|
571
|
+
const person = Person.create({ name: "John", age: 30 });
|
572
|
+
const listener = vi.fn();
|
573
|
+
|
574
|
+
// Subscribe to the person
|
575
|
+
const subscription = new CoValueCoreSubscription(
|
576
|
+
person.$jazz.localNode,
|
577
|
+
person.$jazz.id,
|
578
|
+
(value) => {
|
579
|
+
listener(value);
|
580
|
+
},
|
581
|
+
);
|
582
|
+
|
583
|
+
// Should call listener once for initial value
|
584
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
585
|
+
|
586
|
+
// Unsubscribe from updates
|
587
|
+
subscription.unsubscribe();
|
588
|
+
|
589
|
+
// Update the person to trigger subscription callback
|
590
|
+
person.$jazz.set("name", "Jane");
|
591
|
+
|
592
|
+
// Listener should not be called after unsubscribe
|
593
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
594
|
+
});
|
595
|
+
|
596
|
+
test("should handle multiple unsubscribe calls gracefully", async () => {
|
597
|
+
const Person = co.map({
|
598
|
+
name: z.string(),
|
599
|
+
age: z.number(),
|
600
|
+
});
|
601
|
+
|
602
|
+
// Create a person that's immediately available
|
603
|
+
const person = Person.create({ name: "John", age: 30 });
|
604
|
+
let lastResult: any = null;
|
605
|
+
const listener = vi.fn();
|
606
|
+
|
607
|
+
// Subscribe to the person
|
608
|
+
const subscription = new CoValueCoreSubscription(
|
609
|
+
person.$jazz.localNode,
|
610
|
+
person.$jazz.id,
|
611
|
+
(result) => {
|
612
|
+
lastResult = result;
|
613
|
+
listener(result);
|
614
|
+
},
|
615
|
+
);
|
616
|
+
|
617
|
+
// Should call listener once for initial value
|
618
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
619
|
+
|
620
|
+
// Call unsubscribe multiple times
|
621
|
+
subscription.unsubscribe();
|
622
|
+
subscription.unsubscribe(); // Second call should not cause issues
|
623
|
+
|
624
|
+
// Update the person to trigger subscription callback
|
625
|
+
person.$jazz.set("name", "Jane");
|
626
|
+
|
627
|
+
// Listener should not be called after unsubscribe
|
628
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
629
|
+
});
|
630
|
+
|
631
|
+
test("should unsubscribe during async operations", async () => {
|
632
|
+
const Person = co.map({
|
633
|
+
name: z.string(),
|
634
|
+
age: z.number(),
|
635
|
+
});
|
636
|
+
const bob = await createJazzTestAccount();
|
637
|
+
|
638
|
+
// Create a person on a different account that bob doesn't have access to yet
|
639
|
+
const person = Person.create(
|
640
|
+
{ name: "John", age: 30 },
|
641
|
+
Group.create().makePublic("writer"),
|
642
|
+
);
|
643
|
+
|
644
|
+
let lastResult: any = null;
|
645
|
+
const listener = vi.fn();
|
646
|
+
|
647
|
+
// Subscribe to a CoValue that needs to be loaded
|
648
|
+
const subscription = new CoValueCoreSubscription(
|
649
|
+
bob.$jazz.localNode,
|
650
|
+
person.$jazz.id,
|
651
|
+
(result) => {
|
652
|
+
lastResult = result;
|
653
|
+
listener(result);
|
654
|
+
},
|
655
|
+
);
|
656
|
+
|
657
|
+
// Unsubscribe immediately before the async operation completes
|
658
|
+
subscription.unsubscribe();
|
659
|
+
|
660
|
+
// Wait a bit to ensure async operations would have completed
|
661
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
662
|
+
|
663
|
+
// Listener should not be called after unsubscribe
|
664
|
+
expect(listener).toHaveBeenCalledTimes(0);
|
665
|
+
});
|
666
|
+
|
667
|
+
test("should unsubscribe during checkout operations", async () => {
|
668
|
+
const Person = co.map({
|
669
|
+
name: z.string(),
|
670
|
+
age: z.number(),
|
671
|
+
});
|
672
|
+
const bob = await createJazzTestAccount();
|
673
|
+
|
674
|
+
// Create a person on a different account that bob doesn't have access to yet
|
675
|
+
const person = Person.create(
|
676
|
+
{ name: "John", age: 30 },
|
677
|
+
Group.create().makePublic("writer"),
|
678
|
+
);
|
679
|
+
|
680
|
+
let lastResult: any = null;
|
681
|
+
const listener = vi.fn();
|
682
|
+
|
683
|
+
// Subscribe to a CoValue with branch request that needs to be loaded
|
684
|
+
const subscription = new CoValueCoreSubscription(
|
685
|
+
bob.$jazz.localNode,
|
686
|
+
person.$jazz.id,
|
687
|
+
(result) => {
|
688
|
+
lastResult = result;
|
689
|
+
listener(result);
|
690
|
+
},
|
691
|
+
false,
|
692
|
+
{ name: "main" },
|
693
|
+
);
|
694
|
+
|
695
|
+
// Unsubscribe immediately before the async operation completes
|
696
|
+
subscription.unsubscribe();
|
697
|
+
|
698
|
+
// Wait a bit to ensure async operations would have completed
|
699
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
700
|
+
|
701
|
+
// Listener should not be called after unsubscribe
|
702
|
+
expect(listener).toHaveBeenCalledTimes(0);
|
703
|
+
});
|
704
|
+
});
|
705
|
+
|
706
|
+
/**
|
707
|
+
* Tests concurrent operations and multiple subscriptions
|
708
|
+
* (multiple subscribers to same CoValue, same branch, etc.)
|
709
|
+
*/
|
710
|
+
describe("concurrent operations", () => {
|
711
|
+
test("should handle multiple subscriptions to the same CoValue", async () => {
|
712
|
+
const Person = co.map({
|
713
|
+
name: z.string(),
|
714
|
+
age: z.number(),
|
715
|
+
});
|
716
|
+
const bob = await createJazzTestAccount();
|
717
|
+
|
718
|
+
// Create a person on a different account that bob doesn't have access to yet
|
719
|
+
const person = Person.create(
|
720
|
+
{ name: "John", age: 30 },
|
721
|
+
Group.create().makePublic("writer"),
|
722
|
+
);
|
723
|
+
|
724
|
+
let lastResultSubscription1: any = null;
|
725
|
+
let lastResultSubscription2: any = null;
|
726
|
+
const listener = vi.fn();
|
727
|
+
|
728
|
+
// Subscribe to the CoValue with branch request
|
729
|
+
const subscription1 = new CoValueCoreSubscription(
|
730
|
+
bob.$jazz.localNode,
|
731
|
+
person.$jazz.id,
|
732
|
+
(result) => {
|
733
|
+
lastResultSubscription1 = result;
|
734
|
+
listener(result);
|
735
|
+
},
|
736
|
+
);
|
737
|
+
const subscription2 = new CoValueCoreSubscription(
|
738
|
+
bob.$jazz.localNode,
|
739
|
+
person.$jazz.id,
|
740
|
+
(result) => {
|
741
|
+
lastResultSubscription2 = result;
|
742
|
+
listener(result);
|
743
|
+
},
|
744
|
+
);
|
745
|
+
|
746
|
+
await waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
|
747
|
+
|
748
|
+
// Should return the same instance of the RawCoValue
|
749
|
+
expect(lastResultSubscription1).toBe(lastResultSubscription2);
|
750
|
+
expect(lastResultSubscription1.get("name")).toEqual("John");
|
751
|
+
expect(lastResultSubscription1.id).toBe(person.$jazz.id);
|
752
|
+
|
753
|
+
subscription1.unsubscribe();
|
754
|
+
subscription2.unsubscribe();
|
755
|
+
});
|
756
|
+
|
757
|
+
test("should handle multiple subscriptions to the same branch", async () => {
|
758
|
+
const Person = co.map({
|
759
|
+
name: z.string(),
|
760
|
+
age: z.number(),
|
761
|
+
});
|
762
|
+
const bob = await createJazzTestAccount();
|
763
|
+
|
764
|
+
// Create a person on a different account that bob doesn't have access to yet
|
765
|
+
const person = Person.create(
|
766
|
+
{ name: "John", age: 30 },
|
767
|
+
Group.create().makePublic("writer"),
|
768
|
+
);
|
769
|
+
|
770
|
+
let lastResultSubscription1: any = null;
|
771
|
+
let lastResultSubscription2: any = null;
|
772
|
+
const listener = vi.fn();
|
773
|
+
|
774
|
+
// Subscribe to the CoValue with branch request
|
775
|
+
const subscription1 = new CoValueCoreSubscription(
|
776
|
+
bob.$jazz.localNode,
|
777
|
+
person.$jazz.id,
|
778
|
+
(result) => {
|
779
|
+
lastResultSubscription1 = result;
|
780
|
+
listener(result);
|
781
|
+
},
|
782
|
+
false,
|
783
|
+
{ name: "main" },
|
784
|
+
);
|
785
|
+
const subscription2 = new CoValueCoreSubscription(
|
786
|
+
bob.$jazz.localNode,
|
787
|
+
person.$jazz.id,
|
788
|
+
(result) => {
|
789
|
+
lastResultSubscription2 = result;
|
790
|
+
listener(result);
|
791
|
+
},
|
792
|
+
false,
|
793
|
+
{ name: "main" },
|
794
|
+
);
|
795
|
+
|
796
|
+
// Wait for the async loading to complete
|
797
|
+
await waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
|
798
|
+
|
799
|
+
// Should return the same instance of the RawCoValue
|
800
|
+
expect(lastResultSubscription1).toBe(lastResultSubscription2);
|
801
|
+
expect(lastResultSubscription1.get("name")).toEqual("John");
|
802
|
+
expect(lastResultSubscription1.id).not.toBe(person.$jazz.id);
|
803
|
+
|
804
|
+
subscription1.unsubscribe();
|
805
|
+
subscription2.unsubscribe();
|
806
|
+
});
|
807
|
+
});
|
808
|
+
|
809
|
+
/**
|
810
|
+
* Tests real-time update scenarios
|
811
|
+
* (property changes, branch updates, rapid successive changes)
|
812
|
+
*/
|
813
|
+
describe("updates", () => {
|
814
|
+
test("should receive updates when CoValue properties change", async () => {
|
815
|
+
// Define a Person schema with optional email field
|
816
|
+
const Person = co.map({
|
817
|
+
name: z.string(),
|
818
|
+
age: z.number(),
|
819
|
+
email: z.string().optional(),
|
820
|
+
});
|
821
|
+
|
822
|
+
// Create a person that's immediately available
|
823
|
+
const person = Person.create({ name: "John", age: 30 });
|
824
|
+
let lastResult: any = null;
|
825
|
+
const listener = vi.fn();
|
826
|
+
|
827
|
+
// Subscribe to the person
|
828
|
+
const subscription = new CoValueCoreSubscription(
|
829
|
+
person.$jazz.localNode,
|
830
|
+
person.$jazz.id,
|
831
|
+
(result) => {
|
832
|
+
lastResult = result;
|
833
|
+
listener(result);
|
834
|
+
},
|
835
|
+
);
|
836
|
+
|
837
|
+
// Initial call with default values
|
838
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
839
|
+
expect(lastResult.get("name")).toEqual("John");
|
840
|
+
|
841
|
+
// Update properties to trigger subscription callbacks
|
842
|
+
person.$jazz.set("age", 31);
|
843
|
+
person.$jazz.set("email", "john@example.com");
|
844
|
+
|
845
|
+
// Wait for all updates to be processed
|
846
|
+
await waitFor(() => expect(listener).toHaveBeenCalledTimes(3));
|
847
|
+
|
848
|
+
// Check that we received updates for each change
|
849
|
+
expect(lastResult.get("age")).toEqual(31);
|
850
|
+
expect(lastResult.get("email")).toEqual("john@example.com");
|
851
|
+
expect(lastResult.get("name")).toEqual("John"); // Other properties should remain
|
852
|
+
expect(lastResult.get("age")).toEqual(31);
|
853
|
+
|
854
|
+
subscription.unsubscribe();
|
855
|
+
});
|
856
|
+
|
857
|
+
test("should receive updates when CoValue properties change in branch", async () => {
|
858
|
+
// Define a Person schema with optional email field
|
859
|
+
const Person = co.map({
|
860
|
+
name: z.string(),
|
861
|
+
age: z.number(),
|
862
|
+
email: z.string().optional(),
|
863
|
+
});
|
864
|
+
|
865
|
+
// Create a person that's immediately available
|
866
|
+
const person = Person.create({ name: "John", age: 30 });
|
867
|
+
|
868
|
+
// Create a branch on the person
|
869
|
+
const branch = person.$jazz.raw.core.createBranch(
|
870
|
+
"main",
|
871
|
+
person.$jazz.owner.$jazz.raw.id,
|
872
|
+
);
|
873
|
+
|
874
|
+
// @ts-ignore Update the person age in the branch
|
875
|
+
branch.getCurrentContent().set("age", 25);
|
876
|
+
|
877
|
+
let lastResult: any = null;
|
878
|
+
const listener = vi.fn();
|
879
|
+
|
880
|
+
// Subscribe to the specific branch
|
881
|
+
const subscription = new CoValueCoreSubscription(
|
882
|
+
person.$jazz.localNode,
|
883
|
+
person.$jazz.id,
|
884
|
+
(result) => {
|
885
|
+
lastResult = result;
|
886
|
+
listener(result);
|
887
|
+
},
|
888
|
+
false,
|
889
|
+
{ name: "main", owner: person.$jazz.owner },
|
890
|
+
);
|
891
|
+
|
892
|
+
// Initial call with branch value
|
893
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
894
|
+
expect(lastResult.get("age")).toEqual(25);
|
895
|
+
|
896
|
+
// @ts-ignore Update the person name in the branch
|
897
|
+
branch.getCurrentContent().set("name", "Jane");
|
898
|
+
|
899
|
+
// Wait for the update to be processed
|
900
|
+
await waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
|
901
|
+
|
902
|
+
// Check that we received updates for each change
|
903
|
+
expect(lastResult.get("name")).toEqual("Jane");
|
904
|
+
expect(lastResult.get("age")).toEqual(25); // Should remain from branch
|
905
|
+
|
906
|
+
subscription.unsubscribe();
|
907
|
+
});
|
908
|
+
|
909
|
+
test("should not receive updates when CoValue properties change in source", async () => {
|
910
|
+
// Define a Person schema with optional email field
|
911
|
+
const Person = co.map({
|
912
|
+
name: z.string(),
|
913
|
+
age: z.number(),
|
914
|
+
email: z.string().optional(),
|
915
|
+
});
|
916
|
+
|
917
|
+
// Create a person that's immediately available
|
918
|
+
const person = Person.create({ name: "John", age: 30 });
|
919
|
+
|
920
|
+
// Create a branch on the person
|
921
|
+
const branch = person.$jazz.raw.core.createBranch(
|
922
|
+
"main",
|
923
|
+
person.$jazz.owner.$jazz.raw.id,
|
924
|
+
);
|
925
|
+
|
926
|
+
// @ts-ignore Update the person age in the branch
|
927
|
+
branch.getCurrentContent().set("age", 25);
|
928
|
+
|
929
|
+
let lastResult: any = null;
|
930
|
+
const listener = vi.fn();
|
931
|
+
|
932
|
+
// Subscribe to the specific branch
|
933
|
+
const subscription = new CoValueCoreSubscription(
|
934
|
+
person.$jazz.localNode,
|
935
|
+
person.$jazz.id,
|
936
|
+
(result) => {
|
937
|
+
lastResult = result;
|
938
|
+
listener(result);
|
939
|
+
},
|
940
|
+
false,
|
941
|
+
{ name: "main", owner: person.$jazz.owner },
|
942
|
+
);
|
943
|
+
|
944
|
+
// Initial call with branch value
|
945
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
946
|
+
expect(lastResult.get("age")).toEqual(25);
|
947
|
+
|
948
|
+
// Update properties in the source (not the branch)
|
949
|
+
person.$jazz.set("age", 31);
|
950
|
+
person.$jazz.set("email", "john@example.com");
|
951
|
+
|
952
|
+
// Listener should not be called since we're subscribed to the branch, not the source
|
953
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
954
|
+
|
955
|
+
subscription.unsubscribe();
|
956
|
+
});
|
957
|
+
|
958
|
+
test("should handle rapid successive updates correctly", async () => {
|
959
|
+
// Define a Person schema with score field
|
960
|
+
const Person = co.map({
|
961
|
+
name: z.string(),
|
962
|
+
age: z.number(),
|
963
|
+
score: z.number(),
|
964
|
+
});
|
965
|
+
|
966
|
+
// Create a person that's immediately available
|
967
|
+
const person = Person.create({ name: "John", age: 30, score: 100 });
|
968
|
+
let lastResult: any = null;
|
969
|
+
const listener = vi.fn();
|
970
|
+
|
971
|
+
// Subscribe to the person
|
972
|
+
const subscription = new CoValueCoreSubscription(
|
973
|
+
person.$jazz.localNode,
|
974
|
+
person.$jazz.id,
|
975
|
+
(result) => {
|
976
|
+
lastResult = result;
|
977
|
+
listener(result);
|
978
|
+
},
|
979
|
+
);
|
980
|
+
|
981
|
+
// Initial call with default values
|
982
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
983
|
+
|
984
|
+
// Make rapid successive updates to test update handling
|
985
|
+
person.$jazz.set("age", 31);
|
986
|
+
person.$jazz.set("score", 150);
|
987
|
+
person.$jazz.set("name", "Jane");
|
988
|
+
person.$jazz.set("age", 32);
|
989
|
+
|
990
|
+
expect(listener).toHaveBeenCalledTimes(5);
|
991
|
+
|
992
|
+
// Check final state after all updates
|
993
|
+
expect(lastResult.get("name")).toEqual("Jane");
|
994
|
+
expect(lastResult.get("age")).toEqual(32);
|
995
|
+
expect(lastResult.get("score")).toEqual(150);
|
996
|
+
|
997
|
+
subscription.unsubscribe();
|
998
|
+
});
|
999
|
+
});
|
1000
|
+
});
|