@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.
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableSocketGuards.d.ts +4 -0
- package/dist/enableSocketGuards.d.ts.map +1 -0
- package/dist/enableSocketGuards.js +25 -0
- package/dist/enableSocketGuards.js.map +1 -0
- package/dist/explainSocketSendFailure.d.ts +3 -0
- package/dist/explainSocketSendFailure.d.ts.map +1 -0
- package/dist/explainSocketSendFailure.js +15 -0
- package/dist/explainSocketSendFailure.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/socket.d.ts +2 -1
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +35 -5
- package/dist/socket.js.map +1 -1
- package/package.json +6 -3
- package/src/enableSocketGuards.test.ts +108 -0
- package/src/explainSocketSendFailure.test.ts +63 -0
- package/src/socket.test.ts +96 -3
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -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
package/dist/contract.js.map
CHANGED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
package/dist/socket.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EACN,aAAa,
|
|
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
|
-
|
|
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
|
|
103
|
-
// when the socket is not open
|
|
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.
|
package/dist/socket.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,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,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/socket",
|
|
3
|
-
"version": "0.3.0
|
|
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/
|
|
40
|
-
"@flighthq/
|
|
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
|
+
});
|
package/src/socket.test.ts
CHANGED
|
@@ -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(() =>
|
|
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('
|
|
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() {},
|