@flighthq/socket 0.2.1-next.841.373388f → 0.3.0-edge.1516.1b4fb2b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ export * from './enableSocketGuards';
2
+ export * from './explainSocketSendFailure';
3
+ export * from './socket';
4
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,4 @@
1
+ export * from './enableSocketGuards';
2
+ export * from './explainSocketSendFailure';
3
+ export * from './socket';
4
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
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 * from './socket';
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,cAAc,UAAU,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 * from './socket';
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,cAAc,UAAU,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';
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,iBAAiB,CAAC;AAIzB,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
- import { createSignal, emitSignal } from '@flighthq/signals';
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":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAc7D,mGAAmG;AACnG,kGAAkG;AAClG,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,CAAC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;AACnC,CAAC;AAED,mGAAmG;AACnG,sGAAsG;AACtG,iGAAiG;AACjG,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,IAAa,EAAE,MAAe;IACxE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,OAAO,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO;IAChF,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,UAAU,EAAE,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC1D,CAAC;AAED,kGAAkG;AAClG,kGAAkG;AAClG,uFAAuF;AACvF,qGAAqG;AACrG,gEAAgE;AAChE,MAAM,UAAU,YAAY,CAAC,OAAgC;IAC3D,MAAM,OAAO,GAAkB;QAC7B,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,YAAY;QACxB,UAAU,EAAE,IAAI;KACjB,CAAC;IACF,MAAM,MAAM,GAAW,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;IACrD,OAAO,CAAC,UAAU,GAAG,gBAAgB,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1F,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,iGAAiG;AACjG,kGAAkG;AAClG,qGAAqG;AACrG,4EAA4E;AAC5E,MAAM,UAAU,sBAAsB;IACpC,OAAO;QACL,UAAU,CAAC,OAAO,EAAE,MAAM;YACxB,IAAI,OAAO,SAAS,KAAK,WAAW;gBAAE,OAAO,IAAI,CAAC;YAClD,MAAM,EAAE,GACN,OAAO,CAAC,SAAS,KAAK,SAAS;gBAC7B,CAAC,CAAC,IAAI,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,SAAqB,CAAC;gBAC3D,CAAC,CAAC,IAAI,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,aAAa,CAAC;YACpD,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC5C,EAAE,CAAC,SAAS,GAAG,CAAC,KAAmB,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAChG,EAAE,CAAC,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE,CACjC,MAAM,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YACjG,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAC9C,OAAO;gBACL,eAAe,CAAC,IAAI;oBAClB,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI;wBAAE,OAAO,KAAK,CAAC;oBACnD,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACd,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,qBAAqB,CAAC,IAAI,EAAE,MAAM;oBAChC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACzB,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,CAAC,OAAO,CAAC,UAAU,GAAG,KAAK,CAAC;AACpC,CAAC;AAED,mGAAmG;AACnG,sGAAsG;AACtG,yFAAyF;AACzF,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,WAAW,CAAC,MAAM,CAAC,CAAC;IACpB,YAAY,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,CAAC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;AAChC,CAAC;AAED,oGAAoG;AACpG,gGAAgG;AAChG,uEAAuE;AACvE,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO,CAAC,OAAO,GAAG;YAChB,YAAY,EAAE,YAAY,EAAc;YACxC,eAAe,EAAE,YAAY,EAA8C;YAC3E,aAAa,EAAE,YAAY,EAA6C;YACxE,aAAa,EAAE,YAAY,EAAc;SAC1C,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC,OAAO,CAAC;AACzB,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB;IAC9B,IAAI,QAAQ,KAAK,IAAI;QAAE,QAAQ,GAAG,sBAAsB,EAAE,CAAC;IAC3D,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,mBAAmB,CAAC,MAAwB;IAC1D,OAAO,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;AACnC,CAAC;AAED,mGAAmG;AACnG,oDAAoD;AACpD,MAAM,UAAU,iBAAiB,CAAC,MAAwB,EAAE,IAA0B;IACpF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;IAC/B,IAAI,OAAO,CAAC,UAAU,KAAK,MAAM,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC/E,OAAO,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;AAClD,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,gBAAgB,CAAC,OAA6B;IAC5D,QAAQ,GAAG,OAAO,CAAC;AACrB,CAAC;AAED,IAAI,QAAQ,GAAyB,IAAI,CAAC;AAE1C,oGAAoG;AACpG,oGAAoG;AACpG,mDAAmD;AACnD,SAAS,mBAAmB,CAAC,OAAsB;IACjD,OAAO;QACL,gBAAgB;YACd,IAAI,CAAC,OAAO,CAAC,UAAU;gBAAE,OAAO;YAChC,OAAO,CAAC,UAAU,GAAG,MAAM,CAAC;YAC5B,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;gBAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACzE,CAAC;QACD,mBAAmB,CAAC,OAAO;YACzB,IAAI,CAAC,OAAO,CAAC,UAAU;gBAAE,OAAO;YAChC,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;gBAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QACrF,CAAC;QACD,iBAAiB,CAAC,IAAI;YACpB,IAAI,CAAC,OAAO,CAAC,UAAU;gBAAE,OAAO;YAChC,OAAO,CAAC,UAAU,GAAG,QAAQ,CAAC;YAC9B,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;gBAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QAChF,CAAC;QACD,iBAAiB;YACf,IAAI,CAAC,OAAO,CAAC,UAAU;gBAAE,OAAO;YAChC,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI;gBAAE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QAC1E,CAAC;KACF,CAAC;AACJ,CAAC;AAED,qGAAqG;AACrG,qEAAqE;AACrE,SAAS,eAAe,CAAC,IAAa;IACpC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC7D,OAAO,EAAE,IAAI,EAAE,IAAmB,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/socket",
3
- "version": "0.2.1-next.841.373388f",
3
+ "version": "0.3.0-edge.1516.1b4fb2b",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -13,6 +13,10 @@
13
13
  ".": {
14
14
  "types": "./dist/index.d.ts",
15
15
  "default": "./dist/index.js"
16
+ },
17
+ "./contract": {
18
+ "types": "./dist/contract.d.ts",
19
+ "default": "./dist/contract.js"
16
20
  }
