@gurezo/web-serial-rxjs 0.1.21 → 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 (52) hide show
  1. package/README.ja.md +82 -13
  2. package/README.md +82 -13
  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 -30
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +469 -449
  8. package/dist/index.mjs +469 -449
  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 -1
  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 -250
  40. package/dist/client/index.d.ts.map +0 -1
  41. package/dist/client/serial-client.d.ts +0 -98
  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/types/options.d.ts +0 -107
  52. package/dist/types/options.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- // packages/web-serial-rxjs/src/client/serial-client.ts
2
- import { Observable as Observable2, defer, switchMap } from "rxjs";
1
+ // packages/web-serial-rxjs/src/session/create-serial-session.ts
2
+ import { distinctUntilChanged, map, Observable as Observable3, Subject } from "rxjs";
3
3
 
4
4
  // packages/web-serial-rxjs/src/errors/serial-error-code.ts
5
5
  var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
@@ -13,6 +13,7 @@ var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
13
13
  SerialErrorCode2["CONNECTION_LOST"] = "CONNECTION_LOST";
14
14
  SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
15
15
  SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
16
+ SerialErrorCode2["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
16
17
  SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
17
18
  return SerialErrorCode2;
18
19
  })(SerialErrorCode || {});
@@ -56,54 +57,7 @@ var SerialError = class _SerialError extends Error {
56
57
  }
57
58
  };
58
59
 
59
- // packages/web-serial-rxjs/src/browser/browser-detection.ts
60
- var BrowserType = /* @__PURE__ */ ((BrowserType2) => {
61
- BrowserType2["CHROME"] = "chrome";
62
- BrowserType2["EDGE"] = "edge";
63
- BrowserType2["OPERA"] = "opera";
64
- BrowserType2["UNKNOWN"] = "unknown";
65
- return BrowserType2;
66
- })(BrowserType || {});
67
- function hasWebSerialSupport() {
68
- return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
69
- }
70
- function detectBrowserType() {
71
- if (typeof navigator === "undefined" || !navigator.userAgent) {
72
- return "unknown" /* UNKNOWN */;
73
- }
74
- const ua = navigator.userAgent.toLowerCase();
75
- if (ua.includes("edg/")) {
76
- return "edge" /* EDGE */;
77
- }
78
- if (ua.includes("opr/") || ua.includes("opera/")) {
79
- return "opera" /* OPERA */;
80
- }
81
- if (ua.includes("chrome/")) {
82
- return "chrome" /* CHROME */;
83
- }
84
- return "unknown" /* UNKNOWN */;
85
- }
86
- function isChromiumBased() {
87
- const browserType = detectBrowserType();
88
- return browserType === "chrome" /* CHROME */ || browserType === "edge" /* EDGE */ || browserType === "opera" /* OPERA */;
89
- }
90
-
91
- // packages/web-serial-rxjs/src/browser/browser-support.ts
92
- function checkBrowserSupport() {
93
- if (!hasWebSerialSupport()) {
94
- const browserType = detectBrowserType();
95
- const browserName = browserType === "unknown" /* UNKNOWN */ ? "your browser" : browserType.toUpperCase();
96
- throw new SerialError(
97
- "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
98
- `Web Serial API is not supported in ${browserName}. Please use a Chromium-based browser (Chrome, Edge, or Opera).`
99
- );
100
- }
101
- }
102
- function isBrowserSupported() {
103
- return hasWebSerialSupport();
104
- }
105
-
106
- // packages/web-serial-rxjs/src/filters/build-request-options.ts
60
+ // packages/web-serial-rxjs/src/session/internal/build-request-options.ts
107
61
  function buildRequestOptions(options) {
108
62
  if (!options || !options.filters || options.filters.length === 0) {
109
63
  return void 0;
@@ -137,165 +91,204 @@ function buildRequestOptions(options) {
137
91
  };
138
92
  }
139
93
 
140
- // packages/web-serial-rxjs/src/io/observable-to-writable.ts
141
- function observableToWritable(observable) {
142
- let writer = null;
143
- let subscription = null;
144
- let stream = null;
145
- stream = new WritableStream({
146
- async start() {
147
- if (!stream) {
148
- 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;
149
114
  }
150
- writer = stream.getWriter();
151
- subscription = observable.subscribe({
152
- next: async (chunk) => {
153
- if (writer) {
154
- try {
155
- await writer.write(chunk);
156
- } catch (error) {
157
- subscription?.unsubscribe();
158
- if (writer) {
159
- writer.releaseLock();
160
- }
161
- throw error;
162
- }
163
- }
164
- },
165
- error: async (error) => {
166
- if (writer) {
167
- try {
168
- await writer.abort(error);
169
- } catch {
170
- } finally {
171
- writer.releaseLock();
172
- writer = null;
173
- }
174
- }
175
- },
176
- complete: async () => {
177
- if (writer) {
178
- try {
179
- await writer.close();
180
- } catch {
181
- } finally {
182
- writer.releaseLock();
183
- writer = null;
184
- }
185
- }
186
- }
187
- });
188
- },
189
- abort(reason) {
190
- if (subscription) {
191
- subscription.unsubscribe();
192
- 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;
193
120
  }
194
- if (writer) {
195
- writer.abort(reason).catch(() => {
196
- });
197
- writer.releaseLock();
198
- 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;
126
+ }
127
+ if (cr >= 0 && cr + 1 === buffer.length) {
128
+ break;
199
129
  }
130
+ break;
200
131
  }
201
- });
202
- return stream;
132
+ return out;
133
+ };
134
+ return { feed, clear };
203
135
  }
