@pellux/goodvibes-transport-realtime 0.33.36 → 0.33.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/runtime-events.d.ts +97 -0
- package/dist/runtime-events.d.ts.map +1 -1
- package/dist/runtime-events.js +209 -12
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export type { DomainEventConnector, DomainEvents, RemoteDomainEventsOptions, Ser
|
|
|
2
2
|
export { createRemoteDomainEvents, forSession } from './domain-events.js';
|
|
3
3
|
export type { RemoteRuntimeEvents, RemoteRuntimeEventsOptions, SerializedRuntimeEnvelope } from './runtime-events.js';
|
|
4
4
|
export { buildEventSourceUrl, buildWebSocketUrl, createEventSourceConnector, createRemoteRuntimeEvents, createWebSocketConnector, DEFAULT_WS_MAX_ATTEMPTS, forSessionRuntime, WebSocketTransportError, } from './runtime-events.js';
|
|
5
|
-
export type { AuthTokenSource, RuntimeEventConnectorOptions } from './runtime-events.js';
|
|
5
|
+
export type { AuthTokenSource, BackpressureInfo, ConnectionState, ConnectorTransportEvent, ReconnectAttemptInfo, RuntimeEventConnectorOptions, } from './runtime-events.js';
|
|
6
|
+
export { createWebSocketRemoteError } from './runtime-events.js';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,YAAY,EACZ,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,wBAAwB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AACtH,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,YAAY,EACZ,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,wBAAwB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC1E,YAAY,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AACtH,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { createRemoteDomainEvents, forSession } from './domain-events.js';
|
|
2
2
|
export { buildEventSourceUrl, buildWebSocketUrl, createEventSourceConnector, createRemoteRuntimeEvents, createWebSocketConnector, DEFAULT_WS_MAX_ATTEMPTS, forSessionRuntime, WebSocketTransportError, } from './runtime-events.js';
|
|
3
|
+
export { createWebSocketRemoteError } from './runtime-events.js';
|
package/dist/runtime-events.d.ts
CHANGED
|
@@ -3,6 +3,34 @@ import { GoodVibesSdkError } from '@pellux/goodvibes-errors';
|
|
|
3
3
|
import { type AuthTokenResolver, type StreamReconnectPolicy } from '@pellux/goodvibes-transport-http';
|
|
4
4
|
import { type TransportObserver } from '@pellux/goodvibes-transport-core';
|
|
5
5
|
import { type DomainEventConnector, type DomainEvents, type SerializedEventEnvelope } from './domain-events.js';
|
|
6
|
+
/**
|
|
7
|
+
* Typed transport observability events emitted by the WebSocket connector.
|
|
8
|
+
*
|
|
9
|
+
* A structural subset of the SDK-level `TransportEvent` union — the SDK
|
|
10
|
+
* (`@pellux/goodvibes-sdk`) extends this with additional server-side event
|
|
11
|
+
* types. Client code that holds a reference to the full `TransportEvent` union
|
|
12
|
+
* can use this type without narrowing since the shapes are structurally
|
|
13
|
+
* compatible.
|
|
14
|
+
*/
|
|
15
|
+
export type ConnectorTransportEvent = {
|
|
16
|
+
type: 'TRANSPORT_CONNECTION_STATE';
|
|
17
|
+
transportId: string;
|
|
18
|
+
state: 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed';
|
|
19
|
+
} | {
|
|
20
|
+
type: 'TRANSPORT_RECONNECT_ATTEMPT';
|
|
21
|
+
transportId: string;
|
|
22
|
+
attempt: number;
|
|
23
|
+
maxAttempts: number;
|
|
24
|
+
delayMs: number;
|
|
25
|
+
reason: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'TRANSPORT_BACKPRESSURE';
|
|
28
|
+
transportId: string;
|
|
29
|
+
droppedCount: number;
|
|
30
|
+
queueLength: number;
|
|
31
|
+
queueBytes: number;
|
|
32
|
+
reason: 'message_too_large' | 'queue_full';
|
|
33
|
+
};
|
|
6
34
|
export type SerializedRuntimeEnvelope<TEvent extends RuntimeEventRecord = RuntimeEventRecord> = SerializedEventEnvelope<TEvent>;
|
|
7
35
|
export type RemoteRuntimeEvents<TEvent extends RuntimeEventRecord = RuntimeEventRecord> = DomainEvents<RuntimeEventDomain, TEvent>;
|
|
8
36
|
export interface RemoteRuntimeEventsOptions {
|
|
@@ -36,11 +64,66 @@ export interface RemoteRuntimeEventsOptions {
|
|
|
36
64
|
* });
|
|
37
65
|
*/
|
|
38
66
|
export { forSession as forSessionRuntime } from './domain-events.js';
|
|
67
|
+
/**
|
|
68
|
+
* Connection state for a realtime transport.
|
|
69
|
+
*
|
|
70
|
+
* - `connecting` — the socket is being established (or re-connecting after a clean stop).
|
|
71
|
+
* - `connected` — the connection is authenticated and open; outbound messages may be sent.
|
|
72
|
+
* - `reconnecting` — the connection was lost and the connector is waiting before the next attempt.
|
|
73
|
+
* - `disconnected` — the connector was stopped cleanly (no further reconnects will occur).
|
|
74
|
+
* - `failed` — the maximum reconnect attempts were exhausted; the connection is permanently closed.
|
|
75
|
+
*/
|
|
76
|
+
export type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed';
|
|
77
|
+
/** Metadata emitted on every reconnect attempt. */
|
|
78
|
+
export interface ReconnectAttemptInfo {
|
|
79
|
+
/** 1-based reconnect attempt index. */
|
|
80
|
+
readonly attempt: number;
|
|
81
|
+
/** Maximum attempts configured; `Infinity` means unlimited. */
|
|
82
|
+
readonly maxAttempts: number;
|
|
83
|
+
/** Milliseconds the connector will wait before the attempt. */
|
|
84
|
+
readonly delayMs: number;
|
|
85
|
+
/** Human-readable reason for the reconnect (e.g. the error message or WS close code). */
|
|
86
|
+
readonly reason: string;
|
|
87
|
+
}
|
|
88
|
+
/** Metadata emitted when the outbound queue saturates. */
|
|
89
|
+
export interface BackpressureInfo {
|
|
90
|
+
/** Number of messages dropped since the last successful flush. */
|
|
91
|
+
readonly droppedCount: number;
|
|
92
|
+
/** Current number of messages in the outbound queue. */
|
|
93
|
+
readonly queueLength: number;
|
|
94
|
+
/** Current byte footprint of the outbound queue. */
|
|
95
|
+
readonly queueBytes: number;
|
|
96
|
+
/** The overflow reason: 'message_too_large' or 'queue_full'. */
|
|
97
|
+
readonly reason: 'message_too_large' | 'queue_full';
|
|
98
|
+
}
|
|
39
99
|
export interface RuntimeEventConnectorOptions {
|
|
40
100
|
readonly reconnect?: StreamReconnectPolicy | undefined;
|
|
41
101
|
readonly onError?: ((error: unknown) => void) | undefined;
|
|
42
102
|
readonly onOpen?: (() => void) | undefined;
|
|
103
|
+
/** @deprecated Use `onReconnectAttempt` for richer metadata. This callback is still fired for backward compatibility. */
|
|
43
104
|
readonly onReconnect?: ((attempt: number, delayMs: number) => void) | undefined;
|
|
105
|
+
/** Called on every reconnect attempt with structured metadata. */
|
|
106
|
+
readonly onReconnectAttempt?: ((info: ReconnectAttemptInfo) => void) | undefined;
|
|
107
|
+
/** Called when the connection state changes. Subscribe to drive connection-state UI badges. */
|
|
108
|
+
readonly onConnectionStateChange?: ((state: ConnectionState) => void) | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Called when the outbound queue saturates or a single message is too large to queue.
|
|
111
|
+
*
|
|
112
|
+
* **Throttling:** callbacks are emitted on the 1st overflow and every 10th overflow
|
|
113
|
+
* thereafter to avoid flooding callers during sustained disconnections. `droppedCount`
|
|
114
|
+
* in {@link BackpressureInfo} is always the cumulative total — use it as the source of
|
|
115
|
+
* truth for exact drop counts; do not count callback invocations.
|
|
116
|
+
*/
|
|
117
|
+
readonly onBackpressure?: ((info: BackpressureInfo) => void) | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Called when a typed {@link TransportEvent} is dispatched by the connector.
|
|
120
|
+
*
|
|
121
|
+
* Fires for `TRANSPORT_CONNECTION_STATE`, `TRANSPORT_RECONNECT_ATTEMPT`, and
|
|
122
|
+
* `TRANSPORT_BACKPRESSURE` events in addition to the dedicated callbacks above.
|
|
123
|
+
* Subscribe to this to receive a single unified stream of observability events
|
|
124
|
+
* suitable for forwarding to an event bus or UI state store.
|
|
125
|
+
*/
|
|
126
|
+
readonly onTransportEvent?: ((event: ConnectorTransportEvent) => void) | undefined;
|
|
44
127
|
readonly observer?: TransportObserver | undefined;
|
|
45
128
|
/**
|
|
46
129
|
* Called once the WebSocket connector is set up, providing an `emitLocal`
|
|
@@ -52,6 +135,11 @@ export interface RuntimeEventConnectorOptions {
|
|
|
52
135
|
export type AuthTokenSource = string | null | undefined | AuthTokenResolver;
|
|
53
136
|
/** Default max reconnect attempts for WebSocket connections (finite to prevent infinite auth loops). */
|
|
54
137
|
export declare const DEFAULT_WS_MAX_ATTEMPTS = 10;
|
|
138
|
+
export declare function createWebSocketRemoteError(fallbackMessage: string, body: unknown, options?: {
|
|
139
|
+
readonly code?: string;
|
|
140
|
+
readonly hint?: string;
|
|
141
|
+
readonly cause?: unknown;
|
|
142
|
+
}): WebSocketTransportError;
|
|
55
143
|
export declare class WebSocketTransportError extends GoodVibesSdkError {
|
|
56
144
|
/**
|
|
57
145
|
* WebSocket runtime-event transport error.
|
|
@@ -72,6 +160,15 @@ export declare class WebSocketTransportError extends GoodVibesSdkError {
|
|
|
72
160
|
readonly cause?: unknown;
|
|
73
161
|
readonly hint?: string;
|
|
74
162
|
readonly code: string;
|
|
163
|
+
/** Defaults to `'network'`. Override when the server sends a structured category (e.g. `'rate_limit'`). */
|
|
164
|
+
readonly category?: import('@pellux/goodvibes-errors').ErrorCategory | undefined;
|
|
165
|
+
/** Defaults to `true`. Override to `false` for unrecoverable structured errors. */
|
|
166
|
+
readonly recoverable?: boolean | undefined;
|
|
167
|
+
readonly status?: number | undefined;
|
|
168
|
+
readonly requestId?: string | undefined;
|
|
169
|
+
readonly provider?: string | undefined;
|
|
170
|
+
readonly operation?: string | undefined;
|
|
171
|
+
readonly phase?: string | undefined;
|
|
75
172
|
});
|
|
76
173
|
}
|
|
77
174
|
export declare function createRemoteRuntimeEvents<TEvent extends RuntimeEventRecord = RuntimeEventRecord>(connect: DomainEventConnector<RuntimeEventDomain, TEvent>, options?: RemoteRuntimeEventsOptions): RemoteRuntimeEvents<TEvent>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-events.d.ts","sourceRoot":"","sources":["../src/runtime-events.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,6BAA6B,CAAC;AACrC,OAAO,
|
|
1
|
+
{"version":3,"file":"runtime-events.d.ts","sourceRoot":"","sources":["../src/runtime-events.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime-events.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RUNTIME_EVENT_DOMAINS, TypedSerializedEventEnvelopeSchema, } from '@pellux/goodvibes-contracts';
|
|
2
|
-
import { ConfigurationError, GoodVibesSdkError } from '@pellux/goodvibes-errors';
|
|
2
|
+
import { ConfigurationError, GoodVibesSdkError, isStructuredDaemonErrorBody, } from '@pellux/goodvibes-errors';
|
|
3
3
|
import { normalizeAuthToken, openRawServerSentEventStream as openServerSentEventStream, normalizeStreamReconnectPolicy, getStreamReconnectDelay, } from '@pellux/goodvibes-transport-http';
|
|
4
4
|
import { buildUrl, normalizeBaseUrl } from '@pellux/goodvibes-transport-http';
|
|
5
5
|
import { describeUnknownTransportError, injectTraceparentAsync, invokeTransportObserver, transportErrorFromUnknown, } from '@pellux/goodvibes-transport-core';
|
|
@@ -48,6 +48,86 @@ function getSocketOpenState(WebSocketImpl) {
|
|
|
48
48
|
function isSocketOpen(socket, WebSocketImpl) {
|
|
49
49
|
return socket.readyState === getSocketOpenState(WebSocketImpl);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a {@link WebSocketTransportError} from a raw server-sent payload body,
|
|
53
|
+
* unpacking a {@link StructuredDaemonErrorBody} when present — matching the
|
|
54
|
+
* envelope parity that `createHttpStatusError` provides for HTTP errors.
|
|
55
|
+
*
|
|
56
|
+
* When the server sends a structured `{ error, code, category, recoverable, … }` body
|
|
57
|
+
* inside a WS message, consumers receive the same richly-typed envelope as HTTP callers.
|
|
58
|
+
* Unstructured or non-object bodies fall back to the provided `fallbackMessage`.
|
|
59
|
+
*
|
|
60
|
+
* @param fallbackMessage - Used when `body` is not a structured daemon error.
|
|
61
|
+
* @param body - The raw body parsed from the server frame (may be any shape).
|
|
62
|
+
* @param options - Optional overrides for `code`, `hint`, and `cause`.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Helper that builds a {@link WebSocketTransportError} options bag without
|
|
66
|
+
* assigning `undefined` to optional keys, satisfying `exactOptionalPropertyTypes`.
|
|
67
|
+
*/
|
|
68
|
+
function buildWsErrorOpts(code, overrides = {}) {
|
|
69
|
+
const opts = { code };
|
|
70
|
+
if (overrides.hint !== undefined)
|
|
71
|
+
opts['hint'] = overrides.hint;
|
|
72
|
+
if (overrides.cause !== undefined)
|
|
73
|
+
opts['cause'] = overrides.cause;
|
|
74
|
+
if (overrides.category !== undefined)
|
|
75
|
+
opts['category'] = overrides.category;
|
|
76
|
+
if (overrides.recoverable !== undefined)
|
|
77
|
+
opts['recoverable'] = overrides.recoverable;
|
|
78
|
+
if (overrides.status !== undefined)
|
|
79
|
+
opts['status'] = overrides.status;
|
|
80
|
+
if (overrides.requestId !== undefined)
|
|
81
|
+
opts['requestId'] = overrides.requestId;
|
|
82
|
+
if (overrides.provider !== undefined)
|
|
83
|
+
opts['provider'] = overrides.provider;
|
|
84
|
+
if (overrides.operation !== undefined)
|
|
85
|
+
opts['operation'] = overrides.operation;
|
|
86
|
+
if (overrides.phase !== undefined)
|
|
87
|
+
opts['phase'] = overrides.phase;
|
|
88
|
+
return opts;
|
|
89
|
+
}
|
|
90
|
+
export function createWebSocketRemoteError(fallbackMessage, body, options = {}) {
|
|
91
|
+
if (isStructuredDaemonErrorBody(body)) {
|
|
92
|
+
const overrides = {};
|
|
93
|
+
const effectiveHint = body.hint ?? options.hint;
|
|
94
|
+
if (effectiveHint !== undefined)
|
|
95
|
+
overrides.hint = effectiveHint;
|
|
96
|
+
if (options.cause !== undefined)
|
|
97
|
+
overrides.cause = options.cause;
|
|
98
|
+
if (body.category !== undefined)
|
|
99
|
+
overrides.category = body.category;
|
|
100
|
+
if (body.recoverable !== undefined)
|
|
101
|
+
overrides.recoverable = body.recoverable;
|
|
102
|
+
if (body.status !== undefined)
|
|
103
|
+
overrides.status = body.status;
|
|
104
|
+
if (body.requestId !== undefined)
|
|
105
|
+
overrides.requestId = body.requestId;
|
|
106
|
+
if (body.provider !== undefined)
|
|
107
|
+
overrides.provider = body.provider;
|
|
108
|
+
if (body.operation !== undefined)
|
|
109
|
+
overrides.operation = body.operation;
|
|
110
|
+
if (body.phase !== undefined)
|
|
111
|
+
overrides.phase = body.phase;
|
|
112
|
+
// Use the caller-supplied code (or 'WS_REMOTE_ERROR') as the canonical transport code.
|
|
113
|
+
// The server's body.code is not used as the WebSocketTransportError code because
|
|
114
|
+
// Symbol.hasInstance enforces a canonical-code allowlist; arbitrary server codes
|
|
115
|
+
// would break instanceof checks. The body message and metadata are still preserved.
|
|
116
|
+
const code = options.code ?? 'WS_REMOTE_ERROR';
|
|
117
|
+
return new WebSocketTransportError(body.error, buildWsErrorOpts(code, overrides));
|
|
118
|
+
}
|
|
119
|
+
const message = typeof body === 'string' && body.trim()
|
|
120
|
+
? body.trim()
|
|
121
|
+
: fallbackMessage;
|
|
122
|
+
const overrides = {};
|
|
123
|
+
if (options.hint !== undefined)
|
|
124
|
+
overrides.hint = options.hint;
|
|
125
|
+
if (options.cause !== undefined)
|
|
126
|
+
overrides.cause = options.cause;
|
|
127
|
+
else if (body !== null && body !== undefined)
|
|
128
|
+
overrides.cause = body;
|
|
129
|
+
return new WebSocketTransportError(message, buildWsErrorOpts(options.code ?? 'WS_REMOTE_ERROR', overrides));
|
|
130
|
+
}
|
|
51
131
|
export class WebSocketTransportError extends GoodVibesSdkError {
|
|
52
132
|
/**
|
|
53
133
|
* WebSocket runtime-event transport error.
|
|
@@ -87,14 +167,23 @@ export class WebSocketTransportError extends GoodVibesSdkError {
|
|
|
87
167
|
&& CANONICAL_WS_CODES.has(String(value.code));
|
|
88
168
|
}
|
|
89
169
|
// Use one of: WS_CLOSE_ABNORMAL, WS_EVENT_ERROR, WS_QUEUE_OVERFLOW, WS_REMOTE_ERROR, WS_FRAME_TOO_LARGE.
|
|
170
|
+
//
|
|
171
|
+
// Extended options mirror the HttpStatusError / createHttpStatusError envelope so that
|
|
172
|
+
// structured daemon error bodies received over WebSocket produce the same richly-typed
|
|
173
|
+
// error as their HTTP equivalents. Fields omitted default to transport-appropriate values.
|
|
90
174
|
constructor(message, options) {
|
|
91
175
|
super(message, {
|
|
92
176
|
code: options.code,
|
|
93
|
-
category: 'network',
|
|
177
|
+
category: options.category ?? 'network',
|
|
94
178
|
source: 'transport',
|
|
95
|
-
recoverable: true,
|
|
179
|
+
recoverable: options.recoverable ?? true,
|
|
96
180
|
hint: options.hint,
|
|
97
181
|
cause: options.cause,
|
|
182
|
+
status: options.status,
|
|
183
|
+
requestId: options.requestId,
|
|
184
|
+
provider: options.provider,
|
|
185
|
+
operation: options.operation,
|
|
186
|
+
phase: options.phase,
|
|
98
187
|
});
|
|
99
188
|
this.name = 'WebSocketTransportError';
|
|
100
189
|
}
|
|
@@ -216,6 +305,23 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
216
305
|
let hasReceivedMessage = false;
|
|
217
306
|
let socket = null;
|
|
218
307
|
let reconnectTimer = null;
|
|
308
|
+
// Track last emitted connection state to avoid duplicate emissions.
|
|
309
|
+
let lastConnectionState = null;
|
|
310
|
+
// Derive a stable transportId from the WS URL hostname (same URL for
|
|
311
|
+
// the lifetime of this connector, so hostname is an appropriate stable key).
|
|
312
|
+
const transportId = (() => { try {
|
|
313
|
+
return new URL(url).hostname;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return url;
|
|
317
|
+
} })();
|
|
318
|
+
const emitConnectionState = (state) => {
|
|
319
|
+
if (state === lastConnectionState)
|
|
320
|
+
return;
|
|
321
|
+
lastConnectionState = state;
|
|
322
|
+
options.onConnectionStateChange?.(state);
|
|
323
|
+
options.onTransportEvent?.({ type: 'TRANSPORT_CONNECTION_STATE', transportId, state });
|
|
324
|
+
};
|
|
219
325
|
// Bounded outbound message queue — max entries and total bytes, drop-oldest policy.
|
|
220
326
|
// Messages pushed while the socket is not yet open or is reconnecting are buffered here
|
|
221
327
|
// and flushed on the next successful open event.
|
|
@@ -256,10 +362,27 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
256
362
|
// reconnects stay observable after the first.
|
|
257
363
|
if (overflowEventCount === 1 || overflowEventCount % 10 === 0) {
|
|
258
364
|
queueOverflowNotified = true;
|
|
259
|
-
|
|
365
|
+
const bpError = new WebSocketTransportError(`WebSocket outbound message too large (${sizeBytes} bytes, limit ${MAX_OUTBOUND_MESSAGE_BYTES}). Dropping the message while the socket reconnects.`, {
|
|
260
366
|
code: 'WS_QUEUE_OVERFLOW',
|
|
261
367
|
hint: 'Split large runtime event frames before enqueueing them while the WebSocket is disconnected.',
|
|
262
|
-
})
|
|
368
|
+
});
|
|
369
|
+
options.onError?.(bpError);
|
|
370
|
+
options.onBackpressure?.({
|
|
371
|
+
droppedCount: droppedOutboundCount,
|
|
372
|
+
queueLength: outboundQueue.length,
|
|
373
|
+
queueBytes: outboundQueueBytes,
|
|
374
|
+
reason: 'message_too_large',
|
|
375
|
+
});
|
|
376
|
+
// Dispatch typed event so UI event-bus subscribers receive the same
|
|
377
|
+
// information as the onBackpressure callback.
|
|
378
|
+
options.onTransportEvent?.({
|
|
379
|
+
type: 'TRANSPORT_BACKPRESSURE',
|
|
380
|
+
transportId,
|
|
381
|
+
droppedCount: droppedOutboundCount,
|
|
382
|
+
queueLength: outboundQueue.length,
|
|
383
|
+
queueBytes: outboundQueueBytes,
|
|
384
|
+
reason: 'message_too_large',
|
|
385
|
+
});
|
|
263
386
|
}
|
|
264
387
|
return;
|
|
265
388
|
}
|
|
@@ -274,10 +397,27 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
274
397
|
// fire on first overflow and every 10th thereafter.
|
|
275
398
|
if (overflowEventCount === 1 || overflowEventCount % 10 === 0) {
|
|
276
399
|
queueOverflowNotified = true;
|
|
277
|
-
|
|
400
|
+
const bpError = new WebSocketTransportError(`WebSocket outbound queue full (limit ${MAX_OUTBOUND_QUEUE} messages / ${MAX_OUTBOUND_QUEUE_BYTES} bytes). Dropping oldest messages until the socket reconnects.`, {
|
|
278
401
|
code: 'WS_QUEUE_OVERFLOW',
|
|
279
402
|
hint: 'Wait for the runtime event WebSocket to reconnect before sending more local frames.',
|
|
280
|
-
})
|
|
403
|
+
});
|
|
404
|
+
options.onError?.(bpError);
|
|
405
|
+
options.onBackpressure?.({
|
|
406
|
+
droppedCount: droppedOutboundCount,
|
|
407
|
+
queueLength: outboundQueue.length,
|
|
408
|
+
queueBytes: outboundQueueBytes,
|
|
409
|
+
reason: 'queue_full',
|
|
410
|
+
});
|
|
411
|
+
// Dispatch typed event so UI event-bus subscribers receive the same
|
|
412
|
+
// information as the onBackpressure callback.
|
|
413
|
+
options.onTransportEvent?.({
|
|
414
|
+
type: 'TRANSPORT_BACKPRESSURE',
|
|
415
|
+
transportId,
|
|
416
|
+
droppedCount: droppedOutboundCount,
|
|
417
|
+
queueLength: outboundQueue.length,
|
|
418
|
+
queueBytes: outboundQueueBytes,
|
|
419
|
+
reason: 'queue_full',
|
|
420
|
+
});
|
|
281
421
|
}
|
|
282
422
|
}
|
|
283
423
|
outboundQueue.push({ data, sizeBytes });
|
|
@@ -301,16 +441,36 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
301
441
|
socket.close();
|
|
302
442
|
socket = null;
|
|
303
443
|
};
|
|
304
|
-
const scheduleReconnect = () => {
|
|
444
|
+
const scheduleReconnect = (reason = 'connection closed') => {
|
|
305
445
|
if (!enabled || stopped)
|
|
306
446
|
return;
|
|
307
447
|
const nextAttempt = reconnectAttempt + 1;
|
|
308
|
-
if (nextAttempt > reconnectPolicy.maxAttempts)
|
|
448
|
+
if (nextAttempt > reconnectPolicy.maxAttempts) {
|
|
449
|
+
emitConnectionState('failed');
|
|
309
450
|
return;
|
|
451
|
+
}
|
|
310
452
|
reconnectAttempt = nextAttempt;
|
|
311
453
|
// Use shared backoff helper so WS and SSE are on identical schedule.
|
|
312
454
|
const delayMs = getStreamReconnectDelay(nextAttempt, reconnectPolicy);
|
|
455
|
+
// Fire legacy callback for backward compatibility AND new structured callback.
|
|
313
456
|
options.onReconnect?.(nextAttempt, delayMs);
|
|
457
|
+
options.onReconnectAttempt?.({
|
|
458
|
+
attempt: nextAttempt,
|
|
459
|
+
maxAttempts: reconnectPolicy.maxAttempts,
|
|
460
|
+
delayMs,
|
|
461
|
+
reason,
|
|
462
|
+
});
|
|
463
|
+
// Dispatch typed event so UI event-bus subscribers receive the same
|
|
464
|
+
// metadata as the onReconnectAttempt callback.
|
|
465
|
+
options.onTransportEvent?.({
|
|
466
|
+
type: 'TRANSPORT_RECONNECT_ATTEMPT',
|
|
467
|
+
transportId,
|
|
468
|
+
attempt: nextAttempt,
|
|
469
|
+
maxAttempts: reconnectPolicy.maxAttempts,
|
|
470
|
+
delayMs,
|
|
471
|
+
reason,
|
|
472
|
+
});
|
|
473
|
+
emitConnectionState('reconnecting');
|
|
314
474
|
reconnectTimer = setTimeout(() => {
|
|
315
475
|
reconnectTimer = null;
|
|
316
476
|
void connect().catch((error) => {
|
|
@@ -343,6 +503,10 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
343
503
|
overflowEventCount = 0;
|
|
344
504
|
};
|
|
345
505
|
const onOpen = async (event) => {
|
|
506
|
+
// NOTE: do NOT call emitConnectionState('connecting') here — connect()
|
|
507
|
+
// already emits it before creating the socket. Calling it here would
|
|
508
|
+
// be a dedup-suppressed no-op (lastConnectionState is already 'connecting')
|
|
509
|
+
// and is semantically wrong since the socket is now open, not connecting.
|
|
346
510
|
const candidateSocket = event.currentTarget;
|
|
347
511
|
const openedSocket = candidateSocket && typeof candidateSocket.send === 'function'
|
|
348
512
|
? candidateSocket
|
|
@@ -374,6 +538,7 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
374
538
|
...(wsTraceHeaders['tracestate'] ? { tracestate: wsTraceHeaders['tracestate'] } : {}),
|
|
375
539
|
}));
|
|
376
540
|
flushOutboundQueue(openedSocket);
|
|
541
|
+
emitConnectionState('connected');
|
|
377
542
|
options.onOpen?.();
|
|
378
543
|
}
|
|
379
544
|
catch (error) {
|
|
@@ -436,13 +601,43 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
436
601
|
};
|
|
437
602
|
const onClose = (event) => {
|
|
438
603
|
hasReceivedMessage = false;
|
|
439
|
-
|
|
604
|
+
// RFC 6455 §7.4.1: code 1005 (No Status Received) is synthesized by runtimes
|
|
605
|
+
// when a socket closes WITHOUT a close frame — including abnormal drops (process
|
|
606
|
+
// death, proxy teardown, RST) where wasClean === false. Only a genuine clean
|
|
607
|
+
// shutdown has wasClean === true AND code === 1000. Everything else must reconnect.
|
|
608
|
+
//
|
|
609
|
+
// Compatibility note: the `wasClean` field is present on the browser CloseEvent
|
|
610
|
+
// and on the 'ws' package's CloseEvent (used by Node.js and Bun). If a runtime
|
|
611
|
+
// somehow does not expose wasClean, `event.wasClean` is undefined (falsy), so
|
|
612
|
+
// the expression `event.wasClean === true` correctly falls back to reconnecting
|
|
613
|
+
// for any ambiguous close — never suppresses reconnect incorrectly.
|
|
614
|
+
const isCleanClose = event.wasClean === true && event.code === 1000;
|
|
615
|
+
if (!stopped && !isCleanClose) {
|
|
616
|
+
// Abnormal close — surface error and schedule reconnect.
|
|
617
|
+
// The raw close reason is forwarded so TRANSPORT_RECONNECT_ATTEMPT
|
|
618
|
+
// metadata is meaningful to diagnostic UIs.
|
|
440
619
|
const closeError = webSocketCloseError(event);
|
|
441
620
|
invokeTransportObserver(() => observer?.onError?.(closeError), observer?.onObserverError);
|
|
442
621
|
options.onError?.(closeError);
|
|
622
|
+
closeSocket();
|
|
623
|
+
// Pass the close reason so reconnect metadata is meaningful.
|
|
624
|
+
const rawReason = typeof event.reason === 'string' && event.reason.trim()
|
|
625
|
+
? event.reason.trim()
|
|
626
|
+
: `code=${event.code}`;
|
|
627
|
+
scheduleReconnect(rawReason);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
// Clean close (wasClean === true && code === 1000) — deliberate server-side
|
|
631
|
+
// disconnect. We do NOT schedule a reconnect: the server explicitly terminated
|
|
632
|
+
// the session and the client should stay disconnected.
|
|
633
|
+
//
|
|
634
|
+
// **Contract:** `scheduleReconnect` is only called for non-clean closes.
|
|
635
|
+
// UIs observing `TRANSPORT_CONNECTION_STATE` will see the state transition
|
|
636
|
+
// to 'disconnected' emitted directly here in onClose, and will NOT see
|
|
637
|
+
// 'reconnecting' following a clean close.
|
|
638
|
+
closeSocket();
|
|
639
|
+
emitConnectionState('disconnected');
|
|
443
640
|
}
|
|
444
|
-
closeSocket();
|
|
445
|
-
scheduleReconnect();
|
|
446
641
|
};
|
|
447
642
|
const onError = (event) => {
|
|
448
643
|
const streamError = webSocketEventError(event, socket, url);
|
|
@@ -453,6 +648,7 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
453
648
|
if (stopped)
|
|
454
649
|
return;
|
|
455
650
|
closeSocket();
|
|
651
|
+
emitConnectionState('connecting');
|
|
456
652
|
const nextSocket = new WebSocketImpl(url);
|
|
457
653
|
socket = nextSocket;
|
|
458
654
|
nextSocket.addEventListener('open', onOpen);
|
|
@@ -470,6 +666,7 @@ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options
|
|
|
470
666
|
clearTimeout(reconnectTimer);
|
|
471
667
|
}
|
|
472
668
|
closeSocket();
|
|
669
|
+
emitConnectionState('disconnected');
|
|
473
670
|
// reset the outbound buffer on disposal so a future reconnect
|
|
474
671
|
// (or re-use of the returned cleanup path in tests) does not accumulate
|
|
475
672
|
// stale byte totals. Both must be reset together to prevent accounting drift.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-transport-realtime",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.38",
|
|
4
4
|
"engines": {
|
|
5
5
|
"bun": "1.3.10",
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"events"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@pellux/goodvibes-contracts": "0.33.
|
|
49
|
-
"@pellux/goodvibes-errors": "0.33.
|
|
50
|
-
"@pellux/goodvibes-transport-core": "0.33.
|
|
51
|
-
"@pellux/goodvibes-transport-http": "0.33.
|
|
48
|
+
"@pellux/goodvibes-contracts": "0.33.38",
|
|
49
|
+
"@pellux/goodvibes-errors": "0.33.38",
|
|
50
|
+
"@pellux/goodvibes-transport-core": "0.33.38",
|
|
51
|
+
"@pellux/goodvibes-transport-http": "0.33.38"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|