ocpp-ws-io 2.1.3

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.
@@ -0,0 +1,1284 @@
1
+ // src/helpers/index.ts
2
+ function defineMiddleware(mw) {
3
+ return mw;
4
+ }
5
+ function defineRpcMiddleware(mw) {
6
+ return mw;
7
+ }
8
+ function defineAuth(cb) {
9
+ return cb;
10
+ }
11
+ function combineAuth(...cbs) {
12
+ return async (ctx) => {
13
+ let accepted = false;
14
+ let rejected = false;
15
+ const trackedAccept = (opts) => {
16
+ accepted = true;
17
+ ctx.accept(opts);
18
+ };
19
+ const trackedReject = (code, message) => {
20
+ rejected = true;
21
+ return ctx.reject(code, message);
22
+ };
23
+ const trackedCtx = {
24
+ ...ctx,
25
+ accept: trackedAccept,
26
+ reject: trackedReject
27
+ };
28
+ try {
29
+ for (const cb of cbs) {
30
+ if (ctx.signal.aborted || accepted || rejected) break;
31
+ const p = cb(trackedCtx);
32
+ if (p instanceof Promise) {
33
+ await p;
34
+ }
35
+ if (accepted || rejected) break;
36
+ }
37
+ if (!accepted && !rejected) {
38
+ trackedReject(
39
+ 401,
40
+ "Unauthorized (All composeAuth handlers passed without accepting)"
41
+ );
42
+ }
43
+ } catch (_err) {
44
+ if (!rejected) {
45
+ trackedReject(
46
+ 500,
47
+ "Internal Server Error during auth compose execution"
48
+ );
49
+ }
50
+ }
51
+ };
52
+ }
53
+ function createLoggingMiddleware(logger, identity, config = {}) {
54
+ const options = typeof config === "object" ? config : {};
55
+ const { exchangeLog = false, prettify = false } = options;
56
+ return async (ctx, next) => {
57
+ const start = Date.now();
58
+ const method = ctx.method;
59
+ const level = exchangeLog ? "info" : "debug";
60
+ switch (ctx.type) {
61
+ case "incoming_call":
62
+ if (exchangeLog && prettify) {
63
+ logger[level]?.(`\u26A1 ${identity} \u2190 ${method} [IN]`, {
64
+ messageId: ctx.messageId,
65
+ method: ctx.method,
66
+ protocol: ctx.protocol,
67
+ payload: ctx.params,
68
+ direction: "IN"
69
+ });
70
+ } else {
71
+ logger[level]?.(`CALL \u2190`, {
72
+ messageId: ctx.messageId,
73
+ method: ctx.method,
74
+ protocol: ctx.protocol,
75
+ payload: ctx.params,
76
+ direction: "IN"
77
+ });
78
+ }
79
+ break;
80
+ case "outgoing_call":
81
+ if (exchangeLog && prettify) {
82
+ logger[level]?.(`\u26A1 ${identity} \u2192 ${method} [OUT]`, {
83
+ method: ctx.method,
84
+ params: ctx.params,
85
+ direction: "OUT"
86
+ });
87
+ } else {
88
+ logger[level]?.(`CALL \u2192`, {
89
+ method: ctx.method,
90
+ params: ctx.params,
91
+ direction: "OUT"
92
+ });
93
+ }
94
+ break;
95
+ }
96
+ try {
97
+ const result = await next();
98
+ const durationMs = Date.now() - start;
99
+ switch (ctx.type) {
100
+ case "incoming_call":
101
+ if (result !== void 0 && result !== null) {
102
+ if (exchangeLog && prettify) {
103
+ logger[level]?.(`\u2705 ${identity} \u2192 ${method} [RES]`, {
104
+ messageId: ctx.messageId,
105
+ method: ctx.method,
106
+ durationMs,
107
+ params: result,
108
+ direction: "OUT"
109
+ });
110
+ } else {
111
+ logger[level]?.(`CALLRESULT \u2192`, {
112
+ messageId: ctx.messageId,
113
+ method: ctx.method,
114
+ durationMs,
115
+ params: result,
116
+ direction: "OUT"
117
+ });
118
+ }
119
+ }
120
+ break;
121
+ case "outgoing_call":
122
+ if (exchangeLog && prettify) {
123
+ logger[level]?.(`\u2705 ${identity} \u2190 ${method} [RES]`, {
124
+ messageId: ctx.messageId,
125
+ method: ctx.method,
126
+ durationMs,
127
+ payload: result,
128
+ direction: "IN"
129
+ });
130
+ } else {
131
+ logger[level]?.(`CALLRESULT \u2190`, {
132
+ messageId: ctx.messageId,
133
+ method: ctx.method,
134
+ durationMs,
135
+ payload: result,
136
+ direction: "IN"
137
+ });
138
+ }
139
+ break;
140
+ }
141
+ return result;
142
+ } catch (err) {
143
+ const msg = err.message;
144
+ const durationMs = Date.now() - start;
145
+ if (ctx.type === "incoming_call") {
146
+ if (exchangeLog && prettify) {
147
+ logger.error?.(`\u{1F6A8} ${identity} \u2192 ${method} [ERR]`, {
148
+ messageId: ctx.messageId,
149
+ method: ctx.method,
150
+ durationMs,
151
+ error: msg,
152
+ direction: "OUT"
153
+ });
154
+ } else {
155
+ logger.error?.(`CALLERROR \u2192`, {
156
+ messageId: ctx.messageId,
157
+ method: ctx.method,
158
+ durationMs,
159
+ error: msg,
160
+ direction: "OUT"
161
+ });
162
+ }
163
+ } else if (ctx.type === "outgoing_call") {
164
+ if (exchangeLog && prettify) {
165
+ logger.warn?.(`\u{1F6A8} ${identity} \u2190 ${method} [ERR]`, {
166
+ messageId: ctx.messageId,
167
+ method: ctx.method,
168
+ durationMs,
169
+ error: msg,
170
+ direction: "IN"
171
+ });
172
+ } else {
173
+ logger.warn?.(`CALLERROR \u2190`, {
174
+ messageId: ctx.messageId,
175
+ method: ctx.method,
176
+ durationMs,
177
+ error: msg,
178
+ direction: "IN"
179
+ });
180
+ }
181
+ }
182
+ throw err;
183
+ }
184
+ };
185
+ }
186
+
187
+ // src/middleware.ts
188
+ var MiddlewareStack = class {
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
190
+ _stack = [];
191
+ /**
192
+ * Add a middleware function to the stack.
193
+ */
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
+ use(middleware) {
196
+ this._stack.push(middleware);
197
+ }
198
+ /**
199
+ * Execute the middleware stack composed with a runner function.
200
+ *
201
+ * @param context The context object to pass through middleware
202
+ * @param runner The final function to execute (the "core" logic)
203
+ */
204
+ async execute(context, runner) {
205
+ let index = -1;
206
+ const dispatch = async (i) => {
207
+ if (i <= index) {
208
+ throw new Error("next() called multiple times");
209
+ }
210
+ index = i;
211
+ const fn = this._stack[i];
212
+ if (i === this._stack.length) {
213
+ return runner(context);
214
+ }
215
+ if (!fn) {
216
+ return void 0;
217
+ }
218
+ return fn(context, () => dispatch(i + 1));
219
+ };
220
+ return dispatch(0);
221
+ }
222
+ };
223
+
224
+ // src/init-logger.ts
225
+ import {
226
+ consoleTransport,
227
+ createLogger,
228
+ prettyTransport
229
+ } from "voltlog-io";
230
+ function hasDisplayCustomization(config) {
231
+ return config.showMetadata === false || config.showSourceMeta === false || config.prettifySource === true || config.prettifyMetadata === true;
232
+ }
233
+ function buildDisplayMiddleware(config) {
234
+ const showMeta = config.showMetadata ?? true;
235
+ const showSource = config.showSourceMeta ?? true;
236
+ const prettySrc = config.prettifySource ?? false;
237
+ const prettyMeta = config.prettifyMetadata ?? false;
238
+ const DIM = "\x1B[2;37m";
239
+ const RESET = "\x1B[0m";
240
+ const CYAN = "\x1B[36m";
241
+ return (entry, next) => {
242
+ if (!showSource) {
243
+ entry.context = void 0;
244
+ } else if (prettySrc && entry.context) {
245
+ const parts = [];
246
+ if (entry.context.component) parts.push(String(entry.context.component));
247
+ if (entry.context.identity) parts.push(String(entry.context.identity));
248
+ if (parts.length > 0) {
249
+ entry.message = `${DIM}[${parts.join("/")}]${RESET} ${entry.message}`;
250
+ entry.context = void 0;
251
+ }
252
+ }
253
+ const meta = entry.meta;
254
+ if (!showMeta) {
255
+ entry.meta = {};
256
+ } else if (prettyMeta && meta && Object.keys(meta).length > 0) {
257
+ const pairs = Object.entries(meta).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => {
258
+ let valStr = typeof v === "object" ? JSON.stringify(v) : String(v);
259
+ if (typeof v === "string") {
260
+ valStr = `${DIM}${valStr}${RESET}`;
261
+ } else {
262
+ valStr = `${DIM}${valStr}${RESET}`;
263
+ }
264
+ return `${CYAN}${k}${RESET}=${valStr}`;
265
+ }).join(" ");
266
+ if (pairs) {
267
+ entry.message = `${entry.message} ${pairs}`;
268
+ }
269
+ entry.meta = {};
270
+ }
271
+ next(entry);
272
+ };
273
+ }
274
+ function initLogger(config, defaultContext) {
275
+ if (config === false) return null;
276
+ if (config?.enabled === false) return null;
277
+ if (config?.logger) {
278
+ if (defaultContext && config.logger.child) {
279
+ return config.logger.child(defaultContext);
280
+ }
281
+ return config.logger;
282
+ }
283
+ const level = config?.level ?? "INFO";
284
+ const usePrettify = config?.prettify ?? false;
285
+ const transports = usePrettify ? [prettyTransport({ level })] : [consoleTransport({ level })];
286
+ if (config?.handler) {
287
+ const customTransport = config.handler;
288
+ transports.push({
289
+ name: "customHandler",
290
+ write: (entry) => customTransport(entry)
291
+ });
292
+ }
293
+ const middleware = [];
294
+ if (config && hasDisplayCustomization(config)) {
295
+ middleware.push(buildDisplayMiddleware(config));
296
+ }
297
+ const logger = createLogger({
298
+ level,
299
+ transports,
300
+ middleware: middleware.length > 0 ? middleware : void 0
301
+ });
302
+ if (defaultContext && Object.keys(defaultContext).length > 0) {
303
+ return logger.child(defaultContext);
304
+ }
305
+ return logger;
306
+ }
307
+
308
+ // src/browser/emitter.ts
309
+ var EventEmitter = class {
310
+ _listeners = /* @__PURE__ */ new Map();
311
+ on(event, listener) {
312
+ const arr = this._listeners.get(event);
313
+ if (arr) {
314
+ arr.push(listener);
315
+ } else {
316
+ this._listeners.set(event, [listener]);
317
+ }
318
+ return this;
319
+ }
320
+ once(event, listener) {
321
+ const wrapper = (...args) => {
322
+ this.off(event, wrapper);
323
+ listener(...args);
324
+ };
325
+ wrapper.__wrapped = listener;
326
+ return this.on(event, wrapper);
327
+ }
328
+ off(event, listener) {
329
+ const arr = this._listeners.get(event);
330
+ if (!arr) return this;
331
+ const idx = arr.findIndex(
332
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
333
+ (fn) => fn === listener || fn.__wrapped === listener
334
+ );
335
+ if (idx !== -1) arr.splice(idx, 1);
336
+ if (arr.length === 0) this._listeners.delete(event);
337
+ return this;
338
+ }
339
+ emit(event, ...args) {
340
+ const arr = this._listeners.get(event);
341
+ if (!arr || arr.length === 0) return false;
342
+ for (const fn of [...arr]) {
343
+ fn(...args);
344
+ }
345
+ return true;
346
+ }
347
+ addListener(event, listener) {
348
+ return this.on(event, listener);
349
+ }
350
+ removeListener(event, listener) {
351
+ return this.off(event, listener);
352
+ }
353
+ removeAllListeners(event) {
354
+ if (event) {
355
+ this._listeners.delete(event);
356
+ } else {
357
+ this._listeners.clear();
358
+ }
359
+ return this;
360
+ }
361
+ listenerCount(event) {
362
+ return this._listeners.get(event)?.length ?? 0;
363
+ }
364
+ };
365
+
366
+ // src/browser/errors.ts
367
+ var TimeoutError = class extends Error {
368
+ constructor(message = "Operation timed out") {
369
+ super(message);
370
+ this.name = "TimeoutError";
371
+ }
372
+ };
373
+ var RPCGenericError = class extends Error {
374
+ rpcErrorCode = "GenericError";
375
+ rpcErrorMessage = "";
376
+ details;
377
+ constructor(message, details = {}) {
378
+ super(message);
379
+ this.name = "RPCGenericError";
380
+ this.details = details;
381
+ }
382
+ };
383
+ var RPCNotImplementedError = class extends RPCGenericError {
384
+ rpcErrorCode = "NotImplemented";
385
+ rpcErrorMessage = "Requested method is not known";
386
+ constructor(message, details = {}) {
387
+ super(message, details);
388
+ this.name = "RPCNotImplementedError";
389
+ }
390
+ };
391
+ var RPCNotSupportedError = class extends RPCGenericError {
392
+ rpcErrorCode = "NotSupported";
393
+ rpcErrorMessage = "Requested method is recognised but not supported";
394
+ constructor(message, details = {}) {
395
+ super(message, details);
396
+ this.name = "RPCNotSupportedError";
397
+ }
398
+ };
399
+ var RPCInternalError = class extends RPCGenericError {
400
+ rpcErrorCode = "InternalError";
401
+ rpcErrorMessage = "An internal error occurred and the receiver was not able to process the requested action successfully";
402
+ constructor(message, details = {}) {
403
+ super(message, details);
404
+ this.name = "RPCInternalError";
405
+ }
406
+ };
407
+ var RPCProtocolError = class extends RPCGenericError {
408
+ rpcErrorCode = "ProtocolError";
409
+ rpcErrorMessage = "Payload for action is incomplete";
410
+ constructor(message, details = {}) {
411
+ super(message, details);
412
+ this.name = "RPCProtocolError";
413
+ }
414
+ };
415
+ var RPCSecurityError = class extends RPCGenericError {
416
+ rpcErrorCode = "SecurityError";
417
+ rpcErrorMessage = "During the processing of action a security issue occurred preventing receiver from completing the action successfully";
418
+ constructor(message, details = {}) {
419
+ super(message, details);
420
+ this.name = "RPCSecurityError";
421
+ }
422
+ };
423
+ var RPCFormationViolationError = class extends RPCGenericError {
424
+ rpcErrorCode = "FormationViolation";
425
+ rpcErrorMessage = "Payload for action is syntactically incorrect or not conform the PDU structure for action";
426
+ constructor(message, details = {}) {
427
+ super(message, details);
428
+ this.name = "RPCFormationViolationError";
429
+ }
430
+ };
431
+ var RPCFormatViolationError = class extends RPCGenericError {
432
+ rpcErrorCode = "FormatViolation";
433
+ rpcErrorMessage = "Payload is syntactically correct but at least one field contains an invalid value";
434
+ constructor(message, details = {}) {
435
+ super(message, details);
436
+ this.name = "RPCFormatViolationError";
437
+ }
438
+ };
439
+ var RPCPropertyConstraintViolationError = class extends RPCGenericError {
440
+ rpcErrorCode = "PropertyConstraintViolation";
441
+ rpcErrorMessage = "Payload is syntactically correct but at least one of the fields violates data type constraints";
442
+ constructor(message, details = {}) {
443
+ super(message, details);
444
+ this.name = "RPCPropertyConstraintViolationError";
445
+ }
446
+ };
447
+ var RPCOccurrenceConstraintViolationError = class extends RPCGenericError {
448
+ rpcErrorCode = "OccurrenceConstraintViolation";
449
+ rpcErrorMessage = "Payload for action is syntactically correct but at least one of the fields violates occurrence constraints";
450
+ constructor(message, details = {}) {
451
+ super(message, details);
452
+ this.name = "RPCOccurrenceConstraintViolationError";
453
+ }
454
+ };
455
+ var RPCTypeConstraintViolationError = class extends RPCGenericError {
456
+ rpcErrorCode = "TypeConstraintViolation";
457
+ rpcErrorMessage = "Payload for action is syntactically correct but at least one of the fields violates type constraints";
458
+ constructor(message, details = {}) {
459
+ super(message, details);
460
+ this.name = "RPCTypeConstraintViolationError";
461
+ }
462
+ };
463
+ var RPCMessageTypeNotSupportedError = class extends RPCGenericError {
464
+ rpcErrorCode = "MessageTypeNotSupported";
465
+ rpcErrorMessage = "A message with a Message Type Number received that is not supported by this implementation";
466
+ constructor(message, details = {}) {
467
+ super(message, details);
468
+ this.name = "RPCMessageTypeNotSupportedError";
469
+ }
470
+ };
471
+ var RPCFrameworkError = class extends RPCGenericError {
472
+ rpcErrorCode = "RpcFrameworkError";
473
+ rpcErrorMessage = "Content of the call is not a valid RPC request";
474
+ constructor(message, details = {}) {
475
+ super(message, details);
476
+ this.name = "RPCFrameworkError";
477
+ }
478
+ };
479
+
480
+ // src/browser/queue.ts
481
+ var Queue = class {
482
+ _concurrency;
483
+ _running = 0;
484
+ _queue = [];
485
+ constructor(concurrency = 1) {
486
+ this._concurrency = Math.max(1, concurrency);
487
+ }
488
+ get concurrency() {
489
+ return this._concurrency;
490
+ }
491
+ get pending() {
492
+ return this._queue.length;
493
+ }
494
+ get running() {
495
+ return this._running;
496
+ }
497
+ get size() {
498
+ return this._running + this._queue.length;
499
+ }
500
+ setConcurrency(concurrency) {
501
+ this._concurrency = Math.max(1, concurrency);
502
+ this._drain();
503
+ }
504
+ push(fn) {
505
+ return new Promise((resolve, reject) => {
506
+ this._queue.push({
507
+ fn,
508
+ resolve,
509
+ reject
510
+ });
511
+ this._drain();
512
+ });
513
+ }
514
+ _drain() {
515
+ while (this._running < this._concurrency && this._queue.length > 0) {
516
+ const item = this._queue.shift();
517
+ this._running++;
518
+ item?.fn().then(item.resolve).catch(item.reject).finally(() => {
519
+ this._running--;
520
+ this._drain();
521
+ });
522
+ }
523
+ }
524
+ };
525
+
526
+ // src/types.ts
527
+ var ConnectionState = {
528
+ CONNECTING: 0,
529
+ OPEN: 1,
530
+ CLOSING: 2,
531
+ CLOSED: 3
532
+ };
533
+ var MessageType = {
534
+ CALL: 2,
535
+ CALLRESULT: 3,
536
+ CALLERROR: 4
537
+ };
538
+ var NOREPLY = /* @__PURE__ */ Symbol("NOREPLY");
539
+
540
+ // src/util.ts
541
+ var NOOP_LOGGER = {
542
+ debug: () => {
543
+ },
544
+ info: () => {
545
+ },
546
+ warn: () => {
547
+ },
548
+ error: () => {
549
+ },
550
+ child: () => NOOP_LOGGER
551
+ };
552
+
553
+ // src/browser/util.ts
554
+ var RPC_ERROR_REGISTRY = /* @__PURE__ */ new Map([
555
+ ["GenericError", RPCGenericError],
556
+ ["RpcFrameworkError", RPCFrameworkError],
557
+ ["MessageTypeNotSupported", RPCMessageTypeNotSupportedError],
558
+ ["NotImplemented", RPCNotImplementedError],
559
+ ["NotSupported", RPCNotSupportedError],
560
+ ["InternalError", RPCInternalError],
561
+ ["ProtocolError", RPCProtocolError],
562
+ ["SecurityError", RPCSecurityError],
563
+ ["FormatViolation", RPCFormatViolationError],
564
+ ["FormationViolation", RPCFormationViolationError],
565
+ ["PropertyConstraintViolation", RPCPropertyConstraintViolationError],
566
+ [
567
+ "OccurrenceConstraintViolation",
568
+ RPCOccurrenceConstraintViolationError
569
+ ],
570
+ ["TypeConstraintViolation", RPCTypeConstraintViolationError]
571
+ ]);
572
+ function createRPCError(code, message, details = {}) {
573
+ const Ctor = RPC_ERROR_REGISTRY.get(code) ?? RPCGenericError;
574
+ return new Ctor(message, details);
575
+ }
576
+ var ERROR_PROPERTIES = [
577
+ "name",
578
+ "message",
579
+ "stack",
580
+ "code",
581
+ "rpcErrorCode",
582
+ "rpcErrorMessage",
583
+ "details"
584
+ ];
585
+ function getErrorPlainObject(err) {
586
+ const result = {};
587
+ for (const prop of ERROR_PROPERTIES) {
588
+ const value = err[prop];
589
+ if (value !== void 0) {
590
+ if (typeof value === "function" || typeof value === "symbol") continue;
591
+ if (typeof value === "object" && value !== null) {
592
+ try {
593
+ JSON.stringify(value);
594
+ result[prop] = value;
595
+ } catch {
596
+ }
597
+ } else {
598
+ result[prop] = value;
599
+ }
600
+ }
601
+ }
602
+ if (!result.name) result.name = err.name;
603
+ if (!result.message) result.message = err.message;
604
+ return result;
605
+ }
606
+
607
+ // src/browser/client.ts
608
+ var { CONNECTING, OPEN, CLOSING, CLOSED } = ConnectionState;
609
+ var BrowserOCPPClient = class _BrowserOCPPClient extends EventEmitter {
610
+ // Static connection states
611
+ static CONNECTING = CONNECTING;
612
+ static OPEN = OPEN;
613
+ static CLOSING = CLOSING;
614
+ static CLOSED = CLOSED;
615
+ _options;
616
+ _state = CLOSED;
617
+ _ws = null;
618
+ _protocol;
619
+ _identity;
620
+ _handlers = /* @__PURE__ */ new Map();
621
+ _wildcardHandler = null;
622
+ _pendingCalls = /* @__PURE__ */ new Map();
623
+ _pendingResponses = /* @__PURE__ */ new Set();
624
+ _callQueue;
625
+ _closePromise = null;
626
+ _reconnectAttempt = 0;
627
+ _reconnectTimer = null;
628
+ _badMessageCount = 0;
629
+ _outboundBuffer = [];
630
+ _logger;
631
+ _middleware;
632
+ constructor(options) {
633
+ super();
634
+ if (!options.identity) {
635
+ throw new Error("identity is required");
636
+ }
637
+ this._identity = options.identity;
638
+ this._options = {
639
+ reconnect: true,
640
+ maxReconnects: Infinity,
641
+ backoffMin: 1e3,
642
+ backoffMax: 3e4,
643
+ callTimeoutMs: 3e4,
644
+ callConcurrency: 1,
645
+ maxBadMessages: Infinity,
646
+ respondWithDetailedErrors: false,
647
+ ...options
648
+ };
649
+ this._callQueue = new Queue(this._options.callConcurrency);
650
+ this._middleware = new MiddlewareStack();
651
+ const loggerInstance = initLogger(this._options.logging, {
652
+ component: "BrowserOCPPClient",
653
+ identity: this._identity
654
+ });
655
+ this._logger = loggerInstance || NOOP_LOGGER;
656
+ }
657
+ // ─── Getters ─────────────────────────────────────────────────
658
+ get log() {
659
+ return this._logger;
660
+ }
661
+ get identity() {
662
+ return this._identity;
663
+ }
664
+ get protocol() {
665
+ return this._protocol;
666
+ }
667
+ get state() {
668
+ return this._state;
669
+ }
670
+ // ─── Connect ─────────────────────────────────────────────────
671
+ async connect() {
672
+ if (this._state !== CLOSED) {
673
+ throw new Error(`Cannot connect: client is in state ${this._state}`);
674
+ }
675
+ this._state = CONNECTING;
676
+ this._reconnectAttempt = 0;
677
+ return this._connectInternal();
678
+ }
679
+ async _connectInternal() {
680
+ return new Promise((resolve, reject) => {
681
+ const endpoint = this._buildEndpoint();
682
+ this._logger.debug?.("Connecting", { url: endpoint });
683
+ this.emit("connecting", { url: endpoint });
684
+ let ws;
685
+ try {
686
+ ws = this._options.protocols?.length ? new WebSocket(endpoint, this._options.protocols) : new WebSocket(endpoint);
687
+ } catch (err) {
688
+ this._state = CLOSED;
689
+ reject(err);
690
+ return;
691
+ }
692
+ this._ws = ws;
693
+ const onOpen = (event) => {
694
+ cleanup();
695
+ this._state = OPEN;
696
+ this._protocol = ws.protocol || void 0;
697
+ this._badMessageCount = 0;
698
+ if (ws.protocol && this._reconnectAttempt === 0) {
699
+ this._options.protocols = [ws.protocol];
700
+ }
701
+ this._attachWebsocket(ws);
702
+ if (this._outboundBuffer.length > 0) {
703
+ const buffer = this._outboundBuffer;
704
+ this._outboundBuffer = [];
705
+ for (const msg of buffer) this._ws?.send(msg);
706
+ }
707
+ this._logger.info?.("Connected", {
708
+ protocol: ws.protocol || void 0
709
+ });
710
+ this.emit("open", event);
711
+ resolve();
712
+ };
713
+ const onError = (event) => {
714
+ cleanup();
715
+ this._state = CLOSED;
716
+ this._logger.error?.("Connection error");
717
+ this.emit("error", event);
718
+ reject(event);
719
+ };
720
+ const onClose = () => {
721
+ cleanup();
722
+ if (this._state === CONNECTING) {
723
+ this._state = CLOSED;
724
+ reject(new Error("WebSocket closed during connection"));
725
+ }
726
+ };
727
+ const cleanup = () => {
728
+ ws.removeEventListener("open", onOpen);
729
+ ws.removeEventListener("error", onError);
730
+ ws.removeEventListener("close", onClose);
731
+ };
732
+ ws.addEventListener("open", onOpen);
733
+ ws.addEventListener("error", onError);
734
+ ws.addEventListener("close", onClose);
735
+ });
736
+ }
737
+ // ─── Close ───────────────────────────────────────────────────
738
+ async close(options = {}) {
739
+ const {
740
+ code = 1e3,
741
+ reason = "",
742
+ awaitPending = true,
743
+ force = false
744
+ } = options;
745
+ if (this._closePromise) return this._closePromise;
746
+ if (this._state === CLOSED) {
747
+ return { code: 1e3, reason: "" };
748
+ }
749
+ if (this._reconnectTimer) {
750
+ clearTimeout(this._reconnectTimer);
751
+ this._reconnectTimer = null;
752
+ }
753
+ this._closePromise = this._closeInternal(code, reason, awaitPending, force);
754
+ return this._closePromise;
755
+ }
756
+ async _closeInternal(code, reason, awaitPending, force) {
757
+ this._state = CLOSING;
758
+ if (!force && awaitPending) {
759
+ const pendingPromises = Array.from(this._pendingCalls.values()).map(
760
+ (p) => new Promise((resolve) => {
761
+ const origResolve = p.resolve;
762
+ const origReject = p.reject;
763
+ p.resolve = (v) => {
764
+ origResolve(v);
765
+ resolve();
766
+ };
767
+ p.reject = (r) => {
768
+ origReject(r);
769
+ resolve();
770
+ };
771
+ })
772
+ );
773
+ if (pendingPromises.length > 0) {
774
+ await Promise.allSettled(pendingPromises);
775
+ }
776
+ }
777
+ return new Promise((resolve) => {
778
+ if (!this._ws || this._ws.readyState === WebSocket.CLOSED) {
779
+ this._state = CLOSED;
780
+ this._cleanup();
781
+ const result = { code, reason };
782
+ this.emit("close", result);
783
+ resolve(result);
784
+ return;
785
+ }
786
+ const onClose = (event) => {
787
+ this._ws?.removeEventListener("close", onClose);
788
+ this._state = CLOSED;
789
+ this._cleanup();
790
+ const result = { code: event.code, reason: event.reason };
791
+ this.emit("close", result);
792
+ resolve(result);
793
+ };
794
+ this._ws.addEventListener("close", onClose);
795
+ if (force) {
796
+ this._ws.close();
797
+ } else {
798
+ const validCode = code >= 1e3 && code <= 4999 && ![1004, 1005, 1006].includes(code);
799
+ this._ws.close(validCode ? code : 1e3, reason);
800
+ }
801
+ });
802
+ }
803
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
804
+ handle(...args) {
805
+ if (args.length === 1 && typeof args[0] === "function") {
806
+ this._wildcardHandler = args[0];
807
+ } else if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "function") {
808
+ this._handlers.set(args[0], args[1]);
809
+ } else if (args.length === 3 && typeof args[0] === "string" && typeof args[1] === "string" && typeof args[2] === "function") {
810
+ this._handlers.set(`${args[0]}:${args[1]}`, args[2]);
811
+ } else {
812
+ throw new Error(
813
+ "Invalid arguments: provide (version, method, handler), (method, handler), or (wildcardHandler)"
814
+ );
815
+ }
816
+ }
817
+ removeHandler(versionOrMethod, method) {
818
+ if (versionOrMethod && method) {
819
+ this._handlers.delete(`${versionOrMethod}:${method}`);
820
+ } else if (versionOrMethod) {
821
+ this._handlers.delete(versionOrMethod);
822
+ } else {
823
+ this._wildcardHandler = null;
824
+ }
825
+ }
826
+ removeAllHandlers() {
827
+ this._handlers.clear();
828
+ this._wildcardHandler = null;
829
+ }
830
+ // ─── Middleware ──────────────────────────────────────────────
831
+ /**
832
+ * Register a middleware function to intercept calls and results.
833
+ * Middleware executes in the order registered.
834
+ */
835
+ use(middleware) {
836
+ this._middleware.use(middleware);
837
+ }
838
+ async call(...args) {
839
+ let method;
840
+ let params;
841
+ let options;
842
+ if (args.length >= 3 && typeof args[0] === "string" && typeof args[1] === "string") {
843
+ method = args[1];
844
+ params = args[2] ?? {};
845
+ options = args[3] ?? {};
846
+ } else {
847
+ method = args[0];
848
+ params = args[1] ?? {};
849
+ options = args[2] ?? {};
850
+ }
851
+ if (this._state !== OPEN) {
852
+ throw new Error(`Cannot call: client is in state ${this._state}`);
853
+ }
854
+ return this._callQueue.push(() => this._sendCall(method, params, options));
855
+ }
856
+ async _sendCall(method, params, options) {
857
+ const msgId = this._generateMessageId();
858
+ const timeoutMs = options.timeoutMs ?? this._options.callTimeoutMs;
859
+ const ctx = {
860
+ type: "outgoing_call",
861
+ messageId: msgId,
862
+ method,
863
+ params,
864
+ options
865
+ };
866
+ let callResult;
867
+ await this._middleware.execute(ctx, async (c) => {
868
+ const ctxvals = c;
869
+ const message = [
870
+ MessageType.CALL,
871
+ msgId,
872
+ ctxvals.method,
873
+ ctxvals.params
874
+ ];
875
+ const messageStr = JSON.stringify(message);
876
+ callResult = await new Promise((resolve, reject) => {
877
+ const timeoutHandle = setTimeout(() => {
878
+ this._pendingCalls.delete(msgId);
879
+ this._logger.warn?.("Call timed out", {
880
+ messageId: msgId,
881
+ method: ctxvals.method,
882
+ timeoutMs
883
+ });
884
+ reject(
885
+ new TimeoutError(
886
+ `Call to "${ctxvals.method}" timed out after ${timeoutMs}ms`
887
+ )
888
+ );
889
+ }, timeoutMs);
890
+ const pending = {
891
+ resolve,
892
+ reject,
893
+ timeoutHandle,
894
+ method: ctxvals.method,
895
+ sentAt: Date.now()
896
+ };
897
+ if (options.signal) {
898
+ if (options.signal.aborted) {
899
+ clearTimeout(timeoutHandle);
900
+ reject(options.signal.reason ?? new Error("Aborted"));
901
+ return;
902
+ }
903
+ const abortHandler = () => {
904
+ clearTimeout(timeoutHandle);
905
+ this._pendingCalls.delete(msgId);
906
+ reject(options.signal?.reason ?? new Error("Aborted"));
907
+ };
908
+ options.signal.addEventListener("abort", abortHandler, {
909
+ once: true
910
+ });
911
+ pending.abortHandler = abortHandler;
912
+ }
913
+ this._pendingCalls.set(msgId, pending);
914
+ this._ws?.send(messageStr);
915
+ this.emit("message", message);
916
+ this.emit("call", message);
917
+ });
918
+ });
919
+ return callResult;
920
+ }
921
+ /**
922
+ * Send a raw string message over the WebSocket (use with caution).
923
+ * Messages sent while CONNECTING are buffered and flushed on open.
924
+ */
925
+ sendRaw(message) {
926
+ if (this._state === OPEN && this._ws) {
927
+ this._ws.send(message);
928
+ } else if (this._state === CONNECTING) {
929
+ this._outboundBuffer.push(message);
930
+ } else {
931
+ throw new Error("Cannot send: client is not connected");
932
+ }
933
+ }
934
+ // ─── Reconfigure ─────────────────────────────────────────────
935
+ reconfigure(options) {
936
+ Object.assign(this._options, options);
937
+ if (options.callConcurrency !== void 0) {
938
+ this._callQueue.setConcurrency(options.callConcurrency);
939
+ }
940
+ }
941
+ // ─── Internal: WebSocket attachment ──────────────────────────
942
+ _attachWebsocket(ws) {
943
+ ws.addEventListener(
944
+ "message",
945
+ (event) => this._onMessage(event.data)
946
+ );
947
+ ws.addEventListener(
948
+ "close",
949
+ (event) => this._onClose(event.code, event.reason)
950
+ );
951
+ ws.addEventListener("error", (event) => this.emit("error", event));
952
+ }
953
+ // ─── Internal: Message handling ──────────────────────────────
954
+ _onMessage(data) {
955
+ const raw = typeof data === "string" ? data : String(data);
956
+ let message;
957
+ try {
958
+ message = JSON.parse(raw);
959
+ if (!Array.isArray(message)) throw new Error("Message is not an array");
960
+ } catch (err) {
961
+ this._onBadMessage(raw, err);
962
+ return;
963
+ }
964
+ const messageType = message[0];
965
+ switch (messageType) {
966
+ case MessageType.CALL:
967
+ this._handleIncomingCall(message);
968
+ break;
969
+ case MessageType.CALLRESULT:
970
+ this._handleCallResult(message);
971
+ break;
972
+ case MessageType.CALLERROR:
973
+ this._handleCallError(message);
974
+ break;
975
+ default:
976
+ this._onBadMessage(
977
+ JSON.stringify(message),
978
+ new RPCMessageTypeNotSupportedError(
979
+ `Unknown message type: ${messageType}`
980
+ )
981
+ );
982
+ }
983
+ }
984
+ async _handleIncomingCall(message) {
985
+ const [, msgId, method, params] = message;
986
+ const ctx = {
987
+ type: "incoming_call",
988
+ messageId: msgId,
989
+ method,
990
+ params,
991
+ protocol: this._protocol
992
+ };
993
+ await this._middleware.execute(ctx, async (c) => {
994
+ const ctxvals = c;
995
+ const modifiedMessage = [
996
+ MessageType.CALL,
997
+ ctxvals.messageId,
998
+ ctxvals.method,
999
+ ctxvals.params
1000
+ ];
1001
+ this.emit("call", modifiedMessage);
1002
+ if (this._state !== OPEN) {
1003
+ return;
1004
+ }
1005
+ try {
1006
+ if (this._pendingResponses.has(ctxvals.messageId)) {
1007
+ throw createRPCError(
1008
+ "RpcFrameworkError",
1009
+ `Already processing call with ID: ${ctxvals.messageId}`
1010
+ );
1011
+ }
1012
+ const specificHandler = (this._protocol ? this._handlers.get(`${this._protocol}:${ctxvals.method}`) : void 0) ?? this._handlers.get(ctxvals.method);
1013
+ if (!specificHandler && !this._wildcardHandler) {
1014
+ throw createRPCError(
1015
+ "NotImplemented",
1016
+ `No handler for method: ${ctxvals.method}`
1017
+ );
1018
+ }
1019
+ this._pendingResponses.add(ctxvals.messageId);
1020
+ const ac = new AbortController();
1021
+ const context = {
1022
+ messageId: ctxvals.messageId,
1023
+ method: ctxvals.method,
1024
+ protocol: this._protocol,
1025
+ params: ctxvals.params,
1026
+ signal: ac.signal
1027
+ };
1028
+ let result;
1029
+ if (specificHandler) {
1030
+ result = await specificHandler(context);
1031
+ } else if (this._wildcardHandler) {
1032
+ result = await this._wildcardHandler(ctxvals.method, context);
1033
+ }
1034
+ this._pendingResponses.delete(ctxvals.messageId);
1035
+ if (result === NOREPLY) return;
1036
+ const response = [
1037
+ MessageType.CALLRESULT,
1038
+ ctxvals.messageId,
1039
+ result
1040
+ ];
1041
+ this._ws?.send(JSON.stringify(response));
1042
+ this.emit("callResult", response);
1043
+ } catch (err) {
1044
+ this._pendingResponses.delete(ctxvals.messageId);
1045
+ this._logger.error?.("Handler error", {
1046
+ messageId: ctxvals.messageId,
1047
+ method: ctxvals.method,
1048
+ error: err.message
1049
+ });
1050
+ const rpcErr = err instanceof RPCGenericError || err.rpcErrorCode ? err : createRPCError("InternalError", err.message);
1051
+ const details = this._options.respondWithDetailedErrors ? getErrorPlainObject(err) : {};
1052
+ const errorResponse = [
1053
+ MessageType.CALLERROR,
1054
+ ctxvals.messageId,
1055
+ rpcErr.rpcErrorCode,
1056
+ rpcErr.rpcErrorMessage || err.message || "",
1057
+ details
1058
+ ];
1059
+ this._ws?.send(JSON.stringify(errorResponse));
1060
+ this.emit("callError", errorResponse);
1061
+ }
1062
+ });
1063
+ }
1064
+ async _handleCallResult(message) {
1065
+ const [, msgId, result] = message;
1066
+ if (!this._pendingCalls.has(msgId)) {
1067
+ this._logger.warn?.("Received CallResult for unknown messageId", {
1068
+ messageId: msgId
1069
+ });
1070
+ return;
1071
+ }
1072
+ const pending = this._pendingCalls.get(msgId);
1073
+ const ctx = {
1074
+ type: "incoming_result",
1075
+ messageId: msgId,
1076
+ method: pending.method,
1077
+ payload: result
1078
+ };
1079
+ await this._middleware.execute(ctx, async (c) => {
1080
+ const ctxvals = c;
1081
+ const modifiedMessage = [
1082
+ MessageType.CALLRESULT,
1083
+ msgId,
1084
+ ctxvals.payload
1085
+ ];
1086
+ this.emit("callResult", modifiedMessage);
1087
+ clearTimeout(pending.timeoutHandle);
1088
+ this._pendingCalls.delete(msgId);
1089
+ pending.resolve(ctxvals.payload);
1090
+ });
1091
+ }
1092
+ async _handleCallError(message) {
1093
+ const [, msgId, errorCode, errorMessage, errorDetails] = message;
1094
+ if (!this._pendingCalls.has(msgId)) {
1095
+ this._logger.warn?.("Received CallError for unknown messageId", {
1096
+ messageId: msgId
1097
+ });
1098
+ return;
1099
+ }
1100
+ const pending = this._pendingCalls.get(msgId);
1101
+ const error = createRPCError(errorCode, errorMessage, errorDetails);
1102
+ const ctx = {
1103
+ type: "incoming_error",
1104
+ messageId: msgId,
1105
+ method: pending.method,
1106
+ error
1107
+ // Map to types.ts expected `error` shape which takes OCPPCallError specifically here, though we pass it via RPCError
1108
+ };
1109
+ await this._middleware.execute(ctx, async (c) => {
1110
+ const ctxvals = c;
1111
+ const resolvedRpcErr = ctxvals.error;
1112
+ const modifiedMessage = [
1113
+ MessageType.CALLERROR,
1114
+ msgId,
1115
+ resolvedRpcErr.rpcErrorCode,
1116
+ resolvedRpcErr.message,
1117
+ resolvedRpcErr.rpcErrorDetails ?? {}
1118
+ ];
1119
+ this.emit("callError", modifiedMessage);
1120
+ clearTimeout(pending.timeoutHandle);
1121
+ this._pendingCalls.delete(msgId);
1122
+ pending.reject(resolvedRpcErr);
1123
+ });
1124
+ }
1125
+ // ─── Internal: Bad message handling ──────────────────────────
1126
+ _onBadMessage(rawMessage, error) {
1127
+ this._badMessageCount++;
1128
+ this._logger?.warn?.("Bad message", {
1129
+ error: error.message,
1130
+ count: this._badMessageCount
1131
+ });
1132
+ this.emit("badMessage", { message: rawMessage, error });
1133
+ const match = rawMessage.match(/^\s*\[\s*2\s*,\s*"([^"]+)"/);
1134
+ if (match?.[1] && this._ws) {
1135
+ const errorResponse = [
1136
+ MessageType.CALLERROR,
1137
+ match[1],
1138
+ "FormatViolation",
1139
+ error.message || "Invalid message format",
1140
+ {}
1141
+ ];
1142
+ this._ws.send(JSON.stringify(errorResponse));
1143
+ this.emit("callError", errorResponse);
1144
+ }
1145
+ if (this._badMessageCount >= this._options.maxBadMessages) {
1146
+ this.close({ code: 1002, reason: "Too many bad messages" }).catch(
1147
+ () => {
1148
+ }
1149
+ );
1150
+ }
1151
+ }
1152
+ // ─── Internal: Close handling ────────────────────────────────
1153
+ /**
1154
+ * Reject all in-flight calls and clear pending state.
1155
+ */
1156
+ _rejectPendingCalls(reason) {
1157
+ for (const [, pending] of this._pendingCalls) {
1158
+ clearTimeout(pending.timeoutHandle);
1159
+ pending.reject(new Error(reason));
1160
+ }
1161
+ this._pendingCalls.clear();
1162
+ this._pendingResponses.clear();
1163
+ }
1164
+ _onClose(code, reason) {
1165
+ this._rejectPendingCalls(`Connection closed (${code}: ${reason})`);
1166
+ if (this._state !== CLOSING) {
1167
+ this._logger?.info?.("Disconnected", { code, reason });
1168
+ this.emit("disconnect", { code, reason });
1169
+ if (this._options.reconnect && this._reconnectAttempt < this._options.maxReconnects) {
1170
+ this._scheduleReconnect();
1171
+ } else {
1172
+ this._state = CLOSED;
1173
+ this.emit("close", { code, reason });
1174
+ }
1175
+ } else {
1176
+ this._state = CLOSED;
1177
+ }
1178
+ }
1179
+ // ─── Internal: Reconnection ──────────────────────────────────
1180
+ /** Errors that should stop reconnection immediately */
1181
+ static _INTOLERABLE_ERRORS = /* @__PURE__ */ new Set([
1182
+ "Maximum redirects exceeded",
1183
+ "Server sent no subprotocol",
1184
+ "Server sent an invalid subprotocol",
1185
+ "Server sent a subprotocol but none was requested",
1186
+ "Invalid Sec-WebSocket-Accept header"
1187
+ ]);
1188
+ _scheduleReconnect() {
1189
+ this._reconnectAttempt++;
1190
+ this._state = CONNECTING;
1191
+ const base = this._options.backoffMin;
1192
+ const max = this._options.backoffMax;
1193
+ const delayMs = Math.min(
1194
+ max,
1195
+ base * 2 ** (this._reconnectAttempt - 1) * (0.5 + Math.random() * 0.5)
1196
+ );
1197
+ this._logger?.warn?.("Reconnecting", {
1198
+ attempt: this._reconnectAttempt,
1199
+ delayMs: Math.round(delayMs)
1200
+ });
1201
+ this.emit("reconnect", { attempt: this._reconnectAttempt, delay: delayMs });
1202
+ this._reconnectTimer = setTimeout(async () => {
1203
+ this._reconnectTimer = null;
1204
+ try {
1205
+ await this._connectInternal();
1206
+ } catch (err) {
1207
+ const msg = err instanceof Error ? err.message : "";
1208
+ if (_BrowserOCPPClient._INTOLERABLE_ERRORS.has(msg)) {
1209
+ this._logger?.error?.("Intolerable error \u2014 stopping reconnection", {
1210
+ error: msg
1211
+ });
1212
+ this._state = CLOSED;
1213
+ this.emit("close", { code: 1001, reason: msg });
1214
+ return;
1215
+ }
1216
+ if (this._reconnectAttempt < this._options.maxReconnects && this._options.reconnect) {
1217
+ this._scheduleReconnect();
1218
+ } else {
1219
+ this._state = CLOSED;
1220
+ this.emit("close", {
1221
+ code: 1001,
1222
+ reason: "Max reconnection attempts exhausted"
1223
+ });
1224
+ }
1225
+ }
1226
+ }, delayMs);
1227
+ }
1228
+ // ─── Internal: Endpoint building ─────────────────────────────
1229
+ _buildEndpoint() {
1230
+ let url = this._options.endpoint;
1231
+ if (!url.endsWith("/")) url += "/";
1232
+ url += encodeURIComponent(this._identity);
1233
+ if (this._options.query) {
1234
+ const params = new URLSearchParams(this._options.query);
1235
+ url += (url.includes("?") ? "&" : "?") + params.toString();
1236
+ }
1237
+ return url;
1238
+ }
1239
+ // ─── Internal: Cleanup ───────────────────────────────────────
1240
+ _cleanup() {
1241
+ this._closePromise = null;
1242
+ this._ws = null;
1243
+ }
1244
+ // ─── Internal: ID Generation ─────────────────────────────────
1245
+ _generateMessageId() {
1246
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1247
+ return crypto.randomUUID();
1248
+ }
1249
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1250
+ const r = Math.random() * 16 | 0;
1251
+ const v = c === "x" ? r : r & 3 | 8;
1252
+ return v.toString(16);
1253
+ });
1254
+ }
1255
+ };
1256
+ export {
1257
+ BrowserOCPPClient,
1258
+ ConnectionState,
1259
+ MessageType,
1260
+ MiddlewareStack,
1261
+ NOREPLY,
1262
+ RPCFormatViolationError,
1263
+ RPCFormationViolationError,
1264
+ RPCFrameworkError,
1265
+ RPCGenericError,
1266
+ RPCInternalError,
1267
+ RPCMessageTypeNotSupportedError,
1268
+ RPCNotImplementedError,
1269
+ RPCNotSupportedError,
1270
+ RPCOccurrenceConstraintViolationError,
1271
+ RPCPropertyConstraintViolationError,
1272
+ RPCProtocolError,
1273
+ RPCSecurityError,
1274
+ RPCTypeConstraintViolationError,
1275
+ TimeoutError,
1276
+ combineAuth,
1277
+ createLoggingMiddleware,
1278
+ createRPCError,
1279
+ defineAuth,
1280
+ defineMiddleware,
1281
+ defineRpcMiddleware,
1282
+ getErrorPlainObject
1283
+ };
1284
+ //# sourceMappingURL=browser.mjs.map