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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js +9 -0
  2. package/dist/InruptAuthenticator.lazy-BvpQLGoj-BvpQLGoj.js.map +1 -0
  3. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js +5 -0
  4. package/dist/LegacyAuthenticator.lazy-ChFFqP14-ChFFqP14.js.map +1 -0
  5. package/dist/aerogel-plugin-local-first.d.ts +269 -0
  6. package/dist/aerogel-plugin-local-first.js +1686 -0
  7. package/dist/aerogel-plugin-local-first.js.map +1 -0
  8. package/package.json +50 -0
  9. package/src/components/AGCloudStatus.vue +31 -0
  10. package/src/components/index.ts +3 -0
  11. package/src/index.ts +29 -0
  12. package/src/jobs/Backup.ts +98 -0
  13. package/src/jobs/Migrate.test.ts +217 -0
  14. package/src/jobs/Migrate.ts +179 -0
  15. package/src/jobs/Sync.test.ts +982 -0
  16. package/src/jobs/Sync.ts +590 -0
  17. package/src/jobs/mixins/LoadsChildren.ts +210 -0
  18. package/src/jobs/mixins/LoadsTypeIndex.ts +23 -0
  19. package/src/jobs/mixins/TracksLocalModels.ts +32 -0
  20. package/src/lib/SyncQueue.ts +94 -0
  21. package/src/lib/inference.ts +124 -0
  22. package/src/lib/mirroring.ts +327 -0
  23. package/src/lib/utils.ts +21 -0
  24. package/src/services/Cloud.state.ts +111 -0
  25. package/src/services/Cloud.ts +450 -0
  26. package/src/services/DocumentsCache.ts +35 -0
  27. package/src/testing/cloud.ts +193 -0
  28. package/src/testing/mocks/Solid.mock.ts +34 -0
  29. package/src/testing/setup.ts +32 -0
  30. package/src/testing/stubs/LegacyTask.schema.ts +15 -0
  31. package/src/testing/stubs/Movie.schema.ts +15 -0
  32. package/src/testing/stubs/Movie.ts +3 -0
  33. package/src/testing/stubs/MoviesContainer.ts +20 -0
  34. package/src/testing/stubs/Task.schema.ts +12 -0
  35. package/src/testing/stubs/Task.ts +3 -0
  36. package/src/testing/stubs/TasksList.ts +20 -0
  37. package/src/testing/stubs/Workspace.ts +15 -0
  38. package/src/types/shims.d.ts +7 -0
