@brass-build/client 0.4.0 → 0.5.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/AGENTS.md +142 -41
- package/CHANGELOG.md +93 -4
- package/README.md +666 -179
- package/dist/acting-organization.d.ts.map +1 -1
- package/dist/acting-organization.js +6 -11
- package/dist/acting-organization.js.map +1 -1
- package/dist/api-fetch.d.ts +8 -0
- package/dist/api-fetch.d.ts.map +1 -1
- package/dist/api-fetch.js +35 -8
- package/dist/api-fetch.js.map +1 -1
- package/dist/batched-get.d.ts +11 -0
- package/dist/batched-get.d.ts.map +1 -0
- package/dist/batched-get.js +92 -0
- package/dist/batched-get.js.map +1 -0
- package/dist/call-service.d.ts +4 -0
- package/dist/call-service.d.ts.map +1 -1
- package/dist/call-service.js +66 -3
- package/dist/call-service.js.map +1 -1
- package/dist/client.d.ts +51 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +982 -124
- package/dist/client.js.map +1 -1
- package/dist/document-checkpoints.d.ts +25 -2
- package/dist/document-checkpoints.d.ts.map +1 -1
- package/dist/document-checkpoints.js +258 -61
- package/dist/document-checkpoints.js.map +1 -1
- package/dist/document-commands.d.ts.map +1 -1
- package/dist/document-commands.js +5 -3
- package/dist/document-commands.js.map +1 -1
- package/dist/document-contents.d.ts +10 -0
- package/dist/document-contents.d.ts.map +1 -1
- package/dist/document-contents.js +23 -1
- package/dist/document-contents.js.map +1 -1
- package/dist/document-set-subscription.d.ts +56 -0
- package/dist/document-set-subscription.d.ts.map +1 -0
- package/dist/document-set-subscription.js +240 -0
- package/dist/document-set-subscription.js.map +1 -0
- package/dist/document-signals.d.ts +6 -0
- package/dist/document-signals.d.ts.map +1 -1
- package/dist/document-signals.js +31 -1
- package/dist/document-signals.js.map +1 -1
- package/dist/document-streams.d.ts +36 -2
- package/dist/document-streams.d.ts.map +1 -1
- package/dist/document-streams.js +69 -18
- package/dist/document-streams.js.map +1 -1
- package/dist/document-thumbnail-capture.d.ts +12 -2
- package/dist/document-thumbnail-capture.d.ts.map +1 -1
- package/dist/document-thumbnail-capture.js +103 -27
- package/dist/document-thumbnail-capture.js.map +1 -1
- package/dist/documents-namespace.d.ts +92 -10
- package/dist/documents-namespace.d.ts.map +1 -1
- package/dist/documents-namespace.js +285 -39
- package/dist/documents-namespace.js.map +1 -1
- package/dist/documents.d.ts +173 -8
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1778 -310
- package/dist/documents.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fetch-retry.d.ts +1 -0
- package/dist/fetch-retry.d.ts.map +1 -1
- package/dist/fetch-retry.js +2 -0
- package/dist/fetch-retry.js.map +1 -1
- package/dist/file-choice.d.ts +2 -0
- package/dist/file-choice.d.ts.map +1 -0
- package/dist/file-choice.js +27 -0
- package/dist/file-choice.js.map +1 -0
- package/dist/guest-link.d.ts +18 -1
- package/dist/guest-link.d.ts.map +1 -1
- package/dist/guest-link.js +329 -1
- package/dist/guest-link.js.map +1 -1
- package/dist/handle-types.d.ts +67 -5
- package/dist/handle-types.d.ts.map +1 -1
- package/dist/handles.d.ts +15 -19
- package/dist/handles.d.ts.map +1 -1
- package/dist/handles.js +231 -76
- package/dist/handles.js.map +1 -1
- package/dist/hosted-ui.d.ts +61 -0
- package/dist/hosted-ui.d.ts.map +1 -1
- package/dist/hosted-ui.js +94 -42
- package/dist/hosted-ui.js.map +1 -1
- package/dist/import-contract.d.ts +74 -0
- package/dist/import-contract.d.ts.map +1 -0
- package/dist/import-contract.js +163 -0
- package/dist/import-contract.js.map +1 -0
- package/dist/import-guest.d.ts +9 -0
- package/dist/import-guest.d.ts.map +1 -0
- package/dist/import-guest.js +155 -0
- package/dist/import-guest.js.map +1 -0
- package/dist/import-report.d.ts +17 -0
- package/dist/import-report.d.ts.map +1 -0
- package/dist/import-report.js +7 -0
- package/dist/import-report.js.map +1 -0
- package/dist/importer-wait.d.ts +2 -0
- package/dist/importer-wait.d.ts.map +1 -0
- package/dist/importer-wait.js +5 -0
- package/dist/importer-wait.js.map +1 -0
- package/dist/importer.d.ts +18 -4
- package/dist/importer.d.ts.map +1 -1
- package/dist/importer.js +322 -105
- package/dist/importer.js.map +1 -1
- package/dist/in-development.d.ts +2 -0
- package/dist/in-development.d.ts.map +1 -0
- package/dist/in-development.js +10 -0
- package/dist/in-development.js.map +1 -0
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/dist/init-keys.d.ts +1 -1
- package/dist/init-keys.d.ts.map +1 -1
- package/dist/init-keys.js +6 -1
- package/dist/init-keys.js.map +1 -1
- package/dist/input-documents.d.ts +67 -0
- package/dist/input-documents.d.ts.map +1 -0
- package/dist/input-documents.js +693 -0
- package/dist/input-documents.js.map +1 -0
- package/dist/inputs-status.d.ts +80 -0
- package/dist/inputs-status.d.ts.map +1 -0
- package/dist/inputs-status.js +460 -0
- package/dist/inputs-status.js.map +1 -0
- package/dist/inputs.d.ts +2 -0
- package/dist/inputs.d.ts.map +1 -0
- package/dist/inputs.js +793 -0
- package/dist/inputs.js.map +1 -0
- package/dist/internal.d.ts +6 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -1
- package/dist/internal.js.map +1 -1
- package/dist/join-content.d.ts +33 -0
- package/dist/join-content.d.ts.map +1 -0
- package/dist/join-content.js +114 -0
- package/dist/join-content.js.map +1 -0
- package/dist/join-stamp.d.ts +3 -0
- package/dist/join-stamp.d.ts.map +1 -0
- package/dist/join-stamp.js +12 -0
- package/dist/join-stamp.js.map +1 -0
- package/dist/manifest-drift.d.ts +9 -0
- package/dist/manifest-drift.d.ts.map +1 -0
- package/dist/manifest-drift.js +46 -0
- package/dist/manifest-drift.js.map +1 -0
- package/dist/open-document.d.ts +109 -2
- package/dist/open-document.d.ts.map +1 -1
- package/dist/open-document.js +1822 -811
- package/dist/open-document.js.map +1 -1
- package/dist/page-restore.d.ts +12 -0
- package/dist/page-restore.d.ts.map +1 -0
- package/dist/page-restore.js +23 -0
- package/dist/page-restore.js.map +1 -0
- package/dist/placement.d.ts +13 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +34 -0
- package/dist/placement.js.map +1 -0
- package/dist/presence.d.ts +1 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +16 -4
- package/dist/presence.js.map +1 -1
- package/dist/read-only-document.d.ts +87 -0
- package/dist/read-only-document.d.ts.map +1 -0
- package/dist/read-only-document.js +4 -0
- package/dist/read-only-document.js.map +1 -0
- package/dist/record-paging.d.ts +63 -6
- package/dist/record-paging.d.ts.map +1 -1
- package/dist/record-paging.js +122 -29
- package/dist/record-paging.js.map +1 -1
- package/dist/references.d.ts +40 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +135 -0
- package/dist/references.js.map +1 -0
- package/dist/request-lane.d.ts +5 -0
- package/dist/request-lane.d.ts.map +1 -0
- package/dist/request-lane.js +48 -0
- package/dist/request-lane.js.map +1 -0
- package/dist/sealed-arrow.d.ts +170 -0
- package/dist/sealed-arrow.d.ts.map +1 -0
- package/dist/sealed-arrow.js +769 -0
- package/dist/sealed-arrow.js.map +1 -0
- package/dist/sealed-cache.d.ts +21 -0
- package/dist/sealed-cache.d.ts.map +1 -0
- package/dist/sealed-cache.js +247 -0
- package/dist/sealed-cache.js.map +1 -0
- package/dist/sealed-encode.d.ts +87 -0
- package/dist/sealed-encode.d.ts.map +1 -0
- package/dist/sealed-encode.js +876 -0
- package/dist/sealed-encode.js.map +1 -0
- package/dist/sealed-handles.d.ts +45 -0
- package/dist/sealed-handles.d.ts.map +1 -0
- package/dist/sealed-handles.js +408 -0
- package/dist/sealed-handles.js.map +1 -0
- package/dist/sealed-store.d.ts +242 -0
- package/dist/sealed-store.d.ts.map +1 -0
- package/dist/sealed-store.js +1155 -0
- package/dist/sealed-store.js.map +1 -0
- package/dist/serializer.d.ts +6 -15
- package/dist/serializer.d.ts.map +1 -1
- package/dist/session-toolbar.d.ts +20 -5
- package/dist/session-toolbar.d.ts.map +1 -1
- package/dist/session-toolbar.js +419 -67
- package/dist/session-toolbar.js.map +1 -1
- package/dist/session.d.ts +38 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -79
- package/dist/session.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +74 -0
- package/dist/sha256.js.map +1 -0
- package/dist/stable-handles.d.ts +19 -0
- package/dist/stable-handles.d.ts.map +1 -0
- package/dist/stable-handles.js +193 -0
- package/dist/stable-handles.js.map +1 -0
- package/dist/staged-seal.d.ts +2 -0
- package/dist/staged-seal.d.ts.map +1 -0
- package/dist/staged-seal.js +76 -0
- package/dist/staged-seal.js.map +1 -0
- package/dist/subscriber-delivery.d.ts +8 -0
- package/dist/subscriber-delivery.d.ts.map +1 -0
- package/dist/subscriber-delivery.js +31 -0
- package/dist/subscriber-delivery.js.map +1 -0
- package/dist/tail-op-import.d.ts +10 -0
- package/dist/tail-op-import.d.ts.map +1 -0
- package/dist/tail-op-import.js +33 -0
- package/dist/tail-op-import.js.map +1 -0
- package/dist/topic-lifecycle.d.ts +6 -0
- package/dist/topic-lifecycle.d.ts.map +1 -1
- package/dist/topic-lifecycle.js +32 -2
- package/dist/topic-lifecycle.js.map +1 -1
- package/dist/trial-replace-dialog.d.ts +10 -0
- package/dist/trial-replace-dialog.d.ts.map +1 -0
- package/dist/trial-replace-dialog.js +153 -0
- package/dist/trial-replace-dialog.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/undo.d.ts +60 -5
- package/dist/undo.d.ts.map +1 -1
- package/dist/undo.js +344 -75
- package/dist/undo.js.map +1 -1
- package/dist/version-bar.d.ts +1 -0
- package/dist/version-bar.d.ts.map +1 -1
- package/dist/version-bar.js +2 -1
- package/dist/version-bar.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/ws-client.d.ts +21 -0
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +192 -8
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-frame-limit.d.ts +1 -0
- package/dist/ws-frame-limit.d.ts.map +1 -1
- package/dist/ws-frame-limit.js.map +1 -1
- package/docs/app-manifest.md +121 -17
- package/docs/custom-chrome.md +24 -16
- package/docs/hosted-ui-advanced.md +38 -6
- package/docs/imported-documents.md +104 -51
- package/docs/presence.md +16 -4
- package/docs/schemas.md +8 -5
- package/docs/services.md +6 -6
- package/docs/streams.md +139 -150
- package/docs/text-fields.md +26 -19
- package/package.json +5 -2
- package/src/acting-organization.ts +6 -12
- package/src/api-fetch.ts +53 -3
- package/src/auth-flow.ts +1 -1
- package/src/batched-get.ts +117 -0
- package/src/call-service.ts +78 -3
- package/src/client.ts +1092 -121
- package/src/document-checkpoints.ts +307 -65
- package/src/document-commands.ts +5 -5
- package/src/document-contents.ts +27 -1
- package/src/document-set-subscription.ts +274 -0
- package/src/document-signals.ts +47 -5
- package/src/document-streams.ts +85 -16
- package/src/document-thumbnail-capture.ts +118 -32
- package/src/documents-namespace.ts +539 -84
- package/src/documents.ts +2017 -348
- package/src/errors.ts +17 -6
- package/src/fetch-retry.ts +5 -0
- package/src/file-choice.ts +26 -0
- package/src/guest-link.ts +362 -1
- package/src/handle-types.ts +73 -47
- package/src/handles.ts +253 -83
- package/src/hosted-ui.ts +211 -50
- package/src/import-contract.ts +227 -0
- package/src/import-guest.ts +193 -0
- package/src/import-report.ts +24 -0
- package/src/importer-wait.ts +4 -0
- package/src/importer.ts +386 -131
- package/src/in-development.ts +10 -0
- package/src/index.ts +76 -16
- package/src/init-keys.ts +6 -1
- package/src/input-documents.ts +819 -0
- package/src/inputs-status.ts +598 -0
- package/src/inputs.ts +1090 -0
- package/src/internal.ts +7 -0
- package/src/join-content.ts +135 -0
- package/src/join-stamp.ts +12 -0
- package/src/manifest-drift.ts +54 -0
- package/src/open-document.ts +2346 -930
- package/src/page-restore.ts +38 -0
- package/src/placement.ts +54 -0
- package/src/presence.ts +21 -5
- package/src/read-only-document.ts +133 -0
- package/src/record-paging.ts +147 -25
- package/src/references.ts +207 -0
- package/src/request-lane.ts +59 -0
- package/src/sealed-arrow.ts +843 -0
- package/src/sealed-cache.ts +267 -0
- package/src/sealed-encode.ts +1015 -0
- package/src/sealed-handles.ts +486 -0
- package/src/sealed-store.ts +1330 -0
- package/src/serializer.ts +8 -20
- package/src/session-toolbar.ts +463 -72
- package/src/session.ts +740 -99
- package/src/sha256.ts +65 -0
- package/src/stable-handles.ts +235 -0
- package/src/staged-seal.ts +90 -0
- package/src/subscriber-delivery.ts +37 -0
- package/src/tail-op-import.ts +30 -0
- package/src/topic-lifecycle.ts +38 -2
- package/src/trial-replace-dialog.ts +163 -0
- package/src/types.ts +6 -6
- package/src/undo.ts +386 -73
- package/src/version-bar.ts +5 -1
- package/src/version.ts +1 -1
- package/src/ws-client.ts +208 -16
- package/src/ws-frame-limit.ts +1 -0
package/src/sha256.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SHA-256 over a string's UTF-8 bytes, synchronous and dependency-free, so
|
|
2
|
+
// the browser, the SDK and the Lambdas compute the same digest without
|
|
3
|
+
// `node:crypto` or an async `crypto.subtle` call on a path that must be
|
|
4
|
+
// synchronous (a status keyed by a reference set is computed inside a
|
|
5
|
+
// subscription callback).
|
|
6
|
+
|
|
7
|
+
const K = new Uint32Array([
|
|
8
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
9
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
10
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
11
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
12
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
13
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
14
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
15
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
|
|
19
|
+
|
|
20
|
+
/** Lower-case hex SHA-256 of `input`'s UTF-8 encoding. */
|
|
21
|
+
export function sha256Hex(input: string): string {
|
|
22
|
+
const bytes = new TextEncoder().encode(input);
|
|
23
|
+
const bitLen = bytes.length * 8;
|
|
24
|
+
const padded = new Uint8Array((((bytes.length + 9 + 63) >> 6) << 6));
|
|
25
|
+
padded.set(bytes);
|
|
26
|
+
padded[bytes.length] = 0x80;
|
|
27
|
+
const view = new DataView(padded.buffer);
|
|
28
|
+
// Messages here are far below 2^32 bits, so the high word stays zero.
|
|
29
|
+
view.setUint32(padded.length - 4, bitLen >>> 0);
|
|
30
|
+
view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
|
|
31
|
+
|
|
32
|
+
const h = new Uint32Array([
|
|
33
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
34
|
+
]);
|
|
35
|
+
const w = new Uint32Array(64);
|
|
36
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
37
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
|
|
38
|
+
for (let i = 16; i < 64; i++) {
|
|
39
|
+
const a = w[i - 15]!;
|
|
40
|
+
const b = w[i - 2]!;
|
|
41
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
|
|
42
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
|
|
43
|
+
w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
let a = h[0]!, b = h[1]!, c = h[2]!, d = h[3]!;
|
|
46
|
+
let e = h[4]!, f = h[5]!, g = h[6]!, hh = h[7]!;
|
|
47
|
+
for (let i = 0; i < 64; i++) {
|
|
48
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
49
|
+
const ch = (e & f) ^ (~e & g);
|
|
50
|
+
const t1 = (hh + s1 + ch + K[i]! + w[i]!) >>> 0;
|
|
51
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
52
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
53
|
+
const t2 = (s0 + maj) >>> 0;
|
|
54
|
+
hh = g; g = f; f = e; e = (d + t1) >>> 0;
|
|
55
|
+
d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
56
|
+
}
|
|
57
|
+
h[0] = (h[0]! + a) >>> 0; h[1] = (h[1]! + b) >>> 0;
|
|
58
|
+
h[2] = (h[2]! + c) >>> 0; h[3] = (h[3]! + d) >>> 0;
|
|
59
|
+
h[4] = (h[4]! + e) >>> 0; h[5] = (h[5]! + f) >>> 0;
|
|
60
|
+
h[6] = (h[6]! + g) >>> 0; h[7] = (h[7]! + hh) >>> 0;
|
|
61
|
+
}
|
|
62
|
+
let out = '';
|
|
63
|
+
for (const word of h) out += word.toString(16).padStart(8, '0');
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// The handles a Document hands an app.
|
|
2
|
+
//
|
|
3
|
+
// A document's storage can change under an open copy, from frozen content to
|
|
4
|
+
// a CRDT when it is edited and back when a frozen copy reloads onto a reseal.
|
|
5
|
+
// Each handle here passes every call to the handle the current storage
|
|
6
|
+
// provides for the same container, and owns its subscriptions, so a swap
|
|
7
|
+
// moves them in one place and the handles behind it never learn of it.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
HandleUnsubscribe,
|
|
11
|
+
ListHandle,
|
|
12
|
+
ListSubscriber,
|
|
13
|
+
MapHandle,
|
|
14
|
+
MapSubscriber,
|
|
15
|
+
OrderedMapHandle,
|
|
16
|
+
TextHandle,
|
|
17
|
+
} from './handle-types.js';
|
|
18
|
+
import { hideOwnFields } from './handles.js';
|
|
19
|
+
|
|
20
|
+
type Call = (...args: unknown[]) => unknown;
|
|
21
|
+
type Backing = Record<string, Call>;
|
|
22
|
+
type Kind = 'map' | 'list' | 'text';
|
|
23
|
+
|
|
24
|
+
interface Scope {
|
|
25
|
+
generation: number;
|
|
26
|
+
// Every handle holding a subscription, which a swap has to move.
|
|
27
|
+
readonly subscribed: Set<StableHandle>;
|
|
28
|
+
readonly report: (err: unknown) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Subscription {
|
|
32
|
+
cb: Call;
|
|
33
|
+
settled: boolean;
|
|
34
|
+
off: HandleUnsubscribe;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A map subscriber whose first fire also reports the keys it was handed
|
|
38
|
+
* before and the new container no longer holds, which is what a swap owes
|
|
39
|
+
* a subscriber that moves with it. */
|
|
40
|
+
function reportingLeftKeys<T>(cb: MapSubscriber<T>, before: readonly string[]): MapSubscriber<T> {
|
|
41
|
+
let first = true;
|
|
42
|
+
return (change, origin) => {
|
|
43
|
+
if (first) {
|
|
44
|
+
first = false;
|
|
45
|
+
const left = before.filter((key) => !change.set.has(key));
|
|
46
|
+
if (left.length > 0) change = { ...change, deleted: [...change.deleted, ...left] };
|
|
47
|
+
}
|
|
48
|
+
cb(change, origin);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The list counterpart: the first fire also removes the items handed before. */
|
|
53
|
+
function replacingHeldItems<T>(cb: ListSubscriber<T>, held: number): ListSubscriber<T> {
|
|
54
|
+
let first = true;
|
|
55
|
+
return (change) => {
|
|
56
|
+
if (first) {
|
|
57
|
+
first = false;
|
|
58
|
+
if (held > 0) {
|
|
59
|
+
const [head, ...rest] = change.splices;
|
|
60
|
+
change =
|
|
61
|
+
head !== undefined && head.at === 0
|
|
62
|
+
? { splices: [{ ...head, removed: head.removed + held }, ...rest] }
|
|
63
|
+
: { splices: [{ at: 0, removed: held, inserted: [] }, ...change.splices] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
cb(change);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class StableHandle {
|
|
71
|
+
private _backing: Backing;
|
|
72
|
+
private _at: number;
|
|
73
|
+
private readonly _subs = new Set<Subscription>();
|
|
74
|
+
// Keyed by the scoped handle the backing hands out, so whether two calls
|
|
75
|
+
// return the same one is the backing's decision.
|
|
76
|
+
private readonly _children = new WeakMap<object, StableHandle>();
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
private readonly _scope: Scope,
|
|
80
|
+
private readonly _kind: Kind,
|
|
81
|
+
private readonly _resolve: () => unknown,
|
|
82
|
+
backing: unknown = _resolve(),
|
|
83
|
+
) {
|
|
84
|
+
this._backing = backing as Backing;
|
|
85
|
+
this._at = _scope.generation;
|
|
86
|
+
hideOwnFields(this);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private get _h(): Backing {
|
|
90
|
+
if (this._at !== this._scope.generation) {
|
|
91
|
+
this._backing = this._resolve() as Backing;
|
|
92
|
+
this._at = this._scope.generation;
|
|
93
|
+
}
|
|
94
|
+
return this._backing;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_call(method: string, args: unknown[]): unknown {
|
|
98
|
+
return this._h[method]!(...args);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
map(key: string): StableHandle {
|
|
102
|
+
return this._child('map', key);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
list(key: string): StableHandle {
|
|
106
|
+
return this._child('list', key);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private _child(kind: Kind, key: string): StableHandle {
|
|
110
|
+
const backing = this._call(kind, [key]) as object;
|
|
111
|
+
let child = this._children.get(backing);
|
|
112
|
+
if (child === undefined) {
|
|
113
|
+
child = new StableHandle(this._scope, kind, () => this._call(kind, [key]), backing);
|
|
114
|
+
this._children.set(backing, child);
|
|
115
|
+
}
|
|
116
|
+
return child;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
subscribe(cb: Call): HandleUnsubscribe {
|
|
120
|
+
return this._add(cb, false);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onSettled(cb: Call): HandleUnsubscribe {
|
|
124
|
+
return this._add(cb, true);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _add(cb: Call, settled: boolean): HandleUnsubscribe {
|
|
128
|
+
const sub: Subscription = { cb, settled, off: () => {} };
|
|
129
|
+
sub.off = this._call(settled ? 'onSettled' : 'subscribe', [cb]) as HandleUnsubscribe;
|
|
130
|
+
this._subs.add(sub);
|
|
131
|
+
this._scope.subscribed.add(this);
|
|
132
|
+
return () => {
|
|
133
|
+
sub.off();
|
|
134
|
+
this._subs.delete(sub);
|
|
135
|
+
if (this._subs.size === 0) this._scope.subscribed.delete(this);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Take every subscription off the current handle, noting what its
|
|
140
|
+
// subscribers were handed; the returned call registers them with the handle
|
|
141
|
+
// the storage provides by then, as a whole-container delivery.
|
|
142
|
+
_detach(): () => void {
|
|
143
|
+
const held = this._kind === 'text' ? null : this._call(this._kind === 'map' ? 'keys' : 'length', []);
|
|
144
|
+
const moved = (cb: Call): Call =>
|
|
145
|
+
this._kind === 'map'
|
|
146
|
+
? (reportingLeftKeys(cb as MapSubscriber<unknown>, held as string[]) as Call)
|
|
147
|
+
: this._kind === 'list'
|
|
148
|
+
? (replacingHeldItems(cb as ListSubscriber<unknown>, held as number) as Call)
|
|
149
|
+
: cb;
|
|
150
|
+
for (const sub of this._subs) sub.off();
|
|
151
|
+
return () => {
|
|
152
|
+
// Subscribers first, so each `onSettled` fires after the delivery it
|
|
153
|
+
// reports.
|
|
154
|
+
for (const settled of [false, true]) {
|
|
155
|
+
for (const sub of this._subs) {
|
|
156
|
+
if (sub.settled !== settled) continue;
|
|
157
|
+
const cb = settled ? sub.cb : moved(sub.cb);
|
|
158
|
+
try {
|
|
159
|
+
sub.off = this._call(settled ? 'onSettled' : 'subscribe', [cb]) as HandleUnsubscribe;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
sub.off = () => {};
|
|
162
|
+
this._scope.report(err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type Passed = Exclude<
|
|
171
|
+
keyof MapHandle | keyof ListHandle | keyof OrderedMapHandle | keyof TextHandle,
|
|
172
|
+
'subscribe' | 'onSettled' | 'map' | 'list'
|
|
173
|
+
>;
|
|
174
|
+
|
|
175
|
+
// Every other call passes straight through. A record, so a method the handle
|
|
176
|
+
// interfaces gain does not compile until it is listed here.
|
|
177
|
+
const PASSED: Record<Passed | 'read' | '_pageIndexRebuilds', true> = {
|
|
178
|
+
get: true,
|
|
179
|
+
has: true,
|
|
180
|
+
keys: true,
|
|
181
|
+
entries: true,
|
|
182
|
+
values: true,
|
|
183
|
+
length: true,
|
|
184
|
+
read: true,
|
|
185
|
+
ordered: true,
|
|
186
|
+
mutate: true,
|
|
187
|
+
insert: true,
|
|
188
|
+
move: true,
|
|
189
|
+
update: true,
|
|
190
|
+
_pageIndexRebuilds: true,
|
|
191
|
+
};
|
|
192
|
+
for (const method of Object.keys(PASSED)) {
|
|
193
|
+
Object.defineProperty(StableHandle.prototype, method, {
|
|
194
|
+
value(this: StableHandle, ...args: unknown[]): unknown {
|
|
195
|
+
return this._call(method, args);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The handles one Document has handed out, one per root. */
|
|
201
|
+
export class StableHandles {
|
|
202
|
+
private _scope: Scope;
|
|
203
|
+
private readonly _roots = new Map<string, StableHandle>();
|
|
204
|
+
|
|
205
|
+
constructor(private readonly _report: (err: unknown) => void) {
|
|
206
|
+
this._scope = { generation: 0, subscribed: new Set(), report: _report };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The handle under `cacheKey`, made over what `resolve` returns. `resolve`
|
|
210
|
+
* is asked again after every `swap`. */
|
|
211
|
+
root<H>(cacheKey: string, kind: Kind, resolve: () => H): H {
|
|
212
|
+
let handle = this._roots.get(cacheKey);
|
|
213
|
+
if (handle === undefined) {
|
|
214
|
+
handle = new StableHandle(this._scope, kind, resolve);
|
|
215
|
+
this._roots.set(cacheKey, handle);
|
|
216
|
+
}
|
|
217
|
+
return handle as unknown as H;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Change the storage behind every handle: `change` runs while nothing is
|
|
221
|
+
* subscribed to the old storage, and every subscription then moves to the
|
|
222
|
+
* new one. */
|
|
223
|
+
swap(change: () => void): void {
|
|
224
|
+
const moves = Array.from(this._scope.subscribed, (handle) => handle._detach());
|
|
225
|
+
change();
|
|
226
|
+
this._scope.generation++;
|
|
227
|
+
for (const move of moves) move();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Forget every handle. One an app still holds stays on the storage it had. */
|
|
231
|
+
clear(): void {
|
|
232
|
+
this._roots.clear();
|
|
233
|
+
this._scope = { generation: 0, subscribed: new Set(), report: this._report };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Waiting on a staged upload's seal. The platform answers `202 { seal_id }`
|
|
2
|
+
// once the upload is queued and seals it afterwards; the caller resolves or
|
|
3
|
+
// rejects on the outcome, exactly as it did on the synchronous answer.
|
|
4
|
+
|
|
5
|
+
import { brassErrorFromBody } from './api-fetch.js';
|
|
6
|
+
import { BrassError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
/** @internal */
|
|
9
|
+
export type SealStatus =
|
|
10
|
+
| { state: 'pending' }
|
|
11
|
+
| { state: 'stored'; body: unknown }
|
|
12
|
+
| {
|
|
13
|
+
state: 'refused';
|
|
14
|
+
status: number;
|
|
15
|
+
error: string;
|
|
16
|
+
code?: string;
|
|
17
|
+
import_failed?: { message: string };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** @internal A seal the platform refused. `importFailed` is the failure it
|
|
21
|
+
* recorded on the document when the seal completed an import. */
|
|
22
|
+
export class SealRefusedError extends BrassError {
|
|
23
|
+
readonly importFailed?: string;
|
|
24
|
+
|
|
25
|
+
constructor(refused: Extract<SealStatus, { state: 'refused' }>) {
|
|
26
|
+
const base = brassErrorFromBody(refused.status, {
|
|
27
|
+
error: refused.error,
|
|
28
|
+
...(refused.code !== undefined ? { code: refused.code } : {}),
|
|
29
|
+
});
|
|
30
|
+
super(base.message, {
|
|
31
|
+
...(base.status !== undefined ? { status: base.status } : {}),
|
|
32
|
+
...(base.code !== undefined ? { code: base.code } : {}),
|
|
33
|
+
});
|
|
34
|
+
if (refused.import_failed !== undefined) {
|
|
35
|
+
this.importFailed = refused.import_failed.message;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The worker may take both of the queue's deliveries before the DLQ drain
|
|
41
|
+
// records a refusal, so the wait outlasts them.
|
|
42
|
+
const SEAL_WAIT_MS = 40 * 60 * 1000;
|
|
43
|
+
const SEAL_POLL_MS = [250, 500, 1000, 1000, 2000];
|
|
44
|
+
const MAX_FAILED_READS = 10;
|
|
45
|
+
|
|
46
|
+
function retryable(status: number | undefined): boolean {
|
|
47
|
+
return status === undefined || status === 429 || status >= 500;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sealIdOf(accepted: unknown): string | null {
|
|
51
|
+
if (typeof accepted !== 'object' || accepted === null) return null;
|
|
52
|
+
const id = (accepted as { seal_id?: unknown }).seal_id;
|
|
53
|
+
return typeof id === 'string' && id !== '' ? id : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @internal Resolve with the stored outcome's body once the seal `accepted`
|
|
57
|
+
* names has landed, or throw its refusal. A body naming no seal is a server
|
|
58
|
+
* that sealed synchronously, and is the answer itself. */
|
|
59
|
+
export async function awaitSeal<T>(
|
|
60
|
+
accepted: unknown,
|
|
61
|
+
read: (sealId: string) => Promise<SealStatus>,
|
|
62
|
+
): Promise<T> {
|
|
63
|
+
const sealId = sealIdOf(accepted);
|
|
64
|
+
if (sealId === null) return accepted as T;
|
|
65
|
+
const deadline = Date.now() + SEAL_WAIT_MS;
|
|
66
|
+
let failedReads = 0;
|
|
67
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
68
|
+
let status: SealStatus | null = null;
|
|
69
|
+
try {
|
|
70
|
+
status = await read(sealId);
|
|
71
|
+
failedReads = 0;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// A read that failed in transit says nothing about the seal, which goes
|
|
74
|
+
// on regardless; a refusal of the read itself, or a run of failures,
|
|
75
|
+
// ends the wait.
|
|
76
|
+
failedReads += 1;
|
|
77
|
+
if (err instanceof BrassError && !retryable(err.status)) throw err;
|
|
78
|
+
if (failedReads >= MAX_FAILED_READS) throw err;
|
|
79
|
+
}
|
|
80
|
+
if (status?.state === 'stored') return status.body as T;
|
|
81
|
+
if (status?.state === 'refused') throw new SealRefusedError(status);
|
|
82
|
+
if (Date.now() >= deadline) {
|
|
83
|
+
throw new BrassError('The upload is taking too long to store. Try again.', {
|
|
84
|
+
status: 504,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const delay = SEAL_POLL_MS[Math.min(attempt, SEAL_POLL_MS.length - 1)]!;
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// What every handle kind shares about handing data to an app's callback.
|
|
2
|
+
|
|
3
|
+
import type { Splice } from './handle-types.js';
|
|
4
|
+
|
|
5
|
+
// The natural apply is `rows.splice(at, removed, ...inserted)`, and a spread
|
|
6
|
+
// passes every item as a call argument, which overflows the engine's stack
|
|
7
|
+
// somewhere past tens of thousands of items. A large insert is therefore
|
|
8
|
+
// delivered as consecutive splices, which applied in order make the same edit.
|
|
9
|
+
const SPLICE_MAX_INSERTED = 10_000;
|
|
10
|
+
|
|
11
|
+
export function pushBoundedSplice<E>(out: Splice<E>[], splice: Splice<E>): void {
|
|
12
|
+
const { at, removed, inserted } = splice;
|
|
13
|
+
if (inserted.length <= SPLICE_MAX_INSERTED) {
|
|
14
|
+
out.push(splice);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (let i = 0; i < inserted.length; i += SPLICE_MAX_INSERTED) {
|
|
18
|
+
out.push({
|
|
19
|
+
at: at + i,
|
|
20
|
+
removed: i === 0 ? removed : 0,
|
|
21
|
+
inserted: inserted.slice(i, i + SPLICE_MAX_INSERTED),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The error `Brass.onError` receives when an app's own callback threw while
|
|
27
|
+
* being handed a document or a change to one. The app usually shows an empty
|
|
28
|
+
* or stale view after that, so the sentence is one it can show the reader;
|
|
29
|
+
* the throw rides as `cause`. */
|
|
30
|
+
export function appCallbackError(err: unknown): Error {
|
|
31
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
32
|
+
const report: Error & { cause?: unknown } = new Error(
|
|
33
|
+
`The app could not display a change: ${detail}`,
|
|
34
|
+
);
|
|
35
|
+
report.cause = err;
|
|
36
|
+
return report;
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { LoroDoc } from './wasm-runtime.js';
|
|
2
|
+
|
|
3
|
+
// An op whose dependencies are missing still imports here (as pending), so a
|
|
4
|
+
// refusal means the bytes are not an update anything could apply.
|
|
5
|
+
function importsIntoEmptyDoc(blob: Uint8Array): boolean {
|
|
6
|
+
const probe = new LoroDoc();
|
|
7
|
+
try {
|
|
8
|
+
probe.import(blob);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
} finally {
|
|
13
|
+
probe.free();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Import one op from a join's tail or a live delivery. Bytes that do not decode
|
|
19
|
+
* even into an empty document can never be applied by any peer, and compaction
|
|
20
|
+
* seals past them, so they are skipped: one such op in a tail must not fail
|
|
21
|
+
* every open until that seal. An op that fails only against `engine`'s state
|
|
22
|
+
* throws.
|
|
23
|
+
*/
|
|
24
|
+
export function importTailOp(engine: LoroDoc, blob: Uint8Array): void {
|
|
25
|
+
try {
|
|
26
|
+
engine.import(blob);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (importsIntoEmptyDoc(blob)) throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/topic-lifecycle.ts
CHANGED
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
// that: it describes the frame the server could not process, not the
|
|
22
22
|
// subscription it arrived on, so every live phase keeps its state and hands
|
|
23
23
|
// the error to the topic's callbacks, which hold the frame that drew it.
|
|
24
|
+
// While the subscribe is unacked the subscribe may be the frame that drew it,
|
|
25
|
+
// so the client also sends that again after a backoff (a duplicate subscribe
|
|
26
|
+
// is idempotent on the server).
|
|
27
|
+
//
|
|
28
|
+
// A `server_ended` event (the server evicted every subscriber on the topic
|
|
29
|
+
// and deleted their rows) returns any phase to `idle` and detaches the
|
|
30
|
+
// topic's callbacks, so whoever subscribes next sends a subscribe of its own
|
|
31
|
+
// rather than attaching to a subscription the server no longer holds.
|
|
24
32
|
//
|
|
25
33
|
// Invariant: every transition into `tornDown` emits `send_unsubscribe`,
|
|
26
34
|
// so no server-side subscription row can outlive the client-side state
|
|
@@ -49,10 +57,17 @@ export type TopicEvent =
|
|
|
49
57
|
| { type: 'socket_open' }
|
|
50
58
|
| { type: 'socket_closed' }
|
|
51
59
|
| { type: 'subscribed_ack' }
|
|
60
|
+
| { type: 'server_ended' }
|
|
61
|
+
// The server reports it holds no subscription row for an established topic
|
|
62
|
+
// while the socket stays up (a presence row reaped as stale).
|
|
63
|
+
| { type: 'server_dropped' }
|
|
52
64
|
| { type: 'error'; reason: string };
|
|
53
65
|
|
|
54
66
|
export type TopicEffect =
|
|
55
67
|
| { kind: 'send_subscribe' }
|
|
68
|
+
// Send the subscribe again after the client's backoff: the server could
|
|
69
|
+
// not finish the one in flight.
|
|
70
|
+
| { kind: 'resend_subscribe_later' }
|
|
56
71
|
| { kind: 'send_unsubscribe' }
|
|
57
72
|
// Flush the frames held for the unacked subscribe, in order.
|
|
58
73
|
| { kind: 'flush_held' }
|
|
@@ -87,6 +102,7 @@ export function isFrameScopedReason(reason: string): boolean {
|
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
const SEND_SUBSCRIBE: TopicEffect = { kind: 'send_subscribe' };
|
|
105
|
+
const RESEND_SUBSCRIBE_LATER: TopicEffect = { kind: 'resend_subscribe_later' };
|
|
90
106
|
const FLUSH_HELD: TopicEffect = { kind: 'flush_held' };
|
|
91
107
|
const DROP_HELD: TopicEffect = { kind: 'drop_held' };
|
|
92
108
|
|
|
@@ -126,6 +142,10 @@ function unsubscribed(): StepResult {
|
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
export function step(state: TopicState, event: TopicEvent): StepResult {
|
|
145
|
+
if (event.type === 'server_ended') {
|
|
146
|
+
if (state.phase === 'idle') return { state, effects: [] };
|
|
147
|
+
return { state: IDLE_TOPIC, effects: [DROP_HELD, { kind: 'drop_listeners' }] };
|
|
148
|
+
}
|
|
129
149
|
switch (state.phase) {
|
|
130
150
|
case 'idle':
|
|
131
151
|
if (event.type === 'user_subscribe') {
|
|
@@ -145,7 +165,13 @@ export function step(state: TopicState, event: TopicEvent): StepResult {
|
|
|
145
165
|
};
|
|
146
166
|
case 'error':
|
|
147
167
|
if (isFrameScopedReason(event.reason)) {
|
|
148
|
-
return
|
|
168
|
+
return {
|
|
169
|
+
state,
|
|
170
|
+
effects: [
|
|
171
|
+
{ kind: 'deliver_error', reason: event.reason },
|
|
172
|
+
RESEND_SUBSCRIBE_LATER,
|
|
173
|
+
],
|
|
174
|
+
};
|
|
149
175
|
}
|
|
150
176
|
// Never established on this connection: the rejection is final.
|
|
151
177
|
return tearDown(state, event.reason);
|
|
@@ -182,6 +208,10 @@ export function step(state: TopicState, event: TopicEvent): StepResult {
|
|
|
182
208
|
return tearDown(state, event.reason);
|
|
183
209
|
case 'subscribed_ack':
|
|
184
210
|
return { state, effects: [FLUSH_HELD] };
|
|
211
|
+
// Subscribe again, holding frames until it is acked. It spends no
|
|
212
|
+
// retry: the report arrives once per heartbeat, so it cannot loop.
|
|
213
|
+
case 'server_dropped':
|
|
214
|
+
return { state: { ...state, phase: 'subscribing' }, effects: [SEND_SUBSCRIBE] };
|
|
185
215
|
case 'socket_open':
|
|
186
216
|
return reopened();
|
|
187
217
|
case 'socket_closed':
|
|
@@ -197,7 +227,13 @@ export function step(state: TopicState, event: TopicEvent): StepResult {
|
|
|
197
227
|
switch (event.type) {
|
|
198
228
|
case 'error':
|
|
199
229
|
if (isFrameScopedReason(event.reason)) {
|
|
200
|
-
return
|
|
230
|
+
return {
|
|
231
|
+
state,
|
|
232
|
+
effects: [
|
|
233
|
+
{ kind: 'deliver_error', reason: event.reason },
|
|
234
|
+
RESEND_SUBSCRIBE_LATER,
|
|
235
|
+
],
|
|
236
|
+
};
|
|
201
237
|
}
|
|
202
238
|
// An eviction draws errors for EVERY frame in flight against the
|
|
203
239
|
// reaped row; the trailing ones land while the retry's subscribe
|