@floegence/flowersec-core 0.20.2 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client-connect/connectCore.d.ts +1 -1
- package/dist/proxy/controllerWindow.js +23 -3
- package/dist/proxy/preset.d.ts +0 -1
- package/dist/proxy/preset.js +0 -13
- package/dist/proxy/profiles.d.ts +1 -8
- package/dist/proxy/profiles.js +1 -7
- package/dist/proxy/runtime.d.ts +1 -0
- package/dist/proxy/runtime.js +38 -1
- package/dist/proxy/serviceWorker.js +23 -5
- package/dist/proxy/windowBridgeProtocol.d.ts +1 -0
- package/dist/reconnect/index.js +23 -13
- package/dist/yamux/session.d.ts +6 -1
- package/dist/yamux/session.js +5 -0
- package/dist/yamux/stream.d.ts +1 -0
- package/dist/yamux/stream.js +19 -4
- package/package.json +1 -1
|
@@ -31,7 +31,7 @@ export type ConnectOptionsBase = Readonly<{
|
|
|
31
31
|
outboundRecordChunkBytes?: number;
|
|
32
32
|
/** WebSocket inbound and outbound queue limits. */
|
|
33
33
|
webSocketLimits?: Partial<WebSocketLimits>;
|
|
34
|
-
/** Yamux stream, frame,
|
|
34
|
+
/** Yamux stream, frame, receive-memory, and per-stream write-queue limits. */
|
|
35
35
|
yamuxLimits?: Partial<YamuxLimits>;
|
|
36
36
|
/** Optional factory for creating the WebSocket instance. */
|
|
37
37
|
wsFactory?: (url: string, origin: string) => WebSocketLike;
|
|
@@ -39,6 +39,23 @@ function hasExpectedCapability(data, capabilityNonce) {
|
|
|
39
39
|
return false;
|
|
40
40
|
return data.capabilityNonce === capabilityNonce;
|
|
41
41
|
}
|
|
42
|
+
function withTrustedExternalOrigin(req, rawOrigin) {
|
|
43
|
+
let externalOrigin;
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(rawOrigin);
|
|
46
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
47
|
+
externalOrigin = url.origin;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Opaque and invalid origins must not become upstream authority metadata.
|
|
52
|
+
}
|
|
53
|
+
const sanitized = { ...req };
|
|
54
|
+
delete sanitized.external_origin;
|
|
55
|
+
return externalOrigin === undefined
|
|
56
|
+
? sanitized
|
|
57
|
+
: { ...sanitized, external_origin: externalOrigin };
|
|
58
|
+
}
|
|
42
59
|
function bridgeWebSocket(runtime, msg, port) {
|
|
43
60
|
const ac = new AbortController();
|
|
44
61
|
let terminal = false;
|
|
@@ -191,7 +208,8 @@ export function registerProxyControllerWindow(opts) {
|
|
|
191
208
|
throw new Error("targetWindow is not available");
|
|
192
209
|
}
|
|
193
210
|
const onMessage = (ev) => {
|
|
194
|
-
|
|
211
|
+
const eventOrigin = String(ev.origin ?? "").trim();
|
|
212
|
+
if (!allowedOrigins.includes(eventOrigin))
|
|
195
213
|
return;
|
|
196
214
|
if (opts.expectedSource != null && ev.source !== opts.expectedSource)
|
|
197
215
|
return;
|
|
@@ -205,9 +223,11 @@ export function registerProxyControllerWindow(opts) {
|
|
|
205
223
|
if (!port)
|
|
206
224
|
return;
|
|
207
225
|
switch (type) {
|
|
208
|
-
case PROXY_WINDOW_FETCH_MSG_TYPE:
|
|
209
|
-
|
|
226
|
+
case PROXY_WINDOW_FETCH_MSG_TYPE: {
|
|
227
|
+
const msg = data;
|
|
228
|
+
opts.runtime.dispatchFetch(withTrustedExternalOrigin(msg.req, eventOrigin), port);
|
|
210
229
|
return;
|
|
230
|
+
}
|
|
211
231
|
case PROXY_WINDOW_WS_OPEN_MSG_TYPE:
|
|
212
232
|
bridgeWebSocket(opts.runtime, data, port);
|
|
213
233
|
return;
|
package/dist/proxy/preset.d.ts
CHANGED
|
@@ -24,7 +24,6 @@ export type ResolvedProxyPreset = Readonly<{
|
|
|
24
24
|
}>;
|
|
25
25
|
}>;
|
|
26
26
|
export declare const DEFAULT_PROXY_PRESET_MANIFEST: ProxyPresetManifest;
|
|
27
|
-
export declare const CODESERVER_PROXY_PRESET_MANIFEST: ProxyPresetManifest;
|
|
28
27
|
export type ProxyPresetInput = ProxyPresetManifest | Partial<ProxyPresetLimits>;
|
|
29
28
|
export declare function assertProxyPresetManifest(value: unknown): ProxyPresetManifest;
|
|
30
29
|
export declare function resolveNamedProxyPreset(name: string): ProxyPresetManifest;
|
package/dist/proxy/preset.js
CHANGED
|
@@ -17,17 +17,6 @@ export const DEFAULT_PROXY_PRESET_MANIFEST = Object.freeze({
|
|
|
17
17
|
max_ws_frame_bytes: DEFAULT_MAX_WS_FRAME_BYTES,
|
|
18
18
|
},
|
|
19
19
|
});
|
|
20
|
-
export const CODESERVER_PROXY_PRESET_MANIFEST = Object.freeze({
|
|
21
|
-
v: 1,
|
|
22
|
-
preset_id: "codeserver",
|
|
23
|
-
deprecated: true,
|
|
24
|
-
limits: {
|
|
25
|
-
max_json_frame_bytes: DEFAULT_MAX_JSON_FRAME_BYTES,
|
|
26
|
-
max_chunk_bytes: DEFAULT_MAX_CHUNK_BYTES,
|
|
27
|
-
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
|
|
28
|
-
max_ws_frame_bytes: 32 * 1024 * 1024,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
20
|
function isRecord(v) {
|
|
32
21
|
return typeof v === "object" && v != null && !Array.isArray(v);
|
|
33
22
|
}
|
|
@@ -91,8 +80,6 @@ export function resolveNamedProxyPreset(name) {
|
|
|
91
80
|
case "":
|
|
92
81
|
case "default":
|
|
93
82
|
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
94
|
-
case "codeserver":
|
|
95
|
-
return CODESERVER_PROXY_PRESET_MANIFEST;
|
|
96
83
|
default:
|
|
97
84
|
throw new Error(`unknown proxy preset: ${name}`);
|
|
98
85
|
}
|
package/dist/proxy/profiles.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type ProxyProfile = Readonly<{
|
|
|
6
6
|
maxWsFrameBytes: number;
|
|
7
7
|
timeoutMs: number;
|
|
8
8
|
}>;
|
|
9
|
-
export type ProxyProfileName = "default"
|
|
9
|
+
export type ProxyProfileName = "default";
|
|
10
10
|
export declare const PROXY_PROFILE_DEFAULT: Readonly<{
|
|
11
11
|
maxJsonFrameBytes: number;
|
|
12
12
|
maxChunkBytes: number;
|
|
@@ -14,12 +14,5 @@ export declare const PROXY_PROFILE_DEFAULT: Readonly<{
|
|
|
14
14
|
maxWsFrameBytes: number;
|
|
15
15
|
timeoutMs: number;
|
|
16
16
|
}>;
|
|
17
|
-
export declare const PROXY_PROFILE_CODESERVER: Readonly<{
|
|
18
|
-
maxJsonFrameBytes: number;
|
|
19
|
-
maxChunkBytes: number;
|
|
20
|
-
maxBodyBytes: number;
|
|
21
|
-
maxWsFrameBytes: number;
|
|
22
|
-
timeoutMs: number;
|
|
23
|
-
}>;
|
|
24
17
|
export declare function resolveProxyProfile(profile?: ProxyProfileName | Partial<ProxyProfile>): ProxyProfile;
|
|
25
18
|
export declare function profileToPresetManifest(profile?: ProxyProfileName | Partial<ProxyProfile>): ProxyPresetManifest;
|
package/dist/proxy/profiles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_PROXY_PRESET_MANIFEST, resolveProxyPreset, } from "./preset.js";
|
|
2
2
|
function toLegacyProfile(manifest) {
|
|
3
3
|
const resolved = resolveProxyPreset(manifest);
|
|
4
4
|
return Object.freeze({
|
|
@@ -10,9 +10,7 @@ function toLegacyProfile(manifest) {
|
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
12
|
const DEFAULT_PROFILE = toLegacyProfile(DEFAULT_PROXY_PRESET_MANIFEST);
|
|
13
|
-
const CODESERVER_PROFILE = toLegacyProfile(CODESERVER_PROXY_PRESET_MANIFEST);
|
|
14
13
|
export const PROXY_PROFILE_DEFAULT = DEFAULT_PROFILE;
|
|
15
|
-
export const PROXY_PROFILE_CODESERVER = CODESERVER_PROFILE;
|
|
16
14
|
function normalizeSafeInt(name, value) {
|
|
17
15
|
if (!Number.isFinite(value))
|
|
18
16
|
throw new Error(`${name} must be a finite number`);
|
|
@@ -27,8 +25,6 @@ function resolveNamedProfile(name) {
|
|
|
27
25
|
switch (name) {
|
|
28
26
|
case "default":
|
|
29
27
|
return DEFAULT_PROFILE;
|
|
30
|
-
case "codeserver":
|
|
31
|
-
return CODESERVER_PROFILE;
|
|
32
28
|
default:
|
|
33
29
|
throw new Error(`unknown proxy profile: ${name}`);
|
|
34
30
|
}
|
|
@@ -55,8 +51,6 @@ export function profileToPresetManifest(profile) {
|
|
|
55
51
|
switch (profile) {
|
|
56
52
|
case "default":
|
|
57
53
|
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
58
|
-
case "codeserver":
|
|
59
|
-
return CODESERVER_PROXY_PRESET_MANIFEST;
|
|
60
54
|
default:
|
|
61
55
|
throw new Error(`unknown proxy profile: ${profile}`);
|
|
62
56
|
}
|
package/dist/proxy/runtime.d.ts
CHANGED
package/dist/proxy/runtime.js
CHANGED
|
@@ -345,11 +345,44 @@ export function createProxyRuntime(opts) {
|
|
|
345
345
|
const ac = new AbortController();
|
|
346
346
|
let stream = null;
|
|
347
347
|
let releaseAdmission = null;
|
|
348
|
+
const usesResponseCredits = req.response_flow_control === "chunk_credit_v1";
|
|
349
|
+
let responseCreditAvailable = false;
|
|
350
|
+
let responseCreditResolve = null;
|
|
351
|
+
const wakeResponseCreditWaiter = () => {
|
|
352
|
+
const resolve = responseCreditResolve;
|
|
353
|
+
responseCreditResolve = null;
|
|
354
|
+
resolve?.();
|
|
355
|
+
};
|
|
348
356
|
port.onmessage = (ev) => {
|
|
349
357
|
const m = ev.data;
|
|
350
|
-
if (m
|
|
358
|
+
if (!m || typeof m !== "object")
|
|
359
|
+
return;
|
|
360
|
+
if (m.type === "flowersec-proxy:abort") {
|
|
361
|
+
responseCreditAvailable = false;
|
|
351
362
|
ac.abort("aborted");
|
|
363
|
+
wakeResponseCreditWaiter();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (usesResponseCredits && m.type === "flowersec-proxy:response_credit") {
|
|
367
|
+
responseCreditAvailable = true;
|
|
368
|
+
wakeResponseCreditWaiter();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const waitForResponseCredit = async () => {
|
|
372
|
+
if (!usesResponseCredits)
|
|
373
|
+
return;
|
|
374
|
+
if (ac.signal.aborted)
|
|
375
|
+
throw new AbortError("aborted");
|
|
376
|
+
while (!responseCreditAvailable) {
|
|
377
|
+
if (ac.signal.aborted)
|
|
378
|
+
throw new AbortError("aborted");
|
|
379
|
+
await new Promise((resolve) => {
|
|
380
|
+
responseCreditResolve = resolve;
|
|
381
|
+
});
|
|
352
382
|
}
|
|
383
|
+
if (ac.signal.aborted)
|
|
384
|
+
throw new AbortError("aborted");
|
|
385
|
+
responseCreditAvailable = false;
|
|
353
386
|
};
|
|
354
387
|
void (async () => {
|
|
355
388
|
try {
|
|
@@ -400,6 +433,9 @@ export function createProxyRuntime(opts) {
|
|
|
400
433
|
port.postMessage({ type: "flowersec-proxy:response_meta", status, headers: passthrough });
|
|
401
434
|
const chunks = await readChunkFrames(reader, maxChunkBytes, maxBodyBytes);
|
|
402
435
|
for await (const chunk of chunks) {
|
|
436
|
+
await waitForResponseCredit();
|
|
437
|
+
if (ac.signal.aborted)
|
|
438
|
+
throw new AbortError("aborted");
|
|
403
439
|
// Always transfer an ArrayBuffer (SharedArrayBuffer is not transferable).
|
|
404
440
|
const ab = chunk.slice().buffer;
|
|
405
441
|
port.postMessage({ type: "flowersec-proxy:response_chunk", data: ab }, [ab]);
|
|
@@ -429,6 +465,7 @@ export function createProxyRuntime(opts) {
|
|
|
429
465
|
}
|
|
430
466
|
}
|
|
431
467
|
finally {
|
|
468
|
+
wakeResponseCreditWaiter();
|
|
432
469
|
releaseAdmission?.();
|
|
433
470
|
try {
|
|
434
471
|
port.close();
|
|
@@ -472,12 +472,25 @@ async function handleFetch(event) {
|
|
|
472
472
|
const donePromise = new Promise((resolve, reject) => { doneResolve = resolve; doneReject = reject; });
|
|
473
473
|
|
|
474
474
|
let controller = null;
|
|
475
|
+
let responseCreditOutstanding = false;
|
|
476
|
+
|
|
477
|
+
function requestResponseCredit() {
|
|
478
|
+
if (responseCreditOutstanding) return;
|
|
479
|
+
responseCreditOutstanding = true;
|
|
480
|
+
try { port.postMessage({ type: "flowersec-proxy:response_credit" }); } catch {}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function abortRuntimeResponse() {
|
|
484
|
+
responseCreditOutstanding = false;
|
|
485
|
+
try { port.postMessage({ type: "flowersec-proxy:abort" }); } catch {}
|
|
486
|
+
try { port.close(); } catch {}
|
|
487
|
+
}
|
|
475
488
|
|
|
476
489
|
const stream = new ReadableStream({
|
|
477
490
|
start(c) { controller = c; },
|
|
491
|
+
pull() { requestResponseCredit(); },
|
|
478
492
|
cancel() {
|
|
479
|
-
|
|
480
|
-
try { port.close(); } catch {}
|
|
493
|
+
abortRuntimeResponse();
|
|
481
494
|
}
|
|
482
495
|
});
|
|
483
496
|
|
|
@@ -490,7 +503,7 @@ async function handleFetch(event) {
|
|
|
490
503
|
metaReject(err);
|
|
491
504
|
doneReject(err);
|
|
492
505
|
controller?.error(err);
|
|
493
|
-
|
|
506
|
+
abortRuntimeResponse();
|
|
494
507
|
return false;
|
|
495
508
|
}
|
|
496
509
|
htmlChunks.push(b);
|
|
@@ -516,27 +529,30 @@ async function handleFetch(event) {
|
|
|
516
529
|
metaReject(err);
|
|
517
530
|
doneReject(err);
|
|
518
531
|
controller?.error(err);
|
|
519
|
-
|
|
532
|
+
abortRuntimeResponse();
|
|
520
533
|
return;
|
|
521
534
|
}
|
|
522
535
|
}
|
|
523
536
|
}
|
|
524
537
|
metaResolve(m);
|
|
538
|
+
if (shouldInjectHTML) requestResponseCredit();
|
|
525
539
|
if (controller && !shouldInjectHTML) for (const q of queued) controller.enqueue(q);
|
|
526
540
|
if (shouldInjectHTML) for (const q of queued) if (!pushInjectChunk(q)) return;
|
|
527
541
|
queued.length = 0;
|
|
528
542
|
return;
|
|
529
543
|
}
|
|
530
544
|
if (m.type === "flowersec-proxy:response_chunk") {
|
|
545
|
+
responseCreditOutstanding = false;
|
|
531
546
|
const b = new Uint8Array(m.data);
|
|
532
547
|
if (shouldInjectHTML) {
|
|
533
|
-
pushInjectChunk(b);
|
|
548
|
+
if (pushInjectChunk(b)) requestResponseCredit();
|
|
534
549
|
return;
|
|
535
550
|
}
|
|
536
551
|
if (controller) controller.enqueue(b); else queued.push(b);
|
|
537
552
|
return;
|
|
538
553
|
}
|
|
539
554
|
if (m.type === "flowersec-proxy:response_end") {
|
|
555
|
+
responseCreditOutstanding = false;
|
|
540
556
|
if (shouldInjectHTML) {
|
|
541
557
|
doneResolve(htmlChunks);
|
|
542
558
|
return;
|
|
@@ -545,6 +561,7 @@ async function handleFetch(event) {
|
|
|
545
561
|
return;
|
|
546
562
|
}
|
|
547
563
|
if (m.type === "flowersec-proxy:response_error") {
|
|
564
|
+
responseCreditOutstanding = false;
|
|
548
565
|
const status = typeof m.status === "number" && Number.isFinite(m.status) ? Math.floor(m.status) : 502;
|
|
549
566
|
lastErrorStatus = status > 0 ? status : 502;
|
|
550
567
|
lastErrorMessage = String(m.message || "proxy error");
|
|
@@ -572,6 +589,7 @@ async function handleFetch(event) {
|
|
|
572
589
|
path,
|
|
573
590
|
headers: headersToPairs(req.headers),
|
|
574
591
|
...(externalOrigin ? { external_origin: externalOrigin } : {}),
|
|
592
|
+
response_flow_control: "chunk_credit_v1",
|
|
575
593
|
body
|
|
576
594
|
}
|
|
577
595
|
}, [port2]);
|
package/dist/reconnect/index.js
CHANGED
|
@@ -138,8 +138,9 @@ export function createReconnectManager() {
|
|
|
138
138
|
}
|
|
139
139
|
token += 1;
|
|
140
140
|
const nextToken = token;
|
|
141
|
+
const reconnectPromise = startConnectLoop(nextToken, cfg);
|
|
141
142
|
setState({ status: "connecting", error, client: null });
|
|
142
|
-
void
|
|
143
|
+
void reconnectPromise.catch(() => {
|
|
143
144
|
// connectWithRetry updates state; keep errors observable via state().
|
|
144
145
|
});
|
|
145
146
|
};
|
|
@@ -262,6 +263,22 @@ export function createReconnectManager() {
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
};
|
|
266
|
+
const startConnectLoop = (t, cfg) => {
|
|
267
|
+
let resolveLoop;
|
|
268
|
+
let rejectLoop;
|
|
269
|
+
const loop = new Promise((resolve, reject) => {
|
|
270
|
+
resolveLoop = resolve;
|
|
271
|
+
rejectLoop = reject;
|
|
272
|
+
});
|
|
273
|
+
let promise;
|
|
274
|
+
promise = loop.finally(() => {
|
|
275
|
+
if (activeConnectPromise === promise)
|
|
276
|
+
activeConnectPromise = null;
|
|
277
|
+
});
|
|
278
|
+
activeConnectPromise = promise;
|
|
279
|
+
void connectWithRetry(t, cfg).then(resolveLoop, rejectLoop);
|
|
280
|
+
return promise;
|
|
281
|
+
};
|
|
265
282
|
const connect = async (cfg) => {
|
|
266
283
|
cancelRetrySleep();
|
|
267
284
|
abortActiveAttempt();
|
|
@@ -277,25 +294,18 @@ export function createReconnectManager() {
|
|
|
277
294
|
// ignore
|
|
278
295
|
}
|
|
279
296
|
}
|
|
297
|
+
const connectPromise = startConnectLoop(t, cfg);
|
|
280
298
|
setState({ status: "connecting", error: null, client: null });
|
|
281
|
-
|
|
282
|
-
activeConnectPromise = p;
|
|
283
|
-
try {
|
|
284
|
-
await p;
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
if (activeConnectPromise === p)
|
|
288
|
-
activeConnectPromise = null;
|
|
289
|
-
}
|
|
299
|
+
await connectPromise;
|
|
290
300
|
};
|
|
291
301
|
const connectIfNeeded = async (cfg) => {
|
|
292
302
|
if (isSameConfig(active, cfg)) {
|
|
293
|
-
if (
|
|
294
|
-
return;
|
|
295
|
-
if (s.status === "connecting" && activeConnectPromise) {
|
|
303
|
+
if (activeConnectPromise) {
|
|
296
304
|
await activeConnectPromise;
|
|
297
305
|
return;
|
|
298
306
|
}
|
|
307
|
+
if (s.status === "connected" && s.client)
|
|
308
|
+
return;
|
|
299
309
|
}
|
|
300
310
|
await connect(cfg);
|
|
301
311
|
};
|
package/dist/yamux/session.d.ts
CHANGED
|
@@ -12,8 +12,11 @@ export type YamuxLimits = Readonly<{
|
|
|
12
12
|
preferredOutboundFrameBytes: number;
|
|
13
13
|
maxStreamReceiveBytes: number;
|
|
14
14
|
maxSessionReceiveBytes: number;
|
|
15
|
+
/** Maximum bytes retained by pending write calls on one stream. */
|
|
16
|
+
maxStreamWriteQueueBytes?: number;
|
|
15
17
|
}>;
|
|
16
|
-
|
|
18
|
+
type ResolvedYamuxLimits = Readonly<Required<YamuxLimits>>;
|
|
19
|
+
export declare const DEFAULT_YAMUX_LIMITS: ResolvedYamuxLimits;
|
|
17
20
|
export type ByteDuplex = {
|
|
18
21
|
/** Reads the next chunk from the underlying connection. */
|
|
19
22
|
read(): Promise<Uint8Array>;
|
|
@@ -60,6 +63,7 @@ export declare class YamuxSession {
|
|
|
60
63
|
getStream(id: number): YamuxStream | undefined;
|
|
61
64
|
writeRaw(chunk: Uint8Array): Promise<void>;
|
|
62
65
|
outboundFrameBytes(): number;
|
|
66
|
+
streamWriteQueueBytes(): number;
|
|
63
67
|
releaseReceiveBytes(bytes: number): void;
|
|
64
68
|
probeLiveness(timeoutMs?: number): Promise<number>;
|
|
65
69
|
private startLivenessProbe;
|
|
@@ -79,3 +83,4 @@ export declare class YamuxSession {
|
|
|
79
83
|
private isInboundStreamIdValid;
|
|
80
84
|
private diagnostic;
|
|
81
85
|
}
|
|
86
|
+
export {};
|
package/dist/yamux/session.js
CHANGED
|
@@ -10,6 +10,7 @@ export const DEFAULT_YAMUX_LIMITS = Object.freeze({
|
|
|
10
10
|
preferredOutboundFrameBytes: 64 * 1024,
|
|
11
11
|
maxStreamReceiveBytes: 256 * 1024,
|
|
12
12
|
maxSessionReceiveBytes: 16 * (1 << 20),
|
|
13
|
+
maxStreamWriteQueueBytes: 4 * (1 << 20),
|
|
13
14
|
});
|
|
14
15
|
// YamuxSession multiplexes multiple streams over a single byte stream.
|
|
15
16
|
export class YamuxSession {
|
|
@@ -92,6 +93,9 @@ export class YamuxSession {
|
|
|
92
93
|
outboundFrameBytes() {
|
|
93
94
|
return this.limits.preferredOutboundFrameBytes;
|
|
94
95
|
}
|
|
96
|
+
streamWriteQueueBytes() {
|
|
97
|
+
return this.limits.maxStreamWriteQueueBytes;
|
|
98
|
+
}
|
|
95
99
|
releaseReceiveBytes(bytes) {
|
|
96
100
|
this.sessionReceiveBytes = Math.max(0, this.sessionReceiveBytes - Math.max(0, bytes));
|
|
97
101
|
}
|
|
@@ -436,6 +440,7 @@ function normalizeYamuxLimits(input) {
|
|
|
436
440
|
preferredOutboundFrameBytes: input?.preferredOutboundFrameBytes ?? Math.min(DEFAULT_YAMUX_LIMITS.preferredOutboundFrameBytes, maxFrameBytes),
|
|
437
441
|
maxStreamReceiveBytes: input?.maxStreamReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxStreamReceiveBytes,
|
|
438
442
|
maxSessionReceiveBytes: input?.maxSessionReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxSessionReceiveBytes,
|
|
443
|
+
maxStreamWriteQueueBytes: input?.maxStreamWriteQueueBytes ?? DEFAULT_YAMUX_LIMITS.maxStreamWriteQueueBytes,
|
|
439
444
|
};
|
|
440
445
|
for (const [name, value] of Object.entries(limits)) {
|
|
441
446
|
if (!Number.isSafeInteger(value) || value <= 0)
|
package/dist/yamux/stream.d.ts
CHANGED
package/dist/yamux/stream.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { concatBytes } from "../utils/bin.js";
|
|
2
2
|
import { encodeHeader } from "./header.js";
|
|
3
3
|
import { DEFAULT_MAX_STREAM_WINDOW, FLAG_ACK, FLAG_FIN, FLAG_RST, FLAG_SYN, TYPE_DATA, TYPE_WINDOW_UPDATE } from "./constants.js";
|
|
4
|
+
import { YamuxResourceExhaustedError } from "./errors.js";
|
|
4
5
|
// YamuxStream manages per-stream flow control and state transitions.
|
|
5
6
|
export class YamuxStream {
|
|
6
7
|
// Stream identifier within the session.
|
|
@@ -24,6 +25,7 @@ export class YamuxStream {
|
|
|
24
25
|
error = null;
|
|
25
26
|
resetTask;
|
|
26
27
|
writeChain = Promise.resolve();
|
|
28
|
+
writeQueueBytes = 0;
|
|
27
29
|
finalized = false;
|
|
28
30
|
constructor(session, id, state) {
|
|
29
31
|
this.session = session;
|
|
@@ -87,10 +89,23 @@ export class YamuxStream {
|
|
|
87
89
|
}
|
|
88
90
|
// write sends DATA frames, respecting the send window.
|
|
89
91
|
async write(data) {
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
this.ensureWritable();
|
|
93
|
+
const byteCount = data.byteLength;
|
|
94
|
+
const nextQueueBytes = this.writeQueueBytes + byteCount;
|
|
95
|
+
const limit = this.session.streamWriteQueueBytes();
|
|
96
|
+
if (!Number.isSafeInteger(nextQueueBytes) || nextQueueBytes > limit) {
|
|
97
|
+
throw new YamuxResourceExhaustedError("stream_write_queue_bytes", nextQueueBytes, limit);
|
|
98
|
+
}
|
|
99
|
+
this.writeQueueBytes = nextQueueBytes;
|
|
100
|
+
try {
|
|
101
|
+
const payload = data.slice();
|
|
102
|
+
const write = this.writeChain.then(() => this.writeSerial(payload));
|
|
103
|
+
this.writeChain = write.catch(() => { });
|
|
104
|
+
await write;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
this.writeQueueBytes = Math.max(0, this.writeQueueBytes - byteCount);
|
|
108
|
+
}
|
|
94
109
|
}
|
|
95
110
|
async writeSerial(data) {
|
|
96
111
|
this.ensureWritable();
|