@jcbuisson/express-x 3.1.0 → 3.1.1
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/CLAUDE.md +2 -3
- package/package.json +1 -1
- package/src/server.mjs +4 -190
package/CLAUDE.md
CHANGED
|
@@ -40,10 +40,9 @@ No build step — the source is run directly as ES modules.
|
|
|
40
40
|
|
|
41
41
|
### Source Files
|
|
42
42
|
- `src/server.mjs` — complete server-side framework; all exports live here.
|
|
43
|
-
- `src/client.mjs` — local mirror of the client-side library (`@jcbuisson/express-x-client`). Used for development and to track changes before publishing.
|
|
44
43
|
|
|
45
44
|
### Related Packages (also authored here, published separately)
|
|
46
|
-
- **`@jcbuisson/express-x-client`** (`node_modules/@jcbuisson/express-x-client/src/client.mts`) — published client library: `createClient`, `offlinePlugin`, `Mutex`, `wherePredicate`.
|
|
45
|
+
- **`@jcbuisson/express-x-client`** (`node_modules/@jcbuisson/express-x-client/src/client.mts`) — published client library: `createClient`, `offlinePlugin`, `Mutex`, `wherePredicate`.
|
|
47
46
|
- **`@jcbuisson/express-x-drizzle`** (`node_modules/@jcbuisson/express-x-drizzle/src/drizzle-plugins.mjs`) — Drizzle ORM offline plugin. The installed version may lag behind local development; tests wire it via `serverApp.configure(drizzleOfflinePlugin, ...)`.
|
|
48
47
|
|
|
49
48
|
### Core Concepts
|
|
@@ -56,7 +55,7 @@ No build step — the source is run directly as ES modules.
|
|
|
56
55
|
|
|
57
56
|
4. **Channels & Pub/Sub**: Socket.io rooms. After a service method completes, `service.publishFunction(context)` returns channel names; the result is broadcast as `service-event` to all sockets in those rooms. Clients subscribe with `app.service(name).on(action, handler)`.
|
|
58
57
|
|
|
59
|
-
5. **WebSocket Protocol**: Client calls `socket.timeout(ms).emitWithAck('client-request', { name, action, args })` → server handler receives `({ name, action, args }, ack)` and responds via `ack({ result })` or `ack({ error })`. Correlation is handled by Socket.io's built-in acknowledgment mechanism
|
|
58
|
+
5. **WebSocket Protocol**: Client calls `socket.timeout(ms).emitWithAck('client-request', { name, action, args })` → server handler receives `({ name, action, args }, ack)` and responds via `ack({ result })` or `ack({ error })`. Correlation is handled by Socket.io's built-in acknowledgment mechanism.
|
|
60
59
|
|
|
61
60
|
### Offline Sync Architecture
|
|
62
61
|
|
package/package.json
CHANGED
package/src/server.mjs
CHANGED
|
@@ -253,11 +253,11 @@ export function expressX(config) {
|
|
|
253
253
|
* create a service `name` with given `methods`
|
|
254
254
|
*/
|
|
255
255
|
function createService(name, methods) {
|
|
256
|
-
const service = {}
|
|
256
|
+
const service = { _name: name }
|
|
257
257
|
|
|
258
258
|
for (const methodName in methods) {
|
|
259
259
|
const method = methods[methodName]
|
|
260
|
-
if (!
|
|
260
|
+
if (!(method instanceof Function)) continue
|
|
261
261
|
|
|
262
262
|
// `context` is the context of execution (transport type, connection, app)
|
|
263
263
|
// `args` is the list of arguments of the method
|
|
@@ -442,7 +442,7 @@ export async function reloadPlugin(app) {
|
|
|
442
442
|
roomCache[socket.id] = new Set(socket.rooms)
|
|
443
443
|
|
|
444
444
|
if (alreadySavedData) dataCache[socket.id] = Object.assign(dataCache[socket.id], alreadySavedData)
|
|
445
|
-
if (alreadySavedRooms) roomCache[socket.id].add(
|
|
445
|
+
if (alreadySavedRooms) for (const room of alreadySavedRooms) roomCache[socket.id].add(room)
|
|
446
446
|
})
|
|
447
447
|
|
|
448
448
|
app.addConnectListener((socket) => {
|
|
@@ -474,194 +474,8 @@ export async function reloadPlugin(app) {
|
|
|
474
474
|
toSocket.emit('cnx-transfer-ack', fromSocketId, toSocketId)
|
|
475
475
|
} else {
|
|
476
476
|
console.log(`*** CNX TRANSFER ERROR, ${fromSocketId} -> ${toSocketId}`)
|
|
477
|
-
toSocket.emit('cnx-transfer-error', fromSocketId, toSocketId)
|
|
477
|
+
if (toSocket) toSocket.emit('cnx-transfer-error', fromSocketId, toSocketId)
|
|
478
478
|
}
|
|
479
479
|
})
|
|
480
480
|
})
|
|
481
481
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
////////////////////////// OFFLINE PLUGIN //////////////////////////
|
|
485
|
-
|
|
486
|
-
const mutex = new Mutex()
|
|
487
|
-
|
|
488
|
-
export function offlinePlugin(app, modelNames) {
|
|
489
|
-
|
|
490
|
-
const prisma = app.get('prisma');
|
|
491
|
-
|
|
492
|
-
if (!prisma.metadata) {
|
|
493
|
-
app.log('error', "A model named 'metadata' is expected in the prisma schema - see https://expressx.jcbuisson.dev/server-api.html#offline")
|
|
494
|
-
return
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// add a database service for each model
|
|
498
|
-
for (const modelName of modelNames) {
|
|
499
|
-
const model = prisma[modelName];
|
|
500
|
-
|
|
501
|
-
if (!model) {
|
|
502
|
-
app.log('error', `There is no model named '${modelName}}' in the prisma schema`)
|
|
503
|
-
return
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
app.createService(modelName, {
|
|
507
|
-
|
|
508
|
-
findUnique: model.findUnique,
|
|
509
|
-
findMany: model.findMany,
|
|
510
|
-
|
|
511
|
-
createWithMeta: async (uid, data, created_at) => {
|
|
512
|
-
const [value, meta] = await prisma.$transaction([
|
|
513
|
-
model.create({ data: { uid, ...data } }),
|
|
514
|
-
prisma.metadata.create({ data: { uid, created_at } })
|
|
515
|
-
])
|
|
516
|
-
return [value, meta]
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
updateWithMeta: async (uid, data, updated_at) => {
|
|
520
|
-
const [value, meta] = await prisma.$transaction([
|
|
521
|
-
model.update({ where: { uid }, data }),
|
|
522
|
-
prisma.metadata.update({ where: { uid }, data: { updated_at } })
|
|
523
|
-
])
|
|
524
|
-
return [value, meta]
|
|
525
|
-
},
|
|
526
|
-
|
|
527
|
-
deleteWithMeta: async (uid, deleted_at) => {
|
|
528
|
-
const [value, meta] = await prisma.$transaction([
|
|
529
|
-
model.delete({ where: { uid } }),
|
|
530
|
-
prisma.metadata.update({ where: { uid }, data: { deleted_at } })
|
|
531
|
-
])
|
|
532
|
-
return [value, meta]
|
|
533
|
-
},
|
|
534
|
-
})
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// add a synchronization service
|
|
538
|
-
app.createService('sync', {
|
|
539
|
-
|
|
540
|
-
// AMÉLIORER : ne pas avoir une exclusion mutuelle globale, mais seulement par model/where
|
|
541
|
-
go: async (modelName, where, cutoffDate, clientMetadataDict) => {
|
|
542
|
-
await mutex.acquire()
|
|
543
|
-
try {
|
|
544
|
-
console.log('>>>>> SYNC', modelName, where, cutoffDate)
|
|
545
|
-
const databaseService = app.service(modelName)
|
|
546
|
-
const prisma = app.get('prisma')
|
|
547
|
-
|
|
548
|
-
// STEP 1: get existing database `where` values
|
|
549
|
-
const databaseValues = await databaseService.findMany({ where })
|
|
550
|
-
|
|
551
|
-
const databaseValuesDict = databaseValues.reduce((accu, value) => {
|
|
552
|
-
accu[value.uid] = value
|
|
553
|
-
return accu
|
|
554
|
-
}, {})
|
|
555
|
-
// console.log('clientMetadataDict', clientMetadataDict)
|
|
556
|
-
// console.log('databaseValuesDict', databaseValuesDict)
|
|
557
|
-
|
|
558
|
-
// STEP 2: compute intersections between client and database uids
|
|
559
|
-
const onlyDatabaseIds = new Set()
|
|
560
|
-
const onlyClientIds = new Set()
|
|
561
|
-
const databaseAndClientIds = new Set()
|
|
562
|
-
|
|
563
|
-
for (const uid in databaseValuesDict) {
|
|
564
|
-
if (uid in clientMetadataDict) {
|
|
565
|
-
databaseAndClientIds.add(uid)
|
|
566
|
-
} else {
|
|
567
|
-
onlyDatabaseIds.add(uid)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
for (const uid in clientMetadataDict) {
|
|
572
|
-
if (uid in databaseValuesDict) {
|
|
573
|
-
databaseAndClientIds.add(uid)
|
|
574
|
-
} else {
|
|
575
|
-
onlyClientIds.add(uid)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// console.log('onlyDatabaseIds', onlyDatabaseIds)
|
|
579
|
-
// console.log('onlyClientIds', onlyClientIds)
|
|
580
|
-
// console.log('databaseAndClientIds', databaseAndClientIds)
|
|
581
|
-
|
|
582
|
-
// STEP 3: build add/update/delete sets
|
|
583
|
-
const addDatabase = []
|
|
584
|
-
const updateDatabase = []
|
|
585
|
-
const deleteDatabase = []
|
|
586
|
-
|
|
587
|
-
const addClient = []
|
|
588
|
-
const updateClient = []
|
|
589
|
-
const deleteClient = []
|
|
590
|
-
|
|
591
|
-
for (const uid of onlyDatabaseIds) {
|
|
592
|
-
const databaseValue = databaseValuesDict[uid]
|
|
593
|
-
let databaseMetaData = await prisma.metadata.findUnique({ where: { uid }})
|
|
594
|
-
if (!databaseMetaData) {
|
|
595
|
-
console.log('no metadata - should not happen', modelName, where, uid)
|
|
596
|
-
databaseMetaData = { uid, created_at: new Date() }
|
|
597
|
-
}
|
|
598
|
-
addClient.push([databaseValue, databaseMetaData])
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
for (const uid of onlyClientIds) {
|
|
602
|
-
const clientMetaData = clientMetadataDict[uid]
|
|
603
|
-
if (clientMetaData.deleted_at) {
|
|
604
|
-
deleteClient.push([uid, clientMetaData.deleted_at])
|
|
605
|
-
} else if (new Date(clientMetaData.created_at) > cutoffDate) {
|
|
606
|
-
addDatabase.push(clientMetaData)
|
|
607
|
-
} else {
|
|
608
|
-
// ???
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
for (const uid of databaseAndClientIds) {
|
|
613
|
-
const databaseValue = databaseValuesDict[uid]
|
|
614
|
-
const clientMetaData = clientMetadataDict[uid]
|
|
615
|
-
|| { uid, created_at: new Date() } // should not happen
|
|
616
|
-
if (clientMetaData.deleted_at) {
|
|
617
|
-
deleteDatabase.push(uid)
|
|
618
|
-
deleteClient.push([uid, clientMetaData.deleted_at])
|
|
619
|
-
} else {
|
|
620
|
-
const databaseMetaData = await prisma.metadata.findUnique({ where: { uid }})
|
|
621
|
-
|| { uid, created_at: new Date() } // should not happen
|
|
622
|
-
const clientUpdatedAt = new Date(clientMetaData.updated_at || clientMetaData.created_at)
|
|
623
|
-
const databaseUpdatedAt = new Date(databaseMetaData.updated_at || databaseMetaData.created_at)
|
|
624
|
-
const dateDifference = clientUpdatedAt - databaseUpdatedAt
|
|
625
|
-
// console.log('databaseMetaData', databaseMetaData, 'clientMetaData', clientMetaData, 'dateDifference', dateDifference)
|
|
626
|
-
if (dateDifference > 0) {
|
|
627
|
-
updateDatabase.push(clientMetaData)
|
|
628
|
-
} else if (dateDifference < 0) {
|
|
629
|
-
updateClient.push(databaseValue)
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
console.log('addDatabase', truncateString(JSON.stringify(addDatabase)))
|
|
634
|
-
console.log('deleteDatabase', truncateString(JSON.stringify(deleteDatabase)))
|
|
635
|
-
console.log('updateDatabase', truncateString(JSON.stringify(updateDatabase)))
|
|
636
|
-
|
|
637
|
-
console.log('addClient', truncateString(JSON.stringify(addClient)))
|
|
638
|
-
console.log('deleteClient', truncateString(JSON.stringify(deleteClient)))
|
|
639
|
-
console.log('updateClient', truncateString(JSON.stringify(updateClient)))
|
|
640
|
-
|
|
641
|
-
// STEP4: execute database deletions
|
|
642
|
-
for (const uid of deleteDatabase) {
|
|
643
|
-
const clientMetaData = clientMetadataDict[uid]
|
|
644
|
-
// console.log('---delete', uid, clientMetaData)
|
|
645
|
-
await databaseService.deleteWithMeta(uid, clientMetaData.deleted_at)
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// STEP5: return to client the changes to perform on its cache, and create/update to perform on database with full data
|
|
649
|
-
// database creations & updates are done later by the client with complete data (this function only has client values's meta-data)
|
|
650
|
-
return {
|
|
651
|
-
toAdd: addClient,
|
|
652
|
-
toUpdate: updateClient,
|
|
653
|
-
toDelete: deleteClient,
|
|
654
|
-
|
|
655
|
-
addDatabase,
|
|
656
|
-
updateDatabase,
|
|
657
|
-
}
|
|
658
|
-
} catch(err) {
|
|
659
|
-
console.log('*** err sync', err)
|
|
660
|
-
} finally {
|
|
661
|
-
mutex.release()
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
|