@aerogel/plugin-local-first 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/aerogel-plugin-local-first.d.ts +48 -17
  2. package/dist/aerogel-plugin-local-first.js +1219 -1232
  3. package/dist/aerogel-plugin-local-first.js.map +1 -1
  4. package/package.json +5 -5
  5. package/src/components/Account.vue +9 -0
  6. package/src/components/AccountLoggedIn.vue +85 -0
  7. package/src/components/AccountLoggedOut.vue +26 -0
  8. package/src/components/AccountLoginModal.vue +36 -0
  9. package/src/components/AccountModal.vue +206 -0
  10. package/src/components/index.ts +5 -3
  11. package/src/components/settings/PurgeData.vue +43 -0
  12. package/src/components/settings/index.ts +10 -0
  13. package/src/index.css +1 -0
  14. package/src/index.ts +5 -1
  15. package/src/jobs/Migrate.test.ts +1 -1
  16. package/src/jobs/Migrate.ts +8 -1
  17. package/src/jobs/Sync.test.ts +313 -35
  18. package/src/jobs/Sync.ts +183 -77
  19. package/src/jobs/mixins/LoadsChildren.ts +48 -31
  20. package/src/jobs/mixins/LoadsTypeIndex.ts +7 -5
  21. package/src/jobs/mixins/TracksLocalModels.ts +4 -4
  22. package/src/lib/mirroring.ts +45 -21
  23. package/src/services/Cloud.ts +84 -25
  24. package/src/services/DocumentsCache.ts +6 -2
  25. package/src/testing/cloud.ts +1 -1
  26. package/src/testing/setup.ts +3 -2
  27. package/src/testing/stubs/Person.schema.ts +9 -0
  28. package/src/testing/stubs/Person.ts +18 -0
  29. package/src/testing/stubs/Post.schema.ts +17 -0
  30. package/src/testing/stubs/Post.ts +18 -0
  31. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +0 -9
  32. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +0 -1
  33. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +0 -5
  34. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +0 -1
  35. package/src/components/AGCloudStatus.vue +0 -31
package/src/jobs/Sync.ts CHANGED
@@ -1,11 +1,21 @@
1
- import { App, Job } from '@aerogel/core';
2
- import { isInstanceOf, map, mixed, requireUrlParentDirectory, required, round } from '@noeldemartin/utils';
1
+ import {
2
+ isInstanceOf,
3
+ map,
4
+ mixed,
5
+ requireUrlParentDirectory,
6
+ required,
7
+ round,
8
+ toString,
9
+ urlRoute,
10
+ } from '@noeldemartin/utils';
11
+ import { App, Errors, Job } from '@aerogel/core';
12
+ import { InvalidModelAttributes, requireBootedModel } from 'soukai';
3
13
  import { MalformedSolidDocumentError } from '@noeldemartin/solid-utils';
4
14
  import { Solid } from '@aerogel/plugin-solid';
5
- import { SolidModel, Tombstone, isContainer, isContainerClass } from 'soukai-solid';
15
+ import { SolidContainer, SolidModel, Tombstone, isContainer, isContainerClass } from 'soukai-solid';
6
16
  import type { ObjectsMap } from '@noeldemartin/utils';
7
17
  import type { JobListener, JobStatus } from '@aerogel/core';
8
- import type { SolidContainsRelation, SolidEngine, SolidTypeRegistration } from 'soukai-solid';
18
+ import type { SolidContainsRelation, SolidEngine, SolidTypeIndex } from 'soukai-solid';
9
19
 
