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.js CHANGED
@@ -32,6 +32,7 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  ConnectionState: () => ConnectionState,
34
34
  InMemoryAdapter: () => InMemoryAdapter,
35
+ LRUMap: () => LRUMap,
35
36
  MessageType: () => MessageType,
36
37
  MiddlewareStack: () => MiddlewareStack,
37
38
  NOREPLY: () => NOREPLY,
@@ -119,6 +120,11 @@ var InMemoryAdapter = class {
119
120
  async removePresence(identity) {
120
121
  this._presence.delete(identity);
121
122
  }
123
+ async setPresenceBatch(entries) {
124
+ for (const { identity, nodeId } of entries) {
125
+ this._presence.set(identity, nodeId);
126
+ }
127
+ }
122
128
  };
123
129
  function defineAdapter(adapter) {
124
130
  return adapter;
@@ -234,6 +240,25 @@ var IoRedisDriver = class {
234
240
  };
235
241
  await Promise.all([close(this.pub), close(this.sub)]);
236
242
  }
243
+ async setPresenceBatch(entries) {
244
+ if (entries.length === 0) return;
245
+ const pipeline = this.pub.pipeline();
246
+ for (const { key, value, ttlSeconds } of entries) {
247
+ pipeline.set(key, value, "EX", ttlSeconds);
248
+ }
249
+ await pipeline.exec();
250
+ }
251
+ async expire(key, ttlSeconds) {
252
+ await this.pub.expire(key, ttlSeconds);
253
+ }
254
+ onError(handler) {
255
+ this.pub.on("error", handler);
256
+ return () => this.pub.removeListener("error", handler);
257
+ }
258
+ onReconnect(handler) {
259
+ this.pub.on("connect", handler);
260
+ return () => this.pub.removeListener("connect", handler);
261
+ }
237
262
  };
