@gurezo/web-serial-rxjs 2.3.6 → 2.4.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 (32) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/errors/serial-error-code.d.ts +43 -0
  4. package/dist/errors/serial-error-code.d.ts.map +1 -1
  5. package/dist/index.d.ts +6 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.mjs +447 -84
  8. package/dist/index.mjs.map +4 -4
  9. package/dist/internal/newline-tokenizer.d.ts +38 -0
  10. package/dist/internal/newline-tokenizer.d.ts.map +1 -0
  11. package/dist/session/create-serial-session.d.ts +2 -1
  12. package/dist/session/create-serial-session.d.ts.map +1 -1
  13. package/dist/session/index.d.ts +1 -0
  14. package/dist/session/index.d.ts.map +1 -1
  15. package/dist/session/internal/line-buffer.d.ts +24 -2
  16. package/dist/session/internal/line-buffer.d.ts.map +1 -1
  17. package/dist/session/internal/receive-replay-buffer.d.ts +37 -0
  18. package/dist/session/internal/receive-replay-buffer.d.ts.map +1 -0
  19. package/dist/session/read-pump.d.ts +9 -1
  20. package/dist/session/read-pump.d.ts.map +1 -1
  21. package/dist/session/serial-session-options.d.ts +42 -1
  22. package/dist/session/serial-session-options.d.ts.map +1 -1
  23. package/dist/session/serial-session-state.d.ts +1 -0
  24. package/dist/session/serial-session-state.d.ts.map +1 -1
  25. package/dist/session/serial-session.d.ts +35 -2
  26. package/dist/session/serial-session.d.ts.map +1 -1
  27. package/dist/session/session-state-machine.d.ts +1 -0
  28. package/dist/session/session-state-machine.d.ts.map +1 -1
  29. package/dist/terminal/create-terminal-buffer.d.ts +47 -1
  30. package/dist/terminal/create-terminal-buffer.d.ts.map +1 -1
  31. package/package.json +2 -1
  32. package/dist/index.js +0 -724
package/dist/index.mjs CHANGED
@@ -3,10 +3,9 @@ import {
3
3
  BehaviorSubject as BehaviorSubject2,
4
4
  distinctUntilChanged,
5
5
  map as map2,
6
- Observable as Observable3,
7
- ReplaySubject,
6
+ Observable as Observable4,
8
7
  share,
9
- Subject,
8
+ Subject as Subject2,
10
9
  switchMap
11
10
  } from "rxjs";
12
11
 
@@ -23,6 +22,10 @@ var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
23
22
  SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
24
23
  SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
25
24
  SerialErrorCode2["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
25
+ SerialErrorCode2["LINE_BUFFER_OVERFLOW"] = "LINE_BUFFER_OVERFLOW";
26
+ SerialErrorCode2["INVALID_RECEIVE_REPLAY_OPTIONS"] = "INVALID_RECEIVE_REPLAY_OPTIONS";
27
+ SerialErrorCode2["RECEIVE_REPLAY_BUFFER_OVERFLOW"] = "RECEIVE_REPLAY_BUFFER_OVERFLOW";
28
+ SerialErrorCode2["SESSION_DISPOSED"] = "SESSION_DISPOSED";
26
29
  SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
27
30
  return SerialErrorCode2;
28
31
  })(SerialErrorCode || {});
