@aerogel/plugin-local-first 0.0.0-next.4c77ee2164e8a4f72bdd920cb56ec059299ccc5f

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.
Files changed (38) hide show
  1. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +9 -0
  2. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +1 -0
  3. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +5 -0
  4. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +1 -0
  5. package/dist/aerogel-plugin-local-first.d.ts +269 -0
  6. package/dist/aerogel-plugin-local-first.js +1686 -0
  7. package/dist/aerogel-plugin-local-first.js.map +1 -0
  8. package/package.json +50 -0
  9. package/src/components/AGCloudStatus.vue +31 -0
  10. package/src/components/index.ts +3 -0
  11. package/src/index.ts +29 -0
  12. package/src/jobs/Backup.ts +98 -0
  13. package/src/jobs/Migrate.test.ts +217 -0
  14. package/src/jobs/Migrate.ts +179 -0
  15. package/src/jobs/Sync.test.ts +982 -0
  16. package/src/jobs/Sync.ts +590 -0
  17. package/src/jobs/mixins/LoadsChildren.ts +210 -0
  18. package/src/jobs/mixins/LoadsTypeIndex.ts +23 -0
  19. package/src/jobs/mixins/TracksLocalModels.ts +32 -0
  20. package/src/lib/SyncQueue.ts +94 -0
  21. package/src/lib/inference.ts +124 -0
  22. package/src/lib/mirroring.ts +327 -0
  23. package/src/lib/utils.ts +21 -0
  24. package/src/services/Cloud.state.ts +111 -0
  25. package/src/services/Cloud.ts +450 -0
  26. package/src/services/DocumentsCache.ts +35 -0
  27. package/src/testing/cloud.ts +193 -0
  28. package/src/testing/mocks/Solid.mock.ts +34 -0
  29. package/src/testing/setup.ts +32 -0
  30. package/src/testing/stubs/LegacyTask.schema.ts +15 -0
  31. package/src/testing/stubs/Movie.schema.ts +15 -0
  32. package/src/testing/stubs/Movie.ts +3 -0
  33. package/src/testing/stubs/MoviesContainer.ts +20 -0
  34. package/src/testing/stubs/Task.schema.ts +12 -0
  35. package/src/testing/stubs/Task.ts +3 -0
  36. package/src/testing/stubs/TasksList.ts +20 -0
  37. package/src/testing/stubs/Workspace.ts +15 -0
  38. package/src/types/shims.d.ts +7 -0
