@gurezo/web-serial-rxjs 2.3.6 → 2.4.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/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/errors/serial-error-code.d.ts +43 -0
- package/dist/errors/serial-error-code.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +447 -84
- package/dist/index.mjs.map +4 -4
- package/dist/internal/newline-tokenizer.d.ts +38 -0
- package/dist/internal/newline-tokenizer.d.ts.map +1 -0
- package/dist/session/create-serial-session.d.ts +2 -1
- package/dist/session/create-serial-session.d.ts.map +1 -1
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/internal/line-buffer.d.ts +24 -2
- package/dist/session/internal/line-buffer.d.ts.map +1 -1
- package/dist/session/internal/receive-replay-buffer.d.ts +37 -0
- package/dist/session/internal/receive-replay-buffer.d.ts.map +1 -0
- package/dist/session/read-pump.d.ts +9 -1
- package/dist/session/read-pump.d.ts.map +1 -1
- package/dist/session/serial-session-options.d.ts +42 -1
- package/dist/session/serial-session-options.d.ts.map +1 -1
- package/dist/session/serial-session-state.d.ts +1 -0
- package/dist/session/serial-session-state.d.ts.map +1 -1
- package/dist/session/serial-session.d.ts +35 -2
- package/dist/session/serial-session.d.ts.map +1 -1
- package/dist/session/session-state-machine.d.ts +1 -0
- package/dist/session/session-state-machine.d.ts.map +1 -1
- package/dist/terminal/create-terminal-buffer.d.ts +47 -1
- package/dist/terminal/create-terminal-buffer.d.ts.map +1 -1
- package/package.json +2 -1
- package/dist/index.js +0 -724
package/dist/index.mjs
CHANGED
|
@@ -3,10 +3,9 @@ import {
|
|
|
3
3
|
BehaviorSubject as BehaviorSubject2,
|
|
4
4
|
distinctUntilChanged,
|
|
5
5
|
map as map2,
|
|
6
|
-
Observable as
|
|
7
|
-
ReplaySubject,
|
|
6
|
+
Observable as Observable4,
|
|
8
7
|
share,
|
|
9
|
-
Subject,
|
|
8
|
+
Subject as Subject2,
|
|
10
9
|
switchMap
|
|
11
10
|
} from "rxjs";
|
|
12
11
|
|
|
@@ -23,6 +22,10 @@ var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
|
|
|
23
22
|
SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
|
|
24
23
|
SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
|
|
25
24
|
SerialErrorCode2["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
|
|
25
|
+
SerialErrorCode2["LINE_BUFFER_OVERFLOW"] = "LINE_BUFFER_OVERFLOW";
|
|
26
|
+
SerialErrorCode2["INVALID_RECEIVE_REPLAY_OPTIONS"] = "INVALID_RECEIVE_REPLAY_OPTIONS";
|
|
27
|
+
SerialErrorCode2["RECEIVE_REPLAY_BUFFER_OVERFLOW"] = "RECEIVE_REPLAY_BUFFER_OVERFLOW";
|
|
28
|
+
SerialErrorCode2["SESSION_DISPOSED"] = "SESSION_DISPOSED";
|
|
26
29
|
SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
|
|
27
30
|
return SerialErrorCode2;
|
|
28
31
|
})(SerialErrorCode || {});
|
|
@@ -68,42 +71,180 @@ var SerialError = class _SerialError extends Error {
|
|
|
68
71
|
|
|
69
72
|
// src/terminal/create-terminal-buffer.ts
|
|
70
73
|
import { map, scan, shareReplay } from "rxjs";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
|
|
75
|
+
// src/internal/newline-tokenizer.ts
|
|
76
|
+
function createNewlineTokenizer(mode) {
|
|
77
|
+
let pending = "";
|
|
78
|
+
let deferredTrailingCr = false;
|
|
79
|
+
const clear = () => {
|
|
80
|
+
pending = "";
|
|
81
|
+
deferredTrailingCr = false;
|
|
82
|
+
};
|
|
83
|
+
const restorePending = (text) => {
|
|
84
|
+
pending = text;
|
|
85
|
+
deferredTrailingCr = false;
|
|
86
|
+
};
|
|
87
|
+
const feed = (chunk) => {
|
|
88
|
+
const events = [];
|
|
89
|
+
let i = 0;
|
|
90
|
+
if (mode === "line" && deferredTrailingCr) {
|
|
91
|
+
deferredTrailingCr = false;
|
|
92
|
+
if (chunk.length > 0 && chunk.charAt(0) === "\n") {
|
|
93
|
+
events.push({ type: "line", content: pending });
|
|
94
|
+
pending = "";
|
|
95
|
+
i = 1;
|
|
82
96
|
} else {
|
|
83
|
-
|
|
97
|
+
events.push({ type: "line", content: pending });
|
|
98
|
+
pending = "";
|
|
84
99
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
}
|
|
101
|
+
const len = chunk.length;
|
|
102
|
+
for (; i < len; i++) {
|
|
103
|
+
const c = chunk.charAt(i);
|
|
104
|
+
if (c === "\r") {
|
|
105
|
+
const next = i + 1 < len ? chunk.charAt(i + 1) : "";
|
|
106
|
+
if (next === "\n") {
|
|
107
|
+
events.push({ type: "line", content: pending });
|
|
108
|
+
pending = "";
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (mode === "line") {
|
|
113
|
+
if (next !== "") {
|
|
114
|
+
events.push({ type: "line", content: pending });
|
|
115
|
+
pending = "";
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
deferredTrailingCr = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
events.push({ type: "carriage-return" });
|
|
122
|
+
pending = "";
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (c === "\n") {
|
|
126
|
+
events.push({ type: "line", content: pending });
|
|
127
|
+
pending = "";
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
pending += c;
|
|
131
|
+
}
|
|
132
|
+
return events;
|
|
133
|
+
};
|
|
134
|
+
const trimPending = (maxChars) => {
|
|
135
|
+
if (maxChars <= 0) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const text = deferredTrailingCr ? `${pending}\r` : pending;
|
|
139
|
+
if (text.length <= maxChars) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const trimmed = text.slice(text.length - maxChars);
|
|
143
|
+
if (trimmed.endsWith("\r")) {
|
|
144
|
+
pending = trimmed.slice(0, -1);
|
|
145
|
+
deferredTrailingCr = true;
|
|
88
146
|
} else {
|
|
89
|
-
|
|
147
|
+
pending = trimmed;
|
|
148
|
+
deferredTrailingCr = false;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
feed,
|
|
154
|
+
clear,
|
|
155
|
+
restorePending,
|
|
156
|
+
getPendingText: () => deferredTrailingCr ? `${pending}\r` : pending,
|
|
157
|
+
trimPending
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/terminal/create-terminal-buffer.ts
|
|
162
|
+
function applyTerminalChunk(state, chunk) {
|
|
163
|
+
const tokenizer = createNewlineTokenizer("terminal");
|
|
164
|
+
tokenizer.restorePending(state.currentLine);
|
|
165
|
+
const events = tokenizer.feed(chunk);
|
|
166
|
+
let { completed } = state;
|
|
167
|
+
for (const event of events) {
|
|
168
|
+
if (event.type === "line") {
|
|
169
|
+
completed += event.content + "\n";
|
|
90
170
|
}
|
|
91
171
|
}
|
|
92
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
completed,
|
|
174
|
+
currentLine: tokenizer.getPendingText()
|
|
175
|
+
};
|
|
93
176
|
}
|
|
94
177
|
function terminalDisplayText(state) {
|
|
95
178
|
return state.completed + state.currentLine;
|
|
96
179
|
}
|
|
180
|
+
function countCompletedLines(completed) {
|
|
181
|
+
if (completed.length === 0) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
let count = 0;
|
|
185
|
+
for (let i = 0; i < completed.length; i++) {
|
|
186
|
+
if (completed.charAt(i) === "\n") {
|
|
187
|
+
count++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return count;
|
|
191
|
+
}
|
|
192
|
+
function trimCompletedByMaxLines(completed, maxLines) {
|
|
193
|
+
if (maxLines <= 0) {
|
|
194
|
+
return completed;
|
|
195
|
+
}
|
|
196
|
+
let trimmed = completed;
|
|
197
|
+
while (countCompletedLines(trimmed) > maxLines) {
|
|
198
|
+
const firstNewline = trimmed.indexOf("\n");
|
|
199
|
+
if (firstNewline < 0) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
trimmed = trimmed.slice(firstNewline + 1);
|
|
203
|
+
}
|
|
204
|
+
return trimmed;
|
|
205
|
+
}
|
|
206
|
+
function trimTerminalState(state, limits) {
|
|
207
|
+
let { completed, currentLine } = state;
|
|
208
|
+
if (limits.maxLines > 0) {
|
|
209
|
+
completed = trimCompletedByMaxLines(completed, limits.maxLines);
|
|
210
|
+
}
|
|
211
|
+
if (limits.maxChars > 0) {
|
|
212
|
+
let total = completed.length + currentLine.length;
|
|
213
|
+
while (total > limits.maxChars) {
|
|
214
|
+
const excess = total - limits.maxChars;
|
|
215
|
+
if (completed.length >= excess) {
|
|
216
|
+
completed = completed.slice(excess);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
const removeFromCurrent = excess - completed.length;
|
|
220
|
+
completed = "";
|
|
221
|
+
currentLine = currentLine.slice(removeFromCurrent);
|
|
222
|
+
total = completed.length + currentLine.length;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { completed, currentLine };
|
|
226
|
+
}
|
|
227
|
+
var DEFAULT_TERMINAL_BUFFER_OPTIONS = {
|
|
228
|
+
maxLines: 1e4,
|
|
229
|
+
maxChars: 1048576
|
|
230
|
+
};
|
|
231
|
+
function resolveTerminalBufferLimits(options) {
|
|
232
|
+
return {
|
|
233
|
+
...DEFAULT_TERMINAL_BUFFER_OPTIONS,
|
|
234
|
+
...options
|
|
235
|
+
};
|
|
236
|
+
}
|
|
97
237
|
var initialTerminalState = {
|
|
98
238
|
completed: "",
|
|
99
239
|
currentLine: ""
|
|
100
240
|
};
|
|
101
|
-
function createTerminalBuffer(receive
|
|
241
|
+
function createTerminalBuffer(receive$, options) {
|
|
242
|
+
const limits = resolveTerminalBufferLimits(options);
|
|
102
243
|
const text$ = receive$.pipe(
|
|
103
|
-
scan(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
),
|
|
244
|
+
scan((state, chunk) => {
|
|
245
|
+
const next = applyTerminalChunk(state, chunk);
|
|
246
|
+
return trimTerminalState(next, limits);
|
|
247
|
+
}, initialTerminalState),
|
|
107
248
|
map(terminalDisplayText),
|
|
108
249
|
shareReplay({ bufferSize: 1, refCount: true })
|
|
109
250
|
);
|
|
@@ -150,43 +291,86 @@ function hasWebSerialSupport() {
|
|
|
150
291
|
}
|
|
151
292
|
|
|
152
293
|
// src/session/internal/line-buffer.ts
|
|
153
|
-
|
|
154
|
-
|
|
294
|
+
var DEFAULT_LINE_BUFFER_OPTIONS = {
|
|
295
|
+
maxChars: 1048576
|
|
296
|
+
};
|
|
297
|
+
function createLineBuffer(options) {
|
|
298
|
+
const limits = {
|
|
299
|
+
...DEFAULT_LINE_BUFFER_OPTIONS,
|
|
300
|
+
...options
|
|
301
|
+
};
|
|
302
|
+
const tokenizer = createNewlineTokenizer("line");
|
|
155
303
|
const clear = () => {
|
|
156
|
-
|
|
304
|
+
tokenizer.clear();
|
|
157
305
|
};
|
|
158
306
|
const feed = (chunk) => {
|
|
159
|
-
|
|
307
|
+
const events = tokenizer.feed(chunk);
|
|
160
308
|
const out = [];
|
|
161
|
-
for (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
out.push(buffer.slice(0, crlf));
|
|
165
|
-
buffer = buffer.slice(crlf + 2);
|
|
166
|
-
continue;
|
|
309
|
+
for (const event of events) {
|
|
310
|
+
if (event.type === "line") {
|
|
311
|
+
out.push(event.content);
|
|
167
312
|
}
|
|
168
|
-
const cr = buffer.indexOf("\r");
|
|
169
|
-
if (cr >= 0 && cr + 1 < buffer.length && buffer[cr + 1] !== "\n") {
|
|
170
|
-
out.push(buffer.slice(0, cr));
|
|
171
|
-
buffer = buffer.slice(cr + 1);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const nl = buffer.indexOf("\n");
|
|
175
|
-
if (nl >= 0) {
|
|
176
|
-
out.push(buffer.slice(0, nl));
|
|
177
|
-
buffer = buffer.slice(nl + 1);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (cr >= 0 && cr + 1 === buffer.length) {
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
break;
|
|
184
313
|
}
|
|
185
|
-
|
|
314
|
+
const overflowed = tokenizer.trimPending(limits.maxChars);
|
|
315
|
+
return { lines: out, overflowed };
|
|
186
316
|
};
|
|
187
317
|
return { feed, clear };
|
|
188
318
|
}
|
|
189
319
|
|
|
320
|
+
// src/session/internal/receive-replay-buffer.ts
|
|
321
|
+
import { Observable, Subject } from "rxjs";
|
|
322
|
+
function totalChars(chunks) {
|
|
323
|
+
return chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
324
|
+
}
|
|
325
|
+
function createReceiveReplayBuffer(options) {
|
|
326
|
+
const chunks = [];
|
|
327
|
+
const live$ = new Subject();
|
|
328
|
+
let completed = false;
|
|
329
|
+
const trim = () => {
|
|
330
|
+
let overflowed = false;
|
|
331
|
+
while (chunks.length > options.bufferSize) {
|
|
332
|
+
chunks.shift();
|
|
333
|
+
overflowed = true;
|
|
334
|
+
}
|
|
335
|
+
if (options.maxChars > 0) {
|
|
336
|
+
let chars = totalChars(chunks);
|
|
337
|
+
while (chars > options.maxChars && chunks.length > 1) {
|
|
338
|
+
chars -= chunks.shift().length;
|
|
339
|
+
overflowed = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return overflowed;
|
|
343
|
+
};
|
|
344
|
+
const next = (chunk) => {
|
|
345
|
+
if (completed) {
|
|
346
|
+
return { overflowed: false };
|
|
347
|
+
}
|
|
348
|
+
chunks.push(chunk);
|
|
349
|
+
const overflowed = trim();
|
|
350
|
+
live$.next(chunk);
|
|
351
|
+
return { overflowed };
|
|
352
|
+
};
|
|
353
|
+
const asObservable = () => new Observable((subscriber) => {
|
|
354
|
+
for (const chunk of chunks) {
|
|
355
|
+
subscriber.next(chunk);
|
|
356
|
+
}
|
|
357
|
+
if (completed) {
|
|
358
|
+
subscriber.complete();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
return live$.subscribe(subscriber);
|
|
362
|
+
});
|
|
363
|
+
const complete = () => {
|
|
364
|
+
if (completed) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
completed = true;
|
|
368
|
+
chunks.length = 0;
|
|
369
|
+
live$.complete();
|
|
370
|
+
};
|
|
371
|
+
return { next, asObservable, complete };
|
|
372
|
+
}
|
|
373
|
+
|
|
190
374
|
// src/session/normalize-serial-error.ts
|
|
191
375
|
var DEFAULT_MESSAGE_PREFIX = "Serial operation failed";
|
|
192
376
|
var isDomExceptionWithName = (error, name) => typeof DOMException !== "undefined" && error instanceof DOMException && error.name === name;
|
|
@@ -211,7 +395,7 @@ function normalizeSerialError(error, options) {
|
|
|
211
395
|
}
|
|
212
396
|
|
|
213
397
|
// src/session/read-pump.ts
|
|
214
|
-
function createReadPump(port, { onChunk, onError }) {
|
|
398
|
+
function createReadPump(port, { onChunk, onError, onDone }) {
|
|
215
399
|
let reader = null;
|
|
216
400
|
let running = false;
|
|
217
401
|
let stopped = false;
|
|
@@ -233,6 +417,9 @@ function createReadPump(port, { onChunk, onError }) {
|
|
|
233
417
|
while (!stopped) {
|
|
234
418
|
const { done, value } = await reader.read();
|
|
235
419
|
if (done) {
|
|
420
|
+
if (!stopped) {
|
|
421
|
+
onDone?.();
|
|
422
|
+
}
|
|
236
423
|
break;
|
|
237
424
|
}
|
|
238
425
|
if (value && value.byteLength > 0) {
|
|
@@ -302,13 +489,13 @@ function createReadPump(port, { onChunk, onError }) {
|
|
|
302
489
|
}
|
|
303
490
|
|
|
304
491
|
// src/session/send-queue.ts
|
|
305
|
-
import { Observable, defer } from "rxjs";
|
|
492
|
+
import { Observable as Observable2, defer } from "rxjs";
|
|
306
493
|
function createSendQueue() {
|
|
307
494
|
let chain = Promise.resolve();
|
|
308
495
|
return {
|
|
309
496
|
enqueue(operation) {
|
|
310
497
|
return defer(
|
|
311
|
-
() => new
|
|
498
|
+
() => new Observable2((subscriber) => {
|
|
312
499
|
let cancelled = false;
|
|
313
500
|
const run = async () => {
|
|
314
501
|
try {
|
|
@@ -341,10 +528,33 @@ function createSendQueue() {
|
|
|
341
528
|
}
|
|
342
529
|
|
|
343
530
|
// src/session/serial-session-options.ts
|
|
531
|
+
var MAX_RECEIVE_REPLAY_BUFFER_SIZE = 65536;
|
|
532
|
+
var MAX_RECEIVE_REPLAY_MAX_CHARS = 1048576;
|
|
344
533
|
var DEFAULT_RECEIVE_REPLAY = {
|
|
345
534
|
enabled: false,
|
|
346
|
-
bufferSize: 512
|
|
535
|
+
bufferSize: 512,
|
|
536
|
+
maxChars: 0
|
|
347
537
|
};
|
|
538
|
+
function resolveReceiveReplayOptions(options) {
|
|
539
|
+
const merged = {
|
|
540
|
+
...DEFAULT_RECEIVE_REPLAY,
|
|
541
|
+
...options
|
|
542
|
+
};
|
|
543
|
+
const { bufferSize, maxChars } = merged;
|
|
544
|
+
if (!Number.isSafeInteger(bufferSize) || bufferSize < 1 || bufferSize > MAX_RECEIVE_REPLAY_BUFFER_SIZE) {
|
|
545
|
+
throw new SerialError(
|
|
546
|
+
"INVALID_RECEIVE_REPLAY_OPTIONS" /* INVALID_RECEIVE_REPLAY_OPTIONS */,
|
|
547
|
+
`Invalid receiveReplay.bufferSize: ${bufferSize}. Must be a safe integer between 1 and ${MAX_RECEIVE_REPLAY_BUFFER_SIZE}.`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 0 || maxChars > MAX_RECEIVE_REPLAY_MAX_CHARS) {
|
|
551
|
+
throw new SerialError(
|
|
552
|
+
"INVALID_RECEIVE_REPLAY_OPTIONS" /* INVALID_RECEIVE_REPLAY_OPTIONS */,
|
|
553
|
+
`Invalid receiveReplay.maxChars: ${maxChars}. Must be a safe integer between 0 and ${MAX_RECEIVE_REPLAY_MAX_CHARS}.`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return merged;
|
|
557
|
+
}
|
|
348
558
|
var DEFAULT_SERIAL_SESSION_OPTIONS = {
|
|
349
559
|
baudRate: 9600,
|
|
350
560
|
dataBits: 8,
|
|
@@ -353,7 +563,9 @@ var DEFAULT_SERIAL_SESSION_OPTIONS = {
|
|
|
353
563
|
bufferSize: 255,
|
|
354
564
|
flowControl: "none",
|
|
355
565
|
filters: void 0,
|
|
356
|
-
receiveReplay: { ...DEFAULT_RECEIVE_REPLAY }
|
|
566
|
+
receiveReplay: { ...DEFAULT_RECEIVE_REPLAY },
|
|
567
|
+
terminalBuffer: { ...DEFAULT_TERMINAL_BUFFER_OPTIONS },
|
|
568
|
+
lineBuffer: { ...DEFAULT_LINE_BUFFER_OPTIONS }
|
|
357
569
|
};
|
|
358
570
|
|
|
359
571
|
// src/session/serial-session-state.ts
|
|
@@ -363,19 +575,21 @@ var SerialSessionState = {
|
|
|
363
575
|
Connected: "connected",
|
|
364
576
|
Disconnecting: "disconnecting",
|
|
365
577
|
Unsupported: "unsupported",
|
|
366
|
-
Error: "error"
|
|
578
|
+
Error: "error",
|
|
579
|
+
Disposed: "disposed"
|
|
367
580
|
};
|
|
368
581
|
|
|
369
582
|
// src/session/session-state-machine.ts
|
|
370
583
|
import { BehaviorSubject } from "rxjs";
|
|
371
584
|
var S = SerialSessionState;
|
|
372
585
|
var ALLOWED_TRANSITIONS = {
|
|
373
|
-
[S.Idle]: [S.Connecting, S.Error],
|
|
374
|
-
[S.Connecting]: [S.Connected, S.Error, S.Idle],
|
|
375
|
-
[S.Connected]: [S.Disconnecting, S.Error],
|
|
376
|
-
[S.Disconnecting]: [S.Idle, S.Error],
|
|
377
|
-
[S.Error]: [S.Idle, S.Connecting],
|
|
378
|
-
[S.Unsupported]: []
|
|
586
|
+
[S.Idle]: [S.Connecting, S.Error, S.Disposed],
|
|
587
|
+
[S.Connecting]: [S.Connected, S.Error, S.Idle, S.Disposed],
|
|
588
|
+
[S.Connected]: [S.Disconnecting, S.Error, S.Disposed],
|
|
589
|
+
[S.Disconnecting]: [S.Idle, S.Error, S.Disposed],
|
|
590
|
+
[S.Error]: [S.Idle, S.Connecting, S.Disposed],
|
|
591
|
+
[S.Unsupported]: [S.Disposed],
|
|
592
|
+
[S.Disposed]: []
|
|
379
593
|
};
|
|
380
594
|
var SessionStateMachine = class {
|
|
381
595
|
constructor(initial = SerialSessionState.Idle) {
|
|
@@ -405,6 +619,9 @@ var SessionStateMachine = class {
|
|
|
405
619
|
toUnsupported() {
|
|
406
620
|
return this.transition(S.Unsupported);
|
|
407
621
|
}
|
|
622
|
+
toDisposed() {
|
|
623
|
+
return this.transition(S.Disposed);
|
|
624
|
+
}
|
|
408
625
|
complete() {
|
|
409
626
|
this.subject.complete();
|
|
410
627
|
}
|
|
@@ -433,25 +650,33 @@ function createSerialSession(options) {
|
|
|
433
650
|
...DEFAULT_SERIAL_SESSION_OPTIONS,
|
|
434
651
|
...options,
|
|
435
652
|
filters: options?.filters,
|
|
436
|
-
receiveReplay:
|
|
437
|
-
|
|
438
|
-
...
|
|
653
|
+
receiveReplay: resolveReceiveReplayOptions(options?.receiveReplay),
|
|
654
|
+
terminalBuffer: {
|
|
655
|
+
...DEFAULT_SERIAL_SESSION_OPTIONS.terminalBuffer,
|
|
656
|
+
...options?.terminalBuffer
|
|
657
|
+
},
|
|
658
|
+
lineBuffer: {
|
|
659
|
+
...DEFAULT_SERIAL_SESSION_OPTIONS.lineBuffer,
|
|
660
|
+
...options?.lineBuffer
|
|
439
661
|
}
|
|
440
662
|
};
|
|
441
663
|
const supported = hasWebSerialSupport();
|
|
442
664
|
const machine = new SessionStateMachine(
|
|
443
665
|
supported ? SerialSessionState.Idle : SerialSessionState.Unsupported
|
|
444
666
|
);
|
|
445
|
-
const errorsSubject = new
|
|
446
|
-
const receiveSubject = new
|
|
447
|
-
const linesSubject = new
|
|
667
|
+
const errorsSubject = new Subject2();
|
|
668
|
+
const receiveSubject = new Subject2();
|
|
669
|
+
const linesSubject = new Subject2();
|
|
448
670
|
const sendQueue = createSendQueue();
|
|
449
671
|
const textEncoder = new TextEncoder();
|
|
450
|
-
const lineBuffer = createLineBuffer();
|
|
672
|
+
const lineBuffer = createLineBuffer(resolvedOptions.lineBuffer);
|
|
451
673
|
const errors$ = errorsSubject.asObservable();
|
|
452
674
|
const receive$ = receiveSubject.asObservable();
|
|
453
675
|
const lines$ = linesSubject.asObservable();
|
|
454
|
-
const terminalText$ = createTerminalBuffer(
|
|
676
|
+
const terminalText$ = createTerminalBuffer(
|
|
677
|
+
receive$,
|
|
678
|
+
resolvedOptions.terminalBuffer
|
|
679
|
+
).text$;
|
|
455
680
|
const isConnected$ = machine.state$.pipe(
|
|
456
681
|
map2((state) => state === SerialSessionState.Connected),
|
|
457
682
|
distinctUntilChanged()
|
|
@@ -477,13 +702,70 @@ function createSerialSession(options) {
|
|
|
477
702
|
activeReceiveReplay.complete();
|
|
478
703
|
activeReceiveReplay = null;
|
|
479
704
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
705
|
+
const buffer = createReceiveReplayBuffer({
|
|
706
|
+
bufferSize: resolvedOptions.receiveReplay.bufferSize,
|
|
707
|
+
maxChars: resolvedOptions.receiveReplay.maxChars
|
|
708
|
+
});
|
|
709
|
+
activeReceiveReplay = buffer;
|
|
710
|
+
receiveReplayStream$.next(buffer.asObservable());
|
|
483
711
|
};
|
|
484
712
|
const receiveReplay$ = receiveReplayStream$ ? receiveReplayStream$.pipe(switchMap((inner) => inner), share()) : receive$;
|
|
485
713
|
let activePort = null;
|
|
486
714
|
let activePump = null;
|
|
715
|
+
let activeConnectCancel = null;
|
|
716
|
+
let disposed = false;
|
|
717
|
+
const createDisposedError = () => new SerialError(
|
|
718
|
+
"SESSION_DISPOSED" /* SESSION_DISPOSED */,
|
|
719
|
+
"SerialSession has been disposed"
|
|
720
|
+
);
|
|
721
|
+
const completeSession = async () => {
|
|
722
|
+
const current = machine.current;
|
|
723
|
+
if (current === SerialSessionState.Connecting) {
|
|
724
|
+
activeConnectCancel?.();
|
|
725
|
+
}
|
|
726
|
+
sendQueue.clear();
|
|
727
|
+
if (current === SerialSessionState.Connected || current === SerialSessionState.Error || current === SerialSessionState.Disconnecting) {
|
|
728
|
+
const portToClose = activePort;
|
|
729
|
+
await teardownPump();
|
|
730
|
+
await closePortSafely(portToClose);
|
|
731
|
+
setActivePort(null);
|
|
732
|
+
}
|
|
733
|
+
lineBuffer.clear();
|
|
734
|
+
machine.toDisposed();
|
|
735
|
+
machine.complete();
|
|
736
|
+
errorsSubject.complete();
|
|
737
|
+
receiveSubject.complete();
|
|
738
|
+
linesSubject.complete();
|
|
739
|
+
portInfoSubject.complete();
|
|
740
|
+
receiveReplayStream$?.complete();
|
|
741
|
+
};
|
|
742
|
+
const dispose$ = () => new Observable4((subscriber) => {
|
|
743
|
+
if (disposed) {
|
|
744
|
+
subscriber.next();
|
|
745
|
+
subscriber.complete();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
disposed = true;
|
|
749
|
+
const run = async () => {
|
|
750
|
+
try {
|
|
751
|
+
await completeSession();
|
|
752
|
+
subscriber.next();
|
|
753
|
+
subscriber.complete();
|
|
754
|
+
} catch (error) {
|
|
755
|
+
const serialError = normalizeSerialError(error, {
|
|
756
|
+
fallbackCode: "UNKNOWN" /* UNKNOWN */,
|
|
757
|
+
messagePrefix: "Unexpected dispose failure"
|
|
758
|
+
});
|
|
759
|
+
subscriber.error(serialError);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
void run();
|
|
763
|
+
});
|
|
764
|
+
const clearActiveConnectCancel = (cancel) => {
|
|
765
|
+
if (activeConnectCancel === cancel) {
|
|
766
|
+
activeConnectCancel = null;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
487
769
|
const setActivePort = (port) => {
|
|
488
770
|
activePort = port;
|
|
489
771
|
portInfoSubject.next(port ? port.getInfo() : null);
|
|
@@ -508,6 +790,9 @@ function createSerialSession(options) {
|
|
|
508
790
|
};
|
|
509
791
|
const reportError = (error, severity, options2) => {
|
|
510
792
|
const serialError = normalizeSerialError(error, options2);
|
|
793
|
+
if (disposed) {
|
|
794
|
+
return serialError;
|
|
795
|
+
}
|
|
511
796
|
errorsSubject.next(serialError);
|
|
512
797
|
if (severity === "fatal") {
|
|
513
798
|
machine.toError();
|
|
@@ -541,7 +826,11 @@ function createSerialSession(options) {
|
|
|
541
826
|
return hasWebSerialSupport();
|
|
542
827
|
},
|
|
543
828
|
connect$() {
|
|
544
|
-
return new
|
|
829
|
+
return new Observable4((subscriber) => {
|
|
830
|
+
if (disposed) {
|
|
831
|
+
subscriber.error(createDisposedError());
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
545
834
|
if (!hasWebSerialSupport()) {
|
|
546
835
|
const error = reportError(
|
|
547
836
|
new SerialError(
|
|
@@ -568,6 +857,13 @@ function createSerialSession(options) {
|
|
|
568
857
|
return;
|
|
569
858
|
}
|
|
570
859
|
let cancelled = false;
|
|
860
|
+
const cancelInFlightConnect = () => {
|
|
861
|
+
cancelled = true;
|
|
862
|
+
if (machine.current === SerialSessionState.Connecting) {
|
|
863
|
+
machine.toIdle();
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
activeConnectCancel = cancelInFlightConnect;
|
|
571
867
|
machine.toConnecting();
|
|
572
868
|
const run = async () => {
|
|
573
869
|
let selectedPort = null;
|
|
@@ -596,7 +892,7 @@ function createSerialSession(options) {
|
|
|
596
892
|
}
|
|
597
893
|
return;
|
|
598
894
|
}
|
|
599
|
-
if (cancelled) {
|
|
895
|
+
if (cancelled || disposed) {
|
|
600
896
|
await closePortSafely(selectedPort);
|
|
601
897
|
return;
|
|
602
898
|
}
|
|
@@ -609,33 +905,89 @@ function createSerialSession(options) {
|
|
|
609
905
|
onChunk: (text) => {
|
|
610
906
|
receiveSubject.next(text);
|
|
611
907
|
if (activeReceiveReplay) {
|
|
612
|
-
activeReceiveReplay.next(text);
|
|
908
|
+
const { overflowed: overflowed2 } = activeReceiveReplay.next(text);
|
|
909
|
+
if (overflowed2) {
|
|
910
|
+
reportError(
|
|
911
|
+
new SerialError(
|
|
912
|
+
"RECEIVE_REPLAY_BUFFER_OVERFLOW" /* RECEIVE_REPLAY_BUFFER_OVERFLOW */,
|
|
913
|
+
`Receive replay buffer exceeded configured limits; oldest chunks were discarded`
|
|
914
|
+
),
|
|
915
|
+
"non-fatal",
|
|
916
|
+
{
|
|
917
|
+
fallbackCode: "RECEIVE_REPLAY_BUFFER_OVERFLOW" /* RECEIVE_REPLAY_BUFFER_OVERFLOW */
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const { lines, overflowed } = lineBuffer.feed(text);
|
|
923
|
+
if (overflowed) {
|
|
924
|
+
reportError(
|
|
925
|
+
new SerialError(
|
|
926
|
+
"LINE_BUFFER_OVERFLOW" /* LINE_BUFFER_OVERFLOW */,
|
|
927
|
+
`Line buffer exceeded maxChars (${resolvedOptions.lineBuffer.maxChars}); leading data was discarded`
|
|
928
|
+
),
|
|
929
|
+
"non-fatal",
|
|
930
|
+
{ fallbackCode: "LINE_BUFFER_OVERFLOW" /* LINE_BUFFER_OVERFLOW */ }
|
|
931
|
+
);
|
|
613
932
|
}
|
|
614
|
-
for (const line of
|
|
933
|
+
for (const line of lines) {
|
|
615
934
|
linesSubject.next(line);
|
|
616
935
|
}
|
|
617
936
|
},
|
|
618
937
|
onError: (pumpError) => reportError(pumpError, "fatal", {
|
|
619
938
|
fallbackCode: "READ_FAILED" /* READ_FAILED */,
|
|
620
939
|
messagePrefix: "Read pump failed"
|
|
621
|
-
})
|
|
940
|
+
}),
|
|
941
|
+
onDone: () => {
|
|
942
|
+
if (machine.current !== SerialSessionState.Connected) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
reportError(
|
|
946
|
+
new SerialError(
|
|
947
|
+
"CONNECTION_LOST" /* CONNECTION_LOST */,
|
|
948
|
+
"Read pump ended unexpectedly: stream closed while connected"
|
|
949
|
+
),
|
|
950
|
+
"fatal",
|
|
951
|
+
{
|
|
952
|
+
fallbackCode: "CONNECTION_LOST" /* CONNECTION_LOST */
|
|
953
|
+
}
|
|
954
|
+
);
|
|
955
|
+
}
|
|
622
956
|
});
|
|
623
957
|
activePump.start();
|
|
624
958
|
sendQueue.clear();
|
|
959
|
+
if (disposed) {
|
|
960
|
+
await teardownPump();
|
|
961
|
+
await closePortSafely(selectedPort);
|
|
962
|
+
setActivePort(null);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
625
965
|
machine.toConnected();
|
|
626
966
|
subscriber.next();
|
|
627
967
|
subscriber.complete();
|
|
628
968
|
};
|
|
629
969
|
void run();
|
|
630
970
|
return () => {
|
|
631
|
-
|
|
971
|
+
clearActiveConnectCancel(cancelInFlightConnect);
|
|
972
|
+
cancelInFlightConnect();
|
|
632
973
|
};
|
|
633
974
|
});
|
|
634
975
|
},
|
|
635
976
|
disconnect$() {
|
|
636
|
-
return new
|
|
977
|
+
return new Observable4((subscriber) => {
|
|
978
|
+
if (disposed) {
|
|
979
|
+
subscriber.next();
|
|
980
|
+
subscriber.complete();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
637
983
|
const current = machine.current;
|
|
638
|
-
if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported) {
|
|
984
|
+
if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported || current === SerialSessionState.Disconnecting) {
|
|
985
|
+
subscriber.next();
|
|
986
|
+
subscriber.complete();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (current === SerialSessionState.Connecting) {
|
|
990
|
+
activeConnectCancel?.();
|
|
639
991
|
subscriber.next();
|
|
640
992
|
subscriber.complete();
|
|
641
993
|
return;
|
|
@@ -672,7 +1024,9 @@ function createSerialSession(options) {
|
|
|
672
1024
|
}
|
|
673
1025
|
}
|
|
674
1026
|
setActivePort(null);
|
|
675
|
-
|
|
1027
|
+
if (!disposed) {
|
|
1028
|
+
machine.toIdle();
|
|
1029
|
+
}
|
|
676
1030
|
subscriber.next();
|
|
677
1031
|
subscriber.complete();
|
|
678
1032
|
} catch (error) {
|
|
@@ -686,7 +1040,14 @@ function createSerialSession(options) {
|
|
|
686
1040
|
void run();
|
|
687
1041
|
});
|
|
688
1042
|
},
|
|
1043
|
+
dispose$,
|
|
1044
|
+
destroy$: dispose$,
|
|
689
1045
|
send$(data) {
|
|
1046
|
+
if (disposed) {
|
|
1047
|
+
return new Observable4((subscriber) => {
|
|
1048
|
+
subscriber.error(createDisposedError());
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
690
1051
|
return sendQueue.enqueue(async () => {
|
|
691
1052
|
const payload = typeof data === "string" ? textEncoder.encode(data) : data;
|
|
692
1053
|
try {
|
|
@@ -716,6 +1077,8 @@ function createSerialSession(options) {
|
|
|
716
1077
|
};
|
|
717
1078
|
}
|
|
718
1079
|
export {
|
|
1080
|
+
DEFAULT_LINE_BUFFER_OPTIONS,
|
|
1081
|
+
DEFAULT_TERMINAL_BUFFER_OPTIONS,
|
|
719
1082
|
SerialError,
|
|
720
1083
|
SerialErrorCode,
|
|
721
1084
|
SerialSessionState,
|