@jcbuisson/express-x 3.1.0 → 3.1.2

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 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`. TypeScript source consumed directly via `tsx`. `src/client.mjs` is the local development counterpart.
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 — no custom uid tracking.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "src/server.mjs",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@electric-sql/pglite": "^0.4.5",
32
32
  "@jcbuisson/express-x-client": "^3.0.5",
33
- "@jcbuisson/express-x-drizzle": "^1.0.7",
33
+ "@jcbuisson/express-x-drizzle": "^1.0.8",
34
34
  "@vueuse/core": "^14.3.0",
35
35
  "dexie": "^4.4.2",
36
36
  "drizzle-orm": "^0.45.2",
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 (! method instanceof Function) continue
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(alreadySavedRooms)
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
-