@mcpwarp/ws-mixer 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +357 -0
- package/LICENSE +202 -0
- package/README.md +431 -0
- package/dist/client.d.ts +453 -0
- package/dist/conn.d.ts +328 -0
- package/dist/control.d.ts +74 -0
- package/dist/errors.d.ts +96 -0
- package/dist/frame.d.ts +52 -0
- package/dist/index.cjs +2866 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2806 -0
- package/dist/index.js.map +1 -0
- package/dist/stream.d.ts +124 -0
- package/dist/util.d.ts +3 -0
- package/package.json +59 -0
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MixerStream: one ws-mixer byte stream, exposed as a Node Duplex.
|
|
3
|
+
* Mirrors `go/wsmixer/stream.go`'s state machine, credit accounting and
|
|
4
|
+
* half-close semantics (WIRE.md section 2.5-2.6). The JS SDK is always
|
|
5
|
+
* the answering peer: it never opens streams, only receives OPEN from the
|
|
6
|
+
* server and reads/writes/closes/resets what arrives.
|
|
7
|
+
*
|
|
8
|
+
* Three ways a stream ends in RESET, all converging on the same terminal
|
|
9
|
+
* state and `resetCode`, but differing in whether `'reset'` fires:
|
|
10
|
+
* - the peer sends RESET (handleReset()) -- `'reset'` fires; the stream's
|
|
11
|
+
* owner didn't choose this.
|
|
12
|
+
* - the SDK itself detects a violation on this stream (abort(), called from
|
|
13
|
+
* MixerConn's dispatch loop) -- `'reset'` fires, same reasoning as above:
|
|
14
|
+
* the app didn't choose it either.
|
|
15
|
+
* - the app calls the public reset() -- `'reset'` does NOT fire; the caller
|
|
16
|
+
* already knows why, by definition.
|
|
17
|
+
* `resetCode` is set by all three, always, whether or not an `'error'`
|
|
18
|
+
* listener is attached.
|
|
19
|
+
*/
|
|
20
|
+
import { Duplex } from "node:stream";
|
|
21
|
+
export type StreamState = "idle" | "open" | "half_closed_local" | "half_closed_remote" | "closed";
|
|
22
|
+
/** Payload of the `'reset'` event: fired once, whenever this stream is RESET by something other than the app's own reset() call -- a peer RESET (handleReset()) or an SDK-detected violation (abort()) -- whether or not an `'error'` listener is attached. */
|
|
23
|
+
export interface StreamResetInfo {
|
|
24
|
+
code: number;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
/** Callback the stream uses to hand an outbound DATA/WINDOW/CLOSE/RESET frame to the connection. */
|
|
28
|
+
export interface StreamHost {
|
|
29
|
+
/** Enqueue a DATA chunk for round-robin transmission; resolves once written (or rejects on failure). */
|
|
30
|
+
sendData(streamId: number, chunk: Uint8Array): Promise<void>;
|
|
31
|
+
/** Send a control-priority frame (WINDOW/CLOSE/RESET) immediately. */
|
|
32
|
+
sendControlFrame(frame: Uint8Array): void;
|
|
33
|
+
/** Called when the stream is fully closed so the connection can retire its id. */
|
|
34
|
+
retireStream(streamId: number): void;
|
|
35
|
+
}
|
|
36
|
+
export declare class MixerStream extends Duplex {
|
|
37
|
+
readonly id: number;
|
|
38
|
+
private readonly host;
|
|
39
|
+
private state;
|
|
40
|
+
private sendWindow;
|
|
41
|
+
private recvWindow;
|
|
42
|
+
private readonly initialWindow;
|
|
43
|
+
private unacked;
|
|
44
|
+
private remoteClosed;
|
|
45
|
+
private terminalError;
|
|
46
|
+
/** Resolved when sendWindow becomes > 0 or the stream can no longer send. */
|
|
47
|
+
private sendWaiters;
|
|
48
|
+
/** Bytes queued via _write, waiting on credit; drives backpressure. */
|
|
49
|
+
private writeQueue;
|
|
50
|
+
private draining;
|
|
51
|
+
/** Write callbacks (_write's own terminal-error branch, or drainWriteQueue's catch) whose error must wait until the read side has finished delivering buffered data and emitted 'end' -- see terminateNoThrow's doc comment. Non-null exactly while that wait is pending; flushed (and reset to null) from _destroy(), or from the 'end' listener drainWriteQueue's catch installs. */
|
|
52
|
+
private heldWriteCallbacks;
|
|
53
|
+
/** Set once this stream is RESET -- by the peer (handleReset), the SDK (abort()), or the app itself (reset()); undefined until then. Set even when no `'error'` listener is attached. */
|
|
54
|
+
resetCode?: number;
|
|
55
|
+
constructor(id: number, host: StreamHost, initialRecvWindow: number, initialSendWindow: number);
|
|
56
|
+
getState(): StreamState;
|
|
57
|
+
getSendWindow(): number;
|
|
58
|
+
getRecvWindow(): number;
|
|
59
|
+
_read(_size: number): void;
|
|
60
|
+
_write(chunk: Uint8Array, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
|
|
61
|
+
_final(callback: (error?: Error | null) => void): void;
|
|
62
|
+
_destroy(err: Error | null, callback: (error?: Error | null) => void): void;
|
|
63
|
+
private pumpWriteQueue;
|
|
64
|
+
private drainWriteQueue;
|
|
65
|
+
/** Waits until send credit is available, then reserves and returns min(want, sendWindow, MAX_CHUNK). */
|
|
66
|
+
private reserveSendCredit;
|
|
67
|
+
private wakeSendWaiters;
|
|
68
|
+
/**
|
|
69
|
+
* Credits the peer back once bytes leave the internal buffer, mirroring Go
|
|
70
|
+
* `Stream.creditConsumed` (stream.go) and its half-window threshold.
|
|
71
|
+
*/
|
|
72
|
+
private creditConsumed;
|
|
73
|
+
private eofDelivered;
|
|
74
|
+
private internalBufferEmpty;
|
|
75
|
+
/**
|
|
76
|
+
* Covers every consumption mode that goes through the buffer: explicit
|
|
77
|
+
* paused-mode `.read()` calls, and Node's internal flow loop -- which
|
|
78
|
+
* calls this same public `read()` method to back `on('data')`, `pipe()`
|
|
79
|
+
* and `for await` once bytes are actually sitting in the buffer (as
|
|
80
|
+
* opposed to handleData()'s synchronous fast-path bypass above, which
|
|
81
|
+
* never reaches the buffer at all). Between the two, every byte is
|
|
82
|
+
* credited exactly once: whatever left the buffer here, plus whatever
|
|
83
|
+
* never entered it there.
|
|
84
|
+
*/
|
|
85
|
+
read(size?: number): unknown;
|
|
86
|
+
private maybeDeliverEOF;
|
|
87
|
+
/** True once something is listening for `'error'`: gates attaching the internal no-op `'error'` listener (terminateNoThrow, drainWriteQueue). */
|
|
88
|
+
private hasErrorListener;
|
|
89
|
+
/** Sends CLOSE: "I will send no more DATA on this stream." end() is sugar for this via _final. */
|
|
90
|
+
closeWrite(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Sends CLOSE (if not already sent) and stops delivering further reads. If
|
|
93
|
+
* the peer has not yet half-closed its own send side, a plain closeWrite()
|
|
94
|
+
* would leave it writing into a window nobody drains, so close() also
|
|
95
|
+
* RESETs with CANCEL in that case (mirrors go/wsmixer/stream.go Close()).
|
|
96
|
+
*/
|
|
97
|
+
close(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Aborts the stream in both directions with the given error code and
|
|
100
|
+
* message, discarding buffered data. `code` may be a numeric ws-mixer.v1
|
|
101
|
+
* error code or its wire name (e.g. `"CANCEL"`); `message` is truncated to
|
|
102
|
+
* 256 UTF-8 bytes on a character boundary (WIRE.md section 2.3: a RESET
|
|
103
|
+
* message SHOULD be <= 256 B).
|
|
104
|
+
*
|
|
105
|
+
* Same "don't throw with nobody listening" rule as handleReset(): an
|
|
106
|
+
* internal no-op 'error' listener (attached by terminateNoThrow() when
|
|
107
|
+
* nothing else is) keeps this from crashing a consumer with no 'error' of
|
|
108
|
+
* its own, while still surfacing the code via `resetCode`/`stream.errored`
|
|
109
|
+
* (buffered data discarded, per RESET's own semantics: terminateNoThrow()
|
|
110
|
+
* is called with `{discardBuffered: true}`).
|
|
111
|
+
*
|
|
112
|
+
* This is the app-facing API: calling code already knows why it's
|
|
113
|
+
* resetting its own stream, so unlike a peer-sent RESET (handleReset()) or
|
|
114
|
+
* an SDK-detected violation (abort()), `reset()` does NOT emit `'reset'`.
|
|
115
|
+
*/
|
|
116
|
+
reset(code: number | string, message?: string): void;
|
|
117
|
+
/** Shared implementation behind reset()/abort(); returns false (no-op) if the stream was already closed. */
|
|
118
|
+
private applyReset;
|
|
119
|
+
/**
|
|
120
|
+
* Wraps this stream as Web Streams API `{ readable, writable }`, e.g. for
|
|
121
|
+
* `fetch`'s `duplex` option or any other Web Streams consumer.
|
|
122
|
+
*/
|
|
123
|
+
toWeb(): ReturnType<typeof Duplex.toWeb>;
|
|
124
|
+
}
|
package/dist/util.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcpwarp/ws-mixer",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org/",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"description": "ws-mixer.v1 client SDK: N byte streams over one WebSocket, JS/TypeScript client.",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/mcpwarp/ws-mixer-js.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/mcpwarp/ws-mixer-js#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/mcpwarp/ws-mixer-js/issues"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"CHANGELOG.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"prepublishOnly": "node scripts/check-registry.mjs && npm run build",
|
|
40
|
+
"build": "tsup && npm run build:types",
|
|
41
|
+
"build:types": "tsc src/index.ts --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --declaration --emitDeclarationOnly --stripInternal --esModuleInterop --skipLibCheck --outDir dist --rootDir src",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"fetch-spec": "node scripts/fetch-spec.mjs",
|
|
46
|
+
"fetch-goserver": "node scripts/fetch-goserver.mjs"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"ws": "^8.18.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.0",
|
|
53
|
+
"@types/ws": "^8.5.13",
|
|
54
|
+
"ajv": "^8.20.0",
|
|
55
|
+
"tsup": "^8.3.5",
|
|
56
|
+
"typescript": "^5.7.2",
|
|
57
|
+
"vitest": "^2.1.8"
|
|
58
|
+
}
|
|
59
|
+
}
|