@verdant-web/store 4.1.2 → 4.1.3
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/bundle/index.js +5 -5
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/__tests__/queries.test.js +51 -2
- package/dist/esm/__tests__/queries.test.js.map +1 -1
- package/dist/esm/client/Client.d.ts +3 -1
- package/dist/esm/client/Client.js +8 -1
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.d.ts +6 -2
- package/dist/esm/client/ClientDescriptor.js +7 -6
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/context/context.d.ts +17 -3
- package/dist/esm/entities/Entity.d.ts +2 -2
- package/dist/esm/entities/Entity.js +5 -1
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/types.d.ts +41 -0
- package/dist/esm/files/FileManager.d.ts +3 -1
- package/dist/esm/files/FileManager.js +9 -2
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/persistence/PersistenceFiles.js +3 -2
- package/dist/esm/persistence/PersistenceFiles.js.map +1 -1
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.d.ts +2 -2
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js +12 -4
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -1
- package/dist/esm/persistence/persistence.js +3 -3
- package/dist/esm/persistence/persistence.js.map +1 -1
- package/dist/esm/queries/BaseQuery.js +2 -1
- package/dist/esm/queries/BaseQuery.js.map +1 -1
- package/dist/esm/queries/QueryCache.d.ts +8 -1
- package/dist/esm/queries/QueryCache.js +33 -2
- package/dist/esm/queries/QueryCache.js.map +1 -1
- package/dist/esm/sync/FileSync.d.ts +1 -1
- package/dist/esm/sync/FileSync.js +2 -1
- package/dist/esm/sync/FileSync.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/queries.test.ts +70 -3
- package/src/client/Client.ts +13 -7
- package/src/client/ClientDescriptor.ts +14 -8
- package/src/context/context.ts +18 -3
- package/src/entities/Entity.ts +8 -4
- package/src/entities/types.ts +57 -24
- package/src/files/FileManager.ts +15 -2
- package/src/persistence/PersistenceFiles.ts +11 -6
- package/src/persistence/idb/files/IdbPersistenceFileDb.ts +22 -9
- package/src/persistence/persistence.ts +4 -2
- package/src/queries/BaseQuery.ts +3 -1
- package/src/queries/QueryCache.ts +56 -2
- package/src/sync/FileSync.ts +3 -2
|
@@ -6,17 +6,17 @@ import {
|
|
|
6
6
|
StorageSchema,
|
|
7
7
|
noop,
|
|
8
8
|
} from '@verdant-web/common';
|
|
9
|
-
import { FileConfig, InitialContext } from '../context/context.js';
|
|
9
|
+
import { FileConfig, InitialContext, QueryConfig } from '../context/context.js';
|
|
10
|
+
import { ShutdownHandler } from '../context/ShutdownHandler.js';
|
|
11
|
+
import { Time } from '../context/Time.js';
|
|
12
|
+
import { FakeWeakRef } from '../FakeWeakRef.js';
|
|
13
|
+
import { IdbPersistence } from '../persistence/idb/idbPersistence.js';
|
|
14
|
+
import { deleteAllDatabases } from '../persistence/idb/util.js';
|
|
15
|
+
import { PersistenceImplementation } from '../persistence/interfaces.js';
|
|
16
|
+
import { initializePersistence } from '../persistence/persistence.js';
|
|
10
17
|
import { ServerSyncOptions } from '../sync/Sync.js';
|
|
11
18
|
import { UndoHistory } from '../UndoHistory.js';
|
|
12
19
|
import { Client } from './Client.js';
|
|
13
|
-
import { deleteAllDatabases } from '../persistence/idb/util.js';
|
|
14
|
-
import { FakeWeakRef } from '../FakeWeakRef.js';
|
|
15
|
-
import { Time } from '../context/Time.js';
|
|
16
|
-
import { initializePersistence } from '../persistence/persistence.js';
|
|
17
|
-
import { PersistenceImplementation } from '../persistence/interfaces.js';
|
|
18
|
-
import { IdbPersistence } from '../persistence/idb/idbPersistence.js';
|
|
19
|
-
import { ShutdownHandler } from '../context/ShutdownHandler.js';
|
|
20
20
|
|
|
21
21
|
export interface ClientDescriptorOptions<Presence = any, Profile = any> {
|
|
22
22
|
/** The schema used to create this client */
|
|
@@ -76,6 +76,11 @@ export interface ClientDescriptorOptions<Presence = any, Profile = any> {
|
|
|
76
76
|
* before turning it on.
|
|
77
77
|
*/
|
|
78
78
|
EXPERIMENTAL_weakRefs?: boolean;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Customize querying behavior.
|
|
82
|
+
*/
|
|
83
|
+
queries?: QueryConfig;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/**
|
|
@@ -155,6 +160,7 @@ export class ClientDescriptor<
|
|
|
155
160
|
disableRebasing: init.disableRebasing,
|
|
156
161
|
rebaseTimeout: init.rebaseTimeout,
|
|
157
162
|
},
|
|
163
|
+
queries: init.queries,
|
|
158
164
|
},
|
|
159
165
|
persistence:
|
|
160
166
|
init.persistence || new IdbPersistence(environment.indexedDB),
|
package/src/context/context.ts
CHANGED
|
@@ -10,15 +10,15 @@ import {
|
|
|
10
10
|
StorageSchema,
|
|
11
11
|
} from '@verdant-web/common';
|
|
12
12
|
import { UndoHistory } from '../UndoHistory.js';
|
|
13
|
-
import { Time } from './Time.js';
|
|
14
|
-
import type { PersistenceDocuments } from '../persistence/PersistenceQueries.js';
|
|
15
|
-
import type { PersistenceMetadata } from '../persistence/PersistenceMetadata.js';
|
|
16
13
|
import { PersistenceFiles } from '../persistence/PersistenceFiles.js';
|
|
14
|
+
import type { PersistenceMetadata } from '../persistence/PersistenceMetadata.js';
|
|
15
|
+
import type { PersistenceDocuments } from '../persistence/PersistenceQueries.js';
|
|
17
16
|
import {
|
|
18
17
|
PersistedFileData,
|
|
19
18
|
PersistenceImplementation,
|
|
20
19
|
} from '../persistence/interfaces.js';
|
|
21
20
|
import { ShutdownHandler } from './ShutdownHandler.js';
|
|
21
|
+
import { Time } from './Time.js';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Common components utilized across various client
|
|
@@ -57,6 +57,7 @@ export interface Context {
|
|
|
57
57
|
filesDeleted: (files: FileRef[]) => void;
|
|
58
58
|
fileAdded: (file: FileData) => void;
|
|
59
59
|
[ev: `fileUploaded:${string}`]: (file: FileData) => void;
|
|
60
|
+
fileUploaded: (file: FileData) => void;
|
|
60
61
|
}>;
|
|
61
62
|
globalEvents: EventSubscriber<{
|
|
62
63
|
/**
|
|
@@ -99,6 +100,7 @@ export interface Context {
|
|
|
99
100
|
files?: FileConfig;
|
|
100
101
|
sync?: SyncConfig;
|
|
101
102
|
persistence?: PersistenceConfig;
|
|
103
|
+
queries?: QueryConfig;
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
environment: {
|
|
@@ -209,4 +211,17 @@ export interface PersistenceConfig {
|
|
|
209
211
|
rebaseTimeout?: number;
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
export interface QueryConfig {
|
|
215
|
+
/**
|
|
216
|
+
* Milliseconds to hold a query in memory after it is unsubscribed before
|
|
217
|
+
* disposing of it. Once a query is disposed, it must be loaded fresh again
|
|
218
|
+
* on next use. Queries are cached based on their `key`, which you can
|
|
219
|
+
* manually override. By default keys are determined by the parameters
|
|
220
|
+
* passed to the query.
|
|
221
|
+
*
|
|
222
|
+
* Defaults to 5 seconds.
|
|
223
|
+
*/
|
|
224
|
+
evictionTime?: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
212
227
|
export type InitialContext = Omit<Context, 'documents' | 'meta' | 'files'>;
|
package/src/entities/Entity.ts
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
getChildFieldSchema,
|
|
14
14
|
getDefault,
|
|
15
15
|
hasDefault,
|
|
16
|
-
isFileRef,
|
|
17
16
|
isFile,
|
|
17
|
+
isFileRef,
|
|
18
18
|
isNullable,
|
|
19
19
|
isObject,
|
|
20
20
|
isRef,
|
|
@@ -29,6 +29,8 @@ import { processValueFiles } from '../files/utils.js';
|
|
|
29
29
|
import { EntityFile } from '../index.js';
|
|
30
30
|
import { EntityCache } from './EntityCache.js';
|
|
31
31
|
import { EntityFamilyMetadata, EntityMetadataView } from './EntityMetadata.js';
|
|
32
|
+
import { EntityStoreEventData, EntityStoreEvents } from './EntityStore.js';
|
|
33
|
+
import { entityFieldSubscriber } from './entityFieldSubscriber.js';
|
|
32
34
|
import {
|
|
33
35
|
BaseEntityValue,
|
|
34
36
|
DataFromInit,
|
|
@@ -39,8 +41,6 @@ import {
|
|
|
39
41
|
ListItemValue,
|
|
40
42
|
ObjectEntity,
|
|
41
43
|
} from './types.js';
|
|
42
|
-
import { EntityStoreEventData, EntityStoreEvents } from './EntityStore.js';
|
|
43
|
-
import { entityFieldSubscriber } from './entityFieldSubscriber.js';
|
|
44
44
|
|
|
45
45
|
export interface EntityInit {
|
|
46
46
|
oid: ObjectIdentifier;
|
|
@@ -73,7 +73,7 @@ export class Entity<
|
|
|
73
73
|
private entityFamily: EntityCache;
|
|
74
74
|
private metadataFamily;
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
readonly schema;
|
|
77
77
|
private parent: Entity | undefined;
|
|
78
78
|
private ctx;
|
|
79
79
|
private files;
|
|
@@ -657,6 +657,10 @@ export class Entity<
|
|
|
657
657
|
})
|
|
658
658
|
) {
|
|
659
659
|
if (hasDefault(fieldSchema)) {
|
|
660
|
+
// FIXME: this returns []/{} for arrays and objects, but the contract
|
|
661
|
+
// of this method should return an Entity for such object fields.
|
|
662
|
+
// I want to write a test case for this one before attempting to fix
|
|
663
|
+
// just to be sure the fix works.
|
|
660
664
|
return getDefault(fieldSchema);
|
|
661
665
|
}
|
|
662
666
|
if (isNullable(fieldSchema)) {
|
package/src/entities/types.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { ObjectIdentifier, StorageFieldSchema } from '@verdant-web/common';
|
|
2
2
|
import type { Entity } from './Entity.js';
|
|
3
3
|
|
|
4
|
-
export type AccessibleEntityProperty<T> =
|
|
5
|
-
? number
|
|
6
|
-
: T extends object
|
|
7
|
-
? keyof T
|
|
8
|
-
: never;
|
|
4
|
+
export type AccessibleEntityProperty<T> =
|
|
5
|
+
T extends Array<any> ? number : T extends object ? keyof T : never;
|
|
9
6
|
|
|
10
7
|
export type DataFromInit<Init> = Init extends { [key: string]: any }
|
|
11
8
|
? {
|
|
12
9
|
[Key in keyof Init]: Init[Key];
|
|
13
|
-
|
|
10
|
+
}
|
|
14
11
|
: Init extends Array<any>
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
? Init
|
|
13
|
+
: any;
|
|
17
14
|
|
|
18
15
|
// reduces keys of an object to only ones with an optional
|
|
19
16
|
// value
|
|
@@ -23,15 +20,11 @@ export type DeletableKeys<T> = keyof {
|
|
|
23
20
|
type IfNullableThen<T, Out> = undefined extends T
|
|
24
21
|
? Out
|
|
25
22
|
: null extends T
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
? Out
|
|
24
|
+
: never;
|
|
28
25
|
|
|
29
|
-
export type EntityShape<E extends Entity<any, any>> =
|
|
30
|
-
infer Value,
|
|
31
|
-
any
|
|
32
|
-
>
|
|
33
|
-
? Value
|
|
34
|
-
: never;
|
|
26
|
+
export type EntityShape<E extends Entity<any, any>> =
|
|
27
|
+
E extends Entity<infer Value, any> ? Value : never;
|
|
35
28
|
|
|
36
29
|
export type BaseEntityValue = { [Key: string]: any } | any[];
|
|
37
30
|
|
|
@@ -73,21 +66,62 @@ export interface BaseEntity<
|
|
|
73
66
|
) => void,
|
|
74
67
|
): () => void;
|
|
75
68
|
get<Key extends keyof Value>(key: Key): Value[Key];
|
|
69
|
+
/**
|
|
70
|
+
* Returns a plain object or array containing sub-Entities and their data.
|
|
71
|
+
* Equivalent to "destructuring" the entity. Unlike getSnapshot, sub-level
|
|
72
|
+
* data is still reactive.
|
|
73
|
+
*/
|
|
76
74
|
getAll(): Readonly<Value>;
|
|
75
|
+
/**
|
|
76
|
+
* Returns a plain Javascript object representing the current state of the entity.
|
|
77
|
+
*/
|
|
77
78
|
getSnapshot(): Snapshot;
|
|
79
|
+
/**
|
|
80
|
+
* Returns the schema for the entity as specified in your Verdant schema.
|
|
81
|
+
* For root Documents, this will be an Object schema with properties
|
|
82
|
+
* representing each field in the document.
|
|
83
|
+
*/
|
|
84
|
+
readonly schema: StorageFieldSchema;
|
|
85
|
+
/**
|
|
86
|
+
* Returns the schema for a field in the entity as specified in
|
|
87
|
+
* your Verdant schema.
|
|
88
|
+
*/
|
|
78
89
|
getFieldSchema<FieldName extends keyof Value>(
|
|
79
90
|
key: FieldName,
|
|
80
91
|
): StorageFieldSchema;
|
|
92
|
+
/**
|
|
93
|
+
* Will be marked true after an entity has been deleted. Any attempt to
|
|
94
|
+
* access the entity's data will result in an error.
|
|
95
|
+
*/
|
|
81
96
|
readonly deleted: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* A Unix Epoch timestamp representing the last time this entity was updated.
|
|
99
|
+
*/
|
|
82
100
|
readonly updatedAt: number;
|
|
101
|
+
/**
|
|
102
|
+
* A Unix Epoch timestamp representing the last time this entity or any of its
|
|
103
|
+
* sub-entities were updated.
|
|
104
|
+
*
|
|
105
|
+
* NOTE: reading this property requires a bit of computation, but the result
|
|
106
|
+
* is cached. If an entity is being frequently updated and this is frequently
|
|
107
|
+
* read, it may result in mild performance degradation.
|
|
108
|
+
*/
|
|
109
|
+
readonly deepUpdatedAt: number;
|
|
110
|
+
/** A unique, opaque key for this Entity in the system. */
|
|
83
111
|
readonly uid: string;
|
|
112
|
+
/** If true, this Entity has authorization rules applied to it. */
|
|
84
113
|
readonly isAuthorized: boolean;
|
|
114
|
+
/** The authorization configuration string applied to this entity. */
|
|
115
|
+
readonly access: string | undefined;
|
|
116
|
+
readonly invalid: boolean;
|
|
117
|
+
/** The Verdant store namespace which contains this object */
|
|
118
|
+
readonly namespace: string;
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
export type DeepPartial<T> = T extends object
|
|
88
122
|
? {
|
|
89
123
|
[P in keyof T]?: DeepPartial<T[P]>;
|
|
90
|
-
|
|
124
|
+
}
|
|
91
125
|
: T;
|
|
92
126
|
|
|
93
127
|
export interface ObjectEntity<
|
|
@@ -200,22 +234,21 @@ export type AnyEntity<
|
|
|
200
234
|
| ListEntity<Init, KeyValue, Snapshot>
|
|
201
235
|
| ObjectEntity<Init, KeyValue, Snapshot>;
|
|
202
236
|
|
|
203
|
-
export type ListItemValue<KeyValue> =
|
|
204
|
-
? T
|
|
205
|
-
: never;
|
|
237
|
+
export type ListItemValue<KeyValue> =
|
|
238
|
+
KeyValue extends Array<infer T> ? T : never;
|
|
206
239
|
export type ListItemInit<Init> = Init extends Array<infer T> ? T : never;
|
|
207
240
|
|
|
208
241
|
export type EntityDestructured<T extends AnyEntity<any, any, any> | null> =
|
|
209
242
|
| (T extends ListEntity<any, infer KeyValue, any>
|
|
210
243
|
? KeyValue
|
|
211
244
|
: T extends ObjectEntity<any, infer KeyValue, any>
|
|
212
|
-
|
|
213
|
-
|
|
245
|
+
? KeyValue
|
|
246
|
+
: never)
|
|
214
247
|
| (T extends null ? null : never);
|
|
215
248
|
|
|
216
249
|
export type EntityInit<T extends AnyEntity<any, any, any>> =
|
|
217
250
|
T extends ListEntity<infer Init, any, any>
|
|
218
251
|
? Init
|
|
219
252
|
: T extends ObjectEntity<infer Init, any, any>
|
|
220
|
-
|
|
221
|
-
|
|
253
|
+
? Init
|
|
254
|
+
: never;
|
package/src/files/FileManager.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { FileData } from '@verdant-web/common';
|
|
2
2
|
import { Context } from '../context/context.js';
|
|
3
|
+
import { Disposable } from '../internal.js';
|
|
3
4
|
import { Sync } from '../sync/Sync.js';
|
|
4
5
|
import { EntityFile, MARK_FAILED, UPDATE } from './EntityFile.js';
|
|
5
6
|
|
|
6
|
-
export class FileManager {
|
|
7
|
+
export class FileManager extends Disposable {
|
|
7
8
|
private sync;
|
|
8
9
|
private context;
|
|
9
10
|
|
|
10
11
|
private cache = new Map<string, EntityFile>();
|
|
11
12
|
|
|
12
13
|
constructor({ sync, context }: { sync: Sync; context: Context }) {
|
|
14
|
+
super();
|
|
13
15
|
this.sync = sync;
|
|
14
16
|
this.context = context;
|
|
17
|
+
this.addDispose(
|
|
18
|
+
this.context.internalEvents.subscribe(
|
|
19
|
+
'fileUploaded',
|
|
20
|
+
this.onFileUploaded,
|
|
21
|
+
),
|
|
22
|
+
);
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
add = async (file: FileData) => {
|
|
@@ -72,8 +80,8 @@ export class FileManager {
|
|
|
72
80
|
|
|
73
81
|
const result = await this.sync.getFile(file.id);
|
|
74
82
|
if (result.success) {
|
|
75
|
-
file[UPDATE](result.data);
|
|
76
83
|
await this.context.files.add(result.data);
|
|
84
|
+
file[UPDATE](result.data);
|
|
77
85
|
} else {
|
|
78
86
|
this.context.log('error', 'Failed to load file', result);
|
|
79
87
|
file[MARK_FAILED]();
|
|
@@ -84,4 +92,9 @@ export class FileManager {
|
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
};
|
|
95
|
+
|
|
96
|
+
private onFileUploaded = (data: FileData) => {
|
|
97
|
+
this.context.log('debug', 'Marking file as uploaded', data.id);
|
|
98
|
+
this.context.files.onUploaded(data.id);
|
|
99
|
+
};
|
|
87
100
|
}
|
|
@@ -65,10 +65,10 @@ export class PersistenceFiles {
|
|
|
65
65
|
// and must be uploaded, even if it is cloned from an uploaded file.
|
|
66
66
|
file.remote = false;
|
|
67
67
|
|
|
68
|
-
// fire event for processing immediately
|
|
69
|
-
this.context.internalEvents.emit('fileAdded', file);
|
|
70
68
|
// store in persistence db
|
|
71
69
|
await this.db.add(file);
|
|
70
|
+
// fire event for sync to pick up and upload the file
|
|
71
|
+
this.context.internalEvents.emit('fileAdded', file);
|
|
72
72
|
this.context.globalEvents.emit('fileSaved', file);
|
|
73
73
|
this.context.log(
|
|
74
74
|
'debug',
|
|
@@ -79,10 +79,10 @@ export class PersistenceFiles {
|
|
|
79
79
|
file.file
|
|
80
80
|
? 'with binary file'
|
|
81
81
|
: file.url
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
? 'with url'
|
|
83
|
+
: file.localPath
|
|
84
|
+
? 'with local path'
|
|
85
|
+
: 'with no data',
|
|
86
86
|
);
|
|
87
87
|
return file;
|
|
88
88
|
};
|
|
@@ -206,6 +206,11 @@ export class PersistenceFiles {
|
|
|
206
206
|
}, 1000);
|
|
207
207
|
});
|
|
208
208
|
} else {
|
|
209
|
+
this.context.log(
|
|
210
|
+
'error',
|
|
211
|
+
`Failed to download file after ${maxRetries} retries`,
|
|
212
|
+
err,
|
|
213
|
+
);
|
|
209
214
|
throw new Error(`Failed to download file after ${maxRetries} retries`, {
|
|
210
215
|
cause: err,
|
|
211
216
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FileData } from '@verdant-web/common';
|
|
2
|
+
import { Context } from '../../../internal.js';
|
|
2
3
|
import {
|
|
3
4
|
AbstractTransaction,
|
|
4
5
|
PersistedFileData,
|
|
@@ -6,7 +7,6 @@ import {
|
|
|
6
7
|
} from '../../interfaces.js';
|
|
7
8
|
import { IdbService } from '../IdbService.js';
|
|
8
9
|
import { getAllFromObjectStores, getSizeOfObjectStore } from '../util.js';
|
|
9
|
-
import { Context } from '../../../internal.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* When stored in IDB, replace the file blob with an array buffer
|
|
@@ -24,9 +24,7 @@ export class IdbPersistenceFileDb
|
|
|
24
24
|
extends IdbService
|
|
25
25
|
implements PersistenceFileDb
|
|
26
26
|
{
|
|
27
|
-
add = async (
|
|
28
|
-
file: FileData,
|
|
29
|
-
): Promise<void> => {
|
|
27
|
+
add = async (file: FileData): Promise<void> => {
|
|
30
28
|
let buffer = file.file ? await fileToArrayBuffer(file.file) : undefined;
|
|
31
29
|
|
|
32
30
|
await this.run(
|
|
@@ -50,6 +48,10 @@ export class IdbPersistenceFileDb
|
|
|
50
48
|
);
|
|
51
49
|
};
|
|
52
50
|
markUploaded = async (id: string): Promise<void> => {
|
|
51
|
+
if (this.disposed) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
53
55
|
const current = await this.getFileRaw(id);
|
|
54
56
|
|
|
55
57
|
if (!current) {
|
|
@@ -185,19 +187,21 @@ export class IdbPersistenceFileDb
|
|
|
185
187
|
if (file.url) {
|
|
186
188
|
const response = await ctx.environment.fetch(file.url);
|
|
187
189
|
if (!response.ok) {
|
|
188
|
-
throw new Error(
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Failed to download file ${file.url}: ${response.statusText}`,
|
|
192
|
+
);
|
|
189
193
|
}
|
|
190
194
|
return response.blob();
|
|
191
195
|
}
|
|
192
196
|
throw new Error('File is missing url, file, and localPath');
|
|
193
|
-
}
|
|
197
|
+
};
|
|
194
198
|
|
|
195
199
|
private hydrateFileData = (raw: StoredFileData): PersistedFileData => {
|
|
196
200
|
(raw as any).remote = raw.remote === 'true';
|
|
197
201
|
const buffer = raw.buffer;
|
|
198
202
|
delete raw.buffer;
|
|
199
203
|
(raw as unknown as FileData).file = buffer
|
|
200
|
-
? arrayBufferToBlob(buffer, raw.type)
|
|
204
|
+
? arrayBufferToBlob(buffer, raw.type, raw.name)
|
|
201
205
|
: undefined;
|
|
202
206
|
return raw as unknown as PersistedFileData;
|
|
203
207
|
};
|
|
@@ -206,6 +210,9 @@ export class IdbPersistenceFileDb
|
|
|
206
210
|
id: string,
|
|
207
211
|
{ transaction }: { transaction?: AbstractTransaction } = {},
|
|
208
212
|
): Promise<StoredFileData | undefined> => {
|
|
213
|
+
if (this.disposed) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
209
216
|
const raw = await this.run<StoredFileData>(
|
|
210
217
|
'files',
|
|
211
218
|
(store) => {
|
|
@@ -220,8 +227,14 @@ export class IdbPersistenceFileDb
|
|
|
220
227
|
};
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
export function arrayBufferToBlob(
|
|
224
|
-
|
|
230
|
+
export function arrayBufferToBlob(
|
|
231
|
+
buffer: ArrayBuffer,
|
|
232
|
+
type: string,
|
|
233
|
+
name?: string,
|
|
234
|
+
) {
|
|
235
|
+
return new File([new Blob([buffer], { type })], name ?? 'blob', {
|
|
236
|
+
type,
|
|
237
|
+
});
|
|
225
238
|
}
|
|
226
239
|
|
|
227
240
|
function fileToArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { EventSubscriber, getOidRoot, VerdantError } from '@verdant-web/common';
|
|
2
2
|
import { Context, InitialContext } from '../context/context.js';
|
|
3
|
+
import { ShutdownHandler } from '../context/ShutdownHandler.js';
|
|
3
4
|
import { getWipNamespace } from '../utils/wip.js';
|
|
4
5
|
import { ExportedData } from './interfaces.js';
|
|
6
|
+
import { migrate } from './migration/migrate.js';
|
|
5
7
|
import { PersistenceFiles } from './PersistenceFiles.js';
|
|
6
8
|
import { PersistenceMetadata } from './PersistenceMetadata.js';
|
|
7
9
|
import { PersistenceDocuments } from './PersistenceQueries.js';
|
|
8
|
-
import { migrate } from './migration/migrate.js';
|
|
9
|
-
import { ShutdownHandler } from '../context/ShutdownHandler.js';
|
|
10
10
|
|
|
11
11
|
export async function initializePersistence(
|
|
12
12
|
ctx: InitialContext,
|
|
@@ -130,6 +130,8 @@ export async function importPersistence(
|
|
|
130
130
|
originalNamespace: importedNamespace,
|
|
131
131
|
// no-op entity events -- don't need to inform queries of changes.
|
|
132
132
|
entityEvents: new EventSubscriber(),
|
|
133
|
+
internalEvents: new EventSubscriber(),
|
|
134
|
+
globalEvents: new EventSubscriber(),
|
|
133
135
|
config: {
|
|
134
136
|
...ctx.config,
|
|
135
137
|
persistence: {
|
package/src/queries/BaseQuery.ts
CHANGED
|
@@ -57,7 +57,9 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
57
57
|
this.isListQuery = Array.isArray(initial);
|
|
58
58
|
this._events = new EventSubscriber<BaseQueryEvents>(
|
|
59
59
|
(event: keyof BaseQueryEvents) => {
|
|
60
|
-
if (event === 'change')
|
|
60
|
+
if (event === 'change') {
|
|
61
|
+
this._allUnsubscribedHandler?.(this);
|
|
62
|
+
}
|
|
61
63
|
},
|
|
62
64
|
);
|
|
63
65
|
this.context = context;
|
|
@@ -6,6 +6,8 @@ export class QueryCache extends Disposable {
|
|
|
6
6
|
private _cache: Map<string, BaseQuery<any>> = new Map();
|
|
7
7
|
private _evictionTime;
|
|
8
8
|
private context;
|
|
9
|
+
/** A set of query keys to keep alive even if they unsubscribe */
|
|
10
|
+
private _holds = new Set<string>();
|
|
9
11
|
|
|
10
12
|
constructor({
|
|
11
13
|
evictionTime = 5 * 1000,
|
|
@@ -26,13 +28,22 @@ export class QueryCache extends Disposable {
|
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
get activeKeys() {
|
|
32
|
+
return Array.from(this._cache.keys());
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
get<T extends BaseQuery<any>>(key: string): T | null {
|
|
30
36
|
return (this._cache.get(key) as T) || null;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
set<V extends BaseQuery<any>>(value: V) {
|
|
34
40
|
this._cache.set(value.key, value);
|
|
35
|
-
value[ON_ALL_UNSUBSCRIBED](this.
|
|
41
|
+
value[ON_ALL_UNSUBSCRIBED](this.enqueueQueryEviction);
|
|
42
|
+
// immediately enqueue a check to see if this query should be evicted --
|
|
43
|
+
// this basically gives code X seconds to subscribe to the query before
|
|
44
|
+
// it gets evicted.
|
|
45
|
+
this.enqueueQueryEviction(value);
|
|
46
|
+
|
|
36
47
|
return value;
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -50,13 +61,24 @@ export class QueryCache extends Disposable {
|
|
|
50
61
|
return this.set(create());
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
private
|
|
64
|
+
private enqueueQueryEviction = (query: BaseQuery<any>) => {
|
|
54
65
|
setTimeout(() => {
|
|
55
66
|
if (query.subscribed) return;
|
|
67
|
+
|
|
68
|
+
if (this._holds.has(query.key)) {
|
|
69
|
+
this.context.log(
|
|
70
|
+
'debug',
|
|
71
|
+
'QueryCache: keepAlive hold on query preserves after unsubscribe',
|
|
72
|
+
query.key,
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
// double check before evicting... possible the cache
|
|
57
78
|
// got a different version of this query.
|
|
58
79
|
if (this._cache.get(query.key) === query) {
|
|
59
80
|
this._cache.delete(query.key);
|
|
81
|
+
this.context.log('debug', 'QueryCache: evicted query', query.key);
|
|
60
82
|
}
|
|
61
83
|
}, this._evictionTime);
|
|
62
84
|
};
|
|
@@ -81,4 +103,36 @@ export class QueryCache extends Disposable {
|
|
|
81
103
|
);
|
|
82
104
|
this._cache.forEach((q) => q.execute());
|
|
83
105
|
};
|
|
106
|
+
|
|
107
|
+
keepAlive(key: string) {
|
|
108
|
+
this._holds.add(key);
|
|
109
|
+
this.context.log('debug', 'QueryCache: keepAlive', key);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
dropKeepAlive(key: string) {
|
|
113
|
+
this._holds.delete(key);
|
|
114
|
+
const cached = this.get(key);
|
|
115
|
+
if (!cached) return;
|
|
116
|
+
if (!cached.subscribed) {
|
|
117
|
+
this.context.log(
|
|
118
|
+
'debug',
|
|
119
|
+
'QueryCache: dropKeepAlive on unsubscribed query; queuing eviction',
|
|
120
|
+
key,
|
|
121
|
+
);
|
|
122
|
+
this.enqueueQueryEviction(cached);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get keepAlives() {
|
|
127
|
+
return this._holds;
|
|
128
|
+
}
|
|
84
129
|
}
|
|
130
|
+
|
|
131
|
+
export type PublicQueryCacheAPI = Pick<
|
|
132
|
+
QueryCache,
|
|
133
|
+
| 'keepAlive'
|
|
134
|
+
| 'dropKeepAlive'
|
|
135
|
+
| 'keepAlives'
|
|
136
|
+
| 'forceRefreshAll'
|
|
137
|
+
| 'activeKeys'
|
|
138
|
+
>;
|
package/src/sync/FileSync.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileData } from '@verdant-web/common';
|
|
2
|
-
import { ServerSyncEndpointProvider } from './ServerSyncEndpointProvider.js';
|
|
3
2
|
import { Context } from '../context/context.js';
|
|
4
3
|
import { Disposable } from '../utils/Disposable.js';
|
|
4
|
+
import { ServerSyncEndpointProvider } from './ServerSyncEndpointProvider.js';
|
|
5
5
|
|
|
6
6
|
export interface FileUploadResult {
|
|
7
7
|
success: boolean;
|
|
@@ -42,7 +42,6 @@ export class FileSync extends Disposable {
|
|
|
42
42
|
this.ctx.log('debug', 'Uploading file', data.id, data.name);
|
|
43
43
|
try {
|
|
44
44
|
await this.uploadFile(data);
|
|
45
|
-
this.ctx.internalEvents.emit(`fileUploaded:${data.id}`, data);
|
|
46
45
|
} catch (e) {
|
|
47
46
|
this.ctx.log('error', 'File upload failed', e);
|
|
48
47
|
}
|
|
@@ -83,6 +82,8 @@ export class FileSync extends Disposable {
|
|
|
83
82
|
);
|
|
84
83
|
|
|
85
84
|
if (response.ok) {
|
|
85
|
+
this.ctx.internalEvents.emit(`fileUploaded:${data.id}`, data);
|
|
86
|
+
this.ctx.internalEvents.emit('fileUploaded', data);
|
|
86
87
|
this.ctx.log('info', 'File upload successful');
|
|
87
88
|
return {
|
|
88
89
|
success: true,
|