@flighthq/socket 0.5.1-edge.1059.c56d3cd → 0.5.1-edge.1350.8ce488b
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/README.md +2 -2
- package/dist/contract.d.ts +0 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +0 -1
- package/dist/contract.js.map +1 -1
- package/dist/socket.d.ts +3 -5
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +13 -54
- package/dist/socket.js.map +1 -1
- package/package.json +5 -5
- package/src/enableSocketGuards.test.ts +9 -9
- package/src/explainSocketSendFailure.test.ts +9 -9
- package/src/socket.test.ts +43 -196
- package/src/socketHost.test.ts +11 -9
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/socket`. The `@fligh
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/socket@0.5.1-edge.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/socket@0.5.1-edge.1350.8ce488b](https://github.com/flighthq/flight/tree/8ce488bfe444eded2fb281e7b9696e5ce7fabdc0/packages/socket)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/8ce488bfe444eded2fb281e7b9696e5ce7fabdc0/LICENSE.md)
|
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,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,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,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,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"}
|
package/dist/socket.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostSocketProvider, Socket, SocketGuard, SocketOptions, SocketReadyState, SocketSignals, TcpSocketConnection, TcpSocketOptions } 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
|
-
export declare function createSocket(
|
|
5
|
-
export declare function createWebSocketBackend(): SocketBackend & Entity;
|
|
4
|
+
export declare function createSocket(hostSocket: Readonly<HostSocketProvider>, options: Readonly<SocketOptions>): Socket;
|
|
6
5
|
export declare function detachSocket(socket: Socket): void;
|
|
7
6
|
export declare function disposeSocket(socket: Socket): void;
|
|
8
7
|
export declare function enableSocketSignals(socket: Socket): SocketSignals;
|
|
9
8
|
export declare function getSocketReadyState(socket: Readonly<Socket>): SocketReadyState;
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function openTcpSocket(host: HasNetSocket, options: Readonly<TcpSocketOptions>): TcpSocketConnection | null;
|
|
9
|
+
export declare function openTcpSocket(hostSocket: Readonly<HostSocketProvider>, options: Readonly<TcpSocketOptions>): TcpSocketConnection | null;
|
|
12
10
|
export declare function sendSocketMessage(socket: Readonly<Socket>, data: string | ArrayBuffer): boolean;
|
|
13
11
|
export declare function setSocketGuard(guard: SocketGuard | null): void;
|
|
14
12
|
//# 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":"AAEA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,MAAM,EAGN,WAAW,EAEX,aAAa,EACb,gBAAgB,EAEhB,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EACjB,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;AAQD,wBAAgB,YAAY,CAAC,UAAU,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAkB/G;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,CAcjE;AAGD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,gBAAgB,CAE9E;AAKD,wBAAgB,aAAa,CAC3B,UAAU,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACxC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAClC,mBAAmB,GAAG,IAAI,CAE5B;AAKD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAQ/F;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAE9D"}
|
package/dist/socket.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
|
|
2
2
|
import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
3
|
-
// Resumes delivery of
|
|
3
|
+
// Resumes delivery of provider events to the socket's signals (idempotent). Pair with detachSocket.
|
|
4
4
|
// createSocket leaves a new socket attached, so this is only needed to resume after detachSocket. A
|
|
5
5
|
// disposed socket is terminal and cannot be reattached.
|
|
6
6
|
export function attachSocket(socket) {
|
|
@@ -9,7 +9,7 @@ export function attachSocket(socket) {
|
|
|
9
9
|
socket.runtime.delivering = true;
|
|
10
10
|
}
|
|
11
11
|
// Begins a clean close of the live connection. Transitions readyState connecting/open → 'closing';
|
|
12
|
-
// the
|
|
12
|
+
// the provider's close event later transitions it to 'closed'. A no-op when already closing or closed.
|
|
13
13
|
// This is the connection command, distinct from disposeSocket (which releases the entity to GC).
|
|
14
14
|
export function closeSocket(socket, code, reason) {
|
|
15
15
|
const runtime = socket.runtime;
|
|
@@ -24,11 +24,11 @@ export function closeSocket(socket, code, reason) {
|
|
|
24
24
|
}
|
|
25
25
|
// Allocates a Socket entity plus its runtime and opens the connection through the host's socket
|
|
26
26
|
// provider,
|
|
27
|
-
// wiring the
|
|
27
|
+
// wiring the provider's open/message/close/error into the socket's (still inert) signal group. The
|
|
28
28
|
// socket starts in 'connecting' and is left attached (delivering). Enable signals with
|
|
29
|
-
// enableSocketSignals to observe events. A
|
|
29
|
+
// enableSocketSignals to observe events. A provider that does not support the transport yields a null
|
|
30
30
|
// connection and the socket stays in 'connecting' until closed.
|
|
31
|
-
export function createSocket(
|
|
31
|
+
export function createSocket(hostSocket, options) {
|
|
32
32
|
const runtime = {
|
|
33
33
|
connection: null,
|
|
34
34
|
signals: null,
|
|
@@ -42,18 +42,13 @@ export function createSocket(host, options) {
|
|
|
42
42
|
// The provider comes from the host the caller passed. A host that carries no socket transport
|
|
43
43
|
// yields no connection rather than reaching a process-global fallback: absence is an answer here,
|
|
44
44
|
// and the guard reports it.
|
|
45
|
-
const backend =
|
|
45
|
+
const backend = hostSocket;
|
|
46
46
|
runtime.connection = backend === undefined ? null : backend.openSocket(options, makeSocketEventSink(runtime));
|
|
47
47
|
if (runtime.connection === null)
|
|
48
48
|
_guard?.({ operation: 'createSocket', reason: 'no-connection', socket });
|
|
49
49
|
return socket;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
const out = allocateEntity();
|
|
53
|
-
initializeWebSocketBackend(out);
|
|
54
|
-
return finishEntity(out);
|
|
55
|
-
}
|
|
56
|
-
// Stops delivery of backend events to the socket's signals. The live connection is untouched — use
|
|
51
|
+
// Stops delivery of provider events to the socket's signals. The live connection is untouched — use
|
|
57
52
|
// closeSocket to close it. Safe to call repeatedly; resume with attachSocket.
|
|
58
53
|
export function detachSocket(socket) {
|
|
59
54
|
socket.runtime.delivering = false;
|
|
@@ -92,48 +87,19 @@ export function enableSocketSignals(socket) {
|
|
|
92
87
|
}
|
|
93
88
|
return runtime.signals;
|
|
94
89
|
}
|
|
95
|
-
// The socket's current connection phase, tracked on the runtime from
|
|
90
|
+
// The socket's current connection phase, tracked on the runtime from provider events and closeSocket.
|
|
96
91
|
export function getSocketReadyState(socket) {
|
|
97
92
|
return socket.runtime.readyState;
|
|
98
93
|
}
|
|
99
|
-
// Builds the web backend over the DOM WebSocket. Nothing constructs one at import time, so importing
|
|
100
|
-
// the package has no side effect; a host composes this value into its own net group. Returns a
|
|
101
|
-
// null connection when WebSocket is unavailable (non-browser host) rather than throwing; raw TCP/UDP
|
|
102
|
-
// is likewise unsupported here and only reachable through a native backend.
|
|
103
|
-
export function initializeWebSocketBackend(out) {
|
|
104
|
-
out.openSocket = (options, events) => {
|
|
105
|
-
if (typeof WebSocket === 'undefined')
|
|
106
|
-
return null;
|
|
107
|
-
const ws = options.protocols !== undefined
|
|
108
|
-
? new WebSocket(options.url, options.protocols)
|
|
109
|
-
: new WebSocket(options.url);
|
|
110
|
-
ws.binaryType = options.binaryType ?? 'arraybuffer';
|
|
111
|
-
ws.onopen = () => events.handleSocketOpen();
|
|
112
|
-
ws.onmessage = (event) => events.handleSocketMessage(toSocketMessage(event.data));
|
|
113
|
-
ws.onclose = (event) => events.handleSocketClose({ code: event.code, reason: event.reason, wasClean: event.wasClean });
|
|
114
|
-
ws.onerror = () => events.handleSocketError();
|
|
115
|
-
return {
|
|
116
|
-
sendSocketFrame(data) {
|
|
117
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
118
|
-
return false;
|
|
119
|
-
ws.send(data);
|
|
120
|
-
return true;
|
|
121
|
-
},
|
|
122
|
-
closeSocketConnection(code, reason) {
|
|
123
|
-
ws.close(code, reason);
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
94
|
// Opens a raw TCP byte stream through the socket provider selected by the caller's host. Browser and
|
|
129
95
|
// other framed-only providers omit openTcpSocket, so unsupported raw TCP returns null without trying
|
|
130
96
|
// openSocket or interpreting the endpoint as a WebSocket URL.
|
|
131
|
-
export function openTcpSocket(
|
|
132
|
-
return
|
|
97
|
+
export function openTcpSocket(hostSocket, options) {
|
|
98
|
+
return hostSocket?.openTcpSocket?.(options) ?? null;
|
|
133
99
|
}
|
|
134
100
|
// Sends a text or binary frame over the live connection without mutating or copying either argument.
|
|
135
101
|
// Returns false — a sentinel, not a throw — when the socket is disposed, not open, has no connection,
|
|
136
|
-
// or the
|
|
102
|
+
// or the provider rejects the send. explainSocketSendFailure diagnoses the deterministic preflight paths.
|
|
137
103
|
export function sendSocketMessage(socket, data) {
|
|
138
104
|
const runtime = socket.runtime;
|
|
139
105
|
if (runtime.disposed) {
|
|
@@ -150,9 +116,9 @@ export function setSocketGuard(guard) {
|
|
|
150
116
|
_guard = guard;
|
|
151
117
|
}
|
|
152
118
|
let _guard = null;
|
|
153
|
-
// Builds the
|
|
119
|
+
// Builds the provider→entity sink bound to one socket's runtime: it updates readyState and emits the
|
|
154
120
|
// opt-in signals. Every handler is a no-op once the runtime stops delivering (detach/dispose), so a
|
|
155
|
-
// late
|
|
121
|
+
// late provider event after teardown fires nothing.
|
|
156
122
|
function makeSocketEventSink(runtime) {
|
|
157
123
|
return {
|
|
158
124
|
handleSocketOpen() {
|
|
@@ -183,11 +149,4 @@ function makeSocketEventSink(runtime) {
|
|
|
183
149
|
},
|
|
184
150
|
};
|
|
185
151
|
}
|
|
186
|
-
// Maps a raw WebSocket message payload onto a SocketMessage. A string is a text frame; anything else
|
|
187
|
-
// (with binaryType 'arraybuffer', an ArrayBuffer) is a binary frame.
|
|
188
|
-
function toSocketMessage(data) {
|
|
189
|
-
if (typeof data === 'string')
|
|
190
|
-
return { data, binary: false };
|
|
191
|
-
return { data: data, binary: true };
|
|
192
|
-
}
|
|
193
152
|
//# sourceMappingURL=socket.js.map
|
package/dist/socket.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzE,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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/socket",
|
|
3
|
-
"version": "0.5.1-edge.
|
|
3
|
+
"version": "0.5.1-edge.1350.8ce488b",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/entity": "0.5.1-edge.
|
|
42
|
-
"@flighthq/log": "0.5.1-edge.
|
|
43
|
-
"@flighthq/signals": "0.5.1-edge.
|
|
44
|
-
"@flighthq/types": "0.5.1-edge.
|
|
41
|
+
"@flighthq/entity": "0.5.1-edge.1350.8ce488b",
|
|
42
|
+
"@flighthq/log": "0.5.1-edge.1350.8ce488b",
|
|
43
|
+
"@flighthq/signals": "0.5.1-edge.1350.8ce488b",
|
|
44
|
+
"@flighthq/types": "0.5.1-edge.1350.8ce488b"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"typescript": "^5.3.0"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
-
import type {
|
|
2
|
+
import type { HostSocketProvider, LogEntry } from '@flighthq/types/contract';
|
|
3
3
|
|
|
4
4
|
import { areSocketGuardsEnabled, disableSocketGuards, enableSocketGuards } from './enableSocketGuards';
|
|
5
5
|
import { closeSocket, createSocket, disposeSocket, enableSocketSignals, sendSocketMessage } from './socket';
|
|
@@ -15,7 +15,7 @@ function captureLog(run: () => void): readonly LogEntry[] {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function backend(hasConnection: boolean):
|
|
18
|
+
function backend(hasConnection: boolean): HostSocketProvider {
|
|
19
19
|
return {
|
|
20
20
|
openSocket() {
|
|
21
21
|
if (!hasConnection) return null;
|
|
@@ -28,8 +28,8 @@ afterEach(() => {
|
|
|
28
28
|
disableSocketGuards();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
function hostOf(backend:
|
|
32
|
-
return { net: { socket: backend } } as
|
|
31
|
+
function hostOf(backend: HostSocketProvider): { readonly net: { readonly socket: HostSocketProvider } } {
|
|
32
|
+
return { net: { socket: backend } } as { readonly net: { readonly socket: HostSocketProvider } };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
describe('areSocketGuardsEnabled', () => {
|
|
@@ -46,7 +46,7 @@ describe('disableSocketGuards', () => {
|
|
|
46
46
|
enableSocketGuards();
|
|
47
47
|
disableSocketGuards();
|
|
48
48
|
const host = hostOf(backend(false));
|
|
49
|
-
expect(captureLog(() => createSocket(host, { url: 'tcp://silent' }))).toEqual([]);
|
|
49
|
+
expect(captureLog(() => createSocket(host.net.socket, { url: 'tcp://silent' }))).toEqual([]);
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -55,8 +55,8 @@ describe('enableSocketGuards', () => {
|
|
|
55
55
|
enableSocketGuards();
|
|
56
56
|
const host = hostOf(backend(false));
|
|
57
57
|
const entries = captureLog(() => {
|
|
58
|
-
createSocket(host, { url: 'tcp://unsupported' });
|
|
59
|
-
createSocket(host, { url: 'tcp://unsupported-again' });
|
|
58
|
+
createSocket(host.net.socket, { url: 'tcp://unsupported' });
|
|
59
|
+
createSocket(host.net.socket, { url: 'tcp://unsupported-again' });
|
|
60
60
|
});
|
|
61
61
|
expect(entries).toHaveLength(1);
|
|
62
62
|
expect(entries[0]).toMatchObject({ channel: 'socket' });
|
|
@@ -71,7 +71,7 @@ describe('enableSocketGuards', () => {
|
|
|
71
71
|
it('warns once per command issued after terminal disposal', () => {
|
|
72
72
|
enableSocketGuards();
|
|
73
73
|
const host = hostOf(backend(true));
|
|
74
|
-
const socket = createSocket(host, { url: 'ws://disposed' });
|
|
74
|
+
const socket = createSocket(host.net.socket, { url: 'ws://disposed' });
|
|
75
75
|
disposeSocket(socket);
|
|
76
76
|
const entries = captureLog(() => {
|
|
77
77
|
closeSocket(socket);
|
|
@@ -94,7 +94,7 @@ describe('enableSocketGuards', () => {
|
|
|
94
94
|
it('stays silent for ordinary pre-open sends and closes', () => {
|
|
95
95
|
enableSocketGuards();
|
|
96
96
|
const host = hostOf(backend(true));
|
|
97
|
-
const socket = createSocket(host, { url: 'ws://connecting' });
|
|
97
|
+
const socket = createSocket(host.net.socket, { url: 'ws://connecting' });
|
|
98
98
|
const entries = captureLog(() => {
|
|
99
99
|
sendSocketMessage(socket, 'early');
|
|
100
100
|
closeSocket(socket);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostSocketProvider, SocketEventSink } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
3
|
import { explainSocketSendFailure } from './explainSocketSendFailure';
|
|
4
4
|
import { createSocket, disposeSocket, sendSocketMessage } from './socket';
|
|
@@ -6,9 +6,9 @@ import { createSocket, disposeSocket, sendSocketMessage } from './socket';
|
|
|
6
6
|
function installBackend(
|
|
7
7
|
hasConnection: boolean,
|
|
8
8
|
sendResult = true,
|
|
9
|
-
): { host:
|
|
9
|
+
): { host: { readonly net: { readonly socket: HostSocketProvider } }; sink: () => SocketEventSink } {
|
|
10
10
|
let sink!: SocketEventSink;
|
|
11
|
-
const backend:
|
|
11
|
+
const backend: HostSocketProvider = {
|
|
12
12
|
openSocket(_options, events) {
|
|
13
13
|
sink = events;
|
|
14
14
|
if (!hasConnection) return null;
|
|
@@ -21,14 +21,14 @@ function installBackend(
|
|
|
21
21
|
return { host: hostOf(backend), sink: () => sink };
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function hostOf(backend:
|
|
25
|
-
return { net: { socket: backend } } as
|
|
24
|
+
function hostOf(backend: HostSocketProvider): { readonly net: { readonly socket: HostSocketProvider } } {
|
|
25
|
+
return { net: { socket: backend } } as { readonly net: { readonly socket: HostSocketProvider } };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe('explainSocketSendFailure', () => {
|
|
29
29
|
it('identifies an unsupported backend with no connection', () => {
|
|
30
30
|
const { host } = installBackend(false);
|
|
31
|
-
const socket = createSocket(host, { url: 'tcp://host' });
|
|
31
|
+
const socket = createSocket(host.net.socket, { url: 'tcp://host' });
|
|
32
32
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
33
33
|
reason: 'no-connection',
|
|
34
34
|
readyState: 'connecting',
|
|
@@ -38,7 +38,7 @@ describe('explainSocketSendFailure', () => {
|
|
|
38
38
|
|
|
39
39
|
it('reports the current non-open phase when a connection exists', () => {
|
|
40
40
|
const { host } = installBackend(true);
|
|
41
|
-
const socket = createSocket(host, { url: 'ws://host' });
|
|
41
|
+
const socket = createSocket(host.net.socket, { url: 'ws://host' });
|
|
42
42
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
43
43
|
reason: 'not-open',
|
|
44
44
|
readyState: 'connecting',
|
|
@@ -48,7 +48,7 @@ describe('explainSocketSendFailure', () => {
|
|
|
48
48
|
|
|
49
49
|
it('returns null once the socket can reach its backend, even when that backend returns false', () => {
|
|
50
50
|
const { host, sink: getSink } = installBackend(true, false);
|
|
51
|
-
const socket = createSocket(host, { url: 'ws://host' });
|
|
51
|
+
const socket = createSocket(host.net.socket, { url: 'ws://host' });
|
|
52
52
|
getSink().handleSocketOpen();
|
|
53
53
|
expect(sendSocketMessage(socket, 'frame')).toBe(false);
|
|
54
54
|
expect(explainSocketSendFailure(socket)).toBeNull();
|
|
@@ -56,7 +56,7 @@ describe('explainSocketSendFailure', () => {
|
|
|
56
56
|
|
|
57
57
|
it('distinguishes terminal disposal from other closed or disconnected states', () => {
|
|
58
58
|
const { host } = installBackend(true);
|
|
59
|
-
const socket = createSocket(host, { url: 'ws://host' });
|
|
59
|
+
const socket = createSocket(host.net.socket, { url: 'ws://host' });
|
|
60
60
|
disposeSocket(socket);
|
|
61
61
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
62
62
|
reason: 'disposed',
|
package/src/socket.test.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
SocketBackend,
|
|
3
|
+
HostSocketProvider,
|
|
5
4
|
SocketCloseInfo,
|
|
6
5
|
SocketConnection,
|
|
7
6
|
SocketEventSink,
|
|
@@ -15,18 +14,16 @@ import {
|
|
|
15
14
|
attachSocket,
|
|
16
15
|
closeSocket,
|
|
17
16
|
createSocket,
|
|
18
|
-
createWebSocketBackend,
|
|
19
17
|
detachSocket,
|
|
20
18
|
disposeSocket,
|
|
21
19
|
enableSocketSignals,
|
|
22
20
|
getSocketReadyState,
|
|
23
|
-
initializeWebSocketBackend,
|
|
24
21
|
sendSocketMessage,
|
|
25
22
|
setSocketGuard,
|
|
26
23
|
} from './socket';
|
|
27
24
|
|
|
28
25
|
interface FakeSocket {
|
|
29
|
-
backend:
|
|
26
|
+
backend: HostSocketProvider;
|
|
30
27
|
sink: SocketEventSink;
|
|
31
28
|
sent: (string | ArrayBuffer)[];
|
|
32
29
|
closes: { code?: number; reason?: string }[];
|
|
@@ -46,7 +43,7 @@ function fakeBackend(): FakeSocket {
|
|
|
46
43
|
sendReturns: true,
|
|
47
44
|
lastOptions: null,
|
|
48
45
|
sink: null as unknown as SocketEventSink,
|
|
49
|
-
backend: null as unknown as
|
|
46
|
+
backend: null as unknown as HostSocketProvider,
|
|
50
47
|
};
|
|
51
48
|
state.backend = {
|
|
52
49
|
openSocket(options, events): SocketConnection | null {
|
|
@@ -71,8 +68,8 @@ afterEach(() => {
|
|
|
71
68
|
setSocketGuard(null);
|
|
72
69
|
});
|
|
73
70
|
|
|
74
|
-
function hostOf(backend:
|
|
75
|
-
return { net: { socket: backend } } as
|
|
71
|
+
function hostOf(backend: HostSocketProvider): { readonly net: { readonly socket: HostSocketProvider } } {
|
|
72
|
+
return { net: { socket: backend } } as { readonly net: { readonly socket: HostSocketProvider } };
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
function tcpConnection(): TcpSocketConnection {
|
|
@@ -87,7 +84,7 @@ describe('attachSocket', () => {
|
|
|
87
84
|
it('resumes delivery after a detach', () => {
|
|
88
85
|
const fake = fakeBackend();
|
|
89
86
|
const host = hostOf(fake.backend);
|
|
90
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
87
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
91
88
|
const signals = enableSocketSignals(socket);
|
|
92
89
|
let opens = 0;
|
|
93
90
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -100,7 +97,7 @@ describe('attachSocket', () => {
|
|
|
100
97
|
it('cannot resume delivery after terminal disposal', () => {
|
|
101
98
|
const fake = fakeBackend();
|
|
102
99
|
const host = hostOf(fake.backend);
|
|
103
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
100
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
104
101
|
const signals = enableSocketSignals(socket);
|
|
105
102
|
let opens = 0;
|
|
106
103
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -116,7 +113,7 @@ describe('closeSocket', () => {
|
|
|
116
113
|
it('transitions to closing and forwards code/reason to the connection', () => {
|
|
117
114
|
const fake = fakeBackend();
|
|
118
115
|
const host = hostOf(fake.backend);
|
|
119
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
116
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
120
117
|
fake.sink.handleSocketOpen();
|
|
121
118
|
closeSocket(socket, 1000, 'bye');
|
|
122
119
|
expect(getSocketReadyState(socket)).toBe('closing');
|
|
@@ -126,7 +123,7 @@ describe('closeSocket', () => {
|
|
|
126
123
|
it('reaches closed once the backend close event arrives', () => {
|
|
127
124
|
const fake = fakeBackend();
|
|
128
125
|
const host = hostOf(fake.backend);
|
|
129
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
126
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
130
127
|
fake.sink.handleSocketOpen();
|
|
131
128
|
closeSocket(socket);
|
|
132
129
|
fake.sink.handleSocketClose({ code: 1000, reason: '', wasClean: true });
|
|
@@ -136,7 +133,7 @@ describe('closeSocket', () => {
|
|
|
136
133
|
it('is a no-op when already closed', () => {
|
|
137
134
|
const fake = fakeBackend();
|
|
138
135
|
const host = hostOf(fake.backend);
|
|
139
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
136
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
140
137
|
fake.sink.handleSocketOpen();
|
|
141
138
|
closeSocket(socket);
|
|
142
139
|
fake.sink.handleSocketClose({ code: 1000, reason: '', wasClean: true });
|
|
@@ -147,7 +144,7 @@ describe('closeSocket', () => {
|
|
|
147
144
|
it('stays closed and does not close the connection again after disposal', () => {
|
|
148
145
|
const fake = fakeBackend();
|
|
149
146
|
const host = hostOf(fake.backend);
|
|
150
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
147
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
151
148
|
disposeSocket(socket);
|
|
152
149
|
closeSocket(socket);
|
|
153
150
|
expect(getSocketReadyState(socket)).toBe('closed');
|
|
@@ -159,7 +156,7 @@ describe('createSocket', () => {
|
|
|
159
156
|
it('opens through the backend in the connecting state and records the url', () => {
|
|
160
157
|
const fake = fakeBackend();
|
|
161
158
|
const host = hostOf(fake.backend);
|
|
162
|
-
const socket = createSocket(host, { url: 'ws://host/path' });
|
|
159
|
+
const socket = createSocket(host.net.socket, { url: 'ws://host/path' });
|
|
163
160
|
expect(socket.url).toBe('ws://host/path');
|
|
164
161
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
165
162
|
expect(fake.lastOptions?.url).toBe('ws://host/path');
|
|
@@ -168,7 +165,7 @@ describe('createSocket', () => {
|
|
|
168
165
|
it('passes protocols and binaryType through to the backend', () => {
|
|
169
166
|
const fake = fakeBackend();
|
|
170
167
|
const host = hostOf(fake.backend);
|
|
171
|
-
createSocket(host, { url: 'ws://x', protocols: ['a', 'b'], binaryType: 'arraybuffer' });
|
|
168
|
+
createSocket(host.net.socket, { url: 'ws://x', protocols: ['a', 'b'], binaryType: 'arraybuffer' });
|
|
172
169
|
expect(fake.lastOptions?.protocols).toEqual(['a', 'b']);
|
|
173
170
|
expect(fake.lastOptions?.binaryType).toBe('arraybuffer');
|
|
174
171
|
});
|
|
@@ -177,7 +174,7 @@ describe('createSocket', () => {
|
|
|
177
174
|
const fake = fakeBackend();
|
|
178
175
|
fake.openReturnsNull = true;
|
|
179
176
|
const host = hostOf(fake.backend);
|
|
180
|
-
const socket = createSocket(host, { url: 'tcp://x' });
|
|
177
|
+
const socket = createSocket(host.net.socket, { url: 'tcp://x' });
|
|
181
178
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
182
179
|
expect(sendSocketMessage(socket, 'x')).toBe(false);
|
|
183
180
|
});
|
|
@@ -185,7 +182,7 @@ describe('createSocket', () => {
|
|
|
185
182
|
it('emits a text message with binary false and a binary message with binary true', () => {
|
|
186
183
|
const fake = fakeBackend();
|
|
187
184
|
const host = hostOf(fake.backend);
|
|
188
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
185
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
189
186
|
const signals = enableSocketSignals(socket);
|
|
190
187
|
const received: SocketMessage[] = [];
|
|
191
188
|
connectSignal(signals.onSocketMessage, (m) => received.push(m));
|
|
@@ -202,7 +199,7 @@ describe('createSocket', () => {
|
|
|
202
199
|
it('emits close info with code, reason, and wasClean', () => {
|
|
203
200
|
const fake = fakeBackend();
|
|
204
201
|
const host = hostOf(fake.backend);
|
|
205
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
202
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
206
203
|
const signals = enableSocketSignals(socket);
|
|
207
204
|
const infos: SocketCloseInfo[] = [];
|
|
208
205
|
connectSignal(signals.onSocketClose, (i) => infos.push(i));
|
|
@@ -213,7 +210,7 @@ describe('createSocket', () => {
|
|
|
213
210
|
it('emits onSocketError', () => {
|
|
214
211
|
const fake = fakeBackend();
|
|
215
212
|
const host = hostOf(fake.backend);
|
|
216
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
213
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
217
214
|
const signals = enableSocketSignals(socket);
|
|
218
215
|
let errors = 0;
|
|
219
216
|
connectSignal(signals.onSocketError, () => errors++);
|
|
@@ -222,102 +219,11 @@ describe('createSocket', () => {
|
|
|
222
219
|
});
|
|
223
220
|
});
|
|
224
221
|
|
|
225
|
-
describe('createWebSocketBackend', () => {
|
|
226
|
-
it('constructs a WebSocket with url and protocols and sets binaryType', () => {
|
|
227
|
-
const restore = installFakeWebSocket();
|
|
228
|
-
try {
|
|
229
|
-
createWebSocketBackend().openSocket(
|
|
230
|
-
{ url: 'ws://host', protocols: ['chat'], binaryType: 'arraybuffer' },
|
|
231
|
-
noopSink(),
|
|
232
|
-
);
|
|
233
|
-
const ws = FakeWebSocket.last!;
|
|
234
|
-
expect(ws.url).toBe('ws://host');
|
|
235
|
-
expect(ws.protocols).toEqual(['chat']);
|
|
236
|
-
expect(ws.binaryType).toBe('arraybuffer');
|
|
237
|
-
} finally {
|
|
238
|
-
restore();
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('translates an incoming string message to binary false', () => {
|
|
243
|
-
const restore = installFakeWebSocket();
|
|
244
|
-
try {
|
|
245
|
-
const received: SocketMessage[] = [];
|
|
246
|
-
createWebSocketBackend().openSocket({ url: 'ws://x' }, sinkCollecting(received));
|
|
247
|
-
FakeWebSocket.last!.onmessage!({ data: 'hello' } as MessageEvent);
|
|
248
|
-
expect(received).toEqual([{ data: 'hello', binary: false }]);
|
|
249
|
-
} finally {
|
|
250
|
-
restore();
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('translates an incoming ArrayBuffer message to binary true', () => {
|
|
255
|
-
const restore = installFakeWebSocket();
|
|
256
|
-
try {
|
|
257
|
-
const received: SocketMessage[] = [];
|
|
258
|
-
createWebSocketBackend().openSocket({ url: 'ws://x' }, sinkCollecting(received));
|
|
259
|
-
const buffer = new Uint8Array([9]).buffer;
|
|
260
|
-
FakeWebSocket.last!.onmessage!({ data: buffer } as MessageEvent);
|
|
261
|
-
expect(received[0]).toEqual({ data: buffer, binary: true });
|
|
262
|
-
} finally {
|
|
263
|
-
restore();
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('maps close events and open into the sink', () => {
|
|
268
|
-
const restore = installFakeWebSocket();
|
|
269
|
-
try {
|
|
270
|
-
let opened = false;
|
|
271
|
-
const closes: SocketCloseInfo[] = [];
|
|
272
|
-
const sink: SocketEventSink = {
|
|
273
|
-
...noopSink(),
|
|
274
|
-
handleSocketOpen: () => (opened = true),
|
|
275
|
-
handleSocketClose: (i) => closes.push(i),
|
|
276
|
-
};
|
|
277
|
-
createWebSocketBackend().openSocket({ url: 'ws://x' }, sink);
|
|
278
|
-
const ws = FakeWebSocket.last!;
|
|
279
|
-
ws.onopen!(new Event('open'));
|
|
280
|
-
ws.onclose!({ code: 1000, reason: 'done', wasClean: true } as CloseEvent);
|
|
281
|
-
expect(opened).toBe(true);
|
|
282
|
-
expect(closes).toEqual([{ code: 1000, reason: 'done', wasClean: true }]);
|
|
283
|
-
} finally {
|
|
284
|
-
restore();
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('sends only when the WebSocket is OPEN and closes with code/reason', () => {
|
|
289
|
-
const restore = installFakeWebSocket();
|
|
290
|
-
try {
|
|
291
|
-
const connection = createWebSocketBackend().openSocket({ url: 'ws://x' }, noopSink())!;
|
|
292
|
-
const ws = FakeWebSocket.last!;
|
|
293
|
-
ws.readyState = FakeWebSocket.CONNECTING;
|
|
294
|
-
expect(connection.sendSocketFrame('x')).toBe(false);
|
|
295
|
-
ws.readyState = FakeWebSocket.OPEN;
|
|
296
|
-
expect(connection.sendSocketFrame('y')).toBe(true);
|
|
297
|
-
expect(ws.sent).toEqual(['y']);
|
|
298
|
-
connection.closeSocketConnection(1001, 'later');
|
|
299
|
-
expect(ws.closed).toEqual({ code: 1001, reason: 'later' });
|
|
300
|
-
} finally {
|
|
301
|
-
restore();
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('returns a null connection when WebSocket is unavailable', () => {
|
|
306
|
-
const original = (globalThis as { WebSocket?: unknown }).WebSocket;
|
|
307
|
-
(globalThis as { WebSocket?: unknown }).WebSocket = undefined;
|
|
308
|
-
try {
|
|
309
|
-
expect(createWebSocketBackend().openSocket({ url: 'ws://x' }, noopSink())).toBeNull();
|
|
310
|
-
} finally {
|
|
311
|
-
(globalThis as { WebSocket?: unknown }).WebSocket = original;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
222
|
describe('detachSocket', () => {
|
|
317
223
|
it('stops backend events from reaching the signals', () => {
|
|
318
224
|
const fake = fakeBackend();
|
|
319
225
|
const host = hostOf(fake.backend);
|
|
320
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
226
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
321
227
|
const signals = enableSocketSignals(socket);
|
|
322
228
|
let opens = 0;
|
|
323
229
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -331,7 +237,7 @@ describe('disposeSocket', () => {
|
|
|
331
237
|
it('closes an open connection and detaches so later events fire no signal', () => {
|
|
332
238
|
const fake = fakeBackend();
|
|
333
239
|
const host = hostOf(fake.backend);
|
|
334
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
240
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
335
241
|
const signals = enableSocketSignals(socket);
|
|
336
242
|
let messages = 0;
|
|
337
243
|
connectSignal(signals.onSocketMessage, () => messages++);
|
|
@@ -347,7 +253,7 @@ describe('disposeSocket', () => {
|
|
|
347
253
|
it('is safe to call on a fresh socket', () => {
|
|
348
254
|
const fake = fakeBackend();
|
|
349
255
|
const host = hostOf(fake.backend);
|
|
350
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
256
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
351
257
|
expect(() => disposeSocket(socket)).not.toThrow();
|
|
352
258
|
});
|
|
353
259
|
});
|
|
@@ -356,21 +262,21 @@ describe('enableSocketSignals', () => {
|
|
|
356
262
|
it('returns the same group on repeated calls', () => {
|
|
357
263
|
const fake = fakeBackend();
|
|
358
264
|
const host = hostOf(fake.backend);
|
|
359
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
265
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
360
266
|
expect(enableSocketSignals(socket)).toBe(enableSocketSignals(socket));
|
|
361
267
|
});
|
|
362
268
|
|
|
363
269
|
it('leaves a bare socket without signals', () => {
|
|
364
270
|
const fake = fakeBackend();
|
|
365
271
|
const host = hostOf(fake.backend);
|
|
366
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
272
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
367
273
|
expect(socket.runtime.signals).toBeNull();
|
|
368
274
|
});
|
|
369
275
|
|
|
370
276
|
it('returns an inert stable group without reviving a disposed socket', () => {
|
|
371
277
|
const fake = fakeBackend();
|
|
372
278
|
const host = hostOf(fake.backend);
|
|
373
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
279
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
374
280
|
disposeSocket(socket);
|
|
375
281
|
const first = enableSocketSignals(socket);
|
|
376
282
|
expect(enableSocketSignals(socket)).toBe(first);
|
|
@@ -383,7 +289,7 @@ describe('getSocketReadyState', () => {
|
|
|
383
289
|
it('reflects connecting → open → closing → closed transitions', () => {
|
|
384
290
|
const fake = fakeBackend();
|
|
385
291
|
const host = hostOf(fake.backend);
|
|
386
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
292
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
387
293
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
388
294
|
fake.sink.handleSocketOpen();
|
|
389
295
|
expect(getSocketReadyState(socket)).toBe('open');
|
|
@@ -394,46 +300,43 @@ describe('getSocketReadyState', () => {
|
|
|
394
300
|
});
|
|
395
301
|
});
|
|
396
302
|
|
|
397
|
-
describe('initializeWebSocketBackend', () => {
|
|
398
|
-
it('is the construction initializer of createWebSocketBackend', () => {
|
|
399
|
-
expect(typeof initializeWebSocketBackend).toBe('function');
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
303
|
describe('openTcpSocket', () => {
|
|
404
304
|
it('forwards the endpoint unchanged and returns the backend connection by identity', () => {
|
|
405
305
|
const expected = tcpConnection();
|
|
406
306
|
const options: TcpSocketOptions = { host: 'db.internal', port: 5432 };
|
|
407
307
|
const openTcpSocketBackend = vi.fn(() => expected);
|
|
408
|
-
const backend:
|
|
308
|
+
const backend: HostSocketProvider = {
|
|
409
309
|
openSocket: vi.fn(() => null),
|
|
410
310
|
openTcpSocket: openTcpSocketBackend,
|
|
411
311
|
};
|
|
412
312
|
|
|
413
|
-
expect(openTcpSocket(hostOf(backend), options)).toBe(expected);
|
|
313
|
+
expect(openTcpSocket(hostOf(backend).net.socket, options)).toBe(expected);
|
|
414
314
|
expect(openTcpSocketBackend).toHaveBeenCalledWith(options);
|
|
415
315
|
});
|
|
416
316
|
|
|
417
317
|
it('returns null when the host carries no socket backend', () => {
|
|
418
|
-
const host = { net: {} } as
|
|
419
|
-
expect(openTcpSocket(host, { host: 'localhost', port: 9000 })).toBeNull();
|
|
318
|
+
const host = { net: {} } as { readonly net: { readonly socket: HostSocketProvider } };
|
|
319
|
+
expect(openTcpSocket(host.net.socket, { host: 'localhost', port: 9000 })).toBeNull();
|
|
420
320
|
});
|
|
421
321
|
|
|
422
|
-
|
|
423
|
-
|
|
322
|
+
// A framed-only provider — the shape every browser transport takes — omits openTcpSocket entirely.
|
|
323
|
+
// Raw TCP must then report absence rather than falling through to openSocket and reinterpreting the
|
|
324
|
+
// endpoint as a WebSocket URL.
|
|
325
|
+
it('returns null for a framed-only backend without attempting framed openSocket', () => {
|
|
326
|
+
const backend: HostSocketProvider = { openSocket: vi.fn(() => null) };
|
|
424
327
|
const openSocket = vi.spyOn(backend, 'openSocket');
|
|
425
328
|
|
|
426
|
-
expect(openTcpSocket(hostOf(backend), { host: 'localhost', port: 9000 })).toBeNull();
|
|
329
|
+
expect(openTcpSocket(hostOf(backend).net.socket, { host: 'localhost', port: 9000 })).toBeNull();
|
|
427
330
|
expect(openSocket).not.toHaveBeenCalled();
|
|
428
331
|
});
|
|
429
332
|
|
|
430
333
|
it('preserves an explicit null from a backend that cannot open the endpoint', () => {
|
|
431
|
-
const backend:
|
|
334
|
+
const backend: HostSocketProvider = {
|
|
432
335
|
openSocket: vi.fn(() => null),
|
|
433
336
|
openTcpSocket: vi.fn(() => null),
|
|
434
337
|
};
|
|
435
338
|
|
|
436
|
-
expect(openTcpSocket(hostOf(backend), { host: 'unreachable.internal', port: 22 })).toBeNull();
|
|
339
|
+
expect(openTcpSocket(hostOf(backend).net.socket, { host: 'unreachable.internal', port: 22 })).toBeNull();
|
|
437
340
|
});
|
|
438
341
|
});
|
|
439
342
|
|
|
@@ -441,7 +344,7 @@ describe('sendSocketMessage', () => {
|
|
|
441
344
|
it('sends through the connection and returns true when open', () => {
|
|
442
345
|
const fake = fakeBackend();
|
|
443
346
|
const host = hostOf(fake.backend);
|
|
444
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
347
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
445
348
|
fake.sink.handleSocketOpen();
|
|
446
349
|
expect(sendSocketMessage(socket, 'ping')).toBe(true);
|
|
447
350
|
expect(fake.sent).toEqual(['ping']);
|
|
@@ -450,7 +353,7 @@ describe('sendSocketMessage', () => {
|
|
|
450
353
|
it('returns false without throwing when not open', () => {
|
|
451
354
|
const fake = fakeBackend();
|
|
452
355
|
const host = hostOf(fake.backend);
|
|
453
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
356
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
454
357
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
455
358
|
expect(fake.sent).toEqual([]);
|
|
456
359
|
});
|
|
@@ -458,7 +361,7 @@ describe('sendSocketMessage', () => {
|
|
|
458
361
|
it('returns false after disposal without reaching the released connection', () => {
|
|
459
362
|
const fake = fakeBackend();
|
|
460
363
|
const host = hostOf(fake.backend);
|
|
461
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
364
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
462
365
|
fake.sink.handleSocketOpen();
|
|
463
366
|
disposeSocket(socket);
|
|
464
367
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
@@ -469,7 +372,7 @@ describe('sendSocketMessage', () => {
|
|
|
469
372
|
const fake = fakeBackend();
|
|
470
373
|
fake.openReturnsNull = true;
|
|
471
374
|
const host = hostOf(fake.backend);
|
|
472
|
-
const socket = createSocket(host, { url: 'tcp://x' });
|
|
375
|
+
const socket = createSocket(host.net.socket, { url: 'tcp://x' });
|
|
473
376
|
fake.sink.handleSocketOpen();
|
|
474
377
|
expect(getSocketReadyState(socket)).toBe('open');
|
|
475
378
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
@@ -479,7 +382,7 @@ describe('sendSocketMessage', () => {
|
|
|
479
382
|
const fake = fakeBackend();
|
|
480
383
|
fake.sendReturns = false;
|
|
481
384
|
const host = hostOf(fake.backend);
|
|
482
|
-
const socket = createSocket(host, { url: 'ws://x' });
|
|
385
|
+
const socket = createSocket(host.net.socket, { url: 'ws://x' });
|
|
483
386
|
fake.sink.handleSocketOpen();
|
|
484
387
|
Object.freeze(socket.runtime);
|
|
485
388
|
Object.freeze(socket);
|
|
@@ -489,62 +392,6 @@ describe('sendSocketMessage', () => {
|
|
|
489
392
|
});
|
|
490
393
|
});
|
|
491
394
|
|
|
492
|
-
function noopSink(): SocketEventSink {
|
|
493
|
-
return {
|
|
494
|
-
handleSocketOpen() {},
|
|
495
|
-
handleSocketMessage() {},
|
|
496
|
-
handleSocketClose() {},
|
|
497
|
-
handleSocketError() {},
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function sinkCollecting(received: SocketMessage[]): SocketEventSink {
|
|
502
|
-
return { ...noopSink(), handleSocketMessage: (m) => received.push(m) };
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// A minimal stand-in for the DOM WebSocket, recording constructor args, sends, and close, and
|
|
506
|
-
// exposing dispatchable onopen/onmessage/onclose/onerror handlers.
|
|
507
|
-
class FakeWebSocket {
|
|
508
|
-
static CONNECTING = 0;
|
|
509
|
-
static OPEN = 1;
|
|
510
|
-
static CLOSING = 2;
|
|
511
|
-
static CLOSED = 3;
|
|
512
|
-
static last: FakeWebSocket | null = null;
|
|
513
|
-
|
|
514
|
-
url: string;
|
|
515
|
-
protocols?: readonly string[];
|
|
516
|
-
binaryType = 'blob';
|
|
517
|
-
readyState = FakeWebSocket.CONNECTING;
|
|
518
|
-
sent: (string | ArrayBuffer)[] = [];
|
|
519
|
-
closed: { code?: number; reason?: string } | null = null;
|
|
520
|
-
onopen: ((event: Event) => void) | null = null;
|
|
521
|
-
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
522
|
-
onclose: ((event: CloseEvent) => void) | null = null;
|
|
523
|
-
onerror: ((event: Event) => void) | null = null;
|
|
524
|
-
|
|
525
|
-
constructor(url: string, protocols?: string | string[]) {
|
|
526
|
-
this.url = url;
|
|
527
|
-
if (protocols !== undefined) this.protocols = typeof protocols === 'string' ? [protocols] : protocols;
|
|
528
|
-
FakeWebSocket.last = this;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
send(data: string | ArrayBuffer): void {
|
|
532
|
-
this.sent.push(data);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
close(code?: number, reason?: string): void {
|
|
536
|
-
this.closed = { code, reason };
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function installFakeWebSocket(): () => void {
|
|
541
|
-
const original = (globalThis as { WebSocket?: unknown }).WebSocket;
|
|
542
|
-
FakeWebSocket.last = null;
|
|
543
|
-
(globalThis as { WebSocket?: unknown }).WebSocket = FakeWebSocket;
|
|
544
|
-
return () => {
|
|
545
|
-
(globalThis as { WebSocket?: unknown }).WebSocket = original;
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
395
|
describe('setSocketGuard', () => {
|
|
549
396
|
it('installs and removes the core notice hook', () => {
|
|
550
397
|
const notices: string[] = [];
|
|
@@ -552,10 +399,10 @@ describe('setSocketGuard', () => {
|
|
|
552
399
|
const unsupported = fakeBackend();
|
|
553
400
|
unsupported.openReturnsNull = true;
|
|
554
401
|
const host = hostOf(unsupported.backend);
|
|
555
|
-
createSocket(host, { url: 'tcp://x' });
|
|
402
|
+
createSocket(host.net.socket, { url: 'tcp://x' });
|
|
556
403
|
|
|
557
404
|
const supported = fakeBackend();
|
|
558
|
-
const socket = createSocket(hostOf(supported.backend), { url: 'ws://x' });
|
|
405
|
+
const socket = createSocket(hostOf(supported.backend).net.socket, { url: 'ws://x' });
|
|
559
406
|
disposeSocket(socket);
|
|
560
407
|
closeSocket(socket);
|
|
561
408
|
sendSocketMessage(socket, 'x');
|
package/src/socketHost.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostSocketProvider, SocketConnection, SocketEventSink } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
3
|
import { createSocket, getSocketReadyState } from './socket';
|
|
4
4
|
|
|
5
|
-
function recordingBackend(label: string, opened: string[]):
|
|
5
|
+
function recordingBackend(label: string, opened: string[]): HostSocketProvider {
|
|
6
6
|
return {
|
|
7
7
|
openSocket(_options, events: SocketEventSink): SocketConnection | null {
|
|
8
8
|
opened.push(label);
|
|
@@ -17,14 +17,16 @@ function recordingBackend(label: string, opened: string[]): SocketBackend {
|
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function hostWith(backend:
|
|
21
|
-
return { net: backend === undefined ? {} : { socket: backend } } as
|
|
20
|
+
function hostWith(backend: HostSocketProvider | undefined): { readonly net: { readonly socket: HostSocketProvider } } {
|
|
21
|
+
return { net: backend === undefined ? {} : { socket: backend } } as {
|
|
22
|
+
readonly net: { readonly socket: HostSocketProvider };
|
|
23
|
+
};
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
describe('createSocket', () => {
|
|
25
27
|
it('opens through the provider carried by the host it is given', () => {
|
|
26
28
|
const opened: string[] = [];
|
|
27
|
-
createSocket(hostWith(recordingBackend('a', opened)), { url: 'wss://example.test' });
|
|
29
|
+
createSocket(hostWith(recordingBackend('a', opened)).net.socket, { url: 'wss://example.test' });
|
|
28
30
|
expect(opened).toEqual(['a']);
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -33,8 +35,8 @@ describe('createSocket', () => {
|
|
|
33
35
|
// it carried — this is the property the migration exists to create.
|
|
34
36
|
it('keeps two hosts independent', () => {
|
|
35
37
|
const opened: string[] = [];
|
|
36
|
-
createSocket(hostWith(recordingBackend('first', opened)), { url: 'wss://one.test' });
|
|
37
|
-
createSocket(hostWith(recordingBackend('second', opened)), { url: 'wss://two.test' });
|
|
38
|
+
createSocket(hostWith(recordingBackend('first', opened)).net.socket, { url: 'wss://one.test' });
|
|
39
|
+
createSocket(hostWith(recordingBackend('second', opened)).net.socket, { url: 'wss://two.test' });
|
|
38
40
|
expect(opened).toEqual(['first', 'second']);
|
|
39
41
|
});
|
|
40
42
|
|
|
@@ -42,7 +44,7 @@ describe('createSocket', () => {
|
|
|
42
44
|
// quietly reaching a process-global web backend. The socket stays 'connecting', which is the
|
|
43
45
|
// documented shape for a backend that cannot open the transport.
|
|
44
46
|
it('yields no connection when the host carries no socket provider', () => {
|
|
45
|
-
const socket = createSocket(hostWith(undefined), { url: 'wss://absent.test' });
|
|
47
|
+
const socket = createSocket(hostWith(undefined).net.socket, { url: 'wss://absent.test' });
|
|
46
48
|
expect(socket.runtime.connection).toBeNull();
|
|
47
49
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
48
50
|
});
|
|
@@ -50,7 +52,7 @@ describe('createSocket', () => {
|
|
|
50
52
|
it('does not consult one host provider when another host is passed', () => {
|
|
51
53
|
const opened: string[] = [];
|
|
52
54
|
const unused = recordingBackend('unused', opened);
|
|
53
|
-
createSocket(hostWith(recordingBackend('used', opened)), { url: 'wss://x.test' });
|
|
55
|
+
createSocket(hostWith(recordingBackend('used', opened)).net.socket, { url: 'wss://x.test' });
|
|
54
56
|
expect(opened).not.toContain('unused');
|
|
55
57
|
expect(unused).toBeDefined();
|
|
56
58
|
});
|