@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.
- package/dist/contract.d.ts +4 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +4 -0
- package/dist/contract.js.map +1 -0
- 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 +36 -6
- package/dist/socket.js.map +1 -1
- package/package.json +8 -3
- package/src/enableSocketGuards.test.ts +108 -0
- package/src/explainSocketSendFailure.test.ts +63 -0
- package/src/socket.test.ts +104 -5
|
@@ -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"}
|
package/dist/contract.js
ADDED
|
@@ -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 @@
|
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
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
|
-
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
|
-
|
|
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,
|
|
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.
|
|
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/
|
|
36
|
-
"@flighthq/
|
|
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
|
+
});
|
package/src/socket.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { connectSignal } from '@flighthq/signals';
|
|
2
|
-
import type {
|
|
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(() =>
|
|
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('
|
|
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() {},
|