@jcbuisson/express-x-client 2.2.1 → 3.0.0
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/index.mjs +500 -8
- package/.vscode/launch.json +0 -18
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import Dexie from "dexie";
|
|
2
|
+
import { from } from 'rxjs';
|
|
3
|
+
import { distinctUntilChanged } from 'rxjs/operators';
|
|
4
|
+
import { liveQuery } from "dexie";
|
|
5
|
+
// uuidv7 are monotonically increasing and much improve database performance amid B-tree indexes
|
|
6
|
+
import { v7 as uuidv7 } from 'uuid';
|
|
7
|
+
import { tryOnScopeDispose } from '@vueuse/core';
|
|
8
|
+
import { useSessionStorage } from '@vueuse/core'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
////////////////////////// UTILITIES //////////////////////////
|
|
1
12
|
|
|
2
13
|
function generateUID(length) {
|
|
3
14
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
@@ -11,7 +22,9 @@ function generateUID(length) {
|
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
////////////////////////// EXPRESSX //////////////////////////
|
|
26
|
+
|
|
27
|
+
export function createClient(socket, options={}) {
|
|
15
28
|
if (options.debug === undefined) options.debug = false
|
|
16
29
|
|
|
17
30
|
const waitingPromisesByUid = {}
|
|
@@ -21,22 +34,26 @@ export default function expressXClient(socket, options={}) {
|
|
|
21
34
|
let disconnectListeners = []
|
|
22
35
|
let errorListeners = []
|
|
23
36
|
|
|
37
|
+
function configure(callback, ...args) {
|
|
38
|
+
callback(app, ...args)
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
socket.on("connect", async () => {
|
|
25
|
-
console.log("socket connected", socket.id)
|
|
42
|
+
if (options.debug) console.log("socket connected", socket.id)
|
|
26
43
|
for (const func of connectListeners) {
|
|
27
44
|
func(socket)
|
|
28
45
|
}
|
|
29
46
|
})
|
|
30
47
|
|
|
31
48
|
socket.on("connect_error", async (err) => {
|
|
32
|
-
console.log("socket connection error", socket.id)
|
|
49
|
+
if (options.debug) console.log("socket connection error", socket.id)
|
|
33
50
|
for (const func of errorListeners) {
|
|
34
51
|
func(socket)
|
|
35
52
|
}
|
|
36
53
|
})
|
|
37
54
|
|
|
38
55
|
socket.on("disconnect", async () => {
|
|
39
|
-
console.log("socket disconnected", socket.id)
|
|
56
|
+
if (options.debug) console.log("socket disconnected", socket.id)
|
|
40
57
|
for (const func of disconnectListeners) {
|
|
41
58
|
func(socket)
|
|
42
59
|
}
|
|
@@ -133,7 +150,7 @@ export default function expressXClient(socket, options={}) {
|
|
|
133
150
|
return new Proxy(service, handler)
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
|
|
153
|
+
//-------------------- APPLICATION-LEVEL EVENTS --------------------
|
|
137
154
|
|
|
138
155
|
// There is a need for application-wide events sent outside any service method call, for example when backend state changes
|
|
139
156
|
// without front-end interactions
|
|
@@ -141,17 +158,16 @@ export default function expressXClient(socket, options={}) {
|
|
|
141
158
|
if (options.debug) console.log('app-event', type, value)
|
|
142
159
|
if (!type2appHandler[type]) type2appHandler[type] = {}
|
|
143
160
|
const handler = type2appHandler[type]
|
|
144
|
-
console.log('handler', handler)
|
|
145
161
|
if (handler) handler(value)
|
|
146
162
|
})
|
|
147
163
|
|
|
148
164
|
// add a handler for application-wide events
|
|
149
165
|
function on(type, handler) {
|
|
150
166
|
type2appHandler[type] = handler
|
|
151
|
-
console.log('type2appHandler[type]', type2appHandler[type])
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
|
|
169
|
+
const app = {
|
|
170
|
+
configure,
|
|
155
171
|
addConnectListener,
|
|
156
172
|
removeConnectListener,
|
|
157
173
|
addDisconnectListener,
|
|
@@ -162,4 +178,480 @@ export default function expressXClient(socket, options={}) {
|
|
|
162
178
|
service,
|
|
163
179
|
on,
|
|
164
180
|
}
|
|
181
|
+
|
|
182
|
+
return app
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
////////////////////////// RELOAD PLUGIN //////////////////////////
|
|
187
|
+
// enrich `app` with listeners handling socket data transfer on page reload
|
|
188
|
+
|
|
189
|
+
export async function reloadPlugin(app) {
|
|
190
|
+
|
|
191
|
+
const cnxid = useSessionStorage('cnxid', '')
|
|
192
|
+
|
|
193
|
+
app.addConnectListener(async (socket) => {
|
|
194
|
+
const socketId = socket.id
|
|
195
|
+
console.log('connect', socketId)
|
|
196
|
+
// handle reconnections & reloads
|
|
197
|
+
// look for a previously stored connection id
|
|
198
|
+
const prevSocketId = cnxid.value
|
|
199
|
+
if (prevSocketId) {
|
|
200
|
+
// it's a connection after a reload/refresh
|
|
201
|
+
// ask server to transfer all data from connection `prevSocketId` to connection `socketId`
|
|
202
|
+
console.log('cnx-transfer', prevSocketId, 'to', socketId)
|
|
203
|
+
await socket.emit('cnx-transfer', prevSocketId, socketId)
|
|
204
|
+
// update connection id
|
|
205
|
+
cnxid.value = socketId
|
|
206
|
+
|
|
207
|
+
} else {
|
|
208
|
+
// set connection id
|
|
209
|
+
cnxid.value = socketId
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
socket.on('cnx-transfer-ack', async (fromSocketId, toSocketId) => {
|
|
213
|
+
console.log('ACK ACK!!!', fromSocketId, toSocketId)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
socket.on('cnx-transfer-error', async (fromSocketId, toSocketId) => {
|
|
217
|
+
console.log('ERR ERR!!!', fromSocketId, toSocketId)
|
|
218
|
+
// appState.value.unrecoverableError = true
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
////////////////////////// OFFLINE PLUGIN //////////////////////////
|
|
225
|
+
// enrich `app` with methods, attributes and listeners to handle offline-first database access
|
|
226
|
+
|
|
227
|
+
export function offlinePlugin(app) {
|
|
228
|
+
|
|
229
|
+
function createOfflineModel(modelName, fields) {
|
|
230
|
+
|
|
231
|
+
const dbName = modelName;
|
|
232
|
+
const db = getOrCreateDB(dbName, fields);
|
|
233
|
+
|
|
234
|
+
db.open().then(() => {
|
|
235
|
+
// console.log('db ready', dbName, modelName)
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
db.values.hook("updating", (changes, primaryKey, previousValue) => {
|
|
239
|
+
// console.log("CHANGES", primaryKey, changes, previousValue);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const reset = async () => {
|
|
243
|
+
console.log('reset', modelName);
|
|
244
|
+
await db.whereList.clear();
|
|
245
|
+
await db.values.clear();
|
|
246
|
+
await db.metadata.clear();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
///////////// PUB / SUB /////////////
|
|
251
|
+
|
|
252
|
+
app.service(modelName).on('createWithMeta', async ([value, meta]) => {
|
|
253
|
+
console.log(`${modelName} EVENT createWithMeta`, value);
|
|
254
|
+
await db.values.put(value);
|
|
255
|
+
await db.metadata.put(meta);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
app.service(modelName).on('updateWithMeta', async ([value, meta]) => {
|
|
259
|
+
console.log(`${modelName} EVENT updateWithMeta`, value);
|
|
260
|
+
await db.values.put(value);
|
|
261
|
+
await db.metadata.put(meta);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
app.service(modelName).on('deleteWithMeta', async ([value, meta]) => {
|
|
265
|
+
console.log(`${modelName} EVENT deleteWithMeta`, value)
|
|
266
|
+
await db.values.delete(value.uid)
|
|
267
|
+
await db.metadata.put(meta)
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
///////////// CREATE/UPDATE/REMOVE /////////////
|
|
272
|
+
|
|
273
|
+
async function create(data) {
|
|
274
|
+
const uid = uuidv7()
|
|
275
|
+
// optimistic update
|
|
276
|
+
const now = new Date()
|
|
277
|
+
await db.values.add({ uid, ...data })
|
|
278
|
+
await db.metadata.add({ uid, created_at: now })
|
|
279
|
+
// execute on server, asynchronously, if connection is active
|
|
280
|
+
if (app.isConnected) {
|
|
281
|
+
app.service(modelName).createWithMeta(uid, data, now)
|
|
282
|
+
.catch(async err => {
|
|
283
|
+
console.log(`*** err sync ${modelName} create`, err)
|
|
284
|
+
// rollback
|
|
285
|
+
await db.values.delete(uid)
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
return await db.values.get(uid)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const update = async (uid: string, data: object) => {
|
|
292
|
+
const previousValue = { ...(await db.values.get(uid)) }
|
|
293
|
+
const previousMetadata = { ...(await db.metadata.get(uid)) }
|
|
294
|
+
// optimistic update of cache
|
|
295
|
+
const now = new Date()
|
|
296
|
+
await db.values.update(uid, data)
|
|
297
|
+
await db.metadata.update(uid, { updated_at: now })
|
|
298
|
+
// execute on server, asynchronously, if connection is active
|
|
299
|
+
if (app.isConnected) {
|
|
300
|
+
app.service(modelName).updateWithMeta(uid, data, now)
|
|
301
|
+
.catch(async err => {
|
|
302
|
+
console.log(`*** err sync ${modelName} update`, err)
|
|
303
|
+
// rollback
|
|
304
|
+
delete previousValue.uid
|
|
305
|
+
await db.values.update(uid, previousValue)
|
|
306
|
+
delete previousMetadata.uid
|
|
307
|
+
await db.metadata.update(uid, previousMetadata)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
return await db.values.get(uid)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const remove = async (uid: string) => {
|
|
314
|
+
const deleted_at = new Date()
|
|
315
|
+
// optimistic delete in cache
|
|
316
|
+
await db.values.update(uid, { __deleted__: true })
|
|
317
|
+
await db.metadata.update(uid, { deleted_at })
|
|
318
|
+
// and in database, if connected
|
|
319
|
+
if (app.isConnected) {
|
|
320
|
+
app.service(modelName).deleteWithMeta(uid, deleted_at)
|
|
321
|
+
.catch(async err => {
|
|
322
|
+
console.log(`*** err sync ${modelName} remove`, err)
|
|
323
|
+
// rollback
|
|
324
|
+
await db.values.update(uid, { __deleted__: null })
|
|
325
|
+
await db.metadata.update(uid, { deleted_at: null })
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
///////////// DIRECT CACHE ACCESS /////////////
|
|
331
|
+
|
|
332
|
+
function findByUID(uid) {
|
|
333
|
+
return db.values.get(uid)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function findWhere(where = {}) {
|
|
337
|
+
const predicate = wherePredicate(where)
|
|
338
|
+
return db.values.filter(value => !value.__deleted__ && predicate(value)).toArray()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
///////////// REAL-TIME OBSERVABLE /////////////
|
|
342
|
+
|
|
343
|
+
function getObservable(where = {}) {
|
|
344
|
+
addSynchroWhere(where).then((isNew: boolean) => {
|
|
345
|
+
// console.log('getObservable addSynchroWhere', modelName, where, isNew);
|
|
346
|
+
if (isNew && app.isConnected) {
|
|
347
|
+
synchronize(modelName, db.values, db.metadata, where, app.disconnectedDate)
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
const predicate = wherePredicate(where)
|
|
351
|
+
return from(liveQuery(() => db.values.filter(value => !value.__deleted__ && predicate(value)).toArray())).pipe(
|
|
352
|
+
distinctUntilChanged((prev, curr) => {
|
|
353
|
+
// Deep equality check to prevent unnecessary emissions (in particular on database write)
|
|
354
|
+
return JSON.stringify(prev) === JSON.stringify(curr)
|
|
355
|
+
})
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let count = 0;
|
|
360
|
+
|
|
361
|
+
function addSynchroWhere(where: object) {
|
|
362
|
+
const promise = addSynchroDBWhere(where, db.whereList)
|
|
363
|
+
promise.then(isNew => isNew && count++ && console.log(`addSynchroWhere (${count})`, dbName, modelName, where))
|
|
364
|
+
return promise
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function removeSynchroWhere(where: object) {
|
|
368
|
+
console.log('removeSynchroWhere', dbName, modelName, where)
|
|
369
|
+
count -= 1
|
|
370
|
+
return removeSynchroDBWhere(where, db.whereList)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// async function synchronizeAll() {
|
|
374
|
+
// await synchronizeModelWhereList(app, modelName, db.values, db.metadata, app.getDisconnectedDate(), db.whereList)
|
|
375
|
+
// }
|
|
376
|
+
|
|
377
|
+
// Automatically clean up when the component using this composable unmounts
|
|
378
|
+
tryOnScopeDispose(async () => {
|
|
379
|
+
console.log('CLEANING', dbName, modelName)
|
|
380
|
+
const whereList = await db.whereList.toArray()
|
|
381
|
+
for (const where of whereList) {
|
|
382
|
+
removeSynchroWhere(JSON.parse(where.sortedjson))
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
db, reset,
|
|
388
|
+
create, update, remove,
|
|
389
|
+
findByUID, findWhere,
|
|
390
|
+
getObservable,
|
|
391
|
+
// synchronizeAll,
|
|
392
|
+
addSynchroWhere,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
app.addConnectListener(async (_socket) => {
|
|
397
|
+
app.connectedDate = new Date()
|
|
398
|
+
console.log('onConnect', app.connectedDate)
|
|
399
|
+
app.disconnectedDate = null
|
|
400
|
+
app.isConnected = true
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
app.addDisconnectListener(async (_socket) => {
|
|
404
|
+
app.connectedDate = null
|
|
405
|
+
app.disconnectedDate = new Date()
|
|
406
|
+
console.log('onDisconnect', app.disconnectedDate)
|
|
407
|
+
app.isConnected = false
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
const mutex = new Mutex()
|
|
412
|
+
|
|
413
|
+
// ex: where = { uid: 'azer' }
|
|
414
|
+
async function synchronize(app, modelName, idbValues, idbMetadata, where, cutoffDate) {
|
|
415
|
+
await mutex.acquire()
|
|
416
|
+
console.log('synchronize', modelName, where)
|
|
417
|
+
|
|
418
|
+
let toAdd = []
|
|
419
|
+
try {
|
|
420
|
+
const requestPredicate = wherePredicate(where)
|
|
421
|
+
|
|
422
|
+
// collect meta-data of local values
|
|
423
|
+
// NOTE: __delete__ on values allows to collect metadata from cache-deleted values
|
|
424
|
+
const valueList = await idbValues.filter(requestPredicate).toArray()
|
|
425
|
+
const clientMetadataDict = {}
|
|
426
|
+
for (const value of valueList) {
|
|
427
|
+
const metadata = await idbMetadata.get(value.uid)
|
|
428
|
+
if (metadata) {
|
|
429
|
+
clientMetadataDict[value.uid] = metadata
|
|
430
|
+
} else {
|
|
431
|
+
// should not happen
|
|
432
|
+
clientMetadataDict[value.uid] = {}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// call sync service on `where` perimeter
|
|
437
|
+
const syncResult = await app.service('sync').go(modelName, where, cutoffDate, clientMetadataDict)
|
|
438
|
+
toAdd = syncResult.toAdd
|
|
439
|
+
const { toUpdate, toDelete, addDatabase, updateDatabase } = syncResult
|
|
440
|
+
console.log('-> service.sync', modelName, where, toAdd, toUpdate, toDelete, addDatabase, updateDatabase)
|
|
441
|
+
|
|
442
|
+
// 1- add missing elements in indexedDB cache
|
|
443
|
+
// Use a single transaction for all adds to ensure atomicity
|
|
444
|
+
if (toAdd.length > 0) {
|
|
445
|
+
await idbValues.db.transaction('rw', [idbValues, idbMetadata], async () => {
|
|
446
|
+
for (const [value, metaData] of toAdd) {
|
|
447
|
+
await idbValues.add(value)
|
|
448
|
+
await idbMetadata.add(metaData)
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
// 2- delete elements from indexedDB cache
|
|
453
|
+
for (const [uid, deleted_at] of toDelete) {
|
|
454
|
+
await idbValues.delete(uid)
|
|
455
|
+
await idbMetadata.update(uid, { deleted_at })
|
|
456
|
+
}
|
|
457
|
+
// 3- update elements of cache
|
|
458
|
+
for (const elt of toUpdate) {
|
|
459
|
+
// get full value of element to update
|
|
460
|
+
const value = await app.service(modelName).findUnique({ where:{ uid: elt.uid }})
|
|
461
|
+
delete value.uid
|
|
462
|
+
delete value.__deleted__
|
|
463
|
+
await idbValues.update(elt.uid, value)
|
|
464
|
+
const metadata = await idbMetadata.get(elt.uid)
|
|
465
|
+
await idbMetadata.update(elt.uid, { updated_at: metadata.updated_at })
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 4- create elements of `addDatabase` with full data from cache
|
|
469
|
+
for (const elt of addDatabase) {
|
|
470
|
+
const fullValue = await idbValues.get(elt.uid)
|
|
471
|
+
const meta = await idbMetadata.get(elt.uid)
|
|
472
|
+
delete fullValue.uid
|
|
473
|
+
delete fullValue.__deleted__
|
|
474
|
+
try {
|
|
475
|
+
await app.service(modelName).createWithMeta(elt.uid, fullValue, meta.created_at)
|
|
476
|
+
} catch(err) {
|
|
477
|
+
console.log("*** err sync user addDatabase", err, elt.uid, fullValue, meta.created_at)
|
|
478
|
+
// rollback
|
|
479
|
+
await idbValues.delete(elt.uid)
|
|
480
|
+
await idbMetadata.delete(elt.uid)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 5- update elements of `updateDatabase` with full data from cache
|
|
485
|
+
for (const elt of updateDatabase) {
|
|
486
|
+
const fullValue = await idbValues.get(elt.uid)
|
|
487
|
+
const meta = await idbMetadata.get(elt.uid)
|
|
488
|
+
delete fullValue.uid
|
|
489
|
+
delete fullValue.__deleted__
|
|
490
|
+
try {
|
|
491
|
+
await app.service(modelName).updateWithMeta(elt.uid, fullValue, meta.updated_at)
|
|
492
|
+
} catch(err) {
|
|
493
|
+
console.log("*** err sync user updateDatabase", err)
|
|
494
|
+
// rollback
|
|
495
|
+
const previousDatabaseValue = await app.service(modelName).findUnique({ where:{ uid: elt.uid }})
|
|
496
|
+
const previousDatabaseMetadata = await app.service('metadata').findUnique({ where:{ uid: elt.uid }})
|
|
497
|
+
await idbValues.update(elt.uid, previousDatabaseValue)
|
|
498
|
+
await idbMetadata.update(elt.uid, previousDatabaseMetadata)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch(err) {
|
|
502
|
+
console.log('err synchronize', modelName, where, err)
|
|
503
|
+
} finally {
|
|
504
|
+
mutex.release()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function wherePredicate(where) {
|
|
509
|
+
return (elt) => {
|
|
510
|
+
for (const [attr, value] of Object.entries(where)) {
|
|
511
|
+
const eltAttrValue = elt[attr]
|
|
512
|
+
|
|
513
|
+
if (typeof(value) === 'string' || typeof(value) === 'number') {
|
|
514
|
+
// 'attr = value' clause
|
|
515
|
+
if (eltAttrValue !== value) return false
|
|
516
|
+
|
|
517
|
+
} else if (typeof(value) === 'object') {
|
|
518
|
+
// 'attr = { lt/lte/gt/gte: value }' clause
|
|
519
|
+
if (value.lte) {
|
|
520
|
+
if (eltAttrValue > value.lte) return false
|
|
521
|
+
} else if (value.lt) {
|
|
522
|
+
if (eltAttrValue >= value.lt) return false
|
|
523
|
+
} else if (value.gte) {
|
|
524
|
+
if (eltAttrValue < value.gte) return false
|
|
525
|
+
} else if (value.gt) {
|
|
526
|
+
if (eltAttrValue <= value.gt) return false
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return true
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function getWhereList(whereDb) {
|
|
535
|
+
const list = await whereDb.toArray()
|
|
536
|
+
return list.map(elt => JSON.parse(elt.sortedjson))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function addSynchroDBWhere(where, whereDb) {
|
|
540
|
+
await mutex.acquire()
|
|
541
|
+
let modified = false
|
|
542
|
+
try {
|
|
543
|
+
const whereList = await getWhereList(whereDb)
|
|
544
|
+
if (!isSubsetAmong(where, whereList)) {
|
|
545
|
+
// sortedjson is used as a unique standardized representation of a 'where' object ; it is used both as key and value in 'wheredb' database
|
|
546
|
+
await whereDb.add({ sortedjson: stringifyWithSortedKeys(where) })
|
|
547
|
+
modified = true
|
|
548
|
+
}
|
|
549
|
+
} catch(err) {
|
|
550
|
+
console.log('err addSynchroDBWhere', where, err)
|
|
551
|
+
} finally {
|
|
552
|
+
mutex.release()
|
|
553
|
+
}
|
|
554
|
+
return modified
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function removeSynchroDBWhere(where, whereDb) {
|
|
558
|
+
await mutex.acquire()
|
|
559
|
+
try {
|
|
560
|
+
const swhere = stringifyWithSortedKeys(where)
|
|
561
|
+
await whereDb.filter(value => (value.sortedjson === swhere)).delete()
|
|
562
|
+
} catch(err) {
|
|
563
|
+
console.log('err removeSynchroDBWhere', err)
|
|
564
|
+
} finally {
|
|
565
|
+
mutex.release()
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// async function synchronizeModelWhereList(modelName, idbValues, idbMetadata, cutoffDate, whereDb) {
|
|
570
|
+
// const whereList = await getWhereList(whereDb)
|
|
571
|
+
// for (const where of whereList) {
|
|
572
|
+
// await synchronize(modelName, idbValues, idbMetadata, where, cutoffDate)
|
|
573
|
+
// }
|
|
574
|
+
// }
|
|
575
|
+
|
|
576
|
+
// Singleton map to reuse Dexie instances per database name
|
|
577
|
+
const dbInstances = new Map();
|
|
578
|
+
|
|
579
|
+
function getOrCreateDB(dbName: string, fields: string[]) {
|
|
580
|
+
if (!dbInstances.has(dbName)) {
|
|
581
|
+
const db = new Dexie(dbName);
|
|
582
|
+
db.version(1).stores({
|
|
583
|
+
whereList: "sortedjson",
|
|
584
|
+
values: ['uid', '__deleted__', ...fields].join(','),
|
|
585
|
+
metadata: "uid, created_at, updated_at, deleted_at",
|
|
586
|
+
});
|
|
587
|
+
dbInstances.set(dbName, db);
|
|
588
|
+
}
|
|
589
|
+
return dbInstances.get(dbName);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// enrich `app` with new methods and attributes
|
|
593
|
+
return Object.assign(app, {
|
|
594
|
+
createOfflineModel,
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
function stringifyWithSortedKeys(obj, space = null) {
|
|
600
|
+
return JSON.stringify(obj, (key, value) => {
|
|
601
|
+
// If the value is a plain object (not an array, null, or other object type like Date)
|
|
602
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && Object.prototype.toString.call(value) === '[object Object]') {
|
|
603
|
+
const sorted = {};
|
|
604
|
+
// Get all keys, sort them, and then re-add them to a new object
|
|
605
|
+
// This new object will maintain the sorted order when stringified
|
|
606
|
+
Object.keys(value).sort().forEach(k => {
|
|
607
|
+
sorted[k] = value[k];
|
|
608
|
+
});
|
|
609
|
+
return sorted;
|
|
610
|
+
}
|
|
611
|
+
// For all other types (primitives, arrays, null, etc.), return the value as is
|
|
612
|
+
return value;
|
|
613
|
+
}, space); // 'space' is optional for pretty-printing (e.g., 2 or 4)
|
|
165
614
|
}
|
|
615
|
+
// console.log('stringifyWithSortedKeys({ age: 30, name: "Alice", data: { city: "Paris", color: "red" }})', stringifyWithSortedKeys({ age: 30, name: "Alice", data: { city: "Paris", color: "red" } }))
|
|
616
|
+
|
|
617
|
+
export class Mutex {
|
|
618
|
+
constructor() {
|
|
619
|
+
this.locked = false;
|
|
620
|
+
this.queue = [];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async acquire() {
|
|
624
|
+
if (this.locked) {
|
|
625
|
+
return new Promise(resolve => this.queue.push(resolve));
|
|
626
|
+
}
|
|
627
|
+
this.locked = true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
release() {
|
|
631
|
+
if (this.queue.length > 0) {
|
|
632
|
+
const next = this.queue.shift();
|
|
633
|
+
next();
|
|
634
|
+
} else {
|
|
635
|
+
this.locked = false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isSubset(subset, fullObject) {
|
|
641
|
+
// return Object.entries(subset).some(([key, value]) => fullObject[key] === value)
|
|
642
|
+
for (const key in fullObject) {
|
|
643
|
+
if (fullObject[key] !== subset[key]) return false
|
|
644
|
+
}
|
|
645
|
+
return true
|
|
646
|
+
}
|
|
647
|
+
// console.log('isSubset({a: 1, b: 2}, {b: 2})=true', isSubset({a: 1, b: 2}, {b: 2}))
|
|
648
|
+
// console.log('isSubset({}, {})=true', isSubset({}, {}))
|
|
649
|
+
// console.log('isSubset({a: 1}, {})=true', isSubset({a: 1}, {}))
|
|
650
|
+
// console.log('isSubset({a: 1}, {b: 2})=false', isSubset({a: 1}, {b: 2}))
|
|
651
|
+
// console.log('isSubset({a: 1}, {a: 1})=true', isSubset({a: 1}, {a: 1}))
|
|
652
|
+
|
|
653
|
+
function isSubsetAmong(subset, fullObjectList) {
|
|
654
|
+
return fullObjectList.some(fullObject => isSubset(subset, fullObject));
|
|
655
|
+
}
|
|
656
|
+
// console.log('isSubsetAmong({a: 1, b: 2}, [{c: 3}, {b: 2}])=true', isSubsetAmong({a: 1, b: 2}, [{c: 3}, {b: 2}]))
|
|
657
|
+
|
package/.vscode/launch.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
// Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
|
|
3
|
-
// Pointez pour afficher la description des attributs existants.
|
|
4
|
-
// Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
-
"version": "0.2.0",
|
|
6
|
-
"configurations": [
|
|
7
|
-
{
|
|
8
|
-
"type": "node",
|
|
9
|
-
"request": "launch",
|
|
10
|
-
"name": "test",
|
|
11
|
-
"skipFiles": [
|
|
12
|
-
"<node_internals>/**"
|
|
13
|
-
],
|
|
14
|
-
"cwd": "${workspaceFolder}",
|
|
15
|
-
"program": "${workspaceFolder}/test/index.test.js"
|
|
16
|
-
}
|
|
17
|
-
]
|
|
18
|
-
}
|