@@ -68,42 +71,180 @@ var SerialError = class _SerialError extends Error {
68
71
 
69
72
  // src/terminal/create-terminal-buffer.ts
70
73
  import { map, scan, shareReplay } from "rxjs";
71
- function applyTerminalChunk(state, chunk) {
72
- let { completed, currentLine } = state;
73
- const len = chunk.length;
74
- for (let i = 0; i < len; i++) {
75
- const c = chunk.charAt(i);
76
- if (c === "\r") {
77
- const next = i + 1 < len ? chunk.charAt(i + 1) : "";
78
- if (next === "\n") {
79
- completed += currentLine + "\n";
80
- currentLine = "";
81
- i++;
74
+
75
+ // src/internal/newline-tokenizer.ts
76
+ function createNewlineTokenizer(mode) {
77
+ let pending = "";
78
+ let deferredTrailingCr = false;
79
+ const clear = () => {
80
+ pending = "";
81
+ deferredTrailingCr = false;
82
+ };
83
+ const restorePending = (text) => {
84
+ pending = text;
85
+ deferredTrailingCr = false;
86
+ };
87
+ const feed = (chunk) => {
88
+ const events = [];
89
+ let i = 0;
90
+ if (mode === "line" && deferredTrailingCr) {
91
+ deferredTrailingCr = false;
92
+ if (chunk.length > 0 && chunk.charAt(0) === "\n") {
93
+ events.push({ type: "line", content: pending });
94
+ pending = "";
95
+ i = 1;
82
96
  } else {
83
- currentLine = "";
97
+ events.push({ type: "line", content: pending });
98
+ pending = "";
84
99
  }
85
- } else if (c === "\n") {
86
- completed += currentLine + "\n";
87
- currentLine = "";
100
+ }
101
+ const len = chunk.length;
102
+ for (; i < len; i++) {
103
+ const c = chunk.charAt(i);
104
+ if (c === "\r") {
105
+ const next = i + 1 < len ? chunk.charAt(i + 1) : "";
106
+ if (next === "\n") {
107
+ events.push({ type: "line", content: pending });
108
+ pending = "";
109
+ i++;
110
+ continue;
111
+ }
112
+ if (mode === "line") {
113
+ if (next !== "") {
114
+ events.push({ type: "line", content: pending });
115
+ pending = "";
116
+ continue;
117
+ }
118
+ deferredTrailingCr = true;
119
+ break;
120
+ }
121
+ events.push({ type: "carriage-return" });
122
+ pending = "";
123
+ continue;
124
+ }
125
+ if (c === "\n") {
126
+ events.push({ type: "line", content: pending });
127
+ pending = "";
128
+ continue;
129
+ }
130
+ pending += c;
131
+ }
132
+ return events;
133
+ };
134
+ const trimPending = (maxChars) => {
135
+ if (maxChars <= 0) {
136
+ return false;
137
+ }
138
+ const text = deferredTrailingCr ? `${pending}\r` : pending;
139
+ if (text.length <= maxChars) {
140
+ return false;
141
+ }
142
+ const trimmed = text.slice(text.length - maxChars);
143
+ if (trimmed.endsWith("\r")) {
144
+ pending = trimmed.slice(0, -1);
145
+ deferredTrailingCr = true;
88
146
  } else {
89
- currentLine += c;
147
+ pending = trimmed;
148
+ deferredTrailingCr = false;
149
+ }
150
+ return true;
151
+ };
152
+ return {
153
+ feed,
154
+ clear,
155
+ restorePending,
156
+ getPendingText: () => deferredTrailingCr ? `${pending}\r` : pending,
157
+ trimPending
158
+ };
159
+ }
160
+
161
+ // src/terminal/create-terminal-buffer.ts
162
+ function applyTerminalChunk(state, chunk) {
163
+ const tokenizer = createNewlineTokenizer("terminal");
164
+ tokenizer.restorePending(state.currentLine);
165
+ const events = tokenizer.feed(chunk);
166
+ let { completed } = state;
167
+ for (const event of events) {
168
+ if (event.type === "line") {
169
+ completed += event.content + "\n";
90
170
  }
91
171
  }
92
- return { completed, currentLine };
172
+ return {
173
+ completed,
174
+ currentLine: tokenizer.getPendingText()
175
+ };
93
176
  }
94
177
  function terminalDisplayText(state) {
95
178
  return state.completed + state.currentLine;
96
179
  }
180
+ function countCompletedLines(completed) {
181
+ if (completed.length === 0) {
182
+ return 0;
183
+ }
184
+ let count = 0;
185
+ for (let i = 0; i < completed.length; i++) {
186
+ if (completed.charAt(i) === "\n") {
187
+ count++;
188
+ }
189
+ }
190
+ return count;
191
+ }
192
+ function trimCompletedByMaxLines(completed, maxLines) {
193
+ if (maxLines <= 0) {
194
+ return completed;
195
+ }
196
+ let trimmed = completed;
197
+ while (countCompletedLines(trimmed) > maxLines) {
198
+ const firstNewline = trimmed.indexOf("\n");
199
+ if (firstNewline < 0) {
200
+ break;
201
+ }
202
+ trimmed = trimmed.slice(firstNewline + 1);
203
+ }
204
+ return trimmed;
205
+ }
206
+ function trimTerminalState(state, limits) {
207
+ let { completed, currentLine } = state;
208
+ if (limits.maxLines > 0) {
209
+ completed = trimCompletedByMaxLines(completed, limits.maxLines);
210
+ }
211
+ if (limits.maxChars > 0) {
212
+ let total = completed.length + currentLine.length;
213
+ while (total > limits.maxChars) {
214
+ const excess = total - limits.maxChars;
215
+ if (completed.length >= excess) {
216
+ completed = completed.slice(excess);
217
+ break;
218
+ }
219
+ const removeFromCurrent = excess - completed.length;
220
+ completed = "";
221
+ currentLine = currentLine.slice(removeFromCurrent);
222
+ total = completed.length + currentLine.length;
223
+ }
224
+ }
225
+ return { completed, currentLine };
226
+ }
227
+ var DEFAULT_TERMINAL_BUFFER_OPTIONS = {
228
+ maxLines: 1e4,
229
+ maxChars: 1048576
230
+ };
231
+ function resolveTerminalBufferLimits(options) {
232
+ return {
233
+ ...DEFAULT_TERMINAL_BUFFER_OPTIONS,
234
+ ...options
235
+ };
236
+ }
97
237
  var initialTerminalState = {
98
238
  completed: "",
99
239
  currentLine: ""
100
240
  };
101
- function createTerminalBuffer(receive$) {
241
+ function createTerminalBuffer(receive$, options) {
242
+ const limits = resolveTerminalBufferLimits(options);
102
243
  const text$ = receive$.pipe(
103
- scan(
104
- (state, chunk) => applyTerminalChunk(state, chunk),
105
- initialTerminalState
106
- ),
244
+ scan((state, chunk) => {
245
+ const next = applyTerminalChunk(state, chunk);
246
+ return trimTerminalState(next, limits);
247
+ }, initialTerminalState),
107
248
  map(terminalDisplayText),
108
249
  shareReplay({ bufferSize: 1, refCount: true })
109
250
  );
@@ -150,43 +291,86 @@ function hasWebSerialSupport() {
150
291
  }
151
292
 
152
293
  // src/session/internal/line-buffer.ts
153
- function createLineBuffer() {
154
- let buffer = "";
294
+ var DEFAULT_LINE_BUFFER_OPTIONS = {
295
+ maxChars: 1048576
296
+ };
297
+ function createLineBuffer(options) {
298
+ const limits = {
299
+ ...DEFAULT_LINE_BUFFER_OPTIONS,
300
+ ...options
301
+ };
302
+ const tokenizer = createNewlineTokenizer("line");
155
303
  const clear = () => {
156
- buffer = "";
304
+ tokenizer.clear();
157
305
  };
158
306
  const feed = (chunk) => {
159
- buffer += chunk;
307
+ const events = tokenizer.feed(chunk);
160
308
  const out = [];
161
- for (; ; ) {
162
- const crlf = buffer.indexOf("\r\n");
163
- if (crlf >= 0) {
164
- out.push(buffer.slice(0, crlf));
165
- buffer = buffer.slice(crlf + 2);
166
- continue;
309
+ for (const event of events) {
310
+ if (event.type === "line") {
311
+ out.push(event.content);
167
312
  }
168
- const cr = buffer.indexOf("\r");
169
- if (cr >= 0 && cr + 1 < buffer.length && buffer[cr + 1] !== "\n") {
170
- out.push(buffer.slice(0, cr));
171
- buffer = buffer.slice(cr + 1);
172
- continue;
173
- }
174
- const nl = buffer.indexOf("\n");
175
- if (nl >= 0) {
176
- out.push(buffer.slice(0, nl));
177
- buffer = buffer.slice(nl + 1);
178
- continue;
179
- }
180
- if (cr >= 0 && cr + 1 === buffer.length) {
181
- break;
182
- }
183
- break;
184
313
  }
185
- return out;
314
+ const overflowed = tokenizer.trimPending(limits.maxChars);
315
+ return { lines: out, overflowed };
186
316
  };
187
317
  return { feed, clear };
188
318
  }
189
319
 
320
+ // src/session/internal/receive-replay-buffer.ts
321
+ import { Observable, Subject } from "rxjs";
322
+ function totalChars(chunks) {
323
+ return chunks.reduce((sum, chunk) => sum + chunk.length, 0);
324
+ }
325
+ function createReceiveReplayBuffer(options) {
326
+ const chunks = [];
327
+ const live$ = new Subject();
328
+ let completed = false;
329
+ const trim = () => {
330
+ let overflowed = false;
331
+ while (chunks.length > options.bufferSize) {
332
+ chunks.shift();
333
+ overflowed = true;
334
+ }
335
+ if (options.maxChars > 0) {
336
+ let chars = totalChars(chunks);
337
+ while (chars > options.maxChars && chunks.length > 1) {
338
+ chars -= chunks.shift().length;
339
+ overflowed = true;
340
+ }
341
+ }
342
+ return overflowed;
343
+ };
344
+ const next = (chunk) => {
345
+ if (completed) {
346
+ return { overflowed: false };
347
+ }
348
+ chunks.push(chunk);
349
+ const overflowed = trim();
350
+ live$.next(chunk);
351
+ return { overflowed };
352
+ };
353
+ const asObservable = () => new Observable((subscriber) => {
354
+ for (const chunk of chunks) {
355
+ subscriber.next(chunk);
356
+ }
357
+ if (completed) {
358
+ subscriber.complete();
359
+ return;
360
+ }
361
+ return live$.subscribe(subscriber);
362
+ });
363
+ const complete = () => {
364
+ if (completed) {
365
+ return;
366
+ }
367
+ completed = true;
368
+ chunks.length = 0;
369
+ live$.complete();
370
+ };
371
+ return { next, asObservable, complete };
372
+ }
373
+
190
374
  // src/session/normalize-serial-error.ts
191
375
  var DEFAULT_MESSAGE_PREFIX = "Serial operation failed";
192
376
  var isDomExceptionWithName = (error, name) => typeof DOMException !== "undefined" && error instanceof DOMException && error.name === name;
@@ -211,7 +395,7 @@ function normalizeSerialError(error, options) {
211
395
  }
212
396
 
213
397
  // src/session/read-pump.ts
214
- function createReadPump(port, { onChunk, onError }) {
398
+ function createReadPump(port, { onChunk, onError, onDone }) {
215
399
  let reader = null;
216
400
  let running = false;
217
401
  let stopped = false;
@@ -233,6 +417,9 @@ function createReadPump(port, { onChunk, onError }) {
233
417
  while (!stopped) {
234
418
  const { done, value } = await reader.read();
235
419
  if (done) {
420
+ if (!stopped) {
421
+ onDone?.();
422
+ }
236
423
  break;
237
424
  }
238
425
  if (value && value.byteLength > 0) {
@@ -302,13 +489,13 @@ function createReadPump(port, { onChunk, onError }) {
302
489
  }
303
490
 
304
491
  // src/session/send-queue.ts
305
- import { Observable, defer } from "rxjs";
492
+ import { Observable as Observable2, defer } from "rxjs";
306
493
  function createSendQueue() {
307
494
  let chain = Promise.resolve();
308
495
  return {
309
496
  enqueue(operation) {
310
497
  return defer(
311
- () => new Observable((subscriber) => {
498
+ () => new Observable2((subscriber) => {
312
499
  let cancelled = false;
313
500
  const run = async () => {
314
501
  try {
@@ -341,10 +528,33 @@ function createSendQueue() {
341
528
  }
342
529
 
343
530
  // src/session/serial-session-options.ts
531
+ var MAX_RECEIVE_REPLAY_BUFFER_SIZE = 65536;
532
+ var MAX_RECEIVE_REPLAY_MAX_CHARS = 1048576;
344
533
  var DEFAULT_RECEIVE_REPLAY = {
345
534
  enabled: false,
346
- bufferSize: 512
535
+ bufferSize: 512,
536
+ maxChars: 0
347
537
  };
538
+ function resolveReceiveReplayOptions(options) {
539
+ const merged = {
540
+ ...DEFAULT_RECEIVE_REPLAY,
541
+ ...options
542
+ };
543
+ const { bufferSize, maxChars } = merged;
544
+ if (!Number.isSafeInteger(bufferSize) || bufferSize < 1 || bufferSize > MAX_RECEIVE_REPLAY_BUFFER_SIZE) {
545
+ throw new SerialError(
546
+ "INVALID_RECEIVE_REPLAY_OPTIONS" /* INVALID_RECEIVE_REPLAY_OPTIONS */,
547
+ `Invalid receiveReplay.bufferSize: ${bufferSize}. Must be a safe integer between 1 and ${MAX_RECEIVE_REPLAY_BUFFER_SIZE}.`
548
+ );
549
+ }
550
+ if (!Number.isSafeInteger(maxChars) || maxChars < 0 || maxChars > MAX_RECEIVE_REPLAY_MAX_CHARS) {
551
+ throw new SerialError(
552
+ "INVALID_RECEIVE_REPLAY_OPTIONS" /* INVALID_RECEIVE_REPLAY_OPTIONS */,
553
+ `Invalid receiveReplay.maxChars: ${maxChars}. Must be a safe integer between 0 and ${MAX_RECEIVE_REPLAY_MAX_CHARS}.`
554
+ );
555
+ }
556
+ return merged;
557
+ }
348
558
  var DEFAULT_SERIAL_SESSION_OPTIONS = {
349
559
  baudRate: 9600,
350
560
  dataBits: 8,
@@ -353,7 +563,9 @@ var DEFAULT_SERIAL_SESSION_OPTIONS = {
353
563
  bufferSize: 255,
354
564
  flowControl: "none",
355
565
  filters: void 0,
356
- receiveReplay: { ...DEFAULT_RECEIVE_REPLAY }
566
+ receiveReplay: { ...DEFAULT_RECEIVE_REPLAY },
567
+ terminalBuffer: { ...DEFAULT_TERMINAL_BUFFER_OPTIONS },
568
+ lineBuffer: { ...DEFAULT_LINE_BUFFER_OPTIONS }
357
569
  };
358
570
 
359
571
  // src/session/serial-session-state.ts
@@ -363,19 +575,21 @@ var SerialSessionState = {
363
575
  Connected: "connected",
364
576
  Disconnecting: "disconnecting",
365
577
  Unsupported: "unsupported",
366
- Error: "error"
578
+ Error: "error",
579
+ Disposed: "disposed"
367
580
  };
368
581
 
369
582
  // src/session/session-state-machine.ts
370
583
  import { BehaviorSubject } from "rxjs";
371
584
  var S = SerialSessionState;
372
585
  var ALLOWED_TRANSITIONS = {
373
- [S.Idle]: [S.Connecting, S.Error],
374
- [S.Connecting]: [S.Connected, S.Error, S.Idle],
375
- [S.Connected]: [S.Disconnecting, S.Error],
376
- [S.Disconnecting]: [S.Idle, S.Error],
377
- [S.Error]: [S.Idle, S.Connecting],
378
- [S.Unsupported]: []
586
+ [S.Idle]: [S.Connecting, S.Error, S.Disposed],
587
+ [S.Connecting]: [S.Connected, S.Error, S.Idle, S.Disposed],
588
+ [S.Connected]: [S.Disconnecting, S.Error, S.Disposed],
589
+ [S.Disconnecting]: [S.Idle, S.Error, S.Disposed],
590
+ [S.Error]: [S.Idle, S.Connecting, S.Disposed],
591
+ [S.Unsupported]: [S.Disposed],
592
+ [S.Disposed]: []
379
593
  };
380
594
  var SessionStateMachine = class {
381
595
  constructor(initial = SerialSessionState.Idle) {
@@ -405,6 +619,9 @@ var SessionStateMachine = class {
405
619
  toUnsupported() {
406
620
  return this.transition(S.Unsupported);
407
621
  }
622
+ toDisposed() {
623
+ return this.transition(S.Disposed);
624
+ }
408
625
  complete() {
409
626
  this.subject.complete();
410
627
  }
@@ -433,25 +650,33 @@ function createSerialSession(options) {
433
650
  ...DEFAULT_SERIAL_SESSION_OPTIONS,
434
651
  ...options,
435
652
  filters: options?.filters,
436
- receiveReplay: {
437
- ...DEFAULT_SERIAL_SESSION_OPTIONS.receiveReplay,
438
- ...options?.receiveReplay
653
+ receiveReplay: resolveReceiveReplayOptions(options?.receiveReplay),
654
+ terminalBuffer: {
655
+ ...DEFAULT_SERIAL_SESSION_OPTIONS.terminalBuffer,
656
+ ...options?.terminalBuffer
657
+ },
658
+ lineBuffer: {
659
+ ...DEFAULT_SERIAL_SESSION_OPTIONS.lineBuffer,
660
+ ...options?.lineBuffer
439
661
  }
440
662
  };
441
663
  const supported = hasWebSerialSupport();
442
664
  const machine = new SessionStateMachine(
443
665
  supported ? SerialSessionState.Idle : SerialSessionState.Unsupported
444
666
  );
445
- const errorsSubject = new Subject();
446
- const receiveSubject = new Subject();
447
- const linesSubject = new Subject();
667
+ const errorsSubject = new Subject2();
668
+ const receiveSubject = new Subject2();
669
+ const linesSubject = new Subject2();
448
670
  const sendQueue = createSendQueue();
449
671
  const textEncoder = new TextEncoder();
450
- const lineBuffer = createLineBuffer();
672
+ const lineBuffer = createLineBuffer(resolvedOptions.lineBuffer);
451
673
  const errors$ = errorsSubject.asObservable();
452
674
  const receive$ = receiveSubject.asObservable();
453
675
  const lines$ = linesSubject.asObservable();
454
- const terminalText$ = createTerminalBuffer(receive$).text$;
676
+ const terminalText$ = createTerminalBuffer(
677
+ receive$,
678
+ resolvedOptions.terminalBuffer
679
+ ).text$;
455
680
  const isConnected$ = machine.state$.pipe(
456
681
  map2((state) => state === SerialSessionState.Connected),
457
682
  distinctUntilChanged()
@@ -477,13 +702,70 @@ function createSerialSession(options) {
477
702
  activeReceiveReplay.complete();
478
703
  activeReceiveReplay = null;
479
704
  }
480
- const rs = new ReplaySubject(resolvedOptions.receiveReplay.bufferSize);
481
- activeReceiveReplay = rs;
482
- receiveReplayStream$.next(rs.asObservable());
705
+ const buffer = createReceiveReplayBuffer({
706
+ bufferSize: resolvedOptions.receiveReplay.bufferSize,
707
+ maxChars: resolvedOptions.receiveReplay.maxChars
708
+ });
709
+ activeReceiveReplay = buffer;
710
+ receiveReplayStream$.next(buffer.asObservable());
483
711
  };
484
712
  const receiveReplay$ = receiveReplayStream$ ? receiveReplayStream$.pipe(switchMap((inner) => inner), share()) : receive$;
485
713
  let activePort = null;
486
714
  let activePump = null;
715
+ let activeConnectCancel = null;
716
+ let disposed = false;
717
+ const createDisposedError = () => new SerialError(
718
+ "SESSION_DISPOSED" /* SESSION_DISPOSED */,
719
+ "SerialSession has been disposed"
720
+ );
721
+ const completeSession = async () => {
722
+ const current = machine.current;
723
+ if (current === SerialSessionState.Connecting) {
724
+ activeConnectCancel?.();
725
+ }
726
+ sendQueue.clear();
727
+ if (current === SerialSessionState.Connected || current === SerialSessionState.Error || current === SerialSessionState.Disconnecting) {
728
+ const portToClose = activePort;
729
+ await teardownPump();
730
+ await closePortSafely(portToClose);
731
+ setActivePort(null);
732
+ }
733
+ lineBuffer.clear();
734
+ machine.toDisposed();
735
+ machine.complete();
736
+ errorsSubject.complete();
737
+ receiveSubject.complete();
738
+ linesSubject.complete();
739
+ portInfoSubject.complete();
740
+ receiveReplayStream$?.complete();
741
+ };
742
+ const dispose$ = () => new Observable4((subscriber) => {
743
+ if (disposed) {
744
+ subscriber.next();
745
+ subscriber.complete();
746
+ return;
747
+ }
748
+ disposed = true;
749
+ const run = async () => {
750
+ try {
751
+ await completeSession();
752
+ subscriber.next();
753
+ subscriber.complete();
754
+ } catch (error) {
755
+ const serialError = normalizeSerialError(error, {
756
+ fallbackCode: "UNKNOWN" /* UNKNOWN */,
757
+ messagePrefix: "Unexpected dispose failure"
758
+ });
759
+ subscriber.error(serialError);
760
+ }
761
+ };
762
+ void run();
763
+ });
764
+ const clearActiveConnectCancel = (cancel) => {
765
+ if (activeConnectCancel === cancel) {
766
+ activeConnectCancel = null;
767
+ }
768
+ };
487
769
  const setActivePort = (port) => {
488
770
  activePort = port;
489
771
  portInfoSubject.next(port ? port.getInfo() : null);
@@ -508,6 +790,9 @@ function createSerialSession(options) {
508
790
  };
509
791
  const reportError = (error, severity, options2) => {
510
792
  const serialError = normalizeSerialError(error, options2);
793
+ if (disposed) {
794
+ return serialError;
795
+ }
511
796
  errorsSubject.next(serialError);
512
797
  if (severity === "fatal") {
513
798
  machine.toError();
@@ -541,7 +826,11 @@ function createSerialSession(options) {
541
826
  return hasWebSerialSupport();
542
827
  },
543
828
  connect$() {
544
- return new Observable3((subscriber) => {
829
+ return new Observable4((subscriber) => {
830
+ if (disposed) {
831
+ subscriber.error(createDisposedError());
832
+ return;
833
+ }
545
834
  if (!hasWebSerialSupport()) {
546
835
  const error = reportError(
547
836
  new SerialError(
@@ -568,6 +857,13 @@ function createSerialSession(options) {
568
857
  return;
569
858
  }
570
859
  let cancelled = false;
860
+ const cancelInFlightConnect = () => {
861
+ cancelled = true;
862
+ if (machine.current === SerialSessionState.Connecting) {
863
+ machine.toIdle();
864
+ }
865
+ };
866
+ activeConnectCancel = cancelInFlightConnect;
571
867
  machine.toConnecting();
572
868
  const run = async () => {
573
869
  let selectedPort = null;
@@ -596,7 +892,7 @@ function createSerialSession(options) {
596
892
  }
597
893
  return;
598
894
  }
599
- if (cancelled) {
895
+ if (cancelled || disposed) {
600
896
  await closePortSafely(selectedPort);
601
897
  return;
602
898
  }
@@ -609,33 +905,89 @@ function createSerialSession(options) {
609
905
  onChunk: (text) => {
610
906
  receiveSubject.next(text);
611
907
  if (activeReceiveReplay) {
612
- activeReceiveReplay.next(text);
908
+ const { overflowed: overflowed2 } = activeReceiveReplay.next(text);
909
+ if (overflowed2) {
910
+ reportError(
911
+ new SerialError(
912
+ "RECEIVE_REPLAY_BUFFER_OVERFLOW" /* RECEIVE_REPLAY_BUFFER_OVERFLOW */,
913
+ `Receive replay buffer exceeded configured limits; oldest chunks were discarded`
914
+ ),
915
+ "non-fatal",
916
+ {
917
+ fallbackCode: "RECEIVE_REPLAY_BUFFER_OVERFLOW" /* RECEIVE_REPLAY_BUFFER_OVERFLOW */
918
+ }
919
+ );
920
+ }
921
+ }
922
+ const { lines, overflowed } = lineBuffer.feed(text);
923
+ if (overflowed) {
924
+ reportError(
925
+ new SerialError(
926
+ "LINE_BUFFER_OVERFLOW" /* LINE_BUFFER_OVERFLOW */,
927
+ `Line buffer exceeded maxChars (${resolvedOptions.lineBuffer.maxChars}); leading data was discarded`
928
+ ),
929
+ "non-fatal",
930
+ { fallbackCode: "LINE_BUFFER_OVERFLOW" /* LINE_BUFFER_OVERFLOW */ }
931
+ );
613
932
  }
614
- for (const line of lineBuffer.feed(text)) {
933
+ for (const line of lines) {
615
934
  linesSubject.next(line);
616
935
  }
617
936
  },
618
937
  onError: (pumpError) => reportError(pumpError, "fatal", {
619
938
  fallbackCode: "READ_FAILED" /* READ_FAILED */,
620
939
  messagePrefix: "Read pump failed"
621
- })
940
+ }),
941
+ onDone: () => {
942
+ if (machine.current !== SerialSessionState.Connected) {
943
+ return;
944
+ }
945
+ reportError(
946
+ new SerialError(
947
+ "CONNECTION_LOST" /* CONNECTION_LOST */,
948
+ "Read pump ended unexpectedly: stream closed while connected"
949
+ ),
950
+ "fatal",
951
+ {
952
+ fallbackCode: "CONNECTION_LOST" /* CONNECTION_LOST */
953
+ }
954
+ );
955
+ }
622
956
  });
623
957
  activePump.start();
624
958
  sendQueue.clear();
959
+ if (disposed) {
960
+ await teardownPump();
961
+ await closePortSafely(selectedPort);
962
+ setActivePort(null);
963
+ return;
964
+ }
625
965
  machine.toConnected();
626
966
  subscriber.next();
627
967
  subscriber.complete();
628
968
  };
629
969
  void run();
630
970
  return () => {
631
- cancelled = true;
971
+ clearActiveConnectCancel(cancelInFlightConnect);
972
+ cancelInFlightConnect();
632
973
  };
633
974
  });
634
975
  },
635
976
  disconnect$() {
636
- return new Observable3((subscriber) => {
977
+ return new Observable4((subscriber) => {
978
+ if (disposed) {
979
+ subscriber.next();
980
+ subscriber.complete();
981
+ return;
982
+ }
637
983
  const current = machine.current;
638
- if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported) {
984
+ if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported || current === SerialSessionState.Disconnecting) {
985
+ subscriber.next();
986
+ subscriber.complete();
987
+ return;
988
+ }
989
+ if (current === SerialSessionState.Connecting) {
990
+ activeConnectCancel?.();
639
991
  subscriber.next();
640
992
  subscriber.complete();
641
993
  return;
@@ -672,7 +1024,9 @@ function createSerialSession(options) {
672
1024
  }
673
1025
  }
674
1026
  setActivePort(null);
675
- machine.toIdle();
1027
+ if (!disposed) {
1028
+ machine.toIdle();
1029
+ }
676
1030
  subscriber.next();
677
1031
  subscriber.complete();
678
1032
  } catch (error) {
@@ -686,7 +1040,14 @@ function createSerialSession(options) {
686
1040
  void run();
687
1041
  });
688
1042
  },
1043
+ dispose$,
1044
+ destroy$: dispose$,
689
1045
  send$(data) {
1046
+ if (disposed) {
1047
+ return new Observable4((subscriber) => {
1048
+ subscriber.error(createDisposedError());
1049
+ });
1050
+ }
690
1051
  return sendQueue.enqueue(async () => {
691
1052
  const payload = typeof data === "string" ? textEncoder.encode(data) : data;
692
1053
  try {
@@ -716,6 +1077,8 @@ function createSerialSession(options) {
716
1077
  };
717
1078
  }
718
1079
  export {
1080
+ DEFAULT_LINE_BUFFER_OPTIONS,
1081
+ DEFAULT_TERMINAL_BUFFER_OPTIONS,
719
1082
  SerialError,
720
1083
  SerialErrorCode,
721
1084
  SerialSessionState,