@gurezo/web-serial-rxjs 0.2.0 → 2.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 +82 -15
  2. package/README.md +82 -15
  3. package/dist/errors/serial-error-code.d.ts +7 -0
  4. package/dist/errors/serial-error-code.d.ts.map +1 -1
  5. package/dist/index.d.ts +31 -32
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +459 -673
  8. package/dist/index.mjs +459 -673
  9. package/dist/index.mjs.map +4 -4
  10. package/dist/session/create-serial-session.d.ts +49 -0
  11. package/dist/session/create-serial-session.d.ts.map +1 -0
  12. package/dist/session/index.d.ts +5 -0
  13. package/dist/session/index.d.ts.map +1 -0
  14. package/dist/session/internal/build-request-options.d.ts +18 -0
  15. package/dist/session/internal/build-request-options.d.ts.map +1 -0
  16. package/dist/session/internal/has-web-serial-support.d.ts +12 -0
  17. package/dist/session/internal/has-web-serial-support.d.ts.map +1 -0
  18. package/dist/session/internal/line-buffer.d.ts +14 -0
  19. package/dist/session/internal/line-buffer.d.ts.map +1 -0
  20. package/dist/session/normalize-serial-error.d.ts +55 -0
  21. package/dist/session/normalize-serial-error.d.ts.map +1 -0
  22. package/dist/session/read-pump.d.ts +74 -0
  23. package/dist/session/read-pump.d.ts.map +1 -0
  24. package/dist/session/send-queue.d.ts +58 -0
  25. package/dist/session/send-queue.d.ts.map +1 -0
  26. package/dist/session/serial-session-options.d.ts +80 -0
  27. package/dist/session/serial-session-options.d.ts.map +1 -0
  28. package/dist/session/serial-session-state.d.ts +35 -0
  29. package/dist/session/serial-session-state.d.ts.map +1 -0
  30. package/dist/session/serial-session.d.ts +143 -0
  31. package/dist/session/serial-session.d.ts.map +1 -0
  32. package/dist/session/session-state-machine.d.ts +39 -0
  33. package/dist/session/session-state-machine.d.ts.map +1 -0
  34. package/package.json +1 -5
  35. package/dist/browser/browser-detection.d.ts +0 -104
  36. package/dist/browser/browser-detection.d.ts.map +0 -1
  37. package/dist/browser/browser-support.d.ts +0 -57
  38. package/dist/browser/browser-support.d.ts.map +0 -1
  39. package/dist/client/index.d.ts +0 -283
  40. package/dist/client/index.d.ts.map +0 -1
  41. package/dist/client/serial-client.d.ts +0 -137
  42. package/dist/client/serial-client.d.ts.map +0 -1
  43. package/dist/filters/build-request-options.d.ts +0 -42
  44. package/dist/filters/build-request-options.d.ts.map +0 -1
  45. package/dist/io/observable-to-writable.d.ts +0 -65
  46. package/dist/io/observable-to-writable.d.ts.map +0 -1
  47. package/dist/io/readable-to-observable.d.ts +0 -44
  48. package/dist/io/readable-to-observable.d.ts.map +0 -1
  49. package/dist/lib/web-serial-rxjs.d.ts +0 -7
  50. package/dist/lib/web-serial-rxjs.d.ts.map +0 -1
  51. package/dist/shell/create-shell-client.d.ts +0 -17
  52. package/dist/shell/create-shell-client.d.ts.map +0 -1
  53. package/dist/shell/index.d.ts +0 -2
  54. package/dist/shell/index.d.ts.map +0 -1
  55. package/dist/types/options.d.ts +0 -107
  56. package/dist/types/options.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,13 +1,5 @@
1
- // packages/web-serial-rxjs/src/client/serial-client.ts
2
- import {
3
- BehaviorSubject,
4
- Observable as Observable2,
5
- Subject,
6
- defer,
7
- map,
8
- share,
9
- switchMap
10
- } from "rxjs";
1
+ // packages/web-serial-rxjs/src/session/create-serial-session.ts
2
+ import { distinctUntilChanged, map, Observable as Observable3, Subject } from "rxjs";
11
3
 
12
4
  // packages/web-serial-rxjs/src/errors/serial-error-code.ts
13
5
  var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
@@ -21,6 +13,7 @@ var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
21
13
  SerialErrorCode2["CONNECTION_LOST"] = "CONNECTION_LOST";
22
14
  SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
23
15
  SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
16
+ SerialErrorCode2["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
24
17
  SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
25
18
  return SerialErrorCode2;
26
19
  })(SerialErrorCode || {});
@@ -64,54 +57,7 @@ var SerialError = class _SerialError extends Error {
64
57
  }
65
58
  };
66
59
 
