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