@jcbuisson/express-x 3.0.4 → 3.0.5

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
@@ -12,98 +12,88 @@ Core concept: ExpressX is a thin wrapper around Express.js and Socket.io that pr
12
12
  **Documentation**: https://expressx.jcbuisson.dev
13
13
  **Version**: 3.0.3 (ES modules)
14
14
 
15
- ## Architecture
15
+ ## Development Commands
16
16
 
17
- ### Single Entry Point
18
- - `/src/server.mjs` (625 lines) is the complete framework implementation. This is the only source file.
19
- - Exports the main `expressX()` factory function and various utilities/plugins.
17
+ ```bash
18
+ # Install dependencies
19
+ npm install
20
20
 
21
- ### Core Concepts
21
+ # Run integration tests (round-trip + offline sync via real sockets/PGlite/Dexie)
22
+ npm test
22
23
 
23
- 1. **Services**: Named groups of callable methods. Created via `app.createService(name, methods)`. Each method receives a `context` object containing metadata about the call (transport, socket, service/method names, args).
24
+ # Run unit tests for the sync algorithm only
25
+ node --import tsx/esm --test test/sync.test.mjs
24
26
 
25
- 2. **Context Object**: Contains execution metadata:
26
- - `app`, `socket`, `serviceName`, `methodName`, `args`, `result`
27
- - `caller` ('client' or 'server'), `transport` ('ws')
28
- - Modified by hooks before/after method execution
27
+ # Run both test files
28
+ node --import tsx/esm --test --test-force-exit test/round-trip.test.mjs test/sync.test.mjs
29
29
 
30
- 3. **Hooks**: Pre/post-execution functions that can modify context or validate requests. Attached to services via `service.hooks()`. Support method-specific and "all methods" variants:
31
- ```js
32
- service.hooks({
33
- before: { methodName: [hook1, hook2], all: [globalHook] },
34
- after: { methodName: [hook], all: [globalHook] }
35
- })
36
- ```
30
+ # Run a single named test (partial match on test name)
31
+ node --import tsx/esm --test --test-force-exit --test-name-pattern="service call" test/round-trip.test.mjs
32
+ ```
37
33
 
38
- 4. **Channels & Pub/Sub**: Enable real-time broadcasting:
39
- - Channels are Socket.io rooms with names
40
- - Services attach a `publishFunction` via `service.publish()` that determines which channels receive events after method execution
41
- - Clients join/subscribe to channels via `app.joinChannel(name, socket)`
42
- - Events are published as `service-event` messages containing `{name, action, result}`
34
+ No build step the source is run directly as ES modules.
43
35
 
44
- 5. **WebSocket Protocol**: Single `client-request` event from client → `client-response` event from server, with uid-based request/response correlation. Errors are serialized and sent back to client.
36
+ ## Architecture
45
37
 
46
- ### Key Plugins
38
+ ### Single Source File
39
+ - `src/server.mjs` is the complete server-side framework. All exports live here.
47
40
 
48
- - **reloadPlugin**: Manages connection recovery across page reloads/reconnects by caching socket data and rooms.
49
- - **offlinePlugin**: Implements offline-first CRUD operations with Prisma. Provides `sync.go()` service method that reconciles client cache with database using timestamps (created_at, updated_at, deleted_at).
41
+ ### Related Packages (also authored here, published separately)
42
+ - **`@jcbuisson/express-x-client`** (`node_modules/@jcbuisson/express-x-client/src/client.mts`) client-side library: `createClient`, `offlinePlugin`, `Mutex`, `wherePredicate`. TypeScript source consumed directly via `tsx`.
43
+ - **`@jcbuisson/express-x-drizzle`** (`node_modules/@jcbuisson/express-x-drizzle/src/drizzle-plugins.mjs`) — Drizzle ORM variant of the offline plugin. The installed version may lag behind local development; the authoritative Drizzle plugin logic for tests is wired via `serverApp.configure(drizzleOfflinePlugin, ...)`.
50
44
 
