@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 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
@@ -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,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC"}
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';
@@ -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":"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"}
1
+ {"version":3,"file":"runtime-events.d.ts","sourceRoot":"","sources":["../src/runtime-events.ts"],"names":[],"mappings":"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"}
@@ -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
- options.onError?.(new WebSocketTransportError(`WebSocket outbound message too large (${sizeBytes} bytes, limit ${MAX_OUTBOUND_MESSAGE_BYTES}). Dropping the message while the socket reconnects.`, {
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
- options.onError?.(new WebSocketTransportError(`WebSocket outbound queue full (limit ${MAX_OUTBOUND_QUEUE} messages / ${MAX_OUTBOUND_QUEUE_BYTES} bytes). Dropping oldest messages until the socket reconnects.`, {
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
- if (!stopped && event.code !== 1000 && event.code !== 1005) {
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.36",
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.36",
49
- "@pellux/goodvibes-errors": "0.33.36",
50
- "@pellux/goodvibes-transport-core": "0.33.36",
51
- "@pellux/goodvibes-transport-http": "0.33.36"
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"