204
- function subscribeToWritable(observable, stream) {
205
- const writer = stream.getWriter();
206
- const errorHandler = async (error) => {
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
+ );
158
+ }
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
+ }
207
170
  try {
208
- await writer.abort(error);
171
+ reader.releaseLock();
209
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
+ }
210
207
  } finally {
211
- writer.releaseLock();
208
+ running = false;
209
+ releaseReader();
212
210
  }
213
211
  };
214
- const subscription = observable.subscribe({
215
- next: async (chunk) => {
216
- try {
217
- await writer.write(chunk);
218
- } catch (error) {
219
- subscription.unsubscribe();
220
- writer.releaseLock();
221
- const serialError = new SerialError(
222
- "WRITE_FAILED" /* WRITE_FAILED */,
223
- `Failed to write to stream: ${error instanceof Error ? error.message : String(error)}`,
224
- 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
+ )
225
225
  );
226
- await errorHandler(serialError);
226
+ return;
227
227
  }
228
+ void pump(stream);
228
229
  },
229
- error: errorHandler,
230
- complete: async () => {
230
+ async stop() {
231
+ if (stopped) {
232
+ return;
233
+ }
234
+ stopped = true;
235
+ if (!reader) {
236
+ return;
237
+ }
231
238
  try {
232
- await writer.close();
239
+ await reader.cancel();
233
240
  } catch {
234
241
  } finally {
235
- writer.releaseLock();
242
+ releaseReader();
236
243
  }
237
- }
238
- });
239
- return {
240
- unsubscribe: () => {
241
- subscription.unsubscribe();
242
- writer.releaseLock();
244
+ },
245
+ get isRunning() {
246
+ return running;
243
247
  }
244
248
  };
245
249
  }
246
250
 
