ocpp-ws-io 2.1.6 → 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/README.md +3 -2
- package/dist/adapters/redis.d.mts +2 -1
- package/dist/adapters/redis.d.ts +2 -1
- package/dist/adapters/redis.js +27 -6
- package/dist/adapters/redis.js.map +1 -1
- package/dist/adapters/redis.mjs +27 -6
- package/dist/adapters/redis.mjs.map +1 -1
- package/dist/browser.d.mts +1129 -2
- package/dist/browser.d.ts +1129 -2
- package/dist/browser.js +4 -0
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +4 -0
- package/dist/browser.mjs.map +1 -1
- package/dist/index-C0mn42-8.d.mts +161 -0
- package/dist/index-s9f97CmV.d.ts +161 -0
- package/dist/index.d.mts +131 -287
- package/dist/index.d.ts +131 -287
- package/dist/index.js +672 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +686 -59
- package/dist/index.mjs.map +1 -1
- package/dist/plugins.d.mts +246 -0
- package/dist/plugins.d.ts +246 -0
- package/dist/plugins.js +370 -0
- package/dist/plugins.js.map +1 -0
- package/dist/plugins.mjs +350 -0
- package/dist/plugins.mjs.map +1 -0
- package/dist/{index-1QBeqAuc.d.mts → types-BZXEmDQ1.d.mts} +481 -95
- package/dist/{index-1QBeqAuc.d.ts → types-BZXEmDQ1.d.ts} +481 -95
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
AdaptiveLimiter: () => AdaptiveLimiter,
|
|
34
|
+
ClusterDriver: () => ClusterDriver,
|
|
33
35
|
ConnectionState: () => ConnectionState,
|
|
34
36
|
InMemoryAdapter: () => InMemoryAdapter,
|
|
35
37
|
LRUMap: () => LRUMap,
|
|
@@ -61,6 +63,7 @@ __export(src_exports, {
|
|
|
61
63
|
WebsocketUpgradeError: () => WebsocketUpgradeError,
|
|
62
64
|
combineAuth: () => combineAuth,
|
|
63
65
|
createLoggingMiddleware: () => createLoggingMiddleware,
|
|
66
|
+
createPlugin: () => createPlugin,
|
|
64
67
|
createRPCError: () => createRPCError,
|
|
65
68
|
createRouter: () => createRouter,
|
|
66
69
|
createValidator: () => createValidator,
|
|
@@ -130,6 +133,145 @@ function defineAdapter(adapter) {
|
|
|
130
133
|
return adapter;
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
// src/adapters/redis/cluster-driver.ts
|
|
137
|
+
var ClusterDriver = class {
|
|
138
|
+
_cluster;
|
|
139
|
+
_subscriber;
|
|
140
|
+
_handlers = /* @__PURE__ */ new Map();
|
|
141
|
+
constructor(_options) {
|
|
142
|
+
try {
|
|
143
|
+
const { createRequire } = require("module");
|
|
144
|
+
const dynamicRequire = createRequire(__filename);
|
|
145
|
+
const Redis = dynamicRequire("ioredis");
|
|
146
|
+
const redisOpts = _options.redisOptions ?? {};
|
|
147
|
+
if (_options.natMap) {
|
|
148
|
+
redisOpts.natMap = _options.natMap;
|
|
149
|
+
}
|
|
150
|
+
this._cluster = new Redis.Cluster(
|
|
151
|
+
_options.nodes.map((n) => ({ host: n.host, port: n.port })),
|
|
152
|
+
{ redisOptions: redisOpts }
|
|
153
|
+
);
|
|
154
|
+
this._subscriber = new Redis.Cluster(
|
|
155
|
+
_options.nodes.map((n) => ({ host: n.host, port: n.port })),
|
|
156
|
+
{ redisOptions: redisOpts }
|
|
157
|
+
);
|
|
158
|
+
this._subscriber.on("message", (channel, message) => {
|
|
159
|
+
const handler = this._handlers.get(channel);
|
|
160
|
+
if (handler) handler(message);
|
|
161
|
+
});
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"ClusterDriver requires 'ioredis' as a peer dependency. Install it with: npm i ioredis"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async publish(channel, message) {
|
|
169
|
+
await this._cluster.publish(channel, message);
|
|
170
|
+
}
|
|
171
|
+
async subscribe(channel, handler) {
|
|
172
|
+
this._handlers.set(channel, handler);
|
|
173
|
+
await this._subscriber.subscribe(channel);
|
|
174
|
+
}
|
|
175
|
+
async unsubscribe(channel) {
|
|
176
|
+
await this._subscriber.unsubscribe(channel);
|
|
177
|
+
this._handlers.delete(channel);
|
|
178
|
+
}
|
|
179
|
+
async set(key, value, ttlSeconds) {
|
|
180
|
+
if (ttlSeconds) {
|
|
181
|
+
await this._cluster.set(key, value, "EX", ttlSeconds);
|
|
182
|
+
} else {
|
|
183
|
+
await this._cluster.set(key, value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async get(key) {
|
|
187
|
+
return await this._cluster.get(key) || null;
|
|
188
|
+
}
|
|
189
|
+
async mget(keys) {
|
|
190
|
+
if (keys.length === 0) return [];
|
|
191
|
+
try {
|
|
192
|
+
return await this._cluster.mget(...keys);
|
|
193
|
+
} catch {
|
|
194
|
+
return await Promise.all(keys.map((k) => this.get(k)));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async del(key) {
|
|
198
|
+
await this._cluster.del(key);
|
|
199
|
+
}
|
|
200
|
+
async xadd(stream, args, maxLen) {
|
|
201
|
+
const flatArgs = [];
|
|
202
|
+
if (maxLen) {
|
|
203
|
+
flatArgs.push("MAXLEN", "~", maxLen.toString());
|
|
204
|
+
}
|
|
205
|
+
flatArgs.push("*");
|
|
206
|
+
for (const [k, v] of Object.entries(args)) {
|
|
207
|
+
flatArgs.push(k, v);
|
|
208
|
+
}
|
|
209
|
+
return await this._cluster.xadd(stream, ...flatArgs);
|
|
210
|
+
}
|
|
211
|
+
async xaddBatch(messages, maxLen) {
|
|
212
|
+
if (messages.length === 0) return;
|
|
213
|
+
const pipeline = this._cluster.pipeline();
|
|
214
|
+
for (const msg of messages) {
|
|
215
|
+
const flatArgs = [];
|
|
216
|
+
if (maxLen) {
|
|
217
|
+
flatArgs.push("MAXLEN", "~", maxLen.toString());
|
|
218
|
+
}
|
|
219
|
+
flatArgs.push("*");
|
|
220
|
+
for (const [k, v] of Object.entries(msg.args)) {
|
|
221
|
+
flatArgs.push(k, v);
|
|
222
|
+
}
|
|
223
|
+
pipeline.xadd(msg.stream, ...flatArgs);
|
|
224
|
+
}
|
|
225
|
+
await pipeline.exec();
|
|
226
|
+
}
|
|
227
|
+
async xread(streams, count, block) {
|
|
228
|
+
const args = [];
|
|
229
|
+
if (count) args.push("COUNT", count);
|
|
230
|
+
if (typeof block === "number") args.push("BLOCK", block);
|
|
231
|
+
args.push("STREAMS");
|
|
232
|
+
for (const s of streams) args.push(s.key);
|
|
233
|
+
for (const s of streams) args.push(s.id);
|
|
234
|
+
const result = await this._cluster.xread(...args);
|
|
235
|
+
if (!result) return null;
|
|
236
|
+
return result.map(([stream, messages]) => ({
|
|
237
|
+
stream,
|
|
238
|
+
messages: messages.map(([id, fields]) => {
|
|
239
|
+
const data = {};
|
|
240
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
241
|
+
data[fields[i]] = fields[i + 1];
|
|
242
|
+
}
|
|
243
|
+
return { id, data };
|
|
244
|
+
})
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
async xlen(stream) {
|
|
248
|
+
return await this._cluster.xlen(stream);
|
|
249
|
+
}
|
|
250
|
+
async disconnect() {
|
|
251
|
+
this._handlers.clear();
|
|
252
|
+
await Promise.allSettled([this._cluster.quit(), this._subscriber.quit()]);
|
|
253
|
+
}
|
|
254
|
+
async setPresenceBatch(entries) {
|
|
255
|
+
if (entries.length === 0) return;
|
|
256
|
+
const pipeline = this._cluster.pipeline();
|
|
257
|
+
for (const { key, value, ttlSeconds } of entries) {
|
|
258
|
+
pipeline.set(key, value, "EX", ttlSeconds);
|
|
259
|
+
}
|
|
260
|
+
await pipeline.exec();
|
|
261
|
+
}
|
|
262
|
+
async expire(key, ttlSeconds) {
|
|
263
|
+
await this._cluster.expire(key, ttlSeconds);
|
|
264
|
+
}
|
|
265
|
+
onError(handler) {
|
|
266
|
+
this._cluster.on("error", handler);
|
|
267
|
+
return () => this._cluster.removeListener("error", handler);
|
|
268
|
+
}
|
|
269
|
+
onReconnect(handler) {
|
|
270
|
+
this._cluster.on("reconnecting", handler);
|
|
271
|
+
return () => this._cluster.removeListener("reconnecting", handler);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
133
275
|
// src/adapters/redis/helpers.ts
|
|
134
276
|
var IoRedisDriver = class {
|
|
135
277
|
constructor(pub, sub, blocking) {
|
|
@@ -393,6 +535,9 @@ var RedisAdapter = class {
|
|
|
393
535
|
_unsubReconnect;
|
|
394
536
|
// Stored presence entries for rehydration on reconnect
|
|
395
537
|
_presenceCache = /* @__PURE__ */ new Map();
|
|
538
|
+
// Connection pool
|
|
539
|
+
_driverPool;
|
|
540
|
+
_nextPoolIndex;
|
|
396
541
|
constructor(options) {
|
|
397
542
|
this._prefix = options.prefix ?? "ocpp-ws-io:";
|
|
398
543
|
this._streamMaxLen = options.streamMaxLen ?? 1e3;
|
|
@@ -403,6 +548,14 @@ var RedisAdapter = class {
|
|
|
403
548
|
options.subClient,
|
|
404
549
|
options.blockingClient
|
|
405
550
|
);
|
|
551
|
+
const poolSize = options.poolSize ?? 1;
|
|
552
|
+
this._driverPool = [this._driver];
|
|
553
|
+
this._nextPoolIndex = 0;
|
|
554
|
+
if (poolSize > 1 && options.driverFactory) {
|
|
555
|
+
for (let i = 1; i < poolSize; i++) {
|
|
556
|
+
this._driverPool.push(options.driverFactory());
|
|
557
|
+
}
|
|
558
|
+
}
|
|
406
559
|
if (this._driver.onError) {
|
|
407
560
|
this._unsubError = this._driver.onError((err) => {
|
|
408
561
|
console.error("[RedisAdapter] Redis error:", err.message);
|
|
@@ -415,6 +568,13 @@ var RedisAdapter = class {
|
|
|
415
568
|
});
|
|
416
569
|
}
|
|
417
570
|
}
|
|
571
|
+
/** Get the next driver from the pool (round-robin) */
|
|
572
|
+
_getPoolDriver() {
|
|
573
|
+
if (this._driverPool.length === 1) return this._driver;
|
|
574
|
+
const driver = this._driverPool[this._nextPoolIndex];
|
|
575
|
+
this._nextPoolIndex = (this._nextPoolIndex + 1) % this._driverPool.length;
|
|
576
|
+
return driver;
|
|
577
|
+
}
|
|
418
578
|
async publish(channel, data) {
|
|
419
579
|
const prefixedChannel = this._prefix + channel;
|
|
420
580
|
const payload = data;
|
|
@@ -425,11 +585,12 @@ var RedisAdapter = class {
|
|
|
425
585
|
}
|
|
426
586
|
const message = JSON.stringify(data);
|
|
427
587
|
if (channel.startsWith("ocpp:node:")) {
|
|
428
|
-
|
|
429
|
-
await
|
|
588
|
+
const poolDriver = this._getPoolDriver();
|
|
589
|
+
await poolDriver.xadd(prefixedChannel, { message }, this._streamMaxLen);
|
|
590
|
+
await poolDriver.expire(prefixedChannel, this._streamTtlSeconds).catch(() => {
|
|
430
591
|
});
|
|
431
592
|
} else {
|
|
432
|
-
await this.
|
|
593
|
+
await this._getPoolDriver().publish(prefixedChannel, message);
|
|
433
594
|
}
|
|
434
595
|
}
|
|
435
596
|
async publishBatch(messages) {
|
|
@@ -446,13 +607,15 @@ var RedisAdapter = class {
|
|
|
446
607
|
}
|
|
447
608
|
const promises = [];
|
|
448
609
|
if (streamMessages.length > 0) {
|
|
449
|
-
promises.push(
|
|
610
|
+
promises.push(
|
|
611
|
+
this._getPoolDriver().xaddBatch(streamMessages, this._streamMaxLen)
|
|
612
|
+
);
|
|
450
613
|
}
|
|
451
614
|
if (broadcastMessages.length > 0) {
|
|
452
615
|
promises.push(
|
|
453
616
|
Promise.all(
|
|
454
617
|
broadcastMessages.map(
|
|
455
|
-
(bm) => this.
|
|
618
|
+
(bm) => this._getPoolDriver().publish(bm.channel, bm.message)
|
|
456
619
|
)
|
|
457
620
|
).then(() => {
|
|
458
621
|
})
|
|
@@ -497,7 +660,7 @@ var RedisAdapter = class {
|
|
|
497
660
|
this._sequenceCounters.clear();
|
|
498
661
|
if (this._unsubError) this._unsubError();
|
|
499
662
|
if (this._unsubReconnect) this._unsubReconnect();
|
|
500
|
-
await this.
|
|
663
|
+
await Promise.allSettled(this._driverPool.map((d) => d.disconnect()));
|
|
501
664
|
}
|
|
502
665
|
_handleMessage(channel, message) {
|
|
503
666
|
const handlers = this._handlers.get(channel);
|
|
@@ -526,7 +689,7 @@ var RedisAdapter = class {
|
|
|
526
689
|
async _pollLoop() {
|
|
527
690
|
while (!this._closed) {
|
|
528
691
|
if (this._streams.size === 0) {
|
|
529
|
-
await new Promise((
|
|
692
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
530
693
|
continue;
|
|
531
694
|
}
|
|
532
695
|
const streamsArg = Array.from(this._streams).map((key) => ({
|
|
@@ -548,7 +711,7 @@ var RedisAdapter = class {
|
|
|
548
711
|
}
|
|
549
712
|
}
|
|
550
713
|
} catch (_err) {
|
|
551
|
-
await new Promise((
|
|
714
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
552
715
|
}
|
|
553
716
|
}
|
|
554
717
|
this._polling = false;
|
|
@@ -627,8 +790,84 @@ var RedisAdapter = class {
|
|
|
627
790
|
}
|
|
628
791
|
};
|
|
629
792
|
|
|
630
|
-
// src/
|
|
793
|
+
// src/adaptive-limiter.ts
|
|
631
794
|
var import_node_events = require("events");
|
|
795
|
+
var import_node_os = require("os");
|
|
796
|
+
var AdaptiveLimiter = class extends import_node_events.EventEmitter {
|
|
797
|
+
_cpuThreshold;
|
|
798
|
+
_memThreshold;
|
|
799
|
+
_cooldownMs;
|
|
800
|
+
_sampleInterval;
|
|
801
|
+
_timer = null;
|
|
802
|
+
_lastOverloadTime = 0;
|
|
803
|
+
_multiplier = 1;
|
|
804
|
+
_prevCpuUsage = null;
|
|
805
|
+
_prevTimestamp = 0;
|
|
806
|
+
constructor(options = {}) {
|
|
807
|
+
super();
|
|
808
|
+
this._cpuThreshold = options.cpuThresholdPercent ?? 70;
|
|
809
|
+
this._memThreshold = options.memThresholdPercent ?? 85;
|
|
810
|
+
this._cooldownMs = options.cooldownMs ?? 1e4;
|
|
811
|
+
this._sampleInterval = options.sampleIntervalMs ?? 2e3;
|
|
812
|
+
}
|
|
813
|
+
/** Current rate multiplier: 1.0 = normal, 0.25 = heavily throttled */
|
|
814
|
+
get multiplier() {
|
|
815
|
+
return this._multiplier;
|
|
816
|
+
}
|
|
817
|
+
/** Start periodic sampling */
|
|
818
|
+
start() {
|
|
819
|
+
if (this._timer) return;
|
|
820
|
+
this._prevCpuUsage = process.cpuUsage();
|
|
821
|
+
this._prevTimestamp = Date.now();
|
|
822
|
+
this._timer = setInterval(() => this._sample(), this._sampleInterval);
|
|
823
|
+
if (this._timer.unref) this._timer.unref();
|
|
824
|
+
}
|
|
825
|
+
/** Stop sampling and reset multiplier */
|
|
826
|
+
stop() {
|
|
827
|
+
if (this._timer) {
|
|
828
|
+
clearInterval(this._timer);
|
|
829
|
+
this._timer = null;
|
|
830
|
+
}
|
|
831
|
+
this._multiplier = 1;
|
|
832
|
+
}
|
|
833
|
+
_sample() {
|
|
834
|
+
const now = Date.now();
|
|
835
|
+
const cpuUsage = process.cpuUsage(this._prevCpuUsage ?? void 0);
|
|
836
|
+
const elapsedMs = now - this._prevTimestamp;
|
|
837
|
+
const cpuPercent = (cpuUsage.user + cpuUsage.system) / 1e3 / elapsedMs / (0, import_node_os.cpus)().length * 100;
|
|
838
|
+
this._prevCpuUsage = process.cpuUsage();
|
|
839
|
+
this._prevTimestamp = now;
|
|
840
|
+
const total = (0, import_node_os.totalmem)();
|
|
841
|
+
const free = (0, import_node_os.freemem)();
|
|
842
|
+
const memPercent = (total - free) / total * 100;
|
|
843
|
+
const cpuOverload = cpuPercent > this._cpuThreshold;
|
|
844
|
+
const memOverload = memPercent > this._memThreshold;
|
|
845
|
+
const prevMultiplier = this._multiplier;
|
|
846
|
+
if (cpuOverload || memOverload) {
|
|
847
|
+
this._lastOverloadTime = now;
|
|
848
|
+
this._multiplier = Math.max(0.25, this._multiplier * 0.5);
|
|
849
|
+
} else if (now - this._lastOverloadTime > this._cooldownMs) {
|
|
850
|
+
this._multiplier = Math.min(1, this._multiplier + 0.1);
|
|
851
|
+
}
|
|
852
|
+
if (this._multiplier !== prevMultiplier) {
|
|
853
|
+
this.emit("adapted", {
|
|
854
|
+
multiplier: this._multiplier,
|
|
855
|
+
cpuPercent: Math.round(cpuPercent * 100) / 100,
|
|
856
|
+
memPercent: Math.round(memPercent * 100) / 100
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Typed event emitter overrides
|
|
861
|
+
on(event, listener) {
|
|
862
|
+
return super.on(event, listener);
|
|
863
|
+
}
|
|
864
|
+
emit(event, data) {
|
|
865
|
+
return super.emit(event, data);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
// src/client.ts
|
|
870
|
+
var import_node_events2 = require("events");
|
|
632
871
|
var import_cuid2 = require("@paralleldrive/cuid2");
|
|
633
872
|
var import_ws = __toESM(require("ws"));
|
|
634
873
|
|
|
@@ -766,6 +1005,9 @@ var RPCFrameworkError = class extends RPCGenericError {
|
|
|
766
1005
|
function defineMiddleware(mw) {
|
|
767
1006
|
return mw;
|
|
768
1007
|
}
|
|
1008
|
+
function createPlugin(plugin) {
|
|
1009
|
+
return plugin;
|
|
1010
|
+
}
|
|
769
1011
|
function defineRpcMiddleware(mw) {
|
|
770
1012
|
return mw;
|
|
771
1013
|
}
|
|
@@ -1090,10 +1332,10 @@ var Queue = class {
|
|
|
1090
1332
|
this._drain();
|
|
1091
1333
|
}
|
|
1092
1334
|
push(fn) {
|
|
1093
|
-
return new Promise((
|
|
1335
|
+
return new Promise((resolve2, reject) => {
|
|
1094
1336
|
this._queue.push({
|
|
1095
1337
|
fn,
|
|
1096
|
-
resolve,
|
|
1338
|
+
resolve: resolve2,
|
|
1097
1339
|
reject
|
|
1098
1340
|
});
|
|
1099
1341
|
this._drain();
|
|
@@ -37193,7 +37435,7 @@ ${body}`
|
|
|
37193
37435
|
|
|
37194
37436
|
// src/client.ts
|
|
37195
37437
|
var { CONNECTING, OPEN, CLOSING, CLOSED } = ConnectionState;
|
|
37196
|
-
var OCPPClient = class _OCPPClient extends
|
|
37438
|
+
var OCPPClient = class _OCPPClient extends import_node_events2.EventEmitter {
|
|
37197
37439
|
// Static connection states
|
|
37198
37440
|
static CONNECTING = CONNECTING;
|
|
37199
37441
|
static OPEN = OPEN;
|
|
@@ -37352,7 +37594,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37352
37594
|
return this._connectInternal();
|
|
37353
37595
|
}
|
|
37354
37596
|
async _connectInternal() {
|
|
37355
|
-
return new Promise((
|
|
37597
|
+
return new Promise((resolve2, reject) => {
|
|
37356
37598
|
const endpoint = this._buildEndpoint();
|
|
37357
37599
|
const wsOptions = this._buildWsOptions();
|
|
37358
37600
|
this._logger?.debug?.("Connecting", { url: endpoint });
|
|
@@ -37385,7 +37627,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37385
37627
|
response
|
|
37386
37628
|
};
|
|
37387
37629
|
this.emit("open", result);
|
|
37388
|
-
|
|
37630
|
+
resolve2(result);
|
|
37389
37631
|
};
|
|
37390
37632
|
const onError = (err) => {
|
|
37391
37633
|
cleanup();
|
|
@@ -37449,16 +37691,16 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37449
37691
|
this._stopPing();
|
|
37450
37692
|
if (!force && awaitPending) {
|
|
37451
37693
|
const pendingPromises = Array.from(this._pendingCalls.values()).map(
|
|
37452
|
-
(p) => new Promise((
|
|
37694
|
+
(p) => new Promise((resolve2) => {
|
|
37453
37695
|
const origResolve = p.resolve;
|
|
37454
37696
|
const origReject = p.reject;
|
|
37455
37697
|
p.resolve = (v) => {
|
|
37456
37698
|
origResolve(v);
|
|
37457
|
-
|
|
37699
|
+
resolve2();
|
|
37458
37700
|
};
|
|
37459
37701
|
p.reject = (r) => {
|
|
37460
37702
|
origReject(r);
|
|
37461
|
-
|
|
37703
|
+
resolve2();
|
|
37462
37704
|
};
|
|
37463
37705
|
})
|
|
37464
37706
|
);
|
|
@@ -37466,13 +37708,13 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37466
37708
|
await Promise.allSettled(pendingPromises);
|
|
37467
37709
|
}
|
|
37468
37710
|
}
|
|
37469
|
-
return new Promise((
|
|
37711
|
+
return new Promise((resolve2) => {
|
|
37470
37712
|
if (!this._ws || this._ws.readyState === import_ws.default.CLOSED) {
|
|
37471
37713
|
this._state = CLOSED;
|
|
37472
37714
|
this._cleanup();
|
|
37473
37715
|
const result = { code, reason };
|
|
37474
37716
|
this.emit("close", result);
|
|
37475
|
-
|
|
37717
|
+
resolve2(result);
|
|
37476
37718
|
return;
|
|
37477
37719
|
}
|
|
37478
37720
|
const onClose = (closeCode, closeReason) => {
|
|
@@ -37481,7 +37723,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37481
37723
|
this._cleanup();
|
|
37482
37724
|
const result = { code: closeCode, reason: closeReason.toString() };
|
|
37483
37725
|
this.emit("close", result);
|
|
37484
|
-
|
|
37726
|
+
resolve2(result);
|
|
37485
37727
|
};
|
|
37486
37728
|
this._ws.on("close", onClose);
|
|
37487
37729
|
if (force) {
|
|
@@ -37556,7 +37798,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37556
37798
|
}
|
|
37557
37799
|
if (this._state !== OPEN) {
|
|
37558
37800
|
if (this._options.offlineQueue && (this._state === CLOSED || this._state === CONNECTING)) {
|
|
37559
|
-
return new Promise((
|
|
37801
|
+
return new Promise((resolve2, reject) => {
|
|
37560
37802
|
const maxSize = this._options.offlineQueueMaxSize ?? 100;
|
|
37561
37803
|
if (this._offlineQueue.length >= maxSize) {
|
|
37562
37804
|
this._offlineQueue.shift();
|
|
@@ -37568,7 +37810,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37568
37810
|
}
|
|
37569
37811
|
);
|
|
37570
37812
|
}
|
|
37571
|
-
this._offlineQueue.push({ method, params, options, resolve, reject });
|
|
37813
|
+
this._offlineQueue.push({ method, params, options, resolve: resolve2, reject });
|
|
37572
37814
|
this._logger?.debug?.("Call queued offline", {
|
|
37573
37815
|
method,
|
|
37574
37816
|
queueSize: this._offlineQueue.length
|
|
@@ -37627,7 +37869,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37627
37869
|
ctxvals.params
|
|
37628
37870
|
];
|
|
37629
37871
|
const messageStr = JSON.stringify(message);
|
|
37630
|
-
callResult = await new Promise((
|
|
37872
|
+
callResult = await new Promise((resolve2, reject) => {
|
|
37631
37873
|
const timeoutHandle = setTimeout(() => {
|
|
37632
37874
|
this._pendingCalls.delete(msgId);
|
|
37633
37875
|
reject(
|
|
@@ -37645,7 +37887,7 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37645
37887
|
options.signal.addEventListener("abort", abortHandler);
|
|
37646
37888
|
}
|
|
37647
37889
|
this._pendingCalls.set(msgId, {
|
|
37648
|
-
resolve,
|
|
37890
|
+
resolve: resolve2,
|
|
37649
37891
|
reject,
|
|
37650
37892
|
timeoutHandle,
|
|
37651
37893
|
abortHandler: options.signal ? () => options.signal?.removeEventListener("abort", abortHandler) : void 0,
|
|
@@ -37725,11 +37967,15 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37725
37967
|
});
|
|
37726
37968
|
}
|
|
37727
37969
|
// ─── Internal: Message handling ──────────────────────────────
|
|
37728
|
-
_onMessage(rawData) {
|
|
37970
|
+
_onMessage(rawData, preParsed) {
|
|
37729
37971
|
this._recordActivity();
|
|
37730
37972
|
let message;
|
|
37731
37973
|
try {
|
|
37732
|
-
|
|
37974
|
+
if (preParsed !== void 0) {
|
|
37975
|
+
message = preParsed;
|
|
37976
|
+
} else {
|
|
37977
|
+
message = JSON.parse(rawData);
|
|
37978
|
+
}
|
|
37733
37979
|
if (!Array.isArray(message)) throw new Error("Message is not an array");
|
|
37734
37980
|
} catch (err) {
|
|
37735
37981
|
this._onBadMessage(
|
|
@@ -37739,6 +37985,36 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
37739
37985
|
return;
|
|
37740
37986
|
}
|
|
37741
37987
|
const messageType = message[0];
|
|
37988
|
+
const messageId = message[1];
|
|
37989
|
+
if (typeof messageId !== "string") {
|
|
37990
|
+
this._onBadMessage(
|
|
37991
|
+
typeof rawData === "string" ? rawData : rawData.toString(),
|
|
37992
|
+
new RPCMessageTypeNotSupportedError(
|
|
37993
|
+
`Invalid MessageId type: ${typeof messageId} (expected string)`
|
|
37994
|
+
)
|
|
37995
|
+
);
|
|
37996
|
+
return;
|
|
37997
|
+
}
|
|
37998
|
+
if (messageType === MessageType.CALL && message.length < 4 || messageType === MessageType.CALLRESULT && message.length < 3 || messageType === MessageType.CALLERROR && message.length < 5) {
|
|
37999
|
+
this._onBadMessage(
|
|
38000
|
+
JSON.stringify(message),
|
|
38001
|
+
new RPCMessageTypeNotSupportedError(
|
|
38002
|
+
`Missing payload elements for message type ${messageType}`
|
|
38003
|
+
)
|
|
38004
|
+
);
|
|
38005
|
+
return;
|
|
38006
|
+
}
|
|
38007
|
+
const payloadIndex = messageType === MessageType.CALLERROR ? 4 : messageType === MessageType.CALL ? 3 : 2;
|
|
38008
|
+
const payload = message[payloadIndex];
|
|
38009
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
38010
|
+
this._onBadMessage(
|
|
38011
|
+
JSON.stringify(message),
|
|
38012
|
+
new RPCMessageTypeNotSupportedError(
|
|
38013
|
+
`Payload must be a JSON object, got ${payload === null ? "null" : Array.isArray(payload) ? "array" : typeof payload}`
|
|
38014
|
+
)
|
|
38015
|
+
);
|
|
38016
|
+
return;
|
|
38017
|
+
}
|
|
37742
38018
|
switch (messageType) {
|
|
37743
38019
|
case MessageType.CALL:
|
|
37744
38020
|
this._handleIncomingCall(message);
|
|
@@ -38218,6 +38494,23 @@ var OCPPClient = class _OCPPClient extends import_node_events.EventEmitter {
|
|
|
38218
38494
|
if (tls.passphrase) opts.passphrase = tls.passphrase;
|
|
38219
38495
|
}
|
|
38220
38496
|
}
|
|
38497
|
+
const compression = this._options.compression;
|
|
38498
|
+
if (compression) {
|
|
38499
|
+
opts.perMessageDeflate = compression === true ? {
|
|
38500
|
+
zlibDeflateOptions: { level: 6, memLevel: 8 },
|
|
38501
|
+
zlibInflateOptions: {},
|
|
38502
|
+
clientNoContextTakeover: true,
|
|
38503
|
+
serverNoContextTakeover: true
|
|
38504
|
+
} : {
|
|
38505
|
+
zlibDeflateOptions: {
|
|
38506
|
+
level: compression.level ?? 6,
|
|
38507
|
+
memLevel: compression.memLevel ?? 8
|
|
38508
|
+
},
|
|
38509
|
+
zlibInflateOptions: {},
|
|
38510
|
+
clientNoContextTakeover: compression.clientNoContextTakeover ?? true,
|
|
38511
|
+
serverNoContextTakeover: compression.serverNoContextTakeover ?? true
|
|
38512
|
+
};
|
|
38513
|
+
}
|
|
38221
38514
|
return opts;
|
|
38222
38515
|
}
|
|
38223
38516
|
// ─── Internal: Cleanup ───────────────────────────────────────
|
|
@@ -38283,7 +38576,7 @@ var LRUMap = class extends Map {
|
|
|
38283
38576
|
};
|
|
38284
38577
|
|
|
38285
38578
|
// src/router.ts
|
|
38286
|
-
var
|
|
38579
|
+
var import_node_events3 = require("events");
|
|
38287
38580
|
async function executeMiddlewareChain(middlewares, ctx) {
|
|
38288
38581
|
let index = -1;
|
|
38289
38582
|
const dispatch = async (i, payload) => {
|
|
@@ -38307,7 +38600,7 @@ async function executeMiddlewareChain(middlewares, ctx) {
|
|
|
38307
38600
|
};
|
|
38308
38601
|
await dispatch(0);
|
|
38309
38602
|
}
|
|
38310
|
-
var OCPPRouter = class extends
|
|
38603
|
+
var OCPPRouter = class extends import_node_events3.EventEmitter {
|
|
38311
38604
|
/** Raw registered patterns (strings and/or RegExp) for reference. */
|
|
38312
38605
|
patterns;
|
|
38313
38606
|
/** Connection middlewares attached to this router. */
|
|
@@ -38403,7 +38696,7 @@ function createRouter(...patterns) {
|
|
|
38403
38696
|
}
|
|
38404
38697
|
|
|
38405
38698
|
// src/server.ts
|
|
38406
|
-
var
|
|
38699
|
+
var import_node_events4 = require("events");
|
|
38407
38700
|
var import_node_http2 = require("http");
|
|
38408
38701
|
var import_node_https = require("https");
|
|
38409
38702
|
var import_cuid22 = require("@paralleldrive/cuid2");
|
|
@@ -38633,6 +38926,8 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38633
38926
|
super(options);
|
|
38634
38927
|
this._serverSession = context.session;
|
|
38635
38928
|
this._serverHandshake = context.handshake;
|
|
38929
|
+
this._adaptiveMultiplier = context.adaptiveMultiplier ?? null;
|
|
38930
|
+
this._workerPool = context.workerPool ?? null;
|
|
38636
38931
|
this._state = ConnectionState.OPEN;
|
|
38637
38932
|
this._identity = this._options.identity;
|
|
38638
38933
|
this._ws = context.ws;
|
|
@@ -38642,6 +38937,8 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38642
38937
|
}
|
|
38643
38938
|
// ─── Rate Limiting State ──────────────────────────────────────────
|
|
38644
38939
|
_rateLimits = {};
|
|
38940
|
+
_adaptiveMultiplier = null;
|
|
38941
|
+
_workerPool = null;
|
|
38645
38942
|
_checkRateLimit(method) {
|
|
38646
38943
|
const limits = this._options.rateLimit;
|
|
38647
38944
|
if (!limits) return true;
|
|
@@ -38653,7 +38950,8 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38653
38950
|
this._rateLimits[key] = bucket;
|
|
38654
38951
|
} else {
|
|
38655
38952
|
const timePassed = now - bucket.lastRefill;
|
|
38656
|
-
const
|
|
38953
|
+
const adaptiveScale = this._adaptiveMultiplier?.() ?? 1;
|
|
38954
|
+
const refillRate = limit / windowMs * adaptiveScale;
|
|
38657
38955
|
const tokensToAdd = timePassed * refillRate;
|
|
38658
38956
|
if (tokensToAdd > 0) {
|
|
38659
38957
|
bucket.tokens = Math.min(limit, bucket.tokens + tokensToAdd);
|
|
@@ -38698,6 +38996,19 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38698
38996
|
this._handleRateLimitExceeded(pData || data.toString());
|
|
38699
38997
|
return;
|
|
38700
38998
|
}
|
|
38999
|
+
if (pData !== void 0) {
|
|
39000
|
+
this._onMessage(data, pData);
|
|
39001
|
+
return;
|
|
39002
|
+
}
|
|
39003
|
+
}
|
|
39004
|
+
if (this._workerPool) {
|
|
39005
|
+
const raw = typeof data === "string" ? data : data;
|
|
39006
|
+
this._workerPool.parse(raw).then((result) => {
|
|
39007
|
+
this._onMessage(data, result.message);
|
|
39008
|
+
}).catch(() => {
|
|
39009
|
+
this._onMessage(data);
|
|
39010
|
+
});
|
|
39011
|
+
return;
|
|
38701
39012
|
}
|
|
38702
39013
|
this._onMessage(data);
|
|
38703
39014
|
});
|
|
@@ -38708,7 +39019,18 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38708
39019
|
this._onClose(code, reason)
|
|
38709
39020
|
)
|
|
38710
39021
|
);
|
|
38711
|
-
ws.on("error", (err) =>
|
|
39022
|
+
ws.on("error", (err) => {
|
|
39023
|
+
if (this.listenerCount("error") > 0) {
|
|
39024
|
+
this.emit("error", err);
|
|
39025
|
+
} else {
|
|
39026
|
+
this._logger?.debug?.(
|
|
39027
|
+
"WebSocket error (unhandled by client listener)",
|
|
39028
|
+
{
|
|
39029
|
+
error: err.message
|
|
39030
|
+
}
|
|
39031
|
+
);
|
|
39032
|
+
}
|
|
39033
|
+
});
|
|
38712
39034
|
ws.on("ping", () => {
|
|
38713
39035
|
this._recordActivity();
|
|
38714
39036
|
this.emit("ping");
|
|
@@ -38787,8 +39109,110 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38787
39109
|
}
|
|
38788
39110
|
};
|
|
38789
39111
|
|
|
39112
|
+
// src/worker-pool.ts
|
|
39113
|
+
var import_node_os2 = require("os");
|
|
39114
|
+
var import_node_path = require("path");
|
|
39115
|
+
var import_node_worker_threads = require("worker_threads");
|
|
39116
|
+
var WorkerPool = class {
|
|
39117
|
+
_workers = [];
|
|
39118
|
+
_nextWorker = 0;
|
|
39119
|
+
_taskId = 0;
|
|
39120
|
+
_pending = /* @__PURE__ */ new Map();
|
|
39121
|
+
_maxQueueSize;
|
|
39122
|
+
_terminated = false;
|
|
39123
|
+
constructor(options = {}) {
|
|
39124
|
+
const poolSize = options.poolSize ?? Math.max(2, (0, import_node_os2.cpus)().length - 2);
|
|
39125
|
+
this._maxQueueSize = options.maxQueueSize ?? 1e4;
|
|
39126
|
+
const workerPath = (0, import_node_path.resolve)(__dirname, "parse-worker.js");
|
|
39127
|
+
for (let i = 0; i < poolSize; i++) {
|
|
39128
|
+
const worker = new import_node_worker_threads.Worker(workerPath);
|
|
39129
|
+
worker.on(
|
|
39130
|
+
"message",
|
|
39131
|
+
(response) => {
|
|
39132
|
+
const task = this._pending.get(response.id);
|
|
39133
|
+
if (!task) return;
|
|
39134
|
+
this._pending.delete(response.id);
|
|
39135
|
+
if (response.error) {
|
|
39136
|
+
task.reject(new Error(response.error));
|
|
39137
|
+
} else {
|
|
39138
|
+
task.resolve({
|
|
39139
|
+
message: response.message,
|
|
39140
|
+
validationError: response.validationError
|
|
39141
|
+
});
|
|
39142
|
+
}
|
|
39143
|
+
}
|
|
39144
|
+
);
|
|
39145
|
+
worker.on("error", (err) => {
|
|
39146
|
+
console.error(`[WorkerPool] Worker ${i} error:`, err.message);
|
|
39147
|
+
});
|
|
39148
|
+
this._workers.push(worker);
|
|
39149
|
+
}
|
|
39150
|
+
}
|
|
39151
|
+
/** Number of worker threads in the pool */
|
|
39152
|
+
get size() {
|
|
39153
|
+
return this._workers.length;
|
|
39154
|
+
}
|
|
39155
|
+
/** Number of pending (unresolved) parse tasks */
|
|
39156
|
+
get pendingTasks() {
|
|
39157
|
+
return this._pending.size;
|
|
39158
|
+
}
|
|
39159
|
+
/**
|
|
39160
|
+
* Send raw data to a worker for JSON parsing + optional validation.
|
|
39161
|
+
* Uses round-robin worker selection.
|
|
39162
|
+
*/
|
|
39163
|
+
parse(data, schemaInfo) {
|
|
39164
|
+
if (this._terminated) {
|
|
39165
|
+
return Promise.reject(new Error("WorkerPool has been shut down"));
|
|
39166
|
+
}
|
|
39167
|
+
if (this._pending.size >= this._maxQueueSize) {
|
|
39168
|
+
return Promise.reject(
|
|
39169
|
+
new Error(
|
|
39170
|
+
`WorkerPool queue full (${this._maxQueueSize} pending tasks)`
|
|
39171
|
+
)
|
|
39172
|
+
);
|
|
39173
|
+
}
|
|
39174
|
+
return new Promise((resolve2, reject) => {
|
|
39175
|
+
const id = this._taskId++;
|
|
39176
|
+
this._pending.set(id, {
|
|
39177
|
+
resolve: resolve2,
|
|
39178
|
+
reject
|
|
39179
|
+
});
|
|
39180
|
+
const worker = this._workers[this._nextWorker % this._workers.length];
|
|
39181
|
+
this._nextWorker = (this._nextWorker + 1) % this._workers.length;
|
|
39182
|
+
worker.postMessage({ id, buffer: data, schemaInfo });
|
|
39183
|
+
});
|
|
39184
|
+
}
|
|
39185
|
+
/** Gracefully terminate all workers */
|
|
39186
|
+
async shutdown() {
|
|
39187
|
+
if (this._terminated) return;
|
|
39188
|
+
this._terminated = true;
|
|
39189
|
+
for (const [id, task] of this._pending) {
|
|
39190
|
+
task.reject(new Error("WorkerPool shutting down"));
|
|
39191
|
+
this._pending.delete(id);
|
|
39192
|
+
}
|
|
39193
|
+
const terminatePromises = this._workers.map(async (worker) => {
|
|
39194
|
+
try {
|
|
39195
|
+
await Promise.race([
|
|
39196
|
+
worker.terminate(),
|
|
39197
|
+
new Promise((resolve2) => setTimeout(resolve2, 5e3))
|
|
39198
|
+
]);
|
|
39199
|
+
} catch {
|
|
39200
|
+
}
|
|
39201
|
+
});
|
|
39202
|
+
await Promise.allSettled(terminatePromises);
|
|
39203
|
+
this._workers = [];
|
|
39204
|
+
}
|
|
39205
|
+
};
|
|
39206
|
+
function createWorkerPool(options = {}) {
|
|
39207
|
+
try {
|
|
39208
|
+
return new WorkerPool(options);
|
|
39209
|
+
} catch {
|
|
39210
|
+
return null;
|
|
39211
|
+
}
|
|
39212
|
+
}
|
|
39213
|
+
|
|
38790
39214
|
// src/server.ts
|
|
38791
|
-
var OCPPServer = class extends
|
|
39215
|
+
var OCPPServer = class extends import_node_events4.EventEmitter {
|
|
38792
39216
|
_options;
|
|
38793
39217
|
/** Radix trie for O(k) route matching (string patterns). */
|
|
38794
39218
|
_trie = new RadixTrie();
|
|
@@ -38807,6 +39231,9 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
38807
39231
|
_globalCORS;
|
|
38808
39232
|
// Connection-level rate limiting (per-IP token bucket)
|
|
38809
39233
|
_connectionBuckets = /* @__PURE__ */ new Map();
|
|
39234
|
+
_adaptiveLimiter = null;
|
|
39235
|
+
_plugins = [];
|
|
39236
|
+
_workerPool = null;
|
|
38810
39237
|
// Robustness & Clustering
|
|
38811
39238
|
_nodeId = (0, import_cuid22.createId)();
|
|
38812
39239
|
_sessions;
|
|
@@ -38816,9 +39243,9 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
38816
39243
|
super();
|
|
38817
39244
|
this.setMaxListeners(0);
|
|
38818
39245
|
if (options.strictMode) {
|
|
38819
|
-
if (!options.
|
|
39246
|
+
if (!options.protocols?.length) {
|
|
38820
39247
|
throw new Error(
|
|
38821
|
-
"strictMode requires
|
|
39248
|
+
"strictMode requires protocols to be specified (e.g. protocols: ['ocpp1.6'])"
|
|
38822
39249
|
);
|
|
38823
39250
|
}
|
|
38824
39251
|
}
|
|
@@ -38839,7 +39266,8 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
38839
39266
|
this._sessions = new LRUMap(maxSessions);
|
|
38840
39267
|
this._wss = new import_ws2.WebSocketServer({
|
|
38841
39268
|
noServer: true,
|
|
38842
|
-
maxPayload: this._options.maxPayloadBytes ?? 65536
|
|
39269
|
+
maxPayload: this._options.maxPayloadBytes ?? 65536,
|
|
39270
|
+
perMessageDeflate: this._buildCompressionConfig()
|
|
38843
39271
|
});
|
|
38844
39272
|
this._gcInterval = setInterval(() => {
|
|
38845
39273
|
const now = Date.now();
|
|
@@ -38852,6 +39280,32 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
38852
39280
|
this._logger = initLogger(this._options.logging, {
|
|
38853
39281
|
component: "OCPPServer"
|
|
38854
39282
|
});
|
|
39283
|
+
const rl = this._options.rateLimit;
|
|
39284
|
+
if (rl?.adaptive) {
|
|
39285
|
+
this._adaptiveLimiter = new AdaptiveLimiter({
|
|
39286
|
+
cpuThresholdPercent: rl.cpuThresholdPercent,
|
|
39287
|
+
memThresholdPercent: rl.memThresholdPercent,
|
|
39288
|
+
cooldownMs: rl.cooldownMs
|
|
39289
|
+
});
|
|
39290
|
+
this._adaptiveLimiter.on(
|
|
39291
|
+
"adapted",
|
|
39292
|
+
(event) => {
|
|
39293
|
+
this._logger?.info?.("Adaptive rate limit adjusted", event);
|
|
39294
|
+
this.emit("rateLimit:adapted", event);
|
|
39295
|
+
}
|
|
39296
|
+
);
|
|
39297
|
+
this._adaptiveLimiter.start();
|
|
39298
|
+
}
|
|
39299
|
+
const wt = this._options.workerThreads;
|
|
39300
|
+
if (wt) {
|
|
39301
|
+
const poolOpts = typeof wt === "object" ? wt : {};
|
|
39302
|
+
this._workerPool = createWorkerPool(poolOpts);
|
|
39303
|
+
if (this._workerPool) {
|
|
39304
|
+
this._logger?.info?.("Worker thread pool initialized", {
|
|
39305
|
+
poolSize: this._workerPool.size
|
|
39306
|
+
});
|
|
39307
|
+
}
|
|
39308
|
+
}
|
|
38855
39309
|
}
|
|
38856
39310
|
// ─── Getters ─────────────────────────────────────────────────
|
|
38857
39311
|
get log() {
|
|
@@ -38974,8 +39428,44 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
38974
39428
|
return this;
|
|
38975
39429
|
}
|
|
38976
39430
|
/**
|
|
38977
|
-
* Registers
|
|
38978
|
-
*
|
|
39431
|
+
* Registers one or more plugins for server lifecycle hooks.
|
|
39432
|
+
* Plugins are called in registration order for all lifecycle events.
|
|
39433
|
+
*
|
|
39434
|
+
* @example Single plugin
|
|
39435
|
+
* ```ts
|
|
39436
|
+
* server.plugin(metricsPlugin);
|
|
39437
|
+
* ```
|
|
39438
|
+
*
|
|
39439
|
+
* @example Multiple plugins
|
|
39440
|
+
* ```ts
|
|
39441
|
+
* server.plugin(metricsPlugin, loggingPlugin, otelPlugin);
|
|
39442
|
+
* ```
|
|
39443
|
+
*/
|
|
39444
|
+
plugin(...plugins) {
|
|
39445
|
+
for (const plugin of plugins) {
|
|
39446
|
+
this._plugins.push(plugin);
|
|
39447
|
+
this._logger?.info?.("Plugin registered", { name: plugin.name });
|
|
39448
|
+
if (plugin.onInit) {
|
|
39449
|
+
const result = plugin.onInit(this);
|
|
39450
|
+
if (result instanceof Promise) {
|
|
39451
|
+
result.catch((err) => {
|
|
39452
|
+
this._logger?.error?.("Plugin onInit error", {
|
|
39453
|
+
name: plugin.name,
|
|
39454
|
+
error: err.message
|
|
39455
|
+
});
|
|
39456
|
+
});
|
|
39457
|
+
}
|
|
39458
|
+
}
|
|
39459
|
+
}
|
|
39460
|
+
return this;
|
|
39461
|
+
}
|
|
39462
|
+
/**
|
|
39463
|
+
* Registers middleware chain(s) as a wildcard/catch-all router.
|
|
39464
|
+
*
|
|
39465
|
+
* @example
|
|
39466
|
+
* ```ts
|
|
39467
|
+
* server.use(myMiddleware).route("/api").on("client", ...);
|
|
39468
|
+
* ```
|
|
38979
39469
|
*/
|
|
38980
39470
|
use(...middlewares) {
|
|
38981
39471
|
const router = new OCPPRouter();
|
|
@@ -39131,7 +39621,7 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39131
39621
|
);
|
|
39132
39622
|
}
|
|
39133
39623
|
if (!options?.server) {
|
|
39134
|
-
await new Promise((
|
|
39624
|
+
await new Promise((resolve2, reject) => {
|
|
39135
39625
|
httpServer.on("error", reject);
|
|
39136
39626
|
httpServer.listen(port, host, () => {
|
|
39137
39627
|
httpServer.removeListener("error", reject);
|
|
@@ -39140,7 +39630,7 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39140
39630
|
port: typeof addr === "object" ? addr?.port : port,
|
|
39141
39631
|
host: host ?? "0.0.0.0"
|
|
39142
39632
|
});
|
|
39143
|
-
|
|
39633
|
+
resolve2();
|
|
39144
39634
|
});
|
|
39145
39635
|
});
|
|
39146
39636
|
}
|
|
@@ -39449,13 +39939,13 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39449
39939
|
selectedProtocol = handshake.protocols.values().next().value ?? void 0;
|
|
39450
39940
|
} else {
|
|
39451
39941
|
acceptOptions = await new Promise(
|
|
39452
|
-
(
|
|
39942
|
+
(resolve2, reject) => {
|
|
39453
39943
|
let settled = false;
|
|
39454
39944
|
const accept = (opts) => {
|
|
39455
39945
|
if (settled) return;
|
|
39456
39946
|
settled = true;
|
|
39457
39947
|
if (opts?.protocol) selectedProtocol = opts.protocol;
|
|
39458
|
-
|
|
39948
|
+
resolve2(opts);
|
|
39459
39949
|
};
|
|
39460
39950
|
const rejectAuth = (code = 401, message = "Unauthorized") => {
|
|
39461
39951
|
if (!settled) {
|
|
@@ -39587,7 +40077,9 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39587
40077
|
ws,
|
|
39588
40078
|
handshake,
|
|
39589
40079
|
session: finalSession,
|
|
39590
|
-
protocol: selectedProtocol
|
|
40080
|
+
protocol: selectedProtocol,
|
|
40081
|
+
adaptiveMultiplier: this._adaptiveLimiter ? () => this._adaptiveLimiter.multiplier : void 0,
|
|
40082
|
+
workerPool: this._workerPool ?? void 0
|
|
39591
40083
|
});
|
|
39592
40084
|
this._updateSessionActivity(identity, client.session);
|
|
39593
40085
|
const existingClient = this._clientsByIdentity.get(identity);
|
|
@@ -39619,21 +40111,52 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39619
40111
|
remoteAddress: req.socket.remoteAddress,
|
|
39620
40112
|
protocol: selectedProtocol
|
|
39621
40113
|
});
|
|
39622
|
-
client.on(
|
|
39623
|
-
|
|
39624
|
-
|
|
39625
|
-
this.
|
|
40114
|
+
client.on(
|
|
40115
|
+
"close",
|
|
40116
|
+
({ code, reason }) => {
|
|
40117
|
+
this._clients.delete(client);
|
|
40118
|
+
if (this._clientsByIdentity.get(identity) === client) {
|
|
40119
|
+
this._clientsByIdentity.delete(identity);
|
|
40120
|
+
}
|
|
40121
|
+
if (this?._adapter?.removePresence) {
|
|
40122
|
+
this._adapter.removePresence(identity).catch((err) => {
|
|
40123
|
+
this._logger?.error?.("Error removing presence", {
|
|
40124
|
+
identity,
|
|
40125
|
+
error: err
|
|
40126
|
+
});
|
|
40127
|
+
});
|
|
40128
|
+
}
|
|
40129
|
+
for (const plugin of this._plugins) {
|
|
40130
|
+
try {
|
|
40131
|
+
plugin.onDisconnect?.(client, code, reason);
|
|
40132
|
+
} catch (err) {
|
|
40133
|
+
this._logger?.error?.("Plugin onDisconnect error", {
|
|
40134
|
+
name: plugin.name,
|
|
40135
|
+
error: err.message
|
|
40136
|
+
});
|
|
40137
|
+
}
|
|
40138
|
+
}
|
|
40139
|
+
this._logger?.info?.("Client disconnected", { identity });
|
|
39626
40140
|
}
|
|
39627
|
-
|
|
39628
|
-
|
|
39629
|
-
|
|
39630
|
-
|
|
39631
|
-
|
|
40141
|
+
);
|
|
40142
|
+
for (const plugin of this._plugins) {
|
|
40143
|
+
try {
|
|
40144
|
+
const result = plugin.onConnection?.(client);
|
|
40145
|
+
if (result instanceof Promise) {
|
|
40146
|
+
result.catch((err) => {
|
|
40147
|
+
this._logger?.error?.("Plugin onConnection error", {
|
|
40148
|
+
name: plugin.name,
|
|
40149
|
+
error: err.message
|
|
40150
|
+
});
|
|
39632
40151
|
});
|
|
40152
|
+
}
|
|
40153
|
+
} catch (err) {
|
|
40154
|
+
this._logger?.error?.("Plugin onConnection error", {
|
|
40155
|
+
name: plugin.name,
|
|
40156
|
+
error: err.message
|
|
39633
40157
|
});
|
|
39634
40158
|
}
|
|
39635
|
-
|
|
39636
|
-
});
|
|
40159
|
+
}
|
|
39637
40160
|
this.emit("client", client);
|
|
39638
40161
|
for (const router of matchedRouters) {
|
|
39639
40162
|
router.emit("client", client);
|
|
@@ -39668,13 +40191,13 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39668
40191
|
identity: client.identity,
|
|
39669
40192
|
bufferedAmount: ws.bufferedAmount
|
|
39670
40193
|
});
|
|
39671
|
-
await new Promise((
|
|
40194
|
+
await new Promise((resolve2) => {
|
|
39672
40195
|
let elapsed = 0;
|
|
39673
40196
|
const check = setInterval(() => {
|
|
39674
40197
|
elapsed += 50;
|
|
39675
40198
|
if (!ws || ws.bufferedAmount === 0 || elapsed >= drainTimeout) {
|
|
39676
40199
|
clearInterval(check);
|
|
39677
|
-
|
|
40200
|
+
resolve2();
|
|
39678
40201
|
}
|
|
39679
40202
|
}, 50);
|
|
39680
40203
|
});
|
|
@@ -39696,12 +40219,28 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39696
40219
|
this._wss = new import_ws2.WebSocketServer({ noServer: true });
|
|
39697
40220
|
}
|
|
39698
40221
|
const serverClosePromises = Array.from(this._httpServers).map(
|
|
39699
|
-
(server) => new Promise((
|
|
39700
|
-
server.close(() =>
|
|
40222
|
+
(server) => new Promise((resolve2) => {
|
|
40223
|
+
server.close(() => resolve2());
|
|
39701
40224
|
})
|
|
39702
40225
|
);
|
|
39703
40226
|
await Promise.allSettled(serverClosePromises);
|
|
39704
40227
|
this._httpServers.clear();
|
|
40228
|
+
if (this._adaptiveLimiter) {
|
|
40229
|
+
this._adaptiveLimiter.stop();
|
|
40230
|
+
}
|
|
40231
|
+
for (const plugin of this._plugins) {
|
|
40232
|
+
try {
|
|
40233
|
+
const result = plugin.onClose?.();
|
|
40234
|
+
if (result instanceof Promise) {
|
|
40235
|
+
await result;
|
|
40236
|
+
}
|
|
40237
|
+
} catch (err) {
|
|
40238
|
+
this._logger?.error?.("Plugin onClose error", {
|
|
40239
|
+
name: plugin.name,
|
|
40240
|
+
error: err.message
|
|
40241
|
+
});
|
|
40242
|
+
}
|
|
40243
|
+
}
|
|
39705
40244
|
if (this._adapter) {
|
|
39706
40245
|
await this._adapter.disconnect();
|
|
39707
40246
|
}
|
|
@@ -39763,6 +40302,55 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39763
40302
|
return void 0;
|
|
39764
40303
|
}
|
|
39765
40304
|
}
|
|
40305
|
+
// ─── Batch Calls ──────────────────────────────────────────────
|
|
40306
|
+
/**
|
|
40307
|
+
* Pipeline multiple calls to a single client into a concurrent batch.
|
|
40308
|
+
* Useful for reconnection warm-up (e.g. GetConfiguration, ChangeAvailability, etc.)
|
|
40309
|
+
* where sequential calls would add unnecessary round-trip latency.
|
|
40310
|
+
*
|
|
40311
|
+
* @param identity The client identity to send calls to
|
|
40312
|
+
* @param calls Array of { method, params, options? } to execute concurrently
|
|
40313
|
+
* @returns Array of results in the same order as the calls array.
|
|
40314
|
+
* Each element is the call result, or `undefined` if that individual call failed.
|
|
40315
|
+
*
|
|
40316
|
+
* @example
|
|
40317
|
+
* ```ts
|
|
40318
|
+
* const results = await server.sendBatch('CP-101', [
|
|
40319
|
+
* { method: 'GetConfiguration', params: { key: ['MeterInterval'] } },
|
|
40320
|
+
* { method: 'ChangeAvailability', params: { type: 'Operative' } },
|
|
40321
|
+
* { method: 'TriggerMessage', params: { requestedMessage: 'StatusNotification' } },
|
|
40322
|
+
* ]);
|
|
40323
|
+
* ```
|
|
40324
|
+
*/
|
|
40325
|
+
async sendBatch(identity, calls) {
|
|
40326
|
+
if (calls.length === 0) return [];
|
|
40327
|
+
const client = this._clientsByIdentity.get(identity);
|
|
40328
|
+
if (!client) {
|
|
40329
|
+
this._logger?.warn?.("sendBatch: client not found locally", { identity });
|
|
40330
|
+
return calls.map(() => void 0);
|
|
40331
|
+
}
|
|
40332
|
+
const originalConcurrency = client.options.callConcurrency ?? 1;
|
|
40333
|
+
if (calls.length > originalConcurrency) {
|
|
40334
|
+
client.reconfigure({ callConcurrency: calls.length });
|
|
40335
|
+
}
|
|
40336
|
+
try {
|
|
40337
|
+
const results = await Promise.allSettled(
|
|
40338
|
+
calls.map((c) => client.call(c.method, c.params, c.options ?? {}))
|
|
40339
|
+
);
|
|
40340
|
+
return results.map((r) => {
|
|
40341
|
+
if (r.status === "fulfilled") return r.value;
|
|
40342
|
+
this._logger?.warn?.("sendBatch: individual call failed", {
|
|
40343
|
+
identity,
|
|
40344
|
+
error: r.reason?.message
|
|
40345
|
+
});
|
|
40346
|
+
return void 0;
|
|
40347
|
+
});
|
|
40348
|
+
} finally {
|
|
40349
|
+
if (calls.length > originalConcurrency) {
|
|
40350
|
+
client.reconfigure({ callConcurrency: originalConcurrency });
|
|
40351
|
+
}
|
|
40352
|
+
}
|
|
40353
|
+
}
|
|
39766
40354
|
// ─── Pub/Sub Adapter ─────────────────────────────────────────
|
|
39767
40355
|
async setAdapter(adapter) {
|
|
39768
40356
|
this._adapter = adapter;
|
|
@@ -39904,9 +40492,35 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39904
40492
|
}
|
|
39905
40493
|
await Promise.all(localPromises);
|
|
39906
40494
|
}
|
|
40495
|
+
// ─── Internal: Compression Config ───────────────────────────────
|
|
40496
|
+
_buildCompressionConfig() {
|
|
40497
|
+
const compression = this._options.compression;
|
|
40498
|
+
if (!compression) return false;
|
|
40499
|
+
if (compression === true) {
|
|
40500
|
+
return {
|
|
40501
|
+
threshold: 1024,
|
|
40502
|
+
zlibDeflateOptions: { level: 6, memLevel: 8 },
|
|
40503
|
+
zlibInflateOptions: {},
|
|
40504
|
+
serverNoContextTakeover: true,
|
|
40505
|
+
clientNoContextTakeover: true
|
|
40506
|
+
};
|
|
40507
|
+
}
|
|
40508
|
+
return {
|
|
40509
|
+
threshold: compression.threshold ?? 1024,
|
|
40510
|
+
zlibDeflateOptions: {
|
|
40511
|
+
level: compression.level ?? 6,
|
|
40512
|
+
memLevel: compression.memLevel ?? 8
|
|
40513
|
+
},
|
|
40514
|
+
zlibInflateOptions: {},
|
|
40515
|
+
serverNoContextTakeover: compression.serverNoContextTakeover ?? true,
|
|
40516
|
+
clientNoContextTakeover: compression.clientNoContextTakeover ?? true
|
|
40517
|
+
};
|
|
40518
|
+
}
|
|
39907
40519
|
};
|
|
39908
40520
|
// Annotate the CommonJS export names for ESM import in node:
|
|
39909
40521
|
0 && (module.exports = {
|
|
40522
|
+
AdaptiveLimiter,
|
|
40523
|
+
ClusterDriver,
|
|
39910
40524
|
ConnectionState,
|
|
39911
40525
|
InMemoryAdapter,
|
|
39912
40526
|
LRUMap,
|
|
@@ -39938,6 +40552,7 @@ var OCPPServer = class extends import_node_events3.EventEmitter {
|
|
|
39938
40552
|
WebsocketUpgradeError,
|
|
39939
40553
|
combineAuth,
|
|
39940
40554
|
createLoggingMiddleware,
|
|
40555
|
+
createPlugin,
|
|
39941
40556
|
createRPCError,
|
|
39942
40557
|
createRouter,
|
|
39943
40558
|
createValidator,
|