@floegence/flowersec-core 0.19.10 → 0.20.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.
Files changed (44) hide show
  1. package/README.md +8 -3
  2. package/dist/browser/index.d.ts +2 -0
  3. package/dist/browser/index.js +1 -0
  4. package/dist/browser/reconnectConfig.d.ts +8 -36
  5. package/dist/browser/reconnectConfig.js +13 -70
  6. package/dist/client-connect/connectCore.d.ts +17 -5
  7. package/dist/client-connect/connectCore.js +131 -32
  8. package/dist/client-connect/transportSecurity.d.ts +19 -0
  9. package/dist/client-connect/transportSecurity.js +114 -0
  10. package/dist/client.d.ts +2 -0
  11. package/dist/e2ee/handshake.d.ts +4 -0
  12. package/dist/e2ee/handshake.js +2 -0
  13. package/dist/e2ee/secureChannel.d.ts +4 -0
  14. package/dist/e2ee/secureChannel.js +18 -9
  15. package/dist/facade.d.ts +5 -0
  16. package/dist/facade.js +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/node/index.d.ts +2 -0
  20. package/dist/node/index.js +1 -0
  21. package/dist/node/reconnectConfig.d.ts +8 -34
  22. package/dist/node/reconnectConfig.js +13 -71
  23. package/dist/node/wsFactory.js +3 -0
  24. package/dist/observability/observer.d.ts +5 -2
  25. package/dist/observability/observer.js +3 -0
  26. package/dist/reconnect/artifactControlplane.d.ts +9 -6
  27. package/dist/reconnect/artifactControlplane.js +28 -27
  28. package/dist/reconnect/index.d.ts +2 -0
  29. package/dist/reconnect/index.js +7 -0
  30. package/dist/rpc/server.d.ts +29 -4
  31. package/dist/rpc/server.js +142 -34
  32. package/dist/tunnel-client/connect.js +6 -6
  33. package/dist/utils/errors.d.ts +1 -1
  34. package/dist/ws-client/binaryTransport.d.ts +20 -5
  35. package/dist/ws-client/binaryTransport.js +110 -8
  36. package/dist/yamux/byteReader.d.ts +1 -0
  37. package/dist/yamux/byteReader.js +26 -0
  38. package/dist/yamux/errors.d.ts +7 -0
  39. package/dist/yamux/errors.js +15 -0
  40. package/dist/yamux/session.d.ts +31 -1
  41. package/dist/yamux/session.js +191 -12
  42. package/dist/yamux/stream.d.ts +10 -3
  43. package/dist/yamux/stream.js +61 -19
  44. package/package.json +1 -1
@@ -1,7 +1,16 @@
1
1
  import { ByteReader } from "./byteReader.js";
2
2
  import { decodeHeader, encodeHeader, HEADER_LEN } from "./header.js";
3
- import { DEFAULT_MAX_STREAM_WINDOW, FLAG_ACK, FLAG_RST, FLAG_SYN, TYPE_DATA, TYPE_GO_AWAY, TYPE_PING, TYPE_WINDOW_UPDATE, YAMUX_VERSION } from "./constants.js";
3
+ import { FLAG_ACK, FLAG_RST, FLAG_SYN, TYPE_DATA, TYPE_GO_AWAY, TYPE_PING, TYPE_WINDOW_UPDATE, YAMUX_VERSION } from "./constants.js";
4
4
  import { YamuxStream } from "./stream.js";
5
+ import { YamuxResourceExhaustedError } from "./errors.js";
6
+ export const DEFAULT_YAMUX_LIMITS = Object.freeze({
7
+ maxActiveStreams: 64,
8
+ maxInboundStreams: 32,
9
+ maxFrameBytes: 256 * 1024,
10
+ preferredOutboundFrameBytes: 64 * 1024,
11
+ maxStreamReceiveBytes: 256 * 1024,
12
+ maxSessionReceiveBytes: 16 * (1 << 20),
13
+ });
5
14
  // YamuxSession multiplexes multiple streams over a single byte stream.