@@ -0,0 +1,982 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { Events } from '@aerogel/core';
3
+ import { FakeResponse, FakeServer, fakeContainerUrl, fakeDocumentUrl } from '@noeldemartin/testing';
4
+ import { arrayFind, required } from '@noeldemartin/utils';
5
+ import { Solid } from '@aerogel/plugin-solid';
6
+ import { SolidTypeRegistration } from 'soukai-solid';
7
+
8
+ import Cloud from '@aerogel/plugin-local-first/services/Cloud';
9
+
10
+ import {
11
+ containerResponse,
12
+ movieResponse,
13
+ setupCloudTests,
14
+ taskResponse,
15
+ testRegisterVariants,
16
+ tombstoneResponse,
17
+ typeIndexResponse,
18
+ } from '@aerogel/plugin-local-first/testing/cloud';
19
+ import Movie from '@aerogel/plugin-local-first/testing/stubs/Movie';
20
+ import MoviesContainer from '@aerogel/plugin-local-first/testing/stubs/MoviesContainer';
21
+ import SolidMock from '@aerogel/plugin-local-first/testing/mocks/Solid.mock';
22
+ import Workspace from '@aerogel/plugin-local-first/testing/stubs/Workspace';
23
+
24
+ describe('Sync', () => {
25
+
26
+ beforeEach(setupCloudTests);
27
+
28
+ it('Syncs containers', async () => {
29
+ // Arrange - Mint urls
30
+ const parentContainerUrl = SolidMock.requireUser().storageUrls[0];
31
+ // fakeContainerUrl({ baseUrl: parentContainerUrl });
32
+ const remoteContainerUrl = parentContainerUrl + 'remote/';
33
+ // fakeContainerUrl({ baseUrl: parentContainerUrl });
34
+ const localContainerUrl = parentContainerUrl + 'local/';
35
+
36
+ // Arrange - Prepare models
37
+ await MoviesContainer.at(parentContainerUrl).create({
38
+ url: localContainerUrl,
39
+ name: 'Local Movies',
40
+ });
41
+
42
+ // Arrange - Prepare responses
43
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
44
+
45
+ FakeServer.respond(typeIndexUrl, typeIndexResponse({ [remoteContainerUrl]: '<https://schema.org/Movie>' }));
46
+ FakeServer.respond(remoteContainerUrl, containerResponse({ name: 'Remote Movies' }));
47
+ FakeServer.respondOnce(localContainerUrl, FakeResponse.notFound()); // Tombstone check
48
+ FakeServer.respondOnce(localContainerUrl, FakeResponse.notFound()); // Check before create
49
+ FakeServer.respondOnce(localContainerUrl, FakeResponse.created()); // Create
50
+ FakeServer.respondOnce(
51
+ // Read described-by header
52
+ localContainerUrl,
53
+ FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
54
+ );
55
+ FakeServer.respondOnce(`${localContainerUrl}.meta`, FakeResponse.success()); // Update meta
56
+
57
+ // Arrange - Prepare service
58
+ await Cloud.launch();
59
+ await Cloud.register(MoviesContainer);
60
+ await Events.emit('application-ready');
61
+
62
+ // Act
63
+ const updates: number[] = [];
64
+
65
+ await Cloud.sync({ onUpdated: (progress) => updates.push(progress) });
66
+
67
+ // Assert
68
+ expect(updates).toEqual([0, 0.9, 1]);
69
+
70
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
71
+ expect(FakeServer.getRequests(remoteContainerUrl)).toHaveLength(1);
72
+ expect(FakeServer.getRequests(localContainerUrl)).toHaveLength(4);
73
+ expect(FakeServer.getRequests(localContainerUrl + '.meta')).toHaveLength(1);
74
+ expect(FakeServer.getRequests()).toHaveLength(9);
75
+ expect(FakeServer.getRequest('PATCH', typeIndexUrl)?.body).toEqualSparql(`
76
+ INSERT DATA {
77
+ <#[[.*]]>
78
+ a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
79
+ <http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Movie> ;
80
+ <http://www.w3.org/ns/solid/terms#instanceContainer> <${localContainerUrl}> .
81
+ }
82
+ `);
83
+
84
+ const typeIndex = await Solid.findPrivateTypeIndex();
85
+ const findRegistration = (url: string) => arrayFind(typeIndex?.registrations ?? [], 'instanceContainer', url);
86
+ expect(typeIndex).not.toBeNull();
87
+ expect(typeIndex?.registrations).toHaveLength(2);
88
+ expect(findRegistration(remoteContainerUrl)).toBeInstanceOf(SolidTypeRegistration);
89
+ expect(findRegistration(localContainerUrl)).toBeInstanceOf(SolidTypeRegistration);
90
+
91
+ const containers = await MoviesContainer.all();
92
+ expect(containers).toHaveLength(2);
93
+ expect(arrayFind(containers, 'name', 'Local Movies')).toBeInstanceOf(MoviesContainer);
94
+ expect(arrayFind(containers, 'name', 'Remote Movies')).toBeInstanceOf(MoviesContainer);
95
+ });
96
+
97
+ it('Syncs container documents', async () => {
98
+ // Arrange - Mint urls
99
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
100
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
101
+ const localDocumentUrl = fakeDocumentUrl({ containerUrl });
102
+ const remoteDocumentUrl = fakeDocumentUrl({ containerUrl });
103
+
104
+ // Arrange - Prepare models
105
+ const container = await MoviesContainer.at(parentContainerUrl).create({
106
+ url: containerUrl,
107
+ name: 'Remote Movies',
108
+ });
109
+
110
+ await container.relatedMovies.create({ url: `${localDocumentUrl}#it`, name: 'The Tale of Princess Kaguya' });
111
+
112
+ // Arrange - Prepare responses
113
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
114
+
115
+ FakeServer.respond(typeIndexUrl, typeIndexResponse({ [containerUrl]: '<https://schema.org/Movie>' }));
116
+ FakeServer.respond(
117
+ containerUrl,
118
+ containerResponse({
119
+ name: 'Remote Movies',
120
+ documentUrls: [remoteDocumentUrl],
121
+ createdAt: container.createdAt,
122
+ updatedAt: container.updatedAt,
123
+ }),
124
+ );
125
+ FakeServer.respond(remoteDocumentUrl, movieResponse('Spirited Away'));
126
+ FakeServer.respondOnce(localDocumentUrl, FakeResponse.notFound()); // Check before create
127
+ FakeServer.respondOnce(localDocumentUrl, FakeResponse.created()); // Create
128
+
129
+ // Arrange - Prepare service
130
+ await Cloud.launch();
131
+ await Cloud.register(MoviesContainer);
132
+ await Events.emit('application-ready');
133
+
134
+ // Act
135
+ const updates: number[] = [];
136
+
137
+ await Cloud.sync({ onUpdated: (progress) => updates.push(progress) });
138
+
139
+ // Assert
140
+ expect(updates).toEqual([0, 0.9, 0.95, 1]);
141
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
142
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(1);
143
+ expect(FakeServer.getRequests(localDocumentUrl)).toHaveLength(2);
144
+ expect(FakeServer.getRequests(remoteDocumentUrl)).toHaveLength(1);
145
+ expect(FakeServer.getRequests()).toHaveLength(5);
146
+
147
+ const movies = await Movie.from(containerUrl).all();
148
+ expect(movies).toHaveLength(2);
149
+ expect(arrayFind(movies, 'name', 'The Tale of Princess Kaguya')).toBeInstanceOf(Movie);
150
+ expect(arrayFind(movies, 'name', 'Spirited Away')).toBeInstanceOf(Movie);
151
+ });
152
+
153
+ it('Syncs documents', async () => {
154
+ // Arrange - Mint urls
155
+ const containerUrl = Solid.requireUser().storageUrls[0];
156
+ const remoteDocumentUrl = fakeDocumentUrl({ containerUrl });
157
+ const localDocumentUrl = fakeDocumentUrl({ containerUrl });
158
+
159
+ // Arrange - Prepare models
160
+ await Movie.at(containerUrl).create({
161
+ url: `${localDocumentUrl}#it`,
162
+ name: 'The Tale of Princess Kaguya',
163
+ });
164
+
165
+ // Arrange - Prepare responses
166
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
167
+
168
+ FakeServer.respond(typeIndexUrl, typeIndexResponse({ [containerUrl]: '<https://schema.org/Movie>' }));
169
+ FakeServer.respond(containerUrl, containerResponse({ name: 'Movies', documentUrls: [remoteDocumentUrl] }));
170
+ FakeServer.respond(remoteDocumentUrl, movieResponse('Spirited Away'));
171
+ FakeServer.respondOnce(localDocumentUrl, FakeResponse.notFound()); // Tombstone check
172
+ FakeServer.respondOnce(localDocumentUrl, FakeResponse.notFound()); // Check before create
173
+ FakeServer.respondOnce(localDocumentUrl, FakeResponse.created()); // Create
174
+
175
+ // Arrange - Prepare service
176
+ await Cloud.launch();
177
+ await Cloud.register(Movie);
178
+ await Events.emit('application-ready');
179
+
180
+ // Act
181
+ const updates: number[] = [];
182
+
183
+ await Cloud.sync({ onUpdated: (progress) => updates.push(progress) });
184
+
185
+ // Assert
186
+ expect(updates).toEqual([0, 0.9, 1]);
187
+
188
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
189
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(1);
190
+ expect(FakeServer.getRequests(localDocumentUrl)).toHaveLength(3);
191
+ expect(FakeServer.getRequests(remoteDocumentUrl)).toHaveLength(1);
192
+ expect(FakeServer.getRequests()).toHaveLength(6);
193
+
194
+ const movies = await Movie.all();
195
+ expect(movies).toHaveLength(2);
196
+ expect(arrayFind(movies, 'name', 'The Tale of Princess Kaguya')).toBeInstanceOf(Movie);
197
+ expect(arrayFind(movies, 'name', 'Spirited Away')).toBeInstanceOf(Movie);
198
+ });
199
+
200
+ it('Syncs individual container updates', async () => {
201
+ // Arrange - Mint urls
202
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
203
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
204
+ const movieDocumentUrl = fakeDocumentUrl({ containerUrl });
205
+
206
+ // Arrange - Prepare models
207
+ const container = await MoviesContainer.at(parentContainerUrl).create({
208
+ url: containerUrl,
209
+ name: 'Movies',
210
+ });
211
+
212
+ const movie = await container.relatedMovies.create({
213
+ url: `${movieDocumentUrl}#it`,
214
+ name: 'The Tale of Princess Kaguya',
215
+ });
216
+
217
+ // Arrange - Prepare responses
218
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
219
+ const typeIndexTurtle = `<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .`;
220
+
221
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
222
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Check before update
223
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
224
+
225
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
226
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
227
+ FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
228
+ FakeServer.respondOnce(
229
+ // Read described-by header
230
+ containerUrl,
231
+ FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
232
+ );
233
+ FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
234
+
235
+ FakeServer.respondOnce(movieDocumentUrl, FakeResponse.notFound()); // Check before create
236
+ FakeServer.respondOnce(movieDocumentUrl, FakeResponse.created()); // Create
237
+
238
+ FakeServer.respondOnce(
239
+ containerUrl,
240
+ containerResponse({
241
+ name: 'Movies',
242
+ documentUrls: [movieDocumentUrl],
243
+ createdAt: container.createdAt,
244
+ updatedAt: container.updatedAt,
245
+ }),
246
+ ); // Sync
247
+ FakeServer.respondOnce(movieDocumentUrl, FakeResponse.success(await movie.toTurtle())); // Sync children
248
+ FakeServer.respondOnce(
249
+ `${containerUrl}.meta`,
250
+ containerResponse({
251
+ name: 'Movies',
252
+ documentUrls: [movieDocumentUrl],
253
+ createdAt: container.createdAt,
254
+ updatedAt: container.updatedAt,
255
+ }),
256
+ ); // Before update
257
+ FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update
258
+
259
+ // Arrange - Prepare service
260
+ await Cloud.launch();
261
+ await Cloud.register(MoviesContainer);
262
+ await Events.emit('application-ready');
263
+
264
+ // Arrange - Initial sync
265
+ await Cloud.sync();
266
+
267
+ // Act
268
+ const updates: number[] = [];
269
+
270
+ await container.update({ name: 'Great Movies' });
271
+ await Cloud.sync({ model: container, onUpdated: (progress) => updates.push(progress) });
272
+
273
+ // Assert
274
+ expect(updates).toEqual([0, 0.9, 1]);
275
+
276
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
277
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(5);
278
+ expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(3);
279
+ expect(FakeServer.getRequests(movieDocumentUrl)).toHaveLength(3);
280
+ expect(FakeServer.getRequests()).toHaveLength(14);
281
+
282
+ expect(FakeServer.getRequests('PATCH', `${containerUrl}.meta`)[1]?.body).toEqualSparql(`
283
+ DELETE DATA {
284
+ @prefix crdt: <https://vocab.noeldemartin.com/crdt/> .
285
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
286
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
287
+
288
+ <${containerUrl}> rdfs:label "Movies" .
289
+ <${containerUrl}#metadata> crdt:updatedAt "[[createdAt][.*]]"^^xsd:dateTime .
290
+ } ;
291
+ INSERT DATA {
292
+ @prefix crdt: <https://vocab.noeldemartin.com/crdt/> .
293
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
294
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
295
+
296
+ <${containerUrl}> rdfs:label "Great Movies" .
297
+ <${containerUrl}#metadata> crdt:updatedAt "[[updatedAt][.*]]"^^xsd:dateTime .
298
+
299
+ <${containerUrl}#operation-[[.*]]>
300
+ a crdt:SetPropertyOperation ;
301
+ crdt:resource <${containerUrl}> ;
302
+ crdt:date "[[createdAt][.*]]"^^xsd:dateTime ;
303
+ crdt:property rdfs:label ;
304
+ crdt:value "Movies" .
305
+
306
+ <${containerUrl}#operation-[[.*]]>
307
+ a crdt:SetPropertyOperation ;
308
+ crdt:resource <${containerUrl}> ;
309
+ crdt:date "[[updatedAt][.*]]"^^xsd:dateTime ;
310
+ crdt:property rdfs:label ;
311
+ crdt:value "Great Movies" .
312
+ } .
313
+ `);
314
+ });
315
+
316
+ it('Syncs individual container documents', async () => {
317
+ // Arrange - Mint urls
318
+ const containerUrl = Solid.requireUser().storageUrls[0];
319
+ const documentUrl = fakeDocumentUrl({ containerUrl });
320
+
321
+ // Arrange - Prepare models
322
+ const container = await MoviesContainer.create({ url: containerUrl });
323
+
324
+ // Arrange - Prepare responses
325
+
326
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Pull
327
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Tombstone check
328
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Check before create
329
+ FakeServer.respondOnce(documentUrl, FakeResponse.created()); // Create
330
+
331
+ // Arrange - Prepare service
332
+ await Cloud.launch();
333
+ await Cloud.register(MoviesContainer);
334
+ await Events.emit('application-ready');
335
+
336
+ // Act
337
+ const updates: number[] = [];
338
+ const movie = await container.relatedMovies.create({
339
+ url: `${documentUrl}#it`,
340
+ name: 'The Tale of Princess Kaguya',
341
+ });
342
+
343
+ await Cloud.sync({ model: movie, onUpdated: (progress) => updates.push(progress) });
344
+
345
+ // Assert
346
+ expect(updates).toEqual([0, 0.9, 1]);
347
+
348
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
349
+ expect(FakeServer.getRequests()).toHaveLength(4);
350
+ });
351
+
352
+ it('Creates containers with documents', async () => {
353
+ // Arrange - Mint urls
354
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
355
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
356
+ const documentUrl = fakeDocumentUrl({ containerUrl });
357
+
358
+ // Arrange - Prepare models
359
+ const container = await MoviesContainer.at(parentContainerUrl).create({
360
+ url: containerUrl,
361
+ name: 'Movies',
362
+ });
363
+
364
+ await container.relatedMovies.create({
365
+ url: `${documentUrl}#it`,
366
+ name: 'The Tale of Princess Kaguya',
367
+ });
368
+
369
+ // Arrange - Prepare responses
370
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
371
+ const typeIndexTurtle = `<${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .`;
372
+
373
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
374
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Check before update
375
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
376
+
377
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
378
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
379
+ FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
380
+ FakeServer.respondOnce(
381
+ // Read described-by header
382
+ containerUrl,
383
+ FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
384
+ );
385
+ FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
386
+
387
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Check before create
388
+ FakeServer.respondOnce(documentUrl, FakeResponse.created()); // Create
389
+
390
+ // Arrange - Prepare service
391
+ await Cloud.launch();
392
+ await Cloud.register(MoviesContainer);
393
+ await Events.emit('application-ready');
394
+
395
+ // Act
396
+ await Cloud.sync();
397
+
398
+ // Assert
399
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
400
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(4);
401
+ expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(1);
402
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(2);
403
+ expect(FakeServer.getRequests()).toHaveLength(10);
404
+ });
405
+
406
+ it('Skips pulling fresh documents', async () => {
407
+ // Arrange - Mint urls
408
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
409
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
410
+ const childContainerUrl = fakeContainerUrl({ baseUrl: containerUrl });
411
+ const freshDocumentUrl = fakeDocumentUrl({ containerUrl });
412
+ const staleDocumentUrl = fakeDocumentUrl({ containerUrl });
413
+ const freshChildDocumentUrl = fakeDocumentUrl({ containerUrl: childContainerUrl });
414
+ const staleChildDocumentUrl = fakeDocumentUrl({ containerUrl: childContainerUrl });
415
+
416
+ // Arrange - Prepare models
417
+ const workspace = await Workspace.at(parentContainerUrl).create({
418
+ url: containerUrl,
419
+ name: 'Tasks',
420
+ });
421
+
422
+ const tasksList = await workspace.relatedLists.create({
423
+ url: childContainerUrl,
424
+ name: 'Child Tasks',
425
+ });
426
+
427
+ await workspace.relatedTasks.create({
428
+ url: `${freshDocumentUrl}#it`,
429
+ name: 'One',
430
+ });
431
+
432
+ await workspace.relatedTasks.create({
433
+ url: `${staleDocumentUrl}#it`,
434
+ name: 'Two',
435
+ });
436
+
437
+ await tasksList.relatedTasks.create({
438
+ url: `${freshChildDocumentUrl}#it`,
439
+ name: 'Three',
440
+ });
441
+
442
+ await tasksList.relatedTasks.create({
443
+ url: `${staleChildDocumentUrl}#it`,
444
+ name: 'Four',
445
+ });
446
+
447
+ // Arrange - Prepare responses
448
+ const now = Date.now();
449
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
450
+ const typeIndexTurtle = `
451
+ <${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .
452
+
453
+ <#tasks>
454
+ a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
455
+ <http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Action> ;
456
+ <http://www.w3.org/ns/solid/terms#instanceContainer> <${containerUrl}> .
457
+ `;
458
+
459
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
460
+ FakeServer.respondOnce(
461
+ containerUrl,
462
+ containerResponse({
463
+ name: 'Tasks',
464
+ documentUrls: [freshDocumentUrl, staleDocumentUrl, childContainerUrl],
465
+ createdAt: workspace.createdAt,
466
+ updatedAt: workspace.updatedAt,
467
+ append: `
468
+ <${freshDocumentUrl}>
469
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
470
+ <http://purl.org/dc/terms/modified>
471
+ "${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
472
+
473
+ <${staleDocumentUrl}>
474
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
475
+ <http://purl.org/dc/terms/modified>
476
+ "${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
477
+ `,
478
+ }),
479
+ ); // Container
480
+ FakeServer.respondOnce(
481
+ childContainerUrl,
482
+ containerResponse({
483
+ name: 'Child Tasks',
484
+ documentUrls: [freshChildDocumentUrl, staleChildDocumentUrl],
485
+ createdAt: tasksList.createdAt,
486
+ updatedAt: tasksList.updatedAt,
487
+ append: `
488
+ <${freshChildDocumentUrl}>
489
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
490
+ <http://purl.org/dc/terms/modified>
491
+ "${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
492
+
493
+ <${staleChildDocumentUrl}>
494
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
495
+ <http://purl.org/dc/terms/modified>
496
+ "${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
497
+ `,
498
+ }),
499
+ ); // Child container
500
+ FakeServer.respondOnce(freshDocumentUrl, taskResponse('One'));
501
+ FakeServer.respondOnce(staleDocumentUrl, taskResponse('Two'));
502
+ FakeServer.respondOnce(freshChildDocumentUrl, taskResponse('Three'));
503
+ FakeServer.respondOnce(staleChildDocumentUrl, taskResponse('Four'));
504
+
505
+ FakeServer.respondOnce(
506
+ containerUrl,
507
+ containerResponse({
508
+ name: 'Tasks',
509
+ documentUrls: [freshDocumentUrl, staleDocumentUrl, childContainerUrl],
510
+ createdAt: workspace.createdAt,
511
+ updatedAt: workspace.updatedAt,
512
+ append: `
513
+ <${freshDocumentUrl}>
514
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
515
+ <http://purl.org/dc/terms/modified>
516
+ "${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
517
+
518
+ <${staleDocumentUrl}>
519
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
520
+ <http://purl.org/dc/terms/modified>
521
+ "${new Date(now + 1000).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
522
+ `,
523
+ }),
524
+ ); // Container
525
+ FakeServer.respondOnce(
526
+ childContainerUrl,
527
+ containerResponse({
528
+ name: 'Child Tasks',
529
+ documentUrls: [freshChildDocumentUrl, staleChildDocumentUrl],
530
+ createdAt: tasksList.createdAt,
531
+ updatedAt: tasksList.updatedAt,
532
+ append: `
533
+ <${freshChildDocumentUrl}>
534
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
535
+ <http://purl.org/dc/terms/modified>
536
+ "${new Date(now).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
537
+
538
+ <${staleChildDocumentUrl}>
539
+ a <http://www.w3.org/ns/iana/media-types/text/turtle#Resource> ;
540
+ <http://purl.org/dc/terms/modified>
541
+ "${new Date(now + 1000).toISOString()}"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
542
+ `,
543
+ }),
544
+ ); // Child container
545
+ FakeServer.respondOnce(staleDocumentUrl, taskResponse('Two'));
546
+ FakeServer.respondOnce(staleChildDocumentUrl, taskResponse('Four'));
547
+
548
+ // Arrange - Prepare service
549
+ Cloud.ready = true;
550
+
551
+ await Cloud.launch();
552
+ await Cloud.register(Workspace);
553
+ await Events.emit('application-ready');
554
+
555
+ // Arrange - Initial sync
556
+ await Cloud.sync();
557
+
558
+ // Act
559
+ await Cloud.sync();
560
+
561
+ // Assert
562
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
563
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(2);
564
+ expect(FakeServer.getRequests(childContainerUrl)).toHaveLength(2);
565
+ expect(FakeServer.getRequests(freshDocumentUrl)).toHaveLength(1);
566
+ expect(FakeServer.getRequests(staleDocumentUrl)).toHaveLength(2);
567
+ expect(FakeServer.getRequests(freshChildDocumentUrl)).toHaveLength(1);
568
+ expect(FakeServer.getRequests(staleChildDocumentUrl)).toHaveLength(2);
569
+ expect(FakeServer.getRequests()).toHaveLength(11);
570
+ });
571
+
572
+ it('Ignores missing children', async () => {
573
+ // Arrange - Mint urls
574
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
575
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
576
+ const existingChildDocumentUrl = fakeContainerUrl({ baseUrl: containerUrl });
577
+ const notFoundChildDocumentUrl = fakeDocumentUrl({ containerUrl });
578
+ const existingChildContainerUrl = fakeContainerUrl({ baseUrl: containerUrl });
579
+ const notFoundChildContainerUrl = fakeContainerUrl({ baseUrl: containerUrl });
580
+
581
+ // Arrange - Prepare responses
582
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
583
+ const typeIndexTurtle = `
584
+ <${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .
585
+
586
+ <#tasks>
587
+ a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
588
+ <http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Action> ;
589
+ <http://www.w3.org/ns/solid/terms#instanceContainer> <${containerUrl}> .
590
+ `;
591
+
592
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
593
+ FakeServer.respondOnce(
594
+ containerUrl,
595
+ containerResponse({
596
+ name: 'Tasks',
597
+ documentUrls: [
598
+ existingChildDocumentUrl,
599
+ notFoundChildDocumentUrl,
600
+ existingChildContainerUrl,
601
+ notFoundChildContainerUrl,
602
+ ],
603
+ }),
604
+ ); // Container
605
+ FakeServer.respondOnce(existingChildDocumentUrl, taskResponse());
606
+ FakeServer.respondOnce(notFoundChildDocumentUrl, FakeResponse.notFound());
607
+ FakeServer.respondOnce(existingChildContainerUrl, containerResponse());
608
+ FakeServer.respondOnce(notFoundChildContainerUrl, FakeResponse.notFound());
609
+
610
+ // Arrange - Prepare service
611
+ Cloud.ready = true;
612
+
613
+ await Cloud.launch();
614
+ await Cloud.register(Workspace);
615
+ await Events.emit('application-ready');
616
+
617
+ // Act
618
+ await Cloud.sync();
619
+
620
+ // Assert
621
+ const workspace = required(await Workspace.find(containerUrl));
622
+
623
+ await workspace.loadRelationIfUnloaded('tasks');
624
+ await workspace.loadRelationIfUnloaded('lists');
625
+
626
+ expect(workspace.tasks).toHaveLength(1);
627
+ expect(workspace.lists).toHaveLength(1);
628
+ });
629
+
630
+ it('Ignores malformed documents', async () => {
631
+ // Arrange - Mint urls
632
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
633
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
634
+ const firstDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
635
+ const secondDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
636
+ const thirdDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
637
+ const fourthDocumentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
638
+
639
+ // Arrange - Prepare models
640
+ const container = await MoviesContainer.at(parentContainerUrl).create({
641
+ url: containerUrl,
642
+ name: 'Movies',
643
+ });
644
+ const movie = await container.relatedMovies.create({
645
+ url: `${thirdDocumentUrl}#it`,
646
+ name: 'The Tale of Princess Kaguya',
647
+ });
648
+
649
+ await container.relatedMovies.create({ url: `${secondDocumentUrl}#it`, name: 'Spirited Away' });
650
+ await movie.update({ name: 'かぐや姫の物語' });
651
+
652
+ // Arrange - Prepare responses
653
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
654
+ const typeIndexTurtle = `
655
+ <${typeIndexUrl}> a <http://www.w3.org/ns/solid/terms#TypeIndex> .
656
+
657
+ <#tasks>
658
+ a <http://www.w3.org/ns/solid/terms#TypeRegistration> ;
659
+ <http://www.w3.org/ns/solid/terms#forClass> <https://schema.org/Movie> ;
660
+ <http://www.w3.org/ns/solid/terms#instanceContainer> <${containerUrl}> .
661
+ `;
662
+
663
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success(typeIndexTurtle)); // Initial fetch
664
+ FakeServer.respondOnce(
665
+ containerUrl,
666
+ containerResponse({
667
+ documentUrls: [firstDocumentUrl, secondDocumentUrl, thirdDocumentUrl, fourthDocumentUrl],
668
+ createdAt: container.createdAt,
669
+ updatedAt: container.updatedAt,
670
+ }),
671
+ ); // Container
672
+ FakeServer.respondOnce(firstDocumentUrl, FakeResponse.success('invalid turtle'));
673
+ FakeServer.respondOnce(secondDocumentUrl, FakeResponse.success('invalid turtle'));
674
+ FakeServer.respondOnce(thirdDocumentUrl, FakeResponse.success('invalid turtle'));
675
+ FakeServer.respondOnce(fourthDocumentUrl, FakeResponse.success('invalid turtle'));
676
+
677
+ // Arrange - Prepare service
678
+ Cloud.ready = true;
679
+ Cloud.localModelUpdates = { [movie.url]: 1 };
680
+
681
+ await Cloud.launch();
682
+ await Cloud.register(MoviesContainer);
683
+ await Events.emit('application-ready');
684
+
685
+ // Act
686
+ await Cloud.sync();
687
+
688
+ // Assert
689
+ expect(FakeServer.getRequests()).toHaveLength(6);
690
+ expect(Cloud.localModelUpdates).toEqual({ [movie.url]: 1 });
691
+ });
692
+
693
+ it('Ignores malformed documents in individual syncs', async () => {
694
+ // Arrange - Mint urls
695
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
696
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
697
+ const documentUrl = fakeDocumentUrl({ containerUrl: containerUrl });
698
+
699
+ // Arrange - Prepare models
700
+ const container = await MoviesContainer.at(parentContainerUrl).create({ url: containerUrl, name: 'Movies' });
701
+ const movie = await container.relatedMovies.create({
702
+ url: `${documentUrl}#it`,
703
+ name: 'The Tale of Princess Kaguya',
704
+ });
705
+
706
+ await movie.update({ name: 'かぐや姫の物語' });
707
+
708
+ // Arrange - Prepare responses
709
+
710
+ FakeServer.respondOnce(documentUrl, FakeResponse.success('invalid turtle'));
711
+
712
+ // Arrange - Prepare service
713
+ Cloud.ready = true;
714
+ Cloud.localModelUpdates = { [movie.url]: 1 };
715
+
716
+ await Cloud.launch();
717
+ await Cloud.register(MoviesContainer);
718
+ await Events.emit('application-ready');
719
+
720
+ // Act
721
+ await Cloud.sync(movie);
722
+
723
+ // Assert
724
+ expect(FakeServer.getRequests()).toHaveLength(1);
725
+ expect(Cloud.localModelUpdates).toEqual({ [movie.url]: 1 });
726
+ });
727
+
728
+ testRegisterVariants('Leaves tombstones behind', async (registerModels) => {
729
+ // Arrange - Mint urls
730
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
731
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
732
+ const documentUrl = fakeDocumentUrl({ containerUrl });
733
+
734
+ // Arrange - Prepare models
735
+ const container = await MoviesContainer.at(parentContainerUrl).create({
736
+ url: containerUrl,
737
+ name: 'Movies',
738
+ });
739
+ const movie = await container.relatedMovies.create({ url: `${documentUrl}#it`, name: 'Spirited Away' });
740
+ const movieTurtle = (await movie.toTurtle()).replaceAll(documentUrl, '');
741
+
742
+ // Arrange - Prepare responses
743
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
744
+
745
+ FakeServer.respond(typeIndexUrl, typeIndexResponse({ [containerUrl]: '<https://schema.org/Movie>' }));
746
+ FakeServer.respond(
747
+ containerUrl,
748
+ containerResponse({
749
+ documentUrls: [documentUrl],
750
+ createdAt: container.createdAt,
751
+ updatedAt: container.updatedAt,
752
+ }),
753
+ );
754
+ FakeServer.respondOnce(documentUrl, FakeResponse.success(movieTurtle)); // Pull
755
+ FakeServer.respondOnce(documentUrl, FakeResponse.success(movieTurtle)); // Model GET before delete
756
+ FakeServer.respondOnce(documentUrl, FakeResponse.success(movieTurtle)); // Client GET before update
757
+ FakeServer.respondOnce(documentUrl, FakeResponse.success()); // Client Tombstone PATCH
758
+
759
+ // Arrange - Prepare service
760
+ await Cloud.launch();
761
+ await registerModels();
762
+ await Events.emit('application-ready');
763
+
764
+ // Act
765
+ await movie.softDelete();
766
+ await Cloud.sync();
767
+
768
+ // Assert
769
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
770
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(1);
771
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
772
+ expect(FakeServer.getRequests()).toHaveLength(6);
773
+
774
+ expect(FakeServer.getRequest('PATCH', documentUrl)?.body).toEqualSparql(`
775
+ DELETE DATA { ${movieTurtle} } ;
776
+ INSERT DATA {
777
+ @prefix crdt: <https://vocab.noeldemartin.com/crdt/> .
778
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
779
+
780
+ <#it-metadata>
781
+ a crdt:Tombstone ;
782
+ crdt:resource <#it> ;
783
+ crdt:deletedAt "[[.*]]"^^xsd:dateTime .
784
+ } .
785
+ `);
786
+ });
787
+
788
+ testRegisterVariants('Deletes tombstone models', async (registerModels, variant) => {
789
+ const usingContainers = variant === 'container registration';
790
+
791
+ // Arrange - Mint urls
792
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
793
+ const containerUrl = parentContainerUrl + 'delete/'; //fakeContainerUrl({ baseUrl: parentContainerUrl });
794
+ const documentUrl = fakeDocumentUrl({ containerUrl });
795
+
796
+ // Arrange - Prepare models
797
+ const container = await MoviesContainer.at(parentContainerUrl).create({
798
+ url: containerUrl,
799
+ name: 'Movies',
800
+ });
801
+
802
+ const movie = await container.relatedMovies.create({ url: `${documentUrl}#it`, name: 'Spirited Away' });
803
+
804
+ // Arrange - Prepare initial sync responses
805
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
806
+
807
+ FakeServer.respondOnce(typeIndexUrl, typeIndexResponse());
808
+
809
+ if (usingContainers) {
810
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
811
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
812
+ FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
813
+ FakeServer.respondOnce(
814
+ // Read described-by header
815
+ containerUrl,
816
+ FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
817
+ );
818
+ FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
819
+ FakeServer.respondOnce(typeIndexUrl, typeIndexResponse()); // Check before update
820
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
821
+ } else {
822
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Tombstone check
823
+ }
824
+
825
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Existence check
826
+ FakeServer.respondOnce(documentUrl, FakeResponse.success()); // Create
827
+
828
+ // Arrange - Prepare service
829
+ if (!usingContainers) {
830
+ Cloud.modelCollections[Movie.modelName] = [containerUrl];
831
+ }
832
+
833
+ await Cloud.launch();
834
+ await registerModels();
835
+ await Events.emit('application-ready');
836
+
837
+ // Arrange - Initial sync
838
+ await Cloud.sync();
839
+
840
+ // Arrange - Sync responses
841
+ FakeServer.respond(
842
+ containerUrl,
843
+ containerResponse({
844
+ documentUrls: [documentUrl],
845
+ createdAt: container.createdAt,
846
+ updatedAt: container.updatedAt,
847
+ }),
848
+ );
849
+
850
+ FakeServer.respond(documentUrl, tombstoneResponse());
851
+
852
+ // Act
853
+ await Cloud.sync();
854
+
855
+ // Assert
856
+ const freshMovie = await Movie.at(containerUrl).find(movie.url);
857
+ const cloud = Cloud;
858
+
859
+ cloud;
860
+
861
+ expect(freshMovie).toBeNull();
862
+
863
+ if (usingContainers) {
864
+ const freshContainer = await container.fresh();
865
+
866
+ await freshContainer.loadRelation('movies');
867
+
868
+ expect(freshContainer.movies).toHaveLength(0);
869
+ expect(freshContainer.resourceUrls).toHaveLength(1);
870
+
871
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
872
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(5);
873
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(3);
874
+ expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(1);
875
+ expect(FakeServer.getRequests()).toHaveLength(12);
876
+ } else {
877
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
878
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
879
+ expect(FakeServer.getRequests()).toHaveLength(5);
880
+ }
881
+ });
882
+
883
+ testRegisterVariants('Deletes updated tombstone models', async (registerModels, variant) => {
884
+ const usingContainers = variant === 'container registration';
885
+
886
+ // Arrange - Mint urls
887
+ const parentContainerUrl = Solid.requireUser().storageUrls[0];
888
+ const containerUrl = fakeContainerUrl({ baseUrl: parentContainerUrl });
889
+ const documentUrl = fakeDocumentUrl({ containerUrl });
890
+
891
+ // Arrange - Prepare models
892
+ const container = await MoviesContainer.at(parentContainerUrl).create({
893
+ url: containerUrl,
894
+ name: 'Movies',
895
+ });
896
+
897
+ const movie = await container.relatedMovies.create({ url: `${documentUrl}#it`, name: 'Spirited Away' });
898
+
899
+ // Arrange - Prepare initial sync responses
900
+ const typeIndexUrl = SolidMock.requireUser().privateTypeIndexUrl ?? '';
901
+
902
+ FakeServer.respondOnce(typeIndexUrl, typeIndexResponse());
903
+
904
+ if (usingContainers) {
905
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Tombstone check
906
+ FakeServer.respondOnce(containerUrl, FakeResponse.notFound()); // Check before create
907
+ FakeServer.respondOnce(containerUrl, FakeResponse.created()); // Create
908
+ FakeServer.respondOnce(
909
+ // Read described-by header
910
+ containerUrl,
911
+ FakeResponse.success('<> a <http://www.w3.org/ns/ldp#Container> .'),
912
+ );
913
+ FakeServer.respondOnce(`${containerUrl}.meta`, FakeResponse.success()); // Update meta
914
+ FakeServer.respondOnce(typeIndexUrl, typeIndexResponse()); // Check before update
915
+ FakeServer.respondOnce(typeIndexUrl, FakeResponse.success()); // Update
916
+ } else {
917
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Tombstone check
918
+ }
919
+
920
+ FakeServer.respondOnce(documentUrl, FakeResponse.notFound()); // Existence check
921
+ FakeServer.respondOnce(documentUrl, FakeResponse.success()); // Create
922
+
923
+ // Arrange - Prepare service
924
+ if (!usingContainers) {
925
+ Cloud.modelCollections[Movie.modelName] = [containerUrl];
926
+ }
927
+
928
+ Cloud.autoPush = false;
929
+
930
+ await Cloud.launch();
931
+ await registerModels();
932
+ await Events.emit('application-ready');
933
+
934
+ // Arrange - Initial sync
935
+ await Cloud.sync();
936
+
937
+ // Arrange - Sync responses
938
+ FakeServer.respond(documentUrl, tombstoneResponse());
939
+
940
+ // Act
941
+ await movie.update({ name: 'Updated' });
942
+ await Cloud.sync([movie]); // Simulate auto push
943
+
944
+ // Assert
945
+ expect(await Movie.at(containerUrl).find(movie.url)).toBeNull();
946
+
947
+ if (usingContainers) {
948
+ const freshContainer = await container.fresh();
949
+
950
+ await freshContainer.loadRelation('movies');
951
+
952
+ expect(freshContainer.movies).toHaveLength(0);
953
+ expect(freshContainer.resourceUrls).toHaveLength(1);
954
+
955
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(3);
956
+ expect(FakeServer.getRequests(containerUrl)).toHaveLength(4);
957
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(4);
958
+ expect(FakeServer.getRequests(`${containerUrl}.meta`)).toHaveLength(1);
959
+ expect(FakeServer.getRequests()).toHaveLength(12);
960
+ } else {
961
+ expect(FakeServer.getRequests(typeIndexUrl)).toHaveLength(1);
962
+ expect(FakeServer.getRequests(documentUrl)).toHaveLength(5);
963
+ expect(FakeServer.getRequests()).toHaveLength(6);
964
+ }
965
+ });
966
+
967
+ testRegisterVariants('Tracks model updates', async (registerModels) => {
968
+ // Arrange
969
+ const container = await MoviesContainer.create({ name: 'Movies' });
970
+ const movie = await container.relatedMovies.create({ name: 'The Boy and The Heron' });
971
+
972
+ await registerModels();
973
+
974
+ // Act
975
+ await movie.update({ releaseDate: new Date('2023-07-14') });
976
+ await container.relatedMovies.create({ name: 'The Tale of Princess Kaguya' });
977
+
978
+ // Assert
979
+ expect(Cloud.localChanges).toEqual(2);
980
+ });
981
+
982
+ });