@flighthq/socket 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.1047.858b9b6
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 +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableSocketGuards.js +1 -1
- package/dist/enableSocketGuards.js.map +1 -1
- 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 +5 -5
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +58 -50
- package/dist/socket.js.map +1 -1
- package/package.json +5 -4
- package/src/enableSocketGuards.test.ts +16 -20
- package/src/explainSocketSendFailure.test.ts +18 -14
- package/src/socket.test.ts +134 -103
- package/src/socketHost.test.ts +57 -0
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.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/socket@0.5.1-edge.1047.858b9b6](https://github.com/flighthq/flight/tree/858b9b601de814f6b6940cbb1ffff0916b903268/packages/socket)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/858b9b601de814f6b6940cbb1ffff0916b903268/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;AACzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,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;AACzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -17,7 +17,7 @@ export function enableSocketGuards() {
|
|
|
17
17
|
function warnOnSocketMisuse(notice) {
|
|
18
18
|
const url = notice.socket.url;
|
|
19
19
|
const message = notice.reason === 'no-connection'
|
|
20
|
-
? 'createSocket:
|
|
20
|
+
? 'createSocket: the host carries no socket provider for this transport, or its provider returned no connection — pass a host whose net.socket supports it'
|
|
21
21
|
: `${notice.operation}: socket is already disposed — call createSocket(...) to create a new socket`;
|
|
22
22
|
logOnce(`socket:${notice.operation}:${notice.reason}`, LogLevel.Warn, { message, operation: notice.operation, reason: notice.reason, url }, 'socket');
|
|
23
23
|
}
|
|
@@ -1 +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,
|
|
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,yJAAyJ;QAC3J,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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { areSocketGuardsEnabled, attachSocket, closeSocket, createSocket, detachSocket, disableSocketGuards, disposeSocket, enableSocketGuards, enableSocketSignals, explainSocketSendFailure, getSocketReadyState, sendSocketMessage, } from './contract';
|
|
1
|
+
export { areSocketGuardsEnabled, attachSocket, closeSocket, createSocket, detachSocket, disableSocketGuards, disposeSocket, enableSocketGuards, enableSocketSignals, explainSocketSendFailure, getSocketReadyState, openTcpSocket, 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,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"}
|
|
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,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { areSocketGuardsEnabled, attachSocket, closeSocket, createSocket, detachSocket, disableSocketGuards, disposeSocket, enableSocketGuards, enableSocketSignals, explainSocketSendFailure, getSocketReadyState, sendSocketMessage, } from './contract';
|
|
1
|
+
export { areSocketGuardsEnabled, attachSocket, closeSocket, createSocket, detachSocket, disableSocketGuards, disposeSocket, enableSocketGuards, enableSocketSignals, explainSocketSendFailure, getSocketReadyState, openTcpSocket, 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,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"}
|
|
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,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/socket.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type { Socket, SocketBackend, SocketGuard, SocketOptions, SocketReadyState, SocketSignals } from '@flighthq/types/contract';
|
|
1
|
+
import type { Entity, EntityConstruction, HasNetSocket, Socket, SocketBackend, 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(options: Readonly<SocketOptions>): Socket;
|
|
5
|
-
export declare function createWebSocketBackend(): SocketBackend;
|
|
4
|
+
export declare function createSocket(host: HasNetSocket, options: Readonly<SocketOptions>): Socket;
|
|
5
|
+
export declare function createWebSocketBackend(): SocketBackend & Entity;
|
|
6
6
|
export declare function detachSocket(socket: Socket): void;
|
|
7
7
|
export declare function disposeSocket(socket: Socket): void;
|
|
8
8
|
export declare function enableSocketSignals(socket: Socket): SocketSignals;
|
|
9
|
-
export declare function getSocketBackend(): SocketBackend;
|
|
10
9
|
export declare function getSocketReadyState(socket: Readonly<Socket>): SocketReadyState;
|
|
10
|
+
export declare function initializeWebSocketBackend(out: EntityConstruction<SocketBackend & Entity>): void;
|
|
11
|
+
export declare function openTcpSocket(host: HasNetSocket, options: Readonly<TcpSocketOptions>): TcpSocketConnection | null;
|
|
11
12
|
export declare function sendSocketMessage(socket: Readonly<Socket>, data: string | ArrayBuffer): boolean;
|
|
12
|
-
export declare function setSocketBackend(backend: SocketBackend | null): void;
|
|
13
13
|
export declare function setSocketGuard(guard: SocketGuard | null): void;
|
|
14
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":"
|
|
1
|
+
{"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../src/socket.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,MAAM,EACN,kBAAkB,EAClB,YAAY,EACZ,MAAM,EACN,aAAa,EAIb,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,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAkBzF;AAED,wBAAgB,sBAAsB,IAAI,aAAa,GAAG,MAAM,CAI/D;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;AAMD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,kBAAkB,CAAC,aAAa,GAAG,MAAM,CAAC,GAAG,IAAI,CAwBhG;AAKD,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,mBAAmB,GAAG,IAAI,CAEjH;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,3 +1,4 @@
|
|
|
1
|
+
import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
|
|
1
2
|
import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
2
3
|
// Resumes delivery of backend events to the socket's signals (idempotent). Pair with detachSocket.
|
|
3
4
|
// createSocket leaves a new socket attached, so this is only needed to resume after detachSocket. A
|
|
@@ -21,12 +22,13 @@ export function closeSocket(socket, code, reason) {
|
|
|
21
22
|
runtime.readyState = 'closing';
|
|
22
23
|
runtime.connection?.closeSocketConnection(code, reason);
|
|
23
24
|
}
|
|
24
|
-
// Allocates a Socket entity plus its runtime and opens the connection through the
|
|
25
|
+
// Allocates a Socket entity plus its runtime and opens the connection through the host's socket
|
|
26
|
+
// provider,
|
|
25
27
|
// wiring the backend's open/message/close/error into the socket's (still inert) signal group. The
|
|
26
28
|
// socket starts in 'connecting' and is left attached (delivering). Enable signals with
|
|
27
29
|
// enableSocketSignals to observe events. A backend that does not support the transport yields a null
|
|
28
30
|
// connection and the socket stays in 'connecting' until closed.
|
|
29
|
-
export function createSocket(options) {
|
|
31
|
+
export function createSocket(host, options) {
|
|
30
32
|
const runtime = {
|
|
31
33
|
connection: null,
|
|
32
34
|
signals: null,
|
|
@@ -34,42 +36,22 @@ export function createSocket(options) {
|
|
|
34
36
|
delivering: true,
|
|
35
37
|
disposed: false,
|
|
36
38
|
};
|
|
37
|
-
const socket =
|
|
38
|
-
|
|
39
|
+
const socket = allocateEntity();
|
|
40
|
+
socket.url = options.url;
|
|
41
|
+
socket.runtime = runtime;
|
|
42
|
+
// The provider comes from the host the caller passed. A host that carries no socket transport
|
|
43
|
+
// yields no connection rather than reaching a process-global fallback: absence is an answer here,
|
|
44
|
+
// and the guard reports it.
|
|
45
|
+
const backend = host.net.socket;
|
|
46
|
+
runtime.connection = backend === undefined ? null : backend.openSocket(options, makeSocketEventSink(runtime));
|
|
39
47
|
if (runtime.connection === null)
|
|
40
48
|
_guard?.({ operation: 'createSocket', reason: 'no-connection', socket });
|
|
41
49
|
return socket;
|
|
42
50
|
}
|
|
43
|
-
// Builds the default web backend over the DOM WebSocket. Created lazily by getSocketBackend — no
|
|
44
|
-
// WebSocket is constructed at import time, so importing the package has no side effect. Returns a
|
|
45
|
-
// null connection when WebSocket is unavailable (non-browser host) rather than throwing; raw TCP/UDP
|
|
46
|
-
// is likewise unsupported here and only reachable through a native backend.
|
|
47
51
|
export function createWebSocketBackend() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return null;
|
|
52
|
-
const ws = options.protocols !== undefined
|
|
53
|
-
? new WebSocket(options.url, options.protocols)
|
|
54
|
-
: new WebSocket(options.url);
|
|
55
|
-
ws.binaryType = options.binaryType ?? 'arraybuffer';
|
|
56
|
-
ws.onopen = () => events.handleSocketOpen();
|
|
57
|
-
ws.onmessage = (event) => events.handleSocketMessage(toSocketMessage(event.data));
|
|
58
|
-
ws.onclose = (event) => events.handleSocketClose({ code: event.code, reason: event.reason, wasClean: event.wasClean });
|
|
59
|
-
ws.onerror = () => events.handleSocketError();
|
|
60
|
-
return {
|
|
61
|
-
sendSocketFrame(data) {
|
|
62
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
63
|
-
return false;
|
|
64
|
-
ws.send(data);
|
|
65
|
-
return true;
|
|
66
|
-
},
|
|
67
|
-
closeSocketConnection(code, reason) {
|
|
68
|
-
ws.close(code, reason);
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
};
|
|
52
|
+
const out = allocateEntity();
|
|
53
|
+
initializeWebSocketBackend(out);
|
|
54
|
+
return finishEntity(out);
|
|
73
55
|
}
|
|
74
56
|
// Stops delivery of backend events to the socket's signals. The live connection is untouched — use
|
|
75
57
|
// closeSocket to close it. Safe to call repeatedly; resume with attachSocket.
|
|
@@ -99,25 +81,56 @@ export function enableSocketSignals(socket) {
|
|
|
99
81
|
if (runtime.disposed)
|
|
100
82
|
_guard?.({ operation: 'enableSocketSignals', reason: 'disposed', socket });
|
|
101
83
|
if (runtime.signals === null) {
|
|
102
|
-
runtime.signals = {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
84
|
+
runtime.signals = (() => {
|
|
85
|
+
const out = allocateEntity();
|
|
86
|
+
out.onSocketOpen = createSignal();
|
|
87
|
+
out.onSocketMessage = createSignal();
|
|
88
|
+
out.onSocketClose = createSignal();
|
|
89
|
+
out.onSocketError = createSignal();
|
|
90
|
+
return finishEntity(out);
|
|
91
|
+
})();
|
|
108
92
|
}
|
|
109
93
|
return runtime.signals;
|
|
110
94
|
}
|
|
111
|
-
// The active socket backend, or a lazily-created web default. There is always a backend.
|
|
112
|
-
export function getSocketBackend() {
|
|
113
|
-
if (_backend === null)
|
|
114
|
-
_backend = createWebSocketBackend();
|
|
115
|
-
return _backend;
|
|
116
|
-
}
|
|
117
95
|
// The socket's current connection phase, tracked on the runtime from backend events and closeSocket.
|
|
118
96
|
export function getSocketReadyState(socket) {
|
|
119
97
|
return socket.runtime.readyState;
|
|
120
98
|
}
|
|
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
|
+
// Opens a raw TCP byte stream through the socket provider selected by the caller's host. Browser and
|
|
129
|
+
// other framed-only providers omit openTcpSocket, so unsupported raw TCP returns null without trying
|
|
130
|
+
// openSocket or interpreting the endpoint as a WebSocket URL.
|
|
131
|
+
export function openTcpSocket(host, options) {
|
|
132
|
+
return host.net.socket?.openTcpSocket?.(options) ?? null;
|
|
133
|
+
}
|
|
121
134
|
// Sends a text or binary frame over the live connection without mutating or copying either argument.
|
|
122
135
|
// Returns false — a sentinel, not a throw — when the socket is disposed, not open, has no connection,
|
|
123
136
|
// or the backend rejects the send. explainSocketSendFailure diagnoses the deterministic preflight paths.
|
|
@@ -131,16 +144,11 @@ export function sendSocketMessage(socket, data) {
|
|
|
131
144
|
return false;
|
|
132
145
|
return runtime.connection.sendSocketFrame(data);
|
|
133
146
|
}
|
|
134
|
-
// Installs a native host socket backend (adding TCP/UDP); pass null to fall back to the web default.
|
|
135
|
-
export function setSocketBackend(backend) {
|
|
136
|
-
_backend = backend;
|
|
137
|
-
}
|
|
138
147
|
// Installs the diagnostics hook used by the separately imported enableSocketGuards module. Null is
|
|
139
148
|
// the production default and removes all guard-path work except the branch.
|
|
140
149
|
export function setSocketGuard(guard) {
|
|
141
150
|
_guard = guard;
|
|
142
151
|
}
|
|
143
|
-
let _backend = null;
|
|
144
152
|
let _guard = null;
|
|
145
153
|
// Builds the backend→entity sink bound to one socket's runtime: it updates readyState and emits the
|
|
146
154
|
// opt-in signals. Every handler is a no-op once the runtime stops delivering (detach/dispose), so a
|
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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/socket",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.1047.858b9b6",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/
|
|
42
|
-
"@flighthq/
|
|
43
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/entity": "0.5.1-edge.1047.858b9b6",
|
|
42
|
+
"@flighthq/log": "0.5.1-edge.1047.858b9b6",
|
|
43
|
+
"@flighthq/signals": "0.5.1-edge.1047.858b9b6",
|
|
44
|
+
"@flighthq/types": "0.5.1-edge.1047.858b9b6"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"typescript": "^5.3.0"
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
-
import type { LogEntry, SocketBackend } from '@flighthq/types/contract';
|
|
2
|
+
import type { HasNetSocket, LogEntry, SocketBackend } from '@flighthq/types/contract';
|
|
3
3
|
|
|
4
4
|
import { areSocketGuardsEnabled, disableSocketGuards, enableSocketGuards } from './enableSocketGuards';
|
|
5
|
-
import {
|
|
6
|
-
closeSocket,
|
|
7
|
-
createSocket,
|
|
8
|
-
disposeSocket,
|
|
9
|
-
enableSocketSignals,
|
|
10
|
-
sendSocketMessage,
|
|
11
|
-
setSocketBackend,
|
|
12
|
-
} from './socket';
|
|
5
|
+
import { closeSocket, createSocket, disposeSocket, enableSocketSignals, sendSocketMessage } from './socket';
|
|
13
6
|
|
|
14
7
|
function captureLog(run: () => void): readonly LogEntry[] {
|
|
15
8
|
const sink = createMemoryLogSink(8);
|
|
@@ -33,9 +26,12 @@ function backend(hasConnection: boolean): SocketBackend {
|
|
|
33
26
|
|
|
34
27
|
afterEach(() => {
|
|
35
28
|
disableSocketGuards();
|
|
36
|
-
setSocketBackend(null);
|
|
37
29
|
});
|
|
38
30
|
|
|
31
|
+
function hostOf(backend: SocketBackend): HasNetSocket {
|
|
32
|
+
return { net: { socket: backend } } as HasNetSocket;
|
|
33
|
+
}
|
|
34
|
+
|
|
39
35
|
describe('areSocketGuardsEnabled', () => {
|
|
40
36
|
it('reports whether the global guard hook is installed', () => {
|
|
41
37
|
enableSocketGuards();
|
|
@@ -49,18 +45,18 @@ describe('disableSocketGuards', () => {
|
|
|
49
45
|
it('restores silent core behavior', () => {
|
|
50
46
|
enableSocketGuards();
|
|
51
47
|
disableSocketGuards();
|
|
52
|
-
|
|
53
|
-
expect(captureLog(() => createSocket({ url: 'tcp://silent' }))).toEqual([]);
|
|
48
|
+
const host = hostOf(backend(false));
|
|
49
|
+
expect(captureLog(() => createSocket(host, { url: 'tcp://silent' }))).toEqual([]);
|
|
54
50
|
});
|
|
55
51
|
});
|
|
56
52
|
|
|
57
53
|
describe('enableSocketGuards', () => {
|
|
58
54
|
it('warns once when socket creation produces no connection', () => {
|
|
59
55
|
enableSocketGuards();
|
|
60
|
-
|
|
56
|
+
const host = hostOf(backend(false));
|
|
61
57
|
const entries = captureLog(() => {
|
|
62
|
-
createSocket({ url: 'tcp://unsupported' });
|
|
63
|
-
createSocket({ url: 'tcp://unsupported-again' });
|
|
58
|
+
createSocket(host, { url: 'tcp://unsupported' });
|
|
59
|
+
createSocket(host, { url: 'tcp://unsupported-again' });
|
|
64
60
|
});
|
|
65
61
|
expect(entries).toHaveLength(1);
|
|
66
62
|
expect(entries[0]).toMatchObject({ channel: 'socket' });
|
|
@@ -69,13 +65,13 @@ describe('enableSocketGuards', () => {
|
|
|
69
65
|
reason: 'no-connection',
|
|
70
66
|
url: 'tcp://unsupported',
|
|
71
67
|
});
|
|
72
|
-
expect(String((entries[0].data as Record<string, unknown>).message)).toContain('
|
|
68
|
+
expect(String((entries[0].data as Record<string, unknown>).message)).toContain('net.socket');
|
|
73
69
|
});
|
|
74
70
|
|
|
75
71
|
it('warns once per command issued after terminal disposal', () => {
|
|
76
72
|
enableSocketGuards();
|
|
77
|
-
|
|
78
|
-
const socket = createSocket({ url: 'ws://disposed' });
|
|
73
|
+
const host = hostOf(backend(true));
|
|
74
|
+
const socket = createSocket(host, { url: 'ws://disposed' });
|
|
79
75
|
disposeSocket(socket);
|
|
80
76
|
const entries = captureLog(() => {
|
|
81
77
|
closeSocket(socket);
|
|
@@ -97,8 +93,8 @@ describe('enableSocketGuards', () => {
|
|
|
97
93
|
|
|
98
94
|
it('stays silent for ordinary pre-open sends and closes', () => {
|
|
99
95
|
enableSocketGuards();
|
|
100
|
-
|
|
101
|
-
const socket = createSocket({ url: 'ws://connecting' });
|
|
96
|
+
const host = hostOf(backend(true));
|
|
97
|
+
const socket = createSocket(host, { url: 'ws://connecting' });
|
|
102
98
|
const entries = captureLog(() => {
|
|
103
99
|
sendSocketMessage(socket, 'early');
|
|
104
100
|
closeSocket(socket);
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { SocketBackend, SocketEventSink } from '@flighthq/types/contract';
|
|
1
|
+
import type { HasNetSocket, SocketBackend, SocketEventSink } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
3
|
import { explainSocketSendFailure } from './explainSocketSendFailure';
|
|
4
|
-
import { createSocket, disposeSocket, sendSocketMessage
|
|
4
|
+
import { createSocket, disposeSocket, sendSocketMessage } from './socket';
|
|
5
5
|
|
|
6
|
-
function installBackend(
|
|
6
|
+
function installBackend(
|
|
7
|
+
hasConnection: boolean,
|
|
8
|
+
sendResult = true,
|
|
9
|
+
): { host: HasNetSocket; sink: () => SocketEventSink } {
|
|
7
10
|
let sink!: SocketEventSink;
|
|
8
11
|
const backend: SocketBackend = {
|
|
9
12
|
openSocket(_options, events) {
|
|
@@ -15,16 +18,17 @@ function installBackend(hasConnection: boolean, sendResult = true): () => Socket
|
|
|
15
18
|
};
|
|
16
19
|
},
|
|
17
20
|
};
|
|
18
|
-
|
|
19
|
-
return () => sink;
|
|
21
|
+
return { host: hostOf(backend), sink: () => sink };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
function hostOf(backend: SocketBackend): HasNetSocket {
|
|
25
|
+
return { net: { socket: backend } } as HasNetSocket;
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
describe('explainSocketSendFailure', () => {
|
|
25
29
|
it('identifies an unsupported backend with no connection', () => {
|
|
26
|
-
installBackend(false);
|
|
27
|
-
const socket = createSocket({ url: 'tcp://host' });
|
|
30
|
+
const { host } = installBackend(false);
|
|
31
|
+
const socket = createSocket(host, { url: 'tcp://host' });
|
|
28
32
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
29
33
|
reason: 'no-connection',
|
|
30
34
|
readyState: 'connecting',
|
|
@@ -33,8 +37,8 @@ describe('explainSocketSendFailure', () => {
|
|
|
33
37
|
});
|
|
34
38
|
|
|
35
39
|
it('reports the current non-open phase when a connection exists', () => {
|
|
36
|
-
installBackend(true);
|
|
37
|
-
const socket = createSocket({ url: 'ws://host' });
|
|
40
|
+
const { host } = installBackend(true);
|
|
41
|
+
const socket = createSocket(host, { url: 'ws://host' });
|
|
38
42
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
39
43
|
reason: 'not-open',
|
|
40
44
|
readyState: 'connecting',
|
|
@@ -43,16 +47,16 @@ describe('explainSocketSendFailure', () => {
|
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
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' });
|
|
50
|
+
const { host, sink: getSink } = installBackend(true, false);
|
|
51
|
+
const socket = createSocket(host, { url: 'ws://host' });
|
|
48
52
|
getSink().handleSocketOpen();
|
|
49
53
|
expect(sendSocketMessage(socket, 'frame')).toBe(false);
|
|
50
54
|
expect(explainSocketSendFailure(socket)).toBeNull();
|
|
51
55
|
});
|
|
52
56
|
|
|
53
57
|
it('distinguishes terminal disposal from other closed or disconnected states', () => {
|
|
54
|
-
installBackend(true);
|
|
55
|
-
const socket = createSocket({ url: 'ws://host' });
|
|
58
|
+
const { host } = installBackend(true);
|
|
59
|
+
const socket = createSocket(host, { url: 'ws://host' });
|
|
56
60
|
disposeSocket(socket);
|
|
57
61
|
expect(explainSocketSendFailure(socket)).toEqual({
|
|
58
62
|
reason: 'disposed',
|
package/src/socket.test.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
2
|
import type {
|
|
3
|
+
HasNetSocket,
|
|
3
4
|
SocketBackend,
|
|
4
5
|
SocketCloseInfo,
|
|
5
6
|
SocketConnection,
|
|
6
7
|
SocketEventSink,
|
|
7
8
|
SocketMessage,
|
|
9
|
+
TcpSocketConnection,
|
|
10
|
+
TcpSocketOptions,
|
|
8
11
|
} from '@flighthq/types/contract';
|
|
9
12
|
|
|
13
|
+
import { openTcpSocket } from './index';
|
|
10
14
|
import {
|
|
11
15
|
attachSocket,
|
|
12
16
|
closeSocket,
|
|
@@ -15,10 +19,9 @@ import {
|
|
|
15
19
|
detachSocket,
|
|
16
20
|
disposeSocket,
|
|
17
21
|
enableSocketSignals,
|
|
18
|
-
getSocketBackend,
|
|
19
22
|
getSocketReadyState,
|
|
23
|
+
initializeWebSocketBackend,
|
|
20
24
|
sendSocketMessage,
|
|
21
|
-
setSocketBackend,
|
|
22
25
|
setSocketGuard,
|
|
23
26
|
} from './socket';
|
|
24
27
|
|
|
@@ -65,15 +68,26 @@ function fakeBackend(): FakeSocket {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
afterEach(() => {
|
|
68
|
-
setSocketBackend(null);
|
|
69
71
|
setSocketGuard(null);
|
|
70
72
|
});
|
|
71
73
|
|
|
74
|
+
function hostOf(backend: SocketBackend): HasNetSocket {
|
|
75
|
+
return { net: { socket: backend } } as HasNetSocket;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tcpConnection(): TcpSocketConnection {
|
|
79
|
+
return {
|
|
80
|
+
readable: new ReadableStream<Uint8Array>(),
|
|
81
|
+
writable: new WritableStream<Uint8Array>(),
|
|
82
|
+
closeTcpSocketConnection(): void {},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
describe('attachSocket', () => {
|
|
73
87
|
it('resumes delivery after a detach', () => {
|
|
74
88
|
const fake = fakeBackend();
|
|
75
|
-
|
|
76
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
89
|
+
const host = hostOf(fake.backend);
|
|
90
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
77
91
|
const signals = enableSocketSignals(socket);
|
|
78
92
|
let opens = 0;
|
|
79
93
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -85,8 +99,8 @@ describe('attachSocket', () => {
|
|
|
85
99
|
|
|
86
100
|
it('cannot resume delivery after terminal disposal', () => {
|
|
87
101
|
const fake = fakeBackend();
|
|
88
|
-
|
|
89
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
102
|
+
const host = hostOf(fake.backend);
|
|
103
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
90
104
|
const signals = enableSocketSignals(socket);
|
|
91
105
|
let opens = 0;
|
|
92
106
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -101,8 +115,8 @@ describe('attachSocket', () => {
|
|
|
101
115
|
describe('closeSocket', () => {
|
|
102
116
|
it('transitions to closing and forwards code/reason to the connection', () => {
|
|
103
117
|
const fake = fakeBackend();
|
|
104
|
-
|
|
105
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
118
|
+
const host = hostOf(fake.backend);
|
|
119
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
106
120
|
fake.sink.handleSocketOpen();
|
|
107
121
|
closeSocket(socket, 1000, 'bye');
|
|
108
122
|
expect(getSocketReadyState(socket)).toBe('closing');
|
|
@@ -111,8 +125,8 @@ describe('closeSocket', () => {
|
|
|
111
125
|
|
|
112
126
|
it('reaches closed once the backend close event arrives', () => {
|
|
113
127
|
const fake = fakeBackend();
|
|
114
|
-
|
|
115
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
128
|
+
const host = hostOf(fake.backend);
|
|
129
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
116
130
|
fake.sink.handleSocketOpen();
|
|
117
131
|
closeSocket(socket);
|
|
118
132
|
fake.sink.handleSocketClose({ code: 1000, reason: '', wasClean: true });
|
|
@@ -121,8 +135,8 @@ describe('closeSocket', () => {
|
|
|
121
135
|
|
|
122
136
|
it('is a no-op when already closed', () => {
|
|
123
137
|
const fake = fakeBackend();
|
|
124
|
-
|
|
125
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
138
|
+
const host = hostOf(fake.backend);
|
|
139
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
126
140
|
fake.sink.handleSocketOpen();
|
|
127
141
|
closeSocket(socket);
|
|
128
142
|
fake.sink.handleSocketClose({ code: 1000, reason: '', wasClean: true });
|
|
@@ -132,8 +146,8 @@ describe('closeSocket', () => {
|
|
|
132
146
|
|
|
133
147
|
it('stays closed and does not close the connection again after disposal', () => {
|
|
134
148
|
const fake = fakeBackend();
|
|
135
|
-
|
|
136
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
149
|
+
const host = hostOf(fake.backend);
|
|
150
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
137
151
|
disposeSocket(socket);
|
|
138
152
|
closeSocket(socket);
|
|
139
153
|
expect(getSocketReadyState(socket)).toBe('closed');
|
|
@@ -144,8 +158,8 @@ describe('closeSocket', () => {
|
|
|
144
158
|
describe('createSocket', () => {
|
|
145
159
|
it('opens through the backend in the connecting state and records the url', () => {
|
|
146
160
|
const fake = fakeBackend();
|
|
147
|
-
|
|
148
|
-
const socket = createSocket({ url: 'ws://host/path' });
|
|
161
|
+
const host = hostOf(fake.backend);
|
|
162
|
+
const socket = createSocket(host, { url: 'ws://host/path' });
|
|
149
163
|
expect(socket.url).toBe('ws://host/path');
|
|
150
164
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
151
165
|
expect(fake.lastOptions?.url).toBe('ws://host/path');
|
|
@@ -153,8 +167,8 @@ describe('createSocket', () => {
|
|
|
153
167
|
|
|
154
168
|
it('passes protocols and binaryType through to the backend', () => {
|
|
155
169
|
const fake = fakeBackend();
|
|
156
|
-
|
|
157
|
-
createSocket({ url: 'ws://x', protocols: ['a', 'b'], binaryType: 'arraybuffer' });
|
|
170
|
+
const host = hostOf(fake.backend);
|
|
171
|
+
createSocket(host, { url: 'ws://x', protocols: ['a', 'b'], binaryType: 'arraybuffer' });
|
|
158
172
|
expect(fake.lastOptions?.protocols).toEqual(['a', 'b']);
|
|
159
173
|
expect(fake.lastOptions?.binaryType).toBe('arraybuffer');
|
|
160
174
|
});
|
|
@@ -162,16 +176,16 @@ describe('createSocket', () => {
|
|
|
162
176
|
it('tolerates a null connection from an unsupported transport', () => {
|
|
163
177
|
const fake = fakeBackend();
|
|
164
178
|
fake.openReturnsNull = true;
|
|
165
|
-
|
|
166
|
-
const socket = createSocket({ url: 'tcp://x' });
|
|
179
|
+
const host = hostOf(fake.backend);
|
|
180
|
+
const socket = createSocket(host, { url: 'tcp://x' });
|
|
167
181
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
168
182
|
expect(sendSocketMessage(socket, 'x')).toBe(false);
|
|
169
183
|
});
|
|
170
184
|
|
|
171
185
|
it('emits a text message with binary false and a binary message with binary true', () => {
|
|
172
186
|
const fake = fakeBackend();
|
|
173
|
-
|
|
174
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
187
|
+
const host = hostOf(fake.backend);
|
|
188
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
175
189
|
const signals = enableSocketSignals(socket);
|
|
176
190
|
const received: SocketMessage[] = [];
|
|
177
191
|
connectSignal(signals.onSocketMessage, (m) => received.push(m));
|
|
@@ -187,8 +201,8 @@ describe('createSocket', () => {
|
|
|
187
201
|
|
|
188
202
|
it('emits close info with code, reason, and wasClean', () => {
|
|
189
203
|
const fake = fakeBackend();
|
|
190
|
-
|
|
191
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
204
|
+
const host = hostOf(fake.backend);
|
|
205
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
192
206
|
const signals = enableSocketSignals(socket);
|
|
193
207
|
const infos: SocketCloseInfo[] = [];
|
|
194
208
|
connectSignal(signals.onSocketClose, (i) => infos.push(i));
|
|
@@ -198,8 +212,8 @@ describe('createSocket', () => {
|
|
|
198
212
|
|
|
199
213
|
it('emits onSocketError', () => {
|
|
200
214
|
const fake = fakeBackend();
|
|
201
|
-
|
|
202
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
215
|
+
const host = hostOf(fake.backend);
|
|
216
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
203
217
|
const signals = enableSocketSignals(socket);
|
|
204
218
|
let errors = 0;
|
|
205
219
|
connectSignal(signals.onSocketError, () => errors++);
|
|
@@ -302,8 +316,8 @@ describe('createWebSocketBackend', () => {
|
|
|
302
316
|
describe('detachSocket', () => {
|
|
303
317
|
it('stops backend events from reaching the signals', () => {
|
|
304
318
|
const fake = fakeBackend();
|
|
305
|
-
|
|
306
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
319
|
+
const host = hostOf(fake.backend);
|
|
320
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
307
321
|
const signals = enableSocketSignals(socket);
|
|
308
322
|
let opens = 0;
|
|
309
323
|
connectSignal(signals.onSocketOpen, () => opens++);
|
|
@@ -316,8 +330,8 @@ describe('detachSocket', () => {
|
|
|
316
330
|
describe('disposeSocket', () => {
|
|
317
331
|
it('closes an open connection and detaches so later events fire no signal', () => {
|
|
318
332
|
const fake = fakeBackend();
|
|
319
|
-
|
|
320
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
333
|
+
const host = hostOf(fake.backend);
|
|
334
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
321
335
|
const signals = enableSocketSignals(socket);
|
|
322
336
|
let messages = 0;
|
|
323
337
|
connectSignal(signals.onSocketMessage, () => messages++);
|
|
@@ -332,8 +346,8 @@ describe('disposeSocket', () => {
|
|
|
332
346
|
|
|
333
347
|
it('is safe to call on a fresh socket', () => {
|
|
334
348
|
const fake = fakeBackend();
|
|
335
|
-
|
|
336
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
349
|
+
const host = hostOf(fake.backend);
|
|
350
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
337
351
|
expect(() => disposeSocket(socket)).not.toThrow();
|
|
338
352
|
});
|
|
339
353
|
});
|
|
@@ -341,22 +355,22 @@ describe('disposeSocket', () => {
|
|
|
341
355
|
describe('enableSocketSignals', () => {
|
|
342
356
|
it('returns the same group on repeated calls', () => {
|
|
343
357
|
const fake = fakeBackend();
|
|
344
|
-
|
|
345
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
358
|
+
const host = hostOf(fake.backend);
|
|
359
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
346
360
|
expect(enableSocketSignals(socket)).toBe(enableSocketSignals(socket));
|
|
347
361
|
});
|
|
348
362
|
|
|
349
363
|
it('leaves a bare socket without signals', () => {
|
|
350
364
|
const fake = fakeBackend();
|
|
351
|
-
|
|
352
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
365
|
+
const host = hostOf(fake.backend);
|
|
366
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
353
367
|
expect(socket.runtime.signals).toBeNull();
|
|
354
368
|
});
|
|
355
369
|
|
|
356
370
|
it('returns an inert stable group without reviving a disposed socket', () => {
|
|
357
371
|
const fake = fakeBackend();
|
|
358
|
-
|
|
359
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
372
|
+
const host = hostOf(fake.backend);
|
|
373
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
360
374
|
disposeSocket(socket);
|
|
361
375
|
const first = enableSocketSignals(socket);
|
|
362
376
|
expect(enableSocketSignals(socket)).toBe(first);
|
|
@@ -365,23 +379,11 @@ describe('enableSocketSignals', () => {
|
|
|
365
379
|
});
|
|
366
380
|
});
|
|
367
381
|
|
|
368
|
-
describe('getSocketBackend', () => {
|
|
369
|
-
it('lazily returns a web backend by default', () => {
|
|
370
|
-
expect(typeof getSocketBackend().openSocket).toBe('function');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('returns the installed backend', () => {
|
|
374
|
-
const fake = fakeBackend();
|
|
375
|
-
setSocketBackend(fake.backend);
|
|
376
|
-
expect(getSocketBackend()).toBe(fake.backend);
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
382
|
describe('getSocketReadyState', () => {
|
|
381
383
|
it('reflects connecting → open → closing → closed transitions', () => {
|
|
382
384
|
const fake = fakeBackend();
|
|
383
|
-
|
|
384
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
385
|
+
const host = hostOf(fake.backend);
|
|
386
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
385
387
|
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
386
388
|
fake.sink.handleSocketOpen();
|
|
387
389
|
expect(getSocketReadyState(socket)).toBe('open');
|
|
@@ -392,11 +394,54 @@ describe('getSocketReadyState', () => {
|
|
|
392
394
|
});
|
|
393
395
|
});
|
|
394
396
|
|
|
397
|
+
describe('initializeWebSocketBackend', () => {
|
|
398
|
+
it('is the construction initializer of createWebSocketBackend', () => {
|
|
399
|
+
expect(typeof initializeWebSocketBackend).toBe('function');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('openTcpSocket', () => {
|
|
404
|
+
it('forwards the endpoint unchanged and returns the backend connection by identity', () => {
|
|
405
|
+
const expected = tcpConnection();
|
|
406
|
+
const options: TcpSocketOptions = { host: 'db.internal', port: 5432 };
|
|
407
|
+
const openTcpSocketBackend = vi.fn(() => expected);
|
|
408
|
+
const backend: SocketBackend = {
|
|
409
|
+
openSocket: vi.fn(() => null),
|
|
410
|
+
openTcpSocket: openTcpSocketBackend,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
expect(openTcpSocket(hostOf(backend), options)).toBe(expected);
|
|
414
|
+
expect(openTcpSocketBackend).toHaveBeenCalledWith(options);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('returns null when the host carries no socket backend', () => {
|
|
418
|
+
const host = { net: {} } as HasNetSocket;
|
|
419
|
+
expect(openTcpSocket(host, { host: 'localhost', port: 9000 })).toBeNull();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('returns null for the web backend without attempting framed openSocket', () => {
|
|
423
|
+
const backend = createWebSocketBackend();
|
|
424
|
+
const openSocket = vi.spyOn(backend, 'openSocket');
|
|
425
|
+
|
|
426
|
+
expect(openTcpSocket(hostOf(backend), { host: 'localhost', port: 9000 })).toBeNull();
|
|
427
|
+
expect(openSocket).not.toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('preserves an explicit null from a backend that cannot open the endpoint', () => {
|
|
431
|
+
const backend: SocketBackend = {
|
|
432
|
+
openSocket: vi.fn(() => null),
|
|
433
|
+
openTcpSocket: vi.fn(() => null),
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
expect(openTcpSocket(hostOf(backend), { host: 'unreachable.internal', port: 22 })).toBeNull();
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
395
440
|
describe('sendSocketMessage', () => {
|
|
396
441
|
it('sends through the connection and returns true when open', () => {
|
|
397
442
|
const fake = fakeBackend();
|
|
398
|
-
|
|
399
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
443
|
+
const host = hostOf(fake.backend);
|
|
444
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
400
445
|
fake.sink.handleSocketOpen();
|
|
401
446
|
expect(sendSocketMessage(socket, 'ping')).toBe(true);
|
|
402
447
|
expect(fake.sent).toEqual(['ping']);
|
|
@@ -404,16 +449,16 @@ describe('sendSocketMessage', () => {
|
|
|
404
449
|
|
|
405
450
|
it('returns false without throwing when not open', () => {
|
|
406
451
|
const fake = fakeBackend();
|
|
407
|
-
|
|
408
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
452
|
+
const host = hostOf(fake.backend);
|
|
453
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
409
454
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
410
455
|
expect(fake.sent).toEqual([]);
|
|
411
456
|
});
|
|
412
457
|
|
|
413
458
|
it('returns false after disposal without reaching the released connection', () => {
|
|
414
459
|
const fake = fakeBackend();
|
|
415
|
-
|
|
416
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
460
|
+
const host = hostOf(fake.backend);
|
|
461
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
417
462
|
fake.sink.handleSocketOpen();
|
|
418
463
|
disposeSocket(socket);
|
|
419
464
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
@@ -423,8 +468,8 @@ describe('sendSocketMessage', () => {
|
|
|
423
468
|
it('returns false for an open state whose backend supplied no connection', () => {
|
|
424
469
|
const fake = fakeBackend();
|
|
425
470
|
fake.openReturnsNull = true;
|
|
426
|
-
|
|
427
|
-
const socket = createSocket({ url: 'tcp://x' });
|
|
471
|
+
const host = hostOf(fake.backend);
|
|
472
|
+
const socket = createSocket(host, { url: 'tcp://x' });
|
|
428
473
|
fake.sink.handleSocketOpen();
|
|
429
474
|
expect(getSocketReadyState(socket)).toBe('open');
|
|
430
475
|
expect(sendSocketMessage(socket, 'ping')).toBe(false);
|
|
@@ -433,8 +478,8 @@ describe('sendSocketMessage', () => {
|
|
|
433
478
|
it('forwards binary data by identity and propagates false without mutating the readonly socket', () => {
|
|
434
479
|
const fake = fakeBackend();
|
|
435
480
|
fake.sendReturns = false;
|
|
436
|
-
|
|
437
|
-
const socket = createSocket({ url: 'ws://x' });
|
|
481
|
+
const host = hostOf(fake.backend);
|
|
482
|
+
const socket = createSocket(host, { url: 'ws://x' });
|
|
438
483
|
fake.sink.handleSocketOpen();
|
|
439
484
|
Object.freeze(socket.runtime);
|
|
440
485
|
Object.freeze(socket);
|
|
@@ -444,46 +489,6 @@ describe('sendSocketMessage', () => {
|
|
|
444
489
|
});
|
|
445
490
|
});
|
|
446
491
|
|
|
447
|
-
describe('setSocketBackend', () => {
|
|
448
|
-
it('restores the lazy web default when passed null', () => {
|
|
449
|
-
const fake = fakeBackend();
|
|
450
|
-
setSocketBackend(fake.backend);
|
|
451
|
-
expect(getSocketBackend()).toBe(fake.backend);
|
|
452
|
-
setSocketBackend(null);
|
|
453
|
-
const web = getSocketBackend();
|
|
454
|
-
expect(web).not.toBe(fake.backend);
|
|
455
|
-
expect(typeof web.openSocket).toBe('function');
|
|
456
|
-
});
|
|
457
|
-
});
|
|
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
|
-
|
|
487
492
|
function noopSink(): SocketEventSink {
|
|
488
493
|
return {
|
|
489
494
|
handleSocketOpen() {},
|
|
@@ -540,3 +545,29 @@ function installFakeWebSocket(): () => void {
|
|
|
540
545
|
(globalThis as { WebSocket?: unknown }).WebSocket = original;
|
|
541
546
|
};
|
|
542
547
|
}
|
|
548
|
+
describe('setSocketGuard', () => {
|
|
549
|
+
it('installs and removes the core notice hook', () => {
|
|
550
|
+
const notices: string[] = [];
|
|
551
|
+
setSocketGuard((notice) => notices.push(`${notice.operation}:${notice.reason}`));
|
|
552
|
+
const unsupported = fakeBackend();
|
|
553
|
+
unsupported.openReturnsNull = true;
|
|
554
|
+
const host = hostOf(unsupported.backend);
|
|
555
|
+
createSocket(host, { url: 'tcp://x' });
|
|
556
|
+
|
|
557
|
+
const supported = fakeBackend();
|
|
558
|
+
const socket = createSocket(hostOf(supported.backend), { url: 'ws://x' });
|
|
559
|
+
disposeSocket(socket);
|
|
560
|
+
closeSocket(socket);
|
|
561
|
+
sendSocketMessage(socket, 'x');
|
|
562
|
+
enableSocketSignals(socket);
|
|
563
|
+
setSocketGuard(null);
|
|
564
|
+
closeSocket(socket);
|
|
565
|
+
|
|
566
|
+
expect(notices).toEqual([
|
|
567
|
+
'createSocket:no-connection',
|
|
568
|
+
'closeSocket:disposed',
|
|
569
|
+
'sendSocketMessage:disposed',
|
|
570
|
+
'enableSocketSignals:disposed',
|
|
571
|
+
]);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HasNetSocket, SocketBackend, SocketConnection, SocketEventSink } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { createSocket, getSocketReadyState } from './socket';
|
|
4
|
+
|
|
5
|
+
function recordingBackend(label: string, opened: string[]): SocketBackend {
|
|
6
|
+
return {
|
|
7
|
+
openSocket(_options, events: SocketEventSink): SocketConnection | null {
|
|
8
|
+
opened.push(label);
|
|
9
|
+
return {
|
|
10
|
+
closeSocketConnection(): void {},
|
|
11
|
+
sendSocketFrame(): boolean {
|
|
12
|
+
events.handleSocketOpen();
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hostWith(backend: SocketBackend | undefined): HasNetSocket {
|
|
21
|
+
return { net: backend === undefined ? {} : { socket: backend } } as HasNetSocket;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('createSocket', () => {
|
|
25
|
+
it('opens through the provider carried by the host it is given', () => {
|
|
26
|
+
const opened: string[] = [];
|
|
27
|
+
createSocket(hostWith(recordingBackend('a', opened)), { url: 'wss://example.test' });
|
|
28
|
+
expect(opened).toEqual(['a']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ★ HOST ISOLATION. Two hosts, two providers, two answers. Under the ambient resolver one
|
|
32
|
+
// process-wide backend answered for every caller, so the second host was unreachable no matter what
|
|
33
|
+
// it carried — this is the property the migration exists to create.
|
|
34
|
+
it('keeps two hosts independent', () => {
|
|
35
|
+
const opened: string[] = [];
|
|
36
|
+
createSocket(hostWith(recordingBackend('first', opened)), { url: 'wss://one.test' });
|
|
37
|
+
createSocket(hostWith(recordingBackend('second', opened)), { url: 'wss://two.test' });
|
|
38
|
+
expect(opened).toEqual(['first', 'second']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ★ NO AMBIENT FALLBACK. A host that carries no socket provider must yield no connection rather than
|
|
42
|
+
// quietly reaching a process-global web backend. The socket stays 'connecting', which is the
|
|
43
|
+
// documented shape for a backend that cannot open the transport.
|
|
44
|
+
it('yields no connection when the host carries no socket provider', () => {
|
|
45
|
+
const socket = createSocket(hostWith(undefined), { url: 'wss://absent.test' });
|
|
46
|
+
expect(socket.runtime.connection).toBeNull();
|
|
47
|
+
expect(getSocketReadyState(socket)).toBe('connecting');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not consult one host provider when another host is passed', () => {
|
|
51
|
+
const opened: string[] = [];
|
|
52
|
+
const unused = recordingBackend('unused', opened);
|
|
53
|
+
createSocket(hostWith(recordingBackend('used', opened)), { url: 'wss://x.test' });
|
|
54
|
+
expect(opened).not.toContain('unused');
|
|
55
|
+
expect(unused).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
});
|