ocpp-ws-io 2.1.3 → 2.1.4
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/LICENSE +21 -0
- package/README.md +32 -6
- package/dist/adapters/redis.d.mts +1 -1
- package/dist/adapters/redis.d.ts +1 -1
- package/dist/adapters/redis.js +104 -0
- package/dist/adapters/redis.js.map +1 -1
- package/dist/adapters/redis.mjs +104 -0
- package/dist/adapters/redis.mjs.map +1 -1
- package/dist/browser.d.mts +16 -0
- package/dist/browser.d.ts +16 -0
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/{index-BixJj_yJ.d.mts → index-CagcFzyZ.d.mts} +111 -2
- package/dist/{index-BixJj_yJ.d.ts → index-CagcFzyZ.d.ts} +111 -2
- package/dist/index.d.mts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +396 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +395 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -27
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rohit Tiwari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Type-safe OCPP WebSocket RPC client & server for Node.js.**
|
|
4
4
|
>
|
|
5
|
-
> built with TypeScript — supports OCPP 1.6, 2.0.1, and 2.1 with full JSON schema validation, all security profiles, and
|
|
5
|
+
> built with TypeScript — supports OCPP 1.6, 2.0.1, and 2.1 with full JSON schema validation, all security profiles, clustering support, and blazing fast structured logging powered by [voltlog-io](https://ocpp-ws-io.rohittiwari.me/docs/voltlog-io).
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/ocpp-ws-io)
|
|
8
8
|
[](https://github.com/rohittiwari-dev/ocpp-ws-io/blob/main/LICENSE)
|
|
@@ -102,6 +102,13 @@ await server.listen(3000);
|
|
|
102
102
|
| `strictMode` | `boolean \| string[]` | `false` | Enable/restrict schema validation |
|
|
103
103
|
| `strictModeMethods` | `string[]` | — | Restrict validation to specific methods |
|
|
104
104
|
|
|
105
|
+
**Call Options**
|
|
106
|
+
When invoking `client.call()` you can safely decouple dynamically generated message IDs and pass your own deterministic keys:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
await client.call("ocpp1.6", "BootNotification", { ... }, { idempotencyKey: "unique-boot-123" });
|
|
110
|
+
```
|
|
111
|
+
|
|
105
112
|
### `OCPPServer` Options
|
|
106
113
|
|
|
107
114
|
| Option | Type | Default | Description |
|
|
@@ -113,6 +120,7 @@ await server.listen(3000);
|
|
|
113
120
|
| `logging` | `LoggingConfig` | `true` | Configure built-in logging |
|
|
114
121
|
| `sessionTtlMs` | `number` | `7200000` | Garbage collection inactivity timeout (ms) |
|
|
115
122
|
| `rateLimit` | `RateLimitOptions` | — | Token bucket socket & method rate-limiter |
|
|
123
|
+
| `healthEndpoint` | `boolean` | `false` | Expose HTTP `/health` and `/metrics` APIs |
|
|
116
124
|
|
|
117
125
|
## 🛠️ Advanced Server Configuration
|
|
118
126
|
|
|
@@ -151,9 +159,21 @@ Executes before the WebSocket connection is officially accepted.
|
|
|
151
159
|
|
|
152
160
|
Executes after the connection is accepted and messages start flowing. 4. **Message Middleware (`client.use` / `server.use`)**: Intercepts every outgoing/incoming message for logging, schema validation, or metric tracking. 5. **Message Handlers (`client.handle` / `server.handle`)**: The **final piece of business logic** where the system reacts to a specific OCPP action (e.g., `BootNotification`).
|
|
153
161
|
|
|
162
|
+
### NOREPLY Suppression
|
|
163
|
+
|
|
164
|
+
Return `NOREPLY` directly from any message handler to safely suppress the automatic outbound `CALLRESULT` without violating strict internal tracking specifications.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { NOREPLY } from "ocpp-ws-io";
|
|
168
|
+
|
|
169
|
+
client.handle("StatusNotification", ({ params }) => {
|
|
170
|
+
return NOREPLY;
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
154
174
|
## 📝 Logging
|
|
155
175
|
|
|
156
|
-
ocpp-ws-io comes with **built-in structured logging**
|
|
176
|
+
`ocpp-ws-io` comes with blazing fast, **built-in structured JSON logging** powered by our custom-built logger, [voltlog-io](https://ocpp-ws-io.rohittiwari.me/docs/voltlog-io). It is finely tuned specifically for high-throughput WebSocket environments.
|
|
157
177
|
|
|
158
178
|
### Default Behavior
|
|
159
179
|
|
|
@@ -197,7 +217,6 @@ const client = new OCPPClient({
|
|
|
197
217
|
|
|
198
218
|
You can bring your own logger (Pino, Winston, etc.) by implementing `LoggerLike`:
|
|
199
219
|
|
|
200
|
-
````typescript
|
|
201
220
|
```typescript
|
|
202
221
|
import pino from "pino";
|
|
203
222
|
|
|
@@ -206,7 +225,7 @@ const client = new OCPPClient({
|
|
|
206
225
|
handler: pino(), // Use existing logger instance
|
|
207
226
|
},
|
|
208
227
|
});
|
|
209
|
-
|
|
228
|
+
```
|
|
210
229
|
|
|
211
230
|
## 🛡️ Safety & Reliability
|
|
212
231
|
|
|
@@ -214,7 +233,7 @@ const client = new OCPPClient({
|
|
|
214
233
|
|
|
215
234
|
Perform RPC calls without `try/catch` blocks. Returns the response data on success, or `undefined` on failure while automatically logging the error. You can also pass per-call config options like timeouts.
|
|
216
235
|
|
|
217
|
-
|
|
236
|
+
```typescript
|
|
218
237
|
const result = await client.safeCall(
|
|
219
238
|
"ocpp1.6",
|
|
220
239
|
"Heartbeat",
|
|
@@ -228,6 +247,7 @@ if (result) {
|
|
|
228
247
|
// Checked for undefined
|
|
229
248
|
console.log("Heartbeat accepted:", result.currentTime);
|
|
230
249
|
}
|
|
250
|
+
```
|
|
231
251
|
|
|
232
252
|
### Unicast Routing (`sendToClient` / `safeSendToClient`) [Server]
|
|
233
253
|
|
|
@@ -236,7 +256,9 @@ Send a message to a specific client ID, even if they are connected to a differen
|
|
|
236
256
|
You have two options depending on your error-handling preference:
|
|
237
257
|
|
|
238
258
|
#### 1. Standard approach (`sendToClient`)
|
|
259
|
+
|
|
239
260
|
Throws an error if the client responds with a `CALLERROR` or if the timeout is reached.
|
|
261
|
+
|
|
240
262
|
```typescript
|
|
241
263
|
try {
|
|
242
264
|
const result = await server.sendToClient(
|
|
@@ -250,7 +272,7 @@ try {
|
|
|
250
272
|
} catch (error) {
|
|
251
273
|
console.error("Failed to get configuration:", error);
|
|
252
274
|
}
|
|
253
|
-
|
|
275
|
+
```
|
|
254
276
|
|
|
255
277
|
#### 2. Safe approach (`safeSendToClient`)
|
|
256
278
|
|
|
@@ -395,3 +417,7 @@ export class CustomAdapter implements EventAdapterInterface {
|
|
|
395
417
|
async metrics?(): Promise<Record<string, unknown>>;
|
|
396
418
|
}
|
|
397
419
|
```
|
|
420
|
+
|
|
421
|
+
## 🙏 Inspiration & Thanks
|
|
422
|
+
|
|
423
|
+
A massive thanks to [Mikuso](https://github.com/mikuso) for their fantastic work on [ocpp-rpc](https://github.com/mikuso/ocpp-rpc), which provided the brilliant early foundation for bridging OCPP-J JSON schemas in JavaScript. While building `ocpp-ws-io`, I wanted to expand on those great ideas by introducing strict, native end-to-end TypeScript support, allowing the community to build even safer CSMS platforms.
|
package/dist/adapters/redis.d.ts
CHANGED
package/dist/adapters/redis.js
CHANGED
|
@@ -134,6 +134,25 @@ var IoRedisDriver = class {
|
|
|
134
134
|
};
|
|
135
135
|
await Promise.all([close(this.pub), close(this.sub)]);
|
|
136
136
|
}
|
|
137
|
+
async setPresenceBatch(entries) {
|
|
138
|
+
if (entries.length === 0) return;
|
|
139
|
+
const pipeline = this.pub.pipeline();
|
|
140
|
+
for (const { key, value, ttlSeconds } of entries) {
|
|
141
|
+
pipeline.set(key, value, "EX", ttlSeconds);
|
|
142
|
+
}
|
|
143
|
+
await pipeline.exec();
|
|
144
|
+
}
|
|
145
|
+
async expire(key, ttlSeconds) {
|
|
146
|
+
await this.pub.expire(key, ttlSeconds);
|
|
147
|
+
}
|
|
148
|
+
onError(handler) {
|
|
149
|
+
this.pub.on("error", handler);
|
|
150
|
+
return () => this.pub.removeListener("error", handler);
|
|
151
|
+
}
|
|
152
|
+
onReconnect(handler) {
|
|
153
|
+
this.pub.on("connect", handler);
|
|
154
|
+
return () => this.pub.removeListener("connect", handler);
|
|
155
|
+
}
|
|
137
156
|
};
|
|
138
157
|
var NodeRedisDriver = class {
|
|
139
158
|
constructor(pub, sub, blocking) {
|
|
@@ -220,6 +239,25 @@ var NodeRedisDriver = class {
|
|
|
220
239
|
async disconnect() {
|
|
221
240
|
await Promise.all([this.pub.disconnect(), this.sub.disconnect()]);
|
|
222
241
|
}
|
|
242
|
+
async setPresenceBatch(entries) {
|
|
243
|
+
if (entries.length === 0) return;
|
|
244
|
+
const multi = this.pub.multi();
|
|
245
|
+
for (const { key, value, ttlSeconds } of entries) {
|
|
246
|
+
multi.set(key, value, { EX: ttlSeconds });
|
|
247
|
+
}
|
|
248
|
+
await multi.exec();
|
|
249
|
+
}
|
|
250
|
+
async expire(key, ttlSeconds) {
|
|
251
|
+
await this.pub.expire(key, ttlSeconds);
|
|
252
|
+
}
|
|
253
|
+
onError(handler) {
|
|
254
|
+
this.pub.on("error", handler);
|
|
255
|
+
return () => this.pub.removeListener("error", handler);
|
|
256
|
+
}
|
|
257
|
+
onReconnect(handler) {
|
|
258
|
+
this.pub.on("connect", handler);
|
|
259
|
+
return () => this.pub.removeListener("connect", handler);
|
|
260
|
+
}
|
|
223
261
|
};
|
|
224
262
|
function createDriver(pub, sub, blocking) {
|
|
225
263
|
if (sub.isOpen !== void 0 && typeof sub.subscribe === "function") {
|
|
@@ -233,6 +271,8 @@ var RedisAdapter = class {
|
|
|
233
271
|
_driver;
|
|
234
272
|
_prefix;
|
|
235
273
|
_streamMaxLen;
|
|
274
|
+
_streamTtlSeconds;
|
|
275
|
+
_presenceTtlSeconds;
|
|
236
276
|
_handlers = /* @__PURE__ */ new Map();
|
|
237
277
|
_streamOffsets = /* @__PURE__ */ new Map();
|
|
238
278
|
// streamKey -> lastId
|
|
@@ -240,20 +280,48 @@ var RedisAdapter = class {
|
|
|
240
280
|
// Active streams to poll
|
|
241
281
|
_polling = false;
|
|
242
282
|
_closed = false;
|
|
283
|
+
// C4: Per-stream sequence counter for message ordering
|
|
284
|
+
_sequenceCounters = /* @__PURE__ */ new Map();
|
|
285
|
+
// C3: Rehydration callbacks
|
|
286
|
+
_unsubError;
|
|
287
|
+
_unsubReconnect;
|
|
288
|
+
// Stored presence entries for rehydration on reconnect
|
|
289
|
+
_presenceCache = /* @__PURE__ */ new Map();
|
|
243
290
|
constructor(options) {
|
|
244
291
|
this._prefix = options.prefix ?? "ocpp-ws-io:";
|
|
245
292
|
this._streamMaxLen = options.streamMaxLen ?? 1e3;
|
|
293
|
+
this._streamTtlSeconds = options.streamTtlSeconds ?? 300;
|
|
294
|
+
this._presenceTtlSeconds = options.presenceTtlSeconds ?? 300;
|
|
246
295
|
this._driver = createDriver(
|
|
247
296
|
options.pubClient,
|
|
248
297
|
options.subClient,
|
|
249
298
|
options.blockingClient
|
|
250
299
|
);
|
|
300
|
+
if (this._driver.onError) {
|
|
301
|
+
this._unsubError = this._driver.onError((err) => {
|
|
302
|
+
console.error("[RedisAdapter] Redis error:", err.message);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (this._driver.onReconnect) {
|
|
306
|
+
this._unsubReconnect = this._driver.onReconnect(() => {
|
|
307
|
+
this._rehydratePresence().catch(() => {
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
251
311
|
}
|
|
252
312
|
async publish(channel, data) {
|
|
253
313
|
const prefixedChannel = this._prefix + channel;
|
|
314
|
+
const payload = data;
|
|
315
|
+
if (payload && typeof payload === "object" && channel.startsWith("ocpp:node:")) {
|
|
316
|
+
const seq = (this._sequenceCounters.get(channel) ?? 0) + 1;
|
|
317
|
+
this._sequenceCounters.set(channel, seq);
|
|
318
|
+
payload.__seq = seq;
|
|
319
|
+
}
|
|
254
320
|
const message = JSON.stringify(data);
|
|
255
321
|
if (channel.startsWith("ocpp:node:")) {
|
|
256
322
|
await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);
|
|
323
|
+
await this._driver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
|
|
324
|
+
});
|
|
257
325
|
} else {
|
|
258
326
|
await this._driver.publish(prefixedChannel, message);
|
|
259
327
|
}
|
|
@@ -319,6 +387,10 @@ var RedisAdapter = class {
|
|
|
319
387
|
this._closed = true;
|
|
320
388
|
this._handlers.clear();
|
|
321
389
|
this._streams.clear();
|
|
390
|
+
this._presenceCache.clear();
|
|
391
|
+
this._sequenceCounters.clear();
|
|
392
|
+
if (this._unsubError) this._unsubError();
|
|
393
|
+
if (this._unsubReconnect) this._unsubReconnect();
|
|
322
394
|
await this._driver.disconnect();
|
|
323
395
|
}
|
|
324
396
|
_handleMessage(channel, message) {
|
|
@@ -378,6 +450,7 @@ var RedisAdapter = class {
|
|
|
378
450
|
// ─── Presence Registry ─────────────────────────────────────────────
|
|
379
451
|
async setPresence(identity, nodeId, ttl) {
|
|
380
452
|
const key = `${this._prefix}presence:${identity}`;
|
|
453
|
+
this._presenceCache.set(identity, { nodeId, ttl });
|
|
381
454
|
await this._driver.set(key, nodeId, ttl);
|
|
382
455
|
}
|
|
383
456
|
async getPresence(identity) {
|
|
@@ -415,6 +488,37 @@ var RedisAdapter = class {
|
|
|
415
488
|
streamDetails
|
|
416
489
|
};
|
|
417
490
|
}
|
|
491
|
+
// ─── C1: Batch Presence Pipeline ────────────────────────────────────
|
|
492
|
+
/**
|
|
493
|
+
* Set multiple presence entries in a single Redis pipeline.
|
|
494
|
+
* Reduces N network round-trips to 1 for bulk presence updates.
|
|
495
|
+
*/
|
|
496
|
+
async setPresenceBatch(entries) {
|
|
497
|
+
if (entries.length === 0) return;
|
|
498
|
+
const batchEntries = entries.map(({ identity, nodeId, ttl }) => {
|
|
499
|
+
const key = `${this._prefix}presence:${identity}`;
|
|
500
|
+
const ttlSeconds = ttl ?? this._presenceTtlSeconds;
|
|
501
|
+
this._presenceCache.set(identity, { nodeId, ttl: ttlSeconds });
|
|
502
|
+
return { key, value: nodeId, ttlSeconds };
|
|
503
|
+
});
|
|
504
|
+
await this._driver.setPresenceBatch(batchEntries);
|
|
505
|
+
}
|
|
506
|
+
// ─── C3: Redis Failure Rehydration ──────────────────────────────────
|
|
507
|
+
/**
|
|
508
|
+
* Re-syncs all cached presence entries to Redis after a reconnection.
|
|
509
|
+
* Called automatically when the Redis client reconnects.
|
|
510
|
+
*/
|
|
511
|
+
async _rehydratePresence() {
|
|
512
|
+
if (this._presenceCache.size === 0) return;
|
|
513
|
+
const entries = Array.from(this._presenceCache.entries()).map(
|
|
514
|
+
([identity, { nodeId, ttl }]) => ({
|
|
515
|
+
key: `${this._prefix}presence:${identity}`,
|
|
516
|
+
value: nodeId,
|
|
517
|
+
ttlSeconds: ttl
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
await this._driver.setPresenceBatch(entries);
|
|
521
|
+
}
|
|
418
522
|
};
|
|
419
523
|
// Annotate the CommonJS export names for ESM import in node:
|
|
420
524
|
0 && (module.exports = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/redis/index.ts","../../src/adapters/redis/helpers.ts"],"sourcesContent":["import type { EventAdapterInterface } from \"../../types.js\";\nimport {\n createDriver,\n type RedisLikeClient,\n type RedisPubSubDriver,\n} from \"./helpers.js\";\n\nexport interface RedisAdapterOptions {\n /** Redis client for publishing */\n pubClient: RedisLikeClient;\n /** Redis client for subscribing (must be a separate connection) */\n subClient: RedisLikeClient;\n /** Redis client for blocking stream operations (recommended for reliability) */\n blockingClient?: RedisLikeClient;\n /** Optional key prefix for channels (default: 'ocpp-ws-io:') */\n prefix?: string;\n /** StreamMaxLen for trimming (default: 1000) */\n streamMaxLen?: number;\n}\n\n/**\n * Redis adapter for cross-process event distribution.\n *\n * Supports `ioredis` and `node-redis` (v4+).\n * Uses Redis Streams for reliable unicast (node-to-node) and Pub/Sub for broadcast.\n */\nexport class RedisAdapter implements EventAdapterInterface {\n private _driver: RedisPubSubDriver;\n private _prefix: string;\n private _streamMaxLen: number;\n private _handlers = new Map<string, Set<(data: unknown) => void>>();\n private _streamOffsets = new Map<string, string>(); // streamKey -> lastId\n private _streams = new Set<string>(); // Active streams to poll\n private _polling = false;\n private _closed = false;\n\n constructor(options: RedisAdapterOptions) {\n this._prefix = options.prefix ?? \"ocpp-ws-io:\";\n this._streamMaxLen = options.streamMaxLen ?? 1000;\n this._driver = createDriver(\n options.pubClient,\n options.subClient,\n options.blockingClient,\n );\n }\n\n async publish(channel: string, data: unknown): Promise<void> {\n const prefixedChannel = this._prefix + channel;\n const message = JSON.stringify(data);\n\n // Unicast (Node-to-Node) -> Use Streams\n if (channel.startsWith(\"ocpp:node:\")) {\n await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);\n } else {\n // Broadcast -> Use Pub/Sub\n await this._driver.publish(prefixedChannel, message);\n }\n }\n\n async publishBatch(\n messages: { channel: string; data: unknown }[],\n ): Promise<void> {\n const streamMessages: { stream: string; args: Record<string, string> }[] =\n [];\n const broadcastMessages: { channel: string; message: string }[] = [];\n\n for (const msg of messages) {\n const prefixedChannel = this._prefix + msg.channel;\n const message = JSON.stringify(msg.data);\n\n if (msg.channel.startsWith(\"ocpp:node:\")) {\n streamMessages.push({ stream: prefixedChannel, args: { message } });\n } else {\n broadcastMessages.push({ channel: prefixedChannel, message });\n }\n }\n\n const promises: Promise<void>[] = [];\n\n if (streamMessages.length > 0) {\n promises.push(this._driver.xaddBatch(streamMessages, this._streamMaxLen));\n }\n\n if (broadcastMessages.length > 0) {\n promises.push(\n Promise.all(\n broadcastMessages.map((bm) =>\n this._driver.publish(bm.channel, bm.message),\n ),\n ).then(() => {}), // Map `Promise<void[]>` to `Promise<void>`\n );\n }\n\n await Promise.all(promises);\n }\n\n async subscribe(\n channel: string,\n handler: (data: unknown) => void,\n ): Promise<void> {\n if (!this._handlers.has(channel)) {\n this._handlers.set(channel, new Set());\n const prefixedChannel = this._prefix + channel;\n\n if (channel.startsWith(\"ocpp:node:\")) {\n // Stream subscription\n // Start from '0' (beginning) to pick up missed messages during downtime (persistence).\n // Since we trim the stream (MAXLEN), this will only replay recent pending messages.\n if (!this._streams.has(prefixedChannel)) {\n this._streams.add(prefixedChannel);\n this._streamOffsets.set(prefixedChannel, \"0\");\n this._ensurePolling();\n }\n } else {\n // Pub/Sub subscription\n await this._driver.subscribe(prefixedChannel, (message) => {\n this._handleMessage(channel, message);\n });\n }\n }\n this._handlers.get(channel)?.add(handler);\n }\n\n async unsubscribe(channel: string): Promise<void> {\n const prefixedChannel = this._prefix + channel;\n\n if (this._streams.has(prefixedChannel)) {\n this._streams.delete(prefixedChannel);\n this._streamOffsets.delete(prefixedChannel); // Cleanup offset\n } else {\n await this._driver.unsubscribe(prefixedChannel);\n }\n\n this._handlers.delete(channel);\n }\n\n async disconnect(): Promise<void> {\n this._closed = true;\n this._handlers.clear();\n this._streams.clear();\n await this._driver.disconnect();\n }\n\n private _handleMessage(channel: string, message: string): void {\n const handlers = this._handlers.get(channel);\n if (!handlers) return;\n\n let data: unknown;\n try {\n data = JSON.parse(message);\n } catch {\n data = message;\n }\n\n for (const handler of handlers) {\n try {\n handler(data);\n } catch {\n // Swallow handler errors\n }\n }\n }\n\n // ─── Stream Polling ───────────────────────────────────────────────\n\n private _ensurePolling() {\n if (this._polling || this._closed) return;\n this._polling = true;\n this._pollLoop().catch(() => {\n this._polling = false;\n });\n }\n\n private async _pollLoop() {\n while (!this._closed) {\n if (this._streams.size === 0) {\n await new Promise((resolve) => setTimeout(resolve, 1000));\n continue;\n }\n\n const streamsArg = Array.from(this._streams).map((key) => ({\n key,\n id: this._streamOffsets.get(key) || \"$\",\n }));\n\n try {\n // Block for 1s. This allows picking up new subscriptions reasonably fast.\n const entries = await this._driver.xread(streamsArg, undefined, 1000);\n\n if (entries) {\n for (const entry of entries) {\n const channel = entry.stream.replace(this._prefix, \"\"); // remove prefix to find handler key\n\n for (const msg of entry.messages) {\n // Update offset\n this._streamOffsets.set(entry.stream, msg.id);\n\n const messageContent = msg.data.message;\n if (messageContent) {\n this._handleMessage(channel, messageContent);\n }\n }\n }\n }\n } catch (_err) {\n // Log error? For now swallow to keep loop alive\n // Avoid tight loop on error\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n }\n this._polling = false;\n }\n\n // ─── Presence Registry ─────────────────────────────────────────────\n\n async setPresence(\n identity: string,\n nodeId: string,\n ttl: number,\n ): Promise<void> {\n const key = `${this._prefix}presence:${identity}`;\n await this._driver.set(key, nodeId, ttl);\n }\n\n async getPresence(identity: string): Promise<string | null> {\n const key = `${this._prefix}presence:${identity}`;\n return await this._driver.get(key);\n }\n\n async getPresenceBatch(identities: string[]): Promise<(string | null)[]> {\n if (identities.length === 0) return [];\n const keys = identities.map((id) => `${this._prefix}presence:${id}`);\n if (this._driver.mget) {\n return await this._driver.mget(keys);\n }\n // Fallback if mget not available\n return await Promise.all(keys.map((k) => this._driver.get(k)));\n }\n\n async removePresence(identity: string): Promise<void> {\n const key = `${this._prefix}presence:${identity}`;\n await this._driver.del(key);\n }\n\n // ─── Observability Pipeline ────────────────────────────────────────\n\n async metrics(): Promise<Record<string, unknown>> {\n let pendingMessages = 0;\n const streamDetails: Record<string, number> = {};\n\n // Calculate \"consumer lag\" by checking the length of all active streams\n // Since we use MAXLEN for trimming, XLEN directly equals pending unread messages\n for (const streamKey of this._streams) {\n try {\n const length = await this._driver.xlen(streamKey);\n pendingMessages += length;\n streamDetails[streamKey] = length;\n } catch {\n // Ignore failures for individual stream stats\n streamDetails[streamKey] = -1;\n }\n }\n\n return {\n pendingMessages,\n activeStreams: this._streams.size,\n streamDetails,\n };\n }\n}\n","export interface RedisLikeClient {\n publish(\n channel: string,\n message: string,\n ): Promise<number | unknown | undefined>;\n subscribe(channel: string, ...args: unknown[]): Promise<unknown | undefined>;\n unsubscribe(\n channel: string,\n ...args: unknown[]\n ): Promise<unknown | undefined>;\n on?(\n event: \"message\",\n callback: (channel: string, message: string) => void,\n ): unknown;\n disconnect?(): Promise<void> | void;\n quit?(): Promise<unknown> | undefined;\n // Node Redis v4 specific\n isOpen?: boolean;\n}\n\n// ─── Stream Types ───────────────────────────────────────────────\n\nexport interface StreamMessage {\n id: string;\n data: Record<string, string>;\n}\n\nexport interface StreamEntry {\n stream: string;\n messages: StreamMessage[];\n}\n\n// ─── Extended Redis Driver ──────────────────────────────────────\n\nexport interface RedisPubSubDriver {\n publish(channel: string, message: string): Promise<void>;\n subscribe(channel: string, handler: (message: string) => void): Promise<void>;\n unsubscribe(channel: string): Promise<void>;\n disconnect(): Promise<void>;\n\n // Key-Value Store for Presence\n set(key: string, value: string, ttlSeconds?: number): Promise<void>;\n get(key: string): Promise<string | null>;\n mget(keys: string[]): Promise<(string | null)[]>;\n del(key: string): Promise<void>;\n\n // Streams\n xadd(\n stream: string,\n args: Record<string, string>,\n maxLen?: number,\n ): Promise<string>;\n xaddBatch(\n messages: { stream: string; args: Record<string, string> }[],\n maxLen?: number,\n ): Promise<void>;\n xread(\n streams: { key: string; id: string }[],\n count?: number,\n block?: number,\n ): Promise<StreamEntry[] | null>;\n xlen(stream: string): Promise<number>;\n}\n\nexport class IoRedisDriver implements RedisPubSubDriver {\n private _handlers = new Map<string, (msg: string) => void>();\n\n constructor(\n private pub: any,\n private sub: any,\n private blocking?: any,\n ) {\n if (this.sub.on) {\n this.sub.on(\"message\", (channel: string, message: string) => {\n const handler = this._handlers.get(channel);\n if (handler) handler(message);\n });\n }\n }\n\n async publish(channel: string, message: string): Promise<void> {\n await this.pub.publish(channel, message);\n }\n\n async subscribe(\n channel: string,\n handler: (message: string) => void,\n ): Promise<void> {\n this._handlers.set(channel, handler);\n await this.sub.subscribe(channel);\n }\n\n async unsubscribe(channel: string): Promise<void> {\n await this.sub.unsubscribe(channel);\n this._handlers.delete(channel);\n }\n\n async set(key: string, value: string, ttlSeconds?: number): Promise<void> {\n if (ttlSeconds) {\n await this.pub.set(key, value, \"EX\", ttlSeconds);\n } else {\n await this.pub.set(key, value);\n }\n }\n\n async get(key: string): Promise<string | null> {\n return (await this.pub.get(key)) || null;\n }\n\n async mget(keys: string[]): Promise<(string | null)[]> {\n if (keys.length === 0) return [];\n return await this.pub.mget(...keys);\n }\n\n async del(key: string): Promise<void> {\n await this.pub.del(key);\n }\n\n async xadd(\n stream: string,\n args: Record<string, string>,\n maxLen?: number,\n ): Promise<string> {\n const flatArgs: string[] = [];\n if (maxLen) {\n flatArgs.push(\"MAXLEN\", \"~\", maxLen.toString());\n }\n flatArgs.push(\"*\"); // ID = auto\n for (const [k, v] of Object.entries(args)) {\n flatArgs.push(k, v);\n }\n return (await this.pub.xadd(stream, ...flatArgs)) as string;\n }\n\n async xaddBatch(\n messages: { stream: string; args: Record<string, string> }[],\n maxLen?: number,\n ): Promise<void> {\n if (messages.length === 0) return;\n const pipeline = this.pub.pipeline();\n for (const msg of messages) {\n const flatArgs: string[] = [];\n if (maxLen) {\n flatArgs.push(\"MAXLEN\", \"~\", maxLen.toString());\n }\n flatArgs.push(\"*\");\n for (const [k, v] of Object.entries(msg.args)) {\n flatArgs.push(k, v);\n }\n pipeline.xadd(msg.stream, ...flatArgs);\n }\n await pipeline.exec();\n }\n\n async xread(\n streams: { key: string; id: string }[],\n count?: number,\n block?: number,\n ): Promise<StreamEntry[] | null> {\n const args: (string | number)[] = [];\n if (count) {\n args.push(\"COUNT\", count);\n }\n if (typeof block === \"number\") {\n args.push(\"BLOCK\", block);\n }\n args.push(\"STREAMS\");\n streams.forEach((s) => {\n args.push(s.key);\n });\n streams.forEach((s) => {\n args.push(s.id);\n });\n\n // Use blocking client if available and blocking is requested\n const client = block && this.blocking ? this.blocking : this.pub;\n\n // ioredis returns [[key, [[id, [k,v,k,v]]]]]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = (await client.xread(...args)) as any;\n\n if (!result) return null;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return result.map(([stream, messages]: any) => ({\n stream,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messages: messages.map(([id, fields]: any) => {\n const data: Record<string, string> = {};\n for (let i = 0; i < fields.length; i += 2) {\n data[fields[i]] = fields[i + 1];\n }\n return { id, data };\n }),\n }));\n }\n\n async xlen(stream: string): Promise<number> {\n return (await this.pub.xlen(stream)) as number;\n }\n\n async disconnect(): Promise<void> {\n this._handlers.clear();\n const close = async (c: any) => {\n if (c.quit) await c.quit();\n else if (c.disconnect) await c.disconnect();\n };\n await Promise.all([close(this.pub), close(this.sub)]);\n }\n}\n\nexport class NodeRedisDriver implements RedisPubSubDriver {\n constructor(\n private pub: any,\n private sub: any,\n private blocking?: any,\n ) {}\n\n async publish(channel: string, message: string): Promise<void> {\n await this.pub.publish(channel, message);\n }\n\n async subscribe(\n channel: string,\n handler: (message: string) => void,\n ): Promise<void> {\n await this.sub.subscribe(channel, handler);\n }\n\n async unsubscribe(channel: string): Promise<void> {\n await this.sub.unsubscribe(channel);\n }\n\n async set(key: string, value: string, ttlSeconds?: number): Promise<void> {\n if (ttlSeconds) {\n await this.pub.set(key, value, { EX: ttlSeconds });\n } else {\n await this.pub.set(key, value);\n }\n }\n\n async get(key: string): Promise<string | null> {\n return (await this.pub.get(key)) || null;\n }\n\n async mget(keys: string[]): Promise<(string | null)[]> {\n if (keys.length === 0) return [];\n return await this.pub.mGet(keys);\n }\n\n async del(key: string): Promise<void> {\n await this.pub.del(key);\n }\n\n async xadd(\n stream: string,\n args: Record<string, string>,\n maxLen?: number,\n ): Promise<string> {\n const options: any = {};\n if (maxLen) {\n options.MKSTREAM = true; // Make sure stream exists\n // node-redis specific options for MAXLEN\n // But basic xadd signature is (key, id, message, options?)\n }\n // Node Redis v4 xAdd: (key, id, message)\n // For trimming, it might be in options.\n // Let's assume standard usage for now.\n // Actually Node Redis v4: .xAdd(key, id, message, options)\n\n // Construct message object\n return await this.pub.xAdd(stream, \"*\", args, {\n TRIM: maxLen\n ? {\n strategy: \"MAXLEN\",\n strategyModifier: \"~\",\n threshold: maxLen,\n }\n : undefined,\n });\n }\n\n async xaddBatch(\n messages: { stream: string; args: Record<string, string> }[],\n maxLen?: number,\n ): Promise<void> {\n if (messages.length === 0) return;\n const multi = this.pub.multi();\n for (const msg of messages) {\n multi.xAdd(msg.stream, \"*\", msg.args, {\n TRIM: maxLen\n ? {\n strategy: \"MAXLEN\",\n strategyModifier: \"~\",\n threshold: maxLen,\n }\n : undefined,\n });\n }\n await multi.exec();\n }\n\n async xread(\n streams: { key: string; id: string }[],\n count?: number,\n block?: number,\n ): Promise<StreamEntry[] | null> {\n // Node Redis v4 .xRead(streams, options)\n const options: any = {};\n if (count) options.COUNT = count;\n if (typeof block === \"number\") options.BLOCK = block;\n\n const streamsParam = streams.map((s) => ({\n key: s.key,\n id: s.id,\n }));\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const client = block && this.blocking ? this.blocking : this.pub;\n const result = (await client.xRead(streamsParam, options)) as any;\n\n if (!result || result.length === 0) return null;\n\n // Node Redis v4 returns: { name: string, messages: { id: string, message: Record<string,string> }[] }[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return result.map((entry: any) => ({\n stream: entry.name,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messages: entry.messages.map((msg: any) => ({\n id: msg.id,\n data: msg.message,\n })),\n }));\n }\n\n async xlen(stream: string): Promise<number> {\n return (await this.pub.xLen(stream)) as number;\n }\n\n async disconnect(): Promise<void> {\n await Promise.all([this.pub.disconnect(), this.sub.disconnect()]);\n }\n}\n\nexport function createDriver(\n pub: any,\n sub: any,\n blocking?: any,\n): RedisPubSubDriver {\n // Simple heuristic: Node Redis v4 clients usually have 'isOpen' boolean\n if (sub.isOpen !== undefined && typeof sub.subscribe === \"function\") {\n return new NodeRedisDriver(pub, sub, blocking);\n }\n // Default to IoRedis / Generic\n return new IoRedisDriver(pub, sub, blocking);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/redis/index.ts","../../src/adapters/redis/helpers.ts"],"sourcesContent":["import type { EventAdapterInterface } from \"../../types.js\";\nimport {\n createDriver,\n type RedisLikeClient,\n type RedisPubSubDriver,\n} from \"./helpers.js\";\n\nexport interface RedisAdapterOptions {\n /** Redis client for publishing */\n pubClient: RedisLikeClient;\n /** Redis client for subscribing (must be a separate connection) */\n subClient: RedisLikeClient;\n /** Redis client for blocking stream operations (recommended for reliability) */\n blockingClient?: RedisLikeClient;\n /** Optional key prefix for channels (default: 'ocpp-ws-io:') */\n prefix?: string;\n /** StreamMaxLen for trimming (default: 1000) */\n streamMaxLen?: number;\n /**\n * TTL in seconds for ephemeral stream keys (default: 300).\n * Prevents abandoned channel keys from leaking memory in Redis.\n */\n streamTtlSeconds?: number;\n /**\n * Presence TTL in seconds (default: 300).\n * Used for batch presence heartbeat pipeline.\n */\n presenceTtlSeconds?: number;\n}\n\n/**\n * Redis adapter for cross-process event distribution.\n *\n * Supports `ioredis` and `node-redis` (v4+).\n * Uses Redis Streams for reliable unicast (node-to-node) and Pub/Sub for broadcast.\n */\nexport class RedisAdapter implements EventAdapterInterface {\n private _driver: RedisPubSubDriver;\n private _prefix: string;\n private _streamMaxLen: number;\n private _streamTtlSeconds: number;\n private _presenceTtlSeconds: number;\n private _handlers = new Map<string, Set<(data: unknown) => void>>();\n private _streamOffsets = new Map<string, string>(); // streamKey -> lastId\n private _streams = new Set<string>(); // Active streams to poll\n private _polling = false;\n private _closed = false;\n\n // C4: Per-stream sequence counter for message ordering\n private _sequenceCounters = new Map<string, number>();\n\n // C3: Rehydration callbacks\n private _unsubError?: () => void;\n private _unsubReconnect?: () => void;\n\n // Stored presence entries for rehydration on reconnect\n private _presenceCache = new Map<string, { nodeId: string; ttl: number }>();\n\n constructor(options: RedisAdapterOptions) {\n this._prefix = options.prefix ?? \"ocpp-ws-io:\";\n this._streamMaxLen = options.streamMaxLen ?? 1000;\n this._streamTtlSeconds = options.streamTtlSeconds ?? 300;\n this._presenceTtlSeconds = options.presenceTtlSeconds ?? 300;\n this._driver = createDriver(\n options.pubClient,\n options.subClient,\n options.blockingClient,\n );\n\n // C3: Redis Failure Rehydration — listen for errors and re-sync on reconnect\n if (this._driver.onError) {\n this._unsubError = this._driver.onError((err) => {\n // Log for observability — consumers can attach their own logger\n console.error(\"[RedisAdapter] Redis error:\", err.message);\n });\n }\n if (this._driver.onReconnect) {\n this._unsubReconnect = this._driver.onReconnect(() => {\n this._rehydratePresence().catch(() => {});\n });\n }\n }\n\n async publish(channel: string, data: unknown): Promise<void> {\n const prefixedChannel = this._prefix + channel;\n\n // C4: Attach sequence ID to unicast messages for ordering\n const payload = data as Record<string, unknown> | null;\n if (\n payload &&\n typeof payload === \"object\" &&\n channel.startsWith(\"ocpp:node:\")\n ) {\n const seq = (this._sequenceCounters.get(channel) ?? 0) + 1;\n this._sequenceCounters.set(channel, seq);\n (payload as Record<string, unknown>).__seq = seq;\n }\n\n const message = JSON.stringify(data);\n\n // Unicast (Node-to-Node) -> Use Streams\n if (channel.startsWith(\"ocpp:node:\")) {\n await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);\n // C2: Set TTL lease on ephemeral stream key to prevent memory leaks\n await this._driver\n .expire(prefixedChannel, this._streamTtlSeconds)\n .catch(() => {});\n } else {\n // Broadcast -> Use Pub/Sub\n await this._driver.publish(prefixedChannel, message);\n }\n }\n\n async publishBatch(\n messages: { channel: string; data: unknown }[],\n ): Promise<void> {\n const streamMessages: { stream: string; args: Record<string, string> }[] =\n [];\n const broadcastMessages: { channel: string; message: string }[] = [];\n\n for (const msg of messages) {\n const prefixedChannel = this._prefix + msg.channel;\n const message = JSON.stringify(msg.data);\n\n if (msg.channel.startsWith(\"ocpp:node:\")) {\n streamMessages.push({ stream: prefixedChannel, args: { message } });\n } else {\n broadcastMessages.push({ channel: prefixedChannel, message });\n }\n }\n\n const promises: Promise<void>[] = [];\n\n if (streamMessages.length > 0) {\n promises.push(this._driver.xaddBatch(streamMessages, this._streamMaxLen));\n }\n\n if (broadcastMessages.length > 0) {\n promises.push(\n Promise.all(\n broadcastMessages.map((bm) =>\n this._driver.publish(bm.channel, bm.message),\n ),\n ).then(() => {}), // Map `Promise<void[]>` to `Promise<void>`\n );\n }\n\n await Promise.all(promises);\n }\n\n async subscribe(\n channel: string,\n handler: (data: unknown) => void,\n ): Promise<void> {\n if (!this._handlers.has(channel)) {\n this._handlers.set(channel, new Set());\n const prefixedChannel = this._prefix + channel;\n\n if (channel.startsWith(\"ocpp:node:\")) {\n // Stream subscription\n // Start from '0' (beginning) to pick up missed messages during downtime (persistence).\n // Since we trim the stream (MAXLEN), this will only replay recent pending messages.\n if (!this._streams.has(prefixedChannel)) {\n this._streams.add(prefixedChannel);\n this._streamOffsets.set(prefixedChannel, \"0\");\n this._ensurePolling();\n }\n } else {\n // Pub/Sub subscription\n await this._driver.subscribe(prefixedChannel, (message) => {\n this._handleMessage(channel, message);\n });\n }\n }\n this._handlers.get(channel)?.add(handler);\n }\n\n async unsubscribe(channel: string): Promise<void> {\n const prefixedChannel = this._prefix + channel;\n\n if (this._streams.has(prefixedChannel)) {\n this._streams.delete(prefixedChannel);\n this._streamOffsets.delete(prefixedChannel); // Cleanup offset\n } else {\n await this._driver.unsubscribe(prefixedChannel);\n }\n\n this._handlers.delete(channel);\n }\n\n async disconnect(): Promise<void> {\n this._closed = true;\n this._handlers.clear();\n this._streams.clear();\n this._presenceCache.clear();\n this._sequenceCounters.clear();\n if (this._unsubError) this._unsubError();\n if (this._unsubReconnect) this._unsubReconnect();\n await this._driver.disconnect();\n }\n\n private _handleMessage(channel: string, message: string): void {\n const handlers = this._handlers.get(channel);\n if (!handlers) return;\n\n let data: unknown;\n try {\n data = JSON.parse(message);\n } catch {\n data = message;\n }\n\n for (const handler of handlers) {\n try {\n handler(data);\n } catch {\n // Swallow handler errors\n }\n }\n }\n\n // ─── Stream Polling ───────────────────────────────────────────────\n\n private _ensurePolling() {\n if (this._polling || this._closed) return;\n this._polling = true;\n this._pollLoop().catch(() => {\n this._polling = false;\n });\n }\n\n private async _pollLoop() {\n while (!this._closed) {\n if (this._streams.size === 0) {\n await new Promise((resolve) => setTimeout(resolve, 1000));\n continue;\n }\n\n const streamsArg = Array.from(this._streams).map((key) => ({\n key,\n id: this._streamOffsets.get(key) || \"$\",\n }));\n\n try {\n // Block for 1s. This allows picking up new subscriptions reasonably fast.\n const entries = await this._driver.xread(streamsArg, undefined, 1000);\n\n if (entries) {\n for (const entry of entries) {\n const channel = entry.stream.replace(this._prefix, \"\"); // remove prefix to find handler key\n\n for (const msg of entry.messages) {\n // Update offset\n this._streamOffsets.set(entry.stream, msg.id);\n\n const messageContent = msg.data.message;\n if (messageContent) {\n this._handleMessage(channel, messageContent);\n }\n }\n }\n }\n } catch (_err) {\n // Log error? For now swallow to keep loop alive\n // Avoid tight loop on error\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n }\n this._polling = false;\n }\n\n // ─── Presence Registry ─────────────────────────────────────────────\n\n async setPresence(\n identity: string,\n nodeId: string,\n ttl: number,\n ): Promise<void> {\n const key = `${this._prefix}presence:${identity}`;\n // Cache for rehydration on reconnect (C3)\n this._presenceCache.set(identity, { nodeId, ttl });\n await this._driver.set(key, nodeId, ttl);\n }\n\n async getPresence(identity: string): Promise<string | null> {\n const key = `${this._prefix}presence:${identity}`;\n return await this._driver.get(key);\n }\n\n async getPresenceBatch(identities: string[]): Promise<(string | null)[]> {\n if (identities.length === 0) return [];\n const keys = identities.map((id) => `${this._prefix}presence:${id}`);\n if (this._driver.mget) {\n return await this._driver.mget(keys);\n }\n // Fallback if mget not available\n return await Promise.all(keys.map((k) => this._driver.get(k)));\n }\n\n async removePresence(identity: string): Promise<void> {\n const key = `${this._prefix}presence:${identity}`;\n await this._driver.del(key);\n }\n\n // ─── Observability Pipeline ────────────────────────────────────────\n\n async metrics(): Promise<Record<string, unknown>> {\n let pendingMessages = 0;\n const streamDetails: Record<string, number> = {};\n\n // Calculate \"consumer lag\" by checking the length of all active streams\n // Since we use MAXLEN for trimming, XLEN directly equals pending unread messages\n for (const streamKey of this._streams) {\n try {\n const length = await this._driver.xlen(streamKey);\n pendingMessages += length;\n streamDetails[streamKey] = length;\n } catch {\n // Ignore failures for individual stream stats\n streamDetails[streamKey] = -1;\n }\n }\n\n return {\n pendingMessages,\n activeStreams: this._streams.size,\n streamDetails,\n };\n }\n\n // ─── C1: Batch Presence Pipeline ────────────────────────────────────\n\n /**\n * Set multiple presence entries in a single Redis pipeline.\n * Reduces N network round-trips to 1 for bulk presence updates.\n */\n async setPresenceBatch(\n entries: { identity: string; nodeId: string; ttl?: number }[],\n ): Promise<void> {\n if (entries.length === 0) return;\n\n const batchEntries = entries.map(({ identity, nodeId, ttl }) => {\n const key = `${this._prefix}presence:${identity}`;\n const ttlSeconds = ttl ?? this._presenceTtlSeconds;\n // Cache for rehydration\n this._presenceCache.set(identity, { nodeId, ttl: ttlSeconds });\n return { key, value: nodeId, ttlSeconds };\n });\n\n await this._driver.setPresenceBatch(batchEntries);\n }\n\n // ─── C3: Redis Failure Rehydration ──────────────────────────────────\n\n /**\n * Re-syncs all cached presence entries to Redis after a reconnection.\n * Called automatically when the Redis client reconnects.\n */\n private async _rehydratePresence(): Promise<void> {\n if (this._presenceCache.size === 0) return;\n\n const entries = Array.from(this._presenceCache.entries()).map(\n ([identity, { nodeId, ttl }]) => ({\n key: `${this._prefix}presence:${identity}`,\n value: nodeId,\n ttlSeconds: ttl,\n }),\n );\n\n await this._driver.setPresenceBatch(entries);\n }\n}\n","export interface RedisLikeClient {\n publish(\n channel: string,\n message: string,\n ): Promise<number | unknown | undefined>;\n subscribe(channel: string, ...args: unknown[]): Promise<unknown | undefined>;\n unsubscribe(\n channel: string,\n ...args: unknown[]\n ): Promise<unknown | undefined>;\n on?(\n event: \"message\",\n callback: (channel: string, message: string) => void,\n ): unknown;\n disconnect?(): Promise<void> | void;\n quit?(): Promise<unknown> | undefined;\n // Node Redis v4 specific\n isOpen?: boolean;\n}\n\n// ─── Stream Types ───────────────────────────────────────────────\n\nexport interface StreamMessage {\n id: string;\n data: Record<string, string>;\n}\n\nexport interface StreamEntry {\n stream: string;\n messages: StreamMessage[];\n}\n\n// ─── Extended Redis Driver ──────────────────────────────────────\n\nexport interface RedisPubSubDriver {\n publish(channel: string, message: string): Promise<void>;\n subscribe(channel: string, handler: (message: string) => void): Promise<void>;\n unsubscribe(channel: string): Promise<void>;\n disconnect(): Promise<void>;\n\n // Key-Value Store for Presence\n set(key: string, value: string, ttlSeconds?: number): Promise<void>;\n get(key: string): Promise<string | null>;\n mget(keys: string[]): Promise<(string | null)[]>;\n del(key: string): Promise<void>;\n\n /**\n * Batch set multiple presence keys with TTL in a single pipeline.\n * Falls back to sequential sets if pipelining is unavailable.\n */\n setPresenceBatch(\n entries: { key: string; value: string; ttlSeconds: number }[],\n ): Promise<void>;\n\n /**\n * Set a TTL on an existing key (for ephemeral stream channel leases).\n */\n expire(key: string, ttlSeconds: number): Promise<void>;\n\n /**\n * Subscribe to connection error events for rehydration.\n * Returns an unsubscribe function.\n */\n onError?(handler: (err: Error) => void): () => void;\n\n /**\n * Subscribe to reconnection events.\n * Returns an unsubscribe function.\n */\n onReconnect?(handler: () => void): () => void;\n\n // Streams\n xadd(\n stream: string,\n args: Record<string, string>,\n maxLen?: number,\n ): Promise<string>;\n xaddBatch(\n messages: { stream: string; args: Record<string, string> }[],\n maxLen?: number,\n ): Promise<void>;\n xread(\n streams: { key: string; id: string }[],\n count?: number,\n block?: number,\n ): Promise<StreamEntry[] | null>;\n xlen(stream: string): Promise<number>;\n}\n\nexport class IoRedisDriver implements RedisPubSubDriver {\n private _handlers = new Map<string, (msg: string) => void>();\n\n constructor(\n private pub: any,\n private sub: any,\n private blocking?: any,\n ) {\n if (this.sub.on) {\n this.sub.on(\"message\", (channel: string, message: string) => {\n const handler = this._handlers.get(channel);\n if (handler) handler(message);\n });\n }\n }\n\n async publish(channel: string, message: string): Promise<void> {\n await this.pub.publish(channel, message);\n }\n\n async subscribe(\n channel: string,\n handler: (message: string) => void,\n ): Promise<void> {\n this._handlers.set(channel, handler);\n await this.sub.subscribe(channel);\n }\n\n async unsubscribe(channel: string): Promise<void> {\n await this.sub.unsubscribe(channel);\n this._handlers.delete(channel);\n }\n\n async set(key: string, value: string, ttlSeconds?: number): Promise<void> {\n if (ttlSeconds) {\n await this.pub.set(key, value, \"EX\", ttlSeconds);\n } else {\n await this.pub.set(key, value);\n }\n }\n\n async get(key: string): Promise<string | null> {\n return (await this.pub.get(key)) || null;\n }\n\n async mget(keys: string[]): Promise<(string | null)[]> {\n if (keys.length === 0) return [];\n return await this.pub.mget(...keys);\n }\n\n async del(key: string): Promise<void> {\n await this.pub.del(key);\n }\n\n async xadd(\n stream: string,\n args: Record<string, string>,\n maxLen?: number,\n ): Promise<string> {\n const flatArgs: string[] = [];\n if (maxLen) {\n flatArgs.push(\"MAXLEN\", \"~\", maxLen.toString());\n }\n flatArgs.push(\"*\"); // ID = auto\n for (const [k, v] of Object.entries(args)) {\n flatArgs.push(k, v);\n }\n return (await this.pub.xadd(stream, ...flatArgs)) as string;\n }\n\n async xaddBatch(\n messages: { stream: string; args: Record<string, string> }[],\n maxLen?: number,\n ): Promise<void> {\n if (messages.length === 0) return;\n const pipeline = this.pub.pipeline();\n for (const msg of messages) {\n const flatArgs: string[] = [];\n if (maxLen) {\n flatArgs.push(\"MAXLEN\", \"~\", maxLen.toString());\n }\n flatArgs.push(\"*\");\n for (const [k, v] of Object.entries(msg.args)) {\n flatArgs.push(k, v);\n }\n pipeline.xadd(msg.stream, ...flatArgs);\n }\n await pipeline.exec();\n }\n\n async xread(\n streams: { key: string; id: string }[],\n count?: number,\n block?: number,\n ): Promise<StreamEntry[] | null> {\n const args: (string | number)[] = [];\n if (count) {\n args.push(\"COUNT\", count);\n }\n if (typeof block === \"number\") {\n args.push(\"BLOCK\", block);\n }\n args.push(\"STREAMS\");\n streams.forEach((s) => {\n args.push(s.key);\n });\n streams.forEach((s) => {\n args.push(s.id);\n });\n\n // Use blocking client if available and blocking is requested\n const client = block && this.blocking ? this.blocking : this.pub;\n\n // ioredis returns [[key, [[id, [k,v,k,v]]]]]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = (await client.xread(...args)) as any;\n\n if (!result) return null;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return result.map(([stream, messages]: any) => ({\n stream,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messages: messages.map(([id, fields]: any) => {\n const data: Record<string, string> = {};\n for (let i = 0; i < fields.length; i += 2) {\n data[fields[i]] = fields[i + 1];\n }\n return { id, data };\n }),\n }));\n }\n\n async xlen(stream: string): Promise<number> {\n return (await this.pub.xlen(stream)) as number;\n }\n\n async disconnect(): Promise<void> {\n this._handlers.clear();\n const close = async (c: any) => {\n if (c.quit) await c.quit();\n else if (c.disconnect) await c.disconnect();\n };\n await Promise.all([close(this.pub), close(this.sub)]);\n }\n\n async setPresenceBatch(\n entries: { key: string; value: string; ttlSeconds: number }[],\n ): Promise<void> {\n if (entries.length === 0) return;\n const pipeline = this.pub.pipeline();\n for (const { key, value, ttlSeconds } of entries) {\n pipeline.set(key, value, \"EX\", ttlSeconds);\n }\n await pipeline.exec();\n }\n\n async expire(key: string, ttlSeconds: number): Promise<void> {\n await this.pub.expire(key, ttlSeconds);\n }\n\n onError(handler: (err: Error) => void): () => void {\n this.pub.on(\"error\", handler);\n return () => this.pub.removeListener(\"error\", handler);\n }\n\n onReconnect(handler: () => void): () => void {\n this.pub.on(\"connect\", handler);\n return () => this.pub.removeListener(\"connect\", handler);\n }\n}\n\nexport class NodeRedisDriver implements RedisPubSubDriver {\n constructor(\n private pub: any,\n private sub: any,\n private blocking?: any,\n ) {}\n\n async publish(channel: string, message: string): Promise<void> {\n await this.pub.publish(channel, message);\n }\n\n async subscribe(\n channel: string,\n handler: (message: string) => void,\n ): Promise<void> {\n await this.sub.subscribe(channel, handler);\n }\n\n async unsubscribe(channel: string): Promise<void> {\n await this.sub.unsubscribe(channel);\n }\n\n async set(key: string, value: string, ttlSeconds?: number): Promise<void> {\n if (ttlSeconds) {\n await this.pub.set(key, value, { EX: ttlSeconds });\n } else {\n await this.pub.set(key, value);\n }\n }\n\n async get(key: string): Promise<string | null> {\n return (await this.pub.get(key)) || null;\n }\n\n async mget(keys: string[]): Promise<(string | null)[]> {\n if (keys.length === 0) return [];\n return await this.pub.mGet(keys);\n }\n\n async del(key: string): Promise<void> {\n await this.pub.del(key);\n }\n\n async xadd(\n stream: string,\n args: Record<string, string>,\n maxLen?: number,\n ): Promise<string> {\n const options: any = {};\n if (maxLen) {\n options.MKSTREAM = true; // Make sure stream exists\n // node-redis specific options for MAXLEN\n // But basic xadd signature is (key, id, message, options?)\n }\n // Node Redis v4 xAdd: (key, id, message)\n // For trimming, it might be in options.\n // Let's assume standard usage for now.\n // Actually Node Redis v4: .xAdd(key, id, message, options)\n\n // Construct message object\n return await this.pub.xAdd(stream, \"*\", args, {\n TRIM: maxLen\n ? {\n strategy: \"MAXLEN\",\n strategyModifier: \"~\",\n threshold: maxLen,\n }\n : undefined,\n });\n }\n\n async xaddBatch(\n messages: { stream: string; args: Record<string, string> }[],\n maxLen?: number,\n ): Promise<void> {\n if (messages.length === 0) return;\n const multi = this.pub.multi();\n for (const msg of messages) {\n multi.xAdd(msg.stream, \"*\", msg.args, {\n TRIM: maxLen\n ? {\n strategy: \"MAXLEN\",\n strategyModifier: \"~\",\n threshold: maxLen,\n }\n : undefined,\n });\n }\n await multi.exec();\n }\n\n async xread(\n streams: { key: string; id: string }[],\n count?: number,\n block?: number,\n ): Promise<StreamEntry[] | null> {\n // Node Redis v4 .xRead(streams, options)\n const options: any = {};\n if (count) options.COUNT = count;\n if (typeof block === \"number\") options.BLOCK = block;\n\n const streamsParam = streams.map((s) => ({\n key: s.key,\n id: s.id,\n }));\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const client = block && this.blocking ? this.blocking : this.pub;\n const result = (await client.xRead(streamsParam, options)) as any;\n\n if (!result || result.length === 0) return null;\n\n // Node Redis v4 returns: { name: string, messages: { id: string, message: Record<string,string> }[] }[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return result.map((entry: any) => ({\n stream: entry.name,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n messages: entry.messages.map((msg: any) => ({\n id: msg.id,\n data: msg.message,\n })),\n }));\n }\n\n async xlen(stream: string): Promise<number> {\n return (await this.pub.xLen(stream)) as number;\n }\n\n async disconnect(): Promise<void> {\n await Promise.all([this.pub.disconnect(), this.sub.disconnect()]);\n }\n\n async setPresenceBatch(\n entries: { key: string; value: string; ttlSeconds: number }[],\n ): Promise<void> {\n if (entries.length === 0) return;\n const multi = this.pub.multi();\n for (const { key, value, ttlSeconds } of entries) {\n multi.set(key, value, { EX: ttlSeconds });\n }\n await multi.exec();\n }\n\n async expire(key: string, ttlSeconds: number): Promise<void> {\n await this.pub.expire(key, ttlSeconds);\n }\n\n onError(handler: (err: Error) => void): () => void {\n this.pub.on(\"error\", handler);\n return () => this.pub.removeListener(\"error\", handler);\n }\n\n onReconnect(handler: () => void): () => void {\n this.pub.on(\"connect\", handler);\n return () => this.pub.removeListener(\"connect\", handler);\n }\n}\n\nexport function createDriver(\n pub: any,\n sub: any,\n blocking?: any,\n): RedisPubSubDriver {\n // Simple heuristic: Node Redis v4 clients usually have 'isOpen' boolean\n if (sub.isOpen !== undefined && typeof sub.subscribe === \"function\") {\n return new NodeRedisDriver(pub, sub, blocking);\n }\n // Default to IoRedis / Generic\n return new IoRedisDriver(pub, sub, blocking);\n}\n"],"mappings":"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","names":[]}
|
package/dist/adapters/redis.mjs
CHANGED
|
@@ -108,6 +108,25 @@ var IoRedisDriver = class {
|
|
|
108
108
|
};
|
|
109
109
|
await Promise.all([close(this.pub), close(this.sub)]);
|
|
110
110
|
}
|
|
111
|
+
async setPresenceBatch(entries) {
|
|
112
|
+
if (entries.length === 0) return;
|
|
113
|
+
const pipeline = this.pub.pipeline();
|
|
114
|
+
for (const { key, value, ttlSeconds } of entries) {
|
|
115
|
+
pipeline.set(key, value, "EX", ttlSeconds);
|
|
116
|
+
}
|
|
117
|
+
await pipeline.exec();
|
|
118
|
+
}
|
|
119
|
+
async expire(key, ttlSeconds) {
|
|
120
|
+
await this.pub.expire(key, ttlSeconds);
|
|
121
|
+
}
|
|
122
|
+
onError(handler) {
|
|
123
|
+
this.pub.on("error", handler);
|
|
124
|
+
return () => this.pub.removeListener("error", handler);
|
|
125
|
+
}
|
|
126
|
+
onReconnect(handler) {
|
|
127
|
+
this.pub.on("connect", handler);
|
|
128
|
+
return () => this.pub.removeListener("connect", handler);
|
|
129
|
+
}
|
|
111
130
|
};
|
|
112
131
|
var NodeRedisDriver = class {
|
|
113
132
|
constructor(pub, sub, blocking) {
|
|
@@ -194,6 +213,25 @@ var NodeRedisDriver = class {
|
|
|
194
213
|
async disconnect() {
|
|
195
214
|
await Promise.all([this.pub.disconnect(), this.sub.disconnect()]);
|
|
196
215
|
}
|
|
216
|
+
async setPresenceBatch(entries) {
|
|
217
|
+
if (entries.length === 0) return;
|
|
218
|
+
const multi = this.pub.multi();
|
|
219
|
+
for (const { key, value, ttlSeconds } of entries) {
|
|
220
|
+
multi.set(key, value, { EX: ttlSeconds });
|
|
221
|
+
}
|
|
222
|
+
await multi.exec();
|
|
223
|
+
}
|
|
224
|
+
async expire(key, ttlSeconds) {
|
|
225
|
+
await this.pub.expire(key, ttlSeconds);
|
|
226
|
+
}
|
|
227
|
+
onError(handler) {
|
|
228
|
+
this.pub.on("error", handler);
|
|
229
|
+
return () => this.pub.removeListener("error", handler);
|
|
230
|
+
}
|
|
231
|
+
onReconnect(handler) {
|
|
232
|
+
this.pub.on("connect", handler);
|
|
233
|
+
return () => this.pub.removeListener("connect", handler);
|
|
234
|
+
}
|
|
197
235
|
};
|
|
198
236
|
function createDriver(pub, sub, blocking) {
|
|
199
237
|
if (sub.isOpen !== void 0 && typeof sub.subscribe === "function") {
|
|
@@ -207,6 +245,8 @@ var RedisAdapter = class {
|
|
|
207
245
|
_driver;
|
|
208
246
|
_prefix;
|
|
209
247
|
_streamMaxLen;
|
|
248
|
+
_streamTtlSeconds;
|
|
249
|
+
_presenceTtlSeconds;
|
|
210
250
|
_handlers = /* @__PURE__ */ new Map();
|
|
211
251
|
_streamOffsets = /* @__PURE__ */ new Map();
|
|
212
252
|
// streamKey -> lastId
|
|
@@ -214,20 +254,48 @@ var RedisAdapter = class {
|
|
|
214
254
|
// Active streams to poll
|
|
215
255
|
_polling = false;
|
|
216
256
|
_closed = false;
|
|
257
|
+
// C4: Per-stream sequence counter for message ordering
|
|
258
|
+
_sequenceCounters = /* @__PURE__ */ new Map();
|
|
259
|
+
// C3: Rehydration callbacks
|
|
260
|
+
_unsubError;
|
|
261
|
+
_unsubReconnect;
|
|
262
|
+
// Stored presence entries for rehydration on reconnect
|
|
263
|
+
_presenceCache = /* @__PURE__ */ new Map();
|
|
217
264
|
constructor(options) {
|
|
218
265
|
this._prefix = options.prefix ?? "ocpp-ws-io:";
|
|
219
266
|
this._streamMaxLen = options.streamMaxLen ?? 1e3;
|
|
267
|
+
this._streamTtlSeconds = options.streamTtlSeconds ?? 300;
|
|
268
|
+
this._presenceTtlSeconds = options.presenceTtlSeconds ?? 300;
|
|
220
269
|
this._driver = createDriver(
|
|
221
270
|
options.pubClient,
|
|
222
271
|
options.subClient,
|
|
223
272
|
options.blockingClient
|
|
224
273
|
);
|
|
274
|
+
if (this._driver.onError) {
|
|
275
|
+
this._unsubError = this._driver.onError((err) => {
|
|
276
|
+
console.error("[RedisAdapter] Redis error:", err.message);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (this._driver.onReconnect) {
|
|
280
|
+
this._unsubReconnect = this._driver.onReconnect(() => {
|
|
281
|
+
this._rehydratePresence().catch(() => {
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
225
285
|
}
|
|
226
286
|
async publish(channel, data) {
|
|
227
287
|
const prefixedChannel = this._prefix + channel;
|
|
288
|
+
const payload = data;
|
|
289
|
+
if (payload && typeof payload === "object" && channel.startsWith("ocpp:node:")) {
|
|
290
|
+
const seq = (this._sequenceCounters.get(channel) ?? 0) + 1;
|
|
291
|
+
this._sequenceCounters.set(channel, seq);
|
|
292
|
+
payload.__seq = seq;
|
|
293
|
+
}
|
|
228
294
|
const message = JSON.stringify(data);
|
|
229
295
|
if (channel.startsWith("ocpp:node:")) {
|
|
230
296
|
await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);
|
|
297
|
+
await this._driver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
|
|
298
|
+
});
|
|
231
299
|
} else {
|
|
232
300
|
await this._driver.publish(prefixedChannel, message);
|
|
233
301
|
}
|
|
@@ -293,6 +361,10 @@ var RedisAdapter = class {
|
|
|
293
361
|
this._closed = true;
|
|
294
362
|
this._handlers.clear();
|
|
295
363
|
this._streams.clear();
|
|
364
|
+
this._presenceCache.clear();
|
|
365
|
+
this._sequenceCounters.clear();
|
|
366
|
+
if (this._unsubError) this._unsubError();
|
|
367
|
+
if (this._unsubReconnect) this._unsubReconnect();
|
|
296
368
|
await this._driver.disconnect();
|
|
297
369
|
}
|
|
298
370
|
_handleMessage(channel, message) {
|
|
@@ -352,6 +424,7 @@ var RedisAdapter = class {
|
|
|
352
424
|
// ─── Presence Registry ─────────────────────────────────────────────
|
|
353
425
|
async setPresence(identity, nodeId, ttl) {
|
|
354
426
|
const key = `${this._prefix}presence:${identity}`;
|
|
427
|
+
this._presenceCache.set(identity, { nodeId, ttl });
|
|
355
428
|
await this._driver.set(key, nodeId, ttl);
|
|
356
429
|
}
|
|
357
430
|
async getPresence(identity) {
|
|
@@ -389,6 +462,37 @@ var RedisAdapter = class {
|
|
|
389
462
|
streamDetails
|
|
390
463
|
};
|
|
391
464
|
}
|
|
465
|
+
// ─── C1: Batch Presence Pipeline ────────────────────────────────────
|
|
466
|
+
/**
|
|
467
|
+
* Set multiple presence entries in a single Redis pipeline.
|
|
468
|
+
* Reduces N network round-trips to 1 for bulk presence updates.
|
|
469
|
+
*/
|
|
470
|
+
async setPresenceBatch(entries) {
|
|
471
|
+
if (entries.length === 0) return;
|
|
472
|
+
const batchEntries = entries.map(({ identity, nodeId, ttl }) => {
|
|
473
|
+
const key = `${this._prefix}presence:${identity}`;
|
|
474
|
+
const ttlSeconds = ttl ?? this._presenceTtlSeconds;
|
|
475
|
+
this._presenceCache.set(identity, { nodeId, ttl: ttlSeconds });
|
|
476
|
+
return { key, value: nodeId, ttlSeconds };
|
|
477
|
+
});
|
|
478
|
+
await this._driver.setPresenceBatch(batchEntries);
|
|
479
|
+
}
|
|
480
|
+
// ─── C3: Redis Failure Rehydration ──────────────────────────────────
|
|
481
|
+
/**
|
|
482
|
+
* Re-syncs all cached presence entries to Redis after a reconnection.
|
|
483
|
+
* Called automatically when the Redis client reconnects.
|
|
484
|
+
*/
|
|
485
|
+
async _rehydratePresence() {
|
|
486
|
+
if (this._presenceCache.size === 0) return;
|
|
487
|
+
const entries = Array.from(this._presenceCache.entries()).map(
|
|
488
|
+
([identity, { nodeId, ttl }]) => ({
|
|
489
|
+
key: `${this._prefix}presence:${identity}`,
|
|
490
|
+
value: nodeId,
|
|
491
|
+
ttlSeconds: ttl
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
await this._driver.setPresenceBatch(entries);
|
|
495
|
+
}
|
|
392
496
|
};
|
|
393
497
|
export {
|
|
394
498
|
RedisAdapter
|