@verdant-web/store 4.5.1 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle/index.js +6 -6
- package/dist/bundle/index.js.map +3 -3
- package/dist/esm/BackoffScheduler.d.ts +1 -1
- package/dist/esm/BackoffScheduler.js +3 -2
- package/dist/esm/BackoffScheduler.js.map +1 -1
- package/dist/esm/client/Client.d.ts +1 -0
- package/dist/esm/client/Client.js +5 -1
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.d.ts +1 -1
- package/dist/esm/client/ClientDescriptor.js +4 -2
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/context/context.d.ts +2 -0
- package/dist/esm/files/EntityFile.d.ts +3 -1
- package/dist/esm/files/EntityFile.js +5 -1
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.js +3 -2
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/persistence/MessageCreator.js +0 -3
- package/dist/esm/persistence/MessageCreator.js.map +1 -1
- package/dist/esm/persistence/PersistenceFiles.js +1 -0
- package/dist/esm/persistence/PersistenceFiles.js.map +1 -1
- package/dist/esm/persistence/idb/IdbService.d.ts +3 -4
- package/dist/esm/persistence/idb/IdbService.js +13 -4
- package/dist/esm/persistence/idb/IdbService.js.map +1 -1
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js +2 -1
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -1
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +2 -3
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +0 -1
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
- package/dist/esm/persistence/idb/queries/IdbDocumentDb.d.ts +2 -3
- package/dist/esm/persistence/idb/queries/IdbDocumentDb.js +5 -3
- package/dist/esm/persistence/idb/queries/IdbDocumentDb.js.map +1 -1
- package/dist/esm/persistence/idb/util.d.ts +2 -1
- package/dist/esm/persistence/idb/util.js +81 -23
- package/dist/esm/persistence/idb/util.js.map +1 -1
- package/dist/esm/queries/BaseQuery.js +7 -1
- package/dist/esm/queries/BaseQuery.js.map +1 -1
- package/dist/esm/sync/FileSync.js +2 -2
- package/dist/esm/sync/FileSync.js.map +1 -1
- package/dist/esm/sync/PushPullSync.d.ts +1 -1
- package/dist/esm/sync/PushPullSync.js +4 -1
- package/dist/esm/sync/PushPullSync.js.map +1 -1
- package/dist/esm/sync/WebSocketSync.d.ts +1 -1
- package/dist/esm/sync/WebSocketSync.js +14 -6
- package/dist/esm/sync/WebSocketSync.js.map +1 -1
- package/package.json +2 -2
- package/src/BackoffScheduler.ts +3 -2
- package/src/client/Client.ts +6 -1
- package/src/client/ClientDescriptor.ts +16 -5
- package/src/context/context.ts +2 -0
- package/src/files/EntityFile.ts +6 -1
- package/src/files/FileManager.ts +2 -2
- package/src/persistence/MessageCreator.ts +0 -3
- package/src/persistence/PersistenceFiles.ts +1 -0
- package/src/persistence/idb/IdbService.ts +13 -4
- package/src/persistence/idb/files/IdbPersistenceFileDb.ts +2 -1
- package/src/persistence/idb/metadata/IdbMetadataDb.ts +6 -9
- package/src/persistence/idb/queries/IdbDocumentDb.ts +12 -6
- package/src/persistence/idb/util.ts +83 -22
- package/src/queries/BaseQuery.ts +18 -1
- package/src/sync/FileSync.ts +7 -1
- package/src/sync/PushPullSync.ts +5 -8
- package/src/sync/WebSocketSync.ts +13 -7
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
StorageSchema,
|
|
7
7
|
VerdantError,
|
|
8
8
|
} from '@verdant-web/common';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Context,
|
|
11
|
+
FileConfig,
|
|
12
|
+
InitialContext,
|
|
13
|
+
QueryConfig,
|
|
14
|
+
} from '../context/context.js';
|
|
10
15
|
import { ShutdownHandler } from '../context/ShutdownHandler.js';
|
|
11
16
|
import { Time } from '../context/Time.js';
|
|
12
17
|
import { FakeWeakRef } from '../FakeWeakRef.js';
|
|
@@ -64,7 +69,7 @@ export interface ClientDescriptorOptions<Presence = any, Profile = any> {
|
|
|
64
69
|
* Normally these are provided by the browser, but in other
|
|
65
70
|
* runtimes you may need to provide your own.
|
|
66
71
|
*/
|
|
67
|
-
environment?: InitialContext['environment']
|
|
72
|
+
environment?: Partial<InitialContext['environment']>;
|
|
68
73
|
|
|
69
74
|
/**
|
|
70
75
|
* Enables experimental WeakRef usage to cull documents
|
|
@@ -132,9 +137,12 @@ export class ClientDescriptor<
|
|
|
132
137
|
new HybridLogicalClockTimestampProvider(),
|
|
133
138
|
init.schema.version,
|
|
134
139
|
);
|
|
135
|
-
const environment = init.environment || defaultBrowserEnvironment;
|
|
136
140
|
const logger =
|
|
137
141
|
init.log === false ? noLogger : init.log || debugLogger('🌿');
|
|
142
|
+
const environment = {
|
|
143
|
+
...defaultBrowserEnvironment,
|
|
144
|
+
...init.environment,
|
|
145
|
+
};
|
|
138
146
|
let ctx: InitialContext = {
|
|
139
147
|
closing: false,
|
|
140
148
|
entityEvents: new EventSubscriber(),
|
|
@@ -224,12 +232,15 @@ export class ClientDescriptor<
|
|
|
224
232
|
};
|
|
225
233
|
|
|
226
234
|
__dangerous__resetLocal = async () => {
|
|
227
|
-
await deleteAllDatabases(this.namespace);
|
|
235
|
+
await deleteAllDatabases(this.namespace, defaultBrowserEnvironment);
|
|
228
236
|
};
|
|
229
237
|
}
|
|
230
238
|
|
|
231
|
-
const defaultBrowserEnvironment = {
|
|
239
|
+
const defaultBrowserEnvironment: Context['environment'] = {
|
|
232
240
|
WebSocket: typeof WebSocket !== 'undefined' ? WebSocket : (undefined as any),
|
|
233
241
|
fetch: typeof window !== 'undefined' ? window.fetch.bind(window) : fetch!,
|
|
234
242
|
indexedDB: typeof indexedDB !== 'undefined' ? indexedDB : (undefined as any),
|
|
243
|
+
location:
|
|
244
|
+
typeof window !== 'undefined' ? window.location : (undefined as any),
|
|
245
|
+
history: typeof window !== 'undefined' ? window.history : (undefined as any),
|
|
235
246
|
};
|
package/src/context/context.ts
CHANGED
package/src/files/EntityFile.ts
CHANGED
|
@@ -27,6 +27,7 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
27
27
|
private _fileData: FileData | null = null;
|
|
28
28
|
private _loading = true;
|
|
29
29
|
private _failed = false;
|
|
30
|
+
private _failedReason: string | undefined;
|
|
30
31
|
private _downloadRemote = false;
|
|
31
32
|
private _uploaded = false;
|
|
32
33
|
private ctx: Context;
|
|
@@ -64,6 +65,9 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
64
65
|
get isUploaded() {
|
|
65
66
|
return this._uploaded || this._fileData?.remote || false;
|
|
66
67
|
}
|
|
68
|
+
get error() {
|
|
69
|
+
return this._failedReason || null;
|
|
70
|
+
}
|
|
67
71
|
|
|
68
72
|
private emitChange() {
|
|
69
73
|
this.parent[CHILD_FILE_CHANGED](this);
|
|
@@ -85,8 +89,9 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
85
89
|
this.emitChange();
|
|
86
90
|
};
|
|
87
91
|
|
|
88
|
-
[MARK_FAILED] = () => {
|
|
92
|
+
[MARK_FAILED] = (reason?: string) => {
|
|
89
93
|
this._failed = true;
|
|
94
|
+
this._failedReason = reason;
|
|
90
95
|
this._loading = false;
|
|
91
96
|
this.emitChange();
|
|
92
97
|
};
|
package/src/files/FileManager.ts
CHANGED
|
@@ -88,11 +88,11 @@ export class FileManager extends Disposable {
|
|
|
88
88
|
file[UPDATE](result.data);
|
|
89
89
|
} else {
|
|
90
90
|
this.context.log('error', 'Failed to load file', result);
|
|
91
|
-
file[MARK_FAILED]();
|
|
91
|
+
file[MARK_FAILED](result.error?.toString());
|
|
92
92
|
}
|
|
93
93
|
} catch (err) {
|
|
94
94
|
this.context.log('error', 'Failed to load file', err);
|
|
95
|
-
file[MARK_FAILED]();
|
|
95
|
+
file[MARK_FAILED](err instanceof Error ? err.message : String(err));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
};
|
|
@@ -141,11 +141,8 @@ export class MessageCreator {
|
|
|
141
141
|
};
|
|
142
142
|
|
|
143
143
|
createHeartbeat = async (): Promise<HeartbeatMessage> => {
|
|
144
|
-
const localReplicaInfo = await this.meta.getLocalReplica();
|
|
145
144
|
return {
|
|
146
145
|
type: 'heartbeat',
|
|
147
|
-
timestamp: this.ctx.time.now,
|
|
148
|
-
replicaId: localReplicaInfo.id,
|
|
149
146
|
};
|
|
150
147
|
};
|
|
151
148
|
|
|
@@ -66,6 +66,7 @@ export class PersistenceFiles {
|
|
|
66
66
|
file.remote = false;
|
|
67
67
|
|
|
68
68
|
// store in persistence db
|
|
69
|
+
this.context.log('debug', 'Adding file to persistence', file);
|
|
69
70
|
await this.db.add(file);
|
|
70
71
|
// fire event for sync to pick up and upload the file
|
|
71
72
|
this.context.internalEvents.emit('fileAdded', file);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Context } from '../../context/context.js';
|
|
1
|
+
import { Context, InitialContext } from '../../context/context.js';
|
|
2
2
|
import { Disposable } from '../../utils/Disposable.js';
|
|
3
3
|
import {
|
|
4
4
|
createAbortableTransaction,
|
|
5
5
|
isAbortError,
|
|
6
|
+
isTransactionAborted,
|
|
6
7
|
storeRequestPromise,
|
|
7
8
|
} from './util.js';
|
|
8
9
|
|
|
@@ -12,10 +13,9 @@ export class IdbService extends Disposable {
|
|
|
12
13
|
|
|
13
14
|
constructor(
|
|
14
15
|
protected db: IDBDatabase,
|
|
15
|
-
|
|
16
|
+
protected readonly ctx: InitialContext,
|
|
16
17
|
) {
|
|
17
18
|
super();
|
|
18
|
-
this.log = log;
|
|
19
19
|
const abortController = new AbortController();
|
|
20
20
|
const abort = abortController.abort.bind(abortController);
|
|
21
21
|
this.globalAbortController = abortController;
|
|
@@ -82,6 +82,9 @@ export class IdbService extends Disposable {
|
|
|
82
82
|
if (this.disposed || opts?.transaction?.error)
|
|
83
83
|
return Promise.resolve(undefined as any);
|
|
84
84
|
const tx = opts?.transaction || this.createTransaction([storeName], opts);
|
|
85
|
+
if (isTransactionAborted(tx)) {
|
|
86
|
+
return Promise.resolve(undefined as any);
|
|
87
|
+
}
|
|
85
88
|
const store = tx.objectStore(storeName);
|
|
86
89
|
const request = getRequest(store);
|
|
87
90
|
return storeRequestPromise<T>(request);
|
|
@@ -97,6 +100,9 @@ export class IdbService extends Disposable {
|
|
|
97
100
|
},
|
|
98
101
|
): Promise<T[]> => {
|
|
99
102
|
if (this.disposed || opts?.transaction?.error) return Promise.resolve([]);
|
|
103
|
+
if (opts?.transaction && isTransactionAborted(opts.transaction)) {
|
|
104
|
+
return Promise.resolve([]);
|
|
105
|
+
}
|
|
100
106
|
const tx = opts?.transaction || this.createTransaction([storeName], opts);
|
|
101
107
|
const store = tx.objectStore(storeName);
|
|
102
108
|
const requests = getRequests(store);
|
|
@@ -122,6 +128,9 @@ export class IdbService extends Disposable {
|
|
|
122
128
|
},
|
|
123
129
|
): Promise<void> => {
|
|
124
130
|
const tx = opts?.transaction || this.createTransaction([storeName], opts);
|
|
131
|
+
if (isTransactionAborted(tx)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
125
134
|
const store = tx.objectStore(storeName);
|
|
126
135
|
const request = getRequest(store);
|
|
127
136
|
if (Array.isArray(request)) {
|
|
@@ -191,7 +200,7 @@ export class IdbService extends Disposable {
|
|
|
191
200
|
this.db.close();
|
|
192
201
|
if (typeof window !== 'undefined') {
|
|
193
202
|
try {
|
|
194
|
-
|
|
203
|
+
this.ctx.environment.location.reload();
|
|
195
204
|
} catch (err) {
|
|
196
205
|
this.log?.('error', 'Failed to reload the page', err);
|
|
197
206
|
}
|
|
@@ -55,7 +55,8 @@ export class IdbPersistenceFileDb
|
|
|
55
55
|
const current = await this.getFileRaw(id);
|
|
56
56
|
|
|
57
57
|
if (!current) {
|
|
58
|
-
|
|
58
|
+
this.ctx.log('error', 'Tried to mark unknown file as uploaded', id);
|
|
59
|
+
return;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
await this.run(
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getOidSubIdRange,
|
|
9
9
|
ObjectIdentifier,
|
|
10
10
|
} from '@verdant-web/common';
|
|
11
|
+
import { InitialContext } from '../../../context/context.js';
|
|
11
12
|
import {
|
|
12
13
|
AbstractTransaction,
|
|
13
14
|
AckInfo,
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
} from '../../interfaces.js';
|
|
20
21
|
import { IdbService } from '../IdbService.js';
|
|
21
22
|
import { closeDatabase, getSizeOfObjectStore } from '../util.js';
|
|
22
|
-
import { Context } from '../../../context/context.js';
|
|
23
23
|
|
|
24
24
|
export type StoredClientOperation = ClientOperation & {
|
|
25
25
|
/** This acts as the primary key */
|
|
@@ -37,10 +37,7 @@ export class IdbMetadataDb
|
|
|
37
37
|
extends IdbService
|
|
38
38
|
implements PersistenceMetadataDb<IDBTransaction>
|
|
39
39
|
{
|
|
40
|
-
constructor(
|
|
41
|
-
db: IDBDatabase,
|
|
42
|
-
private ctx: Pick<Context, 'log' | 'namespace'>,
|
|
43
|
-
) {
|
|
40
|
+
constructor(db: IDBDatabase, ctx: InitialContext) {
|
|
44
41
|
super(db, ctx);
|
|
45
42
|
this.addDispose(() => {
|
|
46
43
|
this.ctx.log('info', `Closing metadata DB for`, this.ctx.namespace);
|
|
@@ -330,10 +327,10 @@ export class IdbMetadataDb
|
|
|
330
327
|
start && end
|
|
331
328
|
? IDBKeyRange.bound(start, end, false, true)
|
|
332
329
|
: start
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
330
|
+
? IDBKeyRange.lowerBound(start, false)
|
|
331
|
+
: end
|
|
332
|
+
? IDBKeyRange.upperBound(end, true)
|
|
333
|
+
: undefined;
|
|
337
334
|
return store.index('timestamp').openCursor(range, 'next');
|
|
338
335
|
},
|
|
339
336
|
iterator,
|
|
@@ -5,17 +5,20 @@ import {
|
|
|
5
5
|
getIndexValues,
|
|
6
6
|
ObjectIdentifier,
|
|
7
7
|
} from '@verdant-web/common';
|
|
8
|
-
import {
|
|
8
|
+
import { InitialContext } from '../../../context/context.js';
|
|
9
9
|
import { PersistenceDocumentDb } from '../../interfaces.js';
|
|
10
10
|
import { IdbService } from '../IdbService.js';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
closeDatabase,
|
|
13
|
+
getSizeOfObjectStore,
|
|
14
|
+
isAbortError,
|
|
15
|
+
isTransactionAborted,
|
|
16
|
+
} from '../util.js';
|
|
12
17
|
import { getRange } from './ranges.js';
|
|
13
18
|
|
|
14
19
|
export class IdbDocumentDb extends IdbService implements PersistenceDocumentDb {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
super(db, { log: context.log });
|
|
18
|
-
this.ctx = context;
|
|
20
|
+
constructor(db: IDBDatabase, context: InitialContext) {
|
|
21
|
+
super(db, context);
|
|
19
22
|
this.addDispose(() => {
|
|
20
23
|
this.ctx.log('info', 'Closing document database for', this.ctx.namespace);
|
|
21
24
|
return closeDatabase(this.db);
|
|
@@ -73,6 +76,9 @@ export class IdbDocumentDb extends IdbService implements PersistenceDocumentDb {
|
|
|
73
76
|
offset?: number;
|
|
74
77
|
}): Promise<{ result: ObjectIdentifier[]; hasNextPage: boolean }> => {
|
|
75
78
|
const tx = this.createTransaction([collection], { mode: 'readonly' });
|
|
79
|
+
if (isTransactionAborted(tx)) {
|
|
80
|
+
return { result: [], hasNextPage: false };
|
|
81
|
+
}
|
|
76
82
|
const store = tx.objectStore(collection);
|
|
77
83
|
const source = index?.where ? store.index(index.where) : store;
|
|
78
84
|
const direction = index?.order === 'desc' ? 'prev' : 'next';
|
|
@@ -130,10 +130,14 @@ export async function closeDatabase(db: IDBDatabase) {
|
|
|
130
130
|
|
|
131
131
|
export async function deleteAllDatabases(
|
|
132
132
|
namespace: string,
|
|
133
|
-
|
|
133
|
+
environment: Context['environment'],
|
|
134
134
|
) {
|
|
135
|
-
const req1 = indexedDB.deleteDatabase(
|
|
136
|
-
|
|
135
|
+
const req1 = environment.indexedDB.deleteDatabase(
|
|
136
|
+
[namespace, 'meta'].join('_'),
|
|
137
|
+
);
|
|
138
|
+
const req2 = environment.indexedDB.deleteDatabase(
|
|
139
|
+
[namespace, 'collections'].join('_'),
|
|
140
|
+
);
|
|
137
141
|
await Promise.all([
|
|
138
142
|
new Promise((resolve, reject) => {
|
|
139
143
|
req1.onsuccess = resolve;
|
|
@@ -144,7 +148,8 @@ export async function deleteAllDatabases(
|
|
|
144
148
|
req2.onerror = reject;
|
|
145
149
|
}),
|
|
146
150
|
]);
|
|
147
|
-
|
|
151
|
+
// reload the page to reset any existing connections
|
|
152
|
+
environment.location.reload();
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
export function deleteDatabase(name: string, indexedDB = window.indexedDB) {
|
|
@@ -164,25 +169,81 @@ export function createAbortableTransaction(
|
|
|
164
169
|
abortSignal?: AbortSignal,
|
|
165
170
|
log?: (...args: any[]) => void,
|
|
166
171
|
) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
abortSignal.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
172
|
+
try {
|
|
173
|
+
const tx = db.transaction(storeNames, mode);
|
|
174
|
+
if (abortSignal) {
|
|
175
|
+
const abort = () => {
|
|
176
|
+
log?.('debug', 'aborting transaction');
|
|
177
|
+
try {
|
|
178
|
+
tx.abort();
|
|
179
|
+
(tx as any).__aborted = true;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
log?.('debug', 'aborting transaction failed', e);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
abortSignal.addEventListener('abort', abort);
|
|
185
|
+
tx.addEventListener('error', () => {
|
|
186
|
+
abortSignal.removeEventListener('abort', abort);
|
|
187
|
+
});
|
|
188
|
+
tx.addEventListener('complete', () => {
|
|
189
|
+
abortSignal.removeEventListener('abort', abort);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return tx;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err instanceof Error && err.name === 'InvalidStateError') {
|
|
195
|
+
// database is probably closing. it's ok, what can you do?
|
|
196
|
+
log?.('warn', 'Failed to create transaction, database is closing');
|
|
197
|
+
// mock a Transaction so code can continue,
|
|
198
|
+
// but doesn't do anything.
|
|
199
|
+
return {
|
|
200
|
+
abort: () => {},
|
|
201
|
+
addEventListener: () => {},
|
|
202
|
+
objectStore: () => {
|
|
203
|
+
return {
|
|
204
|
+
add: () => {},
|
|
205
|
+
put: () => {},
|
|
206
|
+
get: () => {},
|
|
207
|
+
getAll: () => {},
|
|
208
|
+
delete: () => {},
|
|
209
|
+
clear: () => {},
|
|
210
|
+
openCursor: () => {
|
|
211
|
+
const req = {
|
|
212
|
+
onsuccess: () => {},
|
|
213
|
+
onerror: (_: any) => {},
|
|
214
|
+
result: null,
|
|
215
|
+
};
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
req.onerror({} as any);
|
|
218
|
+
}, 0);
|
|
219
|
+
return req;
|
|
220
|
+
},
|
|
221
|
+
index: () => {
|
|
222
|
+
throw new Error('Transaction is not active');
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
oncomplete: null,
|
|
227
|
+
onerror: null,
|
|
228
|
+
onabort: null,
|
|
229
|
+
error: new Error('Transaction is not active') as any,
|
|
230
|
+
commit: () => {},
|
|
231
|
+
db,
|
|
232
|
+
dispatchEvent: () => false,
|
|
233
|
+
removeEventListener: () => {},
|
|
234
|
+
durability: 'default',
|
|
235
|
+
mode: 'readonly',
|
|
236
|
+
objectStoreNames: storeNames as any,
|
|
237
|
+
__aborted: true,
|
|
238
|
+
} as unknown as IDBTransaction;
|
|
239
|
+
} else {
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
184
242
|
}
|
|
185
|
-
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function isTransactionAborted(tx: IDBTransaction) {
|
|
246
|
+
return (tx as any).__aborted;
|
|
186
247
|
}
|
|
187
248
|
|
|
188
249
|
/**
|
package/src/queries/BaseQuery.ts
CHANGED
|
@@ -223,7 +223,13 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
223
223
|
};
|
|
224
224
|
|
|
225
225
|
execute = () => {
|
|
226
|
-
|
|
226
|
+
const startTime = new Date();
|
|
227
|
+
this.context.log(
|
|
228
|
+
'debug',
|
|
229
|
+
`[${startTime.toLocaleTimeString()}]`,
|
|
230
|
+
'Executing query',
|
|
231
|
+
this.key,
|
|
232
|
+
);
|
|
227
233
|
|
|
228
234
|
if (this.status === 'initial') {
|
|
229
235
|
this.status = 'initializing';
|
|
@@ -247,6 +253,17 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
247
253
|
} else {
|
|
248
254
|
throw new Error('Unknown error executing query');
|
|
249
255
|
}
|
|
256
|
+
})
|
|
257
|
+
.finally(() => {
|
|
258
|
+
const endTime = new Date();
|
|
259
|
+
const duration = endTime.getTime() - startTime.getTime();
|
|
260
|
+
this.context.log(
|
|
261
|
+
'debug',
|
|
262
|
+
`[${endTime.toLocaleTimeString()}]`,
|
|
263
|
+
'Query executed',
|
|
264
|
+
this.key,
|
|
265
|
+
`Duration: ${duration}ms`,
|
|
266
|
+
);
|
|
250
267
|
});
|
|
251
268
|
return this._executionPromise;
|
|
252
269
|
};
|
package/src/sync/FileSync.ts
CHANGED
|
@@ -158,6 +158,7 @@ export class FileSync extends Disposable {
|
|
|
158
158
|
this.ctx.log(
|
|
159
159
|
'error',
|
|
160
160
|
'File information fetch failed',
|
|
161
|
+
fileEndpoint + `/${id}`,
|
|
161
162
|
response.status,
|
|
162
163
|
await response.text(),
|
|
163
164
|
);
|
|
@@ -178,7 +179,12 @@ export class FileSync extends Disposable {
|
|
|
178
179
|
});
|
|
179
180
|
}
|
|
180
181
|
} catch (e) {
|
|
181
|
-
this.ctx.log(
|
|
182
|
+
this.ctx.log(
|
|
183
|
+
'error',
|
|
184
|
+
'File information fetch failed',
|
|
185
|
+
`${fileEndpoint}/${id}`,
|
|
186
|
+
e,
|
|
187
|
+
);
|
|
182
188
|
if (retries.current >= retries.max) {
|
|
183
189
|
return {
|
|
184
190
|
success: false,
|
package/src/sync/PushPullSync.ts
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
isVerdantErrorResponse,
|
|
8
8
|
throttle,
|
|
9
9
|
} from '@verdant-web/common';
|
|
10
|
-
import {
|
|
10
|
+
import { Context } from '../context/context.js';
|
|
11
11
|
import { Heartbeat } from './Heartbeat.js';
|
|
12
|
+
import { PresenceManager } from './PresenceManager.js';
|
|
12
13
|
import { ServerSyncEndpointProvider } from './ServerSyncEndpointProvider.js';
|
|
13
14
|
import { SyncTransport, SyncTransportEvents } from './Sync.js';
|
|
14
|
-
import { Context } from '../context/context.js';
|
|
15
15
|
|
|
16
16
|
export class PushPullSync
|
|
17
17
|
extends EventSubscriber<SyncTransportEvents>
|
|
@@ -98,12 +98,7 @@ export class PushPullSync
|
|
|
98
98
|
}
|
|
99
99
|
await handlePromise;
|
|
100
100
|
} else {
|
|
101
|
-
this.ctx.log(
|
|
102
|
-
'error',
|
|
103
|
-
'Sync request failed',
|
|
104
|
-
response.status,
|
|
105
|
-
await response.text(),
|
|
106
|
-
);
|
|
101
|
+
this.ctx.log('error', 'Sync request failed', host, response.status);
|
|
107
102
|
|
|
108
103
|
if (this._isConnected) {
|
|
109
104
|
this._isConnected = false;
|
|
@@ -116,6 +111,8 @@ export class PushPullSync
|
|
|
116
111
|
if (json.code === VerdantErrorCode.TokenExpired) {
|
|
117
112
|
this.endpointProvider.clearCache();
|
|
118
113
|
this.heartbeat.keepAlive();
|
|
114
|
+
} else {
|
|
115
|
+
this.ctx.log('error', 'Server error', json);
|
|
119
116
|
}
|
|
120
117
|
}
|
|
121
118
|
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
ServerMessage,
|
|
5
5
|
} from '@verdant-web/common';
|
|
6
6
|
import { Backoff, BackoffScheduler } from '../BackoffScheduler.js';
|
|
7
|
+
import { Context } from '../context/context.js';
|
|
7
8
|
import { Heartbeat } from './Heartbeat.js';
|
|
8
9
|
import { PresenceManager } from './PresenceManager.js';
|
|
9
10
|
import { ServerSyncEndpointProvider } from './ServerSyncEndpointProvider.js';
|
|
10
11
|
import { SyncTransport, SyncTransportEvents } from './Sync.js';
|
|
11
|
-
import { Context } from '../context/context.js';
|
|
12
12
|
|
|
13
13
|
export class WebSocketSync
|
|
14
14
|
extends EventSubscriber<SyncTransportEvents>
|
|
@@ -34,7 +34,7 @@ export class WebSocketSync
|
|
|
34
34
|
private heartbeat = new Heartbeat();
|
|
35
35
|
|
|
36
36
|
private reconnectScheduler = new BackoffScheduler(
|
|
37
|
-
new Backoff(
|
|
37
|
+
new Backoff(2_000, 60_000, 1.5),
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
constructor({
|
|
@@ -77,7 +77,6 @@ export class WebSocketSync
|
|
|
77
77
|
}
|
|
78
78
|
this.ctx.log('debug', 'Sync connected');
|
|
79
79
|
this.onOnlineChange(true);
|
|
80
|
-
this.reconnectScheduler.reset();
|
|
81
80
|
};
|
|
82
81
|
|
|
83
82
|
private onOnlineChange = async (online: boolean) => {
|
|
@@ -106,6 +105,7 @@ export class WebSocketSync
|
|
|
106
105
|
};
|
|
107
106
|
|
|
108
107
|
private onMessage = async (event: MessageEvent) => {
|
|
108
|
+
this.reconnectScheduler.reset();
|
|
109
109
|
if (this._ignoreIncoming) {
|
|
110
110
|
this.ctx.log(
|
|
111
111
|
'warn',
|
|
@@ -116,6 +116,7 @@ export class WebSocketSync
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
const message = JSON.parse(event.data) as ServerMessage;
|
|
119
|
+
this.ctx.log('debug', 'Received', message.type, 'message');
|
|
119
120
|
switch (message.type) {
|
|
120
121
|
case 'sync-resp':
|
|
121
122
|
if (message.ackThisNonce) {
|
|
@@ -178,16 +179,19 @@ export class WebSocketSync
|
|
|
178
179
|
};
|
|
179
180
|
|
|
180
181
|
private onError = (event: Event) => {
|
|
181
|
-
this.ctx.log('error', event);
|
|
182
|
+
this.ctx.log('error', 'Sync socket error', event);
|
|
183
|
+
if (this.disposed) return;
|
|
182
184
|
this.reconnectScheduler.next();
|
|
183
185
|
|
|
184
186
|
this.ctx.log('info', `Attempting reconnect to websocket sync`);
|
|
185
187
|
};
|
|
186
188
|
|
|
187
189
|
private onClose = (event: CloseEvent) => {
|
|
188
|
-
this.ctx.log('info', 'Sync disconnected');
|
|
190
|
+
this.ctx.log('info', 'Sync socket disconnected');
|
|
189
191
|
this.onOnlineChange(false);
|
|
190
|
-
this.
|
|
192
|
+
if (this.disposed) return;
|
|
193
|
+
this.reconnectScheduler.next();
|
|
194
|
+
this.ctx.log('info', `Attempting reconnect to websocket sync`);
|
|
191
195
|
};
|
|
192
196
|
|
|
193
197
|
private initializeSocket = async () => {
|
|
@@ -282,7 +286,9 @@ export class WebSocketSync
|
|
|
282
286
|
stop = () => {
|
|
283
287
|
this.socket?.removeEventListener('message', this.onMessage);
|
|
284
288
|
this.socket?.removeEventListener('close', this.onClose);
|
|
285
|
-
this.socket?.
|
|
289
|
+
if (this.socket?.readyState === WEBSOCKET_OPEN) {
|
|
290
|
+
this.socket.close();
|
|
291
|
+
}
|
|
286
292
|
this.socket = null;
|
|
287
293
|
this._status = 'paused';
|
|
288
294
|
};
|