ocpp-ws-io 2.1.3 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -44,6 +44,11 @@ var InMemoryAdapter = class {
44
44
  async removePresence(identity) {
45
45
  this._presence.delete(identity);
46
46
  }
47
+ async setPresenceBatch(entries) {
48
+ for (const { identity, nodeId } of entries) {
49
+ this._presence.set(identity, nodeId);
50
+ }
51
+ }
47
52
  };
48
53
  function defineAdapter(adapter) {
49
54
  return adapter;
@@ -159,6 +164,25 @@ var IoRedisDriver = class {
159
164
  };
160
165
  await Promise.all([close(this.pub), close(this.sub)]);
161
166
  }
167
+ async setPresenceBatch(entries) {
168
+ if (entries.length === 0) return;
169
+ const pipeline = this.pub.pipeline();
170
+ for (const { key, value, ttlSeconds } of entries) {
171
+ pipeline.set(key, value, "EX", ttlSeconds);
172
+ }
173
+ await pipeline.exec();
174
+ }
175
+ async expire(key, ttlSeconds) {
176
+ await this.pub.expire(key, ttlSeconds);
177
+ }
178
+ onError(handler) {
179
+ this.pub.on("error", handler);
180
+ return () => this.pub.removeListener("error", handler);
181
+ }
182
+ onReconnect(handler) {
183
+ this.pub.on("connect", handler);
184
+ return () => this.pub.removeListener("connect", handler);
185
+ }
162
186
  };
