@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
|
-
##
|
|
15
|
+
## Development Commands
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
```bash
|
|
18
|
+
# Install dependencies
|
|
19
|
+
npm install
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# Run integration tests (round-trip + offline sync via real sockets/PGlite/Dexie)
|
|
22
|
+
npm test
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
# Run unit tests for the sync algorithm only
|
|
25
|
+
node --import tsx/esm --test test/sync.test.mjs
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
## Architecture
|
|
45
37
|
|
|
46
|
-
###
|
|
38
|
+
### Single Source File
|
|
39
|
+
- `src/server.mjs` is the complete server-side framework. All exports live here.
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
2. **Context Object**: Passed to every hooked method — contains `app`, `socket`, `serviceName`, `methodName`, `args`, `result`, `caller` ('client'|'server'), `transport` ('ws').
|
|
60
50
|
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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 => {
|