238
263
  var NodeRedisDriver = class {
239
264
  constructor(pub, sub, blocking) {
@@ -320,6 +345,25 @@ var NodeRedisDriver = class {
320
345
  async disconnect() {
321
346
  await Promise.all([this.pub.disconnect(), this.sub.disconnect()]);
322
347
  }
348
+ async setPresenceBatch(entries) {
349
+ if (entries.length === 0) return;
350
+ const multi = this.pub.multi();
351
+ for (const { key, value, ttlSeconds } of entries) {
352
+ multi.set(key, value, { EX: ttlSeconds });
353
+ }
354
+ await multi.exec();
355
+ }
356
+ async expire(key, ttlSeconds) {
357
+ await this.pub.expire(key, ttlSeconds);
358
+ }
359
+ onError(handler) {
360
+ this.pub.on("error", handler);
361
+ return () => this.pub.removeListener("error", handler);
362
+ }
363
+ onReconnect(handler) {
364
+ this.pub.on("connect", handler);
365
+ return () => this.pub.removeListener("connect", handler);
366
+ }
323
367
  };
324
368
  function createDriver(pub, sub, blocking) {
325
369
  if (sub.isOpen !== void 0 && typeof sub.subscribe === "function") {
@@ -333,6 +377,8 @@ var RedisAdapter = class {
333
377
  _driver;
334
378
  _prefix;
335
379
  _streamMaxLen;
380
+ _streamTtlSeconds;
381
+ _presenceTtlSeconds;
336
382
  _handlers = /* @__PURE__ */ new Map();
337
383
  _streamOffsets = /* @__PURE__ */ new Map();
338
384
  // streamKey -> lastId
@@ -340,20 +386,48 @@ var RedisAdapter = class {
340
386
  // Active streams to poll
341
387
  _polling = false;
342
388
  _closed = false;
389
+ // C4: Per-stream sequence counter for message ordering
390
+ _sequenceCounters = /* @__PURE__ */ new Map();
391
+ // C3: Rehydration callbacks
392
+ _unsubError;
393
+ _unsubReconnect;
394
+ // Stored presence entries for rehydration on reconnect
395
+ _presenceCache = /* @__PURE__ */ new Map();
343
396
  constructor(options) {
344
397
  this._prefix = options.prefix ?? "ocpp-ws-io:";
345
398
  this._streamMaxLen = options.streamMaxLen ?? 1e3;
399
+ this._streamTtlSeconds = options.streamTtlSeconds ?? 300;
400
+ this._presenceTtlSeconds = options.presenceTtlSeconds ?? 300;
346
401
  this._driver = createDriver(
347
402
  options.pubClient,
348
403
  options.subClient,
349
404
  options.blockingClient
350
405
  );
406
+ if (this._driver.onError) {
407
+ this._unsubError = this._driver.onError((err) => {
408
+ console.error("[RedisAdapter] Redis error:", err.message);
409
+ });
410
+ }
411
+ if (this._driver.onReconnect) {
412
+ this._unsubReconnect = this._driver.onReconnect(() => {
413
+ this._rehydratePresence().catch(() => {
414
+ });
415
+ });
416
+ }
351
417
  }
352
418
  async publish(channel, data) {
353
419
  const prefixedChannel = this._prefix + channel;
420
+ const payload = data;
421
+ if (payload && typeof payload === "object" && channel.startsWith("ocpp:node:")) {
422
+ const seq = (this._sequenceCounters.get(channel) ?? 0) + 1;
423
+ this._sequenceCounters.set(channel, seq);
424
+ payload.__seq = seq;
425
+ }
354
426
  const message = JSON.stringify(data);
355
427
  if (channel.startsWith("ocpp:node:")) {
356
428
  await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);
429
+ await this._driver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
430
+ });
357
431
  } else {
358
432
  await this._driver.publish(prefixedChannel, message);
359
433
  }
@@ -419,6 +493,10 @@ var RedisAdapter = class {
419
493
  this._closed = true;
420
494
  this._handlers.clear();
421
495
  this._streams.clear();
496
+ this._presenceCache.clear();
497
+ this._sequenceCounters.clear();
498
+ if (this._unsubError) this._unsubError();
499
+ if (this._unsubReconnect) this._unsubReconnect();
422
500
  await this._driver.disconnect();
423
501
  }
424
502
  _handleMessage(channel, message) {
@@ -478,6 +556,7 @@ var RedisAdapter = class {
478
556
  // ─── Presence Registry ─────────────────────────────────────────────
479
557
  async setPresence(identity, nodeId, ttl) {
480
558
  const key = `${this._prefix}presence:${identity}`;
559
+ this._presenceCache.set(identity, { nodeId, ttl });
481
560
  await this._driver.set(key, nodeId, ttl);
482
561
  }
483
562
  async getPresence(identity) {
@@ -515,6 +594,37 @@ var RedisAdapter = class {
515
594
  streamDetails
516
595
  };
517
596
  }
597
+ // ─── C1: Batch Presence Pipeline ────────────────────────────────────
598
+ /**
599
+ * Set multiple presence entries in a single Redis pipeline.
600
+ * Reduces N network round-trips to 1 for bulk presence updates.
601
+ */
602
+ async setPresenceBatch(entries) {
603
+ if (entries.length === 0) return;
604
+ const batchEntries = entries.map(({ identity, nodeId, ttl }) => {
605
+ const key = `${this._prefix}presence:${identity}`;
606
+ const ttlSeconds = ttl ?? this._presenceTtlSeconds;
607
+ this._presenceCache.set(identity, { nodeId, ttl: ttlSeconds });
608
+ return { key, value: nodeId, ttlSeconds };
609
+ });
610
+ await this._driver.setPresenceBatch(batchEntries);
611
+ }
612
+ // ─── C3: Redis Failure Rehydration ──────────────────────────────────
613
+ /**
614
+ * Re-syncs all cached presence entries to Redis after a reconnection.
615
+ * Called automatically when the Redis client reconnects.
616
+ */
617
+ async _rehydratePresence() {
618
+ if (this._presenceCache.size === 0) return;
619
+ const entries = Array.from(this._presenceCache.entries()).map(
620
+ ([identity, { nodeId, ttl }]) => ({
621
+ key: `${this._prefix}presence:${identity}`,
622
+ value: nodeId,
623
+ ttlSeconds: ttl
624
+ })
625
+ );
626
+ await this._driver.setPresenceBatch(entries);
627
+ }
518
628
  };
519
629
 
520
630
  // src/client.ts
@@ -37095,6 +37205,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
37095
37205
  _badMessageCount = 0;
37096
37206
  _lastActivity = 0;
37097
37207
  _outboundBuffer = [];
37208
+ _offlineQueue = [];
37098
37209
  _middleware;
37099
37210
  _validators = [];
37100
37211
  _strictProtocols = null;
@@ -37249,6 +37360,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
37249
37360
  }
37250
37361
  this._attachWebsocket(ws);
37251
37362
  this._startPing();
37363
+ this._flushOfflineQueue();
37252
37364
  if (this._outboundBuffer.length > 0) {
37253
37365
  const buffer = this._outboundBuffer;
37254
37366
  this._outboundBuffer = [];
@@ -37430,8 +37542,32 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
37430
37542
  options = args[2] ?? {};
37431
37543
  }
37432
37544
  if (this._state !== OPEN) {
37545
+ if (this._options.offlineQueue && (this._state === CLOSED || this._state === CONNECTING)) {
37546
+ return new Promise((resolve, reject) => {
37547
+ const maxSize = this._options.offlineQueueMaxSize ?? 100;
37548
+ if (this._offlineQueue.length >= maxSize) {
37549
+ this._offlineQueue.shift();
37550
+ this._logger?.warn?.(
37551
+ "Offline queue full \u2014 dropping oldest message",
37552
+ {
37553
+ method,
37554
+ queueSize: this._offlineQueue.length
37555
+ }
37556
+ );
37557
+ }
37558
+ this._offlineQueue.push({ method, params, options, resolve, reject });
37559
+ this._logger?.debug?.("Call queued offline", {
37560
+ method,
37561
+ queueSize: this._offlineQueue.length
37562
+ });
37563
+ });
37564
+ }
37433
37565
  throw new Error(`Cannot call: client is in state ${this._state}`);
37434
37566
  }
37567
+ const maxRetries = options.retries ?? 0;
37568
+ if (maxRetries > 0) {
37569
+ return this._callWithRetry(method, params, options, maxRetries);
37570
+ }
37435
37571
  return this._callQueue.push(() => this._sendCall(method, params, options));
37436
37572
  }
37437
37573
  async safeCall(...args) {
@@ -37456,7 +37592,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
37456
37592
  }
37457
37593
  }
37458
37594
  async _sendCall(method, params, options) {
37459
- const msgId = (0, import_cuid2.createId)();
37595
+ const msgId = options.idempotencyKey ?? (0, import_cuid2.createId)();
37460
37596
  const timeoutMs = options.timeoutMs ?? this._options.callTimeoutMs;
37461
37597
  const ctx = {
37462
37598
  type: "outgoing_call",
@@ -37504,7 +37640,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
37504
37640
  sentAt: Date.now()
37505
37641
  });
37506
37642
  if (this._ws?.readyState === import_ws.default.OPEN) {
37507
- this._ws.send(messageStr, (err) => {
37643
+ this._safeSend(this._ws, messageStr, (err) => {
37508
37644
  if (err) {
37509
37645
  clearTimeout(timeoutHandle);
37510
37646
  this._pendingCalls.delete(msgId);
@@ -37851,6 +37987,87 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
37851
37987
  }
37852
37988
  }, delayMs);
37853
37989
  }
37990
+ // ─── Internal: Offline Queue ──────────────────────────────────
37991
+ /**
37992
+ * Atomically drains the offline queue and sends each message via _sendCall.
37993
+ * Uses splice(0) to prevent re-entry bugs (double billing) if the connection
37994
+ * drops again mid-flush — the queue is empty before any sends begin.
37995
+ */
37996
+ _flushOfflineQueue() {
37997
+ if (this._offlineQueue.length === 0) return;
37998
+ const snapshot = this._offlineQueue.splice(0, this._offlineQueue.length);
37999
+ this._logger?.info?.("Flushing offline queue", { count: snapshot.length });
38000
+ for (const entry of snapshot) {
38001
+ this._callQueue.push(() => this._sendCall(entry.method, entry.params, entry.options)).then(entry.resolve).catch(entry.reject);
38002
+ }
38003
+ }
38004
+ // ─── Internal: Call Retry with Full Jitter ───────────────────
38005
+ /**
38006
+ * Retry wrapper using Full Jitter exponential backoff.
38007
+ * delay = random(0, min(retryMaxDelayMs, retryDelayMs * 2^attempt))
38008
+ * Only retries on TimeoutError — all other errors propagate immediately.
38009
+ */
38010
+ async _callWithRetry(method, params, options, maxRetries) {
38011
+ const baseDelay = options.retryDelayMs ?? 1e3;
38012
+ const maxDelay = options.retryMaxDelayMs ?? 3e4;
38013
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
38014
+ try {
38015
+ return await this._callQueue.push(
38016
+ () => this._sendCall(method, params, options)
38017
+ );
38018
+ } catch (err) {
38019
+ if (attempt === maxRetries || !(err instanceof TimeoutError)) {
38020
+ throw err;
38021
+ }
38022
+ const expDelay = Math.min(maxDelay, baseDelay * 2 ** attempt);
38023
+ const jitteredDelay = Math.random() * expDelay;
38024
+ this._logger?.warn?.("Call retry", {
38025
+ method,
38026
+ attempt: attempt + 1,
38027
+ maxRetries,
38028
+ delayMs: Math.round(jitteredDelay)
38029
+ });
38030
+ await new Promise((r) => setTimeout(r, jitteredDelay));
38031
+ }
38032
+ }
38033
+ throw new Error("Retry exhausted");
38034
+ }
38035
+ // ─── Internal: Backpressure-Aware Send ───────────────────────
38036
+ /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
38037
+ static _BACKPRESSURE_THRESHOLD = 512 * 1024;
38038
+ /**
38039
+ * Wraps ws.send() with backpressure protection.
38040
+ * If bufferedAmount exceeds the threshold, waits for the buffer to drain
38041
+ * before sending. Prevents OOM on slow 2G/3G charger connections.
38042
+ */
38043
+ _safeSend(ws, data, cb) {
38044
+ if (!ws || ws.readyState !== import_ws.default.OPEN) {
38045
+ cb?.(new Error("WebSocket is not open"));
38046
+ return;
38047
+ }
38048
+ if (ws.bufferedAmount > _OCPPClient._BACKPRESSURE_THRESHOLD) {
38049
+ this._logger?.warn?.("Backpressure \u2014 pausing send", {
38050
+ bufferedAmount: ws.bufferedAmount,
38051
+ threshold: _OCPPClient._BACKPRESSURE_THRESHOLD
38052
+ });
38053
+ this.emit("backpressure", { bufferedAmount: ws.bufferedAmount });
38054
+ let waited = 0;
38055
+ const drainCheck = setInterval(() => {
38056
+ waited += 50;
38057
+ if (!ws || ws.readyState !== import_ws.default.OPEN) {
38058
+ clearInterval(drainCheck);
38059
+ cb?.(new Error("WebSocket closed during backpressure wait"));
38060
+ return;
38061
+ }
38062
+ if (ws.bufferedAmount <= _OCPPClient._BACKPRESSURE_THRESHOLD || waited >= 1e4) {
38063
+ clearInterval(drainCheck);
38064
+ ws.send(data, cb);
38065
+ }
38066
+ }, 50);
38067
+ } else {
38068
+ ws.send(data, cb);
38069
+ }
38070
+ }
37854
38071
  // ─── Internal: Ping/Pong ─────────────────────────────────────
37855
38072
  _startPing() {
37856
38073
  if (this._options.pingIntervalMs <= 0) return;
@@ -38000,6 +38217,52 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
38000
38217
  }
38001
38218
  };
38002
38219
 
38220
+ // src/lru-map.ts
38221
+ var LRUMap = class extends Map {
38222
+ _maxSize;
38223
+ constructor(maxSize) {
38224
+ super();
38225
+ if (maxSize < 1) throw new RangeError("LRUMap maxSize must be >= 1");
38226
+ this._maxSize = maxSize;
38227
+ }
38228
+ /**
38229
+ * Returns the configured maximum capacity of this LRU cache.
38230
+ */
38231
+ get maxSize() {
38232
+ return this._maxSize;
38233
+ }
38234
+ /**
38235
+ * Sets a key-value pair. If the key already exists, it is promoted to the
38236
+ * most-recently-used position. If inserting a new key would exceed capacity,
38237
+ * the oldest (least-recently-used) entry is evicted.
38238
+ */
38239
+ set(key, value) {
38240
+ if (this.has(key)) {
38241
+ this.delete(key);
38242
+ }
38243
+ super.set(key, value);
38244
+ if (this.size > this._maxSize) {
38245
+ const oldest = this.keys().next().value;
38246
+ if (oldest !== void 0) {
38247
+ this.delete(oldest);
38248
+ }
38249
+ }
38250
+ return this;
38251
+ }
38252
+ /**
38253
+ * Gets a value by key and promotes it to the most-recently-used position.
38254
+ * Uses `this.has(key)` instead of a value truthiness check to correctly
38255
+ * handle stored values of `undefined`, `null`, `0`, `""`, etc.
38256
+ */
38257
+ get(key) {
38258
+ if (!this.has(key)) return void 0;
38259
+ const value = super.get(key);
38260
+ this.delete(key);
38261
+ super.set(key, value);
38262
+ return value;
38263
+ }
38264
+ };
38265
+
38003
38266
  // src/router.ts
38004
38267
  var import_node_events2 = require("events");
38005
38268
  async function executeMiddlewareChain(middlewares, ctx) {
@@ -38356,6 +38619,7 @@ var OCPPServerClient = class extends OCPPClient {
38356
38619
  this._ws = context.ws;
38357
38620
  this._protocol = context.protocol ?? context.ws.protocol;
38358
38621
  this._attachServerWebsocket(context.ws);
38622
+ this._startPing();
38359
38623
  }
38360
38624
  // ─── Rate Limiting State ──────────────────────────────────────────
38361
38625
  _rateLimits = {};
@@ -38523,9 +38787,11 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
38523
38787
  _httpServerAbortControllers = /* @__PURE__ */ new Set();
38524
38788
  _logger = null;
38525
38789
  _globalCORS;
38790
+ // Connection-level rate limiting (per-IP token bucket)
38791
+ _connectionBuckets = /* @__PURE__ */ new Map();
38526
38792
  // Robustness & Clustering
38527
38793
  _nodeId = (0, import_cuid22.createId)();
38528
- _sessions = /* @__PURE__ */ new Map();
38794
+ _sessions;
38529
38795
  _gcInterval = null;
38530
38796
  _sessionTimeoutMs;
38531
38797
  constructor(options = {}) {
@@ -38550,6 +38816,8 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
38550
38816
  ...options
38551
38817
  };
38552
38818
  this._sessionTimeoutMs = this._options.sessionTtlMs;
38819
+ const maxSessions = this._options.maxSessions ?? 5e4;
38820
+ this._sessions = new LRUMap(maxSessions);
38553
38821
  this._wss = new import_ws2.WebSocketServer({ noServer: true });
38554
38822
  this._gcInterval = setInterval(() => {
38555
38823
  const now = Date.now();
@@ -38764,6 +39032,69 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
38764
39032
  };
38765
39033
  httpServer.on("upgrade", upgradeHandler);
38766
39034
  this._httpServers.add(httpServer);
39035
+ if (this._options.healthEndpoint) {
39036
+ httpServer.on("request", (req, res) => {
39037
+ const url = req.url ?? "";
39038
+ if (url === "/health") {
39039
+ const s = this.stats();
39040
+ const body = JSON.stringify({
39041
+ status: this._state === "OPEN" ? "ok" : "degraded",
39042
+ state: this._state,
39043
+ connectedClients: s.connectedClients,
39044
+ activeSessions: s.activeSessions,
39045
+ uptimeSeconds: Math.round(s.uptimeSeconds),
39046
+ pid: s.pid
39047
+ });
39048
+ res.writeHead(200, {
39049
+ "Content-Type": "application/json",
39050
+ "Cache-Control": "no-cache"
39051
+ });
39052
+ res.end(body);
39053
+ return;
39054
+ }
39055
+ if (url === "/metrics") {
39056
+ const s = this.stats();
39057
+ const lines = [
39058
+ "# HELP ocpp_connected_clients Number of currently connected OCPP clients",
39059
+ "# TYPE ocpp_connected_clients gauge",
39060
+ `ocpp_connected_clients ${s.connectedClients}`,
39061
+ "",
39062
+ "# HELP ocpp_active_sessions Number of active in-memory sessions",
39063
+ "# TYPE ocpp_active_sessions gauge",
39064
+ `ocpp_active_sessions ${s.activeSessions}`,
39065
+ "",
39066
+ "# HELP ocpp_uptime_seconds Process uptime in seconds",
39067
+ "# TYPE ocpp_uptime_seconds gauge",
39068
+ `ocpp_uptime_seconds ${Math.round(s.uptimeSeconds)}`,
39069
+ "",
39070
+ "# HELP ocpp_memory_rss_bytes Resident set size in bytes",
39071
+ "# TYPE ocpp_memory_rss_bytes gauge",
39072
+ `ocpp_memory_rss_bytes ${s.memoryUsage.rss}`,
39073
+ "",
39074
+ "# HELP ocpp_memory_heap_used_bytes V8 heap used in bytes",
39075
+ "# TYPE ocpp_memory_heap_used_bytes gauge",
39076
+ `ocpp_memory_heap_used_bytes ${s.memoryUsage.heapUsed}`,
39077
+ "",
39078
+ "# HELP ocpp_memory_heap_total_bytes V8 heap total in bytes",
39079
+ "# TYPE ocpp_memory_heap_total_bytes gauge",
39080
+ `ocpp_memory_heap_total_bytes ${s.memoryUsage.heapTotal}`,
39081
+ "",
39082
+ "# HELP ocpp_ws_buffered_bytes Total buffered WebSocket bytes",
39083
+ "# TYPE ocpp_ws_buffered_bytes gauge",
39084
+ `ocpp_ws_buffered_bytes ${s.webSockets?.bufferedAmount ?? 0}`,
39085
+ ""
39086
+ ];
39087
+ res.writeHead(200, {
39088
+ "Content-Type": "text/plain; version=0.0.4; charset=utf-8",
39089
+ "Cache-Control": "no-cache"
39090
+ });
39091
+ res.end(lines.join("\n"));
39092
+ return;
39093
+ }
39094
+ res.writeHead(404, { "Content-Type": "text/plain" });
39095
+ res.end("Not Found");
39096
+ });
39097
+ }
38767
39098
  if (options?.signal) {
38768
39099
  const ac = new AbortController();
38769
39100
  this._httpServerAbortControllers.add(ac);
@@ -38827,6 +39158,30 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
38827
39158
  abortHandshake(socket, 503, "Server is shutting down");
38828
39159
  return;
38829
39160
  }
39161
+ const connRateLimit = this._options.connectionRateLimit;
39162
+ if (connRateLimit) {
39163
+ const ip = req.socket.remoteAddress ?? "unknown";
39164
+ const now = Date.now();
39165
+ let bucket = this._connectionBuckets.get(ip);
39166
+ if (!bucket) {
39167
+ bucket = { tokens: connRateLimit.limit, lastRefill: now };
39168
+ this._connectionBuckets.set(ip, bucket);
39169
+ } else {
39170
+ const elapsed = now - bucket.lastRefill;
39171
+ const refillRate = connRateLimit.limit / connRateLimit.windowMs;
39172
+ bucket.tokens = Math.min(
39173
+ connRateLimit.limit,
39174
+ bucket.tokens + elapsed * refillRate
39175
+ );
39176
+ bucket.lastRefill = now;
39177
+ }
39178
+ if (bucket.tokens < 1) {
39179
+ this._logger?.warn?.("Connection rate limit exceeded", { ip });
39180
+ abortHandshake(socket, 429, "Too Many Requests");
39181
+ return;
39182
+ }
39183
+ bucket.tokens -= 1;
39184
+ }
38830
39185
  if (socket.readyState !== "open") {
38831
39186
  this._logger?.debug?.("Socket not open at upgrade start");
38832
39187
  if (!socket.destroyed) socket.destroy();
@@ -39145,6 +39500,20 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
39145
39500
  protocol: selectedProtocol
39146
39501
  });
39147
39502
  this._updateSessionActivity(identity, client.session);
39503
+ const existingClient = this._clientsByIdentity.get(identity);
39504
+ if (existingClient && existingClient !== client) {
39505
+ this._logger?.warn?.("Evicting stale connection for identity", {
39506
+ identity,
39507
+ reason: "Duplicate identity replaced by new connection"
39508
+ });
39509
+ existingClient.close({
39510
+ code: 4e3,
39511
+ reason: "Evicted by new connection",
39512
+ force: true
39513
+ }).catch(() => {
39514
+ });
39515
+ this._clients.delete(existingClient);
39516
+ }
39148
39517
  this._clients.add(client);
39149
39518
  this._clientsByIdentity.set(identity, client);
39150
39519
  if (this._adapter?.setPresence) {
@@ -39200,6 +39569,29 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
39200
39569
  clearInterval(this._gcInterval);
39201
39570
  this._gcInterval = null;
39202
39571
  }
39572
+ if (!options.force) {
39573
+ const drainTimeout = 5e3;
39574
+ const drainPromises = Array.from(this._clients).map(async (client) => {
39575
+ const ws = client._ws;
39576
+ if (ws && ws.bufferedAmount > 0) {
39577
+ this._logger?.debug?.("Waiting for client buffer to drain", {
39578
+ identity: client.identity,
39579
+ bufferedAmount: ws.bufferedAmount
39580
+ });
39581
+ await new Promise((resolve) => {
39582
+ let elapsed = 0;
39583
+ const check = setInterval(() => {
39584
+ elapsed += 50;
39585
+ if (!ws || ws.bufferedAmount === 0 || elapsed >= drainTimeout) {
39586
+ clearInterval(check);
39587
+ resolve();
39588
+ }
39589
+ }, 50);
39590
+ });
39591
+ }
39592
+ });
39593
+ await Promise.allSettled(drainPromises);
39594
+ }
39203
39595
  const closePromises = Array.from(this._clients).map(
39204
39596
  (client) => client.close(options).catch(() => {
39205
39597
  })
@@ -39427,6 +39819,7 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
39427
39819
  0 && (module.exports = {
39428
39820
  ConnectionState,
39429
39821
  InMemoryAdapter,
39822
+ LRUMap,
39430
39823
  MessageType,
39431
39824
  MiddlewareStack,
39432
39825
  NOREPLY,