@gurezo/web-serial-rxjs 2.4.0 → 3.1.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 (57) hide show
  1. package/README.ja.md +17 -10
  2. package/README.md +21 -12
  3. package/dist/errors/serial-error-code.d.ts +81 -37
  4. package/dist/errors/serial-error-code.d.ts.map +1 -1
  5. package/dist/errors/serial-error-context.d.ts +61 -0
  6. package/dist/errors/serial-error-context.d.ts.map +1 -0
  7. package/dist/errors/serial-error.d.ts +42 -21
  8. package/dist/errors/serial-error.d.ts.map +1 -1
  9. package/dist/index.d.ts +42 -12
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.mjs +1278 -635
  12. package/dist/index.mjs.map +4 -4
  13. package/dist/internal/assert-never.d.ts +11 -0
  14. package/dist/internal/assert-never.d.ts.map +1 -0
  15. package/dist/internal/branded-numbers.d.ts +37 -0
  16. package/dist/internal/branded-numbers.d.ts.map +1 -0
  17. package/dist/internal/strip-ansi-sequences.d.ts +25 -0
  18. package/dist/internal/strip-ansi-sequences.d.ts.map +1 -0
  19. package/dist/session/create-serial-session.d.ts +6 -3
  20. package/dist/session/create-serial-session.d.ts.map +1 -1
  21. package/dist/session/index.d.ts +7 -2
  22. package/dist/session/index.d.ts.map +1 -1
  23. package/dist/session/internal/build-request-options.d.ts +7 -7
  24. package/dist/session/internal/build-request-options.d.ts.map +1 -1
  25. package/dist/session/internal/error-severity.d.ts +38 -0
  26. package/dist/session/internal/error-severity.d.ts.map +1 -0
  27. package/dist/session/internal/line-buffer.d.ts +15 -4
  28. package/dist/session/internal/line-buffer.d.ts.map +1 -1
  29. package/dist/session/internal/receive-pipeline.d.ts +34 -0
  30. package/dist/session/internal/receive-pipeline.d.ts.map +1 -0
  31. package/dist/session/internal/receive-replay-buffer.d.ts +3 -2
  32. package/dist/session/internal/receive-replay-buffer.d.ts.map +1 -1
  33. package/dist/session/internal/session-error-reporter.d.ts +32 -0
  34. package/dist/session/internal/session-error-reporter.d.ts.map +1 -0
  35. package/dist/session/internal/session-lifecycle.d.ts +44 -0
  36. package/dist/session/internal/session-lifecycle.d.ts.map +1 -0
  37. package/dist/session/internal/validate-serial-port-filters.d.ts +12 -0
  38. package/dist/session/internal/validate-serial-port-filters.d.ts.map +1 -0
  39. package/dist/session/is-connected-session-state.d.ts +11 -0
  40. package/dist/session/is-connected-session-state.d.ts.map +1 -0
  41. package/dist/session/normalize-serial-error.d.ts +1 -1
  42. package/dist/session/normalize-serial-error.d.ts.map +1 -1
  43. package/dist/session/serial-session-options.d.ts +118 -65
  44. package/dist/session/serial-session-options.d.ts.map +1 -1
  45. package/dist/session/serial-session-state.d.ts +50 -12
  46. package/dist/session/serial-session-state.d.ts.map +1 -1
  47. package/dist/session/serial-session.d.ts +52 -18
  48. package/dist/session/serial-session.d.ts.map +1 -1
  49. package/dist/session/session-runtime.d.ts +140 -0
  50. package/dist/session/session-runtime.d.ts.map +1 -0
  51. package/dist/terminal/create-terminal-buffer.d.ts +15 -3
  52. package/dist/terminal/create-terminal-buffer.d.ts.map +1 -1
  53. package/dist/types.d.ts +18 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/package.json +1 -1
  56. package/dist/session/session-state-machine.d.ts +0 -40
  57. package/dist/session/session-state-machine.d.ts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,77 +1,233 @@
1
+ // src/internal/assert-never.ts
2
+ function assertNever(value) {
3
+ throw new Error(`Unexpected value: ${String(value)}`);
4
+ }
5
+
1
6
  // src/session/create-serial-session.ts
2
7
  import {
3
- BehaviorSubject as BehaviorSubject2,
8
+ BehaviorSubject as BehaviorSubject3,
4
9
  distinctUntilChanged,
5
10
  map as map2,
6
- Observable as Observable4,
7
- share,
8
- Subject as Subject2,
9
- switchMap
11
+ Observable as Observable5,
12
+ Subject as Subject3
10
13
  } from "rxjs";
11
14
 
12
15
  // src/errors/serial-error-code.ts
