@verdant-web/store 4.5.2 → 4.6.1

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 (63) hide show
  1. package/dist/bundle/index.js +6 -6
  2. package/dist/bundle/index.js.map +3 -3
  3. package/dist/esm/BackoffScheduler.d.ts +1 -1
  4. package/dist/esm/BackoffScheduler.js +3 -2
  5. package/dist/esm/BackoffScheduler.js.map +1 -1
  6. package/dist/esm/client/Client.d.ts +1 -0
  7. package/dist/esm/client/Client.js +5 -1
  8. package/dist/esm/client/Client.js.map +1 -1
  9. package/dist/esm/client/ClientDescriptor.d.ts +1 -1
  10. package/dist/esm/client/ClientDescriptor.js +4 -2
  11. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  12. package/dist/esm/context/context.d.ts +2 -0
  13. package/dist/esm/files/EntityFile.d.ts +3 -1
  14. package/dist/esm/files/EntityFile.js +5 -1
  15. package/dist/esm/files/EntityFile.js.map +1 -1
  16. package/dist/esm/files/FileManager.js +3 -2
  17. package/dist/esm/files/FileManager.js.map +1 -1
  18. package/dist/esm/persistence/MessageCreator.js +0 -3
  19. package/dist/esm/persistence/MessageCreator.js.map +1 -1
  20. package/dist/esm/persistence/PersistenceFiles.js +1 -0
  21. package/dist/esm/persistence/PersistenceFiles.js.map +1 -1
  22. package/dist/esm/persistence/idb/IdbService.d.ts +3 -4
  23. package/dist/esm/persistence/idb/IdbService.js +13 -4
  24. package/dist/esm/persistence/idb/IdbService.js.map +1 -1
  25. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js +2 -1
  26. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -1
  27. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +2 -3
  28. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +0 -1
  29. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
  30. package/dist/esm/persistence/idb/queries/IdbDocumentDb.d.ts +2 -3
  31. package/dist/esm/persistence/idb/queries/IdbDocumentDb.js +5 -3
  32. package/dist/esm/persistence/idb/queries/IdbDocumentDb.js.map +1 -1
  33. package/dist/esm/persistence/idb/util.d.ts +2 -1
  34. package/dist/esm/persistence/idb/util.js +81 -23
  35. package/dist/esm/persistence/idb/util.js.map +1 -1
  36. package/dist/esm/sync/FileSync.js +2 -2
  37. package/dist/esm/sync/FileSync.js.map +1 -1
  38. package/dist/esm/sync/Heartbeat.js +2 -0
  39. package/dist/esm/sync/Heartbeat.js.map +1 -1
  40. package/dist/esm/sync/PushPullSync.d.ts +1 -1
  41. package/dist/esm/sync/PushPullSync.js +10 -1
  42. package/dist/esm/sync/PushPullSync.js.map +1 -1
  43. package/dist/esm/sync/WebSocketSync.d.ts +1 -1
  44. package/dist/esm/sync/WebSocketSync.js +14 -6
  45. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/BackoffScheduler.ts +3 -2
  48. package/src/client/Client.ts +6 -1
  49. package/src/client/ClientDescriptor.ts +16 -5
  50. package/src/context/context.ts +2 -0
  51. package/src/files/EntityFile.ts +6 -1
  52. package/src/files/FileManager.ts +2 -2
  53. package/src/persistence/MessageCreator.ts +0 -3
  54. package/src/persistence/PersistenceFiles.ts +1 -0
  55. package/src/persistence/idb/IdbService.ts +13 -4
  56. package/src/persistence/idb/files/IdbPersistenceFileDb.ts +2 -1
  57. package/src/persistence/idb/metadata/IdbMetadataDb.ts +6 -9
  58. package/src/persistence/idb/queries/IdbDocumentDb.ts +12 -6
  59. package/src/persistence/idb/util.ts +83 -22
  60. package/src/sync/FileSync.ts +7 -1
  61. package/src/sync/Heartbeat.ts +2 -0
  62. package/src/sync/PushPullSync.ts +15 -8
  63. 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 { FileConfig, InitialContext, QueryConfig } from '../context/context.js';
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
  };
