@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 +2 -2
- package/package.json +1 -1
- package/src/client.mts +45 -10
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`
|
|
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,
|
|
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
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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,
|
|
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
|
-
|
|
373
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
579
|
+
if (syncWhere) await syncWhere(where)
|
|
580
|
+
else await synchronize(modelName, idbValues, idbMetadata, where)
|
|
546
581
|
}
|
|
547
582
|
}
|
|
548
583
|
|