@aerogel/plugin-local-first 0.0.0-next.004d5deedf15bedf609a660bfd4543d3c0df9d9d
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 +299 -0
- package/dist/aerogel-plugin-local-first.js +1661 -0
- package/dist/aerogel-plugin-local-first.js.map +1 -0
- package/package.json +50 -0
- 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 -0
- 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 +33 -0
- package/src/jobs/Backup.ts +98 -0
- package/src/jobs/Migrate.test.ts +217 -0
- package/src/jobs/Migrate.ts +186 -0
- package/src/jobs/Sync.test.ts +1198 -0
- package/src/jobs/Sync.ts +696 -0
- package/src/jobs/mixins/LoadsChildren.ts +229 -0
- package/src/jobs/mixins/LoadsTypeIndex.ts +25 -0
- package/src/jobs/mixins/TracksLocalModels.ts +32 -0
- package/src/lib/SyncQueue.ts +94 -0
- package/src/lib/inference.ts +124 -0
- package/src/lib/mirroring.ts +352 -0
- package/src/lib/utils.ts +21 -0
- package/src/services/Cloud.state.ts +111 -0
- package/src/services/Cloud.ts +494 -0
- package/src/services/DocumentsCache.ts +39 -0
- package/src/testing/cloud.ts +193 -0
- package/src/testing/mocks/Solid.mock.ts +34 -0
- package/src/testing/setup.ts +33 -0
- package/src/testing/stubs/LegacyTask.schema.ts +15 -0
- package/src/testing/stubs/Movie.schema.ts +15 -0
- package/src/testing/stubs/Movie.ts +3 -0
- package/src/testing/stubs/MoviesContainer.ts +20 -0
- 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/src/testing/stubs/Task.schema.ts +12 -0
- package/src/testing/stubs/Task.ts +3 -0
- package/src/testing/stubs/TasksList.ts +20 -0
- package/src/testing/stubs/Workspace.ts +15 -0
- package/src/types/shims.d.ts +7 -0
package/src/jobs/Sync.ts
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
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';
|
|
13
|
+
import { MalformedSolidDocumentError } from '@noeldemartin/solid-utils';
|
|
14
|
+
import { Solid } from '@aerogel/plugin-solid';
|
|
15
|
+
import { SolidContainer, SolidModel, Tombstone, isContainer, isContainerClass } from 'soukai-solid';
|
|
16
|
+
import type { ObjectsMap } from '@noeldemartin/utils';
|
|
17
|
+
import type { JobListener, JobStatus } from '@aerogel/core';
|
|
18
|
+
import type { SolidContainsRelation, SolidEngine, SolidTypeIndex } from 'soukai-solid';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
clearLocalModelUpdates,
|
|
22
|
+
cloneLocalModel,
|
|
23
|
+
cloneRemoteModel,
|
|
24
|
+
completeRemoteModels,
|
|
25
|
+
getLocalModel,
|
|
26
|
+
getLocalModels,
|
|
27
|
+
getRemoteClass,
|
|
28
|
+
getRemoteModel,
|
|
29
|
+
isRegisteredModel,
|
|
30
|
+
isRemoteModel,
|
|
31
|
+
trackModelsCollection,
|
|
32
|
+
} from '@aerogel/plugin-local-first/lib/mirroring';
|
|
33
|
+
import {
|
|
34
|
+
getContainerRegisteredClasses,
|
|
35
|
+
getContainerRelations,
|
|
36
|
+
getRelatedAppModels,
|
|
37
|
+
isDirtyOrHasDirtyChildren,
|
|
38
|
+
} from '@aerogel/plugin-local-first/lib/inference';
|
|
39
|
+
import DocumentsCache from '@aerogel/plugin-local-first/services/DocumentsCache';
|
|
40
|
+
|
|
41
|
+
import LoadsChildren from './mixins/LoadsChildren';
|
|
42
|
+
import LoadsTypeIndex from './mixins/LoadsTypeIndex';
|
|
43
|
+
import TracksLocalModels from './mixins/TracksLocalModels';
|
|
44
|
+
import type { ResourceJobStatus } from './mixins/LoadsChildren';
|
|
45
|
+
|
|
46
|
+
const Cloud = required(() => App.service('$cloud'));
|
|
47
|
+
const BaseJob = Job as typeof Job<JobListener, SyncJobStatus, SyncJobStatus>;
|
|
48
|
+
|
|
49
|
+
interface SyncJobStatus extends JobStatus {
|
|
50
|
+
root: true;
|
|
51
|
+
children: [SyncPullJobStatus, JobStatus];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SyncPullJobStatus extends JobStatus {
|
|
55
|
+
children?: ResourceJobStatus[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRootStatus(status?: JobStatus): status is SyncJobStatus {
|
|
59
|
+
return !!status && 'root' in status;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex, TracksLocalModels]) {
|
|
63
|
+
|
|
64
|
+
protected models?: SolidModel[];
|
|
65
|
+
protected dirtyRemoteModels?: SolidModel[];
|
|
66
|
+
protected rootLocalModels: SolidModel[];
|
|
67
|
+
protected tombstones: ObjectsMap<Tombstone>;
|
|
68
|
+
protected documentsWithErrors: Set<string>;
|
|
69
|
+
protected syncedModelUrls: Set<string>;
|
|
70
|
+
protected documentsModifiedAt: Record<string, Date>;
|
|
71
|
+
protected screenLock: Promise<void | { release(): Promise<void> }> | null = null;
|
|
72
|
+
protected override localModels: ObjectsMap<SolidModel>;
|
|
73
|
+
|
|
74
|
+
constructor(models?: SolidModel[]) {
|
|
75
|
+
super();
|
|
76
|
+
|
|
77
|
+
this.models = models;
|
|
78
|
+
this.rootLocalModels = [];
|
|
79
|
+
this.localModels = map([], 'url');
|
|
80
|
+
this.tombstones = map([], 'resourceUrl');
|
|
81
|
+
this.documentsWithErrors = new Set();
|
|
82
|
+
this.syncedModelUrls = new Set();
|
|
83
|
+
this.documentsModifiedAt = {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected async run(): Promise<void> {
|
|
87
|
+
const engine = Solid.requireAuthenticator().engine as SolidEngine;
|
|
88
|
+
const updateDocumentDate = (url: string, { headers }: { headers?: Headers }, fallbackDate?: Date) => {
|
|
89
|
+
const date = headers?.get('last-modified') ?? fallbackDate;
|
|
90
|
+
|
|
91
|
+
if (!date || url.endsWith('/')) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.documentsModifiedAt[url] = new Date(date);
|
|
96
|
+
};
|
|
97
|
+
const clearListener = engine.listeners.add({
|
|
98
|
+
onDocumentRead: (url, metadata) => updateDocumentDate(url, metadata),
|
|
99
|
+
onDocumentUpdated: (url, metadata) => updateDocumentDate(url, metadata, new Date()),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
this.lockScreen();
|
|
104
|
+
this.rootLocalModels = this.models ?? getLocalModels();
|
|
105
|
+
|
|
106
|
+
await this.indexLocalModels(this.rootLocalModels);
|
|
107
|
+
await this.pullChanges();
|
|
108
|
+
await this.pushChanges();
|
|
109
|
+
} finally {
|
|
110
|
+
this.releaseScreen();
|
|
111
|
+
clearListener();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
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
|
+
|
|
142
|
+
protected override getInitialStatus(): SyncJobStatus {
|
|
143
|
+
return {
|
|
144
|
+
root: true,
|
|
145
|
+
completed: false,
|
|
146
|
+
children: [{ completed: false }, { completed: false }],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected override calculateCurrentProgress(status?: JobStatus): number {
|
|
151
|
+
status ??= this.status;
|
|
152
|
+
|
|
153
|
+
if (isRootStatus(status)) {
|
|
154
|
+
return round(
|
|
155
|
+
this.calculateCurrentProgress(status.children[0]) * 0.9 +
|
|
156
|
+
this.calculateCurrentProgress(status.children[1]) * 0.1,
|
|
157
|
+
2,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return super.calculateCurrentProgress(status);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async pullChanges(): Promise<void> {
|
|
165
|
+
const remoteModelsArray = this.models
|
|
166
|
+
? await this.fetchRemoteModelsForLocal(this.models)
|
|
167
|
+
: await this.fetchRemoteModels();
|
|
168
|
+
const remoteModels = map(remoteModelsArray, (model) => {
|
|
169
|
+
if (model instanceof Tombstone) {
|
|
170
|
+
return model.resourceUrl;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return model.url;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.dirtyRemoteModels = await this.synchronizeModels(remoteModels);
|
|
177
|
+
|
|
178
|
+
await this.updateProgress((status) => (status.children[0].completed = true));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async pushChanges(): Promise<void> {
|
|
182
|
+
const remoteModels = this.dirtyRemoteModels ?? [];
|
|
183
|
+
const childrenStatus = (this.status.children[1].children = remoteModels.map(() => ({ completed: false })));
|
|
184
|
+
|
|
185
|
+
for (let index = 0; index < remoteModels.length; index++) {
|
|
186
|
+
const remoteModel = remoteModels[index] as SolidModel;
|
|
187
|
+
const childStatus = childrenStatus[index] as JobStatus;
|
|
188
|
+
|
|
189
|
+
if (remoteModel.isSoftDeleted()) {
|
|
190
|
+
remoteModel.enableHistory();
|
|
191
|
+
remoteModel.enableTombstone();
|
|
192
|
+
|
|
193
|
+
await remoteModel.forceDelete();
|
|
194
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
195
|
+
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await this.saveModelAndChildren(remoteModel, childStatus);
|
|
200
|
+
await this.updateTypeRegistrations(remoteModel);
|
|
201
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const [documentUrl, modifiedAt] of Object.entries(this.documentsModifiedAt)) {
|
|
205
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
clearLocalModelUpdates(this.syncedModelUrls, this.documentsWithErrors);
|
|
209
|
+
|
|
210
|
+
await this.updateProgress((status) => (status.children[1].completed = true));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async fetchRemoteModels(): Promise<SolidModel[]> {
|
|
214
|
+
const pullStatus = this.status.children[0];
|
|
215
|
+
const typeIndex = await this.loadTypeIndex({ fresh: true });
|
|
216
|
+
|
|
217
|
+
if (!typeIndex) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const remoteModels: SolidModel[] = [];
|
|
222
|
+
const registrations = this.getTypeRegistrations(typeIndex);
|
|
223
|
+
|
|
224
|
+
pullStatus.children = registrations.map(() => ({ completed: false }));
|
|
225
|
+
|
|
226
|
+
for (let index = 0; index < registrations.length; index++) {
|
|
227
|
+
await this.ignoringDocumentErrors(async () => {
|
|
228
|
+
const { registration, registeredModel } = required(registrations[index]);
|
|
229
|
+
const remoteClass = getRemoteClass(registeredModel.modelClass);
|
|
230
|
+
|
|
231
|
+
if (!registration.instanceContainer) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (isContainerClass(remoteClass)) {
|
|
236
|
+
await trackModelsCollection(
|
|
237
|
+
registeredModel.modelClass,
|
|
238
|
+
requireUrlParentDirectory(registration.instanceContainer),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const container = await remoteClass.find(registration.instanceContainer);
|
|
242
|
+
|
|
243
|
+
if (container) {
|
|
244
|
+
await this.loadChildren(container, required(pullStatus.children?.[index]));
|
|
245
|
+
|
|
246
|
+
remoteModels.push(container);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await this.updateProgress(() => (required(pullStatus.children?.[index]).completed = true));
|
|
250
|
+
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await trackModelsCollection(registeredModel.modelClass, registration.instanceContainer);
|
|
255
|
+
|
|
256
|
+
remoteModels.push(...(await remoteClass.from(registration.instanceContainer).all()));
|
|
257
|
+
|
|
258
|
+
await this.updateProgress(() => (required(pullStatus.children?.[index]).completed = true));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return completeRemoteModels(this.rootLocalModels, remoteModels, this.documentsWithErrors);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async fetchRemoteModelsForLocal(models: SolidModel[]): Promise<SolidModel[]> {
|
|
266
|
+
const remoteModels = [] as SolidModel[];
|
|
267
|
+
const statusChildren = (this.status.children[0].children = models.map(() => ({ completed: false })));
|
|
268
|
+
|
|
269
|
+
for (let index = 0; index < models.length; index++) {
|
|
270
|
+
await this.ignoringDocumentErrors(async () => {
|
|
271
|
+
const localModel = models[index] as SolidModel;
|
|
272
|
+
const childStatus = statusChildren[index] as JobStatus;
|
|
273
|
+
const remoteModel = await getRemoteClass(localModel.static()).find(localModel.url);
|
|
274
|
+
|
|
275
|
+
if (!remoteModel) {
|
|
276
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
277
|
+
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
isContainer(remoteModel) && (await this.loadChildren(remoteModel, childStatus));
|
|
282
|
+
|
|
283
|
+
remoteModels.push(remoteModel);
|
|
284
|
+
|
|
285
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
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
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async updateTypeRegistrations(remoteModel: SolidModel): Promise<void> {
|
|
341
|
+
if (!isRegisteredModel(remoteModel)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
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() });
|
|
353
|
+
|
|
354
|
+
if (registeredChildren.length === 0) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const typeIndex = await this.loadTypeIndex({ create: true });
|
|
359
|
+
|
|
360
|
+
if (typeIndex.registrations.some((registration) => registration.instanceContainer === remoteModel.url)) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await container.register(typeIndex, registeredChildren);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async loadChildren(model: SolidModel, childStatus?: JobStatus): Promise<void> {
|
|
368
|
+
this.assertNotCancelled();
|
|
369
|
+
|
|
370
|
+
if (!isContainer(model)) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const children = await this.loadContainedModels(model, {
|
|
375
|
+
status: childStatus,
|
|
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
|
+
onDocumentsLoaded: async () => {
|
|
385
|
+
this.assertNotCancelled();
|
|
386
|
+
|
|
387
|
+
await this.updateProgress();
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
for (let index = 0; index < children.length; index++) {
|
|
392
|
+
const child = children[index] as SolidModel;
|
|
393
|
+
|
|
394
|
+
if (child instanceof Tombstone) {
|
|
395
|
+
this.tombstones.add(child);
|
|
396
|
+
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (this.tombstones.hasKey(child.url)) {
|
|
401
|
+
await this.deleteChild(model, child);
|
|
402
|
+
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
await this.loadChildren(child, childStatus?.children?.[index]);
|
|
407
|
+
|
|
408
|
+
if (childStatus?.children?.[index]) {
|
|
409
|
+
await this.updateProgress(() => (required(childStatus.children?.[index]).completed = true));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async deleteChild(model: SolidModel, child: SolidModel): Promise<void> {
|
|
415
|
+
for (const relation of model.static('relations')) {
|
|
416
|
+
const relationInstance = model.requireRelation(relation);
|
|
417
|
+
|
|
418
|
+
if (!relationInstance.related) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (Array.isArray(relationInstance.related)) {
|
|
423
|
+
relationInstance.related = relationInstance.related.filter((relatedModel) => relatedModel !== child);
|
|
424
|
+
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (relationInstance.related === child) {
|
|
429
|
+
relationInstance.related = null;
|
|
430
|
+
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await child.forceDelete();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async saveModelAndChildren(model: SolidModel, status?: JobStatus): Promise<void> {
|
|
439
|
+
this.assertNotCancelled();
|
|
440
|
+
|
|
441
|
+
if (isRemoteModel(model) && model.isSoftDeleted()) {
|
|
442
|
+
model.enableHistory();
|
|
443
|
+
model.enableTombstone();
|
|
444
|
+
|
|
445
|
+
await model.forceDelete();
|
|
446
|
+
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (this.documentsWithErrors.has(model.requireDocumentUrl())) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const wasDirty = model.isDirty();
|
|
455
|
+
|
|
456
|
+
await model.save();
|
|
457
|
+
|
|
458
|
+
if (wasDirty || isRemoteModel(model)) {
|
|
459
|
+
this.syncedModelUrls.add(model.url);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!isContainer(model)) {
|
|
463
|
+
if (isRemoteModel(model)) {
|
|
464
|
+
const documentUrl = model.requireDocumentUrl();
|
|
465
|
+
const modifiedAt = this.documentsModifiedAt[documentUrl];
|
|
466
|
+
|
|
467
|
+
if (modifiedAt) {
|
|
468
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
469
|
+
|
|
470
|
+
delete this.documentsModifiedAt[documentUrl];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const children = getContainerRelations(model.static())
|
|
478
|
+
.map((relation) => model.getRelationModels(relation) ?? [])
|
|
479
|
+
.flat();
|
|
480
|
+
|
|
481
|
+
const childrenStatuses = status ? children.map(() => ({ completed: false })) : [];
|
|
482
|
+
|
|
483
|
+
if (status) {
|
|
484
|
+
status.children = childrenStatuses;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
for (let index = 0; index < children.length; index++) {
|
|
488
|
+
const child = children[index] as SolidModel;
|
|
489
|
+
const childStatus = childrenStatuses[index];
|
|
490
|
+
|
|
491
|
+
await this.saveModelAndChildren(child, childStatus);
|
|
492
|
+
|
|
493
|
+
if (childStatus) {
|
|
494
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private async synchronizeModels(remoteModels: ObjectsMap<SolidModel>): Promise<SolidModel[]> {
|
|
500
|
+
const synchronizedModelUrls = new Set<string>();
|
|
501
|
+
const deletedModels = new Set<SolidModel>();
|
|
502
|
+
|
|
503
|
+
for (const remoteModel of remoteModels.items()) {
|
|
504
|
+
if (remoteModel instanceof Tombstone) {
|
|
505
|
+
const localModel = this.localModels.get(remoteModel.resourceUrl);
|
|
506
|
+
|
|
507
|
+
if (localModel) {
|
|
508
|
+
await localModel.forceDelete();
|
|
509
|
+
|
|
510
|
+
deletedModels.add(localModel);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const localModel = getLocalModel(remoteModel, this.localModels);
|
|
517
|
+
|
|
518
|
+
if (!localModel.exists()) {
|
|
519
|
+
this.syncedModelUrls.add(localModel.url);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
await this.loadChildren(localModel);
|
|
523
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
524
|
+
await this.reconcileInconsistencies(localModel, remoteModel);
|
|
525
|
+
await this.saveModelAndChildren(localModel);
|
|
526
|
+
await this.addLocalModel(localModel);
|
|
527
|
+
|
|
528
|
+
this.rootLocalModels.push(localModel);
|
|
529
|
+
synchronizedModelUrls.add(localModel.url);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
for (const localModel of this.rootLocalModels) {
|
|
533
|
+
if (deletedModels.has(localModel) || synchronizedModelUrls.has(localModel.url)) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await this.loadChildren(localModel);
|
|
538
|
+
|
|
539
|
+
const remoteModel = getRemoteModel(localModel, remoteModels);
|
|
540
|
+
|
|
541
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
542
|
+
|
|
543
|
+
remoteModels.add(remoteModel);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const dirtyRemoteModels = [];
|
|
547
|
+
|
|
548
|
+
for (const remoteModel of remoteModels.getItems()) {
|
|
549
|
+
if (isDirtyOrHasDirtyChildren(remoteModel)) {
|
|
550
|
+
dirtyRemoteModels.push(remoteModel);
|
|
551
|
+
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
await this.rememberModelDocuments(remoteModel);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return dirtyRemoteModels;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private async reconcileInconsistencies(
|
|
562
|
+
localModel: SolidModel,
|
|
563
|
+
remoteModel: SolidModel,
|
|
564
|
+
processedModels?: Set<SolidModel>,
|
|
565
|
+
): Promise<void> {
|
|
566
|
+
localModel.rebuildAttributesFromHistory();
|
|
567
|
+
localModel.setAttributes(remoteModel.getAttributes(true));
|
|
568
|
+
|
|
569
|
+
processedModels ??= new Set();
|
|
570
|
+
processedModels.add(localModel);
|
|
571
|
+
|
|
572
|
+
for (const relation of getContainerRelations(localModel.static())) {
|
|
573
|
+
await this.synchronizeContainerDocuments(
|
|
574
|
+
localModel.requireRelation<SolidContainsRelation>(relation),
|
|
575
|
+
remoteModel.requireRelation<SolidContainsRelation>(relation),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const remoteRelatedModels = map(getRelatedAppModels(remoteModel), 'url');
|
|
580
|
+
|
|
581
|
+
for (const localRelatedModel of getRelatedAppModels(localModel)) {
|
|
582
|
+
if (
|
|
583
|
+
processedModels.has(localRelatedModel) ||
|
|
584
|
+
!localRelatedModel.isRelationLoaded('operations') ||
|
|
585
|
+
!remoteRelatedModels.hasKey(localRelatedModel.url)
|
|
586
|
+
) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
await this.reconcileInconsistencies(
|
|
591
|
+
localRelatedModel,
|
|
592
|
+
remoteRelatedModels.require(localRelatedModel.url),
|
|
593
|
+
processedModels,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (localModel.isDirty()) {
|
|
598
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private async synchronizeContainerDocuments(
|
|
603
|
+
localRelation: SolidContainsRelation,
|
|
604
|
+
remoteRelation: SolidContainsRelation,
|
|
605
|
+
): Promise<void> {
|
|
606
|
+
const localModels = map(localRelation.related ?? [], 'url');
|
|
607
|
+
const remoteModels = map(remoteRelation.related ?? [], 'url');
|
|
608
|
+
const newLocalModelUrls = localModels
|
|
609
|
+
.getKeys()
|
|
610
|
+
.filter((url) => !remoteModels.hasKey(url) && !remoteModels.hasKey(encodeURI(url)));
|
|
611
|
+
const newRemoteModelUrls = remoteModels
|
|
612
|
+
.getKeys()
|
|
613
|
+
.filter((url) => !localModels.hasKey(url) && !localModels.hasKey(encodeURI(url)));
|
|
614
|
+
|
|
615
|
+
for (const url of newLocalModelUrls) {
|
|
616
|
+
const localModel = localModels.require(url);
|
|
617
|
+
const remoteModel = cloneLocalModel(localModel);
|
|
618
|
+
|
|
619
|
+
remoteRelation.addRelated(remoteModel);
|
|
620
|
+
remoteModels.add(remoteModel);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (const url of newRemoteModelUrls) {
|
|
624
|
+
const remoteModel = remoteModels.require(url);
|
|
625
|
+
const localModel = cloneRemoteModel(remoteModel);
|
|
626
|
+
|
|
627
|
+
localRelation.addRelated(localModel);
|
|
628
|
+
localModels.add(localModel);
|
|
629
|
+
this.syncedModelUrls.add(localModel.url);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
for (const localModel of localModels.items()) {
|
|
633
|
+
const remoteModel = remoteModels.require(localModel.url);
|
|
634
|
+
|
|
635
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
636
|
+
await this.saveModelAndChildren(localModel);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private async rememberModelDocuments(model: SolidModel): Promise<void> {
|
|
641
|
+
if (!isContainer(model)) {
|
|
642
|
+
const documentUrl = model.requireDocumentUrl();
|
|
643
|
+
const modifiedAt = this.documentsModifiedAt[documentUrl];
|
|
644
|
+
|
|
645
|
+
if (modifiedAt) {
|
|
646
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
647
|
+
|
|
648
|
+
delete this.documentsModifiedAt[documentUrl];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const children = getContainerRelations(model.static())
|
|
655
|
+
.map((relation) => model.getRelationModels(relation) ?? [])
|
|
656
|
+
.flat();
|
|
657
|
+
|
|
658
|
+
for (const child of children) {
|
|
659
|
+
await this.rememberModelDocuments(child);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private async ignoringDocumentErrors(operation: Function): Promise<void> {
|
|
664
|
+
try {
|
|
665
|
+
await operation();
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.handleDocumentError(error);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private handleDocumentError(error: unknown): void {
|
|
672
|
+
if (isInstanceOf(error, MalformedSolidDocumentError)) {
|
|
673
|
+
// eslint-disable-next-line no-console
|
|
674
|
+
console.warn(error.message);
|
|
675
|
+
|
|
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));
|
|
687
|
+
|
|
688
|
+
documentUrl && this.documentsWithErrors.add(documentUrl);
|
|
689
|
+
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
}
|