@jcbuisson/express-x-client 3.1.12 → 3.1.14
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/package.json +1 -1
- package/src/client.mts +70 -15
package/package.json
CHANGED
package/src/client.mts
CHANGED
|
@@ -205,9 +205,9 @@ export function offlinePlugin(app) {
|
|
|
205
205
|
|
|
206
206
|
app.service(modelName).on('updateWithMeta', async ([value, meta]) => {
|
|
207
207
|
console.log(`${modelName} EVENT updateWithMeta`, value);
|
|
208
|
-
// value may be undefined when the server's
|
|
208
|
+
// value may be undefined when the server's update yielded 0 rows
|
|
209
209
|
// (concurrent delete race: record was removed between the sync's findMany
|
|
210
|
-
// snapshot and the actual
|
|
210
|
+
// snapshot and the actual update). Guard to avoid a TypeError crash that
|
|
211
211
|
// would prevent db.metadata.put(meta) from running.
|
|
212
212
|
if (value?.uid) await db.values.put(value);
|
|
213
213
|
await db.metadata.put(meta);
|
|
@@ -215,12 +215,12 @@ export function offlinePlugin(app) {
|
|
|
215
215
|
|
|
216
216
|
app.service(modelName).on('deleteWithMeta', async ([value, meta]) => {
|
|
217
217
|
console.log(`${modelName} EVENT deleteWithMeta`, value)
|
|
218
|
-
// value may be undefined when the server's
|
|
219
|
-
// (double-delete race).
|
|
218
|
+
// value may be undefined when the server's delete yielded 0 rows
|
|
219
|
+
// (double-delete race).
|
|
220
220
|
if (value?.uid) await db.values.delete(value.uid)
|
|
221
221
|
// delete, not put: synchronize() step 2 also deletes idbMetadata for the same
|
|
222
|
-
// uid.
|
|
223
|
-
// metadata row as a permanent orphan.
|
|
222
|
+
// uid. If the pub/sub handler fires AFTER step 2, put() would re-create the
|
|
223
|
+
// metadata row as a permanent orphan. delete() is idempotent regardless of order.
|
|
224
224
|
await db.metadata.delete(meta.uid)
|
|
225
225
|
});
|
|
226
226
|
|
|
@@ -233,10 +233,11 @@ export function offlinePlugin(app) {
|
|
|
233
233
|
// optimistic update
|
|
234
234
|
const now = new Date()
|
|
235
235
|
await db.values.add({ uid, ...data })
|
|
236
|
-
await db.metadata.add({ uid, created_at: now })
|
|
236
|
+
await db.metadata.add({ uid, created_at: now, __dirty__: true })
|
|
237
237
|
// execute on server, asynchronously, if connection is active
|
|
238
238
|
if (app.isConnected) {
|
|
239
239
|
app.service(modelName).createWithMeta(uid, data, now)
|
|
240
|
+
.then(result => applyCreateAcknowledgement(uid, now, result))
|
|
240
241
|
.catch(async err => {
|
|
241
242
|
console.log(`*** err sync ${modelName} create`, err)
|
|
242
243
|
// rollback
|
|
@@ -247,16 +248,49 @@ export function offlinePlugin(app) {
|
|
|
247
248
|
return await db.values.get(uid)
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
async function applyCreateAcknowledgement(uid, requestCreatedAt, result) {
|
|
252
|
+
const currentMetadata = await db.metadata.get(uid)
|
|
253
|
+
if (!currentMetadata || !sameTimestamp(currentMetadata.created_at, requestCreatedAt)) return
|
|
254
|
+
const [value, meta] = Array.isArray(result) ? result : []
|
|
255
|
+
if (value?.uid) await db.values.put(value)
|
|
256
|
+
if (meta?.uid)
|
|
257
|
+
await db.metadata.put({ ...meta, __dirty__: false })
|
|
258
|
+
else
|
|
259
|
+
await db.metadata.update(uid, { __dirty__: false })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function applyUpdateAcknowledgement(uid, requestUpdatedAt, result) {
|
|
263
|
+
const currentMetadata = await db.metadata.get(uid)
|
|
264
|
+
if (!currentMetadata || !sameTimestamp(currentMetadata.updated_at, requestUpdatedAt)) return
|
|
265
|
+
const [value, meta] = Array.isArray(result) ? result : []
|
|
266
|
+
if (value?.uid) await db.values.put(value)
|
|
267
|
+
if (meta?.uid)
|
|
268
|
+
await db.metadata.put({ ...meta, __dirty__: false })
|
|
269
|
+
else
|
|
270
|
+
await db.metadata.update(uid, { __dirty__: false })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function applyDeleteAcknowledgement(uid, requestDeletedAt, result) {
|
|
274
|
+
const currentMetadata = await db.metadata.get(uid)
|
|
275
|
+
if (!currentMetadata || !sameTimestamp(currentMetadata.deleted_at, requestDeletedAt)) return
|
|
276
|
+
const [, meta] = Array.isArray(result) ? result : []
|
|
277
|
+
if (meta?.uid)
|
|
278
|
+
await db.metadata.put({ ...meta, __dirty__: false })
|
|
279
|
+
else
|
|
280
|
+
await db.metadata.update(uid, { __dirty__: false })
|
|
281
|
+
}
|
|
282
|
+
|
|
250
283
|
const update = async (uid: string, data: object) => {
|
|
251
284
|
const previousValue = { ...(await db.values.get(uid)) }
|
|
252
285
|
const previousMetadata = { ...(await db.metadata.get(uid)) }
|
|
253
286
|
// optimistic update of cache
|
|
254
287
|
const now = new Date()
|
|
255
288
|
await db.values.update(uid, data)
|
|
256
|
-
await db.metadata.update(uid, { updated_at: now })
|
|
289
|
+
await db.metadata.update(uid, { updated_at: now, __dirty__: true })
|
|
257
290
|
// execute on server, asynchronously, if connection is active
|
|
258
291
|
if (app.isConnected) {
|
|
259
292
|
app.service(modelName).updateWithMeta(uid, data, now)
|
|
293
|
+
.then(result => applyUpdateAcknowledgement(uid, now, result))
|
|
260
294
|
.catch(async err => {
|
|
261
295
|
console.log(`*** err sync ${modelName} update`, err)
|
|
262
296
|
// rollback
|
|
@@ -266,7 +300,10 @@ export function offlinePlugin(app) {
|
|
|
266
300
|
// Restoring the full previousMetadata snapshot would overwrite any
|
|
267
301
|
// deleted_at that remove() set while the socket round-trip was in flight,
|
|
268
302
|
// silently un-deleting the record.
|
|
269
|
-
await db.metadata.update(uid, {
|
|
303
|
+
await db.metadata.update(uid, {
|
|
304
|
+
updated_at: previousMetadata.updated_at ?? null,
|
|
305
|
+
__dirty__: previousMetadata.__dirty__ ?? false,
|
|
306
|
+
})
|
|
270
307
|
})
|
|
271
308
|
}
|
|
272
309
|
return await db.values.get(uid)
|
|
@@ -276,15 +313,16 @@ export function offlinePlugin(app) {
|
|
|
276
313
|
const deleted_at = new Date()
|
|
277
314
|
// optimistic delete in cache
|
|
278
315
|
await db.values.update(uid, { __deleted__: true })
|
|
279
|
-
await db.metadata.update(uid, { deleted_at })
|
|
316
|
+
await db.metadata.update(uid, { deleted_at, __dirty__: true })
|
|
280
317
|
// and in database, if connected
|
|
281
318
|
if (app.isConnected) {
|
|
282
319
|
app.service(modelName).deleteWithMeta(uid, deleted_at)
|
|
320
|
+
.then(result => applyDeleteAcknowledgement(uid, deleted_at, result))
|
|
283
321
|
.catch(async err => {
|
|
284
322
|
console.log(`*** err sync ${modelName} remove`, err)
|
|
285
323
|
// rollback
|
|
286
324
|
await db.values.update(uid, { __deleted__: null })
|
|
287
|
-
await db.metadata.update(uid, { deleted_at: null })
|
|
325
|
+
await db.metadata.update(uid, { deleted_at: null, __dirty__: false })
|
|
288
326
|
})
|
|
289
327
|
}
|
|
290
328
|
}
|
|
@@ -439,6 +477,12 @@ export function offlinePlugin(app) {
|
|
|
439
477
|
clientMetadataDict[value.uid] = {}
|
|
440
478
|
}
|
|
441
479
|
}
|
|
480
|
+
const dirtyMetadataList = await idbMetadata.filter(metadata => metadata.__dirty__).toArray()
|
|
481
|
+
for (const metadata of dirtyMetadataList) {
|
|
482
|
+
if (metadata.uid in clientMetadataDict) continue
|
|
483
|
+
const value = await idbValues.get(metadata.uid)
|
|
484
|
+
if (value || metadata.deleted_at) clientMetadataDict[metadata.uid] = metadata
|
|
485
|
+
}
|
|
442
486
|
|
|
443
487
|
// call sync service on `where` perimeter
|
|
444
488
|
const { addClient, updateClient, deleteClient, addDatabase, updateDatabase } =
|
|
@@ -458,7 +502,7 @@ export function offlinePlugin(app) {
|
|
|
458
502
|
// add() would throw ConstraintError and abort the entire transaction,
|
|
459
503
|
// silently dropping every other addClient record in the batch.
|
|
460
504
|
await idbValues.put(value)
|
|
461
|
-
await idbMetadata.put(metaData)
|
|
505
|
+
await idbMetadata.put({ ...metaData, __dirty__: false })
|
|
462
506
|
}
|
|
463
507
|
})
|
|
464
508
|
}
|
|
@@ -476,7 +520,7 @@ export function offlinePlugin(app) {
|
|
|
476
520
|
const value = { ...elt }
|
|
477
521
|
delete value.__deleted__
|
|
478
522
|
await idbValues.put(value)
|
|
479
|
-
await idbMetadata.put({ uid: elt.uid, ...serverMeta })
|
|
523
|
+
await idbMetadata.put({ uid: elt.uid, ...serverMeta, __dirty__: false })
|
|
480
524
|
}
|
|
481
525
|
|
|
482
526
|
// 4- create elements of `addDatabase` with full data from cache
|
|
@@ -491,7 +535,10 @@ export function offlinePlugin(app) {
|
|
|
491
535
|
delete fullValue.uid
|
|
492
536
|
delete fullValue.__deleted__
|
|
493
537
|
try {
|
|
494
|
-
await app.service(modelName).createWithMeta(elt.uid, fullValue, elt.created_at)
|
|
538
|
+
const result = await app.service(modelName).createWithMeta(elt.uid, fullValue, elt.created_at)
|
|
539
|
+
const serverMeta = Array.isArray(result) ? result[1] : null
|
|
540
|
+
if (serverMeta?.uid) await idbMetadata.put({ ...serverMeta, __dirty__: false })
|
|
541
|
+
else await idbMetadata.update(elt.uid, { __dirty__: false })
|
|
495
542
|
} catch(err) {
|
|
496
543
|
console.log("*** err sync user addDatabase", err, elt.uid, fullValue, elt.created_at)
|
|
497
544
|
// rollback
|
|
@@ -508,7 +555,10 @@ export function offlinePlugin(app) {
|
|
|
508
555
|
delete fullValue.uid
|
|
509
556
|
delete fullValue.__deleted__
|
|
510
557
|
try {
|
|
511
|
-
await app.service(modelName).updateWithMeta(elt.uid, fullValue, elt.updated_at)
|
|
558
|
+
const result = await app.service(modelName).updateWithMeta(elt.uid, fullValue, elt.updated_at)
|
|
559
|
+
const serverMeta = Array.isArray(result) ? result[1] : null
|
|
560
|
+
if (serverMeta?.uid) await idbMetadata.put({ ...serverMeta, __dirty__: false })
|
|
561
|
+
else await idbMetadata.update(elt.uid, { __dirty__: false })
|
|
512
562
|
} catch(err) {
|
|
513
563
|
console.log("*** err sync user updateDatabase", err)
|
|
514
564
|
// Leave client's local version intact; it will be retried on the next sync.
|
|
@@ -609,6 +659,11 @@ function stringifyWithSortedKeys(obj, space = null) {
|
|
|
609
659
|
}
|
|
610
660
|
// console.log('stringifyWithSortedKeys({ age: 30, name: "Alice", data: { city: "Paris", color: "red" }})', stringifyWithSortedKeys({ age: 30, name: "Alice", data: { city: "Paris", color: "red" } }))
|
|
611
661
|
|
|
662
|
+
function sameTimestamp(a, b) {
|
|
663
|
+
if (!a || !b) return a === b
|
|
664
|
+
return new Date(a).getTime() === new Date(b).getTime()
|
|
665
|
+
}
|
|
666
|
+
|
|
612
667
|
export class Mutex {
|
|
613
668
|
constructor() {
|
|
614
669
|
this.locked = false;
|