51
- ### Common Hook Patterns (Exported Utilities)
52
- - `addTimestamp(field)`: Adds ISO timestamp to result
53
- - `hashPassword(field)`: Hashes password using bcryptjs
54
- - `protect(field)`: Removes sensitive fields from results
45
+ ### Core Concepts
55
46
 
56
- ### Error Handling
57
- - `EXError(code, message)`: Custom error class with error codes. Method errors are caught, serialized, and sent to client as rejected promises.
47
+ 1. **Services**: Named groups of callable methods. Created via `app.createService(name, methods)`. Each method is wrapped to support hooks and pub/sub.
58
48
 
59
- ## Development Commands
49
+ 2. **Context Object**: Passed to every hooked method — contains `app`, `socket`, `serviceName`, `methodName`, `args`, `result`, `caller` ('client'|'server'), `transport` ('ws').
60
50
 
61
- ```bash
62
- # Install dependencies
63
- npm install
51
+ 3. **Hooks**: Pre/post-execution filters attached via `service.hooks({ before: { methodName: [...], all: [...] }, after: {...} })`. Hooks run: `before.all` → `before.method` → method → `after.method` → `after.all`.
64
52
 
65
- # Prisma (optional, if using database)
66
- npm run generate # Generate Prisma client
67
- npm run migrate # Run Prisma migrations
53
+ 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)`.
68
54
 
69
- # No dedicated test, build, or lint scripts
70
- # Manual testing via client-server examples in README
71
- ```
55
+ 5. **WebSocket Protocol**: Client emits `client-request {uid, name, action, args}` → server emits `client-response {uid, result|error}`. Requests are correlated by `uid`.
72
56
 
73
- ## Key Dependencies
74
- - **express** (^4.18.2): HTTP server framework
75
- - **socket.io** (^4.6.0): WebSocket server
76
- - **bcryptjs** (^2.4.3): Password hashing
77
- - **config** (^3.3.9): Configuration management
78
- - Peer: Prisma for database CRUD services (optional)
57
+ ### Offline Sync Architecture
79
58
 
80
- ## File Structure
81
- ```
82
- express-x/
83
- src/server.mjs # Complete framework (625 lines)
84
- package.json # v3.0.3, ES module type
85
- README.md # Getting started + examples
86
- .vscode/launch.json # Debug config
87
- ```
59
+ The sync system reconciles a client Dexie cache with a server database. The protocol:
88
60
 
89
- ## Implementation Notes
61
+ 1. Client calls `sync.go(modelName, where, cutoffDate, clientMetadataDict)` with its local metadata snapshot.
62
+ 2. Server computes diff using `computeSyncResult` (exported from `server.mjs`, pure function, no I/O).
63
+ 3. Server returns `{ addClient, updateClient, deleteClient, addDatabase, updateDatabase }`.
64
+ 4. Client applies each batch: puts addClient records into Dexie, deletes deleteClient, updates updateClient, then calls `createWithMeta`/`updateWithMeta` for addDatabase/updateDatabase.
90
65
 
91
- 1. **Context Execution**: Service methods are wrapped with hook execution:
92
- - All `before.all` hooks → method-specific `before` hooks method execution → method-specific `after` hooks → all `after.all` hooks
93
- - Hooks receive context and can throw errors (which propagate to client)
66
+ **Two offline plugin implementations:**
67
+ - `offlinePlugin` in `server.mjs` Prisma-based (legacy, uses `prisma.$transaction`).
68
+ - `drizzleOfflinePlugin` from `@jcbuisson/express-x-drizzle` Drizzle ORM-based, used in all tests.
94
69
 
95
- 2. **Event Publishing**: After a service method completes, `publishFunction` is called with the context. It returns an array of channel names. The result is broadcast to all sockets in those channels as a `service-event`.
70
+ **`computeSyncResult(databaseValuesDict, clientMetadataDict, databaseMetadataDict)`** is the pure sync algorithm exported from `server.mjs`. It uses `null` (not `new Date()`) as the fallback `created_at` for missing metadata, so a record without metadata is treated as older than any client record, preventing infinite `updateClient` loops.
96
71
 
97
- 3. **Connection State**: Socket.io's built-in `socket.data` object stores per-connection state. Socket.io's connection recovery feature (2-minute default) restores socket.id, rooms, and data on network reconnects.
72
+ ### Key Plugins
73
+ - **reloadPlugin**: Caches socket data/rooms on disconnect; restores them on reconnect via `cnx-transfer` socket event.
74
+ - **offlinePlugin (Prisma)**: CRUD services + `sync.go` for offline-first with Prisma.
75
+ - **drizzleOfflinePlugin**: Same pattern using Drizzle ORM + PGlite-compatible transactions.
98
76
 
99
- 4. **Offline Sync**: The `offlinePlugin` uses transaction-safe Prisma operations and a mutex to ensure consistent synchronization. Clients track operation timestamps and the sync algorithm compares created_at/updated_at to determine add/update/delete operations.
77
+ ### Exported Utilities
78
+ - `Mutex` — simple async mutex used by sync and offline plugins.
79
+ - `truncateString` — log truncation helper.
80
+ - `addTimestamp(field)`, `hashPassword(field)`, `protect(field)` — common hook factories.
81
+ - `EXError(code, message)` — custom error class; `code` is serialized and sent to the client.
82
+ - `computeSyncResult` — pure sync diff algorithm.
100
83
 
101
- 5. **Logging**: App-level logging via `app.log(severity, message)`. If no logger is configured, falls back to console.log.
84
+ ## Test Structure
85
+
86
+ - `test/round-trip.test.mjs` — Integration tests. Spins up a real Express/Socket.io server on a random port, connects via `socket.io-client`, uses PGlite (in-memory Postgres) + Drizzle + Dexie (via `fake-indexeddb`). Tests cover the full client↔server sync protocol, pub/sub, rollbacks, and edge cases. Each test gets an isolated PGlite instance and unique model name.
87
+ - `test/sync.test.mjs` — Unit tests for `computeSyncResult` only. Imports from `#root/src/drizzle-plugins.mjs` (the `#root/*` alias maps to the repo root via `package.json` imports).
102
88
 