247
- // packages/web-serial-rxjs/src/io/readable-to-observable.ts
248
- import { Observable } from "rxjs";
249
- function readableToObservable(stream) {
250
- return new Observable((subscriber) => {
251
- const reader = stream.getReader();
252
- const pump = async () => {
253
- try {
254
- while (true) {
255
- const { done, value } = await reader.read();
256
- if (done) {
257
- subscriber.complete();
258
- break;
259
- }
260
- if (value) {
261
- subscriber.next(value);
262
- }
263
- }
264
- } catch (error) {
265
- if (error instanceof Error) {
266
- subscriber.error(
267
- new SerialError(
268
- "READ_FAILED" /* READ_FAILED */,
269
- `Failed to read from stream: ${error.message}`,
270
- error
271
- )
272
- );
273
- } else {
274
- subscriber.error(
275
- new SerialError(
276
- "READ_FAILED" /* READ_FAILED */,
277
- "Failed to read from stream: Unknown error",
278
- error
279
- )
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
280
277
  );
281
- }
282
- } finally {
283
- reader.releaseLock();
284
- }
285
- };
286
- pump().catch((error) => {
287
- if (!subscriber.closed) {
288
- subscriber.error(error);
289
- }
290
- });
291
- return () => {
292
- reader.releaseLock();
293
- };
294
- });
278
+ return () => {
279
+ cancelled = true;
280
+ };
281
+ })
282
+ );
283
+ },
284
+ clear() {
285
+ chain = Promise.resolve();
286
+ }
287
+ };
295
288
  }
296
289
 
