@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.
- package/package.json +1 -1
- package/src/client.mts +76 -28
package/package.json
CHANGED
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
|
|
203
|
-
await db.
|
|
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
|
|
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
|
|
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
|
|
219
|
-
// (double-delete race).
|
|
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.
|
|
223
|
-
// metadata row as a permanent orphan.
|
|
224
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|