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.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
|
-
|
|
353
|
-
await
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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((
|
|
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((
|
|
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/
|
|
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((
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
37639
|
+
resolve2();
|
|
37386
37640
|
};
|
|
37387
37641
|
p.reject = (r) => {
|
|
37388
37642
|
origReject(r);
|
|
37389
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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(
|
|
@@ -37667,6 +37925,36 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
|
|
|
37667
37925
|
return;
|
|
37668
37926
|
}
|
|
37669
37927
|
const messageType = message[0];
|
|
37928
|
+
const messageId = message[1];
|
|
37929
|
+
if (typeof messageId !== "string") {
|
|
37930
|
+
this._onBadMessage(
|
|
37931
|
+
typeof rawData === "string" ? rawData : rawData.toString(),
|
|
37932
|
+
new RPCMessageTypeNotSupportedError(
|
|
37933
|
+
`Invalid MessageId type: ${typeof messageId} (expected string)`
|
|
37934
|
+
)
|
|
37935
|
+
);
|
|
37936
|
+
return;
|
|
37937
|
+
}
|
|
37938
|
+
if (messageType === MessageType.CALL && message.length < 4 || messageType === MessageType.CALLRESULT && message.length < 3 || messageType === MessageType.CALLERROR && message.length < 5) {
|
|
37939
|
+
this._onBadMessage(
|
|
37940
|
+
JSON.stringify(message),
|
|
37941
|
+
new RPCMessageTypeNotSupportedError(
|
|
37942
|
+
`Missing payload elements for message type ${messageType}`
|
|
37943
|
+
)
|
|
37944
|
+
);
|
|
37945
|
+
return;
|
|
37946
|
+
}
|
|
37947
|
+
const payloadIndex = messageType === MessageType.CALLERROR ? 4 : messageType === MessageType.CALL ? 3 : 2;
|
|
37948
|
+
const payload = message[payloadIndex];
|
|
37949
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
37950
|
+
this._onBadMessage(
|
|
37951
|
+
JSON.stringify(message),
|
|
37952
|
+
new RPCMessageTypeNotSupportedError(
|
|
37953
|
+
`Payload must be a JSON object, got ${payload === null ? "null" : Array.isArray(payload) ? "array" : typeof payload}`
|
|
37954
|
+
)
|
|
37955
|
+
);
|
|
37956
|
+
return;
|
|
37957
|
+
}
|
|
37670
37958
|
switch (messageType) {
|
|
37671
37959
|
case MessageType.CALL:
|
|
37672
37960
|
this._handleIncomingCall(message);
|
|
@@ -38146,6 +38434,23 @@ var OCPPClient = class _OCPPClient extends EventEmitter {
|
|
|
38146
38434
|
if (tls.passphrase) opts.passphrase = tls.passphrase;
|
|
38147
38435
|
}
|
|
38148
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
|
+
}
|
|
38149
38454
|
return opts;
|
|
38150
38455
|
}
|
|
38151
38456
|
// ─── Internal: Cleanup ───────────────────────────────────────
|
|
@@ -38211,7 +38516,7 @@ var LRUMap = class extends Map {
|
|
|
38211
38516
|
};
|
|
38212
38517
|
|
|
38213
38518
|
// src/router.ts
|
|
38214
|
-
import { EventEmitter as
|
|
38519
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
38215
38520
|
async function executeMiddlewareChain(middlewares, ctx) {
|
|
38216
38521
|
let index = -1;
|
|
38217
38522
|
const dispatch = async (i, payload) => {
|
|
@@ -38235,7 +38540,7 @@ async function executeMiddlewareChain(middlewares, ctx) {
|
|
|
38235
38540
|
};
|
|
38236
38541
|
await dispatch(0);
|
|
38237
38542
|
}
|
|
38238
|
-
var OCPPRouter = class extends
|
|
38543
|
+
var OCPPRouter = class extends EventEmitter3 {
|
|
38239
38544
|
/** Raw registered patterns (strings and/or RegExp) for reference. */
|
|
38240
38545
|
patterns;
|
|
38241
38546
|
/** Connection middlewares attached to this router. */
|
|
@@ -38331,7 +38636,7 @@ function createRouter(...patterns) {
|
|
|
38331
38636
|
}
|
|
38332
38637
|
|
|
38333
38638
|
// src/server.ts
|
|
38334
|
-
import { EventEmitter as
|
|
38639
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
38335
38640
|
import {
|
|
38336
38641
|
createServer as createHttpServer
|
|
38337
38642
|
} from "http";
|
|
@@ -38415,8 +38720,8 @@ var TrieNode = class {
|
|
|
38415
38720
|
/** Routers registered at this exact node (leaf). */
|
|
38416
38721
|
routers = [];
|
|
38417
38722
|
};
|
|
38418
|
-
function normalizePath(
|
|
38419
|
-
return
|
|
38723
|
+
function normalizePath(path2) {
|
|
38724
|
+
return path2.replace(/\/+/g, "/").replace(/^\/|\/$/g, "").split("/").filter(Boolean);
|
|
38420
38725
|
}
|
|
38421
38726
|
var RadixTrie = class {
|
|
38422
38727
|
root = new TrieNode();
|
|
@@ -38563,6 +38868,8 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38563
38868
|
super(options);
|
|
38564
38869
|
this._serverSession = context.session;
|
|
38565
38870
|
this._serverHandshake = context.handshake;
|
|
38871
|
+
this._adaptiveMultiplier = context.adaptiveMultiplier ?? null;
|
|
38872
|
+
this._workerPool = context.workerPool ?? null;
|
|
38566
38873
|
this._state = ConnectionState.OPEN;
|
|
38567
38874
|
this._identity = this._options.identity;
|
|
38568
38875
|
this._ws = context.ws;
|
|
@@ -38572,6 +38879,8 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38572
38879
|
}
|
|
38573
38880
|
// ─── Rate Limiting State ──────────────────────────────────────────
|
|
38574
38881
|
_rateLimits = {};
|
|
38882
|
+
_adaptiveMultiplier = null;
|
|
38883
|
+
_workerPool = null;
|
|
38575
38884
|
_checkRateLimit(method) {
|
|
38576
38885
|
const limits = this._options.rateLimit;
|
|
38577
38886
|
if (!limits) return true;
|
|
@@ -38583,7 +38892,8 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38583
38892
|
this._rateLimits[key] = bucket;
|
|
38584
38893
|
} else {
|
|
38585
38894
|
const timePassed = now - bucket.lastRefill;
|
|
38586
|
-
const
|
|
38895
|
+
const adaptiveScale = this._adaptiveMultiplier?.() ?? 1;
|
|
38896
|
+
const refillRate = limit / windowMs * adaptiveScale;
|
|
38587
38897
|
const tokensToAdd = timePassed * refillRate;
|
|
38588
38898
|
if (tokensToAdd > 0) {
|
|
38589
38899
|
bucket.tokens = Math.min(limit, bucket.tokens + tokensToAdd);
|
|
@@ -38628,6 +38938,19 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38628
38938
|
this._handleRateLimitExceeded(pData || data.toString());
|
|
38629
38939
|
return;
|
|
38630
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;
|
|
38631
38954
|
}
|
|
38632
38955
|
this._onMessage(data);
|
|
38633
38956
|
});
|
|
@@ -38638,7 +38961,18 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38638
38961
|
this._onClose(code, reason)
|
|
38639
38962
|
)
|
|
38640
38963
|
);
|
|
38641
|
-
ws.on("error", (err) =>
|
|
38964
|
+
ws.on("error", (err) => {
|
|
38965
|
+
if (this.listenerCount("error") > 0) {
|
|
38966
|
+
this.emit("error", err);
|
|
38967
|
+
} else {
|
|
38968
|
+
this._logger?.debug?.(
|
|
38969
|
+
"WebSocket error (unhandled by client listener)",
|
|
38970
|
+
{
|
|
38971
|
+
error: err.message
|
|
38972
|
+
}
|
|
38973
|
+
);
|
|
38974
|
+
}
|
|
38975
|
+
});
|
|
38642
38976
|
ws.on("ping", () => {
|
|
38643
38977
|
this._recordActivity();
|
|
38644
38978
|
this.emit("ping");
|
|
@@ -38717,8 +39051,110 @@ var OCPPServerClient = class extends OCPPClient {
|
|
|
38717
39051
|
}
|
|
38718
39052
|
};
|
|
38719
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
|
+
|
|
38720
39156
|
// src/server.ts
|
|
38721
|
-
var OCPPServer = class extends
|
|
39157
|
+
var OCPPServer = class extends EventEmitter4 {
|
|
38722
39158
|
_options;
|
|
38723
39159
|
/** Radix trie for O(k) route matching (string patterns). */
|
|
38724
39160
|
_trie = new RadixTrie();
|
|
@@ -38737,6 +39173,9 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
38737
39173
|
_globalCORS;
|
|
38738
39174
|
// Connection-level rate limiting (per-IP token bucket)
|
|
38739
39175
|
_connectionBuckets = /* @__PURE__ */ new Map();
|
|
39176
|
+
_adaptiveLimiter = null;
|
|
39177
|
+
_plugins = [];
|
|
39178
|
+
_workerPool = null;
|
|
38740
39179
|
// Robustness & Clustering
|
|
38741
39180
|
_nodeId = createId2();
|
|
38742
39181
|
_sessions;
|
|
@@ -38746,9 +39185,9 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
38746
39185
|
super();
|
|
38747
39186
|
this.setMaxListeners(0);
|
|
38748
39187
|
if (options.strictMode) {
|
|
38749
|
-
if (!options.
|
|
39188
|
+
if (!options.protocols?.length) {
|
|
38750
39189
|
throw new Error(
|
|
38751
|
-
"strictMode requires
|
|
39190
|
+
"strictMode requires protocols to be specified (e.g. protocols: ['ocpp1.6'])"
|
|
38752
39191
|
);
|
|
38753
39192
|
}
|
|
38754
39193
|
}
|
|
@@ -38769,7 +39208,8 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
38769
39208
|
this._sessions = new LRUMap(maxSessions);
|
|
38770
39209
|
this._wss = new WebSocketServer({
|
|
38771
39210
|
noServer: true,
|
|
38772
|
-
maxPayload: this._options.maxPayloadBytes ?? 65536
|
|
39211
|
+
maxPayload: this._options.maxPayloadBytes ?? 65536,
|
|
39212
|
+
perMessageDeflate: this._buildCompressionConfig()
|
|
38773
39213
|
});
|
|
38774
39214
|
this._gcInterval = setInterval(() => {
|
|
38775
39215
|
const now = Date.now();
|
|
@@ -38782,6 +39222,32 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
38782
39222
|
this._logger = initLogger(this._options.logging, {
|
|
38783
39223
|
component: "OCPPServer"
|
|
38784
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
|
+
}
|
|
38785
39251
|
}
|
|
38786
39252
|
// ─── Getters ─────────────────────────────────────────────────
|
|
38787
39253
|
get log() {
|
|
@@ -38904,8 +39370,44 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
38904
39370
|
return this;
|
|
38905
39371
|
}
|
|
38906
39372
|
/**
|
|
38907
|
-
* Registers
|
|
38908
|
-
*
|
|
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
|
+
* ```
|
|
38909
39411
|
*/
|
|
38910
39412
|
use(...middlewares) {
|
|
38911
39413
|
const router = new OCPPRouter();
|
|
@@ -39061,7 +39563,7 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39061
39563
|
);
|
|
39062
39564
|
}
|
|
39063
39565
|
if (!options?.server) {
|
|
39064
|
-
await new Promise((
|
|
39566
|
+
await new Promise((resolve2, reject) => {
|
|
39065
39567
|
httpServer.on("error", reject);
|
|
39066
39568
|
httpServer.listen(port, host, () => {
|
|
39067
39569
|
httpServer.removeListener("error", reject);
|
|
@@ -39070,7 +39572,7 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39070
39572
|
port: typeof addr === "object" ? addr?.port : port,
|
|
39071
39573
|
host: host ?? "0.0.0.0"
|
|
39072
39574
|
});
|
|
39073
|
-
|
|
39575
|
+
resolve2();
|
|
39074
39576
|
});
|
|
39075
39577
|
});
|
|
39076
39578
|
}
|
|
@@ -39379,13 +39881,13 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39379
39881
|
selectedProtocol = handshake.protocols.values().next().value ?? void 0;
|
|
39380
39882
|
} else {
|
|
39381
39883
|
acceptOptions = await new Promise(
|
|
39382
|
-
(
|
|
39884
|
+
(resolve2, reject) => {
|
|
39383
39885
|
let settled = false;
|
|
39384
39886
|
const accept = (opts) => {
|
|
39385
39887
|
if (settled) return;
|
|
39386
39888
|
settled = true;
|
|
39387
39889
|
if (opts?.protocol) selectedProtocol = opts.protocol;
|
|
39388
|
-
|
|
39890
|
+
resolve2(opts);
|
|
39389
39891
|
};
|
|
39390
39892
|
const rejectAuth = (code = 401, message = "Unauthorized") => {
|
|
39391
39893
|
if (!settled) {
|
|
@@ -39517,7 +40019,9 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39517
40019
|
ws,
|
|
39518
40020
|
handshake,
|
|
39519
40021
|
session: finalSession,
|
|
39520
|
-
protocol: selectedProtocol
|
|
40022
|
+
protocol: selectedProtocol,
|
|
40023
|
+
adaptiveMultiplier: this._adaptiveLimiter ? () => this._adaptiveLimiter.multiplier : void 0,
|
|
40024
|
+
workerPool: this._workerPool ?? void 0
|
|
39521
40025
|
});
|
|
39522
40026
|
this._updateSessionActivity(identity, client.session);
|
|
39523
40027
|
const existingClient = this._clientsByIdentity.get(identity);
|
|
@@ -39549,21 +40053,52 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39549
40053
|
remoteAddress: req.socket.remoteAddress,
|
|
39550
40054
|
protocol: selectedProtocol
|
|
39551
40055
|
});
|
|
39552
|
-
client.on(
|
|
39553
|
-
|
|
39554
|
-
|
|
39555
|
-
this.
|
|
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 });
|
|
39556
40082
|
}
|
|
39557
|
-
|
|
39558
|
-
|
|
39559
|
-
|
|
39560
|
-
|
|
39561
|
-
|
|
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
|
+
});
|
|
39562
40093
|
});
|
|
40094
|
+
}
|
|
40095
|
+
} catch (err) {
|
|
40096
|
+
this._logger?.error?.("Plugin onConnection error", {
|
|
40097
|
+
name: plugin.name,
|
|
40098
|
+
error: err.message
|
|
39563
40099
|
});
|
|
39564
40100
|
}
|
|
39565
|
-
|
|
39566
|
-
});
|
|
40101
|
+
}
|
|
39567
40102
|
this.emit("client", client);
|
|
39568
40103
|
for (const router of matchedRouters) {
|
|
39569
40104
|
router.emit("client", client);
|
|
@@ -39598,13 +40133,13 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39598
40133
|
identity: client.identity,
|
|
39599
40134
|
bufferedAmount: ws.bufferedAmount
|
|
39600
40135
|
});
|
|
39601
|
-
await new Promise((
|
|
40136
|
+
await new Promise((resolve2) => {
|
|
39602
40137
|
let elapsed = 0;
|
|
39603
40138
|
const check = setInterval(() => {
|
|
39604
40139
|
elapsed += 50;
|
|
39605
40140
|
if (!ws || ws.bufferedAmount === 0 || elapsed >= drainTimeout) {
|
|
39606
40141
|
clearInterval(check);
|
|
39607
|
-
|
|
40142
|
+
resolve2();
|
|
39608
40143
|
}
|
|
39609
40144
|
}, 50);
|
|
39610
40145
|
});
|
|
@@ -39626,12 +40161,28 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39626
40161
|
this._wss = new WebSocketServer({ noServer: true });
|
|
39627
40162
|
}
|
|
39628
40163
|
const serverClosePromises = Array.from(this._httpServers).map(
|
|
39629
|
-
(server) => new Promise((
|
|
39630
|
-
server.close(() =>
|
|
40164
|
+
(server) => new Promise((resolve2) => {
|
|
40165
|
+
server.close(() => resolve2());
|
|
39631
40166
|
})
|
|
39632
40167
|
);
|
|
39633
40168
|
await Promise.allSettled(serverClosePromises);
|
|
39634
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
|
+
}
|
|
39635
40186
|
if (this._adapter) {
|
|
39636
40187
|
await this._adapter.disconnect();
|
|
39637
40188
|
}
|
|
@@ -39693,6 +40244,55 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39693
40244
|
return void 0;
|
|
39694
40245
|
}
|
|
39695
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
|
+
}
|
|
39696
40296
|
// ─── Pub/Sub Adapter ─────────────────────────────────────────
|
|
39697
40297
|
async setAdapter(adapter) {
|
|
39698
40298
|
this._adapter = adapter;
|
|
@@ -39834,8 +40434,34 @@ var OCPPServer = class extends EventEmitter3 {
|
|
|
39834
40434
|
}
|
|
39835
40435
|
await Promise.all(localPromises);
|
|
39836
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
|
+
}
|
|
39837
40461
|
};
|
|
39838
40462
|
export {
|
|
40463
|
+
AdaptiveLimiter,
|
|
40464
|
+
ClusterDriver,
|
|
39839
40465
|
ConnectionState,
|
|
39840
40466
|
InMemoryAdapter,
|
|
39841
40467
|
LRUMap,
|
|
@@ -39867,6 +40493,7 @@ export {
|
|
|
39867
40493
|
WebsocketUpgradeError,
|
|
39868
40494
|
combineAuth,
|
|
39869
40495
|
createLoggingMiddleware,
|
|
40496
|
+
createPlugin,
|
|
39870
40497
|
createRPCError,
|
|
39871
40498
|
createRouter,
|
|
39872
40499
|
createValidator,
|