17
21
  },
18
22
  "files": [
@@ -32,8 +36,9 @@
32
36
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
37
  },
34
38
  "dependencies": {
35
- "@flighthq/signals": "0.2.1-next.841.373388f",
36
- "@flighthq/types": "0.2.1-next.841.373388f"
39
+ "@flighthq/log": "0.3.0-edge.1516.1b4fb2b",
40
+ "@flighthq/signals": "0.3.0-edge.1516.1b4fb2b",
41
+ "@flighthq/types": "0.3.0-edge.1516.1b4fb2b"
37
42
  },
38
43
  "devDependencies": {
39
44
  "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
+ });
@@ -1,5 +1,11 @@
1
- import { connectSignal } from '@flighthq/signals';
2
- import type { SocketBackend, SocketCloseInfo, SocketConnection, SocketEventSink, SocketMessage } from '@flighthq/types';
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+ import type {
3
+ SocketBackend,
4
+ SocketCloseInfo,
5
+ SocketConnection,
6
+ SocketEventSink,
7
+ SocketMessage,
8
+ } from '@flighthq/types/contract';
3
9
 
4
10
  import {
5
11
  attachSocket,
@@ -13,6 +19,7 @@ import {
13
19
  getSocketReadyState,
14
20
  sendSocketMessage,
15
21
  setSocketBackend,
22
+ setSocketGuard,
16
23
  } from './socket';
17
24
 
18
25
  interface FakeSocket {
@@ -57,7 +64,10 @@ function fakeBackend(): FakeSocket {
57
64
  return state;
58
65
  }
59
66
 
60
- afterEach(() => setSocketBackend(null));
67
+ afterEach(() => {
68
+ setSocketBackend(null);
69
+ setSocketGuard(null);
70
+ });
61
71
 
62
72
  describe('attachSocket', () => {
63
73
  it('resumes delivery after a detach', () => {
@@ -72,6 +82,20 @@ describe('attachSocket', () => {
72
82
  fake.sink.handleSocketOpen();
73
83
  expect(opens).toBe(1);
74
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
+ });
75
99
  });
76
100
 
77
101
  describe('closeSocket', () => {
@@ -105,6 +129,16 @@ describe('closeSocket', () => {
105
129
  closeSocket(socket);
106
130
  expect(fake.closes).toHaveLength(1);
107
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
+ });
108
142
  });
109
143
 
110
144
  describe('createSocket', () => {
@@ -290,6 +324,8 @@ describe('disposeSocket', () => {
290
324
  fake.sink.handleSocketOpen();
291
325
  disposeSocket(socket);
292
326
  expect(fake.closes).toHaveLength(1);
327
+ expect(socket.runtime.connection).toBeNull();
328
+ expect(getSocketReadyState(socket)).toBe('closed');
293
329
  fake.sink.handleSocketMessage({ data: 'x', binary: false });
294
330
  expect(messages).toBe(0);
295
331
  });
@@ -316,6 +352,17 @@ describe('enableSocketSignals', () => {
316
352
  const socket = createSocket({ url: 'ws://x' });
317
353
  expect(socket.runtime.signals).toBeNull();
318
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
+ });
319
366
  });
320
367
 
321
368
  describe('getSocketBackend', () => {
@@ -363,13 +410,37 @@ describe('sendSocketMessage', () => {
363
410
  expect(fake.sent).toEqual([]);
364
411
  });
365
412
 
366
- it('propagates a false send result from the connection', () => {
413
+ it('returns false after disposal without reaching the released connection', () => {
367
414
  const fake = fakeBackend();
368
- fake.sendReturns = false;
369
415
  setSocketBackend(fake.backend);
370
416
  const socket = createSocket({ url: 'ws://x' });
371
417
  fake.sink.handleSocketOpen();
418
+ disposeSocket(socket);
372
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);
373
444
  });
374
445
  });
375
446
 
@@ -385,6 +456,34 @@ describe('setSocketBackend', () => {
385
456
  });
386
457
  });
387
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
+
388
487
  function noopSink(): SocketEventSink {
389
488
  return {
390
489
  handleSocketOpen() {},