@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.
- package/dist/aerogel-plugin-local-first.d.ts +48 -17
- package/dist/aerogel-plugin-local-first.js +1219 -1232
- package/dist/aerogel-plugin-local-first.js.map +1 -1
- package/package.json +5 -5
- package/src/components/Account.vue +9 -0
- package/src/components/AccountLoggedIn.vue +85 -0
- package/src/components/AccountLoggedOut.vue +26 -0
- package/src/components/AccountLoginModal.vue +36 -0
- package/src/components/AccountModal.vue +206 -0
- package/src/components/index.ts +5 -3
- package/src/components/settings/PurgeData.vue +43 -0
- package/src/components/settings/index.ts +10 -0
- package/src/index.css +1 -0
- package/src/index.ts +5 -1
- package/src/jobs/Migrate.test.ts +1 -1
- package/src/jobs/Migrate.ts +8 -1
- package/src/jobs/Sync.test.ts +313 -35
- package/src/jobs/Sync.ts +183 -77
- package/src/jobs/mixins/LoadsChildren.ts +48 -31
- package/src/jobs/mixins/LoadsTypeIndex.ts +7 -5
- package/src/jobs/mixins/TracksLocalModels.ts +4 -4
- package/src/lib/mirroring.ts +45 -21
- package/src/services/Cloud.ts +84 -25
- package/src/services/DocumentsCache.ts +6 -2
- package/src/testing/cloud.ts +1 -1
- package/src/testing/setup.ts +3 -2
- package/src/testing/stubs/Person.schema.ts +9 -0
- package/src/testing/stubs/Person.ts +18 -0
- package/src/testing/stubs/Post.schema.ts +17 -0
- package/src/testing/stubs/Post.ts +18 -0
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +0 -9
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +0 -1
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +0 -5
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +0 -1
- package/src/components/AGCloudStatus.vue +0 -31
package/src/jobs/Sync.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
218
|
-
const registration = required(registrations[index])
|
|
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.
|
|
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.
|
|
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.
|
|
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 (!
|
|
341
|
+
if (!isRegisteredModel(remoteModel)) {
|
|
285
342
|
return;
|
|
286
343
|
}
|
|
287
344
|
|
|
288
|
-
const registeredChildren =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
444
|
+
|
|
445
|
+
await model.forceDelete();
|
|
374
446
|
|
|
375
447
|
return;
|
|
376
448
|
}
|
|
377
449
|
|
|
378
|
-
if (this.
|
|
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]
|
|
465
|
+
const modifiedAt = this.documentsModifiedAt[documentUrl];
|
|
388
466
|
|
|
389
|
-
|
|
467
|
+
if (modifiedAt) {
|
|
468
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
390
469
|
|
|
391
|
-
|
|
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.
|
|
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(
|
|
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]
|
|
643
|
+
const modifiedAt = this.documentsModifiedAt[documentUrl];
|
|
556
644
|
|
|
557
|
-
|
|
645
|
+
if (modifiedAt) {
|
|
646
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
558
647
|
|
|
559
|
-
|
|
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
|
|
663
|
+
private async ignoringDocumentErrors(operation: Function): Promise<void> {
|
|
574
664
|
try {
|
|
575
665
|
await operation();
|
|
576
666
|
} catch (error) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
667
|
+
this.handleDocumentError(error);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
580
670
|
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
+
if (documentUrl.endsWith('/')) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
}
|
|
83
|
+
required(statusChildrenMap.get(documentUrl)).completed = true;
|
|
82
84
|
|
|
83
|
-
|
|
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:
|
|
108
|
+
model: SolidContainer,
|
|
105
109
|
documentUrl: string,
|
|
106
110
|
documents: ObjectsMap<SolidDocument>,
|
|
107
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
157
|
+
model: SolidContainer,
|
|
152
158
|
documentUrl: string,
|
|
153
|
-
|
|
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 (
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
protected async loadTypeIndex(options?: { create
|
|
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
|
|
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 (
|
|
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
|
|
27
|
+
for (const relatedModel of getRelatedAppModels(localModel)) {
|
|
28
28
|
await this.addLocalModel(relatedModel);
|
|
29
29
|
}
|
|
30
30
|
}
|