ocpp-ws-io 2.2.2 → 2.3.0

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.
@@ -4465,6 +4465,11 @@ declare class OCPPRouter extends OCPPRouter_base {
4465
4465
  * @internal
4466
4466
  */
4467
4467
  _regexPatterns: CompiledRegexPattern[];
4468
+ /**
4469
+ * @internal Set by OCPPServer when the router is registered, so patterns
4470
+ * added after registration still reach the trie / regex list.
4471
+ */
4472
+ _onPatternAdded?: (pattern: string | RegExp) => void;
4468
4473
  constructor(patterns?: Array<string | RegExp>, middlewares?: ConnectionMiddleware[]);
4469
4474
  /**
4470
4475
  * Appends URL paths or regular expressions to this router's match condition.
@@ -4573,7 +4578,7 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4573
4578
  private _pendingResponses;
4574
4579
  private _callQueue;
4575
4580
  private _pingTimer;
4576
- private _pongTimer;
4581
+ protected _pongTimer: ReturnType<typeof setTimeout> | null;
4577
4582
  private _closePromise;
4578
4583
  private _reconnectAttempt;
4579
4584
  private _reconnectTimer;
@@ -4608,6 +4613,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4608
4613
  * The connection endpoint URL.
4609
4614
  */
4610
4615
  get endpoint(): string;
4616
+ /**
4617
+ * Bytes currently queued in the underlying WebSocket send buffer
4618
+ * (0 when disconnected). Useful for backpressure monitoring and drain checks.
4619
+ */
4620
+ get bufferedAmount(): number;
4611
4621
  /**
4612
4622
  * The current configuration options for this client.
4613
4623
  */
@@ -4690,6 +4700,11 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4690
4700
  * Remove a registered handler for a specific version and method.
4691
4701
  */
4692
4702
  removeHandler(version: OCPPProtocol, method: string): void;
4703
+ /**
4704
+ * Check whether a handler is registered for a method
4705
+ * (optionally version-scoped, matching the handle() overloads).
4706
+ */
4707
+ hasHandler(method: string, version?: string): boolean;
4693
4708
  /**
4694
4709
  * Remove all registered handlers for this client, including the wildcard handler.
4695
4710
  */
@@ -4715,6 +4730,12 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4715
4730
  call<M extends AllMethodNames<P>>(method: M, params: OCPPRequestType<P, M>, options?: CallOptions): Promise<OCPPResponseType<P, M>>;
4716
4731
  /** Call a known typed method with explicit response type. */
4717
4732
  call<TResult = unknown>(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<TResult>;
4733
+ /**
4734
+ * Execute a call immediately, bypassing the callConcurrency queue.
4735
+ * Used by OCPPServer.sendBatch to pipeline warm-up calls without
4736
+ * mutating the client's configured concurrency (report M9).
4737
+ */
4738
+ callImmediate<TResult = unknown>(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<TResult>;
4718
4739
  /**
4719
4740
  * Version-specific safe call. Returns `undefined` on error instead of throwing.
4720
4741
  */
@@ -4754,7 +4775,7 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4754
4775
  * Reject all in-flight calls and clear pending state.
4755
4776
  */
4756
4777
  private _rejectPendingCalls;
4757
- private _onClose;
4778
+ protected _onClose(code: number, reason: Buffer): void;
4758
4779
  /** Errors that should stop reconnection immediately */
4759
4780
  private static readonly _INTOLERABLE_ERRORS;
4760
4781
  private _scheduleReconnect;
@@ -4772,20 +4793,27 @@ declare class OCPPClient<P extends OCPPProtocol = OCPPProtocol> extends OCPPClie
4772
4793
  private _callWithRetry;
4773
4794
  /** Maximum bytes allowed in the ws send buffer before applying backpressure (512KB) */
4774
4795
  private static readonly _BACKPRESSURE_THRESHOLD;
4796
+ /** Sends queued while the socket is backpressured (FIFO). */
4797
+ private _backpressureQueue;
4798
+ private _backpressureTimer;
4775
4799
  /**
4776
4800
  * Protected hook for plugins to intercept outbound messages before serialization.
4777
4801
  * Return `false` to suppress the message transmission.
4778
4802
  */
4779
4803
  protected _invokeBeforeSend(_message: OCPPMessage): boolean | Promise<boolean>;
4780
4804
  /**
4781
- * Wraps ws.send() with backpressure protection.
4782
- * If bufferedAmount exceeds the threshold, waits for the buffer to drain
4783
- * before sending. Prevents OOM on slow 2G/3G charger connections.
4805
+ * Wraps ws.send() with backpressure protection. When bufferedAmount
4806
+ * exceeds the threshold, sends are queued and flushed FIFO by a single
4807
+ * shared 50ms drain timer (one per client, not one per send — report M10).
4808
+ * Entries older than 10s are sent regardless, preserving the previous
4809
+ * timeout semantics. Prevents OOM on slow 2G/3G charger connections.
4784
4810
  */
4785
4811
  private _safeSend;
4786
- private _startPing;
4812
+ private _startBackpressureDrain;
4813
+ private _stopBackpressureDrain;
4814
+ protected _startPing(): void;
4787
4815
  private _stopPing;
4788
- private _recordActivity;
4816
+ protected _recordActivity(): void;
4789
4817
  private _setupValidators;
4790
4818
  private _validateOutbound;
4791
4819
  private _validateInbound;
@@ -4800,6 +4828,8 @@ interface WorkerPoolOptions {
4800
4828
  poolSize?: number;
4801
4829
  /** Max pending parse jobs before rejecting (default: 10000) */
4802
4830
  maxQueueSize?: number;
4831
+ /** Override the worker entry path (used by tests; defaults to parse-worker.cjs next to this file) */
4832
+ workerPath?: string;
4803
4833
  }
4804
4834
  interface ParseResult {
4805
4835
  message: unknown;
@@ -4815,7 +4845,16 @@ declare class WorkerPool {
4815
4845
  private _pending;
4816
4846
  private _maxQueueSize;
4817
4847
  private _terminated;
4848
+ private readonly _workerPath;
4818
4849
  constructor(options?: WorkerPoolOptions);
4850
+ /**
4851
+ * Create (or recreate) a worker bound to a fixed pool index, wiring up
4852
+ * message/error/exit handlers. On a crash the worker is respawned at the
4853
+ * same index and any tasks it owned are rejected so callers never hang.
4854
+ */
4855
+ private _createWorker;
4856
+ /** Reject every pending task that was dispatched to the given worker index. */
4857
+ private _failWorkerTasks;
4819
4858
  /** Number of worker threads in the pool */
4820
4859
  get size(): number;
4821
4860
  /** Number of pending (unresolved) parse tasks */
@@ -4858,9 +4897,17 @@ declare class OCPPServerClient extends OCPPClient {
4858
4897
  private _rateLimits;
4859
4898
  private _adaptiveMultiplier;
4860
4899
  private _workerPool;
4900
+ /**
4901
+ * Per-connection inbound pipeline. Serializes async pre-processing
4902
+ * (plugin onBeforeReceive, rate-limit parse, worker-pool parse) so
4903
+ * messages are dispatched in wire order — OCPP transaction semantics
4904
+ * depend on it (e.g. StartTransaction before StopTransaction).
4905
+ */
4906
+ private _inboundChain;
4861
4907
  private _checkRateLimit;
4862
4908
  protected _invokeBeforeSend(message: OCPPMessage): boolean | Promise<boolean>;
4863
4909
  private _attachServerWebsocket;
4910
+ private _processInboundMessage;
4864
4911
  private _handleRateLimitExceeded;
4865
4912
  /**
4866
4913
  * Session data associated with this client connection.
@@ -4909,6 +4956,10 @@ declare class OCPPServer extends OCPPServer_base {
4909
4956
  private _clients;
4910
4957
  private _clientsByIdentity;
4911
4958
  private _httpServers;
4959
+ /** HTTP servers created by listen() — we own their lifecycle. */
4960
+ private _ownedHttpServers;
4961
+ /** Listeners we attached per server, for removal on close(). */
4962
+ private _attachedHttpHandlers;
4912
4963
  private _wss;
4913
4964
  private _state;
4914
4965
  private _adapter;
@@ -4920,7 +4971,12 @@ declare class OCPPServer extends OCPPServer_base {
4920
4971
  private _plugins;
4921
4972
  private _workerPool;
4922
4973
  private _telemetryInterval;
4974
+ private _presenceInterval;
4923
4975
  private readonly _nodeId;
4976
+ /** Pending cross-node RPC calls awaiting a correlated response. */
4977
+ private _pendingRemoteCalls;
4978
+ /** Extra wait on top of the call timeout to absorb cross-node transit. */
4979
+ private static readonly _REMOTE_RESPONSE_GRACE_MS;
4924
4980
  private _sessions;
4925
4981
  private _gcInterval;
4926
4982
  private readonly _sessionTimeoutMs;
@@ -5064,21 +5120,25 @@ declare class OCPPServer extends OCPPServer_base {
5064
5120
  */
5065
5121
  private _handleUpgrade;
5066
5122
  private _updateSessionActivity;
5123
+ /**
5124
+ * Evict per-IP connection buckets that have been idle longer than the
5125
+ * rate-limit window. Recreating a bucket grants a full token allowance,
5126
+ * which is exactly what a full refill after `windowMs` idle would yield —
5127
+ * so eviction is behavior-preserving while bounding memory (report H4).
5128
+ */
5129
+ private _sweepConnectionBuckets;
5067
5130
  close(options?: CloseOptions): Promise<void>;
5068
5131
  reconfigure(options: Partial<ServerOptions>): void;
5069
5132
  /**
5070
5133
  * Send a request to a specific client (local or remote).
5071
5134
  *
5072
- * 1. Checks local clients.
5073
- * 2. Checks Presence Registry -> Unicast.
5074
- * 3. Fallback: Broadcast.
5075
- */
5076
- /**
5077
- * Send a request to a specific client (local or remote).
5135
+ * 1. Local clients are called directly.
5136
+ * 2. Otherwise the presence registry routes the call to the owning node,
5137
+ * and the response is correlated back over the adapter (cross-node RPC).
5138
+ * 3. Unknown identity → rejects with "Client <identity> not found".
5078
5139
  *
5079
- * 1. Checks local clients.
5080
- * 2. Checks Presence Registry -> Unicast.
5081
- * 3. Fallback: Error (Client not found).
5140
+ * Backward compatibility: nodes running older versions deliver the call
5141
+ * but never publish a response — such calls reject with TimeoutError.
5082
5142
  */
5083
5143
  sendToClient<V extends OCPPProtocol, M extends AllMethodNames<V>>(identity: string, version: V, method: M, params: OCPPRequestType<V, M>, options?: CallOptions): Promise<OCPPResponseType<V, M> | undefined>;
5084
5144
  sendToClient<M extends AllMethodNames<any>>(identity: string, method: M, params: OCPPRequestType<any, M>, options?: CallOptions): Promise<OCPPResponseType<any, M> | undefined>;
@@ -5111,8 +5171,12 @@ declare class OCPPServer extends OCPPServer_base {
5111
5171
  options?: CallOptions;
5112
5172
  }>): Promise<Array<unknown | undefined>>;
5113
5173
  setAdapter(adapter: EventAdapterInterface): Promise<void>;
5174
+ private _startPresenceRefresh;
5175
+ private _refreshPresence;
5114
5176
  private _onBroadcast;
5115
5177
  private _onUnicast;
5178
+ /** Publish the result of a remotely requested call back to the origin node. */
5179
+ private _publishRemoteResult;
5116
5180
  publish(channel: string, data: unknown): Promise<void>;
5117
5181
  broadcast<V extends AllMethodNames<any>>(method: V, params: OCPPRequestType<any, V>): Promise<void>;
5118
5182
  /**
@@ -5126,6 +5190,13 @@ declare class OCPPServer extends OCPPServer_base {
5126
5190
  * @param options Call options
5127
5191
  */
5128
5192
  broadcastBatch<V extends AllMethodNames<any>>(identities: string[], method: V, params: OCPPRequestType<any, V>, options?: CallOptions): Promise<void>;
5193
+ /**
5194
+ * Builds a `noServer` WebSocketServer with the configured transport-layer
5195
+ * settings (max payload + per-message compression). Centralized so every
5196
+ * (re)creation site — constructor, upgrade re-init, and close/restart —
5197
+ * stays consistent.
5198
+ */
5199
+ private _createWss;
5129
5200
  private _buildCompressionConfig;
5130
5201
  }
5131
5202
 
@@ -5163,9 +5234,11 @@ declare class Validator {
5163
5234
  hasSchema(schemaId: string): boolean;
5164
5235
  }
5165
5236
  /**
5166
- * Create or retrieve a cached validator for a specific subprotocol version.
5167
- * E5: Returns an existing instance if one was already created for this subprotocol,
5168
- * preventing duplicate AJV instances and saving memory.
5237
+ * Create a validator for a specific subprotocol version.
5238
+ *
5239
+ * Always returns a fresh instance so custom schema sets are never shadowed
5240
+ * by previously created validators for the same subprotocol (report M8).
5241
+ * Standard validators are cached separately by getStandardValidators().
5169
5242
  */
5170
5243
  declare function createValidator(subprotocol: string, schemas: ValidatorSchema[]): Validator;
5171
5244
 
@@ -5442,7 +5515,11 @@ interface ClientOptions {
5442
5515
  strictModeMethods?: Array<AllMethodNames<OCPPProtocol>>;
5443
5516
  /** Custom validators for strict mode */
5444
5517
  strictModeValidators?: Validator[];
5445
- /** Max number of bad messages before closing (default: Infinity) */
5518
+ /**
5519
+ * Max number of bad messages before closing.
5520
+ * Default: Infinity (never disconnects on bad messages) — set a finite
5521
+ * value in production.
5522
+ */
5446
5523
  maxBadMessages?: number;
5447
5524
  /** Include error details in responses (default: false) */
5448
5525
  respondWithDetailedErrors?: boolean;
@@ -5548,12 +5625,30 @@ interface RouterConfig {
5548
5625
  rateLimit?: RateLimitOptions;
5549
5626
  }
5550
5627
  interface CORSOptions {
5551
- /** Allowed IPv4, IPv6, or CIDR ranges (e.g. "10.0.0.0/8") */
5628
+ /**
5629
+ * Allowed exact IPv4/IPv6 addresses or CIDR ranges
5630
+ * (e.g. "10.0.0.0/8", "2001:db8::/32").
5631
+ */
5552
5632
  allowedIPs?: string[];
5553
- /** Allowed Origin header values (e.g. "https://dashboard.example.com") */
5633
+ /**
5634
+ * Allowed `Origin` header values (e.g. "https://dashboard.example.com").
5635
+ *
5636
+ * **Note:** this is a *browser-side* defense only. Requests with **no**
5637
+ * `Origin` header pass through (physical charging stations don't send one),
5638
+ * so a non-browser client can bypass this by simply omitting the header.
5639
+ * Do not rely on `allowedOrigins` for authentication — use `auth()` / Basic
5640
+ * Auth / mTLS for that.
5641
+ */
5554
5642
  allowedOrigins?: string[];
5555
5643
  /** Allowed WebSocket protocol schemes */
5556
5644
  allowedSchemes?: ("ws" | "wss")[];
5645
+ /**
5646
+ * Honor `X-Forwarded-Proto` from a reverse proxy when evaluating
5647
+ * `allowedSchemes`. Leave false (default) unless the server is only
5648
+ * reachable through a trusted proxy — otherwise clients can spoof the
5649
+ * header to bypass wss-only rules.
5650
+ */
5651
+ trustProxy?: boolean;
5557
5652
  }
5558
5653
  interface ServerOptionsBase {
5559
5654
  /** OCPP Security Profile (default: NONE) */
@@ -5574,7 +5669,11 @@ interface ServerOptionsBase {
5574
5669
  strictModeValidators?: Validator[];
5575
5670
  /** Rate Limiting configuration — inherited */
5576
5671
  rateLimit?: RateLimitOptions;
5577
- /** Max bad messages — inherited (default: Infinity) */
5672
+ /**
5673
+ * Max bad messages — inherited.
5674
+ * Default: Infinity (never disconnects on bad messages) — set a finite
5675
+ * value in production.
5676
+ */
5578
5677
  maxBadMessages?: number;
5579
5678
  /** Include error details in responses — inherited (default: false) */
5580
5679
  respondWithDetailedErrors?: boolean;
@@ -5583,6 +5682,12 @@ interface ServerOptionsBase {
5583
5682
  * (default: 7200000 / 2 hours)
5584
5683
  */
5585
5684
  sessionTtlMs?: number;
5685
+ /**
5686
+ * TTL (seconds) for cluster presence registry entries, and the basis for
5687
+ * the automatic presence heartbeat (refreshed every ttl/2 while clients
5688
+ * are connected). Default: 300.
5689
+ */
5690
+ presenceTtlSeconds?: number;
5586
5691
  /**
5587
5692
  * Maximum time (ms) to wait for the auth callback to resolve during
5588
5693
  * a WebSocket upgrade handshake. If the callback does not settle within
@@ -5613,10 +5718,20 @@ interface ServerOptionsBase {
5613
5718
  * (default: 50000)
5614
5719
  */
5615
5720
  maxSessions?: number;
5721
+ /**
5722
+ * Hard cap on concurrent client connections, enforced before the TLS/auth
5723
+ * handshake work is done (the connection-guard plugin only closes after
5724
+ * the fact). Excess upgrades are rejected with HTTP 503.
5725
+ */
5726
+ maxConnections?: number;
5616
5727
  /**
5617
5728
  * Enable the built-in HTTP health/metrics endpoint.
5618
5729
  * When enabled, non-upgrade HTTP requests to `/health` return a JSON health check,
5619
5730
  * and requests to `/metrics` return Prometheus-compatible text metrics.
5731
+ * When attaching to a user-provided server (listen(..., { server })),
5732
+ * only /health and /metrics are handled; all other routes are left to
5733
+ * the application, and close() will not close the external server.
5734
+ * Ensure your app does not also write responses for /health or /metrics.
5620
5735
  * (default: false)
5621
5736
  */
5622
5737
  healthEndpoint?: boolean;
@@ -5791,7 +5906,7 @@ interface ClientEvents {
5791
5906
  */
5792
5907
  interface SecurityEvent {
5793
5908
  /** Event type identifier */
5794
- type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "INVALID_PAYLOAD" | "ANOMALY_RAPID_RECONNECT" | "ANOMALY_AUTH_BRUTE_FORCE" | "ANOMALY_MESSAGE_FUZZING" | "ANOMALY_IDENTITY_COLLISION";
5909
+ type: "AUTH_FAILED" | "RATE_LIMIT_EXCEEDED" | "UPGRADE_ABORTED" | "CONNECTION_RATE_LIMIT" | "CONNECTION_LIMIT" | "INVALID_PAYLOAD" | "ANOMALY_RAPID_RECONNECT" | "ANOMALY_AUTH_BRUTE_FORCE" | "ANOMALY_MESSAGE_FUZZING" | "ANOMALY_IDENTITY_COLLISION";
5795
5910
  /** Station identity (if known) */
5796
5911
  identity?: string;
5797
5912
  /** Remote IP address */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocpp-ws-io",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "OCPP RPC WebSocket client and server for OCPP 1.6J, 2.0.1, and 2.1. Type-safe TypeScript toolkit for EV charging, CSMS backends, Redis scaling, and protocol validation.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -171,6 +171,6 @@
171
171
  "reflect-metadata": "^0.2.2",
172
172
  "tsup": "^8.5.1",
173
173
  "typescript": "^5.9.3",
174
- "vitest": "^3.2.4"
174
+ "vitest": "^4.1.8"
175
175
  }
176
176
  }