@jcbuisson/express-x-client 3.1.11 → 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.11",
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
@@ -184,6 +184,8 @@ export function offlinePlugin(app) {
184
184
 
185
185
  const dbName = modelName;
186
186
  const db = getOrCreateDB(dbName, fields);
187
+ const synchronizedWhereKeys = new Set();
188
+ const synchronizeWherePromises = new Map();
187
189
 
188
190
  const reset = async () => {
189
191
  console.log('reset', modelName);
@@ -322,8 +324,9 @@ export function offlinePlugin(app) {
322
324
  // behavior as before.
323
325
  return defer(() => {
324
326
  const ready = addSynchroWhere(where).then((isNew: boolean) => {
325
- if (isNew && app.isConnected) {
326
- 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)
327
330
  }
328
331
  })
329
332
  return from(ready).pipe(switchMap(() => liveQuery$))
@@ -341,11 +344,27 @@ export function offlinePlugin(app) {
341
344
  function removeSynchroWhere(where: object) {
342
345
  console.log('removeSynchroWhere', dbName, modelName, where)
343
346
  count -= 1
347
+ synchronizedWhereKeys.delete(stringifyWithSortedKeys(where))
344
348
  return removeSynchroDBWhere(where, db.whereList)
345
349
  }
346
350
 
347
351
  async function synchronizeAll() {
348
- 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)
349
368
  }
350
369
 
351
370
  // Automatically clean up when the component using this composable unmounts
@@ -369,12 +388,16 @@ export function offlinePlugin(app) {
369
388
  }
370
389
  }
371
390
 
391
+ let hasConnected = false
392
+
372
393
  app.addConnectListener(async (_socket) => {
373
394
  app.connectedDate = new Date()
374
395
  console.log('onConnect', app.connectedDate)
375
396
  app.isConnected = true
376
397
  const disconnectedDate = app.disconnectedDate
377
- if (disconnectedDate) {
398
+ const isInitialConnect = !hasConnected
399
+ hasConnected = true
400
+ if (disconnectedDate || isInitialConnect) {
378
401
  const results = await Promise.allSettled(modelSyncFunctions.map(sync => sync()))
379
402
  const failures = results.filter(result => result.status === 'rejected')
380
403
  if (failures.length > 0) {
@@ -396,7 +419,7 @@ export function offlinePlugin(app) {
396
419
  const mutex = new Mutex()
397
420
 
398
421
  // ex: where = { uid: 'azer' }
399
- async function synchronize(modelName, idbValues, idbMetadata, where, cutoffDate) {
422
+ async function synchronize(modelName, idbValues, idbMetadata, where) {
400
423
  await mutex.acquire()
401
424
  console.log('synchronize', modelName, where)
402
425
 
@@ -419,7 +442,7 @@ export function offlinePlugin(app) {
419
442
 
420
443
  // call sync service on `where` perimeter
421
444
  const { addClient, updateClient, deleteClient, addDatabase, updateDatabase } =
422
- await app.service('sync').go(modelName, where, cutoffDate, clientMetadataDict)
445
+ await app.service('sync').go(modelName, where, clientMetadataDict)
423
446
  console.log('-> service.sync', modelName, where, addClient, updateClient, deleteClient, addDatabase, updateDatabase)
424
447
 
425
448
  // 1- add missing elements in indexedDB cache
@@ -550,10 +573,11 @@ export function offlinePlugin(app) {
550
573
  }
551
574
  }
552
575
 
553
- async function synchronizeModelWhereList(modelName, idbValues, idbMetadata, cutoffDate, whereDb) {
576
+ async function synchronizeModelWhereList(modelName, idbValues, idbMetadata, whereDb, syncWhere = null) {
554
577
  const whereList = await getWhereList(whereDb)
555
578
  for (const where of whereList) {
556
- await synchronize(modelName, idbValues, idbMetadata, where, cutoffDate)
579
+ if (syncWhere) await syncWhere(where)
580
+ else await synchronize(modelName, idbValues, idbMetadata, where)
557
581
  }
558
582
  }
559
583