@gurezo/web-serial-rxjs 4.0.5 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -182,7 +182,13 @@ var SerialErrorCode = {
182
182
  };
183
183
 
184
184
  // src/terminal/create-terminal-buffer.ts
185
- import { map, scan, shareReplay } from "rxjs";
185
+ import {
186
+ ReplaySubject,
187
+ finalize,
188
+ map,
189
+ scan,
190
+ share
191
+ } from "rxjs";
186
192
 
187
193
  // src/errors/serial-error-context.ts
188
194
  var CAUSE_CONTEXT_CODES = /* @__PURE__ */ new Set([
@@ -445,19 +451,46 @@ function createAnsiStripper() {
445
451
  }
446
452
 
447
453
  // src/terminal/create-terminal-buffer.ts
448
- function applyTerminalChunk(state, chunk) {
454
+ var emptyTerminalState = {
455
+ completed: "",
456
+ currentLine: ""
457
+ };
458
+ function createTerminalParser(options = {}) {
459
+ const stripAnsi = options.stripAnsi ?? false;
449
460
  const tokenizer = createNewlineTokenizer("terminal");
450
- tokenizer.restorePending(state.currentLine);
451
- const events = tokenizer.feed(chunk);
452
- let { completed } = state;
453
- for (const event of events) {
454
- if (event.type === "line") {
455
- completed += event.content + "\n";
456
- }
457
- }
458
- return {
461
+ let ansiStripper = stripAnsi ? createAnsiStripper() : null;
462
+ let completed = "";
463
+ const resetAnsiStripper = () => {
464
+ ansiStripper = stripAnsi ? createAnsiStripper() : null;
465
+ };
466
+ const getState = () => ({
459
467
  completed,
460
468
  currentLine: tokenizer.getPendingText()
469
+ });
470
+ const restoreState = (state) => {
471
+ completed = state.completed;
472
+ tokenizer.restorePending(state.currentLine);
473
+ };
474
+ const reset = () => {
475
+ completed = "";
476
+ tokenizer.clear();
477
+ resetAnsiStripper();
478
+ };
479
+ const feed = (chunk) => {
480
+ const normalized = ansiStripper !== null ? ansiStripper.feed(chunk) : chunk;
481
+ const events = tokenizer.feed(normalized);
482
+ for (const event of events) {
483
+ if (event.type === "line") {
484
+ completed += event.content + "\n";
485
+ }
486
+ }
487
+ return getState();
488
+ };
489
+ return {
490
+ feed,
491
+ reset,
492
+ getState,
493
+ restoreState
461
494
  };
462
495
  }
463
496
  function terminalDisplayText(state) {
@@ -479,15 +512,21 @@ function trimCompletedByMaxLines(completed, maxLines) {
479
512
  if (maxLines <= 0) {
480
513
  return completed;
481
514
  }
482
- let trimmed = completed;
483
- while (countCompletedLines(trimmed) > maxLines) {
484
- const firstNewline = trimmed.indexOf("\n");
485
- if (firstNewline < 0) {
486
- break;
515
+ const lineCount = countCompletedLines(completed);
516
+ if (lineCount <= maxLines) {
517
+ return completed;
518
+ }
519
+ const linesToDrop = lineCount - maxLines;
520
+ let dropped = 0;
521
+ for (let i = 0; i < completed.length; i++) {
522
+ if (completed.charAt(i) === "\n") {
523
+ dropped++;
524
+ if (dropped === linesToDrop) {
525
+ return completed.slice(i + 1);
526
+ }
487
527
  }
488
- trimmed = trimmed.slice(firstNewline + 1);
489
528
  }
490
- return trimmed;
529
+ return completed;
491
530
  }
492
531
  function trimTerminalState(state, limits) {
493
532
  let { completed, currentLine } = state;
@@ -551,25 +590,28 @@ function resolveTerminalBufferOptions(options) {
551
590
  stripAnsi: merged.stripAnsi
552
591
  };
553
592
  }
554
- var initialTerminalState = {
555
- completed: "",
556
- currentLine: ""
557
- };
558
593
  function createTerminalBuffer(receive$, options) {
559
594
  const resolved = resolveTerminalBufferOptions(options);
560
595
  const limits = {
561
596
  maxLines: resolved.maxLines,
562
597
  maxChars: resolved.maxChars
563
598
  };
564
- const ansiStripper = resolved.stripAnsi ? createAnsiStripper() : null;
599
+ const parser = createTerminalParser({ stripAnsi: resolved.stripAnsi });
565
600
  const text$ = receive$.pipe(
566
- scan((state, chunk) => {
567
- const normalized = ansiStripper !== null ? ansiStripper.feed(chunk) : chunk;
568
- const next = applyTerminalChunk(state, normalized);
569
- return trimTerminalState(next, limits);
570
- }, initialTerminalState),
601
+ scan((_state, chunk) => {
602
+ parser.feed(chunk);
603
+ const trimmed = trimTerminalState(parser.getState(), limits);
604
+ parser.restoreState(trimmed);
605
+ return trimmed;
606
+ }, emptyTerminalState),
571
607
  map(terminalDisplayText),
572
- shareReplay({ bufferSize: 1, refCount: true })
608
+ finalize(() => {
609
+ parser.reset();
610
+ }),
611
+ share({
612
+ connector: () => new ReplaySubject(1),
613
+ resetOnRefCountZero: true
614
+ })
573
615
  );
574
616
  return { text$ };
575
617
  }
@@ -579,9 +621,156 @@ function isWebSerialSupported() {
579
621
  return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
580
622
  }
581
623
 
624
+ // src/session/session-runtime.ts
625
+ import { BehaviorSubject } from "rxjs";
626
+
627
+ // src/session/serial-session-state.ts
628
+ var SerialSessionStatus = {
629
+ Idle: "idle",
630
+ Connecting: "connecting",
631
+ Connected: "connected",
632
+ Disconnecting: "disconnecting",
633
+ Unsupported: "unsupported",
634
+ Error: "error",
635
+ Disposed: "disposed"
636
+ };
637
+
638
+ // src/session/session-runtime.ts
639
+ var S = SerialSessionStatus;
640
+ var ALLOWED_TRANSITIONS = {
641
+ [S.Idle]: [S.Connecting, S.Error, S.Disposed],
642
+ [S.Connecting]: [S.Connected, S.Error, S.Idle, S.Disposed],
643
+ [S.Connected]: [S.Disconnecting, S.Error, S.Disposed],
644
+ [S.Disconnecting]: [S.Idle, S.Error, S.Disposed],
645
+ [S.Error]: [S.Idle, S.Connecting, S.Disposed],
646
+ [S.Unsupported]: [S.Disposed],
647
+ [S.Disposed]: []
648
+ };
649
+ function runtimeToSessionStatus(runtime) {
650
+ return runtime.status;
651
+ }
652
+ function runtimeToPublicState(runtime) {
653
+ switch (runtime.status) {
654
+ case S.Idle:
655
+ return { status: S.Idle };
656
+ case S.Connecting:
657
+ return { status: S.Connecting };
658
+ case S.Connected:
659
+ return { status: S.Connected, portInfo: runtime.port.getInfo() };
660
+ case S.Disconnecting:
661
+ return { status: S.Disconnecting };
662
+ case S.Unsupported:
663
+ return { status: S.Unsupported };
664
+ case S.Error:
665
+ return { status: S.Error, error: runtime.error };
666
+ case S.Disposed:
667
+ return { status: S.Disposed };
668
+ default:
669
+ return assertNever(runtime);
670
+ }
671
+ }
672
+ function isValidTransition(from, to) {
673
+ if (from === to) {
674
+ return false;
675
+ }
676
+ return ALLOWED_TRANSITIONS[from].includes(
677
+ to
678
+ );
679
+ }
680
+ function createIdleRuntime() {
681
+ return { status: S.Idle };
682
+ }
683
+ function createConnectingRuntime(cancel) {
684
+ return { status: S.Connecting, cancel };
685
+ }
686
+ function createConnectedRuntime(port, pump) {
687
+ return { status: S.Connected, port, pump };
688
+ }
689
+ function createDisconnectingRuntime(port) {
690
+ return { status: S.Disconnecting, port };
691
+ }
692
+ function createErrorRuntime(error) {
693
+ return { status: S.Error, error };
694
+ }
695
+ function createUnsupportedRuntime() {
696
+ return { status: S.Unsupported };
697
+ }
698
+ function createDisposedRuntime() {
699
+ return { status: S.Disposed };
700
+ }
701
+ function createInitialRuntime(supported) {
702
+ return supported ? createIdleRuntime() : createUnsupportedRuntime();
703
+ }
704
+ function getRuntimePort(runtime) {
705
+ switch (runtime.status) {
706
+ case S.Connected:
707
+ return runtime.port;
708
+ case S.Disconnecting:
709
+ return runtime.port;
710
+ default:
711
+ return null;
712
+ }
713
+ }
714
+ function getRuntimePump(runtime) {
715
+ if (runtime.status === S.Connected) {
716
+ return runtime.pump;
717
+ }
718
+ return null;
719
+ }
720
+ function createSessionRuntimeController(initial) {
721
+ let runtime = initial;
722
+ const subject = new BehaviorSubject(
723
+ runtimeToPublicState(initial)
724
+ );
725
+ const transition = (next) => {
726
+ const from = runtimeToSessionStatus(runtime);
727
+ const to = runtimeToSessionStatus(next);
728
+ if (from === to) {
729
+ return false;
730
+ }
731
+ if (!isValidTransition(from, to)) {
732
+ if (typeof console !== "undefined" && console.warn) {
733
+ console.warn(
734
+ `[web-serial-rxjs] Ignoring invalid SerialSession transition ${from} -> ${to}`
735
+ );
736
+ }
737
+ return false;
738
+ }
739
+ const publicState = runtimeToPublicState(next);
740
+ runtime = next;
741
+ subject.next(publicState);
742
+ return true;
743
+ };
744
+ return {
745
+ get runtime() {
746
+ return runtime;
747
+ },
748
+ get status() {
749
+ return runtimeToSessionStatus(runtime);
750
+ },
751
+ get state$() {
752
+ return subject.asObservable();
753
+ },
754
+ transition,
755
+ complete() {
756
+ subject.complete();
757
+ }
758
+ };
759
+ }
760
+
582
761
  // src/session/internal/port-teardown.ts
762
+ function captureResources(runtime) {
763
+ const resources = {
764
+ port: getRuntimePort(runtime),
765
+ pump: getRuntimePump(runtime)
766
+ };
767
+ if (runtime.status === SerialSessionStatus.Connecting) {
768
+ resources.cancelConnect = runtime.cancel;
769
+ }
770
+ return resources;
771
+ }
583
772
  function createPortTeardown(deps) {
584
- const { receivePipeline } = deps;
773
+ const { receivePipeline, sendQueue } = deps;
585
774
  const teardownPump = async (pump) => {
586
775
  receivePipeline.clearLineBuffer();
587
776
  if (pump) {
@@ -597,7 +786,36 @@ function createPortTeardown(deps) {
597
786
  } catch {
598
787
  }
599
788
  };
600
- return { teardownPump, closePortSafely };
789
+ const teardownResources = async (resources, options = {}) => {
790
+ const closeMode = options.closeMode ?? "safe";
791
+ const cancelInFlightConnect = options.cancelInFlightConnect ?? true;
792
+ if (cancelInFlightConnect) {
793
+ resources.cancelConnect?.();
794
+ }
795
+ sendQueue.clear();
796
+ await teardownPump(resources.pump);
797
+ if (!resources.port) {
798
+ return;
799
+ }
800
+ if (closeMode === "safe") {
801
+ await closePortSafely(resources.port);
802
+ return;
803
+ }
804
+ try {
805
+ await resources.port.close();
806
+ } catch (error) {
807
+ if (options.onCloseError) {
808
+ options.onCloseError(error);
809
+ }
810
+ throw error;
811
+ }
812
+ };
813
+ return {
814
+ teardownPump,
815
+ closePortSafely,
816
+ captureResources,
817
+ teardownResources
818
+ };
601
819
  }
602
820
 
603
821
  // src/session/internal/receive-pipeline.ts
@@ -739,155 +957,14 @@ function normalizeSerialError(error, options) {
739
957
  );
740
958
  }
741
959
 
742
- // src/session/session-runtime.ts
743
- import { BehaviorSubject } from "rxjs";
744
-
745
- // src/session/serial-session-state.ts
746
- var SerialSessionStatus = {
747
- Idle: "idle",
748
- Connecting: "connecting",
749
- Connected: "connected",
750
- Disconnecting: "disconnecting",
751
- Unsupported: "unsupported",
752
- Error: "error",
753
- Disposed: "disposed"
754
- };
755
-
756
- // src/session/session-runtime.ts
757
- var S = SerialSessionStatus;
758
- var ALLOWED_TRANSITIONS = {
759
- [S.Idle]: [S.Connecting, S.Error, S.Disposed],
760
- [S.Connecting]: [S.Connected, S.Error, S.Idle, S.Disposed],
761
- [S.Connected]: [S.Disconnecting, S.Error, S.Disposed],
762
- [S.Disconnecting]: [S.Idle, S.Error, S.Disposed],
763
- [S.Error]: [S.Idle, S.Connecting, S.Disposed],
764
- [S.Unsupported]: [S.Disposed],
765
- [S.Disposed]: []
766
- };
767
- function runtimeToSessionStatus(runtime) {
768
- return runtime.status;
769
- }
770
- function runtimeToPublicState(runtime) {
771
- switch (runtime.status) {
772
- case S.Idle:
773
- return { status: S.Idle };
774
- case S.Connecting:
775
- return { status: S.Connecting };
776
- case S.Connected:
777
- return { status: S.Connected, portInfo: runtime.port.getInfo() };
778
- case S.Disconnecting:
779
- return { status: S.Disconnecting };
780
- case S.Unsupported:
781
- return { status: S.Unsupported };
782
- case S.Error:
783
- return { status: S.Error, error: runtime.error };
784
- case S.Disposed:
785
- return { status: S.Disposed };
786
- default:
787
- return assertNeverRuntime(runtime);
788
- }
789
- }
790
- function isValidTransition(from, to) {
791
- if (from === to) {
792
- return false;
793
- }
794
- return ALLOWED_TRANSITIONS[from].includes(
795
- to
796
- );
797
- }
798
- function createIdleRuntime() {
799
- return { status: S.Idle };
800
- }
801
- function createConnectingRuntime(cancel) {
802
- return { status: S.Connecting, cancel };
803
- }
804
- function createConnectedRuntime(port, pump) {
805
- return { status: S.Connected, port, pump };
806
- }
807
- function createDisconnectingRuntime(port) {
808
- return { status: S.Disconnecting, port };
809
- }
810
- function createErrorRuntime(error) {
811
- return { status: S.Error, error };
812
- }
813
- function createUnsupportedRuntime() {
814
- return { status: S.Unsupported };
815
- }
816
- function createDisposedRuntime() {
817
- return { status: S.Disposed };
818
- }
819
- function createInitialRuntime(supported) {
820
- return supported ? createIdleRuntime() : createUnsupportedRuntime();
821
- }
822
- function getRuntimePort(runtime) {
823
- switch (runtime.status) {
824
- case S.Connected:
825
- return runtime.port;
826
- case S.Disconnecting:
827
- return runtime.port;
828
- default:
829
- return null;
830
- }
831
- }
832
- function getRuntimePump(runtime) {
833
- if (runtime.status === S.Connected) {
834
- return runtime.pump;
835
- }
836
- return null;
837
- }
838
- function createSessionRuntimeController(initial) {
839
- let runtime = initial;
840
- const subject = new BehaviorSubject(
841
- runtimeToPublicState(initial)
842
- );
843
- const transition = (next) => {
844
- const from = runtimeToSessionStatus(runtime);
845
- const to = runtimeToSessionStatus(next);
846
- if (from === to) {
847
- return false;
848
- }
849
- if (!isValidTransition(from, to)) {
850
- if (typeof console !== "undefined" && console.warn) {
851
- console.warn(
852
- `[web-serial-rxjs] Ignoring invalid SerialSession transition ${from} -> ${to}`
853
- );
854
- }
855
- return false;
856
- }
857
- const publicState = runtimeToPublicState(next);
858
- runtime = next;
859
- subject.next(publicState);
860
- return true;
861
- };
862
- return {
863
- get runtime() {
864
- return runtime;
865
- },
866
- get status() {
867
- return runtimeToSessionStatus(runtime);
868
- },
869
- get state$() {
870
- return subject.asObservable();
871
- },
872
- transition,
873
- complete() {
874
- subject.complete();
875
- }
876
- };
877
- }
878
- function assertNeverRuntime(value) {
879
- return assertNever(value);
880
- }
881
-
882
960
  // src/session/internal/session-error-reporter.ts
883
961
  function createSessionErrorReporter(deps) {
884
962
  const {
885
963
  controller,
886
964
  errorsSubject,
887
- sendQueue,
888
965
  isDisposed,
889
- teardownPump,
890
- closePortSafely
966
+ captureResources: captureResources2,
967
+ teardownResources
891
968
  } = deps;
892
969
  const createDisposedError = () => new SerialError(
893
970
  SerialErrorCode.SESSION_DISPOSED,
@@ -900,12 +977,12 @@ function createSessionErrorReporter(deps) {
900
977
  }
901
978
  errorsSubject.next(serialError);
902
979
  if (resolveErrorSeverity(serialError.code) === "fatal") {
903
- const runtime = controller.runtime;
904
- const portToClose = getRuntimePort(runtime);
905
- const pump = getRuntimePump(runtime);
980
+ const resources = captureResources2(controller.runtime);
906
981
  controller.transition(createErrorRuntime(serialError));
907
- sendQueue.clear();
908
- void teardownPump(pump).then(() => closePortSafely(portToClose));
982
+ void teardownResources(resources, {
983
+ closeMode: "safe",
984
+ cancelInFlightConnect: false
985
+ });
909
986
  }
910
987
  return serialError;
911
988
  };
@@ -1030,22 +1107,11 @@ function createSessionLifecycle(deps) {
1030
1107
  isDisposed,
1031
1108
  teardownPump,
1032
1109
  closePortSafely,
1110
+ captureResources: captureResources2,
1111
+ teardownResources,
1033
1112
  reportError,
1034
1113
  createDisposedError
1035
1114
  } = deps;
1036
- const teardownFromSnapshot = async (snapshot) => {
1037
- if (snapshot.status === SerialSessionStatus.Connecting) {
1038
- snapshot.cancel();
1039
- }
1040
- sendQueue.clear();
1041
- if (snapshot.status === SerialSessionStatus.Connected || snapshot.status === SerialSessionStatus.Disconnecting || snapshot.status === SerialSessionStatus.Error) {
1042
- const portToClose = getRuntimePort(snapshot);
1043
- const pump = getRuntimePump(snapshot);
1044
- await teardownPump(pump);
1045
- await closePortSafely(portToClose);
1046
- }
1047
- receivePipeline.clearLineBuffer();
1048
- };
1049
1115
  const completeSubjects = () => {
1050
1116
  controller.complete();
1051
1117
  errorsSubject.complete();
@@ -1058,10 +1124,11 @@ function createSessionLifecycle(deps) {
1058
1124
  return;
1059
1125
  }
1060
1126
  const snapshot = controller.runtime;
1127
+ const resources = captureResources2(snapshot);
1061
1128
  controller.transition(createDisposedRuntime());
1062
1129
  const run = async () => {
1063
1130
  try {
1064
- await teardownFromSnapshot(snapshot);
1131
+ await teardownResources(resources, { closeMode: "safe" });
1065
1132
  completeSubjects();
1066
1133
  subscriber.next();
1067
1134
  subscriber.complete();
@@ -1227,31 +1294,29 @@ function createSessionLifecycle(deps) {
1227
1294
  subscriber.error(error);
1228
1295
  return;
1229
1296
  }
1230
- const portToClose = getRuntimePort(runtime);
1231
- controller.transition(createDisconnectingRuntime(portToClose));
1232
- sendQueue.clear();
1297
+ const resources = captureResources2(runtime);
1298
+ controller.transition(createDisconnectingRuntime(resources.port));
1233
1299
  const run = async () => {
1234
1300
  try {
1235
- const pump = getRuntimePump(controller.runtime);
1236
- await teardownPump(pump);
1237
- if (portToClose) {
1238
- try {
1239
- await portToClose.close();
1240
- } catch (error) {
1241
- const serialError = reportError(error, {
1301
+ await teardownResources(resources, {
1302
+ closeMode: "report",
1303
+ onCloseError: (error) => {
1304
+ throw reportError(error, {
1242
1305
  fallbackCode: SerialErrorCode.CONNECTION_LOST,
1243
1306
  messagePrefix: "Failed to close port"
1244
1307
  });
1245
- subscriber.error(serialError);
1246
- return;
1247
1308
  }
1248
- }
1309
+ });
1249
1310
  if (!isDisposed()) {
1250
1311
  controller.transition(createIdleRuntime());
1251
1312
  }
1252
1313
  subscriber.next();
1253
1314
  subscriber.complete();
1254
1315
  } catch (error) {
1316
+ if (error instanceof SerialError) {
1317
+ subscriber.error(error);
1318
+ return;
1319
+ }
1255
1320
  const serialError = reportError(error, {
1256
1321
  fallbackCode: SerialErrorCode.UNKNOWN,
1257
1322
  messagePrefix: "Unexpected disconnect failure"
@@ -1443,16 +1508,21 @@ function createSerialSession(options) {
1443
1508
  const textEncoder = new TextEncoder();
1444
1509
  const isDisposed = () => controller.status === SerialSessionStatus.Disposed;
1445
1510
  const receivePipeline = createReceivePipeline({ resolvedOptions });
1446
- const { teardownPump, closePortSafely } = createPortTeardown({
1447
- receivePipeline
1511
+ const {
1512
+ teardownPump,
1513
+ closePortSafely,
1514
+ captureResources: captureResources2,
1515
+ teardownResources
1516
+ } = createPortTeardown({
1517
+ receivePipeline,
1518
+ sendQueue
1448
1519
  });
1449
1520
  const { reportError, createDisposedError } = createSessionErrorReporter({
1450
1521
  controller,
1451
1522
  errorsSubject,
1452
- sendQueue,
1453
1523
  isDisposed,
1454
- teardownPump,
1455
- closePortSafely
1524
+ captureResources: captureResources2,
1525
+ teardownResources
1456
1526
  });
1457
1527
  receivePipeline.bufferErrors$.subscribe(
1458
1528
  (error) => reportError(error, { fallbackCode: error.code })
@@ -1466,6 +1536,8 @@ function createSerialSession(options) {
1466
1536
  isDisposed,
1467
1537
  teardownPump,
1468
1538
  closePortSafely,
1539
+ captureResources: captureResources2,
1540
+ teardownResources,
1469
1541
  reportError,
1470
1542
  createDisposedError
1471
1543
  });