163
187
  var NodeRedisDriver = class {
164
188
  constructor(pub, sub, blocking) {
@@ -245,6 +269,25 @@ var NodeRedisDriver = class {
245
269
  async disconnect() {
246
270
  await Promise.all([this.pub.disconnect(), this.sub.disconnect()]);
247
271
  }
272
+ async setPresenceBatch(entries) {
273
+ if (entries.length === 0) return;
274
+ const multi = this.pub.multi();
275
+ for (const { key, value, ttlSeconds } of entries) {
276
+ multi.set(key, value, { EX: ttlSeconds });
277
+ }
278
+ await multi.exec();
279
+ }
280
+ async expire(key, ttlSeconds) {
281
+ await this.pub.expire(key, ttlSeconds);
282
+ }
283
+ onError(handler) {
284
+ this.pub.on("error", handler);
285
+ return () => this.pub.removeListener("error", handler);
286
+ }
287
+ onReconnect(handler) {
288
+ this.pub.on("connect", handler);
289
+ return () => this.pub.removeListener("connect", handler);
290
+ }
248
291
  };
249
292
  function createDriver(pub, sub, blocking) {
250
293
  if (sub.isOpen !== void 0 && typeof sub.subscribe === "function") {
@@ -258,6 +301,8 @@ var RedisAdapter = class {
258
301
  _driver;
259
302
  _prefix;
260
303
  _streamMaxLen;
304
+ _streamTtlSeconds;
305
+ _presenceTtlSeconds;
261
306
  _handlers = /* @__PURE__ */ new Map();
262
307
  _streamOffsets = /* @__PURE__ */ new Map();
263
308
  // streamKey -> lastId
@@ -265,20 +310,48 @@ var RedisAdapter = class {
265
310
  // Active streams to poll
266
311
  _polling = false;
267
312
  _closed = false;
313
+ // C4: Per-stream sequence counter for message ordering
314
+ _sequenceCounters = /* @__PURE__ */ new Map();
315
+ // C3: Rehydration callbacks
316
+ _unsubError;
317
+ _unsubReconnect;
318
+ // Stored presence entries for rehydration on reconnect
319
+ _presenceCache = /* @__PURE__ */ new Map();
268
320
  constructor(options) {
269
321
  this._prefix = options.prefix ?? "ocpp-ws-io:";
270
322
  this._streamMaxLen = options.streamMaxLen ?? 1e3;
323
+ this._streamTtlSeconds = options.streamTtlSeconds ?? 300;
324
+ this._presenceTtlSeconds = options.presenceTtlSeconds ?? 300;
271
325
  this._driver = createDriver(
272
326
  options.pubClient,
273
327
  options.subClient,
274
328
  options.blockingClient
275
329
  );
330
+ if (this._driver.onError) {
331
+ this._unsubError = this._driver.onError((err) => {
332
+ console.error("[RedisAdapter] Redis error:", err.message);
333
+ });
334
+ }
335
+ if (this._driver.onReconnect) {
336
+ this._unsubReconnect = this._driver.onReconnect(() => {
337
+ this._rehydratePresence().catch(() => {
338
+ });
339
+ });
340
+ }
276
341
  }
277
342
  async publish(channel, data) {
278
343
  const prefixedChannel = this._prefix + channel;
344
+ const payload = data;
345
+ if (payload && typeof payload === "object" && channel.startsWith("ocpp:node:")) {
346
+ const seq = (this._sequenceCounters.get(channel) ?? 0) + 1;
347
+ this._sequenceCounters.set(channel, seq);
348
+ payload.__seq = seq;
349
+ }
279
350
  const message = JSON.stringify(data);
280
351
  if (channel.startsWith("ocpp:node:")) {
281
352
  await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);
353
+ await this._driver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
354
+ });
282
355
  } else {
283
356
  await this._driver.publish(prefixedChannel, message);
284
357
  }
@@ -344,6 +417,10 @@ var RedisAdapter = class {
344
417
  this._closed = true;
345
418
  this._handlers.clear();
346
419
  this._streams.clear();
420
+ this._presenceCache.clear();
421
+ this._sequenceCounters.clear();
422
+ if (this._unsubError) this._unsubError();
423
+ if (this._unsubReconnect) this._unsubReconnect();
347
424
  await this._driver.disconnect();
348
425
  }
349
426
  _handleMessage(channel, message) {
@@ -403,6 +480,7 @@ var RedisAdapter = class {
403
480
  // ─── Presence Registry ─────────────────────────────────────────────
404
481
  async setPresence(identity, nodeId, ttl) {
405
482
  const key = `${this._prefix}presence:${identity}`;
483
+ this._presenceCache.set(identity, { nodeId, ttl });
406
484
  await this._driver.set(key, nodeId, ttl);
407
485
  }
408
486
  async getPresence(identity) {
@@ -440,6 +518,37 @@ var RedisAdapter = class {
440
518
  streamDetails
441
519
  };
442
520
  }
521
+ // ─── C1: Batch Presence Pipeline ────────────────────────────────────
522
+ /**
523
+ * Set multiple presence entries in a single Redis pipeline.
524
+ * Reduces N network round-trips to 1 for bulk presence updates.
525
+ */
526
+ async setPresenceBatch(entries) {
527
+ if (entries.length === 0) return;
528
+ const batchEntries = entries.map(({ identity, nodeId, ttl }) => {
529
+ const key = `${this._prefix}presence:${identity}`;
530
+ const ttlSeconds = ttl ?? this._presenceTtlSeconds;
531
+ this._presenceCache.set(identity, { nodeId, ttl: ttlSeconds });
532
+ return { key, value: nodeId, ttlSeconds };
533
+ });
534
+ await this._driver.setPresenceBatch(batchEntries);
535
+ }
536
+ // ─── C3: Redis Failure Rehydration ──────────────────────────────────
537
+ /**
538
+ * Re-syncs all cached presence entries to Redis after a reconnection.
539
+ * Called automatically when the Redis client reconnects.
540
+ */
541
+ async _rehydratePresence() {
542
+ if (this._presenceCache.size === 0) return;
543
+ const entries = Array.from(this._presenceCache.entries()).map(
544
+ ([identity, { nodeId, ttl }]) => ({
545
+ key: `${this._prefix}presence:${identity}`,
546
+ value: nodeId,
547
+ ttlSeconds: ttl
548
+ })
549
+ );
550
+ await this._driver.setPresenceBatch(entries);
551
+ }
443
552
  };
444
553
 
445
554
  // src/client.ts
@@ -37024,6 +37133,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37024
37133
  _badMessageCount = 0;
37025
37134
  _lastActivity = 0;
37026
37135
  _outboundBuffer = [];
37136
+ _offlineQueue = [];
37027
37137
  _middleware;
37028
37138
  _validators = [];
37029
37139
  _strictProtocols = null;
@@ -37178,6 +37288,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37178
37288
  }
37179
37289
  this._attachWebsocket(ws);
37180
37290
  this._startPing();
37291
+ this._flushOfflineQueue();
37181
37292
  if (this._outboundBuffer.length > 0) {
37182
37293
  const buffer = this._outboundBuffer;
37183
37294
  this._outboundBuffer = [];
@@ -37359,8 +37470,32 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37359
37470
  options = args[2] ?? {};
37360
37471
  }
37361
37472
  if (this._state !== OPEN) {
37473
+ if (this._options.offlineQueue && (this._state === CLOSED || this._state === CONNECTING)) {
37474
+ return new Promise((resolve, reject) => {
37475
+ const maxSize = this._options.offlineQueueMaxSize ?? 100;
37476
+ if (this._offlineQueue.length >= maxSize) {
37477
+ this._offlineQueue.shift();
37478
+ this._logger?.warn?.(
37479
+ "Offline queue full \u2014 dropping oldest message",
37480
+ {
37481
+ method,
37482
+ queueSize: this._offlineQueue.length
37483
+ }
37484
+ );
37485
+ }
37486
+ this._offlineQueue.push({ method, params, options, resolve, reject });
37487
+ this._logger?.debug?.("Call queued offline", {
37488
+ method,
37489
+ queueSize: this._offlineQueue.length
37490
+ });
37491
+ });
37492
+ }
37362
37493
  throw new Error(`Cannot call: client is in state ${this._state}`);
37363
37494
  }
37495
+ const maxRetries = options.retries ?? 0;
37496
+ if (maxRetries > 0) {
37497
+ return this._callWithRetry(method, params, options, maxRetries);
37498
+ }
37364
37499
  return this._callQueue.push(() => this._sendCall(method, params, options));
37365
37500
  }
37366
37501
  async safeCall(...args) {
@@ -37385,7 +37520,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37385
37520
  }
37386
37521
  }
37387
37522
  async _sendCall(method, params, options) {
37388
- const msgId = createId();
37523
+ const msgId = options.idempotencyKey ?? createId();
37389
37524
  const timeoutMs = options.timeoutMs ?? this._options.callTimeoutMs;
37390
37525
  const ctx = {
37391
37526
  type: "outgoing_call",
@@ -37433,7 +37568,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37433
37568
  sentAt: Date.now()
37434
37569
  });
37435
37570
  if (this._ws?.readyState === WebSocket.OPEN) {
37436
- this._ws.send(messageStr, (err) => {
37571
+ this._safeSend(this._ws, messageStr, (err) => {
37437
37572
  if (err) {
37438
37573
  clearTimeout(timeoutHandle);
37439
37574
  this._pendingCalls.delete(msgId);
@@ -37780,6 +37915,87 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37780
37915
  }
37781
37916
  }, delayMs);
37782
37917
  }
37918
+ // ─── Internal: Offline Queue ──────────────────────────────────
37919
+ /**
37920
+ * Atomically drains the offline queue and sends each message via _sendCall.
37921
+ * Uses splice(0) to prevent re-entry bugs (double billing) if the connection
37922
+ * drops again mid-flush — the queue is empty before any sends begin.
37923
+ */
37924
+ _flushOfflineQueue() {
37925
+ if (this._offlineQueue.length === 0) return;
37926
+ const snapshot = this._offlineQueue.splice(0, this._offlineQueue.length);
37927
+ this._logger?.info?.("Flushing offline queue", { count: snapshot.length });
37928
+ for (const entry of snapshot) {
37929
+ this._callQueue.push(() => this._sendCall(entry.method, entry.params, entry.options)).then(entry.resolve).catch(entry.reject);
37930
+ }
37931
+ }
37932
+ // ─── Internal: Call Retry with Full Jitter ───────────────────
37933
+ /**
37934
+ * Retry wrapper using Full Jitter exponential backoff.
37935
+ * delay = random(0, min(retryMaxDelayMs, retryDelayMs * 2^attempt))
37936
+ * Only retries on TimeoutError — all other errors propagate immediately.
37937
+ */
37938
+ async _callWithRetry(method, params, options, maxRetries) {
37939
+ const baseDelay = options.retryDelayMs ?? 1e3;
37940
+ const maxDelay = options.retryMaxDelayMs ?? 3e4;
37941
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
37942
+ try {
37943
+ return await this._callQueue.push(
37944
+ () => this._sendCall(method, params, options)
37945
+ );
37946
+ } catch (err) {
37947
+ if (attempt === maxRetries || !(err instanceof TimeoutError)) {
37948
+ throw err;
37949
+ }
37950
+ const expDelay = Math.min(maxDelay, baseDelay * 2 ** attempt);
37951
+ const jitteredDelay = Math.random() * expDelay;
37952
+ this._logger?.warn?.("Call retry", {
37953
+ method,
37954
+ attempt: attempt + 1,
37955
+ maxRetries,
37956
+ delayMs: Math.round(jitteredDelay)
37957
+ });
37958
+ await new Promise((r) => setTimeout(r, jitteredDelay));
37959
+ }
37960
+ }
37961
+ throw new Error("Retry exhausted");
37962
+ }
37963
+ // ─── Internal: Backpressure-Aware Send ───────────────────────
37964
+ /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
37965
+ static _BACKPRESSURE_THRESHOLD = 512 * 1024;
37966
+ /**
37967
+ * Wraps ws.send() with backpressure protection.
37968
+ * If bufferedAmount exceeds the threshold, waits for the buffer to drain
37969
+ * before sending. Prevents OOM on slow 2G/3G charger connections.
37970
+ */
37971
+ _safeSend(ws, data, cb) {
37972
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
37973
+ cb?.(new Error("WebSocket is not open"));
37974
+ return;
37975
+ }
37976
+ if (ws.bufferedAmount > _OCPPClient._BACKPRESSURE_THRESHOLD) {
37977
+ this._logger?.warn?.("Backpressure \u2014 pausing send", {
37978
+ bufferedAmount: ws.bufferedAmount,
37979
+ threshold: _OCPPClient._BACKPRESSURE_THRESHOLD
37980
+ });
37981
+ this.emit("backpressure", { bufferedAmount: ws.bufferedAmount });
37982
+ let waited = 0;
37983
+ const drainCheck = setInterval(() => {
37984
+ waited += 50;
37985
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
37986
+ clearInterval(drainCheck);
37987
+ cb?.(new Error("WebSocket closed during backpressure wait"));
37988
+ return;
37989
+ }
37990
+ if (ws.bufferedAmount <= _OCPPClient._BACKPRESSURE_THRESHOLD || waited >= 1e4) {
37991
+ clearInterval(drainCheck);
37992
+ ws.send(data, cb);
37993
+ }
37994
+ }, 50);
37995
+ } else {
37996
+ ws.send(data, cb);
37997
+ }
37998
+ }
37783
37999
  // ─── Internal: Ping/Pong ─────────────────────────────────────
37784
38000
  _startPing() {
37785
38001
  if (this._options.pingIntervalMs <= 0) return;
@@ -37929,6 +38145,52 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37929
38145
  }
37930
38146
  };
