@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.
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +9 -0
- package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +1 -0
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +5 -0
- package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +1 -0
- package/dist/aerogel-plugin-local-first.d.ts +269 -0
- package/dist/aerogel-plugin-local-first.js +1686 -0
- package/dist/aerogel-plugin-local-first.js.map +1 -0
- package/package.json +50 -0
- package/src/components/AGCloudStatus.vue +31 -0
- package/src/components/index.ts +3 -0
- package/src/index.ts +29 -0
- package/src/jobs/Backup.ts +98 -0
- package/src/jobs/Migrate.test.ts +217 -0
- package/src/jobs/Migrate.ts +179 -0
- package/src/jobs/Sync.test.ts +982 -0
- package/src/jobs/Sync.ts +590 -0
- package/src/jobs/mixins/LoadsChildren.ts +210 -0
- package/src/jobs/mixins/LoadsTypeIndex.ts +23 -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 +327 -0
- package/src/lib/utils.ts +21 -0
- package/src/services/Cloud.state.ts +111 -0
- package/src/services/Cloud.ts +450 -0
- package/src/services/DocumentsCache.ts +35 -0
- package/src/testing/cloud.ts +193 -0
- package/src/testing/mocks/Solid.mock.ts +34 -0
- package/src/testing/setup.ts +32 -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/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,590 @@
|
|
|
1
|
+
import { App, Job } from '@aerogel/core';
|
|
2
|
+
import { isInstanceOf, map, mixed, requireUrlParentDirectory, required, round } from '@noeldemartin/utils';
|
|
3
|
+
import { MalformedSolidDocumentError } from '@noeldemartin/solid-utils';
|
|
4
|
+
import { Solid } from '@aerogel/plugin-solid';
|
|
5
|
+
import { SolidModel, Tombstone, isContainer, isContainerClass } from 'soukai-solid';
|
|
6
|
+
import type { ObjectsMap } from '@noeldemartin/utils';
|
|
7
|
+
import type { JobListener, JobStatus } from '@aerogel/core';
|
|
8
|
+
import type { SolidContainsRelation, SolidEngine, SolidTypeRegistration } from 'soukai-solid';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
clearLocalModelUpdates,
|
|
12
|
+
cloneLocalModel,
|
|
13
|
+
cloneRemoteModel,
|
|
14
|
+
completeRemoteModels,
|
|
15
|
+
getLocalModel,
|
|
16
|
+
getLocalModels,
|
|
17
|
+
getRemoteClass,
|
|
18
|
+
getRemoteModel,
|
|
19
|
+
isRegisteredModel,
|
|
20
|
+
isRemoteModel,
|
|
21
|
+
trackModelsCollection,
|
|
22
|
+
} from '@aerogel/plugin-local-first/lib/mirroring';
|
|
23
|
+
import {
|
|
24
|
+
getContainerRegisteredClasses,
|
|
25
|
+
getContainerRelations,
|
|
26
|
+
getRelatedAppModels,
|
|
27
|
+
isDirtyOrHasDirtyChildren,
|
|
28
|
+
} from '@aerogel/plugin-local-first/lib/inference';
|
|
29
|
+
import DocumentsCache from '@aerogel/plugin-local-first/services/DocumentsCache';
|
|
30
|
+
|
|
31
|
+
import LoadsChildren from './mixins/LoadsChildren';
|
|
32
|
+
import LoadsTypeIndex from './mixins/LoadsTypeIndex';
|
|
33
|
+
import TracksLocalModels from './mixins/TracksLocalModels';
|
|
34
|
+
import type { ResourceJobStatus } from './mixins/LoadsChildren';
|
|
35
|
+
|
|
36
|
+
const Cloud = required(() => App.service('$cloud'));
|
|
37
|
+
const BaseJob = Job as typeof Job<JobListener, SyncJobStatus, SyncJobStatus>;
|
|
38
|
+
|
|
39
|
+
interface SyncJobStatus extends JobStatus {
|
|
40
|
+
root: true;
|
|
41
|
+
children: [SyncPullJobStatus, JobStatus];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SyncPullJobStatus extends JobStatus {
|
|
45
|
+
children?: ResourceJobStatus[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRootStatus(status?: JobStatus): status is SyncJobStatus {
|
|
49
|
+
return !!status && 'root' in status;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default class Sync extends mixed(BaseJob, [LoadsChildren, LoadsTypeIndex, TracksLocalModels]) {
|
|
53
|
+
|
|
54
|
+
protected models?: SolidModel[];
|
|
55
|
+
protected dirtyRemoteModels?: SolidModel[];
|
|
56
|
+
protected rootLocalModels: SolidModel[];
|
|
57
|
+
protected tombstones: ObjectsMap<Tombstone>;
|
|
58
|
+
protected malformedDocuments: Set<string>;
|
|
59
|
+
protected documentsModifiedAt: Record<string, Date>;
|
|
60
|
+
protected override localModels: ObjectsMap<SolidModel>;
|
|
61
|
+
|
|
62
|
+
constructor(models?: SolidModel[]) {
|
|
63
|
+
super();
|
|
64
|
+
|
|
65
|
+
this.models = models;
|
|
66
|
+
this.rootLocalModels = [];
|
|
67
|
+
this.localModels = map([], 'url');
|
|
68
|
+
this.tombstones = map([], 'resourceUrl');
|
|
69
|
+
this.malformedDocuments = new Set();
|
|
70
|
+
this.documentsModifiedAt = {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected async run(): Promise<void> {
|
|
74
|
+
const engine = Solid.requireAuthenticator().engine as SolidEngine;
|
|
75
|
+
const updateDocumentDate = (url: string, { headers }: { headers: Headers }) => {
|
|
76
|
+
const date = headers?.get('last-modified');
|
|
77
|
+
|
|
78
|
+
if (!date || url.endsWith('/')) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.documentsModifiedAt[url] = new Date(date);
|
|
83
|
+
};
|
|
84
|
+
const clearListener = engine.listeners.add({
|
|
85
|
+
onDocumentRead: updateDocumentDate,
|
|
86
|
+
onDocumentUpdated: updateDocumentDate,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
this.rootLocalModels = this.models ?? getLocalModels();
|
|
91
|
+
|
|
92
|
+
await this.indexLocalModels(this.rootLocalModels);
|
|
93
|
+
await this.pullChanges();
|
|
94
|
+
await this.pushChanges();
|
|
95
|
+
} finally {
|
|
96
|
+
clearListener();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected override getInitialStatus(): SyncJobStatus {
|
|
101
|
+
return {
|
|
102
|
+
root: true,
|
|
103
|
+
completed: false,
|
|
104
|
+
children: [{ completed: false }, { completed: false }],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected override calculateCurrentProgress(status?: JobStatus): number {
|
|
109
|
+
status ??= this.status;
|
|
110
|
+
|
|
111
|
+
if (isRootStatus(status)) {
|
|
112
|
+
return round(
|
|
113
|
+
this.calculateCurrentProgress(status.children[0]) * 0.9 +
|
|
114
|
+
this.calculateCurrentProgress(status.children[1]) * 0.1,
|
|
115
|
+
2,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return super.calculateCurrentProgress(status);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async pullChanges(): Promise<void> {
|
|
123
|
+
const remoteModelsArray = this.models
|
|
124
|
+
? await this.fetchRemoteModelsForLocal(this.models)
|
|
125
|
+
: await this.fetchRemoteModels();
|
|
126
|
+
const remoteModels = map(remoteModelsArray, (model) => {
|
|
127
|
+
if (model instanceof Tombstone) {
|
|
128
|
+
return model.resourceUrl;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return model.url;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.dirtyRemoteModels = await this.synchronizeModels(remoteModels);
|
|
135
|
+
|
|
136
|
+
await this.updateProgress((status) => (status.children[0].completed = true));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async pushChanges(): Promise<void> {
|
|
140
|
+
const remoteModels = this.dirtyRemoteModels ?? [];
|
|
141
|
+
const childrenStatus = (this.status.children[1].children = remoteModels.map(() => ({ completed: false })));
|
|
142
|
+
|
|
143
|
+
for (let index = 0; index < remoteModels.length; index++) {
|
|
144
|
+
const remoteModel = remoteModels[index] as SolidModel;
|
|
145
|
+
const childStatus = childrenStatus[index] as JobStatus;
|
|
146
|
+
|
|
147
|
+
if (remoteModel.isSoftDeleted()) {
|
|
148
|
+
remoteModel.enableHistory();
|
|
149
|
+
remoteModel.enableTombstone();
|
|
150
|
+
|
|
151
|
+
await remoteModel.delete();
|
|
152
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
153
|
+
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await this.saveModelAndChildren(remoteModel, childStatus);
|
|
158
|
+
await this.updateTypeRegistrations(remoteModel);
|
|
159
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const [documentUrl, modifiedAt] of Object.entries(this.documentsModifiedAt)) {
|
|
163
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
clearLocalModelUpdates(this.models, this.malformedDocuments);
|
|
167
|
+
|
|
168
|
+
await this.updateProgress((status) => (status.children[1].completed = true));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async fetchRemoteModels(): Promise<SolidModel[]> {
|
|
172
|
+
const pullStatus = this.status.children[0];
|
|
173
|
+
const typeIndex = await this.loadTypeIndex();
|
|
174
|
+
|
|
175
|
+
if (!typeIndex) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
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);
|
|
213
|
+
|
|
214
|
+
pullStatus.children = registrations.map(() => ({ completed: false }));
|
|
215
|
+
|
|
216
|
+
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];
|
|
220
|
+
const remoteClass = getRemoteClass(registeredModel.modelClass);
|
|
221
|
+
|
|
222
|
+
if (!registration.instanceContainer) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (isContainerClass(remoteClass)) {
|
|
227
|
+
await trackModelsCollection(
|
|
228
|
+
registeredModel.modelClass,
|
|
229
|
+
requireUrlParentDirectory(registration.instanceContainer),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const container = await remoteClass.find(registration.instanceContainer);
|
|
233
|
+
|
|
234
|
+
if (container) {
|
|
235
|
+
await this.loadChildren(container, required(pullStatus.children?.[index]));
|
|
236
|
+
|
|
237
|
+
remoteModels.push(container);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await this.updateProgress(() => (required(pullStatus.children?.[index]).completed = true));
|
|
241
|
+
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await trackModelsCollection(registeredModel.modelClass, registration.instanceContainer);
|
|
246
|
+
|
|
247
|
+
remoteModels.push(...(await remoteClass.from(registration.instanceContainer).all()));
|
|
248
|
+
|
|
249
|
+
await this.updateProgress(() => (required(pullStatus.children?.[index]).completed = true));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return completeRemoteModels(this.rootLocalModels, remoteModels, this.malformedDocuments);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async fetchRemoteModelsForLocal(models: SolidModel[]): Promise<SolidModel[]> {
|
|
257
|
+
const remoteModels = [] as SolidModel[];
|
|
258
|
+
const statusChildren = (this.status.children[0].children = models.map(() => ({ completed: false })));
|
|
259
|
+
|
|
260
|
+
for (let index = 0; index < models.length; index++) {
|
|
261
|
+
await this.ignoringMalformedDocumentErrors(async () => {
|
|
262
|
+
const localModel = models[index] as SolidModel;
|
|
263
|
+
const childStatus = statusChildren[index] as JobStatus;
|
|
264
|
+
const remoteModel = await getRemoteClass(localModel.static()).find(localModel.url);
|
|
265
|
+
|
|
266
|
+
if (!remoteModel) {
|
|
267
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
268
|
+
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
isContainer(remoteModel) && (await this.loadChildren(remoteModel, childStatus));
|
|
273
|
+
|
|
274
|
+
remoteModels.push(remoteModel);
|
|
275
|
+
|
|
276
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return completeRemoteModels(models, remoteModels, this.malformedDocuments);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async updateTypeRegistrations(remoteModel: SolidModel): Promise<void> {
|
|
284
|
+
if (!isContainer(remoteModel) || !isRegisteredModel(remoteModel)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const registeredChildren = getContainerRegisteredClasses(remoteModel.static());
|
|
289
|
+
|
|
290
|
+
if (registeredChildren.length === 0) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const typeIndex = await this.loadTypeIndex({ create: true });
|
|
295
|
+
|
|
296
|
+
if (typeIndex.registrations.some((registration) => registration.instanceContainer === remoteModel.url)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await remoteModel.register(typeIndex, registeredChildren);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private async loadChildren(model: SolidModel, childStatus?: JobStatus): Promise<void> {
|
|
304
|
+
this.assertNotCancelled();
|
|
305
|
+
|
|
306
|
+
if (!isContainer(model)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const children = await this.loadContainedModels(model, {
|
|
311
|
+
status: childStatus,
|
|
312
|
+
onLoaded: async () => {
|
|
313
|
+
this.assertNotCancelled();
|
|
314
|
+
|
|
315
|
+
await this.updateProgress();
|
|
316
|
+
},
|
|
317
|
+
onMalformedDocument: (url) => this.malformedDocuments.add(url),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
for (let index = 0; index < children.length; index++) {
|
|
321
|
+
const child = children[index] as SolidModel;
|
|
322
|
+
|
|
323
|
+
if (child instanceof Tombstone) {
|
|
324
|
+
this.tombstones.add(child);
|
|
325
|
+
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (this.tombstones.hasKey(child.url)) {
|
|
330
|
+
await this.deleteChild(model, child);
|
|
331
|
+
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await this.loadChildren(child, childStatus?.children?.[index]);
|
|
336
|
+
|
|
337
|
+
if (childStatus?.children?.[index]) {
|
|
338
|
+
await this.updateProgress(() => (required(childStatus.children?.[index]).completed = true));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private async deleteChild(model: SolidModel, child: SolidModel): Promise<void> {
|
|
344
|
+
for (const relation of model.static('relations')) {
|
|
345
|
+
const relationInstance = model.requireRelation(relation);
|
|
346
|
+
|
|
347
|
+
if (!relationInstance.related) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (Array.isArray(relationInstance.related)) {
|
|
352
|
+
relationInstance.related = relationInstance.related.filter((relatedModel) => relatedModel !== child);
|
|
353
|
+
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (relationInstance.related === child) {
|
|
358
|
+
relationInstance.related = null;
|
|
359
|
+
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await child.delete();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async saveModelAndChildren(model: SolidModel, status?: JobStatus): Promise<void> {
|
|
368
|
+
this.assertNotCancelled();
|
|
369
|
+
|
|
370
|
+
if (isRemoteModel(model) && model.isSoftDeleted()) {
|
|
371
|
+
model.enableHistory();
|
|
372
|
+
model.enableTombstone();
|
|
373
|
+
await model.delete();
|
|
374
|
+
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (this.malformedDocuments.has(model.requireDocumentUrl())) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await model.save();
|
|
383
|
+
|
|
384
|
+
if (!isContainer(model)) {
|
|
385
|
+
if (isRemoteModel(model)) {
|
|
386
|
+
const documentUrl = model.requireDocumentUrl();
|
|
387
|
+
const modifiedAt = this.documentsModifiedAt[documentUrl] ?? new Date();
|
|
388
|
+
|
|
389
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
390
|
+
|
|
391
|
+
delete this.documentsModifiedAt[documentUrl];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const children = getContainerRelations(model.static())
|
|
398
|
+
.map((relation) => model.getRelationModels(relation) ?? [])
|
|
399
|
+
.flat();
|
|
400
|
+
|
|
401
|
+
const childrenStatuses = status ? children.map(() => ({ completed: false })) : [];
|
|
402
|
+
|
|
403
|
+
if (status) {
|
|
404
|
+
status.children = childrenStatuses;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (let index = 0; index < children.length; index++) {
|
|
408
|
+
const child = children[index] as SolidModel;
|
|
409
|
+
const childStatus = childrenStatuses[index];
|
|
410
|
+
|
|
411
|
+
await this.saveModelAndChildren(child, childStatus);
|
|
412
|
+
|
|
413
|
+
if (childStatus) {
|
|
414
|
+
await this.updateProgress(() => (childStatus.completed = true));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async synchronizeModels(remoteModels: ObjectsMap<SolidModel>): Promise<SolidModel[]> {
|
|
420
|
+
const synchronizedModelUrls = new Set<string>();
|
|
421
|
+
const deletedModels = new Set<SolidModel>();
|
|
422
|
+
|
|
423
|
+
for (const remoteModel of remoteModels.items()) {
|
|
424
|
+
if (remoteModel instanceof Tombstone) {
|
|
425
|
+
const localModel = this.localModels.get(remoteModel.resourceUrl);
|
|
426
|
+
|
|
427
|
+
if (localModel) {
|
|
428
|
+
await localModel.delete();
|
|
429
|
+
|
|
430
|
+
deletedModels.add(localModel);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const localModel = getLocalModel(remoteModel, this.localModels);
|
|
437
|
+
|
|
438
|
+
await this.loadChildren(localModel);
|
|
439
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
440
|
+
await this.reconcileInconsistencies(localModel, remoteModel);
|
|
441
|
+
await this.saveModelAndChildren(localModel);
|
|
442
|
+
await this.addLocalModel(localModel);
|
|
443
|
+
|
|
444
|
+
this.rootLocalModels.push(localModel);
|
|
445
|
+
synchronizedModelUrls.add(localModel.url);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (const localModel of this.rootLocalModels) {
|
|
449
|
+
if (deletedModels.has(localModel) || synchronizedModelUrls.has(localModel.url)) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await this.loadChildren(localModel);
|
|
454
|
+
|
|
455
|
+
const remoteModel = getRemoteModel(localModel, remoteModels);
|
|
456
|
+
|
|
457
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
458
|
+
|
|
459
|
+
remoteModels.add(remoteModel);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const dirtyRemoteModels = [];
|
|
463
|
+
|
|
464
|
+
for (const remoteModel of remoteModels.getItems()) {
|
|
465
|
+
if (isDirtyOrHasDirtyChildren(remoteModel)) {
|
|
466
|
+
dirtyRemoteModels.push(remoteModel);
|
|
467
|
+
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
await this.rememberModelDocuments(remoteModel);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return dirtyRemoteModels;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private async reconcileInconsistencies(
|
|
478
|
+
localModel: SolidModel,
|
|
479
|
+
remoteModel: SolidModel,
|
|
480
|
+
processedModels?: Set<SolidModel>,
|
|
481
|
+
): Promise<void> {
|
|
482
|
+
localModel.rebuildAttributesFromHistory();
|
|
483
|
+
localModel.setAttributes(remoteModel.getAttributes(true));
|
|
484
|
+
|
|
485
|
+
processedModels ??= new Set();
|
|
486
|
+
processedModels.add(localModel);
|
|
487
|
+
|
|
488
|
+
for (const relation of getContainerRelations(localModel.static())) {
|
|
489
|
+
await this.synchronizeContainerDocuments(
|
|
490
|
+
localModel.requireRelation<SolidContainsRelation>(relation),
|
|
491
|
+
remoteModel.requireRelation<SolidContainsRelation>(relation),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const remoteRelatedModels = map(getRelatedAppModels(remoteModel), 'url');
|
|
496
|
+
|
|
497
|
+
for (const localRelatedModel of getRelatedAppModels(localModel)) {
|
|
498
|
+
if (
|
|
499
|
+
processedModels.has(localRelatedModel) ||
|
|
500
|
+
!localRelatedModel.isRelationLoaded('operations') ||
|
|
501
|
+
!remoteRelatedModels.hasKey(localRelatedModel.url)
|
|
502
|
+
) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
await this.reconcileInconsistencies(localRelatedModel, remoteRelatedModels.require(localRelatedModel.url));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (localModel.isDirty()) {
|
|
510
|
+
await localModel.save();
|
|
511
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private async synchronizeContainerDocuments(
|
|
516
|
+
localRelation: SolidContainsRelation,
|
|
517
|
+
remoteRelation: SolidContainsRelation,
|
|
518
|
+
): Promise<void> {
|
|
519
|
+
const localModels = map(localRelation.related ?? [], 'url');
|
|
520
|
+
const remoteModels = map(remoteRelation.related ?? [], 'url');
|
|
521
|
+
const newLocalModelUrls = localModels
|
|
522
|
+
.getKeys()
|
|
523
|
+
.filter((url) => !remoteModels.hasKey(url) && !remoteModels.hasKey(encodeURI(url)));
|
|
524
|
+
const newRemoteModelUrls = remoteModels
|
|
525
|
+
.getKeys()
|
|
526
|
+
.filter((url) => !localModels.hasKey(url) && !localModels.hasKey(encodeURI(url)));
|
|
527
|
+
|
|
528
|
+
for (const url of newLocalModelUrls) {
|
|
529
|
+
const localModel = localModels.require(url);
|
|
530
|
+
const remoteModel = cloneLocalModel(localModel);
|
|
531
|
+
|
|
532
|
+
remoteRelation.addRelated(remoteModel);
|
|
533
|
+
remoteModels.add(remoteModel);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
for (const url of newRemoteModelUrls) {
|
|
537
|
+
const remoteModel = remoteModels.require(url);
|
|
538
|
+
const localModel = cloneRemoteModel(remoteModel);
|
|
539
|
+
|
|
540
|
+
localRelation.addRelated(localModel);
|
|
541
|
+
localModels.add(localModel);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (const localModel of localModels.items()) {
|
|
545
|
+
const remoteModel = remoteModels.require(localModel.url);
|
|
546
|
+
|
|
547
|
+
await SolidModel.synchronize(localModel, remoteModel);
|
|
548
|
+
await this.saveModelAndChildren(localModel);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private async rememberModelDocuments(model: SolidModel): Promise<void> {
|
|
553
|
+
if (!isContainer(model)) {
|
|
554
|
+
const documentUrl = model.requireDocumentUrl();
|
|
555
|
+
const modifiedAt = this.documentsModifiedAt[documentUrl] ?? new Date();
|
|
556
|
+
|
|
557
|
+
await DocumentsCache.remember(documentUrl, modifiedAt);
|
|
558
|
+
|
|
559
|
+
delete this.documentsModifiedAt[documentUrl];
|
|
560
|
+
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const children = getContainerRelations(model.static())
|
|
565
|
+
.map((relation) => model.getRelationModels(relation) ?? [])
|
|
566
|
+
.flat();
|
|
567
|
+
|
|
568
|
+
for (const child of children) {
|
|
569
|
+
await this.rememberModelDocuments(child);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private async ignoringMalformedDocumentErrors(operation: Function): Promise<void> {
|
|
574
|
+
try {
|
|
575
|
+
await operation();
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (isInstanceOf(error, MalformedSolidDocumentError)) {
|
|
578
|
+
// eslint-disable-next-line no-console
|
|
579
|
+
console.warn(error.message);
|
|
580
|
+
|
|
581
|
+
error.documentUrl && this.malformedDocuments.add(error.documentUrl);
|
|
582
|
+
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
}
|