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 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 clustering support.
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
  [![npm version](https://img.shields.io/npm/v/ocpp-ws-io.svg)](https://www.npmjs.com/package/ocpp-ws-io)
8
8
  [![License](https://img.shields.io/npm/l/ocpp-ws-io.svg)](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** via [voltlog-io](https://www.npmjs.com/package/voltlog-io).
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
- ````typescript
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.
@@ -1,4 +1,4 @@
1
- export { W as RedisAdapter, a0 as RedisAdapterOptions } from '../index-BixJj_yJ.mjs';
1
+ export { W as RedisAdapter, a0 as RedisAdapterOptions } from '../index-CagcFzyZ.mjs';
2
2
  import 'ws';
3
3
  import 'node:https';
4
4
  import 'node:http';
@@ -1,4 +1,4 @@
1
- export { W as RedisAdapter, a0 as RedisAdapterOptions } from '../index-BixJj_yJ.js';
1
+ export { W as RedisAdapter, a0 as RedisAdapterOptions } from '../index-CagcFzyZ.js';
2
2
  import 'ws';
3
3
  import 'node:https';
4
4
  import 'node:http';
@@ -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":[]}
@@ -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