@@ -106,6 +106,8 @@ export interface Context {
106
106
  WebSocket: typeof WebSocket;
107
107
  fetch: typeof fetch;
108
108
  indexedDB: typeof indexedDB;
109
+ location: Location;
110
+ history: History;
109
111
  };
110
112
 
111
113
  persistence: PersistenceImplementation;
@@ -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
  };
@@ -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
- { log }: { log?: Context['log'] } = {},
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
- window.location.reload();
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
- throw new Error('File is not in local database');
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
- ? IDBKeyRange.lowerBound(start, false)
334
- : end
335
- ? IDBKeyRange.upperBound(end, true)
336
- : undefined;
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 { Context } from '../../../context/context.js';
8
+ import { InitialContext } from '../../../context/context.js';
9
9
  import { PersistenceDocumentDb } from '../../interfaces.js';
10
10
  import { IdbService } from '../IdbService.js';
11
- import { closeDatabase, getSizeOfObjectStore, isAbortError } from '../util.js';
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
- private ctx;
16
- constructor(db: IDBDatabase, context: Omit<Context, 'documents' | 'files'>) {
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
- indexedDB: IDBFactory = globalIDB,
133
+ environment: Context['environment'],
134
134
  ) {
135
- const req1 = indexedDB.deleteDatabase([namespace, 'meta'].join('_'));
136
- const req2 = indexedDB.deleteDatabase([namespace, 'collections'].join('_'));
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
- window.location.reload();
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
- const tx = db.transaction(storeNames, mode);
168
- if (abortSignal) {
169
- const abort = () => {
170
- log?.('debug', 'aborting transaction');
171
- try {
172
- tx.abort();
173
- } catch (e) {
174
- log?.('debug', 'aborting transaction failed', e);
175
- }
176
- };
177
- abortSignal.addEventListener('abort', abort);
178
- tx.addEventListener('error', () => {
179
- abortSignal.removeEventListener('abort', abort);
180
- });
181
- tx.addEventListener('complete', () => {
182
- abortSignal.removeEventListener('abort', abort);
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
- return tx;
243
+ }
244
+
245
+ export function isTransactionAborted(tx: IDBTransaction) {
246
+ return (tx as any).__aborted;
186
247
  }
187
248
 
188
249
  /**
@@ -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('error', 'File information fetch failed', e);
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,
@@ -55,9 +55,11 @@ export class Heartbeat extends EventSubscriber<{
55
55
  stop = () => {
56
56
  if (this.nextBeat) {
57
57
  clearTimeout(this.nextBeat);
58
+ this.nextBeat = null;
58
59
  }
59
60
  if (this.deadline) {
60
61
  clearTimeout(this.deadline);
62
+ this.deadline = null;
61
63
  }
62
64
  };
63
65
 
@@ -7,11 +7,11 @@ import {
7
7
  isVerdantErrorResponse,
8
8
  throttle,
9
9
  } from '@verdant-web/common';
10
- import { PresenceManager } from './PresenceManager.js';
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
 
@@ -157,6 +154,14 @@ export class PushPullSync
157
154
  }, 3000);
158
155
 
159
156
  send = (message: ClientMessage) => {
157
+ if (this.status !== 'active') {
158
+ this.ctx.log(
159
+ 'warn',
160
+ 'Attempted to send message while sync is not active',
161
+ message,
162
+ );
163
+ return;
164
+ }
160
165
  // only certain messages are sent for pull-based sync.
161
166
  switch (message.type) {
162
167
  case 'presence-update':
@@ -176,11 +181,13 @@ export class PushPullSync
176
181
  if (this.status === 'active') {
177
182
  return;
178
183
  }
184
+ this.ctx.log('debug', 'Starting push-pull sync');
179
185
  await this.endpointProvider.getEndpoints();
180
186
  this.heartbeat.start(true);
181
187
  this._status = 'active';
182
188
  };
183
189
  stop(): void {
190
+ this.ctx.log('debug', 'Stopping push-pull sync');
184
191
  this.heartbeat.stop();
185
192
  this._status = 'paused';
186
193
  }
@@ -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(60 * 1000, 1.5),
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.onError(event);
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?.close();
289
+ if (this.socket?.readyState === WEBSOCKET_OPEN) {
290
+ this.socket.close();
291
+ }
286
292
  this.socket = null;
287
293
  this._status = 'paused';
288
294
  };