@jcbuisson/express-x-client 3.1.10 → 3.1.12

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/CLAUDE.md CHANGED
@@ -35,7 +35,7 @@ Enriches `app` with offline-first IndexedDB CRUD via Dexie. Call `app.createOffl
35
35
  This plugin also adds three dynamic attributes to `app`:
36
36
  - `app.isConnected` — boolean, updated on socket connect/disconnect
37
37
  - `app.connectedDate` — `Date` of the last connection, or `null`
38
- - `app.disconnectedDate` — `Date` of the last disconnection, or `null` (used as `cutoffDate` for sync)
38
+ - `app.disconnectedDate` — `Date` of the last disconnection, or `null`
39
39
 
40
40
  Each model maintains three Dexie stores under the same DB name:
41
41
  - `values` — the actual records (indexed on `uid`, `__deleted__`)
@@ -47,7 +47,7 @@ Each model maintains three Dexie stores under the same DB name:
47
47
 
48
48
  **Optimistic writes**: `create`, `update`, `remove` write to IndexedDB immediately, then call the server service method. On server error they roll back the local change.
49
49
 
50
- **Sync on reconnect**: When the socket reconnects, every registered model calls `synchronizeAll`, which iterates its `whereList` and calls the server's `sync.go(modelName, where, cutoffDate, clientMetadataDict)` service. The response contains five buckets (`addClient`, `updateClient`, `deleteClient`, `addDatabase`, `updateDatabase`) that are applied in order.
50
+ **Sync on reconnect**: When the socket reconnects, every registered model calls `synchronizeAll`, which iterates its `whereList` and calls the server's `sync.go(modelName, where, clientMetadataDict)` service. The response contains five buckets (`addClient`, `updateClient`, `deleteClient`, `addDatabase`, `updateDatabase`) that are applied in order.
51
51
 
52
52
  **Real-time observables**: `getObservable(where)` returns an RxJS `Observable` backed by Dexie's `liveQuery`. It also registers the `where` in `whereList` and triggers a sync if it is a new, unregistered filter. Vue component lifecycle cleanup is handled by `tryOnScopeDispose`.
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x-client",
3
- "version": "3.1.10",
3
+ "version": "3.1.12",
4
4
  "type": "module",
5
5
  "description": "Client library for ExpressX framework",
6
6
  "main": "src/client.mts",
package/src/client.mts CHANGED
@@ -26,7 +26,11 @@ export function createClient(socket, options={}) {
26
26
  socket.on("connect", async () => {
27
27
  if (options.debug) console.log("socket connected", socket.id)
28
28
  for (const func of connectListeners) {
29
- func(socket)
29
+ try {
30
+ await func(socket)
31
+ } catch(err) {
32
+ console.error('connect listener error', err)
33
+ }
30
34
  }
31
35
  })
32
36
 
@@ -180,6 +184,8 @@ export function offlinePlugin(app) {
180
184
 
181
185
  const dbName = modelName;
182
186
  const db = getOrCreateDB(dbName, fields);
187
+ const synchronizedWhereKeys = new Set();
188
+ const synchronizeWherePromises = new Map();
183
189
 
184
190
  const reset = async () => {
185
191
  console.log('reset', modelName);
@@ -318,8 +324,9 @@ export function offlinePlugin(app) {
318
324
  // behavior as before.
319
325
  return defer(() => {
320
326
  const ready = addSynchroWhere(where).then((isNew: boolean) => {
321
- if (isNew && app.isConnected) {
322
- return synchronize(modelName, db.values, db.metadata, where, app.disconnectedDate)
327
+ const whereKey = stringifyWithSortedKeys(where)
328
+ if (app.isConnected && (isNew || !synchronizedWhereKeys.has(whereKey))) {
329
+ return synchronizeWhere(where)
323
330
  }
324
331
  })
325
332
  return from(ready).pipe(switchMap(() => liveQuery$))
@@ -337,11 +344,27 @@ export function offlinePlugin(app) {
337
344
  function removeSynchroWhere(where: object) {
338
345
  console.log('removeSynchroWhere', dbName, modelName, where)
339
346
  count -= 1
347
+ synchronizedWhereKeys.delete(stringifyWithSortedKeys(where))
340
348
  return removeSynchroDBWhere(where, db.whereList)
341
349
  }
342
350
 
343
351
  async function synchronizeAll() {
344
- await synchronizeModelWhereList(modelName, db.values, db.metadata, app.disconnectedDate, db.whereList)
352
+ await synchronizeModelWhereList(modelName, db.values, db.metadata, db.whereList, synchronizeWhere)
353
+ }
354
+
355
+ async function synchronizeWhere(where) {
356
+ const whereKey = stringifyWithSortedKeys(where)
357
+ if (!synchronizeWherePromises.has(whereKey)) {
358
+ const promise = synchronize(modelName, db.values, db.metadata, where)
359
+ .then(() => {
360
+ synchronizedWhereKeys.add(whereKey)
361
+ })
362
+ .finally(() => {
363
+ synchronizeWherePromises.delete(whereKey)
364
+ })
365
+ synchronizeWherePromises.set(whereKey, promise)
366
+ }
367
+ return synchronizeWherePromises.get(whereKey)
345
368
  }
346
369
 
347
370
  // Automatically clean up when the component using this composable unmounts
@@ -365,12 +388,22 @@ export function offlinePlugin(app) {
365
388
  }
366
389
  }
367
390
 
391
+ let hasConnected = false
392
+
368
393
  app.addConnectListener(async (_socket) => {
369
394
  app.connectedDate = new Date()
370
395
  console.log('onConnect', app.connectedDate)
371
396
  app.isConnected = true
372
- if (app.disconnectedDate) {
373
- modelSyncFunctions.forEach(sync => sync())
397
+ const disconnectedDate = app.disconnectedDate
398
+ const isInitialConnect = !hasConnected
399
+ hasConnected = true
400
+ if (disconnectedDate || isInitialConnect) {
401
+ const results = await Promise.allSettled(modelSyncFunctions.map(sync => sync()))
402
+ const failures = results.filter(result => result.status === 'rejected')
403
+ if (failures.length > 0) {
404
+ console.error('err reconnect synchronizeAll', failures.map(result => result.reason))
405
+ return
406
+ }
374
407
  }
375
408
  app.disconnectedDate = null
376
409
  })
@@ -386,7 +419,7 @@ export function offlinePlugin(app) {
386
419
  const mutex = new Mutex()
387
420
 
388
421
  // ex: where = { uid: 'azer' }
389
- async function synchronize(modelName, idbValues, idbMetadata, where, cutoffDate) {
422
+ async function synchronize(modelName, idbValues, idbMetadata, where) {
390
423
  await mutex.acquire()
391
424
  console.log('synchronize', modelName, where)
392
425
 
@@ -409,7 +442,7 @@ export function offlinePlugin(app) {
409
442
 
410
443
  // call sync service on `where` perimeter
411
444
  const { addClient, updateClient, deleteClient, addDatabase, updateDatabase } =
412
- await app.service('sync').go(modelName, where, cutoffDate, clientMetadataDict)
445
+ await app.service('sync').go(modelName, where, clientMetadataDict)
413
446
  console.log('-> service.sync', modelName, where, addClient, updateClient, deleteClient, addDatabase, updateDatabase)
414
447
 
415
448
  // 1- add missing elements in indexedDB cache
@@ -483,6 +516,7 @@ export function offlinePlugin(app) {
483
516
  }
484
517
  } catch(err) {
485
518
  console.log('err synchronize', modelName, where, err)
519
+ throw err
486
520
  } finally {
487
521
  mutex.release()
488
522
  }
@@ -539,10 +573,11 @@ export function offlinePlugin(app) {
539
573
  }
540
574
  }
541
575
 
542
- async function synchronizeModelWhereList(modelName, idbValues, idbMetadata, cutoffDate, whereDb) {
576
+ async function synchronizeModelWhereList(modelName, idbValues, idbMetadata, whereDb, syncWhere = null) {
543
577
  const whereList = await getWhereList(whereDb)
544
578
  for (const where of whereList) {
545
- await synchronize(modelName, idbValues, idbMetadata, where, cutoffDate)
579
+ if (syncWhere) await syncWhere(where)
580
+ else await synchronize(modelName, idbValues, idbMetadata, where)
546
581
  }
547
582
  }
548
583