ocpp-ws-io 2.1.7 → 2.1.8

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
@@ -1,3 +1,18 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // ../../node_modules/tsup/assets/esm_shims.js
9
+ import path from "path";
10
+ import { fileURLToPath } from "url";
11
+ var getFilename = () => fileURLToPath(import.meta.url);
12
+ var getDirname = () => path.dirname(getFilename());
13
+ var __dirname = /* @__PURE__ */ getDirname();
14
+ var __filename = /* @__PURE__ */ getFilename();
15
+
1
16
  // src/adapters/adapter.ts
2
17
  var InMemoryAdapter = class {
3
18
  _channels = /* @__PURE__ */ new Map();
@@ -54,6 +69,145 @@ function defineAdapter(adapter) {
54
69
  return adapter;
55
70
  }
56
71
 
72
+ // src/adapters/redis/cluster-driver.ts
73
+ var ClusterDriver = class {
74
+ _cluster;
75
+ _subscriber;
76
+ _handlers = /* @__PURE__ */ new Map();
77
+ constructor(_options) {
78
+ try {
79
+ const { createRequire } = __require("module");
80
+ const dynamicRequire = createRequire(__filename);
81
+ const Redis = dynamicRequire("ioredis");
82
+ const redisOpts = _options.redisOptions ?? {};
83
+ if (_options.natMap) {
84
+ redisOpts.natMap = _options.natMap;
85
+ }
86
+ this._cluster = new Redis.Cluster(
87
+ _options.nodes.map((n) => ({ host: n.host, port: n.port })),
88
+ { redisOptions: redisOpts }
89
+ );
90
+ this._subscriber = new Redis.Cluster(
91
+ _options.nodes.map((n) => ({ host: n.host, port: n.port })),
92
+ { redisOptions: redisOpts }
93
+ );
94
+ this._subscriber.on("message", (channel, message) => {
95
+ const handler = this._handlers.get(channel);
96
+ if (handler) handler(message);
97
+ });
98
+ } catch {
99
+ throw new Error(
100
+ "ClusterDriver requires 'ioredis' as a peer dependency. Install it with: npm i ioredis"
101
+ );
102
+ }
103
+ }
104
+ async publish(channel, message) {
105
+ await this._cluster.publish(channel, message);
106
+ }
107
+ async subscribe(channel, handler) {
108
+ this._handlers.set(channel, handler);
109
+ await this._subscriber.subscribe(channel);
110
+ }
111
+ async unsubscribe(channel) {
112
+ await this._subscriber.unsubscribe(channel);
113
+ this._handlers.delete(channel);
114
+ }
115
+ async set(key, value, ttlSeconds) {
116
+ if (ttlSeconds) {
117
+ await this._cluster.set(key, value, "EX", ttlSeconds);
118
+ } else {
119
+ await this._cluster.set(key, value);
120
+ }
121
+ }
122
+ async get(key) {
123
+ return await this._cluster.get(key) || null;
124
+ }
125
+ async mget(keys) {
126
+ if (keys.length === 0) return [];
127
+ try {
128
+ return await this._cluster.mget(...keys);
129
+ } catch {
130
+ return await Promise.all(keys.map((k) => this.get(k)));
131
+ }
132
+ }
133
+ async del(key) {
134
+ await this._cluster.del(key);
135
+ }
136
+ async xadd(stream, args, maxLen) {
137
+ const flatArgs = [];
138
+ if (maxLen) {
139
+ flatArgs.push("MAXLEN", "~", maxLen.toString());
140
+ }
141
+ flatArgs.push("*");
142
+ for (const [k, v] of Object.entries(args)) {
143
+ flatArgs.push(k, v);
144
+ }
145
+ return await this._cluster.xadd(stream, ...flatArgs);
146
+ }
147
+ async xaddBatch(messages, maxLen) {
148
+ if (messages.length === 0) return;
149
+ const pipeline = this._cluster.pipeline();
150
+ for (const msg of messages) {
151
+ const flatArgs = [];
152
+ if (maxLen) {
153
+ flatArgs.push("MAXLEN", "~", maxLen.toString());
154
+ }
155
+ flatArgs.push("*");
156
+ for (const [k, v] of Object.entries(msg.args)) {
157
+ flatArgs.push(k, v);
158
+ }
159
+ pipeline.xadd(msg.stream, ...flatArgs);
160
+ }
161
+ await pipeline.exec();
162
+ }
163
+ async xread(streams, count, block) {
164
+ const args = [];
165
+ if (count) args.push("COUNT", count);
166
+ if (typeof block === "number") args.push("BLOCK", block);
167
+ args.push("STREAMS");
168
+ for (const s of streams) args.push(s.key);
169
+ for (const s of streams) args.push(s.id);
170
+ const result = await this._cluster.xread(...args);
171
+ if (!result) return null;
172
+ return result.map(([stream, messages]) => ({
173
+ stream,
174
+ messages: messages.map(([id, fields]) => {
175
+ const data = {};
176
+ for (let i = 0; i < fields.length; i += 2) {
177
+ data[fields[i]] = fields[i + 1];
178
+ }
179
+ return { id, data };
180
+ })
181
+ }));
182
+ }
183
+ async xlen(stream) {
184
+ return await this._cluster.xlen(stream);
185
+ }
186
+ async disconnect() {
187
+ this._handlers.clear();
188
+ await Promise.allSettled([this._cluster.quit(), this._subscriber.quit()]);
189
+ }
190
+ async setPresenceBatch(entries) {
191
+ if (entries.length === 0) return;
192
+ const pipeline = this._cluster.pipeline();
193
+ for (const { key, value, ttlSeconds } of entries) {
194
+ pipeline.set(key, value, "EX", ttlSeconds);
195
+ }
196
+ await pipeline.exec();
197
+ }
198
+ async expire(key, ttlSeconds) {
199
+ await this._cluster.expire(key, ttlSeconds);
200
+ }
201
+ onError(handler) {
202
+ this._cluster.on("error", handler);
203
+ return () => this._cluster.removeListener("error", handler);
204
+ }
205
+ onReconnect(handler) {
206
+ this._cluster.on("reconnecting", handler);
207
+ return () => this._cluster.removeListener("reconnecting", handler);
208
+ }
209
+ };
210
+
57
211
  // src/adapters/redis/helpers.ts
58
212
  var IoRedisDriver = class {
59
213
  constructor(pub, sub, blocking) {
@@ -317,6 +471,9 @@ var RedisAdapter = class {
317
471
  _unsubReconnect;
318
472
  // Stored presence entries for rehydration on reconnect
319
473
  _presenceCache = /* @__PURE__ */ new Map();
474
+ // Connection pool
475
+ _driverPool;
476
+ _nextPoolIndex;
320
477
  constructor(options) {
321
478
  this._prefix = options.prefix ?? "ocpp-ws-io:";
322
479
  this._streamMaxLen = options.streamMaxLen ?? 1e3;
@@ -327,6 +484,14 @@ var RedisAdapter = class {
327
484
  options.subClient,
328
485
  options.blockingClient
329
486
  );
487
+ const poolSize = options.poolSize ?? 1;
488
+ this._driverPool = [this._driver];
489
+ this._nextPoolIndex = 0;
490
+ if (poolSize > 1 && options.driverFactory) {
491
+ for (let i = 1; i < poolSize; i++) {
492
+ this._driverPool.push(options.driverFactory());
493
+ }
494
+ }
330
495
  if (this._driver.onError) {
331
496
  this._unsubError = this._driver.onError((err) => {
332
497
  console.error("[RedisAdapter] Redis error:", err.message);
@@ -339,6 +504,13 @@ var RedisAdapter = class {
339
504
  });
340
505
  }
341
506
  }
507
+ /** Get the next driver from the pool (round-robin) */
508
+ _getPoolDriver() {
509
+ if (this._driverPool.length === 1) return this._driver;
510
+ const driver = this._driverPool[this._nextPoolIndex];
511
+ this._nextPoolIndex = (this._nextPoolIndex + 1) % this._driverPool.length;
512
+ return driver;
513
+ }
342
514
  async publish(channel, data) {
343
515
  const prefixedChannel = this._prefix + channel;
344
516
  const payload = data;
@@ -349,11 +521,12 @@ var RedisAdapter = class {
349
521
  }
350
522
  const message = JSON.stringify(data);
351
523
  if (channel.startsWith("ocpp:node:")) {
352
- await this._driver.xadd(prefixedChannel, { message }, this._streamMaxLen);
353
- await this._driver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
524
+ const poolDriver = this._getPoolDriver();
525
+ await poolDriver.xadd(prefixedChannel, { message }, this._streamMaxLen);
526
+ await poolDriver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
354
527
  });
355
528
  } else {
356
- await this._driver.publish(prefixedChannel, message);
529
+ await this._getPoolDriver().publish(prefixedChannel, message);
357
530
  }
358
531
  }
359
532
  async publishBatch(messages) {
@@ -370,13 +543,15 @@ var RedisAdapter = class {
370
543
  }
371
544
  const promises = [];
372
545
  if (streamMessages.length > 0) {
373
- promises.push(this._driver.xaddBatch(streamMessages, this._streamMaxLen));
546
+ promises.push(
547
+ this._getPoolDriver().xaddBatch(streamMessages, this._streamMaxLen)
548
+ );
374
549
  }
375
550
  if (broadcastMessages.length > 0) {
376
551
  promises.push(
377
552
  Promise.all(
378
553
  broadcastMessages.map(
379
- (bm) => this._driver.publish(bm.channel, bm.message)
554
+ (bm) => this._getPoolDriver().publish(bm.channel, bm.message)
380
555
  )
381
556
  ).then(() => {
382
557
  })
@@ -421,7 +596,7 @@ var RedisAdapter = class {
421
596
  this._sequenceCounters.clear();
422
597
  if (this._unsubError) this._unsubError();
423
598
  if (this._unsubReconnect) this._unsubReconnect();
424
- await this._driver.disconnect();
599
+ await Promise.allSettled(this._driverPool.map((d) => d.disconnect()));
425
600
  }
426
601
  _handleMessage(channel, message) {
427
602
  const handlers = this._handlers.get(channel);
@@ -450,7 +625,7 @@ var RedisAdapter = class {
450
625
  async _pollLoop() {
451
626
  while (!this._closed) {
452
627
  if (this._streams.size === 0) {
453
- await new Promise((resolve) => setTimeout(resolve, 1e3));
628
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
454
629
  continue;
455
630
  }
456
631
  const streamsArg = Array.from(this._streams).map((key) => ({
@@ -472,7 +647,7 @@ var RedisAdapter = class {
472
647
  }
473
648
  }
474
649
  } catch (_err) {
475
- await new Promise((resolve) => setTimeout(resolve, 1e3));
650
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
476
651
  }
477
652
  }
478
653
  this._polling = false;
@@ -551,8 +726,84 @@ var RedisAdapter = class {
551
726
  }
552
727
  };
553
728
 
554
- // src/client.ts
729
+ // src/adaptive-limiter.ts
555
730
  import { EventEmitter } from "events";
731
+ import { cpus, freemem, totalmem } from "os";
732
+ var AdaptiveLimiter = class extends EventEmitter {
733
+ _cpuThreshold;
734
+ _memThreshold;
735
+ _cooldownMs;
736
+ _sampleInterval;
737
+ _timer = null;
738
+ _lastOverloadTime = 0;
739
+ _multiplier = 1;
740
+ _prevCpuUsage = null;
741
+ _prevTimestamp = 0;
742
+ constructor(options = {}) {
743
+ super();
744
+ this._cpuThreshold = options.cpuThresholdPercent ?? 70;
745
+ this._memThreshold = options.memThresholdPercent ?? 85;
746
+ this._cooldownMs = options.cooldownMs ?? 1e4;
747
+ this._sampleInterval = options.sampleIntervalMs ?? 2e3;
748
+ }
749
+ /** Current rate multiplier: 1.0 = normal, 0.25 = heavily throttled */
750
+ get multiplier() {
751
+ return this._multiplier;
752
+ }
753
+ /** Start periodic sampling */
754
+ start() {
755
+ if (this._timer) return;
756
+ this._prevCpuUsage = process.cpuUsage();
757
+ this._prevTimestamp = Date.now();
758
+ this._timer = setInterval(() => this._sample(), this._sampleInterval);
759
+ if (this._timer.unref) this._timer.unref();
760
+ }
761
+ /** Stop sampling and reset multiplier */
762
+ stop() {
763
+ if (this._timer) {
764
+ clearInterval(this._timer);
765
+ this._timer = null;
766
+ }
767
+ this._multiplier = 1;
768
+ }
769
+ _sample() {
770
+ const now = Date.now();
771
+ const cpuUsage = process.cpuUsage(this._prevCpuUsage ?? void 0);
772
+ const elapsedMs = now - this._prevTimestamp;
773
+ const cpuPercent = (cpuUsage.user + cpuUsage.system) / 1e3 / elapsedMs / cpus().length * 100;
774
+ this._prevCpuUsage = process.cpuUsage();
775
+ this._prevTimestamp = now;
776
+ const total = totalmem();
777
+ const free = freemem();
778
+ const memPercent = (total - free) / total * 100;
779
+ const cpuOverload = cpuPercent > this._cpuThreshold;
780
+ const memOverload = memPercent > this._memThreshold;
781
+ const prevMultiplier = this._multiplier;
782
+ if (cpuOverload || memOverload) {
783
+ this._lastOverloadTime = now;
784
+ this._multiplier = Math.max(0.25, this._multiplier * 0.5);
785
+ } else if (now - this._lastOverloadTime > this._cooldownMs) {
786
+ this._multiplier = Math.min(1, this._multiplier + 0.1);
787
+ }
788
+ if (this._multiplier !== prevMultiplier) {
789
+ this.emit("adapted", {
790
+ multiplier: this._multiplier,
791
+ cpuPercent: Math.round(cpuPercent * 100) / 100,
792
+ memPercent: Math.round(memPercent * 100) / 100
793
+ });
794
+ }
795
+ }
796
+ // Typed event emitter overrides
797
+ on(event, listener) {
798
+ return super.on(event, listener);
799
+ }
800
+ emit(event, data) {
801
+ return super.emit(event, data);
802
+ }
803
+ };
804
+
805
+ // src/client.ts
806
+ import { EventEmitter as EventEmitter2 } from "events";
556
807
  import { createId } from "@paralleldrive/cuid2";
557
808
  import WebSocket from "ws";
558
809
 
@@ -690,6 +941,9 @@ var RPCFrameworkError = class extends RPCGenericError {
690
941
  function defineMiddleware(mw) {
691
942
  return mw;
692
943
  }
944
+ function createPlugin(plugin) {
945
+ return plugin;
946
+ }
693
947
  function defineRpcMiddleware(mw) {
694
948
  return mw;
695
949
  }
@@ -1018,10 +1272,10 @@ var Queue = class {
1018
1272
  this._drain();
1019
1273
  }
1020
1274
  push(fn) {
1021
- return new Promise((resolve, reject) => {
1275
+ return new Promise((resolve2, reject) => {
1022
1276
  this._queue.push({
1023
1277
  fn,
1024
- resolve,
1278
+ resolve: resolve2,
1025
1279
  reject
1026
1280
  });
1027
1281
  this._drain();
@@ -37121,7 +37375,7 @@ ${body}`
37121
37375
 
37122
37376
  // src/client.ts
37123
37377
  var { CONNECTING, OPEN, CLOSING, CLOSED } = ConnectionState;
37124
- var OCPPClient = class _OCPPClient extends EventEmitter {
37378
+ var OCPPClient = class _OCPPClient extends EventEmitter2 {
37125
37379
  // Static connection states
37126
37380
  static CONNECTING = CONNECTING;
37127
37381
  static OPEN = OPEN;
@@ -37280,7 +37534,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37280
37534
  return this._connectInternal();
37281
37535
  }
37282
37536
  async _connectInternal() {
37283
- return new Promise((resolve, reject) => {
37537
+ return new Promise((resolve2, reject) => {
37284
37538
  const endpoint = this._buildEndpoint();
37285
37539
  const wsOptions = this._buildWsOptions();
37286
37540
  this._logger?.debug?.("Connecting", { url: endpoint });
@@ -37313,7 +37567,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37313
37567
  response
37314
37568
  };
37315
37569
  this.emit("open", result);
37316
- resolve(result);
37570
+ resolve2(result);
37317
37571
  };
37318
37572
  const onError = (err) => {
37319
37573
  cleanup();
@@ -37377,16 +37631,16 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37377
37631
  this._stopPing();
37378
37632
  if (!force && awaitPending) {
37379
37633
  const pendingPromises = Array.from(this._pendingCalls.values()).map(
37380
- (p) => new Promise((resolve) => {
37634
+ (p) => new Promise((resolve2) => {
37381
37635
  const origResolve = p.resolve;
37382
37636
  const origReject = p.reject;
37383
37637
  p.resolve = (v) => {
37384
37638
  origResolve(v);
37385
- resolve();
37639
+ resolve2();
37386
37640
  };
37387
37641
  p.reject = (r) => {
37388
37642
  origReject(r);
37389
- resolve();
37643
+ resolve2();
37390
37644
  };
37391
37645
  })
37392
37646
  );
@@ -37394,13 +37648,13 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37394
37648
  await Promise.allSettled(pendingPromises);
37395
37649
  }
37396
37650
  }
37397
- return new Promise((resolve) => {
37651
+ return new Promise((resolve2) => {
37398
37652
  if (!this._ws || this._ws.readyState === WebSocket.CLOSED) {
37399
37653
  this._state = CLOSED;
37400
37654
  this._cleanup();
37401
37655
  const result = { code, reason };
37402
37656
  this.emit("close", result);
37403
- resolve(result);
37657
+ resolve2(result);
37404
37658
  return;
37405
37659
  }
37406
37660
  const onClose = (closeCode, closeReason) => {
@@ -37409,7 +37663,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37409
37663
  this._cleanup();
37410
37664
  const result = { code: closeCode, reason: closeReason.toString() };
37411
37665
  this.emit("close", result);
37412
- resolve(result);
37666
+ resolve2(result);
37413
37667
  };
37414
37668
  this._ws.on("close", onClose);
37415
37669
  if (force) {
@@ -37484,7 +37738,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37484
37738
  }
37485
37739
  if (this._state !== OPEN) {
37486
37740
  if (this._options.offlineQueue && (this._state === CLOSED || this._state === CONNECTING)) {
37487
- return new Promise((resolve, reject) => {
37741
+ return new Promise((resolve2, reject) => {
37488
37742
  const maxSize = this._options.offlineQueueMaxSize ?? 100;
37489
37743
  if (this._offlineQueue.length >= maxSize) {
37490
37744
  this._offlineQueue.shift();
@@ -37496,7 +37750,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37496
37750
  }
37497
37751
  );
37498
37752
  }
37499
- this._offlineQueue.push({ method, params, options, resolve, reject });
37753
+ this._offlineQueue.push({ method, params, options, resolve: resolve2, reject });
37500
37754
  this._logger?.debug?.("Call queued offline", {
37501
37755
  method,
37502
37756
  queueSize: this._offlineQueue.length
@@ -37555,7 +37809,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37555
37809
  ctxvals.params
37556
37810
  ];
37557
37811
  const messageStr = JSON.stringify(message);
37558
- callResult = await new Promise((resolve, reject) => {
37812
+ callResult = await new Promise((resolve2, reject) => {
37559
37813
  const timeoutHandle = setTimeout(() => {
37560
37814
  this._pendingCalls.delete(msgId);
37561
37815
  reject(
@@ -37573,7 +37827,7 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37573
37827
  options.signal.addEventListener("abort", abortHandler);
37574
37828
  }
37575
37829
  this._pendingCalls.set(msgId, {
37576
- resolve,
37830
+ resolve: resolve2,
37577
37831
  reject,
37578
37832
  timeoutHandle,
37579
37833
  abortHandler: options.signal ? () => options.signal?.removeEventListener("abort", abortHandler) : void 0,
@@ -37653,11 +37907,15 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
37653
37907
  });
37654
37908
  }
37655
37909
  // ─── Internal: Message handling ──────────────────────────────
37656
- _onMessage(rawData) {
37910
+ _onMessage(rawData, preParsed) {
37657
37911
  this._recordActivity();
37658
37912
  let message;
37659
37913
  try {
37660
- message = JSON.parse(rawData);
37914
+ if (preParsed !== void 0) {
37915
+ message = preParsed;
37916
+ } else {
37917
+ message = JSON.parse(rawData);
37918
+ }
37661
37919
  if (!Array.isArray(message)) throw new Error("Message is not an array");
37662
37920
  } catch (err) {
37663
37921
  this._onBadMessage(
@@ -38176,6 +38434,23 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
38176
38434
  if (tls.passphrase) opts.passphrase = tls.passphrase;
38177
38435
  }
38178
38436
  }
38437
+ const compression = this._options.compression;
38438
+ if (compression) {
38439
+ opts.perMessageDeflate = compression === true ? {
38440
+ zlibDeflateOptions: { level: 6, memLevel: 8 },
38441
+ zlibInflateOptions: {},
38442
+ clientNoContextTakeover: true,
38443
+ serverNoContextTakeover: true
38444
+ } : {
38445
+ zlibDeflateOptions: {
38446
+ level: compression.level ?? 6,
38447
+ memLevel: compression.memLevel ?? 8
38448
+ },
38449
+ zlibInflateOptions: {},
38450
+ clientNoContextTakeover: compression.clientNoContextTakeover ?? true,
38451
+ serverNoContextTakeover: compression.serverNoContextTakeover ?? true
38452
+ };
38453
+ }
38179
38454
  return opts;
38180
38455
  }
38181
38456
  // ─── Internal: Cleanup ───────────────────────────────────────
@@ -38241,7 +38516,7 @@ var LRUMap = class extends Map {
38241
38516
  };
38242
38517
 
38243
38518
  // src/router.ts
38244
- import { EventEmitter as EventEmitter2 } from "events";
38519
+ import { EventEmitter as EventEmitter3 } from "events";
38245
38520
  async function executeMiddlewareChain(middlewares, ctx) {
38246
38521
  let index = -1;
38247
38522
  const dispatch = async (i, payload) => {
@@ -38265,7 +38540,7 @@ async function executeMiddlewareChain(middlewares, ctx) {
38265
38540
  };
38266
38541
  await dispatch(0);
38267
38542
  }
38268
- var OCPPRouter = class extends EventEmitter2 {
38543
+ var OCPPRouter = class extends EventEmitter3 {
38269
38544
  /** Raw registered patterns (strings and/or RegExp) for reference. */
38270
38545
  patterns;
38271
38546
  /** Connection middlewares attached to this router. */
@@ -38361,7 +38636,7 @@ function createRouter(...patterns) {
38361
38636
  }
38362
38637
 
38363
38638
  // src/server.ts
38364
- import { EventEmitter as EventEmitter3 } from "events";
38639
+ import { EventEmitter as EventEmitter4 } from "events";
38365
38640
  import {
38366
38641
  createServer as createHttpServer
38367
38642
  } from "http";
@@ -38445,8 +38720,8 @@ var TrieNode = class {
38445
38720
  /** Routers registered at this exact node (leaf). */
38446
38721
  routers = [];
38447
38722
  };
38448
- function normalizePath(path) {
38449
- return path.replace(/\/+/g, "/").replace(/^\/|\/$/g, "").split("/").filter(Boolean);
38723
+ function normalizePath(path2) {
38724
+ return path2.replace(/\/+/g, "/").replace(/^\/|\/$/g, "").split("/").filter(Boolean);
38450
38725
  }
38451
38726
  var RadixTrie = class {
38452
38727
  root = new TrieNode();
@@ -38593,6 +38868,8 @@ var OCPPServerClient = class extends OCPPClient {
38593
38868
  super(options);
38594
38869
  this._serverSession = context.session;
38595
38870
  this._serverHandshake = context.handshake;
38871
+ this._adaptiveMultiplier = context.adaptiveMultiplier ?? null;
38872
+ this._workerPool = context.workerPool ?? null;
38596
38873
  this._state = ConnectionState.OPEN;
38597
38874
  this._identity = this._options.identity;
38598
38875
  this._ws = context.ws;
@@ -38602,6 +38879,8 @@ var OCPPServerClient = class extends OCPPClient {
38602
38879
  }
38603
38880
  // ─── Rate Limiting State ──────────────────────────────────────────
38604
38881
  _rateLimits = {};
38882
+ _adaptiveMultiplier = null;
38883
+ _workerPool = null;
38605
38884
  _checkRateLimit(method) {
38606
38885
  const limits = this._options.rateLimit;
38607
38886
  if (!limits) return true;
@@ -38613,7 +38892,8 @@ var OCPPServerClient = class extends OCPPClient {
38613
38892
  this._rateLimits[key] = bucket;
38614
38893
  } else {
38615
38894
  const timePassed = now - bucket.lastRefill;
38616
- const refillRate = limit / windowMs;
38895
+ const adaptiveScale = this._adaptiveMultiplier?.() ?? 1;
38896
+ const refillRate = limit / windowMs * adaptiveScale;
38617
38897
  const tokensToAdd = timePassed * refillRate;
38618
38898
  if (tokensToAdd > 0) {
38619
38899
  bucket.tokens = Math.min(limit, bucket.tokens + tokensToAdd);
@@ -38658,6 +38938,19 @@ var OCPPServerClient = class extends OCPPClient {
38658
38938
  this._handleRateLimitExceeded(pData || data.toString());
38659
38939
  return;
38660
38940
  }
38941
+ if (pData !== void 0) {
38942
+ this._onMessage(data, pData);
38943
+ return;
38944
+ }
38945
+ }
38946
+ if (this._workerPool) {
38947
+ const raw = typeof data === "string" ? data : data;
38948
+ this._workerPool.parse(raw).then((result) => {
38949
+ this._onMessage(data, result.message);
38950
+ }).catch(() => {
38951
+ this._onMessage(data);
38952
+ });
38953
+ return;
38661
38954
  }
38662
38955
  this._onMessage(data);
38663
38956
  });
@@ -38758,8 +39051,110 @@ var OCPPServerClient = class extends OCPPClient {
38758
39051
  }
38759
39052
  };
38760
39053
 
39054
+ // src/worker-pool.ts
39055
+ import { cpus as cpus2 } from "os";
39056
+ import { resolve } from "path";
39057
+ import { Worker } from "worker_threads";
39058
+ var WorkerPool = class {
39059
+ _workers = [];
39060
+ _nextWorker = 0;
39061
+ _taskId = 0;
39062
+ _pending = /* @__PURE__ */ new Map();
39063
+ _maxQueueSize;
39064
+ _terminated = false;
39065
+ constructor(options = {}) {
39066
+ const poolSize = options.poolSize ?? Math.max(2, cpus2().length - 2);
39067
+ this._maxQueueSize = options.maxQueueSize ?? 1e4;
39068
+ const workerPath = resolve(__dirname, "parse-worker.js");
39069
+ for (let i = 0; i < poolSize; i++) {
39070
+ const worker = new Worker(workerPath);
39071
+ worker.on(
39072
+ "message",
39073
+ (response) => {
39074
+ const task = this._pending.get(response.id);
39075
+ if (!task) return;
39076
+ this._pending.delete(response.id);
39077
+ if (response.error) {
39078
+ task.reject(new Error(response.error));
39079
+ } else {
39080
+ task.resolve({
39081
+ message: response.message,
39082
+ validationError: response.validationError
39083
+ });
39084
+ }
39085
+ }
39086
+ );
39087
+ worker.on("error", (err) => {
39088
+ console.error(`[WorkerPool] Worker ${i} error:`, err.message);
39089
+ });
39090
+ this._workers.push(worker);
39091
+ }
39092
+ }
39093
+ /** Number of worker threads in the pool */
39094
+ get size() {
39095
+ return this._workers.length;
39096
+ }
39097
+ /** Number of pending (unresolved) parse tasks */
39098
+ get pendingTasks() {
39099
+ return this._pending.size;
39100
+ }
39101
+ /**
39102
+ * Send raw data to a worker for JSON parsing + optional validation.
39103
+ * Uses round-robin worker selection.
39104
+ */
39105
+ parse(data, schemaInfo) {
39106
+ if (this._terminated) {
39107
+ return Promise.reject(new Error("WorkerPool has been shut down"));
39108
+ }
39109
+ if (this._pending.size >= this._maxQueueSize) {
39110
+ return Promise.reject(
39111
+ new Error(
39112
+ `WorkerPool queue full (${this._maxQueueSize} pending tasks)`
39113
+ )
39114
+ );
39115
+ }
39116
+ return new Promise((resolve2, reject) => {
39117
+ const id = this._taskId++;
39118
+ this._pending.set(id, {
39119
+ resolve: resolve2,
39120
+ reject
39121
+ });
39122
+ const worker = this._workers[this._nextWorker % this._workers.length];
39123
+ this._nextWorker = (this._nextWorker + 1) % this._workers.length;
39124
+ worker.postMessage({ id, buffer: data, schemaInfo });
39125
+ });
39126
+ }
39127
+ /** Gracefully terminate all workers */
39128
+ async shutdown() {
39129
+ if (this._terminated) return;
39130
+ this._terminated = true;
39131
+ for (const [id, task] of this._pending) {
39132
+ task.reject(new Error("WorkerPool shutting down"));
39133
+ this._pending.delete(id);
39134
+ }
39135
+ const terminatePromises = this._workers.map(async (worker) => {
39136
+ try {
39137
+ await Promise.race([
39138
+ worker.terminate(),
39139
+ new Promise((resolve2) => setTimeout(resolve2, 5e3))
39140
+ ]);
39141
+ } catch {
39142
+ }
39143
+ });
39144
+ await Promise.allSettled(terminatePromises);
39145
+ this._workers = [];
39146
+ }
39147
+ };
39148
+ function createWorkerPool(options = {}) {
39149
+ try {
39150
+ return new WorkerPool(options);
39151
+ } catch {
39152
+ return null;
39153
+ }
39154
+ }
39155
+
38761
39156
  // src/server.ts
38762
- var OCPPServer = class extends EventEmitter3 {
39157
+ var OCPPServer = class extends EventEmitter4 {
38763
39158
  _options;
38764
39159
  /** Radix trie for O(k) route matching (string patterns). */
38765
39160
  _trie = new RadixTrie();
@@ -38778,6 +39173,9 @@ var OCPPServer = class extends EventEmitter3 {
38778
39173
  _globalCORS;
38779
39174
  // Connection-level rate limiting (per-IP token bucket)
38780
39175
  _connectionBuckets = /* @__PURE__ */ new Map();
39176
+ _adaptiveLimiter = null;
39177
+ _plugins = [];
39178
+ _workerPool = null;
38781
39179
  // Robustness & Clustering
38782
39180
  _nodeId = createId2();
38783
39181
  _sessions;
@@ -38810,7 +39208,8 @@ var OCPPServer = class extends EventEmitter3 {
38810
39208
  this._sessions = new LRUMap(maxSessions);
38811
39209
  this._wss = new WebSocketServer({
38812
39210
  noServer: true,
38813
- maxPayload: this._options.maxPayloadBytes ?? 65536
39211
+ maxPayload: this._options.maxPayloadBytes ?? 65536,
39212
+ perMessageDeflate: this._buildCompressionConfig()
38814
39213
  });
38815
39214
  this._gcInterval = setInterval(() => {
38816
39215
  const now = Date.now();
@@ -38823,6 +39222,32 @@ var OCPPServer = class extends EventEmitter3 {
38823
39222
  this._logger = initLogger(this._options.logging, {
38824
39223
  component: "OCPPServer"
38825
39224
  });
39225
+ const rl = this._options.rateLimit;
39226
+ if (rl?.adaptive) {
39227
+ this._adaptiveLimiter = new AdaptiveLimiter({
39228
+ cpuThresholdPercent: rl.cpuThresholdPercent,
39229
+ memThresholdPercent: rl.memThresholdPercent,
39230
+ cooldownMs: rl.cooldownMs
39231
+ });
39232
+ this._adaptiveLimiter.on(
39233
+ "adapted",
39234
+ (event) => {
39235
+ this._logger?.info?.("Adaptive rate limit adjusted", event);
39236
+ this.emit("rateLimit:adapted", event);
39237
+ }
39238
+ );
39239
+ this._adaptiveLimiter.start();
39240
+ }
39241
+ const wt = this._options.workerThreads;
39242
+ if (wt) {
39243
+ const poolOpts = typeof wt === "object" ? wt : {};
39244
+ this._workerPool = createWorkerPool(poolOpts);
39245
+ if (this._workerPool) {
39246
+ this._logger?.info?.("Worker thread pool initialized", {
39247
+ poolSize: this._workerPool.size
39248
+ });
39249
+ }
39250
+ }
38826
39251
  }
38827
39252
  // ─── Getters ─────────────────────────────────────────────────
38828
39253
  get log() {
@@ -38945,8 +39370,44 @@ var OCPPServer = class extends EventEmitter3 {
38945
39370
  return this;
38946
39371
  }
38947
39372
  /**
38948
- * Registers a new middleware chain, acting as a wildcard/catch-all router if no patterns are added.
38949
- * `server.use(middleware).route("/api").on("client", ...)`
39373
+ * Registers one or more plugins for server lifecycle hooks.
39374
+ * Plugins are called in registration order for all lifecycle events.
39375
+ *
39376
+ * @example Single plugin
39377
+ * ```ts
39378
+ * server.plugin(metricsPlugin);
39379
+ * ```
39380
+ *
39381
+ * @example Multiple plugins
39382
+ * ```ts
39383
+ * server.plugin(metricsPlugin, loggingPlugin, otelPlugin);
39384
+ * ```
39385
+ */
39386
+ plugin(...plugins) {
39387
+ for (const plugin of plugins) {
39388
+ this._plugins.push(plugin);
39389
+ this._logger?.info?.("Plugin registered", { name: plugin.name });
39390
+ if (plugin.onInit) {
39391
+ const result = plugin.onInit(this);
39392
+ if (result instanceof Promise) {
39393
+ result.catch((err) => {
39394
+ this._logger?.error?.("Plugin onInit error", {
39395
+ name: plugin.name,
39396
+ error: err.message
39397
+ });
39398
+ });
39399
+ }
39400
+ }
39401
+ }
39402
+ return this;
39403
+ }
39404
+ /**
39405
+ * Registers middleware chain(s) as a wildcard/catch-all router.
39406
+ *
39407
+ * @example
39408
+ * ```ts
39409
+ * server.use(myMiddleware).route("/api").on("client", ...);
39410
+ * ```
38950
39411
  */
38951
39412
  use(...middlewares) {
38952
39413
  const router = new OCPPRouter();
@@ -39102,7 +39563,7 @@ var OCPPServer = class extends EventEmitter3 {
39102
39563
  );
39103
39564
  }
39104
39565
  if (!options?.server) {
39105
- await new Promise((resolve, reject) => {
39566
+ await new Promise((resolve2, reject) => {
39106
39567
  httpServer.on("error", reject);
39107
39568
  httpServer.listen(port, host, () => {
39108
39569
  httpServer.removeListener("error", reject);
@@ -39111,7 +39572,7 @@ var OCPPServer = class extends EventEmitter3 {
39111
39572
  port: typeof addr === "object" ? addr?.port : port,
39112
39573
  host: host ?? "0.0.0.0"
39113
39574
  });
39114
- resolve();
39575
+ resolve2();
39115
39576
  });
39116
39577
  });
39117
39578
  }
@@ -39420,13 +39881,13 @@ var OCPPServer = class extends EventEmitter3 {
39420
39881
  selectedProtocol = handshake.protocols.values().next().value ?? void 0;
39421
39882
  } else {
39422
39883
  acceptOptions = await new Promise(
39423
- (resolve, reject) => {
39884
+ (resolve2, reject) => {
39424
39885
  let settled = false;
39425
39886
  const accept = (opts) => {
39426
39887
  if (settled) return;
39427
39888
  settled = true;
39428
39889
  if (opts?.protocol) selectedProtocol = opts.protocol;
39429
- resolve(opts);
39890
+ resolve2(opts);
39430
39891
  };
39431
39892
  const rejectAuth = (code = 401, message = "Unauthorized") => {
39432
39893
  if (!settled) {
@@ -39558,7 +40019,9 @@ var OCPPServer = class extends EventEmitter3 {
39558
40019
  ws,
39559
40020
  handshake,
39560
40021
  session: finalSession,
39561
- protocol: selectedProtocol
40022
+ protocol: selectedProtocol,
40023
+ adaptiveMultiplier: this._adaptiveLimiter ? () => this._adaptiveLimiter.multiplier : void 0,
40024
+ workerPool: this._workerPool ?? void 0
39562
40025
  });
39563
40026
  this._updateSessionActivity(identity, client.session);
39564
40027
  const existingClient = this._clientsByIdentity.get(identity);
@@ -39590,21 +40053,52 @@ var OCPPServer = class extends EventEmitter3 {
39590
40053
  remoteAddress: req.socket.remoteAddress,
39591
40054
  protocol: selectedProtocol
39592
40055
  });
39593
- client.on("close", () => {
39594
- this._clients.delete(client);
39595
- if (this._clientsByIdentity.get(identity) === client) {
39596
- this._clientsByIdentity.delete(identity);
40056
+ client.on(
40057
+ "close",
40058
+ ({ code, reason }) => {
40059
+ this._clients.delete(client);
40060
+ if (this._clientsByIdentity.get(identity) === client) {
40061
+ this._clientsByIdentity.delete(identity);
40062
+ }
40063
+ if (this?._adapter?.removePresence) {
40064
+ this._adapter.removePresence(identity).catch((err) => {
40065
+ this._logger?.error?.("Error removing presence", {
40066
+ identity,
40067
+ error: err
40068
+ });
40069
+ });
40070
+ }
40071
+ for (const plugin of this._plugins) {
40072
+ try {
40073
+ plugin.onDisconnect?.(client, code, reason);
40074
+ } catch (err) {
40075
+ this._logger?.error?.("Plugin onDisconnect error", {
40076
+ name: plugin.name,
40077
+ error: err.message
40078
+ });
40079
+ }
40080
+ }
40081
+ this._logger?.info?.("Client disconnected", { identity });
39597
40082
  }
39598
- if (this?._adapter?.removePresence) {
39599
- this._adapter.removePresence(identity).catch((err) => {
39600
- this._logger?.error?.("Error removing presence", {
39601
- identity,
39602
- error: err
40083
+ );
40084
+ for (const plugin of this._plugins) {
40085
+ try {
40086
+ const result = plugin.onConnection?.(client);
40087
+ if (result instanceof Promise) {
40088
+ result.catch((err) => {
40089
+ this._logger?.error?.("Plugin onConnection error", {
40090
+ name: plugin.name,
40091
+ error: err.message
40092
+ });
39603
40093
  });
40094
+ }
40095
+ } catch (err) {
40096
+ this._logger?.error?.("Plugin onConnection error", {
40097
+ name: plugin.name,
40098
+ error: err.message
39604
40099
  });
39605
40100
  }
39606
- this._logger?.info?.("Client disconnected", { identity });
39607
- });
40101
+ }
39608
40102
  this.emit("client", client);
39609
40103
  for (const router of matchedRouters) {
39610
40104
  router.emit("client", client);
@@ -39639,13 +40133,13 @@ var OCPPServer = class extends EventEmitter3 {
39639
40133
  identity: client.identity,
39640
40134
  bufferedAmount: ws.bufferedAmount
39641
40135
  });
39642
- await new Promise((resolve) => {
40136
+ await new Promise((resolve2) => {
39643
40137
  let elapsed = 0;
39644
40138
  const check = setInterval(() => {
39645
40139
  elapsed += 50;
39646
40140
  if (!ws || ws.bufferedAmount === 0 || elapsed >= drainTimeout) {
39647
40141
  clearInterval(check);
39648
- resolve();
40142
+ resolve2();
39649
40143
  }
39650
40144
  }, 50);
39651
40145
  });
@@ -39667,12 +40161,28 @@ var OCPPServer = class extends EventEmitter3 {
39667
40161
  this._wss = new WebSocketServer({ noServer: true });
39668
40162
  }
39669
40163
  const serverClosePromises = Array.from(this._httpServers).map(
39670
- (server) => new Promise((resolve) => {
39671
- server.close(() => resolve());
40164
+ (server) => new Promise((resolve2) => {
40165
+ server.close(() => resolve2());
39672
40166
  })
39673
40167
  );
39674
40168
  await Promise.allSettled(serverClosePromises);
39675
40169
  this._httpServers.clear();
40170
+ if (this._adaptiveLimiter) {
40171
+ this._adaptiveLimiter.stop();
40172
+ }
40173
+ for (const plugin of this._plugins) {
40174
+ try {
40175
+ const result = plugin.onClose?.();
40176
+ if (result instanceof Promise) {
40177
+ await result;
40178
+ }
40179
+ } catch (err) {
40180
+ this._logger?.error?.("Plugin onClose error", {
40181
+ name: plugin.name,
40182
+ error: err.message
40183
+ });
40184
+ }
40185
+ }
39676
40186
  if (this._adapter) {
39677
40187
  await this._adapter.disconnect();
39678
40188
  }
@@ -39734,6 +40244,55 @@ var OCPPServer = class extends EventEmitter3 {
39734
40244
  return void 0;
39735
40245
  }
39736
40246
  }
40247
+ // ─── Batch Calls ──────────────────────────────────────────────
40248
+ /**
40249
+ * Pipeline multiple calls to a single client into a concurrent batch.
40250
+ * Useful for reconnection warm-up (e.g. GetConfiguration, ChangeAvailability, etc.)
40251
+ * where sequential calls would add unnecessary round-trip latency.
40252
+ *
40253
+ * @param identity The client identity to send calls to
40254
+ * @param calls Array of { method, params, options? } to execute concurrently
40255
+ * @returns Array of results in the same order as the calls array.
40256
+ * Each element is the call result, or `undefined` if that individual call failed.
40257
+ *
40258
+ * @example
40259
+ * ```ts
40260
+ * const results = await server.sendBatch('CP-101', [
40261
+ * { method: 'GetConfiguration', params: { key: ['MeterInterval'] } },
40262
+ * { method: 'ChangeAvailability', params: { type: 'Operative' } },
40263
+ * { method: 'TriggerMessage', params: { requestedMessage: 'StatusNotification' } },
40264
+ * ]);
40265
+ * ```
40266
+ */
40267
+ async sendBatch(identity, calls) {
40268
+ if (calls.length === 0) return [];
40269
+ const client = this._clientsByIdentity.get(identity);
40270
+ if (!client) {
40271
+ this._logger?.warn?.("sendBatch: client not found locally", { identity });
40272
+ return calls.map(() => void 0);
40273
+ }
40274
+ const originalConcurrency = client.options.callConcurrency ?? 1;
40275
+ if (calls.length > originalConcurrency) {
40276
+ client.reconfigure({ callConcurrency: calls.length });
40277
+ }
40278
+ try {
40279
+ const results = await Promise.allSettled(
40280
+ calls.map((c) => client.call(c.method, c.params, c.options ?? {}))
40281
+ );
40282
+ return results.map((r) => {
40283
+ if (r.status === "fulfilled") return r.value;
40284
+ this._logger?.warn?.("sendBatch: individual call failed", {
40285
+ identity,
40286
+ error: r.reason?.message
40287
+ });
40288
+ return void 0;
40289
+ });
40290
+ } finally {
40291
+ if (calls.length > originalConcurrency) {
40292
+ client.reconfigure({ callConcurrency: originalConcurrency });
40293
+ }
40294
+ }
40295
+ }
39737
40296
  // ─── Pub/Sub Adapter ─────────────────────────────────────────
39738
40297
  async setAdapter(adapter) {
39739
40298
  this._adapter = adapter;
@@ -39875,8 +40434,34 @@ var OCPPServer = class extends EventEmitter3 {
39875
40434
  }
39876
40435
  await Promise.all(localPromises);
39877
40436
  }
40437
+ // ─── Internal: Compression Config ───────────────────────────────
40438
+ _buildCompressionConfig() {
40439
+ const compression = this._options.compression;
40440
+ if (!compression) return false;
40441
+ if (compression === true) {
40442
+ return {
40443
+ threshold: 1024,
40444
+ zlibDeflateOptions: { level: 6, memLevel: 8 },
40445
+ zlibInflateOptions: {},
40446
+ serverNoContextTakeover: true,
40447
+ clientNoContextTakeover: true
40448
+ };
40449
+ }
40450
+ return {
40451
+ threshold: compression.threshold ?? 1024,
40452
+ zlibDeflateOptions: {
40453
+ level: compression.level ?? 6,
40454
+ memLevel: compression.memLevel ?? 8
40455
+ },
40456
+ zlibInflateOptions: {},
40457
+ serverNoContextTakeover: compression.serverNoContextTakeover ?? true,
40458
+ clientNoContextTakeover: compression.clientNoContextTakeover ?? true
40459
+ };
40460
+ }
39878
40461
  };
39879
40462
  export {
40463
+ AdaptiveLimiter,
40464
+ ClusterDriver,
39880
40465
  ConnectionState,
39881
40466
  InMemoryAdapter,
39882
40467
  LRUMap,
@@ -39908,6 +40493,7 @@ export {
39908
40493
  WebsocketUpgradeError,
39909
40494
  combineAuth,
39910
40495
  createLoggingMiddleware,
40496
+ createPlugin,
39911
40497
  createRPCError,
39912
40498
  createRouter,
39913
40499
  createValidator,