@@ -0,0 +1,210 @@
1
+ import { arrayChunk, arrayFrom, isInstanceOf, map, required } from '@noeldemartin/utils';
2
+ import { DocumentNotFound } from 'soukai';
3
+ import { MalformedSolidDocumentError } from '@noeldemartin/solid-utils';
4
+ import { Tombstone } from 'soukai-solid';
5
+ import type { JobStatus } from '@aerogel/core';
6
+ import type { ObjectsMap } from '@noeldemartin/utils';
7
+ import type { SolidContainer, SolidContainsRelation, SolidDocument, SolidModel } from 'soukai-solid';
8
+
9
+ import DocumentsCache from '@aerogel/plugin-local-first/services/DocumentsCache';
10
+ import { cloneLocalModel, isLocalModel } from '@aerogel/plugin-local-first/lib/mirroring';
11
+ import { getContainerRelations } from '@aerogel/plugin-local-first/lib/inference';
12
+ import { lazy } from '@aerogel/plugin-local-first/lib/utils';
13
+
14
+ export interface ResourceJobStatus extends JobStatus {
15
+ documentUrl?: string;
16
+ children?: ResourceJobStatus[];
17
+ }
18
+
19
+ export default class LoadsChildren {
20
+
21
+ declare protected localModels?: ObjectsMap<SolidModel>;
22
+
23
+ protected async loadContainedModels(
24
+ model: SolidContainer,
25
+ options: {
26
+ ignoreTombstones?: boolean;
27
+ status?: ResourceJobStatus;
28
+ onLoaded?: (urls: string[]) => unknown;
29
+ onMalformedDocument?: (url: string) => unknown;
30
+ } = {},
31
+ ): Promise<SolidModel[]> {
32
+ if (isLocalModel(model)) {
33
+ const children = [];
34
+
35
+ for (const relation of getContainerRelations(model.static())) {
36
+ const relationModels =
37
+ (await model.loadRelationIfUnloaded<SolidModel | SolidModel[] | null>(relation)) ?? [];
38
+
39
+ children.push(...arrayFrom(relationModels));
40
+ }
41
+
42
+ return children;
43
+ }
44
+
45
+ const children: Record<string, SolidModel[]> = {};
46
+ const tombstones: Tombstone[] = [];
47
+ const documents = map(model.documents, 'url');
48
+ const documentUrlChunks = arrayChunk(model.resourceUrls, 10);
49
+ const statusChildren = documentUrlChunks.flat().map((url) => ({
50
+ documentUrl: url,
51
+ completed: false,
52
+ }));
53
+ const statusChildrenMap = map(statusChildren, 'documentUrl');
54
+
55
+ if (options.status) {
56
+ options.status.children = statusChildren;
57
+ }
58
+
59
+ for (const documentUrls of documentUrlChunks) {
60
+ await Promise.all(
61
+ documentUrls.map(async (documentUrl) => {
62
+ const { children: documentChildren, tombstones: documentTombstones } =
63
+ await this.loadDocumentChildren(model, documentUrl, documents, options.onMalformedDocument);
64
+
65
+ tombstones.push(...documentTombstones);
66
+
67
+ for (const [relation, documentModels] of Object.entries(documentChildren)) {
68
+ const relationChildren = (children[relation] ??= []);
69
+
70
+ relationChildren.push(...documentModels);
71
+ }
72
+ }),
73
+ );
74
+
75
+ for (const documentUrl of documentUrls) {
76
+ if (documentUrl.endsWith('/')) {
77
+ continue;
78
+ }
79
+
80
+ required(statusChildrenMap.get(documentUrl)).completed = true;
81
+ }
82
+
83
+ await options.onLoaded?.(documentUrls);
84
+ }
85
+
86
+ for (const [relation, relationModels] of Object.entries(children)) {
87
+ model.setRelationModels(relation, relationModels);
88
+ }
89
+
90
+ const childrenArray = options.ignoreTombstones
91
+ ? Object.values(children).flat()
92
+ : Object.values(children).flat().concat(tombstones);
93
+
94
+ if (options.status) {
95
+ options.status.children = childrenArray.map((child) => {
96
+ return required(statusChildrenMap.get(child.requireDocumentUrl()));
97
+ });
98
+ }
99
+
100
+ return childrenArray;
101
+ }
102
+
103
+ protected async loadDocumentChildren(
104
+ model: SolidModel,
105
+ documentUrl: string,
106
+ documents: ObjectsMap<SolidDocument>,
107
+ onMalformedDocument?: (url: string) => unknown,
108
+ ): Promise<{ children: Record<string, SolidModel[]>; tombstones: Tombstone[] }> {
109
+ const document = documents.get(documentUrl);
110
+
111
+ if (
112
+ document &&
113
+ (await DocumentsCache.isFresh(document.url, document.updatedAt ?? new Date())) &&
114
+ this.localModels?.hasKey(model.url)
115
+ ) {
116
+ return {
117
+ children: await this.loadDocumentChildrenFromLocal(model, document),
118
+ tombstones: [],
119
+ };
120
+ }
121
+
122
+ return this.loadDocumentChildrenFromRemote(model, documentUrl, onMalformedDocument);
123
+ }
124
+
125
+ protected async loadDocumentChildrenFromLocal(
126
+ model: SolidModel,
127
+ document: SolidDocument,
128
+ ): Promise<Record<string, SolidModel[]>> {
129
+ const children: Record<string, SolidModel[]> = {};
130
+
131
+ for (const relation of getContainerRelations(model.static())) {
132
+ const relationInstance = model.requireRelation<SolidContainsRelation>(relation);
133
+
134
+ if (relationInstance.loaded) {
135
+ children[relation] = relationInstance.getLoadedModels();
136
+
137
+ continue;
138
+ }
139
+
140
+ const relatedLocalModels = this.localModels?.get(model.url)?.getRelationModels(relation) ?? [];
141
+
142
+ children[relation] = relatedLocalModels
143
+ .filter((relatedLocalModel) => relatedLocalModel.getDocumentUrl() === document.url)
144
+ .map((relatedLocalModel) => cloneLocalModel(relatedLocalModel, { clean: true }));
145
+ }
146
+
147
+ return children;
148
+ }
149
+
150
+ protected async loadDocumentChildrenFromRemote(
151
+ model: SolidModel,
152
+ documentUrl: string,
153
+ onMalformedDocument?: (url: string) => unknown,
154
+ ): Promise<{ children: Record<string, SolidModel[]>; tombstones: Tombstone[] }> {
155
+ const children: Record<string, SolidModel[]> = {};
156
+ const tombstones: Tombstone[] = [];
157
+ const readDocument = lazy(async () => {
158
+ try {
159
+ const document = await model.requireEngine().readOne(model.static('collection'), documentUrl);
160
+ const documentTombstones = await Tombstone.createManyFromEngineDocuments({
161
+ [documentUrl]: document,
162
+ });
163
+
164
+ tombstones.push(...documentTombstones);
165
+
166
+ return document;
167
+ } catch (error) {
168
+ if (isInstanceOf(error, DocumentNotFound)) {
169
+ return null;
170
+ }
171
+
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;
179
+ }
180
+
181
+ throw error;
182
+ }
183
+ });
184
+
185
+ for (const relation of getContainerRelations(model.static())) {
186
+ const relationInstance = model.requireRelation<SolidContainsRelation>(relation);
187
+
188
+ if (relationInstance.loaded) {
189
+ children[relation] = relationInstance.getLoadedModels();
190
+
191
+ continue;
192
+ }
193
+
194
+ const document = await readDocument();
195
+
196
+ if (!document) {
197
+ continue;
198
+ }
199
+
200
+ const documentChildren = await relationInstance.relatedClass.createManyFromEngineDocuments({
201
+ [documentUrl]: document,
202
+ });
203
+
204
+ children[relation] = documentChildren;
205
+ }
206
+
207
+ return { children, tombstones };
208
+ }
209
+
210
+ }
@@ -0,0 +1,23 @@
1
+ import { Solid } from '@aerogel/plugin-solid';
2
+ import type { SolidTypeIndex } from 'soukai-solid';
3
+ import type { SolidUserProfile } from '@noeldemartin/solid-utils';
4
+
5
+ const typeIndexes: WeakMap<SolidUserProfile, SolidTypeIndex | null> = new WeakMap();
6
+
7
+ export default class LoadsTypeIndex {
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> {
12
+ const user = Solid.requireUser();
13
+
14
+ if (options.create && !typeIndexes.get(user)) {
15
+ typeIndexes.set(user, await Solid.findOrCreatePrivateTypeIndex());
16
+ } else if (!typeIndexes.has(user)) {
17
+ typeIndexes.set(user, await Solid.findPrivateTypeIndex());
18
+ }
19
+
20
+ return typeIndexes.get(user) ?? null;
21
+ }
22
+
23
+ }
@@ -0,0 +1,32 @@
1
+ import type { ObjectsMap } from '@noeldemartin/utils';
2
+ import type { SolidModel } from 'soukai-solid';
3
+
4
+ import { getContainerRelations } from '@aerogel/plugin-local-first/lib/inference';
5
+
6
+ export default class TracksLocalModels {
7
+
8
+ declare protected localModels?: ObjectsMap<SolidModel>;
9
+
10
+ protected async indexLocalModels(models: SolidModel[]): Promise<void> {
11
+ for (const localModel of models) {
12
+ await this.addLocalModel(localModel);
13
+ }
14
+ }
15
+
16
+ protected async addLocalModel(localModel: SolidModel): Promise<void> {
17
+ if (!this.localModels || this.localModels.hasKey(localModel.url)) {
18
+ return;
19
+ }
20
+
21
+ this.localModels.add(localModel);
22
+
23
+ for (const relation of getContainerRelations(localModel.static())) {
24
+ await localModel.loadRelationIfUnloaded(relation);
25
+ }
26
+
27
+ for (const relatedModel of localModel.getRelatedModels()) {
28
+ await this.addLocalModel(relatedModel);
29
+ }
30
+ }
31
+
32
+ }
@@ -0,0 +1,94 @@
1
+ import { after, arrayRemove, facade, required } from '@noeldemartin/utils';
2
+ import { App } from '@aerogel/core';
3
+ import { type SolidModel, isContainer } from 'soukai-solid';
4
+
5
+ import { getContainedModels } from '@aerogel/plugin-local-first/lib/inference';
6
+
7
+ const Cloud = required(() => App.service('$cloud'));
8
+
9
+ class SyncQueue {
10
+
11
+ private models: Set<SolidModel> = new Set();
12
+ private promises: Promise<unknown>[] = [];
13
+
14
+ public push(model: SolidModel): void {
15
+ this.models.add(model);
16
+ this.syncAfter(after({ seconds: 1 }));
17
+ }
18
+
19
+ public clear(models?: SolidModel[]): void {
20
+ if (!models) {
21
+ this.models = new Set();
22
+
23
+ return;
24
+ }
25
+
26
+ for (const model of models) {
27
+ this.models.delete(model);
28
+ }
29
+ }
30
+
31
+ protected async syncAfter<T = void>(operationOrPromise: Promise<T> | (() => Promise<T>)): Promise<T> {
32
+ const promise = typeof operationOrPromise === 'function' ? operationOrPromise() : operationOrPromise;
33
+
34
+ this.promises.push(promise);
35
+
36
+ promise.then(() => arrayRemove(this.promises, promise));
37
+
38
+ if (this.promises.length === 1) {
39
+ this.schedule();
40
+ }
41
+
42
+ return promise as Promise<T>;
43
+ }
44
+
45
+ private schedule(): void {
46
+ Promise.all(this.promises)
47
+ .then(() => after())
48
+ .then(() => {
49
+ if (this.promises.length !== 0) {
50
+ this.schedule();
51
+
52
+ return;
53
+ }
54
+
55
+ const models = this.consume();
56
+
57
+ if (models.length === 0) {
58
+ return;
59
+ }
60
+
61
+ Cloud.syncIfOnline(models);
62
+ });
63
+ }
64
+
65
+ private consume(): SolidModel[] {
66
+ const models: SolidModel[] = [];
67
+ const children: Set<SolidModel> = new Set();
68
+
69
+ for (const model of this.models) {
70
+ if (!isContainer(model)) {
71
+ continue;
72
+ }
73
+
74
+ this.models.delete(model);
75
+ getContainedModels(model).forEach((child) => children.add(child));
76
+ models.push(model);
77
+ }
78
+
79
+ for (const model of this.models) {
80
+ this.models.delete(model);
81
+
82
+ if (children.has(model)) {
83
+ continue;
84
+ }
85
+
86
+ models.push(model);
87
+ }
88
+
89
+ return models;
90
+ }
91
+
92
+ }
93
+
94
+ export default facade(SyncQueue);
@@ -0,0 +1,124 @@
1
+ import {
2
+ Metadata,
3
+ Operation,
4
+ SolidACLAuthorization,
5
+ SolidContainer,
6
+ SolidContainsRelation,
7
+ SolidDocument,
8
+ SolidModel,
9
+ isContainerClass,
10
+ isSolidDocumentRelation,
11
+ } from 'soukai-solid';
12
+ import { arrayFilter, isInstanceOf } from '@noeldemartin/utils';
13
+
14
+ import type { SolidContainerConstructor, SolidModelConstructor } from 'soukai-solid';
15
+
16
+ const containerRelations: WeakMap<typeof SolidModel, string[]> = new WeakMap();
17
+ const sameDocumentRelations: WeakMap<typeof SolidModel, string[]> = new WeakMap();
18
+
19
+ export function getContainerRegisteredClasses(modelClass: SolidContainerConstructor): SolidModelConstructor[] {
20
+ return arrayFilter(
21
+ getContainerRelations(modelClass).map((relation) => {
22
+ const relatedClass = modelClass.instance().requireRelation<SolidContainsRelation>(relation).relatedClass;
23
+
24
+ if (isContainerClass(relatedClass)) {
25
+ return false;
26
+ }
27
+
28
+ return relatedClass;
29
+ }),
30
+ );
31
+ }
32
+
33
+ export function getContainerRelations(modelClass: typeof SolidModel): string[] {
34
+ if (!containerRelations.has(modelClass)) {
35
+ containerRelations.set(
36
+ modelClass,
37
+ modelClass.relations.filter((relation) => {
38
+ if (SolidModel.reservedRelations.includes(relation)) {
39
+ return false;
40
+ }
41
+
42
+ const relationInstance = modelClass.instance().requireRelation(relation);
43
+
44
+ return relationInstance instanceof SolidContainsRelation;
45
+ }),
46
+ );
47
+ }
48
+
49
+ return containerRelations.get(modelClass) ?? [];
50
+ }
51
+
52
+ export function getContainedModels(model: SolidContainer): SolidModel[] {
53
+ const relations = getContainerRelations(model.static());
54
+
55
+ return relations.reduce(
56
+ (models, relation) => models.concat(model.getRelationModels(relation) ?? []),
57
+ [] as SolidModel[],
58
+ );
59
+ }
60
+
61
+ export function getRelatedClasses(modelClass: SolidModelConstructor): SolidModelConstructor[] {
62
+ const relatedClasses = new Set<SolidModelConstructor>();
63
+ const instance = modelClass.instance();
64
+
65
+ relatedClasses.add(modelClass);
66
+
67
+ for (const relation of modelClass.relations) {
68
+ if (modelClass.reservedRelations.includes(relation)) {
69
+ continue;
70
+ }
71
+
72
+ relatedClasses.add(instance.requireRelation(relation).relatedClass);
73
+ }
74
+
75
+ return [...relatedClasses.values()];
76
+ }
77
+
78
+ export function getRelatedAppModels(model: SolidModel): SolidModel[] {
79
+ return model
80
+ .getRelatedModels()
81
+ .filter(
82
+ (related) =>
83
+ !isInstanceOf(related, SolidACLAuthorization) &&
84
+ !isInstanceOf(related, SolidDocument) &&
85
+ !isInstanceOf(related, Metadata) &&
86
+ !isInstanceOf(related, Operation) &&
87
+ related.static() !== SolidContainer,
88
+ );
89
+ }
90
+
91
+ export function getSameDocumentRelations(modelClass: typeof SolidModel): string[] {
92
+ if (!sameDocumentRelations.has(modelClass)) {
93
+ sameDocumentRelations.set(
94
+ modelClass,
95
+ modelClass.relations.filter((relation) => {
96
+ if (SolidModel.reservedRelations.includes(relation)) {
97
+ return false;
98
+ }
99
+
100
+ const relationInstance = modelClass.instance().requireRelation(relation);
101
+
102
+ return isSolidDocumentRelation(relationInstance) && relationInstance.useSameDocument;
103
+ }),
104
+ );
105
+ }
106
+
107
+ return sameDocumentRelations.get(modelClass) ?? [];
108
+ }
109
+
110
+ export function isDirtyOrHasDirtyChildren(model: SolidModel): boolean {
111
+ if (model.isDirty()) {
112
+ return true;
113
+ }
114
+
115
+ if (!(model instanceof SolidContainer)) {
116
+ return false;
117
+ }
118
+
119
+ const relationHasDirtyModels = (relation: string) => {
120
+ return model.getRelationModels(relation)?.some((relatedModel) => isDirtyOrHasDirtyChildren(relatedModel));
121
+ };
122
+
123
+ return getContainerRelations(model.static()).some(relationHasDirtyModels);
124
+ }