@flighthq/socket 0.3.0-next.906.07cea63 → 0.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.
@@ -1,2 +1,4 @@
1
+ export * from './enableSocketGuards';
2
+ export * from './explainSocketSendFailure';
1
3
  export * from './socket';
2
4
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,UAAU,CAAC"}
package/dist/contract.js CHANGED
@@ -1,2 +1,4 @@
1
+ export * from './enableSocketGuards';
2
+ export * from './explainSocketSendFailure';
1
3
  export * from './socket';
2
4
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,UAAU,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function areSocketGuardsEnabled(): boolean;
2
+ export declare function disableSocketGuards(): void;
3
+ export declare function enableSocketGuards(): void;
4
+ //# sourceMappingURL=enableSocketGuards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableSocketGuards.d.ts","sourceRoot":"","sources":["../src/enableSocketGuards.ts"],"names":[],"mappings":"AAMA,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAID,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
@@ -0,0 +1,25 @@
1
+ import { logOnce } from '@flighthq/log/contract';
2
+ import { LogLevel } from '@flighthq/types/contract';
3
+ import { setSocketGuard } from './socket';
4
+ export function areSocketGuardsEnabled() {
5
+ return _enabled;
6
+ }
7
+ export function disableSocketGuards() {
8
+ setSocketGuard(null);
9
+ _enabled = false;
10
+ }
11
+ // Installs opt-in warnings for unsupported socket creation and commands issued after terminal entity
12
+ // disposal. Omitting this module sheds the guidance strings and @flighthq/log dependency.
13
+ export function enableSocketGuards() {
14
+ setSocketGuard(warnOnSocketMisuse);
15
+ _enabled = true;
16
+ }
17
+ function warnOnSocketMisuse(notice) {
18
+ const url = notice.socket.url;
19
+ const message = notice.reason === 'no-connection'
20
+ ? 'createSocket: active backend returned no connection — call setSocketBackend(...) with a backend that supports this transport'
21
+ : `${notice.operation}: socket is already disposed — call createSocket(...) to create a new socket`;
22
+ logOnce(`socket:${notice.operation}:${notice.reason}`, LogLevel.Warn, { message, operation: notice.operation, reason: notice.reason, url }, 'socket');
23
+ }
24
+ let _enabled = false;
25
+ //# sourceMappingURL=enableSocketGuards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableSocketGuards.js","sourceRoot":"","sources":["../src/enableSocketGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,MAAM,UAAU,sBAAsB;IACpC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,QAAQ,GAAG,KAAK,CAAC;AACnB,CAAC;AAED,qGAAqG;AACrG,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB;IAChC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACnC,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAmC;IAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC;IAC9B,MAAM,OAAO,GACX,MAAM,CAAC,MAAM,KAAK,eAAe;QAC/B,CAAC,CAAC,8HAA8H;QAChI,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,8EAA8E,CAAC;IACxG,OAAO,CACL,UAAU,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,EAC7C,QAAQ,CAAC,IAAI,EACb,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,EACpE,QAAQ,CACT,CAAC;AACJ,CAAC;AAED,IAAI,QAAQ,GAAG,KAAK,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Socket, SocketSendFailureExplanation } from '@flighthq/types/contract';
2
+ export declare function explainSocketSendFailure(socket: Readonly<Socket>): SocketSendFailureExplanation | null;
3
+ //# sourceMappingURL=explainSocketSendFailure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainSocketSendFailure.d.ts","sourceRoot":"","sources":["../src/explainSocketSendFailure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAIrF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,4BAA4B,GAAG,IAAI,CAUtG"}
@@ -0,0 +1,15 @@
1
+ // Explains the deterministic preflight reasons sendSocketMessage returns false without invoking its
2
+ // backend. Returns null when the socket can reach the backend; the backend may still return false.
3
+ export function explainSocketSendFailure(socket) {
4
+ const runtime = socket.runtime;
5
+ if (runtime.disposed)
6
+ return { reason: 'disposed', readyState: 'closed', url: socket.url };
7
+ if (runtime.connection === null) {
8
+ return { reason: 'no-connection', readyState: runtime.readyState, url: socket.url };
9
+ }
10
+ if (runtime.readyState !== 'open') {
11
+ return { reason: 'not-open', readyState: runtime.readyState, url: socket.url };
12
+ }
13
+ return null;
14
+ }
15
+ //# sourceMappingURL=explainSocketSendFailure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainSocketSendFailure.js","sourceRoot":"","sources":["../src/explainSocketSendFailure.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,mGAAmG;AACnG,MAAM,UAAU,wBAAwB,CAAC,MAAwB;IAC/D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,CAAC,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IAC3F,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IACtF,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;QAClC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IACjF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { attachSocket, closeSocket, createSocket, detachSocket, disposeSocket, enableSocketSignals, getSocketReadyState, sendSocketMessage, } from './contract';
1
+ export { areSocketGuardsEnabled, attachSocket, closeSocket, createSocket, detachSocket, disableSocketGuards, disposeSocket, enableSocketGuards, enableSocketSignals, explainSocketSendFailure, getSocketReadyState, sendSocketMessage, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { attachSocket, closeSocket, createSocket, detachSocket, disposeSocket, enableSocketSignals, getSocketReadyState, sendSocketMessage, } from './contract';
1
+ export { areSocketGuardsEnabled, attachSocket, closeSocket, createSocket, detachSocket, disableSocketGuards, disposeSocket, enableSocketGuards, enableSocketSignals, explainSocketSendFailure, getSocketReadyState, sendSocketMessage, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
package/dist/socket.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Socket, SocketBackend, SocketOptions, SocketReadyState, SocketSignals } from '@flighthq/types/contract';
1
+ import type { Socket, SocketBackend, SocketGuard, SocketOptions, SocketReadyState, SocketSignals } from '@flighthq/types/contract';
2
2
  export declare function attachSocket(socket: Socket): void;
3
3
  export declare function closeSocket(socket: Socket, code?: number, reason?: string): void;
4
4
  export declare function createSocket(options: Readonly<SocketOptions>): Socket;
@@ -10,4 +10,5 @@ export declare function getSocketBackend(): SocketBackend;
10
10
  export declare function getSocketReadyState(socket: Readonly<Socket>): SocketReadyState;
11
11
  export declare function sendSocketMessage(socket: Readonly<Socket>, data: string | ArrayBuffer): boolean;
12
12
  export declare function setSocketBackend(backend: SocketBackend | null): void;
13
+ export declare function setSocketGuard(guard: SocketGuard | null): void;
13
14
  //# sourceMappingURL=socket.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EACN,aAAa,EAKb,aAAa,EACb,gBAAgB,EAEhB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAIlC,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAEjD;AAKD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAKhF;AAOD,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAUrE;AAMD,wBAAgB,sBAAsB,IAAI,aAAa,CA0BtD;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAEjD;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAIlD;AAKD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAWjE;AAGD,wBAAgB,gBAAgB,IAAI,aAAa,CAGhD;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAE9E;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAI/F;AAGD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAEpE"}
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EACN,aAAa,EAIb,WAAW,EAEX,aAAa,EACb,gBAAgB,EAEhB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGjD;AAKD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAShF;AAOD,wBAAgB,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAYrE;AAMD,wBAAgB,sBAAsB,IAAI,aAAa,CA0BtD;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAEjD;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CASlD;AAMD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAYjE;AAGD,wBAAgB,gBAAgB,IAAI,aAAa,CAGhD;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAE9E;AAKD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAQ/F;AAGD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAEpE;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAE9D"}
package/dist/socket.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { createSignal, emitSignal } from '@flighthq/signals/contract';
2
2
  // Resumes delivery of backend events to the socket's signals (idempotent). Pair with detachSocket.
3
- // createSocket leaves a new socket attached, so this is only needed to resume after detachSocket.
3
+ // createSocket leaves a new socket attached, so this is only needed to resume after detachSocket. A
4
+ // disposed socket is terminal and cannot be reattached.
4
5
  export function attachSocket(socket) {
6
+ if (socket.runtime.disposed)
7
+ return;
5
8
  socket.runtime.delivering = true;
6
9
  }
7
10
  // Begins a clean close of the live connection. Transitions readyState connecting/open → 'closing';
@@ -9,6 +12,10 @@ export function attachSocket(socket) {
9
12
  // This is the connection command, distinct from disposeSocket (which releases the entity to GC).
10
13
  export function closeSocket(socket, code, reason) {
11
14
  const runtime = socket.runtime;
15
+ if (runtime.disposed) {
16
+ _guard?.({ operation: 'closeSocket', reason: 'disposed', socket });
17
+ return;
18
+ }
12
19
  if (runtime.readyState === 'closing' || runtime.readyState === 'closed')
13
20
  return;
14
21
  runtime.readyState = 'closing';
@@ -25,9 +32,12 @@ export function createSocket(options) {
25
32
  signals: null,
26
33
  readyState: 'connecting',
27
34
  delivering: true,
35
+ disposed: false,
28
36
  };
29
37
  const socket = { url: options.url, runtime };
30
38
  runtime.connection = getSocketBackend().openSocket(options, makeSocketEventSink(runtime));
39
+ if (runtime.connection === null)
40
+ _guard?.({ operation: 'createSocket', reason: 'no-connection', socket });
31
41
  return socket;
32
42
  }
33
43
  // Builds the default web backend over the DOM WebSocket. Created lazily by getSocketBackend — no
@@ -70,15 +80,24 @@ export function detachSocket(socket) {
70
80
  // delivery, and drops the signal group. Distinct from closeSocket — dispose is entity teardown, close
71
81
  // is the connection command. After dispose the socket is inert and should not be reused.
72
82
  export function disposeSocket(socket) {
83
+ if (socket.runtime.disposed)
84
+ return;
73
85
  closeSocket(socket);
74
86
  detachSocket(socket);
75
- socket.runtime.signals = null;
87
+ const runtime = socket.runtime;
88
+ runtime.connection = null;
89
+ runtime.signals = null;
90
+ runtime.readyState = 'closed';
91
+ runtime.disposed = true;
76
92
  }
77
93
  // Opts the socket into its typed event signals, allocating the group on first call and returning it
78
94
  // (idempotent — a later call returns the same group). A bare socket that never calls this keeps
79
- // runtime.signals null and pays no signal allocation or dispatch cost.
95
+ // runtime.signals null and pays no signal allocation or dispatch cost. On a disposed socket the group
96
+ // remains inert; the optional guard reports the misuse while retaining this function's return shape.
80
97
  export function enableSocketSignals(socket) {
81
98
  const runtime = socket.runtime;
99
+ if (runtime.disposed)
100
+ _guard?.({ operation: 'enableSocketSignals', reason: 'disposed', socket });
82
101
  if (runtime.signals === null) {
83
102
  runtime.signals = {
84
103
  onSocketOpen: createSignal(),
@@ -99,10 +118,15 @@ export function getSocketBackend() {
99
118
  export function getSocketReadyState(socket) {
100
119
  return socket.runtime.readyState;
101
120
  }
102
- // Sends a text or binary frame over the live connection. Returns false a sentinel, not a throw —
103
- // when the socket is not open or has no connection.
121
+ // Sends a text or binary frame over the live connection without mutating or copying either argument.
122
+ // Returns false — a sentinel, not a throw — when the socket is disposed, not open, has no connection,
123
+ // or the backend rejects the send. explainSocketSendFailure diagnoses the deterministic preflight paths.
104
124
  export function sendSocketMessage(socket, data) {
105
125
  const runtime = socket.runtime;
126
+ if (runtime.disposed) {
127
+ _guard?.({ operation: 'sendSocketMessage', reason: 'disposed', socket });
128
+ return false;
129
+ }
106
130
  if (runtime.readyState !== 'open' || runtime.connection === null)
107
131
  return false;
108
132
  return runtime.connection.sendSocketFrame(data);
@@ -111,7 +135,13 @@ export function sendSocketMessage(socket, data) {
111
135
  export function setSocketBackend(backend) {
112
136
  _backend = backend;
113
137
  }
138
+ // Installs the diagnostics hook used by the separately imported enableSocketGuards module. Null is
139
+ // the production default and removes all guard-path work except the branch.
140
+ export function setSocketGuard(guard) {
141
+ _guard = guard;
142
+ }
114
143
  let _backend = null;
144
+ let _guard = null;
115
145
  // Builds the backend→entity sink bound to one socket's runtime: it updates readyState and emits the
116
146
  // opt-in signals. Every handler is a no-op once the runtime stops delivering (detach/dispose), so a
117
147
  // late backend event after teardown fires nothing.
@@ -1 +1 @@
1
- {"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@flighthq/socket",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,8 +38,9 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/signals": "0.3.0-next.906.07cea63",
40
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/log": "0.3.0",
42
+ "@flighthq/signals": "0.3.0",
43
+ "@flighthq/types": "0.3.0"
41
44
  },
42
45
  "devDependencies": {
43
46
  "typescript": "^5.3.0"
@@ -0,0 +1,108 @@
1
+ import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
2
+ import type { LogEntry, SocketBackend } from '@flighthq/types/contract';
3
+
4
+ import { areSocketGuardsEnabled, disableSocketGuards, enableSocketGuards } from './enableSocketGuards';
5
+ import {
6
+ closeSocket,
7
+ createSocket,
8
+ disposeSocket,
9
+ enableSocketSignals,
10
+ sendSocketMessage,
11
+ setSocketBackend,
12
+ } from './socket';
13
+
14
+ function captureLog(run: () => void): readonly LogEntry[] {
15
+ const sink = createMemoryLogSink(8);
16
+ addLogSink(sink.sink);
17
+ try {
18
+ run();
19
+ return getMemoryLogSinkEntries(sink);
20
+ } finally {
21
+ removeLogSink(sink.sink);
22
+ }
23
+ }
24
+
25
+ function backend(hasConnection: boolean): SocketBackend {
26
+ return {
27
+ openSocket() {
28
+ if (!hasConnection) return null;
29
+ return { closeSocketConnection() {}, sendSocketFrame: () => true };
30
+ },
31
+ };
32
+ }
33
+
34
+ afterEach(() => {
35
+ disableSocketGuards();
36
+ setSocketBackend(null);
37
+ });
38
+
39
+ describe('areSocketGuardsEnabled', () => {
40
+ it('reports whether the global guard hook is installed', () => {
41
+ enableSocketGuards();
42
+ expect(areSocketGuardsEnabled()).toBe(true);
43
+ disableSocketGuards();
44
+ expect(areSocketGuardsEnabled()).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe('disableSocketGuards', () => {
49
+ it('restores silent core behavior', () => {
50
+ enableSocketGuards();
51
+ disableSocketGuards();
52
+ setSocketBackend(backend(false));
53
+ expect(captureLog(() => createSocket({ url: 'tcp://silent' }))).toEqual([]);
54
+ });
55
+ });
56
+
57
+ describe('enableSocketGuards', () => {
58
+ it('warns once when socket creation produces no connection', () => {
59
+ enableSocketGuards();
60
+ setSocketBackend(backend(false));
61
+ const entries = captureLog(() => {
62
+ createSocket({ url: 'tcp://unsupported' });
63
+ createSocket({ url: 'tcp://unsupported-again' });
64
+ });
65
+ expect(entries).toHaveLength(1);
66
+ expect(entries[0]).toMatchObject({ channel: 'socket' });
67
+ expect(entries[0].data).toMatchObject({
68
+ operation: 'createSocket',
69
+ reason: 'no-connection',
70
+ url: 'tcp://unsupported',
71
+ });
72
+ expect(String((entries[0].data as Record<string, unknown>).message)).toContain('setSocketBackend');
73
+ });
74
+
75
+ it('warns once per command issued after terminal disposal', () => {
76
+ enableSocketGuards();
77
+ setSocketBackend(backend(true));
78
+ const socket = createSocket({ url: 'ws://disposed' });
79
+ disposeSocket(socket);
80
+ const entries = captureLog(() => {
81
+ closeSocket(socket);
82
+ closeSocket(socket);
83
+ sendSocketMessage(socket, 'frame');
84
+ enableSocketSignals(socket);
85
+ });
86
+ expect(entries).toHaveLength(3);
87
+ expect(entries.map((entry) => (entry.data as Record<string, unknown>).operation)).toEqual([
88
+ 'closeSocket',
89
+ 'sendSocketMessage',
90
+ 'enableSocketSignals',
91
+ ]);
92
+ for (const entry of entries) {
93
+ expect(entry.data).toMatchObject({ reason: 'disposed', url: 'ws://disposed' });
94
+ expect(String((entry.data as Record<string, unknown>).message)).toContain('createSocket');
95
+ }
96
+ });
97
+
98
+ it('stays silent for ordinary pre-open sends and closes', () => {
99
+ enableSocketGuards();
100
+ setSocketBackend(backend(true));
101
+ const socket = createSocket({ url: 'ws://connecting' });
102
+ const entries = captureLog(() => {
103
+ sendSocketMessage(socket, 'early');
104
+ closeSocket(socket);
105
+ });
106
+ expect(entries).toEqual([]);
107
+ });
108
+ });
@@ -0,0 +1,63 @@
1
+ import type { SocketBackend, SocketEventSink } from '@flighthq/types/contract';
2
+
3
+ import { explainSocketSendFailure } from './explainSocketSendFailure';
4
+ import { createSocket, disposeSocket, sendSocketMessage, setSocketBackend } from './socket';
5
+
6
+ function installBackend(hasConnection: boolean, sendResult = true): () => SocketEventSink {
7
+ let sink!: SocketEventSink;
8
+ const backend: SocketBackend = {
9
+ openSocket(_options, events) {
10
+ sink = events;
11
+ if (!hasConnection) return null;
12
+ return {
13
+ closeSocketConnection() {},
14
+ sendSocketFrame: () => sendResult,
15
+ };
16
+ },
17
+ };
18
+ setSocketBackend(backend);
19
+ return () => sink;
20
+ }
21
+
22
+ afterEach(() => setSocketBackend(null));
23
+
24
+ describe('explainSocketSendFailure', () => {
25
+ it('identifies an unsupported backend with no connection', () => {
26
+ installBackend(false);
27
+ const socket = createSocket({ url: 'tcp://host' });
28
+ expect(explainSocketSendFailure(socket)).toEqual({
29
+ reason: 'no-connection',
30
+ readyState: 'connecting',
31
+ url: 'tcp://host',
32
+ });
33
+ });
34
+
35
+ it('reports the current non-open phase when a connection exists', () => {
36
+ installBackend(true);
37
+ const socket = createSocket({ url: 'ws://host' });
38
+ expect(explainSocketSendFailure(socket)).toEqual({
39
+ reason: 'not-open',
40
+ readyState: 'connecting',
41
+ url: 'ws://host',
42
+ });
43
+ });
44
+
45
+ it('returns null once the socket can reach its backend, even when that backend returns false', () => {
46
+ const getSink = installBackend(true, false);
47
+ const socket = createSocket({ url: 'ws://host' });
48
+ getSink().handleSocketOpen();
49
+ expect(sendSocketMessage(socket, 'frame')).toBe(false);
50
+ expect(explainSocketSendFailure(socket)).toBeNull();
51
+ });
52
+
53
+ it('distinguishes terminal disposal from other closed or disconnected states', () => {
54
+ installBackend(true);
55
+ const socket = createSocket({ url: 'ws://host' });
56
+ disposeSocket(socket);
57
+ expect(explainSocketSendFailure(socket)).toEqual({
58
+ reason: 'disposed',
59
+ readyState: 'closed',
60
+ url: 'ws://host',
61
+ });
62
+ });
63
+ });
@@ -19,6 +19,7 @@ import {
19
19
  getSocketReadyState,
20
20
  sendSocketMessage,
21
21
  setSocketBackend,
22
+ setSocketGuard,
22
23
  } from './socket';
23
24
 
24
25
  interface FakeSocket {
@@ -63,7 +64,10 @@ function fakeBackend(): FakeSocket {
63
64
  return state;
64
65
  }
65
66
 
66
- afterEach(() => setSocketBackend(null));
67
+ afterEach(() => {
68
+ setSocketBackend(null);
69
+ setSocketGuard(null);
70
+ });
67
71
 
68
72
  describe('attachSocket', () => {
69
73
  it('resumes delivery after a detach', () => {
@@ -78,6 +82,20 @@ describe('attachSocket', () => {
78
82
  fake.sink.handleSocketOpen();
79
83
  expect(opens).toBe(1);
80
84
  });
85
+
86
+ it('cannot resume delivery after terminal disposal', () => {
87
+ const fake = fakeBackend();
88
+ setSocketBackend(fake.backend);
89
+ const socket = createSocket({ url: 'ws://x' });
90
+ const signals = enableSocketSignals(socket);
91
+ let opens = 0;
92
+ connectSignal(signals.onSocketOpen, () => opens++);
93
+ disposeSocket(socket);
94
+ attachSocket(socket);
95
+ fake.sink.handleSocketOpen();
96
+ expect(opens).toBe(0);
97
+ expect(socket.runtime.delivering).toBe(false);
98
+ });
81
99
  });
82
100
 
83
101
  describe('closeSocket', () => {
@@ -111,6 +129,16 @@ describe('closeSocket', () => {
111
129
  closeSocket(socket);
112
130
  expect(fake.closes).toHaveLength(1);
113
131
  });
132
+
133
+ it('stays closed and does not close the connection again after disposal', () => {
134
+ const fake = fakeBackend();
135
+ setSocketBackend(fake.backend);
136
+ const socket = createSocket({ url: 'ws://x' });
137
+ disposeSocket(socket);
138
+ closeSocket(socket);
139
+ expect(getSocketReadyState(socket)).toBe('closed');
140
+ expect(fake.closes).toHaveLength(1);
141
+ });
114
142
  });
115
143
 
116
144
  describe('createSocket', () => {
@@ -296,6 +324,8 @@ describe('disposeSocket', () => {
296
324
  fake.sink.handleSocketOpen();
297
325
  disposeSocket(socket);
298
326
  expect(fake.closes).toHaveLength(1);
327
+ expect(socket.runtime.connection).toBeNull();
328
+ expect(getSocketReadyState(socket)).toBe('closed');
299
329
  fake.sink.handleSocketMessage({ data: 'x', binary: false });
300
330
  expect(messages).toBe(0);
301
331
  });
@@ -322,6 +352,17 @@ describe('enableSocketSignals', () => {
322
352
  const socket = createSocket({ url: 'ws://x' });
323
353
  expect(socket.runtime.signals).toBeNull();
324
354
  });
355
+
356
+ it('returns an inert stable group without reviving a disposed socket', () => {
357
+ const fake = fakeBackend();
358
+ setSocketBackend(fake.backend);
359
+ const socket = createSocket({ url: 'ws://x' });
360
+ disposeSocket(socket);
361
+ const first = enableSocketSignals(socket);
362
+ expect(enableSocketSignals(socket)).toBe(first);
363
+ expect(socket.runtime.delivering).toBe(false);
364
+ expect(getSocketReadyState(socket)).toBe('closed');
365
+ });
325
366
  });
326
367
 
327
368
  describe('getSocketBackend', () => {
@@ -369,13 +410,37 @@ describe('sendSocketMessage', () => {
369
410
  expect(fake.sent).toEqual([]);
370
411
  });
371
412
 
372
- it('propagates a false send result from the connection', () => {
413
+ it('returns false after disposal without reaching the released connection', () => {
373
414
  const fake = fakeBackend();
374
- fake.sendReturns = false;
375
415
  setSocketBackend(fake.backend);
376
416
  const socket = createSocket({ url: 'ws://x' });
377
417
  fake.sink.handleSocketOpen();
418
+ disposeSocket(socket);
378
419
  expect(sendSocketMessage(socket, 'ping')).toBe(false);
420
+ expect(fake.sent).toEqual([]);
421
+ });
422
+
423
+ it('returns false for an open state whose backend supplied no connection', () => {
424
+ const fake = fakeBackend();
425
+ fake.openReturnsNull = true;
426
+ setSocketBackend(fake.backend);
427
+ const socket = createSocket({ url: 'tcp://x' });
428
+ fake.sink.handleSocketOpen();
429
+ expect(getSocketReadyState(socket)).toBe('open');
430
+ expect(sendSocketMessage(socket, 'ping')).toBe(false);
431
+ });
432
+
433
+ it('forwards binary data by identity and propagates false without mutating the readonly socket', () => {
434
+ const fake = fakeBackend();
435
+ fake.sendReturns = false;
436
+ setSocketBackend(fake.backend);
437
+ const socket = createSocket({ url: 'ws://x' });
438
+ fake.sink.handleSocketOpen();
439
+ Object.freeze(socket.runtime);
440
+ Object.freeze(socket);
441
+ const frame = new Uint8Array([1, 2, 3]).buffer;
442
+ expect(sendSocketMessage(socket, frame)).toBe(false);
443
+ expect(fake.sent[0]).toBe(frame);
379
444
  });
380
445
  });
381
446
 
@@ -391,6 +456,34 @@ describe('setSocketBackend', () => {
391
456
  });
392
457
  });
393
458
 
459
+ describe('setSocketGuard', () => {
460
+ it('installs and removes the core notice hook', () => {
461
+ const notices: string[] = [];
462
+ setSocketGuard((notice) => notices.push(`${notice.operation}:${notice.reason}`));
463
+ const unsupported = fakeBackend();
464
+ unsupported.openReturnsNull = true;
465
+ setSocketBackend(unsupported.backend);
466
+ createSocket({ url: 'tcp://x' });
467
+
468
+ const supported = fakeBackend();
469
+ setSocketBackend(supported.backend);
470
+ const socket = createSocket({ url: 'ws://x' });
471
+ disposeSocket(socket);
472
+ closeSocket(socket);
473
+ sendSocketMessage(socket, 'x');
474
+ enableSocketSignals(socket);
475
+ setSocketGuard(null);
476
+ closeSocket(socket);
477
+
478
+ expect(notices).toEqual([
479
+ 'createSocket:no-connection',
480
+ 'closeSocket:disposed',
481
+ 'sendSocketMessage:disposed',
482
+ 'enableSocketSignals:disposed',
483
+ ]);
484
+ });
485
+ });
486
+
394
487
  function noopSink(): SocketEventSink {
395
488
  return {
396
489
  handleSocketOpen() {},