ocpp-ws-io 2.1.4 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/redis.d.mts +1 -1
- package/dist/adapters/redis.d.ts +1 -1
- package/dist/adapters/redis.js +2 -2
- package/dist/adapters/redis.js.map +1 -1
- package/dist/adapters/redis.mjs +2 -2
- package/dist/adapters/redis.mjs.map +1 -1
- package/dist/browser.d.mts +5 -0
- package/dist/browser.d.ts +5 -0
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/{index-CagcFzyZ.d.mts → index-1QBeqAuc.d.mts} +34 -2
- package/dist/{index-CagcFzyZ.d.ts → index-1QBeqAuc.d.ts} +34 -2
- package/dist/index.d.mts +26 -8
- package/dist/index.d.ts +26 -8
- package/dist/index.js +109 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +108 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/adapters/redis.d.ts
CHANGED
package/dist/adapters/redis.js
CHANGED
|
@@ -280,9 +280,9 @@ var RedisAdapter = class {
|
|
|
280
280
|
// Active streams to poll
|
|
281
281
|
_polling = false;
|
|
282
282
|
_closed = false;
|
|
283
|
-
//
|
|
283
|
+
// Per-stream sequence counter for message ordering
|
|
284
284
|
_sequenceCounters = /* @__PURE__ */ new Map();
|
|
285
|
-
//
|
|
285
|
+
// Rehydration callbacks
|
|
286
286
|
_unsubError;
|
|
287
287
|
_unsubReconnect;
|
|
288
288
|
// Stored presence entries for rehydration on reconnect
|
|
@@ -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 * 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":[]}
|
|
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 // Per-stream sequence counter for message ordering\n private _sequenceCounters = new Map<string, number>();\n\n // 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 // 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 // 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 // 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
|
@@ -254,9 +254,9 @@ var RedisAdapter = class {
|
|
|
254
254
|
// Active streams to poll
|
|
255
255
|
_polling = false;
|
|
256
256
|
_closed = false;
|
|
257
|
-
//
|
|
257
|
+
// Per-stream sequence counter for message ordering
|
|
258
258
|
_sequenceCounters = /* @__PURE__ */ new Map();
|
|
259
|
-
//
|
|
259
|
+
// Rehydration callbacks
|
|
260
260
|
_unsubError;
|
|
261
261
|
_unsubReconnect;
|
|
262
262
|
// Stored presence entries for rehydration on reconnect
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/redis/helpers.ts","../../src/adapters/redis/index.ts"],"sourcesContent":["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","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"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/redis/helpers.ts","../../src/adapters/redis/index.ts"],"sourcesContent":["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","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 // Per-stream sequence counter for message ordering\n private _sequenceCounters = new Map<string, number>();\n\n // 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 // 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 // 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 // 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"],"mappings":"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","names":[]}
|
package/dist/browser.d.mts
CHANGED
|
@@ -4456,6 +4456,9 @@ interface ValidatorSchema {
|
|
|
4456
4456
|
/**
|
|
4457
4457
|
* Schema validator using AJV for OCPP message validation.
|
|
4458
4458
|
* Each validator is bound to a specific subprotocol version.
|
|
4459
|
+
*
|
|
4460
|
+
* E2: Schemas are registered at construction time but compiled lazily
|
|
4461
|
+
* on first use, reducing startup time from ~400ms to ~5ms.
|
|
4459
4462
|
*/
|
|
4460
4463
|
declare class Validator {
|
|
4461
4464
|
readonly subprotocol: string;
|
|
@@ -4469,6 +4472,8 @@ declare class Validator {
|
|
|
4469
4472
|
/**
|
|
4470
4473
|
* Validate a payload against a schema identified by its $id.
|
|
4471
4474
|
* Throws a typed RPCError if validation fails.
|
|
4475
|
+
*
|
|
4476
|
+
* E2: Schema is compiled on first call to this method (lazy).
|
|
4472
4477
|
*/
|
|
4473
4478
|
validate(schemaId: string, params: unknown): void;
|
|
4474
4479
|
/**
|
package/dist/browser.d.ts
CHANGED
|
@@ -4456,6 +4456,9 @@ interface ValidatorSchema {
|
|
|
4456
4456
|
/**
|
|
4457
4457
|
* Schema validator using AJV for OCPP message validation.
|
|
4458
4458
|
* Each validator is bound to a specific subprotocol version.
|
|
4459
|
+
*
|
|
4460
|
+
* E2: Schemas are registered at construction time but compiled lazily
|
|
4461
|
+
* on first use, reducing startup time from ~400ms to ~5ms.
|
|
4459
4462
|
*/
|
|
4460
4463
|
declare class Validator {
|
|
4461
4464
|
readonly subprotocol: string;
|
|
@@ -4469,6 +4472,8 @@ declare class Validator {
|
|
|
4469
4472
|
/**
|
|
4470
4473
|
* Validate a payload against a schema identified by its $id.
|
|
4471
4474
|
* Throws a typed RPCError if validation fails.
|
|
4475
|
+
*
|
|
4476
|
+
* E2: Schema is compiled on first call to this method (lazy).
|
|
4472
4477
|
*/
|
|
4473
4478
|
validate(schemaId: string, params: unknown): void;
|
|
4474
4479
|
/**
|