67
- // packages/web-serial-rxjs/src/browser/browser-detection.ts
68
- var BrowserType = /* @__PURE__ */ ((BrowserType2) => {
69
- BrowserType2["CHROME"] = "chrome";
70
- BrowserType2["EDGE"] = "edge";
71
- BrowserType2["OPERA"] = "opera";
72
- BrowserType2["UNKNOWN"] = "unknown";
73
- return BrowserType2;
74
- })(BrowserType || {});
75
- function hasWebSerialSupport() {
76
- return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
77
- }
78
- function detectBrowserType() {
79
- if (typeof navigator === "undefined" || !navigator.userAgent) {
80
- return "unknown" /* UNKNOWN */;
81
- }
82
- const ua = navigator.userAgent.toLowerCase();
83
- if (ua.includes("edg/")) {
84
- return "edge" /* EDGE */;
85
- }
86
- if (ua.includes("opr/") || ua.includes("opera/")) {
87
- return "opera" /* OPERA */;
88
- }
89
- if (ua.includes("chrome/")) {
90
- return "chrome" /* CHROME */;
91
- }
92
- return "unknown" /* UNKNOWN */;
93
- }
94
- function isChromiumBased() {
95
- const browserType = detectBrowserType();
96
- return browserType === "chrome" /* CHROME */ || browserType === "edge" /* EDGE */ || browserType === "opera" /* OPERA */;
97
- }
98
-
99
- // packages/web-serial-rxjs/src/browser/browser-support.ts
100
- function checkBrowserSupport() {
101
- if (!hasWebSerialSupport()) {
102
- const browserType = detectBrowserType();
103
- const browserName = browserType === "unknown" /* UNKNOWN */ ? "your browser" : browserType.toUpperCase();
104
- throw new SerialError(
105
- "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
106
- `Web Serial API is not supported in ${browserName}. Please use a Chromium-based browser (Chrome, Edge, or Opera).`
107
- );
108
- }
109
- }
110
- function isBrowserSupported() {
111
- return hasWebSerialSupport();
112
- }
113
-
114
- // packages/web-serial-rxjs/src/filters/build-request-options.ts
60
+ // packages/web-serial-rxjs/src/session/internal/build-request-options.ts
115
61
  function buildRequestOptions(options) {
116
62
  if (!options || !options.filters || options.filters.length === 0) {
117
63
  return void 0;
@@ -145,165 +91,204 @@ function buildRequestOptions(options) {
145
91
  };
146
92
  }
147
93
 
148
- // packages/web-serial-rxjs/src/io/observable-to-writable.ts
149
- function observableToWritable(observable) {
150
- let writer = null;
151
- let subscription = null;
152
- let stream = null;
153
- stream = new WritableStream({
154
- async start() {
155
- if (!stream) {
156
- return;
94
+ // packages/web-serial-rxjs/src/session/internal/has-web-serial-support.ts
95
+ function hasWebSerialSupport() {
96
+ return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
97
+ }
98
+
99
+ // packages/web-serial-rxjs/src/session/internal/line-buffer.ts
100
+ function createLineBuffer() {
101
+ let buffer = "";
102
+ const clear = () => {
103
+ buffer = "";
104
+ };
105
+ const feed = (chunk) => {
106
+ buffer += chunk;
107
+ const out = [];
108
+ for (; ; ) {
109
+ const crlf = buffer.indexOf("\r\n");
110
+ if (crlf >= 0) {
111
+ out.push(buffer.slice(0, crlf));
112
+ buffer = buffer.slice(crlf + 2);
113
+ continue;
157
114
  }
158
- writer = stream.getWriter();
159
- subscription = observable.subscribe({
160
- next: async (chunk) => {
161
- if (writer) {
162
- try {
163
- await writer.write(chunk);
164
- } catch (error) {
165
- subscription?.unsubscribe();
166
- if (writer) {
167
- writer.releaseLock();
168
- }
169
- throw error;
170
- }
171
- }
172
- },
173
- error: async (error) => {
174
- if (writer) {
175
- try {
176
- await writer.abort(error);
177
- } catch {
178
- } finally {
179
- writer.releaseLock();
180
- writer = null;
181
- }
182
- }
183
- },
184
- complete: async () => {
185
- if (writer) {
186
- try {
187
- await writer.close();
188
- } catch {
189
- } finally {
190
- writer.releaseLock();
191
- writer = null;
192
- }
193
- }
194
- }
195
- });
196
- },
197
- abort(reason) {
198
- if (subscription) {
199
- subscription.unsubscribe();
200
- subscription = null;
115
+ const cr = buffer.indexOf("\r");
116
+ if (cr >= 0 && cr + 1 < buffer.length && buffer[cr + 1] !== "\n") {
117
+ out.push(buffer.slice(0, cr));
118
+ buffer = buffer.slice(cr + 1);
119
+ continue;
201
120
  }
202
- if (writer) {
203
- writer.abort(reason).catch(() => {
204
- });
205
- writer.releaseLock();
206
- writer = null;
121
+ const nl = buffer.indexOf("\n");
122
+ if (nl >= 0) {
123
+ out.push(buffer.slice(0, nl));
124
+ buffer = buffer.slice(nl + 1);
125
+ continue;
207
126
  }
127
+ if (cr >= 0 && cr + 1 === buffer.length) {
128
+ break;
129
+ }
130
+ break;
208
131
  }
209
- });
210
- return stream;
132
+ return out;
133
+ };
134
+ return { feed, clear };
135
+ }
136
+
137
+ // packages/web-serial-rxjs/src/session/normalize-serial-error.ts
138
+ var DEFAULT_MESSAGE_PREFIX = "Serial operation failed";
139
+ var isDomExceptionWithName = (error, name) => typeof DOMException !== "undefined" && error instanceof DOMException && error.name === name;
140
+ function normalizeSerialError(error, options) {
141
+ if (error instanceof SerialError) {
142
+ return error;
143
+ }
144
+ const prefix = options.messagePrefix ?? DEFAULT_MESSAGE_PREFIX;
145
+ if (isDomExceptionWithName(error, "NotFoundError")) {
146
+ return new SerialError(
147
+ "OPERATION_CANCELLED" /* OPERATION_CANCELLED */,
148
+ "Port selection was cancelled by the user",
149
+ error
150
+ );
151
+ }
152
+ const cause = error instanceof Error ? error : new Error(String(error));
153
+ return new SerialError(
154
+ options.fallbackCode,
155
+ `${prefix}: ${cause.message}`,
156
+ cause
157
+ );
211
158
  }
212
- function subscribeToWritable(observable, stream) {
213
- const writer = stream.getWriter();
214
- const errorHandler = async (error) => {
159
+
160
+ // packages/web-serial-rxjs/src/session/read-pump.ts
161
+ function createReadPump(port, { onChunk, onError }) {
162
+ let reader = null;
163
+ let running = false;
164
+ let stopped = false;
165
+ const decoder = new TextDecoder(void 0, { fatal: false });
166
+ const releaseReader = () => {
167
+ if (!reader) {
168
+ return;
169
+ }
215
170
  try {
216
- await writer.abort(error);
171
+ reader.releaseLock();
217
172
  } catch {
173
+ }
174
+ reader = null;
175
+ };
176
+ const pump = async (stream) => {
177
+ reader = stream.getReader();
178
+ running = true;
179
+ try {
180
+ while (!stopped) {
181
+ const { done, value } = await reader.read();
182
+ if (done) {
183
+ break;
184
+ }
185
+ if (value && value.byteLength > 0) {
186
+ const text = decoder.decode(value, { stream: true });
187
+ if (text.length > 0) {
188
+ onChunk(text);
189
+ }
190
+ }
191
+ }
192
+ if (!stopped) {
193
+ const tail = decoder.decode();
194
+ if (tail.length > 0) {
195
+ onChunk(tail);
196
+ }
197
+ }
198
+ } catch (error) {
199
+ if (!stopped) {
200
+ onError(
201
+ normalizeSerialError(error, {
202
+ fallbackCode: "READ_FAILED" /* READ_FAILED */,
203
+ messagePrefix: "Read pump failed"
204
+ })
205
+ );
206
+ }
218
207
  } finally {
219
- writer.releaseLock();
208
+ running = false;
209
+ releaseReader();
220
210
  }
221
211
  };
222
- const subscription = observable.subscribe({
223
- next: async (chunk) => {
224
- try {
225
- await writer.write(chunk);
226
- } catch (error) {
227
- subscription.unsubscribe();
228
- writer.releaseLock();
229
- const serialError = new SerialError(
230
- "WRITE_FAILED" /* WRITE_FAILED */,
231
- `Failed to write to stream: ${error instanceof Error ? error.message : String(error)}`,
232
- error instanceof Error ? error : new Error(String(error))
212
+ return {
213
+ start() {
214
+ if (running || stopped) {
215
+ return;
216
+ }
217
+ const stream = port.readable;
218
+ if (!stream) {
219
+ stopped = true;
220
+ onError(
221
+ new SerialError(
222
+ "CONNECTION_LOST" /* CONNECTION_LOST */,
223
+ "Read pump failed: port.readable is not available"
224
+ )
233
225
  );
234
- await errorHandler(serialError);
226
+ return;
235
227
  }
228
+ void pump(stream);
236
229
  },
237
- error: errorHandler,
238
- complete: async () => {
230
+ async stop() {
231
+ if (stopped) {
232
+ return;
233
+ }
234
+ stopped = true;
235
+ if (!reader) {
236
+ return;
237
+ }
239
238
  try {
240
- await writer.close();
239
+ await reader.cancel();
241
240
  } catch {
242
241
  } finally {
243
- writer.releaseLock();
242
+ releaseReader();
244
243
  }
245
- }
246
- });
247
- return {
248
- unsubscribe: () => {
249
- subscription.unsubscribe();
250
- writer.releaseLock();
244
+ },
245
+ get isRunning() {
246
+ return running;
251
247
  }
252
248
  };
253
249
  }
254
250
 
255
- // packages/web-serial-rxjs/src/io/readable-to-observable.ts
256
- import { Observable } from "rxjs";
257
- function readableToObservable(stream) {
258
- return new Observable((subscriber) => {
259
- const reader = stream.getReader();
260
- const pump = async () => {
261
- try {
262
- while (true) {
263
- const { done, value } = await reader.read();
264
- if (done) {
265
- subscriber.complete();
266
- break;
267
- }
268
- if (value) {
269
- subscriber.next(value);
270
- }
271
- }
272
- } catch (error) {
273
- if (error instanceof Error) {
274
- subscriber.error(
275
- new SerialError(
276
- "READ_FAILED" /* READ_FAILED */,
277
- `Failed to read from stream: ${error.message}`,
278
- error
279
- )
280
- );
281
- } else {
282
- subscriber.error(
283
- new SerialError(
284
- "READ_FAILED" /* READ_FAILED */,
285
- "Failed to read from stream: Unknown error",
286
- error
287
- )
251
+ // packages/web-serial-rxjs/src/session/send-queue.ts
252
+ import { Observable, defer } from "rxjs";
253
+ function createSendQueue() {
254
+ let chain = Promise.resolve();
255
+ return {
256
+ enqueue(operation) {
257
+ return defer(
258
+ () => new Observable((subscriber) => {
259
+ let cancelled = false;
260
+ const run = async () => {
261
+ try {
262
+ const value = await operation();
263
+ if (!cancelled) {
264
+ subscriber.next(value);
265
+ subscriber.complete();
266
+ }
267
+ } catch (error) {
268
+ if (!cancelled) {
269
+ subscriber.error(error);
270
+ }
271
+ }
272
+ };
273
+ const scheduled = chain.then(run, run);
274
+ chain = scheduled.then(
275
+ () => void 0,
276
+ () => void 0
288
277
  );
289
- }
290
- } finally {
291
- reader.releaseLock();
292
- }
293
- };
294
- pump().catch((error) => {
295
- if (!subscriber.closed) {
296
- subscriber.error(error);
297
- }
298
- });
299
- return () => {
300
- reader.releaseLock();
301
- };
302
- });
278
+ return () => {
279
+ cancelled = true;
280
+ };
281
+ })
282
+ );
283
+ },
284
+ clear() {
285
+ chain = Promise.resolve();
286
+ }
287
+ };
303
288
  }
304
289
 
305
- // packages/web-serial-rxjs/src/types/options.ts
306
- var DEFAULT_SERIAL_CLIENT_OPTIONS = {
290
+ // packages/web-serial-rxjs/src/session/serial-session-options.ts
291
+ var DEFAULT_SERIAL_SESSION_OPTIONS = {
307
292
  baudRate: 9600,
308
293
  dataBits: 8,
309
294
  stopBits: 1,
@@ -313,517 +298,318 @@ var DEFAULT_SERIAL_CLIENT_OPTIONS = {
313
298
  filters: void 0
314
299
  };
315
300
 
316
- // packages/web-serial-rxjs/src/client/serial-client.ts
317
- var SerialClientImpl = class {
318
- /**
319
- * Creates a new SerialClientImpl instance.
320
- *
321
- * @param options - Optional configuration options for the serial port connection
322
- * @throws {@link SerialError} with code {@link SerialErrorCode.BROWSER_NOT_SUPPORTED} if the browser doesn't support Web Serial API
323
- * @internal
324
- */
325
- constructor(options) {
326
- /** @internal */
327
- this.port = null;
328
- /** @internal */
329
- this.isOpen = false;
330
- /** @internal */
331
- this.readSubscription = null;
332
- /** @internal */
333
- this.writeSubscription = null;
334
- /** @internal */
335
- this.sharedReadStream$ = null;
336
- /** @internal */
337
- this.textEncoder = new TextEncoder();
338
- /** @internal */
339
- this.textDecoder = new TextDecoder();
340
- /** @internal */
341
- this.connectedState$ = new BehaviorSubject(false);
342
- /** @internal */
343
- this.connectionEventsSubject$ = new Subject();
344
- checkBrowserSupport();
345
- this.options = {
346
- ...DEFAULT_SERIAL_CLIENT_OPTIONS,
347
- ...options,
348
- filters: options?.filters
349
- };
301
+ // packages/web-serial-rxjs/src/session/serial-session-state.ts
302
+ var SerialSessionState = {
303
+ Idle: "idle",
304
+ Connecting: "connecting",
305
+ Connected: "connected",
306
+ Disconnecting: "disconnecting",
307
+ Unsupported: "unsupported",
308
+ Error: "error"
309
+ };
310
+
311
+ // packages/web-serial-rxjs/src/session/session-state-machine.ts
312
+ import { BehaviorSubject } from "rxjs";
313
+ var S = SerialSessionState;
314
+ var ALLOWED_TRANSITIONS = {
315
+ [S.Idle]: [S.Connecting, S.Error],
316
+ [S.Connecting]: [S.Connected, S.Error, S.Idle],
317
+ [S.Connected]: [S.Disconnecting, S.Error],
318
+ [S.Disconnecting]: [S.Idle, S.Error],
319
+ [S.Error]: [S.Idle, S.Connecting],
320
+ [S.Unsupported]: []
321
+ };
322
+ var SessionStateMachine = class {
323
+ constructor(initial = SerialSessionState.Idle) {
324
+ this.subject = new BehaviorSubject(initial);
350
325
  }
351
- /**
352
- * Request a serial port from the user.
353
- *
354
- * @returns Observable that emits the selected SerialPort
355
- * @internal
356
- */
357
- requestPort() {
358
- return defer(() => {
359
- checkBrowserSupport();
360
- return navigator.serial.requestPort(buildRequestOptions(this.options)).catch((error) => {
361
- if (error instanceof DOMException && error.name === "NotFoundError") {
362
- throw new SerialError(
363
- "OPERATION_CANCELLED" /* OPERATION_CANCELLED */,
364
- "Port selection was cancelled by the user",
365
- error
366
- );
367
- }
368
- throw new SerialError(
369
- "PORT_NOT_AVAILABLE" /* PORT_NOT_AVAILABLE */,
370
- `Failed to request port: ${error instanceof Error ? error.message : String(error)}`,
371
- error instanceof Error ? error : new Error(String(error))
372
- );
373
- });
374
- });
326
+ get current() {
327
+ return this.subject.getValue();
375
328
  }
376
- /**
377
- * Get available serial ports.
378
- *
379
- * @returns Observable that emits an array of available SerialPorts
380
- * @internal
381
- */
382
- getPorts() {
383
- return defer(() => {
384
- checkBrowserSupport();
385
- return navigator.serial.getPorts().catch((error) => {
386
- throw new SerialError(
387
- "PORT_NOT_AVAILABLE" /* PORT_NOT_AVAILABLE */,
388
- `Failed to get ports: ${error instanceof Error ? error.message : String(error)}`,
389
- error instanceof Error ? error : new Error(String(error))
390
- );
391
- });
392
- });
329
+ get state$() {
330
+ return this.subject.asObservable();
393
331
  }
394
- /**
395
- * Connect to a serial port.
396
- *
397
- * @param port - Optional SerialPort to connect to. If not provided, will request one.
398
- * @returns Observable that completes when the port is opened
399
- * @internal
400
- */
401
- connect(port) {
402
- checkBrowserSupport();
403
- if (this.isOpen) {
404
- return new Observable2((subscriber) => {
405
- subscriber.error(
406
- new SerialError(
407
- "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
408
- "Port is already open"
409
- )
410
- );
411
- });
412
- }
413
- const port$ = port ? new Observable2((subscriber) => {
414
- subscriber.next(port);
415
- subscriber.complete();
416
- }) : this.requestPort();
417
- return port$.pipe(
418
- switchMap((selectedPort) => {
419
- return defer(() => {
420
- this.port = selectedPort;
421
- return this.port.open({
422
- baudRate: this.options.baudRate,
423
- dataBits: this.options.dataBits,
424
- stopBits: this.options.stopBits,
425
- parity: this.options.parity,
426
- bufferSize: this.options.bufferSize,
427
- flowControl: this.options.flowControl
428
- }).then(() => {
429
- this.isOpen = true;
430
- this.connectedState$.next(true);
431
- this.connectionEventsSubject$.next("connected");
432
- }).catch((error) => {
433
- this.port = null;
434
- this.isOpen = false;
435
- if (error instanceof SerialError) {
436
- throw error;
437
- }
438
- throw new SerialError(
439
- "PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
440
- `Failed to open port: ${error instanceof Error ? error.message : String(error)}`,
441
- error instanceof Error ? error : new Error(String(error))
442
- );
443
- });
444
- });
445
- })
446
- );
332
+ toConnecting() {
333
+ return this.transition(S.Connecting);
447
334
  }
448
- /**
449
- * Disconnect from the serial port.
450
- *
451
- * @returns Observable that completes when the port is closed
452
- * @internal
453
- */
454
- disconnect() {
455
- return defer(() => {
456
- if (!this.isOpen || !this.port) {
457
- return Promise.resolve();
458
- }
459
- if (this.readSubscription) {
460
- this.readSubscription.unsubscribe();
461
- this.readSubscription = null;
462
- }
463
- this.sharedReadStream$ = null;
464
- this.textDecoder = new TextDecoder();
465
- if (this.writeSubscription) {
466
- this.writeSubscription.unsubscribe();
467
- this.writeSubscription = null;
468
- }
469
- return this.port.close().then(() => {
470
- this.port = null;
471
- this.isOpen = false;
472
- this.connectedState$.next(false);
473
- this.connectionEventsSubject$.next("disconnected");
474
- }).catch((error) => {
475
- this.port = null;
476
- this.isOpen = false;
477
- this.connectedState$.next(false);
478
- this.connectionEventsSubject$.next("disconnected");
479
- throw new SerialError(
480
- "CONNECTION_LOST" /* CONNECTION_LOST */,
481
- `Failed to close port: ${error instanceof Error ? error.message : String(error)}`,
482
- error instanceof Error ? error : new Error(String(error))
483
- );
484
- });
485
- });
335
+ toConnected() {
336
+ return this.transition(S.Connected);
486
337
  }
487
- /**
488
- * Get an Observable that emits data read from the serial port.
489
- *
490
- * @returns Observable that emits Uint8Array chunks
491
- * @internal
492
- */
493
- getReadStream() {
494
- if (!this.isOpen || !this.port || !this.port.readable) {
495
- throw new SerialError(
496
- "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
497
- "Port is not open or readable stream is not available"
498
- );
338
+ toDisconnecting() {
339
+ return this.transition(S.Disconnecting);
340
+ }
341
+ toIdle() {
342
+ return this.transition(S.Idle);
343
+ }
344
+ toError() {
345
+ return this.transition(S.Error);
346
+ }
347
+ toUnsupported() {
348
+ return this.transition(S.Unsupported);
349
+ }
350
+ complete() {
351
+ this.subject.complete();
352
+ }
353
+ transition(next) {
354
+ const current = this.subject.getValue();
355
+ if (current === next) {
356
+ return false;
499
357
  }
500
- if (!this.sharedReadStream$) {
501
- this.sharedReadStream$ = readableToObservable(this.port.readable).pipe(
502
- share({
503
- resetOnError: true,
504
- resetOnComplete: true,
505
- resetOnRefCountZero: true
506
- })
507
- );
358
+ const allowed = ALLOWED_TRANSITIONS[current];
359
+ if (!allowed.includes(next)) {
360
+ if (typeof console !== "undefined" && console.warn) {
361
+ console.warn(
362
+ `[web-serial-rxjs] Ignoring invalid SerialSession transition ${current} -> ${next}`
363
+ );
364
+ }
365
+ return false;
508
366
  }
509
- return this.sharedReadStream$;
367
+ this.subject.next(next);
368
+ return true;
510
369
  }
511
- /**
512
- * Get an Observable that emits decoded text from the serial port.
513
- *
514
- * @returns Observable that emits text chunks
515
- * @internal
516
- */
517
- getReadStreamAsText() {
518
- return this.getReadStream().pipe(
519
- map((chunk) => this.textDecoder.decode(chunk, { stream: true }))
520
- );
521
- }
522
- /**
523
- * Write data to the serial port from an Observable.
524
- *
525
- * @param data$ - Observable that emits Uint8Array chunks to write
526
- * @returns Observable that completes when writing is finished
527
- * @internal
528
- */
529
- writeStream(data$) {
530
- if (!this.isOpen || !this.port || !this.port.writable) {
370
+ };
371
+
372
+ // packages/web-serial-rxjs/src/session/create-serial-session.ts
373
+ function createSerialSession(options) {
374
+ const resolvedOptions = {
375
+ ...DEFAULT_SERIAL_SESSION_OPTIONS,
376
+ ...options,
377
+ filters: options?.filters
378
+ };
379
+ const supported = hasWebSerialSupport();
380
+ const machine = new SessionStateMachine(
381
+ supported ? SerialSessionState.Idle : SerialSessionState.Unsupported
382
+ );
383
+ const errorsSubject = new Subject();
384
+ const receiveSubject = new Subject();
385
+ const linesSubject = new Subject();
386
+ const sendQueue = createSendQueue();
387
+ const textEncoder = new TextEncoder();
388
+ const lineBuffer = createLineBuffer();
389
+ const errors$ = errorsSubject.asObservable();
390
+ const receive$ = receiveSubject.asObservable();
391
+ const lines$ = linesSubject.asObservable();
392
+ const isConnected$ = machine.state$.pipe(
393
+ map((state) => state === SerialSessionState.Connected),
394
+ distinctUntilChanged()
395
+ );
396
+ let activePort = null;
397
+ let activePump = null;
398
+ const teardownPump = async () => {
399
+ const pump = activePump;
400
+ activePump = null;
401
+ lineBuffer.clear();
402
+ if (pump) {
403
+ await pump.stop();
404
+ }
405
+ };
406
+ const closePortSafely = async (port) => {
407
+ if (!port) {
408
+ return;
409
+ }
410
+ try {
411
+ await port.close();
412
+ } catch {
413
+ }
414
+ };
415
+ const reportError = (error, severity, options2) => {
416
+ const serialError = normalizeSerialError(error, options2);
417
+ errorsSubject.next(serialError);
418
+ if (severity === "fatal") {
419
+ machine.toError();
420
+ sendQueue.clear();
421
+ const portToClose = activePort;
422
+ activePort = null;
423
+ void teardownPump().then(() => closePortSafely(portToClose));
424
+ }
425
+ return serialError;
426
+ };
427
+ const writeToPort = async (payload) => {
428
+ const port = activePort;
429
+ if (machine.current !== SerialSessionState.Connected || !port || !port.writable) {
531
430
  throw new SerialError(
532
431
  "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
533
- "Port is not open or writable stream is not available"
432
+ "Cannot send data while session is not connected"
534
433
  );
535
434
  }
536
- if (this.writeSubscription) {
537
- this.writeSubscription.unsubscribe();
538
- }
539
- this.writeSubscription = subscribeToWritable(data$, this.port.writable);
540
- return new Observable2((subscriber) => {
541
- if (!this.writeSubscription) {
542
- subscriber.error(
543
- new SerialError(
544
- "WRITE_FAILED" /* WRITE_FAILED */,
545
- "Write subscription is not available"
546
- )
547
- );
548
- return;
435
+ const writer = port.writable.getWriter();
436
+ try {
437
+ await writer.write(payload);
438
+ } finally {
439
+ try {
440
+ writer.releaseLock();
441
+ } catch {
549
442
  }
550
- const originalUnsubscribe = this.writeSubscription.unsubscribe;
551
- this.writeSubscription = {
552
- unsubscribe: () => {
553
- originalUnsubscribe();
554
- subscriber.complete();
555
- }
556
- };
557
- data$.subscribe({
558
- complete: () => {
559
- if (this.writeSubscription) {
560
- this.writeSubscription.unsubscribe();
561
- this.writeSubscription = null;
562
- }
563
- subscriber.complete();
564
- },
565
- error: (error) => {
566
- if (this.writeSubscription) {
567
- this.writeSubscription.unsubscribe();
568
- this.writeSubscription = null;
569
- }
443
+ }
444
+ };
445
+ return {
446
+ isBrowserSupported() {
447
+ return hasWebSerialSupport();
448
+ },
449
+ connect$() {
450
+ return new Observable3((subscriber) => {
451
+ if (!hasWebSerialSupport()) {
452
+ const error = reportError(
453
+ new SerialError(
454
+ "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
455
+ "Web Serial API is not supported in this environment"
456
+ ),
457
+ "non-fatal",
458
+ { fallbackCode: "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */ }
459
+ );
570
460
  subscriber.error(error);
461
+ return;
571
462
  }
572
- });
573
- });
574
- }
575
- /**
576
- * Write a single chunk of data to the serial port.
577
- *
578
- * @param data - Data to write
579
- * @returns Observable that completes when the data is written
580
- * @internal
581
- */
582
- write(data) {
583
- return defer(() => {
584
- if (!this.isOpen || !this.port || !this.port.writable) {
585
- throw new SerialError(
586
- "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
587
- "Port is not open or writable stream is not available"
588
- );
589
- }
590
- const writer = this.port.writable.getWriter();
591
- return writer.write(data).then(() => {
592
- writer.releaseLock();
593
- }).catch((error) => {
594
- writer.releaseLock();
595
- throw new SerialError(
596
- "WRITE_FAILED" /* WRITE_FAILED */,
597
- `Failed to write data: ${error instanceof Error ? error.message : String(error)}`,
598
- error instanceof Error ? error : new Error(String(error))
599
- );
600
- });
601
- });
602
- }
603
- /**
604
- * Write text data to the serial port.
605
- *
606
- * @param data - Text data to write
607
- * @returns Observable that completes when the data is written
608
- * @internal
609
- */
610
- writeText(data) {
611
- return this.write(this.textEncoder.encode(data));
612
- }
613
- /**
614
- * Check if the port is currently open.
615
- *
616
- * @returns `true` if a port is currently open, `false` otherwise
617
- * @internal
618
- */
619
- get connected() {
620
- return this.isOpen;
621
- }
622
- /**
623
- * Get an Observable that emits connection state changes.
624
- *
625
- * @returns Observable that emits `true` when connected and `false` when disconnected
626
- * @internal
627
- */
628
- get connected$() {
629
- return this.connectedState$.asObservable();
630
- }
631
- /**
632
- * Get an Observable that emits connection lifecycle events.
633
- *
634
- * @returns Observable that emits 'connected' or 'disconnected'
635
- * @internal
636
- */
637
- get connectionEvents$() {
638
- return this.connectionEventsSubject$.asObservable();
639
- }
640
- /**
641
- * Get the current SerialPort instance.
642
- *
643
- * @returns The current SerialPort instance, or `null` if no port is open
644
- * @internal
645
- */
646
- get currentPort() {
647
- return this.port;
648
- }
649
- };
650
-
651
- // packages/web-serial-rxjs/src/client/index.ts
652
- function createSerialClient(options) {
653
- return new SerialClientImpl(options);
654
- }
655
-
656
- // packages/web-serial-rxjs/src/shell/create-shell-client.ts
657
- import {
658
- Observable as Observable3,
659
- Subject as Subject2,
660
- defaultIfEmpty,
661
- defer as defer2,
662
- firstValueFrom,
663
- shareReplay
664
- } from "rxjs";
665
- var DEFAULT_TIMEOUT = 1e4;
666
- var DEFAULT_RETRY = 0;
667
- var DEFAULT_LINE_ENDING = "\r\n";
668
- var ShellClientImpl = class {
669
- constructor(client, options) {
670
- this.client = client;
671
- this.bufferTick$ = new Subject2();
672
- this.queueChain = Promise.resolve();
673
- this.readBuffer = "";
674
- this.prompt = options.prompt;
675
- this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
676
- this.retry = options.retry ?? DEFAULT_RETRY;
677
- this.lineEnding = options.lineEnding ?? DEFAULT_LINE_ENDING;
678
- this.promptRegex = this.prompt instanceof RegExp ? this.createAnchoredRegex(this.prompt) : null;
679
- this.read$ = this.client.getReadStreamAsText().pipe(shareReplay(1));
680
- this.read$.subscribe({
681
- next: (chunk) => {
682
- this.readBuffer += chunk;
683
- this.bufferTick$.next();
684
- },
685
- error: (error) => this.bufferTick$.error(error),
686
- complete: () => this.bufferTick$.complete()
687
- });
688
- }
689
- exec$(command) {
690
- return this.enqueue(async () => {
691
- for (let attempt = 0; attempt <= this.retry; attempt += 1) {
692
- try {
693
- await this.writeText(command + this.lineEnding);
694
- const stdout = await this.waitUntilPrompt(this.timeout);
695
- return { stdout };
696
- } catch (error) {
697
- if (!this.isTimeoutError(error) || attempt >= this.retry) {
698
- throw error;
699
- }
700
- }
701
- }
702
- throw new Error("Unexpected command execution state");
703
- });
704
- }
705
- readUntilPrompt$() {
706
- return this.enqueue(async () => {
707
- for (let attempt = 0; attempt <= this.retry; attempt += 1) {
708
- try {
709
- return await this.waitUntilPrompt(this.timeout);
710
- } catch (error) {
711
- if (!this.isTimeoutError(error) || attempt >= this.retry) {
712
- throw error;
713
- }
463
+ const current = machine.current;
464
+ if (current !== SerialSessionState.Idle && current !== SerialSessionState.Error) {
465
+ const error = reportError(
466
+ new SerialError(
467
+ "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
468
+ `Cannot connect while session state is '${current}'`
469
+ ),
470
+ "non-fatal",
471
+ { fallbackCode: "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */ }
472
+ );
473
+ subscriber.error(error);
474
+ return;
714
475
  }
715
- }
716
- throw new Error("Unexpected prompt waiting state");
717
- });
718
- }
719
- enqueue(operation) {
720
- return defer2(
721
- () => new Observable3((subscriber) => {
722
476
  let cancelled = false;
477
+ machine.toConnecting();
723
478
  const run = async () => {
479
+ let selectedPort = null;
724
480
  try {
725
- const value = await operation();
726
- if (!cancelled) {
727
- subscriber.next(value);
728
- subscriber.complete();
729
- }
481
+ selectedPort = await navigator.serial.requestPort(
482
+ buildRequestOptions(resolvedOptions)
483
+ );
484
+ await selectedPort.open({
485
+ baudRate: resolvedOptions.baudRate,
486
+ dataBits: resolvedOptions.dataBits,
487
+ stopBits: resolvedOptions.stopBits,
488
+ parity: resolvedOptions.parity,
489
+ bufferSize: resolvedOptions.bufferSize,
490
+ flowControl: resolvedOptions.flowControl
491
+ });
730
492
  } catch (error) {
493
+ if (selectedPort) {
494
+ await closePortSafely(selectedPort);
495
+ }
496
+ activePort = null;
497
+ const serialError = reportError(error, "fatal", {
498
+ fallbackCode: "PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
499
+ messagePrefix: "Failed to open port"
500
+ });
731
501
  if (!cancelled) {
732
- subscriber.error(error);
502
+ subscriber.error(serialError);
733
503
  }
504
+ return;
505
+ }
506
+ if (cancelled) {
507
+ await closePortSafely(selectedPort);
508
+ return;
734
509
  }
510
+ activePort = selectedPort;
511
+ lineBuffer.clear();
512
+ activePump = createReadPump(selectedPort, {
513
+ onChunk: (text) => {
514
+ receiveSubject.next(text);
515
+ for (const line of lineBuffer.feed(text)) {
516
+ linesSubject.next(line);
517
+ }
518
+ },
519
+ onError: (pumpError) => reportError(pumpError, "fatal", {
520
+ fallbackCode: "READ_FAILED" /* READ_FAILED */,
521
+ messagePrefix: "Read pump failed"
522
+ })
523
+ });
524
+ activePump.start();
525
+ sendQueue.clear();
526
+ machine.toConnected();
527
+ subscriber.next();
528
+ subscriber.complete();
735
529
  };
736
- const scheduled = this.queueChain.then(run, run);
737
- this.queueChain = scheduled.then(
738
- () => void 0,
739
- () => void 0
740
- );
530
+ void run();
741
531
  return () => {
742
532
  cancelled = true;
743
533
  };
744
- })
745
- );
746
- }
747
- async writeText(text) {
748
- await firstValueFrom(this.client.writeText(text).pipe(defaultIfEmpty(void 0)));
749
- }
750
- waitUntilPrompt(timeoutMs) {
751
- const immediate = this.tryConsumePrompt();
752
- if (immediate !== null) {
753
- return Promise.resolve(immediate);
754
- }
755
- return new Promise((resolve, reject) => {
756
- const timer = setTimeout(() => {
757
- subscription.unsubscribe();
758
- reject(new Error(`Timed out waiting for prompt after ${timeoutMs}ms`));
759
- }, timeoutMs);
760
- const complete = (value) => {
761
- clearTimeout(timer);
762
- subscription.unsubscribe();
763
- resolve(value);
764
- };
765
- const fail = (error) => {
766
- clearTimeout(timer);
767
- subscription.unsubscribe();
768
- reject(error);
769
- };
770
- const tryResolve = () => {
771
- const output = this.tryConsumePrompt();
772
- if (output !== null) {
773
- complete(output);
534
+ });
535
+ },
536
+ disconnect$() {
537
+ return new Observable3((subscriber) => {
538
+ const current = machine.current;
539
+ if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported) {
540
+ subscriber.next();
541
+ subscriber.complete();
542
+ return;
543
+ }
544
+ if (current !== SerialSessionState.Connected && current !== SerialSessionState.Error) {
545
+ const error = reportError(
546
+ new SerialError(
547
+ "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
548
+ `Cannot disconnect while session state is '${current}'`
549
+ ),
550
+ "non-fatal",
551
+ { fallbackCode: "PORT_NOT_OPEN" /* PORT_NOT_OPEN */ }
552
+ );
553
+ subscriber.error(error);
554
+ return;
555
+ }
556
+ machine.toDisconnecting();
557
+ sendQueue.clear();
558
+ const portToClose = activePort;
559
+ const run = async () => {
560
+ try {
561
+ await teardownPump();
562
+ if (portToClose) {
563
+ try {
564
+ await portToClose.close();
565
+ } catch (error) {
566
+ activePort = null;
567
+ const serialError = reportError(error, "fatal", {
568
+ fallbackCode: "CONNECTION_LOST" /* CONNECTION_LOST */,
569
+ messagePrefix: "Failed to close port"
570
+ });
571
+ subscriber.error(serialError);
572
+ return;
573
+ }
574
+ }
575
+ activePort = null;
576
+ machine.toIdle();
577
+ subscriber.next();
578
+ subscriber.complete();
579
+ } catch (error) {
580
+ const serialError = reportError(error, "fatal", {
581
+ fallbackCode: "UNKNOWN" /* UNKNOWN */,
582
+ messagePrefix: "Unexpected disconnect failure"
583
+ });
584
+ subscriber.error(serialError);
585
+ }
586
+ };
587
+ void run();
588
+ });
589
+ },
590
+ send$(data) {
591
+ return sendQueue.enqueue(async () => {
592
+ const payload = typeof data === "string" ? textEncoder.encode(data) : data;
593
+ try {
594
+ await writeToPort(payload);
595
+ } catch (error) {
596
+ throw reportError(error, "non-fatal", {
597
+ fallbackCode: "WRITE_FAILED" /* WRITE_FAILED */,
598
+ messagePrefix: "Failed to write data"
599
+ });
774
600
  }
775
- };
776
- const subscription = this.bufferTick$.subscribe({
777
- next: () => tryResolve(),
778
- error: (error) => fail(error),
779
- complete: () => fail(new Error("Read stream completed before prompt was found"))
780
601
  });
781
- });
782
- }
783
- tryConsumePrompt() {
784
- if (typeof this.prompt === "string") {
785
- if (this.readBuffer.length >= this.prompt.length && this.readBuffer.endsWith(this.prompt)) {
786
- const body2 = this.readBuffer.slice(0, this.readBuffer.length - this.prompt.length);
787
- this.readBuffer = "";
788
- return body2.trimEnd();
789
- }
790
- return null;
791
- }
792
- if (!this.promptRegex) {
793
- return null;
794
- }
795
- const match = this.promptRegex.exec(this.readBuffer);
796
- if (!match || match.index == null) {
797
- return null;
798
- }
799
- const body = this.readBuffer.slice(0, match.index);
800
- this.readBuffer = "";
801
- return body.trimEnd();
802
- }
803
- createAnchoredRegex(source) {
804
- const flags = source.flags.replace(/g/g, "");
805
- return new RegExp(`(?:${source.source})$`, flags);
806
- }
807
- isTimeoutError(error) {
808
- return error instanceof Error && error.message.includes("Timed out waiting for prompt");
809
- }
810
- };
811
- function createShellClient(client, options) {
812
- return new ShellClientImpl(client, options);
602
+ },
603
+ state$: machine.state$,
604
+ isConnected$,
605
+ errors$,
606
+ receive$,
607
+ lines$
608
+ };
813
609
  }
814
610
  export {
815
- BrowserType,
816
611
  SerialError,
817
612
  SerialErrorCode,
818
- buildRequestOptions,
819
- checkBrowserSupport,
820
- createSerialClient,
821
- createShellClient,
822
- detectBrowserType,
823
- hasWebSerialSupport,
824
- isBrowserSupported,
825
- isChromiumBased,
826
- observableToWritable,
827
- readableToObservable,
828
- subscribeToWritable
613
+ SerialSessionState,
614
+ createSerialSession
829
615
  };