10
20
  import {
11
21
  clearLocalModelUpdates,
@@ -51,29 +61,32 @@ function isRootStatus(status?: JobStatus): status is SyncJobStatus {
51
61
 
52
62
  export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex, TracksLocalModels]) {
53
63
 
64
+ public readonly documentsWithErrors: Set<string>;
54
65
  protected models?: SolidModel[];
55
66
  protected dirtyRemoteModels?: SolidModel[];
56
67
  protected rootLocalModels: SolidModel[];
57
68
  protected tombstones: ObjectsMap<Tombstone>;
58
- protected malformedDocuments: Set<string>;
69
+ protected syncedModelUrls: Set<string>;
59
70
  protected documentsModifiedAt: Record<string, Date>;
71
+ protected screenLock: Promise<void | { release(): Promise<void> }> | null = null;
60
72
  protected override localModels: ObjectsMap<SolidModel>;
61
73
 
62
74
  constructor(models?: SolidModel[]) {
63
75
  super();
64
76
 
77
+ this.documentsWithErrors = new Set();
65
78
  this.models = models;
66
79
  this.rootLocalModels = [];
67
80
  this.localModels = map([], 'url');
68
81
  this.tombstones = map([], 'resourceUrl');
69
- this.malformedDocuments = new Set();
82
+ this.syncedModelUrls = new Set();
70
83
  this.documentsModifiedAt = {};
71
84
  }
72
85
 
73
86
  protected async run(): Promise<void> {
74
87
  const engine = Solid.requireAuthenticator().engine as SolidEngine;
75
- const updateDocumentDate = (url: string, { headers }: { headers: Headers }) => {
76
- const date = headers?.get('last-modified');
88
+ const updateDocumentDate = (url: string, { headers }: { headers?: Headers }, fallbackDate?: Date) => {
89
+ const date = headers?.get('last-modified') ?? fallbackDate;
77
90
 
78
91
  if (!date || url.endsWith('/')) {
79
92
  return;
@@ -82,21 +95,50 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
82
95
  this.documentsModifiedAt[url] = new Date(date);
83
96
  };
84
97
  const clearListener = engine.listeners.add({
85
- onDocumentRead: updateDocumentDate,
86
- onDocumentUpdated: updateDocumentDate,
98
+ onDocumentRead: (url, metadata) => updateDocumentDate(url, metadata),
99
+ onDocumentUpdated: (url, metadata) => updateDocumentDate(url, metadata, new Date()),
87
100
  });
88
101
 
89
102
  try {
103
+ this.lockScreen();
90
104
  this.rootLocalModels = this.models ?? getLocalModels();
91
105
 
92
106
  await this.indexLocalModels(this.rootLocalModels);
93
107
  await this.pullChanges();
94
108
  await this.pushChanges();
95
109
  } finally {
110
+ this.releaseScreen();
96
111
  clearListener();
97
112
  }
98
113
  }
99
114
 
115
+ protected lockScreen(): void {
116
+ const typedNavigator = navigator as {
117
+ wakeLock?: { request(type?: 'screen'): Promise<{ release(): Promise<void> }> };
118
+ };
119
+
120
+ if (!typedNavigator.wakeLock) {
121
+ return;
122
+ }
123
+
124
+ try {
125
+ this.screenLock = typedNavigator.wakeLock.request('screen').catch((error) => {
126
+ Errors.reportDevelopmentError(error, 'Could not request screen wake lock');
127
+ });
128
+ } catch (error) {
129
+ Errors.reportDevelopmentError(error, 'Could not request screen wake lock');
130
+ }
131
+ }
132
+
133
+ protected releaseScreen(): void {
134
+ if (!this.screenLock) {
135
+ return;
136
+ }
137
+
138
+ this.screenLock.then((lock) => lock?.release());
139
+ this.screenLock = null;
140
+ }
141
+
100
142
  protected override getInitialStatus(): SyncJobStatus {
101
143
  return {
102
144
  root: true,
@@ -148,7 +190,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
148
190
  remoteModel.enableHistory();
149
191
  remoteModel.enableTombstone();
150
192
 
151
- await remoteModel.delete();
193
+ await remoteModel.forceDelete();
152
194
  await this.updateProgress(() => (childStatus.completed = true));
153
195
 
154
196
  return;
@@ -163,60 +205,27 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
163
205
  await DocumentsCache.remember(documentUrl, modifiedAt);
164
206
  }
165
207
 
166
- clearLocalModelUpdates(this.models, this.malformedDocuments);
208
+ clearLocalModelUpdates(this.syncedModelUrls, this.documentsWithErrors);
167
209
 
168
210
  await this.updateProgress((status) => (status.children[1].completed = true));
169
211
  }
170
212
 
171
213
  private async fetchRemoteModels(): Promise<SolidModel[]> {
172
214
  const pullStatus = this.status.children[0];
173
- const typeIndex = await this.loadTypeIndex();
215
+ const typeIndex = await this.loadTypeIndex({ fresh: true });
174
216
 
175
217
  if (!typeIndex) {
176
218
  return [];
177
219
  }
178
220
 
179
221
  const remoteModels: SolidModel[] = [];
180
- const processedContainerUrls = new Set();
181
- const registeredModels = [...Cloud.registeredModels].map(({ modelClass }) => {
182
- const registeredChildren = isContainerClass(modelClass) ? getContainerRegisteredClasses(modelClass) : [];
183
-
184
- return {
185
- modelClass,
186
- rdfsClasses:
187
- registeredChildren.length > 0
188
- ? registeredChildren.map((childrenClass) => childrenClass.rdfsClasses).flat()
189
- : modelClass.rdfsClasses,
190
- };
191
- });
192
- const registrations = typeIndex.registrations
193
- .map((registration) => {
194
- const registeredModel = registeredModels.find(
195
- ({ rdfsClasses }) =>
196
- !!registration.instanceContainer &&
197
- rdfsClasses.some((rdfsClass) => registration.forClass.includes(rdfsClass)),
198
- );
199
-
200
- if (
201
- !registeredModel ||
202
- !registration.instanceContainer ||
203
- processedContainerUrls.has(registration.instanceContainer)
204
- ) {
205
- return null;
206
- }
207
-
208
- processedContainerUrls.add(registration.instanceContainer);
209
-
210
- return [registration, registeredModel] as [SolidTypeRegistration, (typeof registeredModels)[number]];
211
- })
212
- .filter((match) => !!match);
222
+ const registrations = this.getTypeRegistrations(typeIndex);
213
223
 
214
224
  pullStatus.children = registrations.map(() => ({ completed: false }));
215
225
 
216
226
  for (let index = 0; index < registrations.length; index++) {
217
- await this.ignoringMalformedDocumentErrors(async () => {
218
- const registration = required(registrations[index])[0];
219
- const registeredModel = required(registrations[index])[1];
227
+ await this.ignoringDocumentErrors(async () => {
228
+ const { registration, registeredModel } = required(registrations[index]);
220
229
  const remoteClass = getRemoteClass(registeredModel.modelClass);
221
230
 
222
231
  if (!registration.instanceContainer) {
@@ -250,7 +259,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
250
259
  });
251
260
  }
252
261
 
253
- return completeRemoteModels(this.rootLocalModels, remoteModels, this.malformedDocuments);
262
+ return completeRemoteModels(this.rootLocalModels, remoteModels, this.documentsWithErrors);
254
263
  }
255
264
 
256
265
  private async fetchRemoteModelsForLocal(models: SolidModel[]): Promise<SolidModel[]> {
@@ -258,7 +267,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
258
267
  const statusChildren = (this.status.children[0].children = models.map(() => ({ completed: false })));
259
268
 
260
269
  for (let index = 0; index < models.length; index++) {
261
- await this.ignoringMalformedDocumentErrors(async () => {
270
+ await this.ignoringDocumentErrors(async () => {
262
271
  const localModel = models[index] as SolidModel;
263
272
  const childStatus = statusChildren[index] as JobStatus;
264
273
  const remoteModel = await getRemoteClass(localModel.static()).find(localModel.url);
@@ -277,15 +286,70 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
277
286
  });
278
287
  }
279
288
 
280
- return completeRemoteModels(models, remoteModels, this.malformedDocuments);
289
+ return completeRemoteModels(models, remoteModels, this.documentsWithErrors);
290
+ }
291
+
292
+ private getTypeRegistrations(typeIndex: SolidTypeIndex) {
293
+ const processedContainerUrls = new Set();
294
+ const registeredModels = [...Cloud.registeredModels].map(({ modelClass }) => {
295
+ const registeredChildren = isContainerClass(modelClass) ? getContainerRegisteredClasses(modelClass) : [];
296
+
297
+ return {
298
+ modelClass,
299
+ rdfsClasses:
300
+ registeredChildren.length > 0
301
+ ? registeredChildren.map((childrenClass) => childrenClass.rdfsClasses).flat()
302
+ : modelClass.rdfsClasses,
303
+ };
304
+ });
305
+ const registrations = typeIndex.registrations
306
+ .map((registration) => {
307
+ const registeredModel = registeredModels.find(
308
+ ({ rdfsClasses }) =>
309
+ !!registration.instanceContainer &&
310
+ rdfsClasses.some((rdfsClass) => registration.forClass.includes(rdfsClass)),
311
+ );
312
+
313
+ if (
314
+ !registeredModel ||
315
+ !registration.instanceContainer ||
316
+ processedContainerUrls.has(registration.instanceContainer)
317
+ ) {
318
+ return null;
319
+ }
320
+
321
+ processedContainerUrls.add(registration.instanceContainer);
322
+
323
+ return { registration, registeredModel };
324
+ })
325
+ .filter((match) => !!match);
326
+
327
+ // Removed nested registrations.
328
+ const containerUrls = registrations.map(({ registration }) => registration.instanceContainer);
329
+
330
+ return registrations.filter(({ registration }) => {
331
+ return !containerUrls.some(
332
+ (containerUrl) =>
333
+ containerUrl &&
334
+ containerUrl !== registration.instanceContainer &&
335
+ registration.instanceContainer?.startsWith(containerUrl),
336
+ );
337
+ });
281
338
  }
282
339
 
283
340
  private async updateTypeRegistrations(remoteModel: SolidModel): Promise<void> {
284
- if (!isContainer(remoteModel) || !isRegisteredModel(remoteModel)) {
341
+ if (!isRegisteredModel(remoteModel)) {
285
342
  return;
286
343
  }
287
344
 
288
- const registeredChildren = getContainerRegisteredClasses(remoteModel.static());
345
+ const registeredChildren = isContainer(remoteModel)
346
+ ? getContainerRegisteredClasses(remoteModel.static())
347
+ : [remoteModel.static()];
348
+
349
+ const RemoteSolidContainer = getRemoteClass(SolidContainer);
350
+ const container = isContainer(remoteModel)
351
+ ? remoteModel
352
+ : new RemoteSolidContainer({ url: remoteModel.requireContainerUrl() });
289
353
 
290
354
  if (registeredChildren.length === 0) {
291
355
  return;
@@ -297,7 +361,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
297
361
  return;
298
362
  }
299
363
 
300
- await remoteModel.register(typeIndex, registeredChildren);
364
+ await container.register(typeIndex, registeredChildren);
301
365
  }
302
366
 
303
367
  private async loadChildren(model: SolidModel, childStatus?: JobStatus): Promise<void> {
@@ -309,12 +373,19 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
309
373
 
310
374
  const children = await this.loadContainedModels(model, {
311
375
  status: childStatus,
312
- onLoaded: async () => {
376
+ onDocumentError: (error) => this.handleDocumentError(error),
377
+ onDocumentRead: (document) => {
378
+ if (document.url.endsWith('/') || !document.updatedAt || document.url in this.documentsModifiedAt) {
379
+ return;
380
+ }
381
+
382
+ this.documentsModifiedAt[document.url] = document.updatedAt;
383
+ },
384
+ onDocumentLoaded: async () => {
313
385
  this.assertNotCancelled();
314
386
 
315
387
  await this.updateProgress();
316
388
  },
317
- onMalformedDocument: (url) => this.malformedDocuments.add(url),
318
389
  });
319
390
 
320
391
  for (let index = 0; index < children.length; index++) {
@@ -361,7 +432,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
361
432
  }
362
433
  }
363
434
 
364
- await child.delete();
435
+ await child.forceDelete();
365
436
  }
366
437
 
367
438
  private async saveModelAndChildren(model: SolidModel, status?: JobStatus): Promise<void> {
@@ -370,25 +441,34 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
370
441
  if (isRemoteModel(model) && model.isSoftDeleted()) {
371
442
  model.enableHistory();
372
443
  model.enableTombstone();
373
- await model.delete();
444
+
445
+ await model.forceDelete();
374
446
 
375
447
  return;
376
448
  }
377
449
 
378
- if (this.malformedDocuments.has(model.requireDocumentUrl())) {
450
+ if (this.documentsWithErrors.has(model.requireDocumentUrl())) {
379
451
  return;
380
452
  }
381
453
 
454
+ const wasDirty = model.isDirty();
455
+
382
456
  await model.save();
383
457
 
458
+ if (wasDirty || isRemoteModel(model)) {
459
+ this.syncedModelUrls.add(model.url);
460
+ }
461
+
384
462
  if (!isContainer(model)) {
385
463
  if (isRemoteModel(model)) {
386
464
  const documentUrl = model.requireDocumentUrl();
387
- const modifiedAt = this.documentsModifiedAt[documentUrl] ?? new Date();
465
+ const modifiedAt = this.documentsModifiedAt[documentUrl];
388
466
 
389
- await DocumentsCache.remember(documentUrl, modifiedAt);
467
+ if (modifiedAt) {
468
+ await DocumentsCache.remember(documentUrl, modifiedAt);
390
469
 
391
- delete this.documentsModifiedAt[documentUrl];
470
+ delete this.documentsModifiedAt[documentUrl];
471
+ }
392
472
  }
393
473
 
394
474
  return;
@@ -425,7 +505,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
425
505
  const localModel = this.localModels.get(remoteModel.resourceUrl);
426
506
 
427
507
  if (localModel) {
428
- await localModel.delete();
508
+ await localModel.forceDelete();
429
509
 
430
510
  deletedModels.add(localModel);
431
511
  }
@@ -435,6 +515,10 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
435
515
 
436
516
  const localModel = getLocalModel(remoteModel, this.localModels);
437
517
 
518
+ if (!localModel.exists()) {
519
+ this.syncedModelUrls.add(localModel.url);
520
+ }
521
+
438
522
  await this.loadChildren(localModel);
439
523
  await SolidModel.synchronize(localModel, remoteModel);
440
524
  await this.reconcileInconsistencies(localModel, remoteModel);
@@ -503,11 +587,14 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
503
587
  continue;
504
588
  }
505
589
 
506
- await this.reconcileInconsistencies(localRelatedModel, remoteRelatedModels.require(localRelatedModel.url));
590
+ await this.reconcileInconsistencies(
591
+ localRelatedModel,
592
+ remoteRelatedModels.require(localRelatedModel.url),
593
+ processedModels,
594
+ );
507
595
  }
508
596
 
509
597
  if (localModel.isDirty()) {
510
- await localModel.save();
511
598
  await SolidModel.synchronize(localModel, remoteModel);
512
599
  }
513
600
  }
@@ -539,6 +626,7 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
539
626
 
540
627
  localRelation.addRelated(localModel);
541
628
  localModels.add(localModel);
629
+ this.syncedModelUrls.add(localModel.url);
542
630
  }
543
631
 
544
632
  for (const localModel of localModels.items()) {
@@ -552,11 +640,13 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
552
640
  private async rememberModelDocuments(model: SolidModel): Promise<void> {
553
641
  if (!isContainer(model)) {
554
642
  const documentUrl = model.requireDocumentUrl();
555
- const modifiedAt = this.documentsModifiedAt[documentUrl] ?? new Date();
643
+ const modifiedAt = this.documentsModifiedAt[documentUrl];
556
644
 
557
- await DocumentsCache.remember(documentUrl, modifiedAt);
645
+ if (modifiedAt) {
646
+ await DocumentsCache.remember(documentUrl, modifiedAt);
558
647
 
559
- delete this.documentsModifiedAt[documentUrl];
648
+ delete this.documentsModifiedAt[documentUrl];
649
+ }
560
650
 
561
651
  return;
562
652
  }
@@ -570,21 +660,37 @@ export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex,
570
660
  }
571
661
  }
572
662
 
573
- private async ignoringMalformedDocumentErrors(operation: Function): Promise<void> {
663
+ private async ignoringDocumentErrors(operation: Function): Promise<void> {
574
664
  try {
575
665
  await operation();
576
666
  } catch (error) {
577
- if (isInstanceOf(error, MalformedSolidDocumentError)) {
578
- // eslint-disable-next-line no-console
579
- console.warn(error.message);
667
+ this.handleDocumentError(error);
668
+ }
669
+ }
580
670
 
581
- error.documentUrl && this.malformedDocuments.add(error.documentUrl);
671
+ private handleDocumentError(error: unknown): void {
672
+ if (isInstanceOf(error, MalformedSolidDocumentError)) {
673
+ // eslint-disable-next-line no-console
674
+ console.warn(error.message);
582
675
 
583
- return;
584
- }
676
+ error.documentUrl && this.documentsWithErrors.add(error.documentUrl);
677
+
678
+ return;
679
+ }
680
+
681
+ if (isInstanceOf(error, InvalidModelAttributes)) {
682
+ // eslint-disable-next-line no-console
683
+ console.warn(error.message);
684
+
685
+ const primaryKey = error.attributes[requireBootedModel(error.modelName).primaryKey];
686
+ const documentUrl = primaryKey && urlRoute(toString(primaryKey));
585
687
 
586
- throw error;
688
+ documentUrl && this.documentsWithErrors.add(documentUrl);
689
+
690
+ return;
587
691
  }
692
+
693
+ throw error;
588
694
  }
589
695
 
590
696
  }
@@ -1,6 +1,5 @@
1
1
  import { arrayChunk, arrayFrom, isInstanceOf, map, required } from '@noeldemartin/utils';
2
2
  import { DocumentNotFound } from 'soukai';
3
- import { MalformedSolidDocumentError } from '@noeldemartin/solid-utils';
4
3
  import { Tombstone } from 'soukai-solid';
5
4
  import type { JobStatus } from '@aerogel/core';
6
5
  import type { ObjectsMap } from '@noeldemartin/utils';
@@ -25,8 +24,9 @@ export default class LoadsChildren {
25
24
  options: {
26
25
  ignoreTombstones?: boolean;
27
26
  status?: ResourceJobStatus;
28
- onLoaded?: (urls: string[]) => unknown;
29
- onMalformedDocument?: (url: string) => unknown;
27
+ onDocumentLoaded?: () => unknown;
28
+ onDocumentError?: (error: unknown) => unknown;
29
+ onDocumentRead?: (document: SolidDocument) => unknown;
30
30
  } = {},
31
31
  ): Promise<SolidModel[]> {
32
32
  if (isLocalModel(model)) {
@@ -60,7 +60,13 @@ export default class LoadsChildren {
60
60
  await Promise.all(
61
61
  documentUrls.map(async (documentUrl) => {
62
62
  const { children: documentChildren, tombstones: documentTombstones } =
63
- await this.loadDocumentChildren(model, documentUrl, documents, options.onMalformedDocument);
63
+ await this.loadDocumentChildren(
64
+ model,
65
+ documentUrl,
66
+ documents,
67
+ options.onDocumentError,
68
+ options.onDocumentRead,
69
+ );
64
70
 
65
71
  tombstones.push(...documentTombstones);
66
72
 
@@ -69,18 +75,16 @@ export default class LoadsChildren {
69
75
 
70
76
  relationChildren.push(...documentModels);
71
77
  }
72
- }),
73
- );
74
78
 
75
- for (const documentUrl of documentUrls) {
76
- if (documentUrl.endsWith('/')) {
77
- continue;
78
- }
79
+ if (documentUrl.endsWith('/')) {
80
+ return;
81
+ }
79
82
 
80
- required(statusChildrenMap.get(documentUrl)).completed = true;
81
- }
83
+ required(statusChildrenMap.get(documentUrl)).completed = true;
82
84
 
83
- await options.onLoaded?.(documentUrls);
85
+ await options.onDocumentLoaded?.();
86
+ }),
87
+ );
84
88
  }
85
89
 
86
90
  for (const [relation, relationModels] of Object.entries(children)) {
@@ -101,16 +105,18 @@ export default class LoadsChildren {
101
105
  }
102
106
 
103
107
  protected async loadDocumentChildren(
104
- model: SolidModel,
108
+ model: SolidContainer,
105
109
  documentUrl: string,
106
110
  documents: ObjectsMap<SolidDocument>,
107
- onMalformedDocument?: (url: string) => unknown,
111
+ onDocumentError?: (error: unknown) => unknown,
112
+ onDocumentRead?: (document: SolidDocument) => unknown,
108
113
  ): Promise<{ children: Record<string, SolidModel[]>; tombstones: Tombstone[] }> {
109
114
  const document = documents.get(documentUrl);
110
115
 
111
116
  if (
112
117
  document &&
113
- (await DocumentsCache.isFresh(document.url, document.updatedAt ?? new Date())) &&
118
+ document.updatedAt &&
119
+ (await DocumentsCache.isFresh(document.url, document.updatedAt)) &&
114
120
  this.localModels?.hasKey(model.url)
115
121
  ) {
116
122
  return {
@@ -119,7 +125,7 @@ export default class LoadsChildren {
119
125
  };
120
126
  }
121
127
 
122
- return this.loadDocumentChildrenFromRemote(model, documentUrl, onMalformedDocument);
128
+ return this.loadDocumentChildrenFromRemote(model, documentUrl, onDocumentError, onDocumentRead);
123
129
  }
124
130
 
125
131
  protected async loadDocumentChildrenFromLocal(
@@ -148,14 +154,16 @@ export default class LoadsChildren {
148
154
  }
149
155
 
150
156
  protected async loadDocumentChildrenFromRemote(
151
- model: SolidModel,
157
+ model: SolidContainer,
152
158
  documentUrl: string,
153
- onMalformedDocument?: (url: string) => unknown,
159
+ onDocumentError?: (error: unknown) => unknown,
160
+ onDocumentRead?: (document: SolidDocument) => unknown,
154
161
  ): Promise<{ children: Record<string, SolidModel[]>; tombstones: Tombstone[] }> {
155
162
  const children: Record<string, SolidModel[]> = {};
156
163
  const tombstones: Tombstone[] = [];
157
164
  const readDocument = lazy(async () => {
158
165
  try {
166
+ const documentModel = model.documents.find((document) => document.url === documentUrl);
159
167
  const document = await model.requireEngine().readOne(model.static('collection'), documentUrl);
160
168
  const documentTombstones = await Tombstone.createManyFromEngineDocuments({
161
169
  [documentUrl]: document,
@@ -163,22 +171,23 @@ export default class LoadsChildren {
163
171
 
164
172
  tombstones.push(...documentTombstones);
165
173
 
174
+ if (documentModel) {
175
+ onDocumentRead?.(documentModel);
176
+ }
177
+
166
178
  return document;
167
179
  } catch (error) {
168
180
  if (isInstanceOf(error, DocumentNotFound)) {
169
181
  return null;
170
182
  }
171
183
 
172
- if (isInstanceOf(error, MalformedSolidDocumentError)) {
173
- // eslint-disable-next-line no-console
174
- console.warn(error.message);
175
-
176
- await onMalformedDocument?.(documentUrl);
177
-
178
- return null;
184
+ if (!onDocumentError) {
185
+ throw error;
179
186
  }
180
187
 
181
- throw error;
188
+ onDocumentError(error);
189
+
190
+ return null;
182
191
  }
183
192
  });
184
193
 
@@ -197,11 +206,19 @@ export default class LoadsChildren {
197
206
  continue;
198
207
  }
199
208
 
200
- const documentChildren = await relationInstance.relatedClass.createManyFromEngineDocuments({
201
- [documentUrl]: document,
202
- });
209
+ try {
210
+ const documentChildren = await relationInstance.relatedClass.createManyFromEngineDocuments({
211
+ [documentUrl]: document,
212
+ });
213
+
214
+ children[relation] = documentChildren;
215
+ } catch (error) {
216
+ if (!onDocumentError) {
217
+ throw error;
218
+ }
203
219
 
204
- children[relation] = documentChildren;
220
+ onDocumentError(error);
221
+ }
205
222
  }
206
223
 
207
224
  return { children, tombstones };
@@ -6,15 +6,17 @@ const typeIndexes: WeakMap<SolidUserProfile, SolidTypeIndex | null> = new WeakMa
6
6
 
7
7
  export default class LoadsTypeIndex {
8
8
 
9
- protected async loadTypeIndex(options: { create: true }): Promise<SolidTypeIndex>;
10
- protected async loadTypeIndex(options?: { create: false }): Promise<SolidTypeIndex | null>;
11
- protected async loadTypeIndex(options: { create?: boolean } = {}): Promise<SolidTypeIndex | null> {
9
+ protected async loadTypeIndex(options?: { create?: false; fresh?: boolean }): Promise<SolidTypeIndex | null>;
10
+ protected async loadTypeIndex(options?: { create?: true; fresh?: boolean }): Promise<SolidTypeIndex>;
11
+ protected async loadTypeIndex(options: { create?: boolean; fresh?: boolean } = {}): Promise<SolidTypeIndex | null> {
12
12
  const user = Solid.requireUser();
13
13
 
14
+ if (options.fresh || !typeIndexes.has(user)) {
15
+ typeIndexes.set(user, await Solid.findPrivateTypeIndex({ fresh: options.fresh }));
16
+ }
17
+
14
18
  if (options.create && !typeIndexes.get(user)) {
15
19
  typeIndexes.set(user, await Solid.findOrCreatePrivateTypeIndex());
16
- } else if (!typeIndexes.has(user)) {
17
- typeIndexes.set(user, await Solid.findPrivateTypeIndex());
18
20
  }
19
21
 
20
22
  return typeIndexes.get(user) ?? null;
@@ -1,11 +1,11 @@
1
1
  import type { ObjectsMap } from '@noeldemartin/utils';
2
2
  import type { SolidModel } from 'soukai-solid';
3
3
 
4
- import { getContainerRelations } from '@aerogel/plugin-local-first/lib/inference';
4
+ import { getContainerRelations, getRelatedAppModels } from '@aerogel/plugin-local-first/lib/inference';
5
5
 
6
6
  export default class TracksLocalModels {
7
7
 
8
- declare protected localModels?: ObjectsMap<SolidModel>;
8
+ declare protected localModels: ObjectsMap<SolidModel>;
9
9
 
10
10
  protected async indexLocalModels(models: SolidModel[]): Promise<void> {
11
11
  for (const localModel of models) {
@@ -14,7 +14,7 @@ export default class TracksLocalModels {
14
14
  }
15
15
 
16
16
  protected async addLocalModel(localModel: SolidModel): Promise<void> {
17
- if (!this.localModels || this.localModels.hasKey(localModel.url)) {
17
+ if (this.localModels.hasKey(localModel.url)) {
18
18
  return;
19
19
  }
20
20
 
@@ -24,7 +24,7 @@ export default class TracksLocalModels {
24
24
  await localModel.loadRelationIfUnloaded(relation);
25
25
  }
26
26
 
27
- for (const relatedModel of localModel.getRelatedModels()) {
27
+ for (const relatedModel of getRelatedAppModels(localModel)) {
28
28
  await this.addLocalModel(relatedModel);
29
29
  }
30
30
  }