@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/client.mts +70 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x-client",
3
- "version": "3.1.12",
3
+ "version": "3.1.14",
4
4
  "type": "module",
5
5
  "description": "Client library for ExpressX framework",
6
6
  "main": "src/client.mts",
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 UPDATE RETURNING yielded 0 rows
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 UPDATE). Guard to avoid a TypeError crash that
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 DELETE RETURNING yielded 0 rows
219
- // (double-delete race). Guard before accessing .uid.
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. 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.
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, { updated_at: previousMetadata.updated_at ?? null })
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;