37931
38147
 
38148
+ // src/lru-map.ts
38149
+ var LRUMap = class extends Map {
38150
+ _maxSize;
38151
+ constructor(maxSize) {
38152
+ super();
38153
+ if (maxSize < 1) throw new RangeError("LRUMap maxSize must be >= 1");
38154
+ this._maxSize = maxSize;
38155
+ }
38156
+ /**
38157
+ * Returns the configured maximum capacity of this LRU cache.
38158
+ */
38159
+ get maxSize() {
38160
+ return this._maxSize;
38161
+ }
38162
+ /**
38163
+ * Sets a key-value pair. If the key already exists, it is promoted to the
38164
+ * most-recently-used position. If inserting a new key would exceed capacity,
38165
+ * the oldest (least-recently-used) entry is evicted.
38166
+ */
38167
+ set(key, value) {
38168
+ if (this.has(key)) {
38169
+ this.delete(key);
38170
+ }
38171
+ super.set(key, value);
38172
+ if (this.size > this._maxSize) {
38173
+ const oldest = this.keys().next().value;
38174
+ if (oldest !== void 0) {
38175
+ this.delete(oldest);
38176
+ }
38177
+ }
38178
+ return this;
38179
+ }
38180
+ /**
38181
+ * Gets a value by key and promotes it to the most-recently-used position.
38182
+ * Uses `this.has(key)` instead of a value truthiness check to correctly
38183
+ * handle stored values of `undefined`, `null`, `0`, `""`, etc.
38184
+ */
38185
+ get(key) {
38186
+ if (!this.has(key)) return void 0;
38187
+ const value = super.get(key);
38188
+ this.delete(key);
38189
+ super.set(key, value);
38190
+ return value;
38191
+ }
38192
+ };
38193
+
37932
38194
  // src/router.ts