297
- // packages/web-serial-rxjs/src/types/options.ts
298
- var DEFAULT_SERIAL_CLIENT_OPTIONS = {
290
+ // packages/web-serial-rxjs/src/session/serial-session-options.ts
291
+ var DEFAULT_SERIAL_SESSION_OPTIONS = {
299
292
  baudRate: 9600,
300
293
  dataBits: 8,
301
294
  stopBits: 1,
@@ -305,291 +298,318 @@ var DEFAULT_SERIAL_CLIENT_OPTIONS = {
305
298
  filters: void 0
306
299
  };
307
300
 
308
- // packages/web-serial-rxjs/src/client/serial-client.ts
309
- var SerialClientImpl = class {
310
- /**
311
- * Creates a new SerialClientImpl instance.
312
- *
313
- * @param options - Optional configuration options for the serial port connection
314
- * @throws {@link SerialError} with code {@link SerialErrorCode.BROWSER_NOT_SUPPORTED} if the browser doesn't support Web Serial API
315
- * @internal
316
- */
317
- constructor(options) {
318
- /** @internal */
319
- this.port = null;
320
- /** @internal */
321
- this.isOpen = false;
322
- /** @internal */
323
- this.readSubscription = null;
324
- /** @internal */
325
- this.writeSubscription = null;
326
- checkBrowserSupport();
327
- this.options = {
328
- ...DEFAULT_SERIAL_CLIENT_OPTIONS,
329
- ...options,
330
- filters: options?.filters
331
- };
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);
332
325
  }
333
- /**
334
- * Request a serial port from the user.
335
- *
336
- * @returns Observable that emits the selected SerialPort
337
- * @internal
338
- */
339
- requestPort() {
340
- return defer(() => {
341
- checkBrowserSupport();
342
- return navigator.serial.requestPort(buildRequestOptions(this.options)).catch((error) => {
343
- if (error instanceof DOMException && error.name === "NotFoundError") {
344
- throw new SerialError(
345
- "OPERATION_CANCELLED" /* OPERATION_CANCELLED */,
346
- "Port selection was cancelled by the user",
347
- error
348
- );
349
- }
350
- throw new SerialError(
351
- "PORT_NOT_AVAILABLE" /* PORT_NOT_AVAILABLE */,
352
- `Failed to request port: ${error instanceof Error ? error.message : String(error)}`,
353
- error instanceof Error ? error : new Error(String(error))
354
- );
355
- });
356
- });
326
+ get current() {
327
+ return this.subject.getValue();
357
328
  }
358
- /**
359
- * Get available serial ports.
360
- *
361
- * @returns Observable that emits an array of available SerialPorts
362
- * @internal
363
- */
364
- getPorts() {
365
- return defer(() => {
366
- checkBrowserSupport();
367
- return navigator.serial.getPorts().catch((error) => {
368
- throw new SerialError(
369
- "PORT_NOT_AVAILABLE" /* PORT_NOT_AVAILABLE */,
370
- `Failed to get ports: ${error instanceof Error ? error.message : String(error)}`,
371
- error instanceof Error ? error : new Error(String(error))
372
- );
373
- });
374
- });
329
+ get state$() {
330
+ return this.subject.asObservable();
375
331
  }
376
- /**
377
- * Connect to a serial port.
378
- *
379
- * @param port - Optional SerialPort to connect to. If not provided, will request one.
380
- * @returns Observable that completes when the port is opened
381
- * @internal
382
- */
383
- connect(port) {
384
- checkBrowserSupport();
385
- if (this.isOpen) {
386
- return new Observable2((subscriber) => {
387
- subscriber.error(
388
- new SerialError(
389
- "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
390
- "Port is already open"
391
- )
392
- );
393
- });
394
- }
395
- const port$ = port ? new Observable2((subscriber) => {
396
- subscriber.next(port);
397
- subscriber.complete();
398
- }) : this.requestPort();
399
- return port$.pipe(
400
- switchMap((selectedPort) => {
401
- return defer(() => {
402
- this.port = selectedPort;
403
- return this.port.open({
404
- baudRate: this.options.baudRate,
405
- dataBits: this.options.dataBits,
406
- stopBits: this.options.stopBits,
407
- parity: this.options.parity,
408
- bufferSize: this.options.bufferSize,
409
- flowControl: this.options.flowControl
410
- }).then(() => {
411
- this.isOpen = true;
412
- }).catch((error) => {
413
- this.port = null;
414
- this.isOpen = false;
415
- if (error instanceof SerialError) {
416
- throw error;
417
- }
418
- throw new SerialError(
419
- "PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
420
- `Failed to open port: ${error instanceof Error ? error.message : String(error)}`,
421
- error instanceof Error ? error : new Error(String(error))
422
- );
423
- });
424
- });
425
- })
426
- );
332
+ toConnecting() {
333
+ return this.transition(S.Connecting);
427
334
  }
428
- /**
429
- * Disconnect from the serial port.
430
- *
431
- * @returns Observable that completes when the port is closed
432
- * @internal
433
- */
434
- disconnect() {
435
- return defer(() => {
436
- if (!this.isOpen || !this.port) {
437
- return Promise.resolve();
438
- }
439
- if (this.readSubscription) {
440
- this.readSubscription.unsubscribe();
441
- this.readSubscription = null;
442
- }
443
- if (this.writeSubscription) {
444
- this.writeSubscription.unsubscribe();
445
- this.writeSubscription = null;
446
- }
447
- return this.port.close().then(() => {
448
- this.port = null;
449
- this.isOpen = false;
450
- }).catch((error) => {
451
- this.port = null;
452
- this.isOpen = false;
453
- throw new SerialError(
454
- "CONNECTION_LOST" /* CONNECTION_LOST */,
455
- `Failed to close port: ${error instanceof Error ? error.message : String(error)}`,
456
- error instanceof Error ? error : new Error(String(error))
457
- );
458
- });
459
- });
335
+ toConnected() {
336
+ return this.transition(S.Connected);
460
337
  }
461
- /**
462
- * Get an Observable that emits data read from the serial port.
463
- *
464
- * @returns Observable that emits Uint8Array chunks
465
- * @internal
466
- */
467
- getReadStream() {
468
- if (!this.isOpen || !this.port || !this.port.readable) {
469
- throw new SerialError(
470
- "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
471
- "Port is not open or readable stream is not available"
472
- );
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;
357
+ }
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;
473
366
  }
474
- return readableToObservable(this.port.readable);
367
+ this.subject.next(next);
368
+ return true;
475
369
  }
476
- /**
477
- * Write data to the serial port from an Observable.
478
- *
479
- * @param data$ - Observable that emits Uint8Array chunks to write
480
- * @returns Observable that completes when writing is finished
481
- * @internal
482
- */
483
- writeStream(data$) {
484
- 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) {
485
430
  throw new SerialError(
486
431
  "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
487
- "Port is not open or writable stream is not available"
432
+ "Cannot send data while session is not connected"
488
433
  );
489
434
  }
490
- if (this.writeSubscription) {
491
- this.writeSubscription.unsubscribe();
492
- }
493
- this.writeSubscription = subscribeToWritable(data$, this.port.writable);
494
- return new Observable2((subscriber) => {
495
- if (!this.writeSubscription) {
496
- subscriber.error(
497
- new SerialError(
498
- "WRITE_FAILED" /* WRITE_FAILED */,
499
- "Write subscription is not available"
500
- )
501
- );
502
- return;
435
+ const writer = port.writable.getWriter();
436
+ try {
437
+ await writer.write(payload);
438
+ } finally {
439
+ try {
440
+ writer.releaseLock();
441
+ } catch {
503
442
  }
504
- const originalUnsubscribe = this.writeSubscription.unsubscribe;
505
- this.writeSubscription = {
506
- unsubscribe: () => {
507
- originalUnsubscribe();
508
- subscriber.complete();
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
+ );
460
+ subscriber.error(error);
461
+ return;
509
462
  }
510
- };
511
- data$.subscribe({
512
- complete: () => {
513
- if (this.writeSubscription) {
514
- this.writeSubscription.unsubscribe();
515
- this.writeSubscription = null;
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;
475
+ }
476
+ let cancelled = false;
477
+ machine.toConnecting();
478
+ const run = async () => {
479
+ let selectedPort = null;
480
+ try {
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
+ });
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
+ });
501
+ if (!cancelled) {
502
+ subscriber.error(serialError);
503
+ }
504
+ return;
516
505
  }
517
- subscriber.complete();
518
- },
519
- error: (error) => {
520
- if (this.writeSubscription) {
521
- this.writeSubscription.unsubscribe();
522
- this.writeSubscription = null;
506
+ if (cancelled) {
507
+ await closePortSafely(selectedPort);
508
+ return;
523
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();
529
+ };
530
+ void run();
531
+ return () => {
532
+ cancelled = true;
533
+ };
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
+ );
524
553
  subscriber.error(error);
554
+ return;
525
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();
526
588
  });
527
- });
528
- }
529
- /**
530
- * Write a single chunk of data to the serial port.
531
- *
532
- * @param data - Data to write
533
- * @returns Observable that completes when the data is written
534
- * @internal
535
- */
536
- write(data) {
537
- return defer(() => {
538
- if (!this.isOpen || !this.port || !this.port.writable) {
539
- throw new SerialError(
540
- "PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
541
- "Port is not open or writable stream is not available"
542
- );
543
- }
544
- const writer = this.port.writable.getWriter();
545
- return writer.write(data).then(() => {
546
- writer.releaseLock();
547
- }).catch((error) => {
548
- writer.releaseLock();
549
- throw new SerialError(
550
- "WRITE_FAILED" /* WRITE_FAILED */,
551
- `Failed to write data: ${error instanceof Error ? error.message : String(error)}`,
552
- error instanceof Error ? error : new Error(String(error))
553
- );
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
+ });
600
+ }
554
601
  });
555
- });
556
- }
557
- /**
558
- * Check if the port is currently open.
559
- *
560
- * @returns `true` if a port is currently open, `false` otherwise
561
- * @internal
562
- */
563
- get connected() {
564
- return this.isOpen;
565
- }
566
- /**
567
- * Get the current SerialPort instance.
568
- *
569
- * @returns The current SerialPort instance, or `null` if no port is open
570
- * @internal
571
- */
572
- get currentPort() {
573
- return this.port;
574
- }
575
- };
576
-
577
- // packages/web-serial-rxjs/src/client/index.ts
578
- function createSerialClient(options) {
579
- return new SerialClientImpl(options);
602
+ },
603
+ state$: machine.state$,
604
+ isConnected$,
605
+ errors$,
606
+ receive$,
607
+ lines$
608
+ };
580
609
  }
581
610
  export {
582
- BrowserType,
583
611
  SerialError,
584
612
  SerialErrorCode,
585
- buildRequestOptions,
586
- checkBrowserSupport,
587
- createSerialClient,
588
- detectBrowserType,
589
- hasWebSerialSupport,
590
- isBrowserSupported,
591
- isChromiumBased,
592
- observableToWritable,
593
- readableToObservable,
594
- subscribeToWritable
613
+ SerialSessionState,
614
+ createSerialSession
595
615
  };