13
- var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
14
- SerialErrorCode2["BROWSER_NOT_SUPPORTED"] = "BROWSER_NOT_SUPPORTED";
15
- SerialErrorCode2["PORT_NOT_AVAILABLE"] = "PORT_NOT_AVAILABLE";
16
- SerialErrorCode2["PORT_OPEN_FAILED"] = "PORT_OPEN_FAILED";
17
- SerialErrorCode2["PORT_ALREADY_OPEN"] = "PORT_ALREADY_OPEN";
18
- SerialErrorCode2["PORT_NOT_OPEN"] = "PORT_NOT_OPEN";
19
- SerialErrorCode2["READ_FAILED"] = "READ_FAILED";
20
- SerialErrorCode2["WRITE_FAILED"] = "WRITE_FAILED";
21
- SerialErrorCode2["CONNECTION_LOST"] = "CONNECTION_LOST";
22
- SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
23
- SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
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";
29
- SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
30
- return SerialErrorCode2;
31
- })(SerialErrorCode || {});
32
-
33
- // src/errors/serial-error.ts
34
- var SerialError = class _SerialError extends Error {
16
+ var SerialErrorCode = {
35
17
  /**
36
- * Creates a new SerialError instance.
18
+ * Browser does not support the Web Serial API.
37
19
  *
38
- * @param code - The error code identifying the type of error
39
- * @param message - A human-readable error message
40
- * @param originalError - The original error that caused this SerialError, if any
20
+ * Emitted on `connect$` when `navigator.serial` is unavailable.
21
+ *
22
+ * **Suggested action**: Inform the user to use a supported browser.
23
+ *
24
+ * @see {@link https://github.com/gurezo/web-serial-rxjs/issues/438 | Issue #438}
41
25
  */
42
- constructor(code, message, originalError) {
43
- super(message);
44
- this.name = "SerialError";
45
- this.code = code;
46
- this.originalError = originalError;
47
- if (Error.captureStackTrace) {
48
- Error.captureStackTrace(this, _SerialError);
49
- }
50
- }
26
+ BROWSER_NOT_SUPPORTED: "BROWSER_NOT_SUPPORTED",
51
27
  /**
52
- * Check if the error matches a specific error code.
28
+ * Serial port is not available.
53
29
  *
54
- * This is a convenience method for checking the error code without directly
55
- * comparing the code property.
30
+ * **Reserved not emitted in v3.x.** The current implementation uses only
31
+ * `navigator.serial.requestPort`; there is no `getPorts` API path. Scheduled
32
+ * for removal in the next major version.
56
33
  *
57
- * @param code - The error code to check against
58
- * @returns `true` if this error's code matches the provided code, `false` otherwise
34
+ * **Suggested action**: Handle port acquisition failures with
35
+ * {@link SerialErrorCode.PORT_OPEN_FAILED} or
36
+ * {@link SerialErrorCode.OPERATION_CANCELLED} instead.
59
37
  *
60
- * @example
61
- * ```typescript
62
- * if (error.is(SerialErrorCode.PORT_NOT_OPEN)) {
63
- * // Handle port not open error
64
- * }
65
- * ```
38
+ * @deprecated Not emitted at runtime in v3.x. Will be removed in the next major version.
39
+ * @see {@link https://github.com/gurezo/web-serial-rxjs/issues/438 | Issue #438}
66
40
  */
67
- is(code) {
68
- return this.code === code;
69
- }
41
+ PORT_NOT_AVAILABLE: "PORT_NOT_AVAILABLE",
42
+ /**
43
+ * Failed to open the serial port.
44
+ *
45
+ * Emitted on `connect$` when `port.open()` rejects.
46
+ *
47
+ * **Suggested action**: Verify connection parameters and check hardware connections.
48
+ */
49
+ PORT_OPEN_FAILED: "PORT_OPEN_FAILED",
50
+ /**
51
+ * Serial port is already open.
52
+ *
53
+ * Emitted on `connect$` when the session is not in `'idle'` or `'error'`
54
+ * (non-fatal; session state is unchanged).
55
+ *
56
+ * **Suggested action**: Disconnect the current port before connecting a new one.
57
+ */
58
+ PORT_ALREADY_OPEN: "PORT_ALREADY_OPEN",
59
+ /**
60
+ * Serial port is not open.
61
+ *
62
+ * Emitted when `send$` or `disconnect$` is called in an invalid session
63
+ * state (for example before `connect$` completes).
64
+ *
65
+ * **Suggested action**: Call {@link SerialSession.connect$} before sending data.
66
+ */
67
+ PORT_NOT_OPEN: "PORT_NOT_OPEN",
68
+ /**
69
+ * Failed to read from the serial port.
70
+ *
71
+ * This error occurs when reading data from the port fails, typically due to
72
+ * connection loss, hardware issues, or stream errors.
73
+ *
74
+ * **Suggested action**: Check the connection and hardware, then retry the read operation.
75
+ */
76
+ READ_FAILED: "READ_FAILED",
77
+ /**
78
+ * Failed to write to the serial port.
79
+ *
80
+ * This error occurs when writing data to the port fails, typically due to
81
+ * connection loss, hardware issues, or stream errors.
82
+ *
83
+ * **Suggested action**: Check the connection and hardware, then retry the write operation.
84
+ */
85
+ WRITE_FAILED: "WRITE_FAILED",
86
+ /**
87
+ * Serial port connection was lost.
88
+ *
89
+ * This error occurs when the connection to the serial port is unexpectedly
90
+ * terminated, such as when the device is disconnected or the port is closed
91
+ * by another process.
92
+ *
93
+ * **Suggested action**: Check the physical connection and reconnect if needed.
94
+ */
95
+ CONNECTION_LOST: "CONNECTION_LOST",
96
+ /**
97
+ * Invalid filter options provided.
98
+ *
99
+ * This error occurs when port filter options are invalid, such as when
100
+ * filter values are out of range or missing required fields.
101
+ *
102
+ * **Suggested action**: Verify filter options match the expected format and value ranges.
103
+ */
104
+ INVALID_FILTER_OPTIONS: "INVALID_FILTER_OPTIONS",
105
+ /**
106
+ * Operation was cancelled by the user.
107
+ *
108
+ * This error occurs when the user cancels a port selection dialog or aborts
109
+ * an operation before it completes.
110
+ *
111
+ * **Suggested action**: This is a normal condition - no action required, but you may want
112
+ * to inform the user that the operation was cancelled.
113
+ */
114
+ OPERATION_CANCELLED: "OPERATION_CANCELLED",
115
+ /**
116
+ * Operation timed out before completion.
117
+ *
118
+ * **Reserved — not emitted in v3.x.** No timeout / prompt detection /
119
+ * transaction API exists yet. Scheduled for removal in the next major version
120
+ * unless a future API adopts this code.
121
+ *
122
+ * @deprecated Not emitted at runtime in v3.x. Will be removed in the next major version.
123
+ * @see {@link https://github.com/gurezo/web-serial-rxjs/issues/438 | Issue #438}
124
+ */
125
+ OPERATION_TIMEOUT: "OPERATION_TIMEOUT",
126
+ /**
127
+ * Internal line buffer exceeded its configured size limit.
128
+ *
129
+ * This error occurs when the incomplete line tail held by the {@link SerialSession.lines$}
130
+ * framing buffer grows beyond {@link SerialSessionOptions.lineBuffer} `maxChars`.
131
+ * Leading characters are discarded to bound memory; the session remains connected.
132
+ *
133
+ * **Suggested action**: Increase `lineBuffer.maxChars`, handle framing on `receive$`,
134
+ * or ensure the device sends line terminators.
135
+ */
136
+ LINE_BUFFER_OVERFLOW: "LINE_BUFFER_OVERFLOW",
137
+ /**
138
+ * Invalid receive replay options provided.
139
+ *
140
+ * This error occurs when {@link SerialSessionOptions.receiveReplay} values are
141
+ * out of range, such as a non-integer `bufferSize` or `maxChars`.
142
+ *
143
+ * **Suggested action**: Verify `receiveReplay` options match the documented
144
+ * ranges and value types.
145
+ */
146
+ INVALID_RECEIVE_REPLAY_OPTIONS: "INVALID_RECEIVE_REPLAY_OPTIONS",
147
+ /**
148
+ * Invalid terminal buffer options provided.
149
+ *
150
+ * This error occurs when {@link SerialSessionOptions.terminalBuffer} values are
151
+ * out of range, such as a negative or non-integer `maxLines` or `maxChars`.
152
+ *
153
+ * **Suggested action**: Verify `terminalBuffer` options match the documented
154
+ * ranges and value types.
155
+ */
156
+ INVALID_TERMINAL_BUFFER_OPTIONS: "INVALID_TERMINAL_BUFFER_OPTIONS",
157
+ /**
158
+ * Invalid line buffer options provided.
159
+ *
160
+ * This error occurs when {@link SerialSessionOptions.lineBuffer} values are
161
+ * out of range, such as a negative or non-integer `maxChars`.
162
+ *
163
+ * **Suggested action**: Verify `lineBuffer` options match the documented
164
+ * ranges and value types.
165
+ */
166
+ INVALID_LINE_BUFFER_OPTIONS: "INVALID_LINE_BUFFER_OPTIONS",
167
+ /**
168
+ * Invalid connection options provided.
169
+ *
170
+ * This error occurs when connection fields such as `baudRate` are out of
171
+ * range at session creation time.
172
+ *
173
+ * **Suggested action**: Verify connection options match the documented
174
+ * ranges and value types.
175
+ */
176
+ INVALID_CONNECTION_OPTIONS: "INVALID_CONNECTION_OPTIONS",
177
+ /**
178
+ * Receive replay buffer exceeded its configured character limit.
179
+ *
180
+ * This error occurs when buffered chunks on {@link SerialSession.receiveReplay$}
181
+ * exceed {@link SerialSessionOptions.receiveReplay} `maxChars`. Oldest chunks
182
+ * are discarded to bound memory; the session remains connected.
183
+ *
184
+ * **Suggested action**: Increase `receiveReplay.maxChars`, reduce chunk size at
185
+ * the source, or subscribe earlier to avoid relying on a large replay buffer.
186
+ */
187
+ RECEIVE_REPLAY_BUFFER_OVERFLOW: "RECEIVE_REPLAY_BUFFER_OVERFLOW",
188
+ /**
189
+ * Session has been disposed and can no longer be used.
190
+ *
191
+ * This error occurs when calling {@link SerialSession.connect$} or
192
+ * {@link SerialSession.send$} after {@link SerialSession.dispose$} has
193
+ * completed. Create a new session instead of reusing a disposed instance.
194
+ *
195
+ * **Suggested action**: Call {@link SerialSession.dispose$} only when
196
+ * permanently tearing down a session, then create a new one if needed.
197
+ */
198
+ SESSION_DISPOSED: "SESSION_DISPOSED",
199
+ /**
200
+ * Unknown error occurred.
201
+ *
202
+ * Emitted as a fallback when dispose or disconnect encounters an error that
203
+ * cannot be classified more specifically. The underlying failure is on
204
+ * {@link SerialError.context | context.cause}.
205
+ *
206
+ * **Suggested action**: Check the error message and `context.cause` for more details.
207
+ */
208
+ UNKNOWN: "UNKNOWN"
70
209
  };
71
210
 
72
211
  // src/terminal/create-terminal-buffer.ts
73
212
  import { map, scan, shareReplay } from "rxjs";
74
213
 
214
+ // src/internal/branded-numbers.ts
215
+ function brandBaudRate(value) {
216
+ return value;
217
+ }
218
+ function brandSerialPortBufferSize(value) {
219
+ return value;
220
+ }
221
+ function brandReceiveReplayBufferSize(value) {
222
+ return value;
223
+ }
224
+ function brandMaxChars(value) {
225
+ return value;
226
+ }
227
+ function brandMaxLines(value) {
228
+ return value;
229
+ }
230
+
75
231
  // src/internal/newline-tokenizer.ts
76
232
  function createNewlineTokenizer(mode) {
77
233
  let pending = "";
@@ -158,6 +314,89 @@ function createNewlineTokenizer(mode) {
158
314
  };
159
315
  }
160
316
 
317
+ // src/internal/strip-ansi-sequences.ts
318
+ var ESC = "\x1B";
319
+ var CSI_FINAL_BYTE = /[@-~]/;
320
+ var BEL = "\x07";
321
+ var OSC_STRING_TERMINATOR = "\x1B\\";
322
+ function findOscTerminator(oscBody) {
323
+ let earliest = null;
324
+ const belIndex = oscBody.indexOf(BEL);
325
+ if (belIndex >= 0) {
326
+ earliest = { index: belIndex, length: 1 };
327
+ }
328
+ const stIndex = oscBody.indexOf(OSC_STRING_TERMINATOR);
329
+ if (stIndex >= 0) {
330
+ const candidate = { index: stIndex, length: OSC_STRING_TERMINATOR.length };
331
+ if (!earliest || stIndex < earliest.index) {
332
+ earliest = candidate;
333
+ }
334
+ }
335
+ return earliest;
336
+ }
337
+ function createAnsiStripper() {
338
+ let pending = "";
339
+ const feed = (chunk) => {
340
+ const input = pending + chunk;
341
+ pending = "";
342
+ let output = "";
343
+ let i = 0;
344
+ while (i < input.length) {
345
+ const escIndex = input.indexOf(ESC, i);
346
+ if (escIndex < 0) {
347
+ output += input.slice(i);
348
+ break;
349
+ }
350
+ output += input.slice(i, escIndex);
351
+ const next = escIndex + 1;
352
+ if (next >= input.length) {
353
+ pending = ESC;
354
+ break;
355
+ }
356
+ const code = input.charCodeAt(next);
357
+ if (code === 91) {
358
+ const csiStart = next + 1;
359
+ let j = csiStart;
360
+ while (j < input.length && !CSI_FINAL_BYTE.test(input.charAt(j))) {
361
+ j++;
362
+ }
363
+ if (j < input.length) {
364
+ i = j + 1;
365
+ continue;
366
+ }
367
+ pending = input.slice(escIndex);
368
+ break;
369
+ }
370
+ if (code === 93) {
371
+ const oscBody = input.slice(next);
372
+ const terminator = findOscTerminator(oscBody);
373
+ if (terminator) {
374
+ i = escIndex + 1 + terminator.index + terminator.length;
375
+ continue;
376
+ }
377
+ pending = input.slice(escIndex);
378
+ break;
379
+ }
380
+ if (next < input.length) {
381
+ i = next + 1;
382
+ continue;
383
+ }
384
+ pending = input.slice(escIndex);
385
+ break;
386
+ }
387
+ return output;
388
+ };
389
+ const flush = () => {
390
+ if (pending.length === 0) {
391
+ return "";
392
+ }
393
+ const remainder = pending;
394
+ pending = "";
395
+ return remainder;
396
+ };
397
+ return { feed, flush };
398
+ }
399
+
161
400
  // src/terminal/create-terminal-buffer.ts
162
401
  function applyTerminalChunk(state, chunk) {
163
402
  const tokenizer = createNewlineTokenizer("terminal");
@@ -226,12 +465,15 @@ function trimTerminalState(state, limits) {
226
465
  }
227
466
  var DEFAULT_TERMINAL_BUFFER_OPTIONS = {
228
467
  maxLines: 1e4,
229
- maxChars: 1048576
468
+ maxChars: 1048576,
469
+ stripAnsi: true
230
470
  };
231
471
  function resolveTerminalBufferLimits(options) {
472
+ const maxLines = options?.maxLines ?? DEFAULT_TERMINAL_BUFFER_OPTIONS.maxLines;
473
+ const maxChars = options?.maxChars ?? DEFAULT_TERMINAL_BUFFER_OPTIONS.maxChars;
232
474
  return {
233
- ...DEFAULT_TERMINAL_BUFFER_OPTIONS,
234
- ...options
475
+ maxLines: brandMaxLines(maxLines),
476
+ maxChars: brandMaxChars(maxChars)
235
477
  };
236
478
  }
237
479
  var initialTerminalState = {
@@ -240,9 +482,12 @@ var initialTerminalState = {
240
482
  };
241
483
  function createTerminalBuffer(receive$, options) {
242
484
  const limits = resolveTerminalBufferLimits(options);
485
+ const stripAnsi = options?.stripAnsi ?? DEFAULT_TERMINAL_BUFFER_OPTIONS.stripAnsi;
486
+ const ansiStripper = stripAnsi ? createAnsiStripper() : null;
243
487
  const text$ = receive$.pipe(
244
488
  scan((state, chunk) => {
245
- const next = applyTerminalChunk(state, chunk);
489
+ const normalized = ansiStripper !== null ? ansiStripper.feed(chunk) : chunk;
490
+ const next = applyTerminalChunk(state, normalized);
246
491
  return trimTerminalState(next, limits);
247
492
  }, initialTerminalState),
248
493
  map(terminalDisplayText),
@@ -251,44 +496,95 @@ function createTerminalBuffer(receive$, options) {
251
496
  return { text$ };
252
497
  }
253
498
 
254
- // src/session/internal/build-request-options.ts
255
- function buildRequestOptions(options) {
256
- if (!options || !options.filters || options.filters.length === 0) {
257
- return void 0;
258
- }
259
- for (const filter of options.filters) {
260
- if (!filter.usbVendorId && !filter.usbProductId) {
261
- throw new SerialError(
262
- "INVALID_FILTER_OPTIONS" /* INVALID_FILTER_OPTIONS */,
263
- "Filter must have at least usbVendorId or usbProductId"
264
- );
499
+ // src/session/internal/has-web-serial-support.ts
500
+ function hasWebSerialSupport() {
501
+ return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
502
+ }
503
+
504
+ // src/session/internal/receive-pipeline.ts
505
+ import {
506
+ BehaviorSubject,
507
+ share,
508
+ Subject as Subject2,
509
+ switchMap
510
+ } from "rxjs";
511
+
512
+ // src/errors/serial-error-context.ts
513
+ var CAUSE_CONTEXT_CODES = /* @__PURE__ */ new Set([
514
+ SerialErrorCode.PORT_NOT_AVAILABLE,
515
+ SerialErrorCode.PORT_OPEN_FAILED,
516
+ SerialErrorCode.READ_FAILED,
517
+ SerialErrorCode.WRITE_FAILED,
518
+ SerialErrorCode.CONNECTION_LOST,
519
+ SerialErrorCode.OPERATION_CANCELLED,
520
+ SerialErrorCode.UNKNOWN
521
+ ]);
522
+ function isCauseContextCode(code) {
523
+ return CAUSE_CONTEXT_CODES.has(code);
524
+ }
525
+
526
+ // src/errors/serial-error.ts
527
+ var ErrorWithCaptureStackTrace = Error;
528
+ var SerialError = class _SerialError extends Error {
529
+ /**
530
+ * Creates a new SerialError instance.
531
+ *
532
+ * @param code - The error code identifying the type of error
533
+ * @param message - A human-readable error message
534
+ * @param originalError - The original error that caused this SerialError, if any.
535
+ * @deprecated Prefer passing `{ cause }` via {@link context}. When omitted,
536
+ * cause-bearing codes derive `{ cause }` from this argument for backward
537
+ * compatibility.
538
+ * @param context - Structured metadata for the error code. For cause-bearing
539
+ * codes, pass `{ cause }` here. When omitted, cause-bearing codes derive
540
+ * `{ cause }` from the legacy {@link originalError} argument.
541
+ */
542
+ constructor(code, message, originalError, context) {
543
+ super(message);
544
+ this.name = "SerialError";
545
+ this.code = code;
546
+ if (context !== void 0) {
547
+ this.context = context;
548
+ } else if (originalError !== void 0 && isCauseContextCode(code)) {
549
+ this.context = { cause: originalError };
550
+ } else {
551
+ this.context = void 0;
265
552
  }
266
- if (filter.usbVendorId !== void 0) {
267
- if (!Number.isInteger(filter.usbVendorId) || filter.usbVendorId < 0 || filter.usbVendorId > 65535) {
268
- throw new SerialError(
269
- "INVALID_FILTER_OPTIONS" /* INVALID_FILTER_OPTIONS */,
270
- `Invalid usbVendorId: ${filter.usbVendorId}. Must be an integer between 0 and 65535.`
271
- );
553
+ if (originalError !== void 0) {
554
+ this.originalError = originalError;
555
+ } else if (isCauseContextCode(code) && this.context !== void 0) {
556
+ const causeContext = this.context;
557
+ if (causeContext.cause instanceof Error) {
558
+ this.originalError = causeContext.cause;
272
559
  }
273
560
  }
274
- if (filter.usbProductId !== void 0) {
275
- if (!Number.isInteger(filter.usbProductId) || filter.usbProductId < 0 || filter.usbProductId > 65535) {
276
- throw new SerialError(
277
- "INVALID_FILTER_OPTIONS" /* INVALID_FILTER_OPTIONS */,
278
- `Invalid usbProductId: ${filter.usbProductId}. Must be an integer between 0 and 65535.`
279
- );
280
- }
561
+ if (ErrorWithCaptureStackTrace.captureStackTrace) {
562
+ ErrorWithCaptureStackTrace.captureStackTrace(this, _SerialError);
281
563
  }
282
564
  }
283
- return {
284
- filters: options.filters
285
- };
286
- }
287
-
288
- // src/session/internal/has-web-serial-support.ts
289
- function hasWebSerialSupport() {
290
- return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
291
- }
565
+ /**
566
+ * Check if the error matches a specific error code.
567
+ *
568
+ * This is a convenience method for checking the error code without directly
569
+ * comparing the code property. When this method returns `true`, TypeScript
570
+ * narrows `this.code` and `this.context` to the shapes defined for the
571
+ * provided `code` argument.
572
+ *
573
+ * @param code - The error code to check against
574
+ * @returns Type predicate: `true` if this error's code matches the provided
575
+ * code (and `this.code` / `this.context` are narrowed), `false` otherwise
576
+ *
577
+ * @example
578
+ * ```typescript
579
+ * if (error.is(SerialErrorCode.LINE_BUFFER_OVERFLOW)) {
580
+ * // error.code and error.context.maxChars are narrowed
581
+ * }
582
+ * ```
583
+ */
584
+ is(code) {
585
+ return this.code === code;
586
+ }
587
+ };
292
588
 
293
589
  // src/session/internal/line-buffer.ts
294
590
  var DEFAULT_LINE_BUFFER_OPTIONS = {
@@ -296,8 +592,9 @@ var DEFAULT_LINE_BUFFER_OPTIONS = {
296
592
  };
297
593
  function createLineBuffer(options) {
298
594
  const limits = {
299
- ...DEFAULT_LINE_BUFFER_OPTIONS,
300
- ...options
595
+ maxChars: brandMaxChars(
596
+ options?.maxChars ?? DEFAULT_LINE_BUFFER_OPTIONS.maxChars
597
+ )
301
598
  };
302
599
  const tokenizer = createNewlineTokenizer("line");
303
600
  const clear = () => {
@@ -371,45 +668,347 @@ function createReceiveReplayBuffer(options) {
371
668
  return { next, asObservable, complete };
372
669
  }
373
670
 
374
- // src/session/normalize-serial-error.ts
375
- var DEFAULT_MESSAGE_PREFIX = "Serial operation failed";
376
- var isDomExceptionWithName = (error, name) => typeof DOMException !== "undefined" && error instanceof DOMException && error.name === name;
377
- function normalizeSerialError(error, options) {
378
- if (error instanceof SerialError) {
379
- return error;
380
- }
381
- const prefix = options.messagePrefix ?? DEFAULT_MESSAGE_PREFIX;
382
- if (isDomExceptionWithName(error, "NotFoundError")) {
383
- return new SerialError(
384
- "OPERATION_CANCELLED" /* OPERATION_CANCELLED */,
385
- "Port selection was cancelled by the user",
386
- error
387
- );
388
- }
389
- const cause = error instanceof Error ? error : new Error(String(error));
390
- return new SerialError(
391
- options.fallbackCode,
392
- `${prefix}: ${cause.message}`,
393
- cause
394
- );
395
- }
396
-
397
- // src/session/read-pump.ts
398
- function createReadPump(port, { onChunk, onError, onDone }) {
399
- let reader = null;
400
- let running = false;
401
- let stopped = false;
402
- const decoder = new TextDecoder(void 0, { fatal: false });
403
- const releaseReader = () => {
404
- if (!reader) {
671
+ // src/session/internal/receive-pipeline.ts
672
+ function createReceivePipeline(deps) {
673
+ const { resolvedOptions, reportError } = deps;
674
+ const receiveSubject = new Subject2();
675
+ const linesSubject = new Subject2();
676
+ const lineBuffer = createLineBuffer(resolvedOptions.lineBuffer);
677
+ const receive$ = receiveSubject.asObservable();
678
+ const lines$ = linesSubject.asObservable();
679
+ const receiveReplayStream$ = resolvedOptions.receiveReplay.enabled ? new BehaviorSubject(receive$) : null;
680
+ let activeReceiveReplay = null;
681
+ const clearReplay = () => {
682
+ if (receiveReplayStream$) {
683
+ if (activeReceiveReplay) {
684
+ activeReceiveReplay.complete();
685
+ activeReceiveReplay = null;
686
+ }
687
+ receiveReplayStream$.next(receive$);
688
+ }
689
+ };
690
+ const startLiveReceiveReplay = () => {
691
+ if (!receiveReplayStream$) {
405
692
  return;
406
693
  }
407
- try {
408
- reader.releaseLock();
409
- } catch {
694
+ if (activeReceiveReplay) {
695
+ activeReceiveReplay.complete();
696
+ activeReceiveReplay = null;
410
697
  }
411
- reader = null;
412
- };
698
+ const buffer = createReceiveReplayBuffer({
699
+ bufferSize: resolvedOptions.receiveReplay.bufferSize,
700
+ maxChars: resolvedOptions.receiveReplay.maxChars
701
+ });
702
+ activeReceiveReplay = buffer;
703
+ receiveReplayStream$.next(buffer.asObservable());
704
+ };
705
+ const receiveReplay$ = receiveReplayStream$ ? receiveReplayStream$.pipe(switchMap((inner) => inner), share()) : receive$;
706
+ const clearLineBuffer = () => {
707
+ lineBuffer.clear();
708
+ };
709
+ const handleChunk = (text) => {
710
+ receiveSubject.next(text);
711
+ if (activeReceiveReplay) {
712
+ const { overflowed: overflowed2 } = activeReceiveReplay.next(text);
713
+ if (overflowed2) {
714
+ reportError(
715
+ new SerialError(
716
+ SerialErrorCode.RECEIVE_REPLAY_BUFFER_OVERFLOW,
717
+ "Receive replay buffer exceeded configured limits; oldest chunks were discarded",
718
+ void 0,
719
+ {
720
+ maxChars: resolvedOptions.receiveReplay.maxChars,
721
+ bufferSize: resolvedOptions.receiveReplay.bufferSize
722
+ }
723
+ ),
724
+ {
725
+ fallbackCode: SerialErrorCode.RECEIVE_REPLAY_BUFFER_OVERFLOW
726
+ }
727
+ );
728
+ }
729
+ }
730
+ const { lines, overflowed } = lineBuffer.feed(text);
731
+ if (overflowed) {
732
+ reportError(
733
+ new SerialError(
734
+ SerialErrorCode.LINE_BUFFER_OVERFLOW,
735
+ "Line buffer exceeded maxChars; leading data was discarded",
736
+ void 0,
737
+ { maxChars: resolvedOptions.lineBuffer.maxChars }
738
+ ),
739
+ { fallbackCode: SerialErrorCode.LINE_BUFFER_OVERFLOW }
740
+ );
741
+ }
742
+ for (const line of lines) {
743
+ linesSubject.next(line);
744
+ }
745
+ };
746
+ const complete = () => {
747
+ receiveSubject.complete();
748
+ linesSubject.complete();
749
+ receiveReplayStream$?.complete();
750
+ };
751
+ return {
752
+ receive$,
753
+ lines$,
754
+ receiveReplay$,
755
+ clearReplay,
756
+ startLiveReceiveReplay,
757
+ clearLineBuffer,
758
+ handleChunk,
759
+ complete
760
+ };
761
+ }
762
+
763
+ // src/session/internal/error-severity.ts
764
+ var ERROR_SEVERITY = {
765
+ [SerialErrorCode.READ_FAILED]: "fatal",
766
+ [SerialErrorCode.CONNECTION_LOST]: "fatal",
767
+ [SerialErrorCode.PORT_OPEN_FAILED]: "fatal",
768
+ [SerialErrorCode.OPERATION_CANCELLED]: "fatal",
769
+ [SerialErrorCode.UNKNOWN]: "fatal",
770
+ [SerialErrorCode.LINE_BUFFER_OVERFLOW]: "non-fatal",
771
+ [SerialErrorCode.RECEIVE_REPLAY_BUFFER_OVERFLOW]: "non-fatal",
772
+ [SerialErrorCode.BROWSER_NOT_SUPPORTED]: "non-fatal",
773
+ [SerialErrorCode.PORT_ALREADY_OPEN]: "non-fatal",
774
+ [SerialErrorCode.PORT_NOT_OPEN]: "non-fatal",
775
+ [SerialErrorCode.WRITE_FAILED]: "non-fatal"
776
+ };
777
+ function resolveErrorSeverity(code) {
778
+ if (code in ERROR_SEVERITY) {
779
+ return ERROR_SEVERITY[code];
780
+ }
781
+ return "fatal";
782
+ }
783
+
784
+ // src/session/normalize-serial-error.ts
785
+ var DEFAULT_MESSAGE_PREFIX = "Serial operation failed";
786
+ var isDomExceptionWithName = (error, name) => typeof DOMException !== "undefined" && error instanceof DOMException && error.name === name;
787
+ function normalizeSerialError(error, options) {
788
+ if (error instanceof SerialError) {
789
+ return error;
790
+ }
791
+ const prefix = options.messagePrefix ?? DEFAULT_MESSAGE_PREFIX;
792
+ if (isDomExceptionWithName(error, "NotFoundError")) {
793
+ return new SerialError(
794
+ SerialErrorCode.OPERATION_CANCELLED,
795
+ "Port selection was cancelled by the user",
796
+ void 0,
797
+ { cause: error }
798
+ );
799
+ }
800
+ const cause = error instanceof Error ? error : new Error(String(error));
801
+ return new SerialError(
802
+ options.fallbackCode,
803
+ `${prefix}: ${cause.message}`,
804
+ void 0,
805
+ { cause }
806
+ );
807
+ }
808
+
809
+ // src/session/session-runtime.ts
810
+ import { BehaviorSubject as BehaviorSubject2 } from "rxjs";
811
+
812
+ // src/session/serial-session-state.ts
813
+ var SerialSessionStatus = {
814
+ Idle: "idle",
815
+ Connecting: "connecting",
816
+ Connected: "connected",
817
+ Disconnecting: "disconnecting",
818
+ Unsupported: "unsupported",
819
+ Error: "error",
820
+ Disposed: "disposed"
821
+ };
822
+
823
+ // src/session/session-runtime.ts
824
+ var S = SerialSessionStatus;
825
+ var ALLOWED_TRANSITIONS = {
826
+ [S.Idle]: [S.Connecting, S.Error, S.Disposed],
827
+ [S.Connecting]: [S.Connected, S.Error, S.Idle, S.Disposed],
828
+ [S.Connected]: [S.Disconnecting, S.Error, S.Disposed],
829
+ [S.Disconnecting]: [S.Idle, S.Error, S.Disposed],
830
+ [S.Error]: [S.Idle, S.Connecting, S.Disposed],
831
+ [S.Unsupported]: [S.Disposed],
832
+ [S.Disposed]: []
833
+ };
834
+ function runtimeToSessionStatus(runtime) {
835
+ return runtime.status;
836
+ }
837
+ function runtimeToPublicState(runtime) {
838
+ switch (runtime.status) {
839
+ case S.Idle:
840
+ return { status: S.Idle };
841
+ case S.Connecting:
842
+ return { status: S.Connecting };
843
+ case S.Connected:
844
+ return { status: S.Connected, portInfo: runtime.port.getInfo() };
845
+ case S.Disconnecting:
846
+ return { status: S.Disconnecting };
847
+ case S.Unsupported:
848
+ return { status: S.Unsupported };
849
+ case S.Error:
850
+ return { status: S.Error, error: runtime.error };
851
+ case S.Disposed:
852
+ return { status: S.Disposed };
853
+ default:
854
+ return assertNeverRuntime(runtime);
855
+ }
856
+ }
857
+ function isValidTransition(from, to) {
858
+ if (from === to) {
859
+ return false;
860
+ }
861
+ return ALLOWED_TRANSITIONS[from].includes(
862
+ to
863
+ );
864
+ }
865
+ function createIdleRuntime() {
866
+ return { status: S.Idle };
867
+ }
868
+ function createConnectingRuntime(cancel) {
869
+ return { status: S.Connecting, cancel };
870
+ }
871
+ function createConnectedRuntime(port, pump) {
872
+ return { status: S.Connected, port, pump };
873
+ }
874
+ function createDisconnectingRuntime(port) {
875
+ return { status: S.Disconnecting, port };
876
+ }
877
+ function createErrorRuntime(error) {
878
+ return { status: S.Error, error };
879
+ }
880
+ function createUnsupportedRuntime() {
881
+ return { status: S.Unsupported };
882
+ }
883
+ function createDisposedRuntime() {
884
+ return { status: S.Disposed };
885
+ }
886
+ function createInitialRuntime(supported) {
887
+ return supported ? createIdleRuntime() : createUnsupportedRuntime();
888
+ }
889
+ function getRuntimePort(runtime) {
890
+ switch (runtime.status) {
891
+ case S.Connected:
892
+ return runtime.port;
893
+ case S.Disconnecting:
894
+ return runtime.port;
895
+ default:
896
+ return null;
897
+ }
898
+ }
899
+ function getRuntimePump(runtime) {
900
+ if (runtime.status === S.Connected) {
901
+ return runtime.pump;
902
+ }
903
+ return null;
904
+ }
905
+ function createSessionRuntimeController(initial) {
906
+ let runtime = initial;
907
+ const subject = new BehaviorSubject2(
908
+ runtimeToPublicState(initial)
909
+ );
910
+ const transition = (next) => {
911
+ const from = runtimeToSessionStatus(runtime);
912
+ const to = runtimeToSessionStatus(next);
913
+ if (from === to) {
914
+ return false;
915
+ }
916
+ if (!isValidTransition(from, to)) {
917
+ if (typeof console !== "undefined" && console.warn) {
918
+ console.warn(
919
+ `[web-serial-rxjs] Ignoring invalid SerialSession transition ${from} -> ${to}`
920
+ );
921
+ }
922
+ return false;
923
+ }
924
+ runtime = next;
925
+ subject.next(runtimeToPublicState(next));
926
+ return true;
927
+ };
928
+ return {
929
+ get runtime() {
930
+ return runtime;
931
+ },
932
+ get status() {
933
+ return runtimeToSessionStatus(runtime);
934
+ },
935
+ get state$() {
936
+ return subject.asObservable();
937
+ },
938
+ transition,
939
+ complete() {
940
+ subject.complete();
941
+ }
942
+ };
943
+ }
944
+ function assertNeverRuntime(value) {
945
+ return assertNever(value);
946
+ }
947
+
948
+ // src/session/internal/session-error-reporter.ts
949
+ function createSessionErrorReporter(deps) {
950
+ const {
951
+ controller,
952
+ errorsSubject,
953
+ sendQueue,
954
+ isDisposed,
955
+ updatePortInfo,
956
+ teardownPump,
957
+ closePortSafely
958
+ } = deps;
959
+ const createDisposedError = () => new SerialError(
960
+ SerialErrorCode.SESSION_DISPOSED,
961
+ "SerialSession has been disposed"
962
+ );
963
+ const reportError = (error, options) => {
964
+ const serialError = normalizeSerialError(error, options);
965
+ if (isDisposed()) {
966
+ return serialError;
967
+ }
968
+ errorsSubject.next(serialError);
969
+ if (resolveErrorSeverity(serialError.code) === "fatal") {
970
+ const runtime = controller.runtime;
971
+ const portToClose = getRuntimePort(runtime);
972
+ const pump = getRuntimePump(runtime);
973
+ controller.transition(createErrorRuntime(serialError));
974
+ sendQueue.clear();
975
+ updatePortInfo(null);
976
+ void teardownPump(pump).then(() => closePortSafely(portToClose));
977
+ }
978
+ return serialError;
979
+ };
980
+ return { reportError, createDisposedError };
981
+ }
982
+
983
+ // src/session/internal/session-lifecycle.ts
984
+ import { Observable as Observable3 } from "rxjs";
985
+
986
+ // src/session/internal/build-request-options.ts
987
+ function buildRequestOptions(options) {
988
+ if (!options?.filters || options.filters.length === 0) {
989
+ return void 0;
990
+ }
991
+ return {
992
+ filters: options.filters
993
+ };
994
+ }
995
+
996
+ // src/session/read-pump.ts
997
+ function createReadPump(port, { onChunk, onError, onDone }) {
998
+ let reader = null;
999
+ let running = false;
1000
+ let stopped = false;
1001
+ const decoder = new TextDecoder(void 0, { fatal: false });
1002
+ const releaseReader = () => {
1003
+ if (!reader) {
1004
+ return;
1005
+ }
1006
+ try {
1007
+ reader.releaseLock();
1008
+ } catch {
1009
+ }
1010
+ reader = null;
1011
+ };
413
1012
  const pump = async (stream) => {
414
1013
  reader = stream.getReader();
415
1014
  running = true;
@@ -439,7 +1038,7 @@ function createReadPump(port, { onChunk, onError, onDone }) {
439
1038
  if (!stopped) {
440
1039
  onError(
441
1040
  normalizeSerialError(error, {
442
- fallbackCode: "READ_FAILED" /* READ_FAILED */,
1041
+ fallbackCode: SerialErrorCode.READ_FAILED,
443
1042
  messagePrefix: "Read pump failed"
444
1043
  })
445
1044
  );
@@ -459,7 +1058,7 @@ function createReadPump(port, { onChunk, onError, onDone }) {
459
1058
  stopped = true;
460
1059
  onError(
461
1060
  new SerialError(
462
- "CONNECTION_LOST" /* CONNECTION_LOST */,
1061
+ SerialErrorCode.CONNECTION_LOST,
463
1062
  "Read pump failed: port.readable is not available"
464
1063
  )
465
1064
  );
@@ -488,272 +1087,75 @@ function createReadPump(port, { onChunk, onError, onDone }) {
488
1087
  };
489
1088
  }
490
1089
 
491
- // src/session/send-queue.ts
492
- import { Observable as Observable2, defer } from "rxjs";
493
- function createSendQueue() {
494
- let chain = Promise.resolve();
495
- return {
496
- enqueue(operation) {
497
- return defer(
498
- () => new Observable2((subscriber) => {
499
- let cancelled = false;
500
- const run = async () => {
501
- try {
502
- const value = await operation();
503
- if (!cancelled) {
504
- subscriber.next(value);
505
- subscriber.complete();
506
- }
507
- } catch (error) {
508
- if (!cancelled) {
509
- subscriber.error(error);
510
- }
511
- }
512
- };
513
- const scheduled = chain.then(run, run);
514
- chain = scheduled.then(
515
- () => void 0,
516
- () => void 0
517
- );
518
- return () => {
519
- cancelled = true;
520
- };
521
- })
522
- );
523
- },
524
- clear() {
525
- chain = Promise.resolve();
526
- }
1090
+ // src/session/internal/session-lifecycle.ts
1091
+ function createSessionLifecycle(deps) {
1092
+ const {
1093
+ controller,
1094
+ resolvedOptions,
1095
+ sendQueue,
1096
+ receivePipeline,
1097
+ portInfoSubject,
1098
+ errorsSubject,
1099
+ isDisposed,
1100
+ reportError,
1101
+ createDisposedError
1102
+ } = deps;
1103
+ const updatePortInfo = (port) => {
1104
+ portInfoSubject.next(port ? port.getInfo() : null);
527
1105
  };
528
- }
529
-
530
- // src/session/serial-session-options.ts
531
- var MAX_RECEIVE_REPLAY_BUFFER_SIZE = 65536;
532
- var MAX_RECEIVE_REPLAY_MAX_CHARS = 1048576;
533
- var DEFAULT_RECEIVE_REPLAY = {
534
- enabled: false,
535
- bufferSize: 512,
536
- maxChars: 0
537
- };
538
- function resolveReceiveReplayOptions(options) {
539
- const merged = {
540
- ...DEFAULT_RECEIVE_REPLAY,
541
- ...options
1106
+ const teardownPump = async (pump) => {
1107
+ receivePipeline.clearReplay();
1108
+ receivePipeline.clearLineBuffer();
1109
+ if (pump) {
1110
+ await pump.stop();
1111
+ }
542
1112
  };
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
- }
558
- var DEFAULT_SERIAL_SESSION_OPTIONS = {
559
- baudRate: 9600,
560
- dataBits: 8,
561
- stopBits: 1,
562
- parity: "none",
563
- bufferSize: 255,
564
- flowControl: "none",
565
- filters: void 0,
566
- receiveReplay: { ...DEFAULT_RECEIVE_REPLAY },
567
- terminalBuffer: { ...DEFAULT_TERMINAL_BUFFER_OPTIONS },
568
- lineBuffer: { ...DEFAULT_LINE_BUFFER_OPTIONS }
569
- };
570
-
571
- // src/session/serial-session-state.ts
572
- var SerialSessionState = {
573
- Idle: "idle",
574
- Connecting: "connecting",
575
- Connected: "connected",
576
- Disconnecting: "disconnecting",
577
- Unsupported: "unsupported",
578
- Error: "error",
579
- Disposed: "disposed"
580
- };
581
-
582
- // src/session/session-state-machine.ts
583
- import { BehaviorSubject } from "rxjs";
584
- var S = SerialSessionState;
585
- var ALLOWED_TRANSITIONS = {
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]: []
593
- };
594
- var SessionStateMachine = class {
595
- constructor(initial = SerialSessionState.Idle) {
596
- this.subject = new BehaviorSubject(initial);
597
- }
598
- get current() {
599
- return this.subject.getValue();
600
- }
601
- get state$() {
602
- return this.subject.asObservable();
603
- }
604
- toConnecting() {
605
- return this.transition(S.Connecting);
606
- }
607
- toConnected() {
608
- return this.transition(S.Connected);
609
- }
610
- toDisconnecting() {
611
- return this.transition(S.Disconnecting);
612
- }
613
- toIdle() {
614
- return this.transition(S.Idle);
615
- }
616
- toError() {
617
- return this.transition(S.Error);
618
- }
619
- toUnsupported() {
620
- return this.transition(S.Unsupported);
621
- }
622
- toDisposed() {
623
- return this.transition(S.Disposed);
624
- }
625
- complete() {
626
- this.subject.complete();
627
- }
628
- transition(next) {
629
- const current = this.subject.getValue();
630
- if (current === next) {
631
- return false;
1113
+ const closePortSafely = async (port) => {
1114
+ if (!port) {
1115
+ return;
632
1116
  }
633
- const allowed = ALLOWED_TRANSITIONS[current];
634
- if (!allowed.includes(next)) {
635
- if (typeof console !== "undefined" && console.warn) {
636
- console.warn(
637
- `[web-serial-rxjs] Ignoring invalid SerialSession transition ${current} -> ${next}`
638
- );
639
- }
640
- return false;
641
- }
642
- this.subject.next(next);
643
- return true;
644
- }
645
- };
646
-
647
- // src/session/create-serial-session.ts
648
- function createSerialSession(options) {
649
- const resolvedOptions = {
650
- ...DEFAULT_SERIAL_SESSION_OPTIONS,
651
- ...options,
652
- filters: options?.filters,
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
661
- }
662
- };
663
- const supported = hasWebSerialSupport();
664
- const machine = new SessionStateMachine(
665
- supported ? SerialSessionState.Idle : SerialSessionState.Unsupported
666
- );
667
- const errorsSubject = new Subject2();
668
- const receiveSubject = new Subject2();
669
- const linesSubject = new Subject2();
670
- const sendQueue = createSendQueue();
671
- const textEncoder = new TextEncoder();
672
- const lineBuffer = createLineBuffer(resolvedOptions.lineBuffer);
673
- const errors$ = errorsSubject.asObservable();
674
- const receive$ = receiveSubject.asObservable();
675
- const lines$ = linesSubject.asObservable();
676
- const terminalText$ = createTerminalBuffer(
677
- receive$,
678
- resolvedOptions.terminalBuffer
679
- ).text$;
680
- const isConnected$ = machine.state$.pipe(
681
- map2((state) => state === SerialSessionState.Connected),
682
- distinctUntilChanged()
683
- );
684
- const portInfoSubject = new BehaviorSubject2(null);
685
- const portInfo$ = portInfoSubject.asObservable();
686
- const receiveReplayStream$ = resolvedOptions.receiveReplay.enabled ? new BehaviorSubject2(receive$) : null;
687
- let activeReceiveReplay = null;
688
- const clearLiveReceiveReplay = () => {
689
- if (receiveReplayStream$) {
690
- if (activeReceiveReplay) {
691
- activeReceiveReplay.complete();
692
- activeReceiveReplay = null;
693
- }
694
- receiveReplayStream$.next(receive$);
695
- }
696
- };
697
- const startLiveReceiveReplay = () => {
698
- if (!receiveReplayStream$) {
699
- return;
700
- }
701
- if (activeReceiveReplay) {
702
- activeReceiveReplay.complete();
703
- activeReceiveReplay = null;
1117
+ try {
1118
+ await port.close();
1119
+ } catch {
704
1120
  }
705
- const buffer = createReceiveReplayBuffer({
706
- bufferSize: resolvedOptions.receiveReplay.bufferSize,
707
- maxChars: resolvedOptions.receiveReplay.maxChars
708
- });
709
- activeReceiveReplay = buffer;
710
- receiveReplayStream$.next(buffer.asObservable());
711
1121
  };
712
- const receiveReplay$ = receiveReplayStream$ ? receiveReplayStream$.pipe(switchMap((inner) => inner), share()) : receive$;
713
- let activePort = null;
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?.();
1122
+ const teardownFromSnapshot = async (snapshot) => {
1123
+ if (snapshot.status === SerialSessionStatus.Connecting) {
1124
+ snapshot.cancel();
725
1125
  }
726
1126
  sendQueue.clear();
727
- if (current === SerialSessionState.Connected || current === SerialSessionState.Error || current === SerialSessionState.Disconnecting) {
728
- const portToClose = activePort;
729
- await teardownPump();
1127
+ if (snapshot.status === SerialSessionStatus.Connected || snapshot.status === SerialSessionStatus.Disconnecting || snapshot.status === SerialSessionStatus.Error) {
1128
+ const portToClose = getRuntimePort(snapshot);
1129
+ const pump = getRuntimePump(snapshot);
1130
+ await teardownPump(pump);
730
1131
  await closePortSafely(portToClose);
731
- setActivePort(null);
1132
+ updatePortInfo(null);
732
1133
  }
733
- lineBuffer.clear();
734
- machine.toDisposed();
735
- machine.complete();
1134
+ receivePipeline.clearLineBuffer();
1135
+ };
1136
+ const completeSubjects = () => {
1137
+ controller.complete();
736
1138
  errorsSubject.complete();
737
- receiveSubject.complete();
738
- linesSubject.complete();
1139
+ receivePipeline.complete();
739
1140
  portInfoSubject.complete();
740
- receiveReplayStream$?.complete();
741
1141
  };
742
- const dispose$ = () => new Observable4((subscriber) => {
743
- if (disposed) {
1142
+ const dispose$ = () => new Observable3((subscriber) => {
1143
+ if (isDisposed()) {
744
1144
  subscriber.next();
745
1145
  subscriber.complete();
746
1146
  return;
747
1147
  }
748
- disposed = true;
1148
+ const snapshot = controller.runtime;
1149
+ controller.transition(createDisposedRuntime());
749
1150
  const run = async () => {
750
1151
  try {
751
- await completeSession();
1152
+ await teardownFromSnapshot(snapshot);
1153
+ completeSubjects();
752
1154
  subscriber.next();
753
1155
  subscriber.complete();
754
1156
  } catch (error) {
755
1157
  const serialError = normalizeSerialError(error, {
756
- fallbackCode: "UNKNOWN" /* UNKNOWN */,
1158
+ fallbackCode: SerialErrorCode.UNKNOWN,
757
1159
  messagePrefix: "Unexpected dispose failure"
758
1160
  });
759
1161
  subscriber.error(serialError);
@@ -761,57 +1163,15 @@ function createSerialSession(options) {
761
1163
  };
762
1164
  void run();
763
1165
  });
764
- const clearActiveConnectCancel = (cancel) => {
765
- if (activeConnectCancel === cancel) {
766
- activeConnectCancel = null;
767
- }
768
- };
769
- const setActivePort = (port) => {
770
- activePort = port;
771
- portInfoSubject.next(port ? port.getInfo() : null);
772
- };
773
- const teardownPump = async () => {
774
- clearLiveReceiveReplay();
775
- const pump = activePump;
776
- activePump = null;
777
- lineBuffer.clear();
778
- if (pump) {
779
- await pump.stop();
780
- }
781
- };
782
- const closePortSafely = async (port) => {
783
- if (!port) {
784
- return;
785
- }
786
- try {
787
- await port.close();
788
- } catch {
789
- }
790
- };
791
- const reportError = (error, severity, options2) => {
792
- const serialError = normalizeSerialError(error, options2);
793
- if (disposed) {
794
- return serialError;
795
- }
796
- errorsSubject.next(serialError);
797
- if (severity === "fatal") {
798
- machine.toError();
799
- sendQueue.clear();
800
- const portToClose = activePort;
801
- setActivePort(null);
802
- void teardownPump().then(() => closePortSafely(portToClose));
803
- }
804
- return serialError;
805
- };
806
1166
  const writeToPort = async (payload) => {
807
- const port = activePort;
808
- if (machine.current !== SerialSessionState.Connected || !port || !port.writable) {
1167
+ const runtime = controller.runtime;
1168
+ if (runtime.status !== SerialSessionStatus.Connected || !runtime.port.writable) {
809
1169
  throw new SerialError(
810
- "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
1170
+ SerialErrorCode.PORT_NOT_OPEN,
811
1171
  "Cannot send data while session is not connected"
812
1172
  );
813
1173
  }
814
- const writer = port.writable.getWriter();
1174
+ const writer = runtime.port.writable.getWriter();
815
1175
  try {
816
1176
  await writer.write(payload);
817
1177
  } finally {
@@ -821,254 +1181,527 @@ function createSerialSession(options) {
821
1181
  }
822
1182
  }
823
1183
  };
824
- return {
825
- isBrowserSupported() {
826
- return hasWebSerialSupport();
827
- },
828
- connect$() {
829
- return new Observable4((subscriber) => {
830
- if (disposed) {
831
- subscriber.error(createDisposedError());
832
- return;
1184
+ const connect$ = () => new Observable3((subscriber) => {
1185
+ if (isDisposed()) {
1186
+ subscriber.error(createDisposedError());
1187
+ return;
1188
+ }
1189
+ if (!hasWebSerialSupport()) {
1190
+ const error = reportError(
1191
+ new SerialError(
1192
+ SerialErrorCode.BROWSER_NOT_SUPPORTED,
1193
+ "Web Serial API is not supported in this environment"
1194
+ ),
1195
+ { fallbackCode: SerialErrorCode.BROWSER_NOT_SUPPORTED }
1196
+ );
1197
+ subscriber.error(error);
1198
+ return;
1199
+ }
1200
+ const current = controller.status;
1201
+ if (current !== SerialSessionStatus.Idle && current !== SerialSessionStatus.Error) {
1202
+ const error = reportError(
1203
+ new SerialError(
1204
+ SerialErrorCode.PORT_ALREADY_OPEN,
1205
+ `Cannot connect while session state is '${current}'`
1206
+ ),
1207
+ { fallbackCode: SerialErrorCode.PORT_ALREADY_OPEN }
1208
+ );
1209
+ subscriber.error(error);
1210
+ return;
1211
+ }
1212
+ let cancelled = false;
1213
+ const cancelInFlightConnect = () => {
1214
+ cancelled = true;
1215
+ if (controller.status === SerialSessionStatus.Connecting) {
1216
+ controller.transition(createIdleRuntime());
1217
+ }
1218
+ };
1219
+ controller.transition(createConnectingRuntime(cancelInFlightConnect));
1220
+ const run = async () => {
1221
+ let selectedPort = null;
1222
+ try {
1223
+ selectedPort = await navigator.serial.requestPort(
1224
+ buildRequestOptions(resolvedOptions)
1225
+ );
1226
+ await selectedPort.open({
1227
+ baudRate: resolvedOptions.baudRate,
1228
+ dataBits: resolvedOptions.dataBits,
1229
+ stopBits: resolvedOptions.stopBits,
1230
+ parity: resolvedOptions.parity,
1231
+ bufferSize: resolvedOptions.bufferSize,
1232
+ flowControl: resolvedOptions.flowControl
1233
+ });
1234
+ } catch (error) {
1235
+ if (selectedPort) {
1236
+ await closePortSafely(selectedPort);
833
1237
  }
834
- if (!hasWebSerialSupport()) {
835
- const error = reportError(
836
- new SerialError(
837
- "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
838
- "Web Serial API is not supported in this environment"
839
- ),
840
- "non-fatal",
841
- { fallbackCode: "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */ }
842
- );
843
- subscriber.error(error);
844
- return;
1238
+ const serialError = reportError(error, {
1239
+ fallbackCode: SerialErrorCode.PORT_OPEN_FAILED,
1240
+ messagePrefix: "Failed to open port"
1241
+ });
1242
+ if (!cancelled) {
1243
+ subscriber.error(serialError);
845
1244
  }
846
- const current = machine.current;
847
- if (current !== SerialSessionState.Idle && current !== SerialSessionState.Error) {
848
- const error = reportError(
1245
+ return;
1246
+ }
1247
+ if (cancelled || isDisposed()) {
1248
+ await closePortSafely(selectedPort);
1249
+ return;
1250
+ }
1251
+ receivePipeline.clearLineBuffer();
1252
+ if (resolvedOptions.receiveReplay.enabled) {
1253
+ receivePipeline.startLiveReceiveReplay();
1254
+ }
1255
+ const pump = createReadPump(selectedPort, {
1256
+ onChunk: receivePipeline.handleChunk,
1257
+ onError: (pumpError) => reportError(pumpError, {
1258
+ fallbackCode: SerialErrorCode.READ_FAILED,
1259
+ messagePrefix: "Read pump failed"
1260
+ }),
1261
+ onDone: () => {
1262
+ if (controller.status !== SerialSessionStatus.Connected) {
1263
+ return;
1264
+ }
1265
+ reportError(
849
1266
  new SerialError(
850
- "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
851
- `Cannot connect while session state is '${current}'`
1267
+ SerialErrorCode.CONNECTION_LOST,
1268
+ "Read pump ended unexpectedly: stream closed while connected"
852
1269
  ),
853
- "non-fatal",
854
- { fallbackCode: "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */ }
1270
+ {
1271
+ fallbackCode: SerialErrorCode.CONNECTION_LOST
1272
+ }
855
1273
  );
856
- subscriber.error(error);
857
- return;
858
1274
  }
859
- let cancelled = false;
860
- const cancelInFlightConnect = () => {
861
- cancelled = true;
862
- if (machine.current === SerialSessionState.Connecting) {
863
- machine.toIdle();
864
- }
865
- };
866
- activeConnectCancel = cancelInFlightConnect;
867
- machine.toConnecting();
868
- const run = async () => {
869
- let selectedPort = null;
1275
+ });
1276
+ pump.start();
1277
+ sendQueue.clear();
1278
+ if (isDisposed()) {
1279
+ await teardownPump(pump);
1280
+ await closePortSafely(selectedPort);
1281
+ return;
1282
+ }
1283
+ controller.transition(createConnectedRuntime(selectedPort, pump));
1284
+ updatePortInfo(selectedPort);
1285
+ subscriber.next();
1286
+ subscriber.complete();
1287
+ };
1288
+ void run();
1289
+ return () => {
1290
+ cancelInFlightConnect();
1291
+ };
1292
+ });
1293
+ const disconnect$ = () => new Observable3((subscriber) => {
1294
+ if (isDisposed()) {
1295
+ subscriber.next();
1296
+ subscriber.complete();
1297
+ return;
1298
+ }
1299
+ const runtime = controller.runtime;
1300
+ if (runtime.status === SerialSessionStatus.Idle || runtime.status === SerialSessionStatus.Unsupported || runtime.status === SerialSessionStatus.Disconnecting) {
1301
+ subscriber.next();
1302
+ subscriber.complete();
1303
+ return;
1304
+ }
1305
+ if (runtime.status === SerialSessionStatus.Connecting) {
1306
+ runtime.cancel();
1307
+ subscriber.next();
1308
+ subscriber.complete();
1309
+ return;
1310
+ }
1311
+ if (runtime.status !== SerialSessionStatus.Connected && runtime.status !== SerialSessionStatus.Error) {
1312
+ const error = reportError(
1313
+ new SerialError(
1314
+ SerialErrorCode.PORT_NOT_OPEN,
1315
+ `Cannot disconnect while session state is '${runtime.status}'`
1316
+ ),
1317
+ { fallbackCode: SerialErrorCode.PORT_NOT_OPEN }
1318
+ );
1319
+ subscriber.error(error);
1320
+ return;
1321
+ }
1322
+ const portToClose = getRuntimePort(runtime);
1323
+ controller.transition(createDisconnectingRuntime(portToClose));
1324
+ sendQueue.clear();
1325
+ const run = async () => {
1326
+ try {
1327
+ const pump = getRuntimePump(controller.runtime);
1328
+ await teardownPump(pump);
1329
+ if (portToClose) {
870
1330
  try {
871
- selectedPort = await navigator.serial.requestPort(
872
- buildRequestOptions(resolvedOptions)
873
- );
874
- await selectedPort.open({
875
- baudRate: resolvedOptions.baudRate,
876
- dataBits: resolvedOptions.dataBits,
877
- stopBits: resolvedOptions.stopBits,
878
- parity: resolvedOptions.parity,
879
- bufferSize: resolvedOptions.bufferSize,
880
- flowControl: resolvedOptions.flowControl
881
- });
1331
+ await portToClose.close();
882
1332
  } catch (error) {
883
- if (selectedPort) {
884
- await closePortSafely(selectedPort);
885
- }
886
- const serialError = reportError(error, "fatal", {
887
- fallbackCode: "PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
888
- messagePrefix: "Failed to open port"
1333
+ updatePortInfo(null);
1334
+ const serialError = reportError(error, {
1335
+ fallbackCode: SerialErrorCode.CONNECTION_LOST,
1336
+ messagePrefix: "Failed to close port"
889
1337
  });
890
- if (!cancelled) {
891
- subscriber.error(serialError);
892
- }
893
- return;
894
- }
895
- if (cancelled || disposed) {
896
- await closePortSafely(selectedPort);
1338
+ subscriber.error(serialError);
897
1339
  return;
898
1340
  }
899
- setActivePort(selectedPort);
900
- lineBuffer.clear();
901
- if (resolvedOptions.receiveReplay.enabled) {
902
- startLiveReceiveReplay();
903
- }
904
- activePump = createReadPump(selectedPort, {
905
- onChunk: (text) => {
906
- receiveSubject.next(text);
907
- if (activeReceiveReplay) {
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
- );
932
- }
933
- for (const line of lines) {
934
- linesSubject.next(line);
1341
+ }
1342
+ updatePortInfo(null);
1343
+ if (!isDisposed()) {
1344
+ controller.transition(createIdleRuntime());
1345
+ }
1346
+ subscriber.next();
1347
+ subscriber.complete();
1348
+ } catch (error) {
1349
+ const serialError = reportError(error, {
1350
+ fallbackCode: SerialErrorCode.UNKNOWN,
1351
+ messagePrefix: "Unexpected disconnect failure"
1352
+ });
1353
+ subscriber.error(serialError);
1354
+ }
1355
+ };
1356
+ void run();
1357
+ });
1358
+ return {
1359
+ connect$,
1360
+ disconnect$,
1361
+ dispose$,
1362
+ writeToPort,
1363
+ teardownPump,
1364
+ closePortSafely,
1365
+ updatePortInfo
1366
+ };
1367
+ }
1368
+
1369
+ // src/session/send-queue.ts
1370
+ import { Observable as Observable4, defer } from "rxjs";
1371
+ function createSendQueue() {
1372
+ let chain = Promise.resolve();
1373
+ return {
1374
+ enqueue(operation) {
1375
+ return defer(
1376
+ () => new Observable4((subscriber) => {
1377
+ let cancelled = false;
1378
+ const run = async () => {
1379
+ try {
1380
+ const value = await operation();
1381
+ if (!cancelled) {
1382
+ subscriber.next(value);
1383
+ subscriber.complete();
935
1384
  }
936
- },
937
- onError: (pumpError) => reportError(pumpError, "fatal", {
938
- fallbackCode: "READ_FAILED" /* READ_FAILED */,
939
- messagePrefix: "Read pump failed"
940
- }),
941
- onDone: () => {
942
- if (machine.current !== SerialSessionState.Connected) {
943
- return;
1385
+ } catch (error) {
1386
+ if (!cancelled) {
1387
+ subscriber.error(error);
944
1388
  }
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
1389
  }
956
- });
957
- activePump.start();
958
- sendQueue.clear();
959
- if (disposed) {
960
- await teardownPump();
961
- await closePortSafely(selectedPort);
962
- setActivePort(null);
963
- return;
964
- }
965
- machine.toConnected();
966
- subscriber.next();
967
- subscriber.complete();
968
- };
969
- void run();
970
- return () => {
971
- clearActiveConnectCancel(cancelInFlightConnect);
972
- cancelInFlightConnect();
973
- };
974
- });
975
- },
976
- disconnect$() {
977
- return new Observable4((subscriber) => {
978
- if (disposed) {
979
- subscriber.next();
980
- subscriber.complete();
981
- return;
982
- }
983
- const current = machine.current;
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?.();
991
- subscriber.next();
992
- subscriber.complete();
993
- return;
994
- }
995
- if (current !== SerialSessionState.Connected && current !== SerialSessionState.Error) {
996
- const error = reportError(
997
- new SerialError(
998
- "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
999
- `Cannot disconnect while session state is '${current}'`
1000
- ),
1001
- "non-fatal",
1002
- { fallbackCode: "PORT_NOT_OPEN" /* PORT_NOT_OPEN */ }
1390
+ };
1391
+ const scheduled = chain.then(run, run);
1392
+ chain = scheduled.then(
1393
+ () => void 0,
1394
+ () => void 0
1003
1395
  );
1004
- subscriber.error(error);
1005
- return;
1396
+ return () => {
1397
+ cancelled = true;
1398
+ };
1399
+ })
1400
+ );
1401
+ },
1402
+ clear() {
1403
+ chain = Promise.resolve();
1404
+ }
1405
+ };
1406
+ }
1407
+
1408
+ // src/session/internal/validate-serial-port-filters.ts
1409
+ function validateSerialPortFilters(filters) {
1410
+ if (!filters || filters.length === 0) {
1411
+ return filters;
1412
+ }
1413
+ for (let filterIndex = 0; filterIndex < filters.length; filterIndex++) {
1414
+ const filter = filters[filterIndex];
1415
+ if (filter === void 0) {
1416
+ continue;
1417
+ }
1418
+ if (!filter.usbVendorId && !filter.usbProductId) {
1419
+ throw new SerialError(
1420
+ SerialErrorCode.INVALID_FILTER_OPTIONS,
1421
+ "Filter must have at least usbVendorId or usbProductId",
1422
+ void 0,
1423
+ {
1424
+ field: "filters",
1425
+ value: filter,
1426
+ constraint: "at-least-one-usb-id",
1427
+ filterIndex
1006
1428
  }
1007
- machine.toDisconnecting();
1008
- sendQueue.clear();
1009
- const portToClose = activePort;
1010
- const run = async () => {
1011
- try {
1012
- await teardownPump();
1013
- if (portToClose) {
1014
- try {
1015
- await portToClose.close();
1016
- } catch (error) {
1017
- setActivePort(null);
1018
- const serialError = reportError(error, "fatal", {
1019
- fallbackCode: "CONNECTION_LOST" /* CONNECTION_LOST */,
1020
- messagePrefix: "Failed to close port"
1021
- });
1022
- subscriber.error(serialError);
1023
- return;
1024
- }
1025
- }
1026
- setActivePort(null);
1027
- if (!disposed) {
1028
- machine.toIdle();
1029
- }
1030
- subscriber.next();
1031
- subscriber.complete();
1032
- } catch (error) {
1033
- const serialError = reportError(error, "fatal", {
1034
- fallbackCode: "UNKNOWN" /* UNKNOWN */,
1035
- messagePrefix: "Unexpected disconnect failure"
1036
- });
1037
- subscriber.error(serialError);
1429
+ );
1430
+ }
1431
+ if (filter.usbVendorId !== void 0) {
1432
+ if (!Number.isInteger(filter.usbVendorId) || filter.usbVendorId < 0 || filter.usbVendorId > 65535) {
1433
+ throw new SerialError(
1434
+ SerialErrorCode.INVALID_FILTER_OPTIONS,
1435
+ `Invalid usbVendorId: ${filter.usbVendorId}. Must be an integer between 0 and 65535.`,
1436
+ void 0,
1437
+ {
1438
+ field: "usbVendorId",
1439
+ value: filter.usbVendorId,
1440
+ constraint: "usb-id-0-65535",
1441
+ filterIndex
1038
1442
  }
1039
- };
1040
- void run();
1041
- });
1443
+ );
1444
+ }
1445
+ }
1446
+ if (filter.usbProductId !== void 0) {
1447
+ if (!Number.isInteger(filter.usbProductId) || filter.usbProductId < 0 || filter.usbProductId > 65535) {
1448
+ throw new SerialError(
1449
+ SerialErrorCode.INVALID_FILTER_OPTIONS,
1450
+ `Invalid usbProductId: ${filter.usbProductId}. Must be an integer between 0 and 65535.`,
1451
+ void 0,
1452
+ {
1453
+ field: "usbProductId",
1454
+ value: filter.usbProductId,
1455
+ constraint: "usb-id-0-65535",
1456
+ filterIndex
1457
+ }
1458
+ );
1459
+ }
1460
+ }
1461
+ }
1462
+ return filters;
1463
+ }
1464
+
1465
+ // src/session/serial-session-options.ts
1466
+ var MAX_RECEIVE_REPLAY_BUFFER_SIZE = 65536;
1467
+ var MAX_RECEIVE_REPLAY_MAX_CHARS = 1048576;
1468
+ var DEFAULT_RECEIVE_REPLAY = {
1469
+ enabled: false,
1470
+ bufferSize: 512,
1471
+ maxChars: 0
1472
+ };
1473
+ function resolveReceiveReplayOptions(options) {
1474
+ const merged = {
1475
+ ...DEFAULT_RECEIVE_REPLAY,
1476
+ ...options
1477
+ };
1478
+ const { bufferSize, maxChars } = merged;
1479
+ if (!Number.isSafeInteger(bufferSize) || bufferSize < 1 || bufferSize > MAX_RECEIVE_REPLAY_BUFFER_SIZE) {
1480
+ throw new SerialError(
1481
+ SerialErrorCode.INVALID_RECEIVE_REPLAY_OPTIONS,
1482
+ `Invalid receiveReplay.bufferSize: ${bufferSize}. Must be a safe integer between 1 and ${MAX_RECEIVE_REPLAY_BUFFER_SIZE}.`,
1483
+ void 0,
1484
+ {
1485
+ field: "receiveReplay.bufferSize",
1486
+ value: bufferSize,
1487
+ constraint: "receive-replay-buffer-size-range"
1488
+ }
1489
+ );
1490
+ }
1491
+ if (!Number.isSafeInteger(maxChars) || maxChars < 0 || maxChars > MAX_RECEIVE_REPLAY_MAX_CHARS) {
1492
+ throw new SerialError(
1493
+ SerialErrorCode.INVALID_RECEIVE_REPLAY_OPTIONS,
1494
+ `Invalid receiveReplay.maxChars: ${maxChars}. Must be a safe integer between 0 and ${MAX_RECEIVE_REPLAY_MAX_CHARS}.`,
1495
+ void 0,
1496
+ {
1497
+ field: "receiveReplay.maxChars",
1498
+ value: maxChars,
1499
+ constraint: "receive-replay-max-chars-range"
1500
+ }
1501
+ );
1502
+ }
1503
+ return {
1504
+ enabled: merged.enabled,
1505
+ bufferSize: brandReceiveReplayBufferSize(bufferSize),
1506
+ maxChars: brandMaxChars(maxChars)
1507
+ };
1508
+ }
1509
+ function resolveTerminalBufferOptions(options) {
1510
+ const merged = {
1511
+ ...DEFAULT_TERMINAL_BUFFER_OPTIONS,
1512
+ ...options
1513
+ };
1514
+ const { maxLines, maxChars } = merged;
1515
+ if (!Number.isSafeInteger(maxLines) || maxLines < 0) {
1516
+ throw new SerialError(
1517
+ SerialErrorCode.INVALID_TERMINAL_BUFFER_OPTIONS,
1518
+ `Invalid terminalBuffer.maxLines: ${maxLines}. Must be a safe integer >= 0.`,
1519
+ void 0,
1520
+ {
1521
+ field: "terminalBuffer.maxLines",
1522
+ value: maxLines,
1523
+ constraint: "non-negative-safe-integer"
1524
+ }
1525
+ );
1526
+ }
1527
+ if (!Number.isSafeInteger(maxChars) || maxChars < 0) {
1528
+ throw new SerialError(
1529
+ SerialErrorCode.INVALID_TERMINAL_BUFFER_OPTIONS,
1530
+ `Invalid terminalBuffer.maxChars: ${maxChars}. Must be a safe integer >= 0.`,
1531
+ void 0,
1532
+ {
1533
+ field: "terminalBuffer.maxChars",
1534
+ value: maxChars,
1535
+ constraint: "non-negative-safe-integer"
1536
+ }
1537
+ );
1538
+ }
1539
+ return {
1540
+ maxLines: brandMaxLines(maxLines),
1541
+ maxChars: brandMaxChars(maxChars),
1542
+ stripAnsi: merged.stripAnsi
1543
+ };
1544
+ }
1545
+ function resolveLineBufferOptions(options) {
1546
+ const merged = {
1547
+ ...DEFAULT_LINE_BUFFER_OPTIONS,
1548
+ ...options
1549
+ };
1550
+ const { maxChars } = merged;
1551
+ if (!Number.isSafeInteger(maxChars) || maxChars < 0) {
1552
+ throw new SerialError(
1553
+ SerialErrorCode.INVALID_LINE_BUFFER_OPTIONS,
1554
+ `Invalid lineBuffer.maxChars: ${maxChars}. Must be a safe integer >= 0.`,
1555
+ void 0,
1556
+ {
1557
+ field: "lineBuffer.maxChars",
1558
+ value: maxChars,
1559
+ constraint: "non-negative-safe-integer"
1560
+ }
1561
+ );
1562
+ }
1563
+ return {
1564
+ maxChars: brandMaxChars(maxChars)
1565
+ };
1566
+ }
1567
+ var DEFAULT_SERIAL_SESSION_OPTIONS = {
1568
+ baudRate: brandBaudRate(9600),
1569
+ dataBits: 8,
1570
+ stopBits: 1,
1571
+ parity: "none",
1572
+ bufferSize: brandSerialPortBufferSize(255),
1573
+ flowControl: "none",
1574
+ receiveReplay: resolveReceiveReplayOptions(),
1575
+ terminalBuffer: resolveTerminalBufferOptions(),
1576
+ lineBuffer: resolveLineBufferOptions()
1577
+ };
1578
+ function resolveConnectionOptions(options) {
1579
+ const merged = {
1580
+ baudRate: DEFAULT_SERIAL_SESSION_OPTIONS.baudRate,
1581
+ dataBits: DEFAULT_SERIAL_SESSION_OPTIONS.dataBits,
1582
+ stopBits: DEFAULT_SERIAL_SESSION_OPTIONS.stopBits,
1583
+ parity: DEFAULT_SERIAL_SESSION_OPTIONS.parity,
1584
+ bufferSize: DEFAULT_SERIAL_SESSION_OPTIONS.bufferSize,
1585
+ flowControl: DEFAULT_SERIAL_SESSION_OPTIONS.flowControl,
1586
+ ...options
1587
+ };
1588
+ const { baudRate, bufferSize } = merged;
1589
+ if (!Number.isSafeInteger(baudRate) || baudRate <= 0) {
1590
+ throw new SerialError(
1591
+ SerialErrorCode.INVALID_CONNECTION_OPTIONS,
1592
+ `Invalid baudRate: ${baudRate}. Must be a safe integer > 0.`,
1593
+ void 0,
1594
+ {
1595
+ field: "baudRate",
1596
+ value: baudRate,
1597
+ constraint: "positive-safe-integer"
1598
+ }
1599
+ );
1600
+ }
1601
+ return {
1602
+ dataBits: merged.dataBits ?? DEFAULT_SERIAL_SESSION_OPTIONS.dataBits,
1603
+ stopBits: merged.stopBits ?? DEFAULT_SERIAL_SESSION_OPTIONS.stopBits,
1604
+ parity: merged.parity ?? DEFAULT_SERIAL_SESSION_OPTIONS.parity,
1605
+ flowControl: merged.flowControl ?? DEFAULT_SERIAL_SESSION_OPTIONS.flowControl,
1606
+ baudRate: brandBaudRate(baudRate),
1607
+ bufferSize: brandSerialPortBufferSize(
1608
+ bufferSize ?? DEFAULT_SERIAL_SESSION_OPTIONS.bufferSize
1609
+ )
1610
+ };
1611
+ }
1612
+ function resolveSerialSessionOptions(options) {
1613
+ const connection = resolveConnectionOptions(options);
1614
+ const filters = validateSerialPortFilters(options?.filters);
1615
+ return {
1616
+ ...connection,
1617
+ ...filters !== void 0 ? { filters } : {},
1618
+ receiveReplay: resolveReceiveReplayOptions(options?.receiveReplay),
1619
+ terminalBuffer: resolveTerminalBufferOptions(options?.terminalBuffer),
1620
+ lineBuffer: resolveLineBufferOptions(options?.lineBuffer)
1621
+ };
1622
+ }
1623
+
1624
+ // src/session/create-serial-session.ts
1625
+ function createSerialSession(options) {
1626
+ const resolvedOptions = resolveSerialSessionOptions(options);
1627
+ const controller = createSessionRuntimeController(
1628
+ createInitialRuntime(hasWebSerialSupport())
1629
+ );
1630
+ const errorsSubject = new Subject3();
1631
+ const sendQueue = createSendQueue();
1632
+ const textEncoder = new TextEncoder();
1633
+ const portInfoSubject = new BehaviorSubject3(null);
1634
+ const isDisposed = () => controller.status === SerialSessionStatus.Disposed;
1635
+ const errorReporterRef = {};
1636
+ const receivePipeline = createReceivePipeline({
1637
+ resolvedOptions,
1638
+ reportError: (error, options2) => errorReporterRef.reportError(error, options2)
1639
+ });
1640
+ const lifecycle = createSessionLifecycle({
1641
+ controller,
1642
+ resolvedOptions,
1643
+ sendQueue,
1644
+ receivePipeline,
1645
+ portInfoSubject,
1646
+ errorsSubject,
1647
+ isDisposed,
1648
+ reportError: (error, options2) => errorReporterRef.reportError(error, options2),
1649
+ createDisposedError: () => errorReporterRef.createDisposedError()
1650
+ });
1651
+ const { reportError, createDisposedError } = createSessionErrorReporter({
1652
+ controller,
1653
+ errorsSubject,
1654
+ sendQueue,
1655
+ isDisposed,
1656
+ updatePortInfo: lifecycle.updatePortInfo,
1657
+ teardownPump: lifecycle.teardownPump,
1658
+ closePortSafely: lifecycle.closePortSafely
1659
+ });
1660
+ errorReporterRef.reportError = reportError;
1661
+ errorReporterRef.createDisposedError = createDisposedError;
1662
+ const { receive$, lines$, receiveReplay$ } = receivePipeline;
1663
+ const errors$ = errorsSubject.asObservable();
1664
+ const terminalText$ = createTerminalBuffer(
1665
+ receive$,
1666
+ resolvedOptions.terminalBuffer
1667
+ ).text$;
1668
+ const isConnected$ = controller.state$.pipe(
1669
+ map2((state) => state.status === SerialSessionStatus.Connected),
1670
+ distinctUntilChanged()
1671
+ );
1672
+ const portInfo$ = portInfoSubject.asObservable();
1673
+ return {
1674
+ isBrowserSupported() {
1675
+ return hasWebSerialSupport();
1042
1676
  },
1043
- dispose$,
1044
- destroy$: dispose$,
1677
+ connect$: lifecycle.connect$,
1678
+ disconnect$: lifecycle.disconnect$,
1679
+ dispose$: lifecycle.dispose$,
1680
+ destroy$: lifecycle.dispose$,
1045
1681
  send$(data) {
1046
- if (disposed) {
1047
- return new Observable4((subscriber) => {
1682
+ if (isDisposed()) {
1683
+ return new Observable5((subscriber) => {
1048
1684
  subscriber.error(createDisposedError());
1049
1685
  });
1050
1686
  }
1051
1687
  return sendQueue.enqueue(async () => {
1052
1688
  const payload = typeof data === "string" ? textEncoder.encode(data) : data;
1053
1689
  try {
1054
- await writeToPort(payload);
1690
+ await lifecycle.writeToPort(payload);
1055
1691
  } catch (error) {
1056
- throw reportError(error, "non-fatal", {
1057
- fallbackCode: "WRITE_FAILED" /* WRITE_FAILED */,
1692
+ throw reportError(error, {
1693
+ fallbackCode: SerialErrorCode.WRITE_FAILED,
1058
1694
  messagePrefix: "Failed to write data"
1059
1695
  });
1060
1696
  }
1061
1697
  });
1062
1698
  },
1063
- state$: machine.state$,
1699
+ state$: controller.state$,
1064
1700
  isConnected$,
1065
1701
  portInfo$,
1066
1702
  getPortInfo() {
1067
1703
  return portInfoSubject.getValue();
1068
1704
  },
1069
- getCurrentPort() {
1070
- return activePort;
1071
- },
1072
1705
  errors$,
1073
1706
  receive$,
1074
1707
  terminalText$,
@@ -1076,13 +1709,23 @@ function createSerialSession(options) {
1076
1709
  lines$
1077
1710
  };
1078
1711
  }
1712
+
1713
+ // src/session/is-connected-session-state.ts
1714
+ function isConnectedSessionState(state) {
1715
+ return state.status === SerialSessionStatus.Connected;
1716
+ }
1079
1717
  export {
1080
1718
  DEFAULT_LINE_BUFFER_OPTIONS,
1081
1719
  DEFAULT_TERMINAL_BUFFER_OPTIONS,
1720
+ MAX_RECEIVE_REPLAY_BUFFER_SIZE,
1721
+ MAX_RECEIVE_REPLAY_MAX_CHARS,
1082
1722
  SerialError,
1083
1723
  SerialErrorCode,
1084
- SerialSessionState,
1724
+ SerialSessionStatus,
1725
+ assertNever,
1085
1726
  createSerialSession,
1086
- createTerminalBuffer
1727
+ createTerminalBuffer,
1728
+ isConnectedSessionState,
1729
+ resolveSerialSessionOptions
1087
1730
  };
1088
1731
  //# sourceMappingURL=index.mjs.map