@jcbuisson/express-x-client 3.1.13 → 3.1.15

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 +76 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x-client",
3
- "version": "3.1.13",
3
+ "version": "3.1.15",
4
4
  "type": "module",
5
5
  "description": "Client library for ExpressX framework",
6
6
  "main": "src/client.mts",
package/src/client.mts CHANGED
@@ -199,31 +199,42 @@ export function offlinePlugin(app) {
199
199
 
200
200
  app.service(modelName).on('createWithMeta', async ([value, meta]) => {
201
201
  console.log(`${modelName} EVENT createWithMeta`, value);
202
- await db.values.put(value);
203
- await db.metadata.put(meta);
202
+ if (await isIncomingEventStale(value?.uid ?? meta?.uid, meta)) return
203
+ if (value?.uid) await db.values.put(value);
204
+ if (meta?.uid) await db.metadata.put({ ...meta, __dirty__: false });
204
205
  });
205
206
 
206
207
  app.service(modelName).on('updateWithMeta', async ([value, meta]) => {
207
208
  console.log(`${modelName} EVENT updateWithMeta`, value);
208
- // value may be undefined when the server's UPDATE RETURNING yielded 0 rows
209
+ // value may be undefined when the server's update yielded 0 rows
209
210
  // (concurrent delete race: record was removed between the sync's findMany
210
- // snapshot and the actual UPDATE). Guard to avoid a TypeError crash that
211
+ // snapshot and the actual update). Guard to avoid a TypeError crash that
211
212
  // would prevent db.metadata.put(meta) from running.
213
+ if (await isIncomingEventStale(value?.uid ?? meta?.uid, meta)) return
212
214
  if (value?.uid) await db.values.put(value);
213
- await db.metadata.put(meta);
215
+ if (meta?.uid) await db.metadata.put({ ...meta, __dirty__: false });
214
216
  });
215
217
 
216
218
  app.service(modelName).on('deleteWithMeta', async ([value, meta]) => {
217
219
  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.
220
- if (value?.uid) await db.values.delete(value.uid)
220
+ // value may be undefined when the server's delete yielded 0 rows
221
+ // (double-delete race).
221
222
  // 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.
224
- await db.metadata.delete(meta.uid)
223
+ // uid. If the pub/sub handler fires AFTER step 2, put() would re-create the
224
+ // metadata row as a permanent orphan. delete() is idempotent regardless of order.
225
+ const uid = value?.uid ?? meta?.uid
226
+ if (await isIncomingEventStale(uid, meta)) return
227
+ if (value?.uid) await db.values.delete(value.uid)
228
+ if (uid) await db.metadata.delete(uid)
225
229
  });
226
230
 
231
+ async function isIncomingEventStale(uid, incomingMeta) {
232
+ if (!uid || !incomingMeta) return false
233
+ const currentMeta = await db.metadata.get(uid)
234
+ if (!currentMeta) return false
235
+ return compareMetadataTime(currentMeta, incomingMeta) > 0
236
+ }
237
+
227
238
 
228
239
  ///////////// CREATE/UPDATE/REMOVE /////////////
229
240
 
@@ -237,12 +248,7 @@ export function offlinePlugin(app) {
237
248
  // execute on server, asynchronously, if connection is active
238
249
  if (app.isConnected) {
239
250
  app.service(modelName).createWithMeta(uid, data, now)
240
- .then(async result => {
241
- const [value, meta] = Array.isArray(result) ? result : []
242
- if (value?.uid) await db.values.put(value)
243
- if (meta?.uid) await db.metadata.put({ ...meta, __dirty__: false })
244
- else await db.metadata.update(uid, { __dirty__: false })
245
- })
251
+ .then(result => applyCreateAcknowledgement(uid, now, result))
246
252
  .catch(async err => {
247
253
  console.log(`*** err sync ${modelName} create`, err)
248
254
  // rollback
@@ -253,6 +259,38 @@ export function offlinePlugin(app) {
253
259
  return await db.values.get(uid)
254
260
  }
255
261
 
262
+ async function applyCreateAcknowledgement(uid, requestCreatedAt, result) {
263
+ const currentMetadata = await db.metadata.get(uid)
264
+ if (!currentMetadata || !sameTimestamp(currentMetadata.created_at, requestCreatedAt)) 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 applyUpdateAcknowledgement(uid, requestUpdatedAt, result) {
274
+ const currentMetadata = await db.metadata.get(uid)
275
+ if (!currentMetadata || !sameTimestamp(currentMetadata.updated_at, requestUpdatedAt)) return
276
+ const [value, meta] = Array.isArray(result) ? result : []
277
+ if (value?.uid) await db.values.put(value)
278
+ if (meta?.uid)
279
+ await db.metadata.put({ ...meta, __dirty__: false })
280
+ else
281
+ await db.metadata.update(uid, { __dirty__: false })
282
+ }
283
+
284
+ async function applyDeleteAcknowledgement(uid, requestDeletedAt, result) {
285
+ const currentMetadata = await db.metadata.get(uid)
286
+ if (!currentMetadata || !sameTimestamp(currentMetadata.deleted_at, requestDeletedAt)) return
287
+ const [, meta] = Array.isArray(result) ? result : []
288
+ if (meta?.uid)
289
+ await db.metadata.put({ ...meta, __dirty__: false })
290
+ else
291
+ await db.metadata.update(uid, { __dirty__: false })
292
+ }
293
+
256
294
  const update = async (uid: string, data: object) => {
257
295
  const previousValue = { ...(await db.values.get(uid)) }
258
296
  const previousMetadata = { ...(await db.metadata.get(uid)) }
@@ -263,12 +301,7 @@ export function offlinePlugin(app) {
263
301
  // execute on server, asynchronously, if connection is active
264
302
  if (app.isConnected) {
265
303
  app.service(modelName).updateWithMeta(uid, data, now)
266
- .then(async result => {
267
- const [value, meta] = Array.isArray(result) ? result : []
268
- if (value?.uid) await db.values.put(value)
269
- if (meta?.uid) await db.metadata.put({ ...meta, __dirty__: false })
270
- else await db.metadata.update(uid, { __dirty__: false })
271
- })
304
+ .then(result => applyUpdateAcknowledgement(uid, now, result))
272
305
  .catch(async err => {
273
306
  console.log(`*** err sync ${modelName} update`, err)
274
307
  // rollback
@@ -295,11 +328,7 @@ export function offlinePlugin(app) {
295
328
  // and in database, if connected
296
329
  if (app.isConnected) {
297
330
  app.service(modelName).deleteWithMeta(uid, deleted_at)
298
- .then(async result => {
299
- const [, meta] = Array.isArray(result) ? result : []
300
- if (meta?.uid) await db.metadata.put({ ...meta, __dirty__: false })
301
- else await db.metadata.update(uid, { __dirty__: false })
302
- })
331
+ .then(result => applyDeleteAcknowledgement(uid, deleted_at, result))
303
332
  .catch(async err => {
304
333
  console.log(`*** err sync ${modelName} remove`, err)
305
334
  // rollback
@@ -641,6 +670,25 @@ function stringifyWithSortedKeys(obj, space = null) {
641
670
  }
642
671
  // console.log('stringifyWithSortedKeys({ age: 30, name: "Alice", data: { city: "Paris", color: "red" }})', stringifyWithSortedKeys({ age: 30, name: "Alice", data: { city: "Paris", color: "red" } }))
643
672
 
673
+ function sameTimestamp(a, b) {
674
+ if (!a || !b) return a === b
675
+ return new Date(a).getTime() === new Date(b).getTime()
676
+ }
677
+
678
+ function compareMetadataTime(a, b) {
679
+ const aTime = metadataTime(a)
680
+ const bTime = metadataTime(b)
681
+ if (aTime == null || bTime == null) return 0
682
+ return aTime - bTime
683
+ }
684
+
685
+ function metadataTime(meta) {
686
+ const value = meta?.deleted_at ?? meta?.updated_at ?? meta?.created_at
687
+ if (!value) return null
688
+ const time = new Date(value).getTime()
689
+ return Number.isNaN(time) ? null : time
690
+ }
691
+
644
692
  export class Mutex {
645
693
  constructor() {
646
694
  this.locked = false;