37933
38195
  import { EventEmitter as EventEmitter2 } from "events";
37934
38196
  async function executeMiddlewareChain(middlewares, ctx) {
@@ -38287,6 +38549,7 @@ var OCPPServerClient = class extends OCPPClient {
38287
38549
  this._ws = context.ws;
38288
38550
  this._protocol = context.protocol ?? context.ws.protocol;
38289
38551
  this._attachServerWebsocket(context.ws);
38552
+ this._startPing();
38290
38553
  }
38291
38554
  // ─── Rate Limiting State ──────────────────────────────────────────
38292
38555
  _rateLimits = {};
@@ -38454,9 +38717,11 @@ var OCPPServer = class extends EventEmitter3 {
38454
38717
  _httpServerAbortControllers = /* @__PURE__ */ new Set();
38455
38718
  _logger = null;
38456
38719
  _globalCORS;
38720
+ // Connection-level rate limiting (per-IP token bucket)
38721
+ _connectionBuckets = /* @__PURE__ */ new Map();
38457
38722
  // Robustness & Clustering
38458
38723
  _nodeId = createId2();
38459
- _sessions = /* @__PURE__ */ new Map();
38724
+ _sessions;
38460
38725
  _gcInterval = null;
38461
38726
  _sessionTimeoutMs;
38462
38727
  constructor(options = {}) {
@@ -38481,6 +38746,8 @@ var OCPPServer = class extends EventEmitter3 {
38481
38746
  ...options
38482
38747
  };
38483
38748
  this._sessionTimeoutMs = this._options.sessionTtlMs;
38749
+ const maxSessions = this._options.maxSessions ?? 5e4;
38750
+ this._sessions = new LRUMap(maxSessions);
38484
38751
  this._wss = new WebSocketServer({ noServer: true });
38485
38752
  this._gcInterval = setInterval(() => {
38486
38753
  const now = Date.now();
@@ -38695,6 +38962,69 @@ var OCPPServer = class extends EventEmitter3 {
38695
38962
  };
38696
38963
  httpServer.on("upgrade", upgradeHandler);
38697
38964
  this._httpServers.add(httpServer);
38965
+ if (this._options.healthEndpoint) {
38966
+ httpServer.on("request", (req, res) => {
38967
+ const url = req.url ?? "";
38968
+ if (url === "/health") {
38969
+ const s = this.stats();
38970
+ const body = JSON.stringify({
38971
+ status: this._state === "OPEN" ? "ok" : "degraded",
38972
+ state: this._state,
38973
+ connectedClients: s.connectedClients,
38974
+ activeSessions: s.activeSessions,
38975
+ uptimeSeconds: Math.round(s.uptimeSeconds),
38976
+ pid: s.pid
38977
+ });
38978
+ res.writeHead(200, {
38979
+ "Content-Type": "application/json",
38980
+ "Cache-Control": "no-cache"
38981
+ });
38982
+ res.end(body);
38983
+ return;
38984
+ }
38985
+ if (url === "/metrics") {
38986
+ const s = this.stats();
38987
+ const lines = [
38988
+ "# HELP ocpp_connected_clients Number of currently connected OCPP clients",
38989
+ "# TYPE ocpp_connected_clients gauge",
38990
+ `ocpp_connected_clients ${s.connectedClients}`,
38991
+ "",
38992
+ "# HELP ocpp_active_sessions Number of active in-memory sessions",
38993
+ "# TYPE ocpp_active_sessions gauge",
38994
+ `ocpp_active_sessions ${s.activeSessions}`,
38995
+ "",
38996
+ "# HELP ocpp_uptime_seconds Process uptime in seconds",
38997
+ "# TYPE ocpp_uptime_seconds gauge",
38998
+ `ocpp_uptime_seconds ${Math.round(s.uptimeSeconds)}`,
38999
+ "",
39000
+ "# HELP ocpp_memory_rss_bytes Resident set size in bytes",
39001
+ "# TYPE ocpp_memory_rss_bytes gauge",
39002
+ `ocpp_memory_rss_bytes ${s.memoryUsage.rss}`,
39003
+ "",
39004
+ "# HELP ocpp_memory_heap_used_bytes V8 heap used in bytes",
39005
+ "# TYPE ocpp_memory_heap_used_bytes gauge",
39006
+ `ocpp_memory_heap_used_bytes ${s.memoryUsage.heapUsed}`,
39007
+ "",
39008
+ "# HELP ocpp_memory_heap_total_bytes V8 heap total in bytes",
39009
+ "# TYPE ocpp_memory_heap_total_bytes gauge",
39010
+ `ocpp_memory_heap_total_bytes ${s.memoryUsage.heapTotal}`,
39011
+ "",
39012
+ "# HELP ocpp_ws_buffered_bytes Total buffered WebSocket bytes",
39013
+ "# TYPE ocpp_ws_buffered_bytes gauge",
39014
+ `ocpp_ws_buffered_bytes ${s.webSockets?.bufferedAmount ?? 0}`,
39015
+ ""
39016
+ ];
39017
+ res.writeHead(200, {
39018
+ "Content-Type": "text/plain; version=0.0.4; charset=utf-8",
39019
+ "Cache-Control": "no-cache"
39020
+ });
39021
+ res.end(lines.join("\n"));
39022
+ return;
39023
+ }
39024
+ res.writeHead(404, { "Content-Type": "text/plain" });
39025
+ res.end("Not Found");
39026
+ });
39027
+ }
38698
39028
  if (options?.signal) {
38699
39029
  const ac = new AbortController();
38700
39030
  this._httpServerAbortControllers.add(ac);
@@ -38758,6 +39088,30 @@ var OCPPServer = class extends EventEmitter3 {
38758
39088
  abortHandshake(socket, 503, "Server is shutting down");
38759
39089
  return;
38760
39090
  }
39091
+ const connRateLimit = this._options.connectionRateLimit;
39092
+ if (connRateLimit) {
39093
+ const ip = req.socket.remoteAddress ?? "unknown";
39094
+ const now = Date.now();
39095
+ let bucket = this._connectionBuckets.get(ip);
39096
+ if (!bucket) {
39097
+ bucket = { tokens: connRateLimit.limit, lastRefill: now };
39098
+ this._connectionBuckets.set(ip, bucket);
39099
+ } else {
39100
+ const elapsed = now - bucket.lastRefill;
39101
+ const refillRate = connRateLimit.limit / connRateLimit.windowMs;
39102
+ bucket.tokens = Math.min(
39103
+ connRateLimit.limit,
39104
+ bucket.tokens + elapsed * refillRate
39105
+ );
39106
+ bucket.lastRefill = now;
39107
+ }
39108
+ if (bucket.tokens < 1) {
39109
+ this._logger?.warn?.("Connection rate limit exceeded", { ip });
39110
+ abortHandshake(socket, 429, "Too Many Requests");
39111
+ return;
39112
+ }
39113
+ bucket.tokens -= 1;
39114
+ }
38761
39115
  if (socket.readyState !== "open") {
38762
39116
  this._logger?.debug?.("Socket not open at upgrade start");
38763
39117
  if (!socket.destroyed) socket.destroy();
@@ -39076,6 +39430,20 @@ var OCPPServer = class extends EventEmitter3 {
39076
39430
  protocol: selectedProtocol
39077
39431
  });
39078
39432
  this._updateSessionActivity(identity, client.session);
39433
+ const existingClient = this._clientsByIdentity.get(identity);
39434
+ if (existingClient && existingClient !== client) {
39435
+ this._logger?.warn?.("Evicting stale connection for identity", {
39436
+ identity,
39437
+ reason: "Duplicate identity replaced by new connection"
39438
+ });
39439
+ existingClient.close({
39440
+ code: 4e3,
39441
+ reason: "Evicted by new connection",
39442
+ force: true
39443
+ }).catch(() => {
39444
+ });
39445
+ this._clients.delete(existingClient);
39446
+ }
39079
39447
  this._clients.add(client);
39080
39448
  this._clientsByIdentity.set(identity, client);
39081
39449
  if (this._adapter?.setPresence) {
@@ -39131,6 +39499,29 @@ var OCPPServer = class extends EventEmitter3 {
39131
39499
  clearInterval(this._gcInterval);
39132
39500
  this._gcInterval = null;
39133
39501
  }
39502
+ if (!options.force) {
39503
+ const drainTimeout = 5e3;
39504
+ const drainPromises = Array.from(this._clients).map(async (client) => {
39505
+ const ws = client._ws;
39506
+ if (ws && ws.bufferedAmount > 0) {
39507
+ this._logger?.debug?.("Waiting for client buffer to drain", {
39508
+ identity: client.identity,
39509
+ bufferedAmount: ws.bufferedAmount
39510
+ });
39511
+ await new Promise((resolve) => {
39512
+ let elapsed = 0;
39513
+ const check = setInterval(() => {
39514
+ elapsed += 50;
39515
+ if (!ws || ws.bufferedAmount === 0 || elapsed >= drainTimeout) {
39516
+ clearInterval(check);
39517
+ resolve();
39518
+ }
39519
+ }, 50);
39520
+ });
39521
+ }
39522
+ });
39523
+ await Promise.allSettled(drainPromises);
39524
+ }
39134
39525
  const closePromises = Array.from(this._clients).map(
39135
39526
  (client) => client.close(options).catch(() => {
39136
39527
  })
@@ -39357,6 +39748,7 @@ var OCPPServer = class extends EventEmitter3 {
39357
39748
  export {
39358
39749
  ConnectionState,
39359
39750
  InMemoryAdapter,
39751
+ LRUMap,
39360
39752
  MessageType,
39361
39753
  MiddlewareStack,
39362
39754
  NOREPLY,