103
89
  ## Configuration
104
90
 
105
- ExpressX accepts an optional config object (first parameter to `expressX(config)`):
106
- - `config.WS_PATH` (default: '/socket.io/'): Socket.io path
91
+ `expressX(config)` accepts:
92
+ - `config.WS_PATH` (default: `'/socket.io/'`): Socket.io path
107
93
  - `config.maxDisconnectionDuration` (default: 2 min): Connection recovery window
108
- - Any config is accessible server-side via `app.get('config')`
94
+ - Accessible server-side as `app.get('config')`
109
95
 
96
+ ## Key Dependencies
97
+ - **express** / **socket.io**: HTTP + WebSocket server
98
+ - **bcryptjs**: Password hashing (hook utility)
99
+ - Dev: **@electric-sql/pglite** (in-memory Postgres for tests), **drizzle-orm**, **fake-indexeddb** (Dexie in Node), **tsx** (runs `.mts` client source directly), **dexie**, **rxjs**, **uuid**, **@vueuse/core**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcbuisson/express-x",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
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.5",
33
+ "@jcbuisson/express-x-drizzle": "^1.0.7",
34
34
  "@vueuse/core": "^14.3.0",
35
35
  "dexie": "^4.4.2",
36
36
  "drizzle-orm": "^0.45.2",
@@ -89,7 +89,7 @@ async function createTestContext(registerServices, { useOfflinePlugin = false }
89
89
 
90
90
  // ─────────────────────────────────────────────────────────────────────────────
91
91
 
92
- describe('Full client ↔ server protocol', () => {
92
+ describe('Full offline-first client ↔ server protocol', () => {
93
93
 
94
94
  test('service call is routed through client-request / client-response', async () => {
95
95
  const { clientApp, cleanup } = await createTestContext(serverApp => {
@@ -1,11 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(git -C /Users/chris/workspaces/NPM/express-x log --oneline --all)",
5
- "Bash(git *)",
6
- "Bash(npm scripts *)",
7
- "Bash(npm test *)",
8
- "Bash(npm install *)"
9
- ]
10
- }
11
- }