6
15
  export class YamuxSession {
7
16
  // Underlying byte stream for yamux frames.
@@ -13,7 +22,8 @@ export class YamuxSession {
13
22
  // Callback for inbound streams created from SYN frames.
14
23
  onIncomingStream;
15
24
  // Maximum allowed DATA frame length.
16
- maxFrameBytes;
25
+ limits;
26
+ onDiagnostic;
17
27
  // True when this side is the yamux client (odd local stream IDs).
18
28
  client;
19
29
  // Next stream ID to allocate (odd/even based on role).
@@ -22,6 +32,11 @@ export class YamuxSession {
22
32
  closed = false;
23
33
  // Writers waiting for send window credits per stream.
24
34
  sendWindowWaiters = new Map();
35
+ inboundStreams = new Set();
36
+ sessionReceiveBytes = 0;
37
+ nextPingId = 1;
38
+ pingWaiters = new Map();
39
+ activeProbe;
25
40
  constructor(conn, opts) {
26
41
  this.conn = conn;
27
42
  this.reader = new ByteReader(async () => {
@@ -33,19 +48,33 @@ export class YamuxSession {
33
48
  }
34
49
  });
35
50
  this.onIncomingStream = opts.onIncomingStream;
36
- this.maxFrameBytes = Math.max(0, opts.maxFrameBytes ?? DEFAULT_MAX_STREAM_WINDOW);
51
+ this.limits = normalizeYamuxLimits({ ...opts.limits, ...(opts.maxFrameBytes === undefined ? {} : { maxFrameBytes: opts.maxFrameBytes }) });
52
+ this.onDiagnostic = opts.onDiagnostic;
37
53
  this.client = opts.client;
38
54
  this.nextStreamId = opts.client ? 1 : 2;
39
55
  void this.readLoop();
40
56
  }
41
57
  // openStream allocates a new stream and performs the SYN handshake.
42
58
  async openStream() {
59
+ if (this.closed)
60
+ throw new Error("session closed");
61
+ if (this.streams.size >= this.limits.maxActiveStreams) {
62
+ const error = new YamuxResourceExhaustedError("active_streams", this.streams.size, this.limits.maxActiveStreams);
63
+ this.diagnostic({ code: "resource_limit_reached", resource: error.resource, current: error.current, limit: error.limit });
64
+ throw error;
65
+ }
43
66
  const id = this.nextStreamId;
44
67
  this.nextStreamId += 2;
45
68
  const s = new YamuxStream(this, id, "init");
46
69
  this.streams.set(id, s);
47
- await s.open();
48
- return s;
70
+ try {
71
+ await s.open();
72
+ return s;
73
+ }
74
+ catch (error) {
75
+ this.onStreamClosed(id);
76
+ throw error;
77
+ }
49
78
  }
50
79
  // getStream returns the stream for an ID, if any.
51
80
  getStream(id) {
@@ -55,6 +84,57 @@ export class YamuxSession {
55
84
  async writeRaw(chunk) {
56
85
  await this.conn.write(chunk);
57
86
  }
87
+ outboundFrameBytes() {
88
+ return this.limits.preferredOutboundFrameBytes;
89
+ }
90
+ releaseReceiveBytes(bytes) {
91
+ this.sessionReceiveBytes = Math.max(0, this.sessionReceiveBytes - Math.max(0, bytes));
92
+ }
93
+ // probeLiveness performs a correlated yamux PING SYN/ACK round trip.
94
+ async probeLiveness(timeoutMs = 10_000) {
95
+ if (this.activeProbe != null)
96
+ return await this.activeProbe;
97
+ const probe = this.startLivenessProbe(timeoutMs);
98
+ this.activeProbe = probe;
99
+ try {
100
+ return await probe;
101
+ }
102
+ finally {
103
+ if (this.activeProbe === probe)
104
+ this.activeProbe = undefined;
105
+ }
106
+ }
107
+ async startLivenessProbe(timeoutMs) {
108
+ if (this.closed)
109
+ throw new Error("session closed");
110
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
111
+ throw new RangeError("timeoutMs must be positive");
112
+ let opaque = this.nextPingId >>> 0;
113
+ do {
114
+ opaque = this.nextPingId++ >>> 0;
115
+ if (this.nextPingId > 0xffffffff)
116
+ this.nextPingId = 1;
117
+ } while (opaque === 0 || this.pingWaiters.has(opaque));
118
+ const startedAt = monotonicMilliseconds();
119
+ return await new Promise((resolve, reject) => {
120
+ const timer = setTimeout(() => {
121
+ this.pingWaiters.delete(opaque);
122
+ reject(new Error("yamux ping timeout"));
123
+ this.close();
124
+ }, timeoutMs);
125
+ timer?.unref?.();
126
+ this.pingWaiters.set(opaque, { startedAt, resolve, reject, timer });
127
+ const hdr = encodeHeader({ type: TYPE_PING, flags: FLAG_SYN, streamId: 0, length: opaque });
128
+ void this.writeRaw(hdr).catch((err) => {
129
+ const waiter = this.pingWaiters.get(opaque);
130
+ if (waiter == null)
131
+ return;
132
+ this.pingWaiters.delete(opaque);
133
+ clearTimeout(waiter.timer);
134
+ reject(err);
135
+ });
136
+ });
137
+ }
58
138
  // sendRst sends a reset frame and removes the stream.
59
139
  async sendRst(id) {
60
140
  const hdr = encodeHeader({ type: TYPE_WINDOW_UPDATE, flags: FLAG_RST, streamId: id, length: 0 });
@@ -102,7 +182,11 @@ export class YamuxSession {
102
182
  onStreamEstablished(_streamId) { }
103
183
  // onStreamClosed removes the stream and wakes any per-stream waiters.
104
184
  onStreamClosed(streamId) {
185
+ const stream = this.streams.get(streamId);
186
+ if (stream != null)
187
+ this.releaseReceiveBytes(stream.releaseBufferedReceiveBytes());
105
188
  this.streams.delete(streamId);
189
+ this.inboundStreams.delete(streamId);
106
190
  this.notifySendWindow(streamId);
107
191
  }
108
192
  // close terminates the session and resets all streams.
@@ -112,6 +196,11 @@ export class YamuxSession {
112
196
  this.closed = true;
113
197
  this.conn.close();
114
198
  this.wakeSendWindowWaiters();
199
+ for (const waiter of this.pingWaiters.values()) {
200
+ clearTimeout(waiter.timer);
201
+ waiter.reject(new Error("session closed"));
202
+ }
203
+ this.pingWaiters.clear();
115
204
  const streams = Array.from(this.streams.values());
116
205
  this.streams.clear();
117
206
  for (const s of streams)
@@ -134,12 +223,47 @@ export class YamuxSession {
134
223
  return;
135
224
  }
136
225
  if (h.type === TYPE_DATA) {
137
- if (this.maxFrameBytes > 0 && h.length > this.maxFrameBytes) {
226
+ if (h.length > this.limits.maxFrameBytes) {
227
+ this.close();
228
+ return;
229
+ }
230
+ if (h.streamId === 0) {
231
+ this.close();
232
+ return;
233
+ }
234
+ const existing = this.streams.get(h.streamId);
235
+ if (existing == null) {
236
+ const inboundAllowed = (h.flags & FLAG_SYN) !== 0 && this.isInboundStreamIdValid(h.streamId);
237
+ const resourceAllowed = this.streams.size < this.limits.maxActiveStreams && this.inboundStreams.size < this.limits.maxInboundStreams;
238
+ if (!inboundAllowed || !resourceAllowed || h.length > this.limits.maxStreamReceiveBytes) {
239
+ await this.reader.discardExactly(h.length);
240
+ this.diagnostic({
241
+ code: "stream_rejected",
242
+ resource: !resourceAllowed ? "inbound_streams" : "stream_receive_bytes",
243
+ current: !resourceAllowed ? this.inboundStreams.size : h.length,
244
+ limit: !resourceAllowed ? this.limits.maxInboundStreams : this.limits.maxStreamReceiveBytes,
245
+ });
246
+ await this.sendRst(h.streamId);
247
+ continue;
248
+ }
249
+ }
250
+ if (existing != null && existing.bufferedReceiveBytes() + h.length > this.limits.maxStreamReceiveBytes) {
251
+ await this.reader.discardExactly(h.length);
252
+ this.diagnostic({ code: "resource_limit_reached", resource: "stream_receive_bytes", current: existing.bufferedReceiveBytes() + h.length, limit: this.limits.maxStreamReceiveBytes });
253
+ await this.sendRst(h.streamId);
254
+ continue;
255
+ }
256
+ if (this.sessionReceiveBytes + h.length > this.limits.maxSessionReceiveBytes) {
257
+ this.diagnostic({ code: "resource_limit_reached", resource: "session_receive_bytes", current: this.sessionReceiveBytes + h.length, limit: this.limits.maxSessionReceiveBytes });
138
258
  this.close();
139
259
  return;
140
260
  }
141
261
  const data = h.length > 0 ? await this.reader.readExactly(h.length) : new Uint8Array();
142
- await this.handleDataFrame(h.streamId, h.flags, data);
262
+ if (data.length > 0)
263
+ this.sessionReceiveBytes += data.length;
264
+ const accepted = await this.handleDataFrame(h.streamId, h.flags, data);
265
+ if (!accepted)
266
+ this.releaseReceiveBytes(data.length);
143
267
  continue;
144
268
  }
145
269
  if (h.type === TYPE_WINDOW_UPDATE) {
@@ -168,30 +292,43 @@ export class YamuxSession {
168
292
  await this.writeRaw(hdr);
169
293
  return;
170
294
  }
295
+ if ((flags & FLAG_ACK) !== 0) {
296
+ const waiter = this.pingWaiters.get(opaque >>> 0);
297
+ if (waiter == null)
298
+ return;
299
+ this.pingWaiters.delete(opaque >>> 0);
300
+ clearTimeout(waiter.timer);
301
+ waiter.resolve(Math.max(0, monotonicMilliseconds() - waiter.startedAt));
302
+ }
171
303
  }
172
304
  async handleDataFrame(streamId, flags, data) {
173
305
  if (streamId === 0) {
174
306
  this.close();
175
- return;
307
+ return false;
176
308
  }
177
309
  let s = this.streams.get(streamId);
178
310
  if (s == null) {
179
311
  if ((flags & FLAG_SYN) !== 0) {
180
312
  if (!this.isInboundStreamIdValid(streamId)) {
181
313
  await this.sendRst(streamId);
182
- return;
314
+ return false;
315
+ }
316
+ if (this.streams.size >= this.limits.maxActiveStreams || this.inboundStreams.size >= this.limits.maxInboundStreams || data.length > this.limits.maxStreamReceiveBytes) {
317
+ await this.sendRst(streamId);
318
+ return false;
183
319
  }
184
320
  s = new YamuxStream(this, streamId, "synReceived");
185
321
  this.streams.set(streamId, s);
322
+ this.inboundStreams.add(streamId);
186
323
  await s.open();
187
324
  this.onIncomingStream?.(s);
188
325
  }
189
326
  else {
190
327
  await this.sendRst(streamId);
191
- return;
328
+ return false;
192
329
  }
193
330
  }
194
- s.onData(data, flags);
331
+ return s.onData(data, flags);
195
332
  }
196
333
  async handleWindowUpdateFrame(streamId, flags, delta) {
197
334
  if (streamId === 0) {
@@ -205,8 +342,13 @@ export class YamuxSession {
205
342
  await this.sendRst(streamId);
206
343
  return;
207
344
  }
345
+ if (this.streams.size >= this.limits.maxActiveStreams || this.inboundStreams.size >= this.limits.maxInboundStreams) {
346
+ await this.sendRst(streamId);
347
+ return;
348
+ }
208
349
  s = new YamuxStream(this, streamId, "synReceived");
209
350
  this.streams.set(streamId, s);
351
+ this.inboundStreams.add(streamId);
210
352
  await s.open();
211
353
  this.onIncomingStream?.(s);
212
354
  }
@@ -215,7 +357,9 @@ export class YamuxSession {
215
357
  return;
216
358
  }
217
359
  }
218
- s.onWindowUpdate(delta, flags);
360
+ if (!s.onWindowUpdate(delta, flags)) {
361
+ this.close();
362
+ }
219
363
  }
220
364
  isInboundStreamIdValid(streamId) {
221
365
  // yamux uses stream ID parity to identify the initiator:
@@ -225,4 +369,39 @@ export class YamuxSession {
225
369
  // When we are the server, the peer is the client and must initiate odd IDs.
226
370
  return (streamId & 1) === (this.client ? 0 : 1);
227
371
  }
372
+ diagnostic(event) {
373
+ try {
374
+ this.onDiagnostic?.(event);
375
+ }
376
+ catch { /* Diagnostics cannot affect transport behavior. */ }
377
+ }
378
+ }
379
+ function normalizeYamuxLimits(input) {
380
+ const maxFrameBytes = input?.maxFrameBytes ?? DEFAULT_YAMUX_LIMITS.maxFrameBytes;
381
+ const limits = {
382
+ maxActiveStreams: input?.maxActiveStreams ?? DEFAULT_YAMUX_LIMITS.maxActiveStreams,
383
+ maxInboundStreams: input?.maxInboundStreams ?? DEFAULT_YAMUX_LIMITS.maxInboundStreams,
384
+ maxFrameBytes,
385
+ preferredOutboundFrameBytes: input?.preferredOutboundFrameBytes ?? Math.min(DEFAULT_YAMUX_LIMITS.preferredOutboundFrameBytes, maxFrameBytes),
386
+ maxStreamReceiveBytes: input?.maxStreamReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxStreamReceiveBytes,
387
+ maxSessionReceiveBytes: input?.maxSessionReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxSessionReceiveBytes,
388
+ };
389
+ for (const [name, value] of Object.entries(limits)) {
390
+ if (!Number.isSafeInteger(value) || value <= 0)
391
+ throw new RangeError(`${name} must be a positive integer`);
392
+ }
393
+ if (limits.maxInboundStreams > limits.maxActiveStreams)
394
+ throw new RangeError("maxInboundStreams must not exceed maxActiveStreams");
395
+ if (limits.preferredOutboundFrameBytes > limits.maxFrameBytes)
396
+ throw new RangeError("preferredOutboundFrameBytes must not exceed maxFrameBytes");
397
+ if (limits.maxFrameBytes > limits.maxStreamReceiveBytes)
398
+ throw new RangeError("maxFrameBytes must not exceed maxStreamReceiveBytes");
399
+ if (limits.maxStreamReceiveBytes < DEFAULT_YAMUX_LIMITS.maxStreamReceiveBytes)
400
+ throw new RangeError("maxStreamReceiveBytes must cover the 256 KiB initial stream window");
401
+ if (limits.maxStreamReceiveBytes > limits.maxSessionReceiveBytes)
402
+ throw new RangeError("maxStreamReceiveBytes must not exceed maxSessionReceiveBytes");
403
+ return Object.freeze(limits);
404
+ }
405
+ function monotonicMilliseconds() {
406
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
228
407
  }
@@ -11,14 +11,21 @@ export declare class YamuxStream {
11
11
  private recvQueueBytes;
12
12
  private readWaiters;
13
13
  private error;
14
+ private resetTask;
15
+ private writeChain;
16
+ private finalized;
14
17
  constructor(session: YamuxSession, id: number, state: StreamState);
15
18
  open(): Promise<void>;
16
- onData(data: Uint8Array, flags: number): void;
17
- onWindowUpdate(delta: number, flags: number): void;
19
+ onData(data: Uint8Array, flags: number): boolean;
20
+ onWindowUpdate(delta: number, flags: number): boolean;
18
21
  read(): Promise<Uint8Array | null>;
19
22
  write(data: Uint8Array): Promise<void>;
23
+ private writeSerial;
20
24
  close(): Promise<void>;
21
- reset(err: Error): void;
25
+ reset(err: Error): Promise<void>;
26
+ private fail;
27
+ bufferedReceiveBytes(): number;
28
+ releaseBufferedReceiveBytes(): number;
22
29
  private processFlags;
23
30
  private finalizeClosed;
24
31
  private shiftRecv;
@@ -22,6 +22,9 @@ export class YamuxStream {
22
22
  readWaiters = [];
23
23
  // Terminal error (reset/overflow) for the stream.
24
24
  error = null;
25
+ resetTask;
26
+ writeChain = Promise.resolve();
27
+ finalized = false;
25
28
  constructor(session, id, state) {
26
29
  this.session = session;
27
30
  this.id = id;
@@ -33,26 +36,35 @@ export class YamuxStream {
33
36
  }
34
37
  // onData handles inbound DATA frames and updates receive window.
35
38
  onData(data, flags) {
36
- this.processFlags(flags);
37
- if (data.length === 0)
38
- return;
39
- if (data.length > this.recvWindow) {
40
- this.reset(new Error("recv window exceeded"));
41
- return;
39
+ this.processFlags(flags & ~FLAG_FIN);
40
+ if (this.state === "reset" || this.state === "closed")
41
+ return false;
42
+ if (data.length > 0) {
43
+ if (data.length > this.recvWindow) {
44
+ void this.reset(new Error("recv window exceeded"));
45
+ return false;
46
+ }
47
+ this.recvWindow -= data.length;
48
+ this.recvQueue.push(data);
49
+ this.recvQueueBytes += data.length;
42
50
  }
43
- this.recvWindow -= data.length;
44
- this.recvQueue.push(data);
45
- this.recvQueueBytes += data.length;
51
+ this.processFlags(flags & FLAG_FIN);
46
52
  const ws = this.readWaiters;
47
53
  this.readWaiters = [];
48
54
  for (const w of ws)
49
55
  w();
56
+ return true;
50
57
  }
51
58
  // onWindowUpdate applies flow-control credits from peer.
52
59
  onWindowUpdate(delta, flags) {
53
60
  this.processFlags(flags);
54
- this.sendWindow += delta >>> 0;
61
+ const credit = delta >>> 0;
62
+ if (credit > DEFAULT_MAX_STREAM_WINDOW - this.sendWindow) {
63
+ return false;
64
+ }
65
+ this.sendWindow += credit;
55
66
  this.session.notifySendWindow(this.id);
67
+ return true;
56
68
  }
57
69
  // read resolves with the next data chunk, null on EOF, or throws on reset/errors.
58
70
  async read() {
@@ -62,7 +74,10 @@ export class YamuxStream {
62
74
  const b = this.shiftRecv();
63
75
  if (b != null) {
64
76
  this.recvQueueBytes -= b.length;
77
+ this.session.releaseReceiveBytes(b.length);
65
78
  await this.sendWindowUpdate();
79
+ if (this.recvQueueBytes === 0 && this.state === "closed")
80
+ this.finalizeClosed();
66
81
  return b;
67
82
  }
68
83
  if (this.state === "closed" || this.state === "remoteClose")
@@ -72,11 +87,17 @@ export class YamuxStream {
72
87
  }
73
88
  // write sends DATA frames, respecting the send window.
74
89
  async write(data) {
90
+ const payload = data.slice();
91
+ const write = this.writeChain.then(() => this.writeSerial(payload));
92
+ this.writeChain = write.catch(() => { });
93
+ await write;
94
+ }
95
+ async writeSerial(data) {
75
96
  this.ensureWritable();
76
97
  let off = 0;
77
98
  while (off < data.length) {
78
99
  this.ensureWritable();
79
- const chunk = data.subarray(off);
100
+ const chunk = data.subarray(off, off + this.session.outboundFrameBytes());
80
101
  const allowed = await this.waitForSendWindow(chunk.length);
81
102
  this.ensureWritable();
82
103
  const sendChunk = chunk.subarray(0, allowed);
@@ -111,26 +132,42 @@ export class YamuxStream {
111
132
  await this.session.writeRaw(hdr);
112
133
  }
113
134
  finally {
114
- if (wasRemoteClose)
135
+ if (wasRemoteClose && this.recvQueueBytes === 0)
115
136
  this.finalizeClosed();
116
137
  }
117
138
  }
118
139
  // reset tears down the stream and notifies the peer.
119
140
  reset(err) {
141
+ if (this.resetTask != null)
142
+ return this.resetTask;
143
+ if (!this.fail(err))
144
+ return Promise.resolve();
145
+ this.resetTask = this.session.sendRst(this.id);
146
+ return this.resetTask;
147
+ }
148
+ fail(err) {
120
149
  if (this.state === "reset")
121
- return;
150
+ return false;
122
151
  this.state = "reset";
123
152
  this.error = err;
124
153
  // Drop any buffered data to free memory on terminal errors.
125
- this.recvQueue.length = 0;
126
- this.recvQueueHead = 0;
127
- this.recvQueueBytes = 0;
154
+ this.session.releaseReceiveBytes(this.releaseBufferedReceiveBytes());
128
155
  const ws = this.readWaiters;
129
156
  this.readWaiters = [];
130
157
  for (const w of ws)
131
158
  w();
132
159
  this.session.notifySendWindow(this.id);
133
- void this.session.sendRst(this.id);
160
+ return true;
161
+ }
162
+ bufferedReceiveBytes() {
163
+ return this.recvQueueBytes;
164
+ }
165
+ releaseBufferedReceiveBytes() {
166
+ const bytes = this.recvQueueBytes;
167
+ this.recvQueue.length = 0;
168
+ this.recvQueueHead = 0;
169
+ this.recvQueueBytes = 0;
170
+ return bytes;
134
171
  }
135
172
  // processFlags updates the state machine for ACK/FIN/RST.
136
173
  processFlags(flags) {
@@ -142,7 +179,8 @@ export class YamuxStream {
142
179
  if ((flags & FLAG_FIN) !== 0) {
143
180
  if (this.state === "localClose") {
144
181
  this.state = "closed";
145
- this.finalizeClosed();
182
+ if (this.recvQueueBytes === 0)
183
+ this.finalizeClosed();
146
184
  }
147
185
  else if (this.state === "established" || this.state === "synSent" || this.state === "synReceived") {
148
186
  this.state = "remoteClose";
@@ -153,10 +191,14 @@ export class YamuxStream {
153
191
  w();
154
192
  }
155
193
  if ((flags & FLAG_RST) !== 0) {
156
- this.reset(new Error("rst"));
194
+ if (this.fail(new Error("rst")))
195
+ this.session.onStreamClosed(this.id);
157
196
  }
158
197
  }
159
198
  finalizeClosed() {
199
+ if (this.finalized)
200
+ return;
201
+ this.finalized = true;
160
202
  const ws = this.readWaiters;
161
203
  this.readWaiters = [];
162
204
  for (const w of ws)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.19.10",
3
+ "version": "0.20.0",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {