@kubohiroya/turbowarp-webrtc-qrcode-pairing 0.2.0 → 0.2.2
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 +3 -3
- package/README.md +4 -4
- package/dist/webrtc-qrcode-pairing.js +117 -53
- package/docs/integration-guide.ja.md +5 -4
- package/docs/integration-guide.md +6 -5
- package/package.json +1 -1
- package/src/pairing/controller.ts +107 -47
- package/src/pairing/types.ts +7 -0
- package/src/ports/qr-scan.ts +13 -1
- package/src/qr/courier.ts +29 -11
package/README.ja.md
CHANGED
|
@@ -15,7 +15,7 @@ QRコードを使ってWebRTCの接続情報を交換するTurboWarp拡張です
|
|
|
15
15
|
- Answerがどの Offer への応答かを追跡し、2台のカメラ側端末の接続情報が入れ替わらないようにします。
|
|
16
16
|
- 搬送の進捗と接続状態を別々の状態として報告します。エラー、取消、再試行、期限にも対応します。
|
|
17
17
|
|
|
18
|
-
`0.2.0`では、メッセージを連結QRコードで運び、1枚のQRを既定でOfferはversion 15、Answerはversion 20までに抑え、読んだペアリング用のQRをすべて報告します。`turbowarp-jsqr` 0.4.0
|
|
18
|
+
`0.2.0`では、メッセージを連結QRコードで運び、1枚のQRを既定でOfferはversion 15、Answerはversion 20までに抑え、読んだペアリング用のQRをすべて報告します。`turbowarp-jsqr` 0.4.0以降が必要です。`0.2.1`では、QRがカメラに写り続けている間の読取りがページの処理を止めてしまう不具合を直しました。`0.2.2`では、1枚目に収まらないheaderを先頭から続くQRで読み、読取りを1つずつ処理して接続情報を一度だけ渡し、規格に合わないQRを読んでも集めていたものを消さないようにしました。
|
|
19
19
|
|
|
20
20
|
> [!NOTE]
|
|
21
21
|
> ペアリング用ブロックは起動時固定のフィーチャーフラグの内側にあり、既定は無効です。
|
|
@@ -62,9 +62,9 @@ Offerは統合側端末がループ表示し、カメラ側端末は順不同で
|
|
|
62
62
|
|
|
63
63
|
ペアリング用メッセージは`twqr/2`形式のテキストです。protocol、JSONのheader、接続情報を1行ずつ並べます。headerには、session、送信側と受信側のpeer名、メッセージ種別、message ID、reply-to、接続情報の長さとSHA-256ハッシュを含みます。
|
|
64
64
|
|
|
65
|
-
メッセージは1つの連結QRコードとして運びます。作成には[`@kubohiroya/qrcode-structured-append`](https://github.com/kubohiroya/qrcode-structured-append)を使います。各QRは規格のheaderに自分の位置、枚数、連結QRのparityを持つため、本拡張独自の包みなしに、読み手はどのQRを読んだかが分かります。header
|
|
65
|
+
メッセージは1つの連結QRコードとして運びます。作成には[`@kubohiroya/qrcode-structured-append`](https://github.com/kubohiroya/qrcode-structured-append)を使います。各QRは規格のheaderに自分の位置、枚数、連結QRのparityを持つため、本拡張独自の包みなしに、読み手はどのQRを読んだかが分かります。headerの行が先頭にあるため、全部そろう前に、先頭のQRで誰宛てのメッセージかが分かります。既定の設定では1枚目だけで、QRが小さいときや誤り訂正が高いときは先頭の数枚で分かります。
|
|
66
66
|
|
|
67
|
-
読取りは連結QR単位です。最初に読んだQRが収集する連結QRを決め、それ以外の連結QRのQRは`foreign`として報告して無視します。ただし収集中の連結QRの
|
|
67
|
+
読取りは連結QR単位です。最初に読んだQRが収集する連結QRを決め、それ以外の連結QRのQRは`foreign`として報告して無視します。ただし収集中の連結QRのheaderをまだ読めていない間は、別の連結QRのQRも脇で集め、そのheaderからこの交換のものと分かれば入れ替えます。古い投影のQRが1枚写り込んでも、正しいOfferの収集を妨げないためです。別の交換のものや破損していたと分かった連結QRは破棄し、表示が続いているQRから集め直します。
|
|
68
68
|
|
|
69
69
|
parityは連結QRをまとめるだけのもので、256回に1回は一致してしまいます。光学搬送での破損や混在を検出するのはheader内のハッシュです。これは認証ではありません。QRを撮影できる人は誰でも同じ値を計算できます。投影された接続情報は、その投影を見られる全員に見えているものとして扱ってください。
|
|
70
70
|
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A TurboWarp extension for exchanging WebRTC connection information through QR co
|
|
|
15
15
|
- Tracks which offer an answer replies to, so two camera machines never receive each other's connection.
|
|
16
16
|
- Reports transport progress and connection state as separate states, plus errors, cancellation, retry and deadlines.
|
|
17
17
|
|
|
18
|
-
Version 0.2.0 carries messages as Structured Append sequences, caps Offer codes at QR version 15 and Answer codes at 20, and reports every pairing code read. It needs `turbowarp-jsqr` 0.4.0 or later. 0.1.0
|
|
18
|
+
Version 0.2.0 carries messages as Structured Append sequences, caps Offer codes at QR version 15 and Answer codes at 20, and reports every pairing code read. It needs `turbowarp-jsqr` 0.4.0 or later. 0.2.1 fixes a camera scan that starved the page while a code stayed in view. 0.2.2 reads a header that runs past the first code, takes reads one at a time so a code is handed over once, and keeps a sequence when a malformed code is read.
|
|
19
19
|
|
|
20
20
|
> [!NOTE]
|
|
21
21
|
> The pairing blocks are behind a startup feature flag that is off by default. See
|
|
@@ -62,9 +62,9 @@ A pairing session cannot be handed from one app to another. Opening a different
|
|
|
62
62
|
|
|
63
63
|
A pairing message is text in the `twqr/2` format: the protocol, a JSON header, and the pairing code, one per line. The header names the session, the sender and target peers, the message kind, the message id, what it replies to, the code's length and its SHA-256 hash.
|
|
64
64
|
|
|
65
|
-
The message is carried as one Structured Append sequence, made by [`@kubohiroya/qrcode-structured-append`](https://github.com/kubohiroya/qrcode-structured-append). Each code holds its position, the count and the sequence's parity in the standard header, so a reader knows which code it has without any wrapper of this extension's. The header line comes first, so the
|
|
65
|
+
The message is carried as one Structured Append sequence, made by [`@kubohiroya/qrcode-structured-append`](https://github.com/kubohiroya/qrcode-structured-append). Each code holds its position, the count and the sequence's parity in the standard header, so a reader knows which code it has without any wrapper of this extension's. The header line comes first, so the leading codes of a sequence tell a reader whose message it is before the rest arrive: the first code alone at the default settings, the first few when codes are small or the error correction is high.
|
|
66
66
|
|
|
67
|
-
Reading goes by sequence. The first code read decides the sequence being collected; codes of any other sequence are reported as `foreign` and ignored. Until
|
|
67
|
+
Reading goes by sequence. The first code read decides the sequence being collected; codes of any other sequence are reported as `foreign` and ignored. Until that sequence's header has been read, codes of another sequence are collected on the side, and if their header shows they belong to this exchange they replace it, so a stray code of an old projection cannot block the right one. A sequence that turns out to be another exchange's, or damaged, is dropped and collected again from the codes still being shown.
|
|
68
68
|
|
|
69
69
|
The parity only groups codes and collides one time in 256, so the hash in the header is what detects optical damage and mixed sequences. It is not authentication: anyone who can photograph the codes can recompute it. Treat a projected pairing code as visible to everyone who can see the projection.
|
|
70
70
|
|
|
@@ -116,7 +116,7 @@ pnpm run dev
|
|
|
116
116
|
|
|
117
117
|
Build output: `dist/webrtc-qrcode-pairing.js` and `dist/extension-manifest.json`. Load the JavaScript file as a custom TurboWarp extension. The manifest always records every declared block, including blocks the feature flag hides at runtime: it is the build-time contract, not the runtime state.
|
|
118
118
|
|
|
119
|
-
Package name and version: `@kubohiroya/turbowarp-webrtc-qrcode-pairing@0.2.
|
|
119
|
+
Package name and version: `@kubohiroya/turbowarp-webrtc-qrcode-pairing@0.2.2`.
|
|
120
120
|
|
|
121
121
|
## Block reference
|
|
122
122
|
|
|
@@ -3021,30 +3021,43 @@
|
|
|
3021
3021
|
if (await sha256Base64Url(message.payload) !== message.header.messageHash) throw new QrPairingError("hash-mismatch", "The pairing code failed its hash check.");
|
|
3022
3022
|
return message;
|
|
3023
3023
|
}
|
|
3024
|
-
/** The header, if the given first code of a sequence holds all of it. */
|
|
3025
|
-
function headerOfFirstCode(read) {
|
|
3026
|
-
return parseHeader(latin1(read.bytes));
|
|
3027
|
-
}
|
|
3028
3024
|
/**
|
|
3029
3025
|
* Collects the codes of one pairing message, in any order.
|
|
3030
3026
|
*
|
|
3031
|
-
* The first code read decides the sequence.
|
|
3032
|
-
*
|
|
3033
|
-
*
|
|
3034
|
-
*
|
|
3027
|
+
* The first code read decides the sequence. The header comes first in the
|
|
3028
|
+
* message, so the codes from the first onwards tell whose message it is before
|
|
3029
|
+
* all of them have arrived — in the first code alone at the default settings,
|
|
3030
|
+
* across the first few when codes are small or the error correction is high.
|
|
3035
3031
|
*/
|
|
3036
3032
|
var PairingAssembler = class {
|
|
3037
3033
|
constructor() {
|
|
3038
3034
|
this.inner = new StructuredAppendAssembler();
|
|
3035
|
+
this.shares = /* @__PURE__ */ new Map();
|
|
3039
3036
|
this.headerVerified = false;
|
|
3040
3037
|
}
|
|
3041
3038
|
/** Throws `conflicting-part` for a position that arrives twice with different content. */
|
|
3042
3039
|
add(read) {
|
|
3040
|
+
let outcome;
|
|
3043
3041
|
try {
|
|
3044
|
-
|
|
3042
|
+
outcome = this.inner.add(read);
|
|
3045
3043
|
} catch (error) {
|
|
3046
3044
|
throw translate(error);
|
|
3047
3045
|
}
|
|
3046
|
+
if (outcome.result === "accepted") this.shares.set(read.index, read.bytes);
|
|
3047
|
+
return outcome;
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* The header, once the codes from the first onwards hold all of it.
|
|
3051
|
+
* Undefined while they do not; throws as soon as they cannot be a pairing
|
|
3052
|
+
* message, or the header is malformed.
|
|
3053
|
+
*/
|
|
3054
|
+
header() {
|
|
3055
|
+
let prefix = "";
|
|
3056
|
+
for (let index = 0; this.shares.has(index); index += 1) prefix += latin1(this.shares.get(index) ?? /* @__PURE__ */ new Uint8Array());
|
|
3057
|
+
return prefix === "" ? void 0 : parseHeader(prefix);
|
|
3058
|
+
}
|
|
3059
|
+
isEmpty() {
|
|
3060
|
+
return this.inner.received() === 0;
|
|
3048
3061
|
}
|
|
3049
3062
|
receivedCount() {
|
|
3050
3063
|
return this.inner.received();
|
|
@@ -3072,6 +3085,7 @@
|
|
|
3072
3085
|
}
|
|
3073
3086
|
clear() {
|
|
3074
3087
|
this.inner.clear();
|
|
3088
|
+
this.shares.clear();
|
|
3075
3089
|
this.headerVerified = false;
|
|
3076
3090
|
}
|
|
3077
3091
|
};
|
|
@@ -3207,6 +3221,7 @@
|
|
|
3207
3221
|
this.runtime = runtime;
|
|
3208
3222
|
this.owner = owner;
|
|
3209
3223
|
this.leasedCameraId = "";
|
|
3224
|
+
this.nextReadAt = 0;
|
|
3210
3225
|
}
|
|
3211
3226
|
async scanOnce(options) {
|
|
3212
3227
|
if (options.signal.aborted) throw cancelled();
|
|
@@ -3244,6 +3259,7 @@
|
|
|
3244
3259
|
}
|
|
3245
3260
|
if (settled) return;
|
|
3246
3261
|
if (read !== null) {
|
|
3262
|
+
this.nextReadAt = Date.now() + interval;
|
|
3247
3263
|
cleanup();
|
|
3248
3264
|
resolve(read);
|
|
3249
3265
|
return;
|
|
@@ -3251,7 +3267,7 @@
|
|
|
3251
3267
|
timer = setTimeout(() => void tick(), interval);
|
|
3252
3268
|
};
|
|
3253
3269
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
3254
|
-
tick();
|
|
3270
|
+
timer = setTimeout(() => void tick(), Math.max(0, this.nextReadAt - Date.now()));
|
|
3255
3271
|
});
|
|
3256
3272
|
}
|
|
3257
3273
|
async release() {
|
|
@@ -3417,13 +3433,35 @@
|
|
|
3417
3433
|
*
|
|
3418
3434
|
* A lone code — one that is not part of a sequence — is read as a whole
|
|
3419
3435
|
* message, the way `ingestQrText` reads one.
|
|
3436
|
+
*
|
|
3437
|
+
* Reads of one session are taken one at a time, in the order they came, so
|
|
3438
|
+
* two scripts feeding codes at once cannot both complete the message.
|
|
3420
3439
|
*/
|
|
3421
3440
|
async ingestQrRead(sessionKey, read) {
|
|
3422
3441
|
this.requireEnabled();
|
|
3423
|
-
const session = this.
|
|
3442
|
+
const session = this.requireSession(sessionKey);
|
|
3443
|
+
return this.inTurn(session, () => this.takeRead(session, read));
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Accepts a whole pairing message as text, the way it would arrive through
|
|
3447
|
+
* something other than a camera, or in one QR code.
|
|
3448
|
+
*/
|
|
3449
|
+
async ingestQrText(sessionKey, text) {
|
|
3450
|
+
this.requireEnabled();
|
|
3451
|
+
const session = this.requireSession(sessionKey);
|
|
3452
|
+
return this.inTurn(session, () => this.takeText(session, text));
|
|
3453
|
+
}
|
|
3454
|
+
/** Runs `work` after every read of the session that came before it has finished. */
|
|
3455
|
+
inTurn(session, work) {
|
|
3456
|
+
const turn = session.readQueue.then(work);
|
|
3457
|
+
session.readQueue = turn.catch(() => void 0);
|
|
3458
|
+
return turn;
|
|
3459
|
+
}
|
|
3460
|
+
async takeRead(session, read) {
|
|
3461
|
+
this.requireOpen(session);
|
|
3424
3462
|
const position = read.structuredAppend;
|
|
3425
3463
|
if (position === null) {
|
|
3426
|
-
await this.
|
|
3464
|
+
await this.takeText(session, read.text);
|
|
3427
3465
|
return;
|
|
3428
3466
|
}
|
|
3429
3467
|
const symbol = {
|
|
@@ -3431,42 +3469,40 @@
|
|
|
3431
3469
|
bytes: read.bytes
|
|
3432
3470
|
};
|
|
3433
3471
|
const part = `${symbol.index + 1} / ${symbol.count}`;
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
outcome = assembler.add(symbol);
|
|
3472
|
+
if (session.delivered) {
|
|
3473
|
+
let repeat = false;
|
|
3474
|
+
try {
|
|
3475
|
+
repeat = session.assembler.add(symbol).result === "duplicate";
|
|
3476
|
+
} catch (error) {
|
|
3477
|
+
if (!isReported(error)) throw error;
|
|
3441
3478
|
}
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
this.noteRead(session, "foreign", codeOf(error));
|
|
3445
|
-
throw error;
|
|
3479
|
+
this.noteRead(session, repeat ? "duplicate" : "foreign", repeat ? part : "message-mismatch");
|
|
3480
|
+
throw alreadyAccepted();
|
|
3446
3481
|
}
|
|
3482
|
+
let outcome = this.addTo(session, session.assembler, symbol);
|
|
3483
|
+
if (outcome.result === "foreign" && !session.assembler.headerVerified) outcome = this.offerToCandidate(session, symbol) ?? outcome;
|
|
3447
3484
|
if (outcome.result === "foreign") {
|
|
3448
3485
|
this.noteRead(session, "foreign", "message-mismatch");
|
|
3449
3486
|
throw new QrPairingError("message-mismatch", "QR code belongs to another sequence than the one being collected.");
|
|
3450
3487
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
}
|
|
3455
|
-
if (outcome.result === "accepted" && symbol.index === 0) try {
|
|
3456
|
-
const header = headerOfFirstCode(symbol);
|
|
3488
|
+
const assembler = session.assembler;
|
|
3489
|
+
if (outcome.result === "accepted" && !assembler.headerVerified) try {
|
|
3490
|
+
const header = assembler.header();
|
|
3457
3491
|
if (header) {
|
|
3458
3492
|
this.verifyHeader(session, header);
|
|
3459
3493
|
assembler.headerVerified = true;
|
|
3460
3494
|
}
|
|
3461
3495
|
} catch (error) {
|
|
3462
3496
|
assembler.clear();
|
|
3463
|
-
if (
|
|
3497
|
+
if (!isReported(error)) throw error;
|
|
3464
3498
|
this.noteRead(session, "foreign", codeOf(error));
|
|
3465
3499
|
throw error;
|
|
3466
3500
|
}
|
|
3467
|
-
this.noteRead(session, outcome.result, part);
|
|
3468
3501
|
session.phase = session.role === "hub" ? "awaiting-answer" : "receiving";
|
|
3469
|
-
if (!assembler.isComplete())
|
|
3502
|
+
if (!assembler.isComplete()) {
|
|
3503
|
+
this.noteRead(session, outcome.result, part);
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3470
3506
|
const epoch = session.epoch;
|
|
3471
3507
|
let message;
|
|
3472
3508
|
try {
|
|
@@ -3480,15 +3516,11 @@
|
|
|
3480
3516
|
throw error;
|
|
3481
3517
|
}
|
|
3482
3518
|
assembler.headerVerified = true;
|
|
3519
|
+
this.noteRead(session, "accepted", part);
|
|
3483
3520
|
await this.deliver(session, message, epoch);
|
|
3484
3521
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
* something other than a camera, or in one QR code.
|
|
3488
|
-
*/
|
|
3489
|
-
async ingestQrText(sessionKey, text) {
|
|
3490
|
-
this.requireEnabled();
|
|
3491
|
-
const session = this.requireReceiving(sessionKey);
|
|
3522
|
+
async takeText(session, text) {
|
|
3523
|
+
this.requireOpen(session);
|
|
3492
3524
|
const epoch = session.epoch;
|
|
3493
3525
|
const message = await readPairingMessage(text);
|
|
3494
3526
|
if (this.isStale(session, epoch)) return;
|
|
@@ -3506,22 +3538,48 @@
|
|
|
3506
3538
|
await this.deliver(session, message, epoch);
|
|
3507
3539
|
}
|
|
3508
3540
|
/**
|
|
3509
|
-
*
|
|
3541
|
+
* Adds a code to a sequence. A position read twice with different content
|
|
3542
|
+
* means this sequence is damaged, so it is dropped and collected again from
|
|
3543
|
+
* the codes still being shown. Anything else wrong with the code leaves the
|
|
3544
|
+
* sequence as it was.
|
|
3545
|
+
*/
|
|
3546
|
+
addTo(session, assembler, symbol) {
|
|
3547
|
+
try {
|
|
3548
|
+
return assembler.add(symbol);
|
|
3549
|
+
} catch (error) {
|
|
3550
|
+
if (codeOf(error) !== "conflicting-part") throw error;
|
|
3551
|
+
assembler.clear();
|
|
3552
|
+
this.noteRead(session, "foreign", "conflicting-part");
|
|
3553
|
+
throw error;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Collects a code of another sequence on the side, while the sequence held
|
|
3558
|
+
* has not shown whose it is. If the other sequence's header shows it is this
|
|
3559
|
+
* exchange's, it takes the place of the one held.
|
|
3510
3560
|
*
|
|
3511
|
-
*
|
|
3512
|
-
*
|
|
3513
|
-
*
|
|
3514
|
-
* the rest of that old sequence forever.
|
|
3561
|
+
* A camera that first catches a stray code of an old projection would
|
|
3562
|
+
* otherwise wait for the rest of that old sequence forever. Returns the
|
|
3563
|
+
* outcome in the sequence that took over, or undefined when nothing changed.
|
|
3515
3564
|
*/
|
|
3516
|
-
|
|
3517
|
-
|
|
3565
|
+
offerToCandidate(session, symbol) {
|
|
3566
|
+
const candidate = session.candidate;
|
|
3518
3567
|
try {
|
|
3519
|
-
|
|
3520
|
-
if (
|
|
3568
|
+
let outcome = candidate.add(symbol);
|
|
3569
|
+
if (outcome.result === "foreign") {
|
|
3570
|
+
candidate.clear();
|
|
3571
|
+
outcome = candidate.add(symbol);
|
|
3572
|
+
}
|
|
3573
|
+
const header = candidate.header();
|
|
3574
|
+
if (!header) return void 0;
|
|
3521
3575
|
this.verifyHeader(session, header);
|
|
3522
|
-
|
|
3576
|
+
candidate.headerVerified = true;
|
|
3577
|
+
session.assembler = candidate;
|
|
3578
|
+
session.candidate = new PairingAssembler();
|
|
3579
|
+
return outcome;
|
|
3523
3580
|
} catch {
|
|
3524
|
-
|
|
3581
|
+
candidate.clear();
|
|
3582
|
+
return;
|
|
3525
3583
|
}
|
|
3526
3584
|
}
|
|
3527
3585
|
/** Hands a checked pairing code to WebRTC, once per exchange. */
|
|
@@ -3533,10 +3591,8 @@
|
|
|
3533
3591
|
if (session.role === "hub") await this.acceptAnswer(session, message.payload, epoch);
|
|
3534
3592
|
else await this.acceptOfferAndPrepareAnswer(session, message.payload, epoch);
|
|
3535
3593
|
}
|
|
3536
|
-
|
|
3537
|
-
const session = this.requireSession(sessionKey);
|
|
3594
|
+
requireOpen(session) {
|
|
3538
3595
|
if (isTerminalPhase(session.phase)) throw new QrPairingError(session.phase === "connected" ? "already-accepted" : "stale-exchange", `Pairing session ${session.sessionKey} is no longer receiving codes.`);
|
|
3539
|
-
return session;
|
|
3540
3596
|
}
|
|
3541
3597
|
/** Records what a pairing code turned out to be, for the application to show. */
|
|
3542
3598
|
noteRead(session, result, detail) {
|
|
@@ -3859,6 +3915,8 @@
|
|
|
3859
3915
|
outgoingSvgs: [],
|
|
3860
3916
|
outgoingCurrentIndex: -1,
|
|
3861
3917
|
assembler: new PairingAssembler(),
|
|
3918
|
+
candidate: new PairingAssembler(),
|
|
3919
|
+
readQueue: Promise.resolve(),
|
|
3862
3920
|
delivered: false,
|
|
3863
3921
|
peerCreated: false,
|
|
3864
3922
|
timeoutMilliseconds: 6e5,
|
|
@@ -3885,6 +3943,7 @@
|
|
|
3885
3943
|
session.outgoingSvgs = [];
|
|
3886
3944
|
session.outgoingCurrentIndex = -1;
|
|
3887
3945
|
session.assembler = new PairingAssembler();
|
|
3946
|
+
session.candidate = new PairingAssembler();
|
|
3888
3947
|
session.delivered = false;
|
|
3889
3948
|
session.peerCreated = false;
|
|
3890
3949
|
session.startedAtMonotonic = this.clock.nowMilliseconds();
|
|
@@ -3944,6 +4003,7 @@
|
|
|
3944
4003
|
this.releaseDisplay(session);
|
|
3945
4004
|
if (!connected) {
|
|
3946
4005
|
session.assembler.clear();
|
|
4006
|
+
session.candidate.clear();
|
|
3947
4007
|
if (session.peerCreated) {
|
|
3948
4008
|
try {
|
|
3949
4009
|
this.webrtc().closePeer(session.remotePeerId);
|
|
@@ -4027,6 +4087,10 @@
|
|
|
4027
4087
|
function alreadyAccepted() {
|
|
4028
4088
|
return new QrPairingError("already-accepted", "The pairing code for this exchange has already been accepted.");
|
|
4029
4089
|
}
|
|
4090
|
+
/** Whether a failure is about a pairing code, rather than about text that is not one at all. */
|
|
4091
|
+
function isReported(error) {
|
|
4092
|
+
return !(error instanceof QrPairingError && unreportedCodes.has(error.code));
|
|
4093
|
+
}
|
|
4030
4094
|
//#endregion
|
|
4031
4095
|
//#region src/extension.ts
|
|
4032
4096
|
var blockDefinitions = block_definitions_default.blocks;
|
|
@@ -150,12 +150,13 @@ say (join (join (current pairing QR part of [pairing-1]) " / ") (pairing QR part
|
|
|
150
150
|
害はありません。別の連結QRのQRは報告して無視し、交換は終わらせません。ポスターなど、ペアリング用
|
|
151
151
|
ではないQRは何も言わずに読み飛ばします。
|
|
152
152
|
|
|
153
|
-
最初に読んだQRで、どの連結QR
|
|
154
|
-
|
|
153
|
+
最初に読んだQRで、どの連結QRを集めるかが決まります。headerはメッセージの先頭にあるので、先頭のQRを
|
|
154
|
+
読めば誰宛てのメッセージかが分かります。既定の設定では1枚目だけで、QRが小さいときや誤り訂正が高いときは
|
|
155
|
+
先頭の数枚で分かります。
|
|
155
156
|
|
|
156
157
|
- 別の交換、別の相手、逆向きのものなら、その連結QRを破棄し、理由を付けて`foreign`と報告します。
|
|
157
|
-
- 集めている連結QRの
|
|
158
|
-
|
|
158
|
+
- 集めている連結QRのheaderをまだ読めていない間は、別の連結QRのQRも脇で集め、そのheaderからこの交換の
|
|
159
|
+
ものと分かれば、そちらに入れ替えます。古い投影のQRが1枚写り込んでも、正しいOfferの収集を妨げません。
|
|
159
160
|
- 全QRが揃うと、メッセージをハッシュで検査します。壊れていた連結QRは破棄して`foreign`
|
|
160
161
|
(`hash-mismatch`)と報告し、表示が続いているQRから集め直します。
|
|
161
162
|
|
|
@@ -156,14 +156,15 @@ Reading the same code repeatedly is normal and costs nothing. A code of another
|
|
|
156
156
|
and ignored without ending the exchange. QR codes that are not pairing codes at all, such as a
|
|
157
157
|
poster, are skipped without a word.
|
|
158
158
|
|
|
159
|
-
The first code read decides which sequence the session collects. The
|
|
160
|
-
|
|
159
|
+
The first code read decides which sequence the session collects. The header comes first in the
|
|
160
|
+
message, so once the leading codes are read the session knows whose message it is — the first code
|
|
161
|
+
alone at the default settings, the first few when codes are small or the error correction is high:
|
|
161
162
|
|
|
162
163
|
- if it belongs to another exchange, another pair of peers or the other direction, the sequence is
|
|
163
164
|
dropped and reported as `foreign` with the reason;
|
|
164
|
-
- while the
|
|
165
|
-
|
|
166
|
-
therefore cannot block the right one;
|
|
165
|
+
- while the header of the sequence being collected has not been read yet, codes of another sequence
|
|
166
|
+
are collected on the side, and a sequence whose header shows it belongs to this exchange takes its
|
|
167
|
+
place. A stray code of an old projection therefore cannot block the right one;
|
|
167
168
|
- when every code has arrived, the message is checked against its hash. A damaged sequence is
|
|
168
169
|
dropped, reported as `foreign` with `hash-mismatch`, and collected again from the codes still being
|
|
169
170
|
cycled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubohiroya/turbowarp-webrtc-qrcode-pairing",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "TurboWarp extension that pairs WebRTC peers by carrying offers and answers as QR codes.",
|
|
5
5
|
"author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -5,7 +5,6 @@ import {QrPairingError, type PairingErrorCode} from '../errors.js';
|
|
|
5
5
|
import type {AddOutcome, StructuredAppendRead} from '@kubohiroya/qrcode-structured-append';
|
|
6
6
|
import {
|
|
7
7
|
createPairingCodes,
|
|
8
|
-
headerOfFirstCode,
|
|
9
8
|
PairingAssembler,
|
|
10
9
|
readPairingMessage,
|
|
11
10
|
type CreateCodesOptions,
|
|
@@ -186,32 +185,58 @@ export class PairingController {
|
|
|
186
185
|
*
|
|
187
186
|
* A lone code — one that is not part of a sequence — is read as a whole
|
|
188
187
|
* message, the way `ingestQrText` reads one.
|
|
188
|
+
*
|
|
189
|
+
* Reads of one session are taken one at a time, in the order they came, so
|
|
190
|
+
* two scripts feeding codes at once cannot both complete the message.
|
|
189
191
|
*/
|
|
190
192
|
public async ingestQrRead(sessionKey: string, read: QrRead): Promise<void> {
|
|
191
193
|
this.requireEnabled();
|
|
192
|
-
const session = this.
|
|
194
|
+
const session = this.requireSession(sessionKey);
|
|
195
|
+
return this.inTurn(session, () => this.takeRead(session, read));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Accepts a whole pairing message as text, the way it would arrive through
|
|
200
|
+
* something other than a camera, or in one QR code.
|
|
201
|
+
*/
|
|
202
|
+
public async ingestQrText(sessionKey: string, text: string): Promise<void> {
|
|
203
|
+
this.requireEnabled();
|
|
204
|
+
const session = this.requireSession(sessionKey);
|
|
205
|
+
return this.inTurn(session, () => this.takeText(session, text));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Runs `work` after every read of the session that came before it has finished. */
|
|
209
|
+
private inTurn(session: PairingSessionState, work: () => Promise<void>): Promise<void> {
|
|
210
|
+
const turn = session.readQueue.then(work);
|
|
211
|
+
session.readQueue = turn.catch(() => undefined);
|
|
212
|
+
return turn;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async takeRead(session: PairingSessionState, read: QrRead): Promise<void> {
|
|
216
|
+
this.requireOpen(session);
|
|
193
217
|
const position = read.structuredAppend;
|
|
194
218
|
if (position === null) {
|
|
195
|
-
await this.
|
|
219
|
+
await this.takeText(session, read.text);
|
|
196
220
|
return;
|
|
197
221
|
}
|
|
198
222
|
const symbol = {...position, bytes: read.bytes};
|
|
199
223
|
const part = `${symbol.index + 1} / ${symbol.count}`;
|
|
200
|
-
const assembler = session.assembler;
|
|
201
224
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
assembler.
|
|
207
|
-
|
|
225
|
+
if (session.delivered) {
|
|
226
|
+
// The code has been handed over; a read can only be a repeat or something else.
|
|
227
|
+
let repeat = false;
|
|
228
|
+
try {
|
|
229
|
+
repeat = session.assembler.add(symbol).result === 'duplicate';
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (!isReported(error)) throw error;
|
|
208
232
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
this.noteRead(session, repeat ? 'duplicate' : 'foreign', repeat ? part : 'message-mismatch');
|
|
234
|
+
throw alreadyAccepted();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let outcome = this.addTo(session, session.assembler, symbol);
|
|
238
|
+
if (outcome.result === 'foreign' && !session.assembler.headerVerified) {
|
|
239
|
+
outcome = this.offerToCandidate(session, symbol) ?? outcome;
|
|
215
240
|
}
|
|
216
241
|
if (outcome.result === 'foreign') {
|
|
217
242
|
this.noteRead(session, 'foreign', 'message-mismatch');
|
|
@@ -220,28 +245,28 @@ export class PairingController {
|
|
|
220
245
|
'QR code belongs to another sequence than the one being collected.'
|
|
221
246
|
);
|
|
222
247
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
throw alreadyAccepted();
|
|
226
|
-
}
|
|
227
|
-
if (outcome.result === 'accepted' && symbol.index === 0) {
|
|
248
|
+
const assembler = session.assembler;
|
|
249
|
+
if (outcome.result === 'accepted' && !assembler.headerVerified) {
|
|
228
250
|
try {
|
|
229
|
-
const header =
|
|
251
|
+
const header = assembler.header();
|
|
230
252
|
if (header) {
|
|
231
253
|
this.verifyHeader(session, header);
|
|
232
254
|
assembler.headerVerified = true;
|
|
233
255
|
}
|
|
234
256
|
} catch (error) {
|
|
235
257
|
assembler.clear();
|
|
236
|
-
if (
|
|
258
|
+
if (!isReported(error)) throw error;
|
|
237
259
|
this.noteRead(session, 'foreign', codeOf(error));
|
|
238
260
|
throw error;
|
|
239
261
|
}
|
|
240
262
|
}
|
|
241
|
-
this.noteRead(session, outcome.result, part);
|
|
242
263
|
session.phase = session.role === 'hub' ? 'awaiting-answer' : 'receiving';
|
|
243
|
-
if (!assembler.isComplete())
|
|
264
|
+
if (!assembler.isComplete()) {
|
|
265
|
+
this.noteRead(session, outcome.result, part);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
244
268
|
|
|
269
|
+
// The last code is reported once, as what the whole message turned out to be.
|
|
245
270
|
const epoch = session.epoch;
|
|
246
271
|
let message: PairingMessage;
|
|
247
272
|
try {
|
|
@@ -255,16 +280,12 @@ export class PairingController {
|
|
|
255
280
|
throw error;
|
|
256
281
|
}
|
|
257
282
|
assembler.headerVerified = true;
|
|
283
|
+
this.noteRead(session, 'accepted', part);
|
|
258
284
|
await this.deliver(session, message, epoch);
|
|
259
285
|
}
|
|
260
286
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
* something other than a camera, or in one QR code.
|
|
264
|
-
*/
|
|
265
|
-
public async ingestQrText(sessionKey: string, text: string): Promise<void> {
|
|
266
|
-
this.requireEnabled();
|
|
267
|
-
const session = this.requireReceiving(sessionKey);
|
|
287
|
+
private async takeText(session: PairingSessionState, text: string): Promise<void> {
|
|
288
|
+
this.requireOpen(session);
|
|
268
289
|
// Text that is not a pairing message at all throws here and is not reported.
|
|
269
290
|
const epoch = session.epoch;
|
|
270
291
|
const message = await readPairingMessage(text);
|
|
@@ -284,25 +305,56 @@ export class PairingController {
|
|
|
284
305
|
}
|
|
285
306
|
|
|
286
307
|
/**
|
|
287
|
-
*
|
|
308
|
+
* Adds a code to a sequence. A position read twice with different content
|
|
309
|
+
* means this sequence is damaged, so it is dropped and collected again from
|
|
310
|
+
* the codes still being shown. Anything else wrong with the code leaves the
|
|
311
|
+
* sequence as it was.
|
|
312
|
+
*/
|
|
313
|
+
private addTo(
|
|
314
|
+
session: PairingSessionState,
|
|
315
|
+
assembler: PairingAssembler,
|
|
316
|
+
symbol: StructuredAppendRead
|
|
317
|
+
): AddOutcome {
|
|
318
|
+
try {
|
|
319
|
+
return assembler.add(symbol);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (codeOf(error) !== 'conflicting-part') throw error;
|
|
322
|
+
assembler.clear();
|
|
323
|
+
this.noteRead(session, 'foreign', 'conflicting-part');
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Collects a code of another sequence on the side, while the sequence held
|
|
330
|
+
* has not shown whose it is. If the other sequence's header shows it is this
|
|
331
|
+
* exchange's, it takes the place of the one held.
|
|
288
332
|
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* the rest of that old sequence forever.
|
|
333
|
+
* A camera that first catches a stray code of an old projection would
|
|
334
|
+
* otherwise wait for the rest of that old sequence forever. Returns the
|
|
335
|
+
* outcome in the sequence that took over, or undefined when nothing changed.
|
|
293
336
|
*/
|
|
294
|
-
private
|
|
337
|
+
private offerToCandidate(
|
|
295
338
|
session: PairingSessionState,
|
|
296
339
|
symbol: StructuredAppendRead
|
|
297
|
-
):
|
|
298
|
-
|
|
340
|
+
): AddOutcome | undefined {
|
|
341
|
+
const candidate = session.candidate;
|
|
299
342
|
try {
|
|
300
|
-
|
|
301
|
-
if (
|
|
343
|
+
let outcome = candidate.add(symbol);
|
|
344
|
+
if (outcome.result === 'foreign') {
|
|
345
|
+
candidate.clear();
|
|
346
|
+
outcome = candidate.add(symbol);
|
|
347
|
+
}
|
|
348
|
+
const header = candidate.header();
|
|
349
|
+
if (!header) return undefined;
|
|
302
350
|
this.verifyHeader(session, header);
|
|
303
|
-
|
|
351
|
+
candidate.headerVerified = true;
|
|
352
|
+
session.assembler = candidate;
|
|
353
|
+
session.candidate = new PairingAssembler();
|
|
354
|
+
return outcome;
|
|
304
355
|
} catch {
|
|
305
|
-
|
|
356
|
+
candidate.clear();
|
|
357
|
+
return undefined;
|
|
306
358
|
}
|
|
307
359
|
}
|
|
308
360
|
|
|
@@ -325,17 +377,16 @@ export class PairingController {
|
|
|
325
377
|
}
|
|
326
378
|
}
|
|
327
379
|
|
|
328
|
-
private
|
|
329
|
-
const session = this.requireSession(sessionKey);
|
|
380
|
+
private requireOpen(session: PairingSessionState): void {
|
|
330
381
|
if (isTerminalPhase(session.phase)) {
|
|
331
382
|
throw new QrPairingError(
|
|
332
383
|
session.phase === 'connected' ? 'already-accepted' : 'stale-exchange',
|
|
333
384
|
`Pairing session ${session.sessionKey} is no longer receiving codes.`
|
|
334
385
|
);
|
|
335
386
|
}
|
|
336
|
-
return session;
|
|
337
387
|
}
|
|
338
388
|
|
|
389
|
+
|
|
339
390
|
/** Records what a pairing code turned out to be, for the application to show. */
|
|
340
391
|
private noteRead(
|
|
341
392
|
session: PairingSessionState,
|
|
@@ -779,6 +830,8 @@ export class PairingController {
|
|
|
779
830
|
outgoingSvgs: [],
|
|
780
831
|
outgoingCurrentIndex: -1,
|
|
781
832
|
assembler: new PairingAssembler(),
|
|
833
|
+
candidate: new PairingAssembler(),
|
|
834
|
+
readQueue: Promise.resolve(),
|
|
782
835
|
delivered: false,
|
|
783
836
|
peerCreated: false,
|
|
784
837
|
timeoutMilliseconds: DEFAULT_TIMEOUT_SECONDS * 1000,
|
|
@@ -806,6 +859,7 @@ export class PairingController {
|
|
|
806
859
|
session.outgoingSvgs = [];
|
|
807
860
|
session.outgoingCurrentIndex = -1;
|
|
808
861
|
session.assembler = new PairingAssembler();
|
|
862
|
+
session.candidate = new PairingAssembler();
|
|
809
863
|
session.delivered = false;
|
|
810
864
|
session.peerCreated = false;
|
|
811
865
|
session.startedAtMonotonic = this.clock.nowMilliseconds();
|
|
@@ -879,6 +933,7 @@ export class PairingController {
|
|
|
879
933
|
this.releaseDisplay(session);
|
|
880
934
|
if (!connected) {
|
|
881
935
|
session.assembler.clear();
|
|
936
|
+
session.candidate.clear();
|
|
882
937
|
if (session.peerCreated) {
|
|
883
938
|
try {
|
|
884
939
|
this.webrtc().closePeer(session.remotePeerId);
|
|
@@ -1004,3 +1059,8 @@ function alreadyAccepted(): QrPairingError {
|
|
|
1004
1059
|
'The pairing code for this exchange has already been accepted.'
|
|
1005
1060
|
);
|
|
1006
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
/** Whether a failure is about a pairing code, rather than about text that is not one at all. */
|
|
1064
|
+
function isReported(error: unknown): boolean {
|
|
1065
|
+
return !(error instanceof QrPairingError && unreportedCodes.has(error.code));
|
|
1066
|
+
}
|
package/src/pairing/types.ts
CHANGED
|
@@ -103,6 +103,13 @@ export interface PairingSessionState {
|
|
|
103
103
|
/** Zero-based index into `outgoingSvgs`, or -1 when nothing is selected. */
|
|
104
104
|
outgoingCurrentIndex: number;
|
|
105
105
|
assembler: PairingAssembler;
|
|
106
|
+
/**
|
|
107
|
+
* Another sequence, collected on the side while the one in `assembler` has
|
|
108
|
+
* not shown whose it is, in case it turns out to be this exchange's.
|
|
109
|
+
*/
|
|
110
|
+
candidate: PairingAssembler;
|
|
111
|
+
/** Settles when every read taken so far has been handled; reads wait their turn on it. */
|
|
112
|
+
readQueue: Promise<void>;
|
|
106
113
|
/** True once the reassembled code has been handed to WebRTC. Prevents a second delivery. */
|
|
107
114
|
delivered: boolean;
|
|
108
115
|
/** True once a WebRTC peer exists for `remotePeerId`, so its state is worth polling. */
|
package/src/ports/qr-scan.ts
CHANGED
|
@@ -66,6 +66,15 @@ export interface QrScanPort {
|
|
|
66
66
|
export class CameraQrScanner implements QrScanPort {
|
|
67
67
|
private lease: CameraLease | undefined;
|
|
68
68
|
private leasedCameraId = '';
|
|
69
|
+
/**
|
|
70
|
+
* Wall-clock time before which the next frame is not read.
|
|
71
|
+
*
|
|
72
|
+
* A code left in front of the camera reads on every frame, and a sequence is read code after
|
|
73
|
+
* code, so the caller asks again at once. Without a pause between reads the loop would run on
|
|
74
|
+
* promise callbacks alone and never give the page its turn: no redraw, no Scratch thread, no
|
|
75
|
+
* timer. Every read therefore waits for the poll interval after the one before it.
|
|
76
|
+
*/
|
|
77
|
+
private nextReadAt = 0;
|
|
69
78
|
|
|
70
79
|
public constructor(
|
|
71
80
|
private readonly runtime: TurboWarpRuntime,
|
|
@@ -118,6 +127,7 @@ export class CameraQrScanner implements QrScanPort {
|
|
|
118
127
|
// Aborted while the frame was decoding: the abort handler already rejected.
|
|
119
128
|
if (settled) return;
|
|
120
129
|
if (read !== null) {
|
|
130
|
+
this.nextReadAt = Date.now() + interval;
|
|
121
131
|
cleanup();
|
|
122
132
|
resolve(read);
|
|
123
133
|
return;
|
|
@@ -125,7 +135,9 @@ export class CameraQrScanner implements QrScanPort {
|
|
|
125
135
|
timer = setTimeout(() => void tick(), interval);
|
|
126
136
|
};
|
|
127
137
|
options.signal.addEventListener('abort', onAbort, {once: true});
|
|
128
|
-
|
|
138
|
+
// Always through a timer, even when no wait is due, so a read never follows the previous
|
|
139
|
+
// one within the same turn of the event loop.
|
|
140
|
+
timer = setTimeout(() => void tick(), Math.max(0, this.nextReadAt - Date.now()));
|
|
129
141
|
});
|
|
130
142
|
}
|
|
131
143
|
|
package/src/qr/courier.ts
CHANGED
|
@@ -125,31 +125,48 @@ export async function readPairingMessage(text: string): Promise<PairingMessage>
|
|
|
125
125
|
return message;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** The header, if the given first code of a sequence holds all of it. */
|
|
129
|
-
export function headerOfFirstCode(read: StructuredAppendRead): PairingMessageHeader | undefined {
|
|
130
|
-
return parseHeader(latin1(read.bytes));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
128
|
/**
|
|
134
129
|
* Collects the codes of one pairing message, in any order.
|
|
135
130
|
*
|
|
136
|
-
* The first code read decides the sequence.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
131
|
+
* The first code read decides the sequence. The header comes first in the
|
|
132
|
+
* message, so the codes from the first onwards tell whose message it is before
|
|
133
|
+
* all of them have arrived — in the first code alone at the default settings,
|
|
134
|
+
* across the first few when codes are small or the error correction is high.
|
|
140
135
|
*/
|
|
141
136
|
export class PairingAssembler {
|
|
142
137
|
private readonly inner = new StructuredAppendAssembler();
|
|
143
|
-
/**
|
|
138
|
+
/** Each position's share of the message, as it arrived. */
|
|
139
|
+
private readonly shares = new Map<number, Uint8Array>();
|
|
140
|
+
/** True once the header showed this sequence belongs to the exchange. */
|
|
144
141
|
public headerVerified = false;
|
|
145
142
|
|
|
146
143
|
/** Throws `conflicting-part` for a position that arrives twice with different content. */
|
|
147
144
|
public add(read: StructuredAppendRead): AddOutcome {
|
|
145
|
+
let outcome: AddOutcome;
|
|
148
146
|
try {
|
|
149
|
-
|
|
147
|
+
outcome = this.inner.add(read);
|
|
150
148
|
} catch (error) {
|
|
151
149
|
throw translate(error);
|
|
152
150
|
}
|
|
151
|
+
if (outcome.result === 'accepted') this.shares.set(read.index, read.bytes);
|
|
152
|
+
return outcome;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The header, once the codes from the first onwards hold all of it.
|
|
157
|
+
* Undefined while they do not; throws as soon as they cannot be a pairing
|
|
158
|
+
* message, or the header is malformed.
|
|
159
|
+
*/
|
|
160
|
+
public header(): PairingMessageHeader | undefined {
|
|
161
|
+
let prefix = '';
|
|
162
|
+
for (let index = 0; this.shares.has(index); index += 1) {
|
|
163
|
+
prefix += latin1(this.shares.get(index) ?? new Uint8Array());
|
|
164
|
+
}
|
|
165
|
+
return prefix === '' ? undefined : parseHeader(prefix);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public isEmpty(): boolean {
|
|
169
|
+
return this.inner.received() === 0;
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
public receivedCount(): number {
|
|
@@ -183,6 +200,7 @@ export class PairingAssembler {
|
|
|
183
200
|
|
|
184
201
|
public clear(): void {
|
|
185
202
|
this.inner.clear();
|
|
203
|
+
this.shares.clear();
|
|
186
204
|
this.headerVerified = false;
|
|
187
205
|
}
|
|
188
206
|
}
|