@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
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
// The FROZEN form: a stream's content as plain bytes, with no CRDT engine on
|
|
2
|
+
// either side of it.
|
|
3
|
+
//
|
|
4
|
+
// Content is JSON in the shape the stream's schema describes: each top-level
|
|
5
|
+
// entry is a root, and a root is a map (an object whose entries are its
|
|
6
|
+
// records), a list (an array), or a text (a string). An ordered map is two
|
|
7
|
+
// roots, the map and the list of its keys. That is exactly what an importer
|
|
8
|
+
// returns and what `doc.map`, `doc.list`, `doc.orderedMap` and `doc.text` read
|
|
9
|
+
// back, so a stream nobody has edited is fully determined by it. A CRDT
|
|
10
|
+
// snapshot of the same content pays for per-container state, a version vector
|
|
11
|
+
// and an op history describing no edits, and every reader pays an engine to
|
|
12
|
+
// get the content back; this holds the content and nothing else.
|
|
13
|
+
//
|
|
14
|
+
// It lives beside `record-paging.ts` because the published `@brass-build/client`
|
|
15
|
+
// ships unbundled and can only import published packages and its own relative
|
|
16
|
+
// files, so the frame and the one decoder every reader uses are defined once,
|
|
17
|
+
// here. Deliberately loro-free.
|
|
18
|
+
//
|
|
19
|
+
// The stored frame (version 4, below) holds every map as typed Arrow tables
|
|
20
|
+
// and the stream as one ZSTD frame (`sealed-arrow.ts` reads the tables,
|
|
21
|
+
// `sealed-encode.ts` writes them). Version 5 is the same tables gzipped, the
|
|
22
|
+
// staging form a producer without ZSTD uploads. Content can also be held in
|
|
23
|
+
// memory with the same roots (`SealedContent.fromJSON`), so a reader never
|
|
24
|
+
// asks which it holds. A join serves the stored frame only to a caller whose
|
|
25
|
+
// `accept` names it (`SEALED_ACCEPT`).
|
|
26
|
+
//
|
|
27
|
+
// A list whose items are exactly a map's keys (an ordered map's order list) is
|
|
28
|
+
// not stored: the map is stored in the list's order and the list is read back
|
|
29
|
+
// as the map's keys in stored order.
|
|
30
|
+
import { HEADER_KEY, META_KEY, ORDER_KEY, RECORDS_KEY, SCHEMA_VERSION_KEY, recordValueOf, } from './record-paging.js';
|
|
31
|
+
import { Gunzip } from 'fflate';
|
|
32
|
+
/**
|
|
33
|
+
* The peer every conversion of frozen content into a CRDT authors as.
|
|
34
|
+
*
|
|
35
|
+
* Pinned, so one frozen stream produces one document: two sides that convert
|
|
36
|
+
* the same content independently write the same ops, and their later edits
|
|
37
|
+
* merge rather than doubling every record.
|
|
38
|
+
*
|
|
39
|
+
* A live editor never authors as this peer. It keeps the one its own document
|
|
40
|
+
* was opened with, so every op it writes after a conversion is its own.
|
|
41
|
+
*/
|
|
42
|
+
export const CONVERSION_PEER_ID = '1';
|
|
43
|
+
/** `BRST`. Four bytes so a reader handed the wrong object fails on its first
|
|
44
|
+
* word rather than on a length field it read out of a compressed run. */
|
|
45
|
+
const MAGIC = 0x42_52_53_54;
|
|
46
|
+
/** Named roots, every map as typed Arrow tables, the whole stream one ZSTD
|
|
47
|
+
* frame. What the platform stores; only its encoder writes it. */
|
|
48
|
+
export const VERSION_ARROW = 4;
|
|
49
|
+
/** Frame 4's body under gzip rather than ZSTD: what a producer without a ZSTD
|
|
50
|
+
* compressor (a browser) uploads. Staging only; the seal stores it as frame
|
|
51
|
+
* 4, so no reader is ever handed it. */
|
|
52
|
+
export const VERSION_ARROW_STAGED = 5;
|
|
53
|
+
/** The newest frame this build reads. */
|
|
54
|
+
export const NEWEST_VERSION = VERSION_ARROW;
|
|
55
|
+
/** The join `accept` token naming the stored frames this build decodes. */
|
|
56
|
+
export const SEALED_ACCEPT = `sealed@${VERSION_ARROW}`;
|
|
57
|
+
// Magic (u32), version (u8), reserved (3 bytes, zero), then 8 reserved bytes
|
|
58
|
+
// before the compressed body.
|
|
59
|
+
const PROLOGUE_BYTES = 16;
|
|
60
|
+
/** The kind a JSON value would be stored as, or null for a value no root can
|
|
61
|
+
* hold. */
|
|
62
|
+
export function sealedRootKindOf(value) {
|
|
63
|
+
if (typeof value === 'string')
|
|
64
|
+
return 'text';
|
|
65
|
+
if (Array.isArray(value))
|
|
66
|
+
return 'list';
|
|
67
|
+
if (value !== null && typeof value === 'object')
|
|
68
|
+
return 'map';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// --- Encoding ---------------------------------------------------------------
|
|
72
|
+
/** Whether a value is a record (`{ type, params }` and nothing else). */
|
|
73
|
+
function isRecordShaped(value) {
|
|
74
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const o = value;
|
|
78
|
+
let count = 0;
|
|
79
|
+
for (const key in o) {
|
|
80
|
+
if (!Object.prototype.hasOwnProperty.call(o, key))
|
|
81
|
+
continue;
|
|
82
|
+
count += 1;
|
|
83
|
+
if (key !== 'type' && key !== 'params')
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return count === 2 && typeof o['type'] === 'string';
|
|
87
|
+
}
|
|
88
|
+
/** A value split the way a record store reports it. */
|
|
89
|
+
export function sealedRecordOf(value) {
|
|
90
|
+
return isRecordShaped(value)
|
|
91
|
+
? { tag: value.type, body: value.params }
|
|
92
|
+
: { tag: null, body: value };
|
|
93
|
+
}
|
|
94
|
+
function entriesFromTriples(records) {
|
|
95
|
+
return {
|
|
96
|
+
keys: records.map((r) => r[0]),
|
|
97
|
+
value: (i) => recordValueOf(records[i][1], records[i][2]),
|
|
98
|
+
release: (i) => {
|
|
99
|
+
records[i][2] = undefined;
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function entriesFromObject(object, keys) {
|
|
104
|
+
return {
|
|
105
|
+
keys,
|
|
106
|
+
value: (i) => object[keys[i]],
|
|
107
|
+
release: (i) => {
|
|
108
|
+
object[keys[i]] = undefined;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The roots `content` is stored as, in its order: an ordered map's key list
|
|
114
|
+
* folded into the map it orders, and every map's entries in stored order.
|
|
115
|
+
*
|
|
116
|
+
* Throws for a root that is not a map, a list or a string.
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
export function planSealedContent(content) {
|
|
121
|
+
const maps = new Map();
|
|
122
|
+
const lists = [];
|
|
123
|
+
for (const name of Object.keys(content)) {
|
|
124
|
+
const value = content[name];
|
|
125
|
+
const kind = sealedRootKindOf(value);
|
|
126
|
+
if (kind === null) {
|
|
127
|
+
throw new TypeError(`Content root "${name}" is neither an object, an array, nor a string.`);
|
|
128
|
+
}
|
|
129
|
+
if (kind === 'map')
|
|
130
|
+
maps.set(name, value);
|
|
131
|
+
else if (kind === 'list')
|
|
132
|
+
lists.push({ name, items: value });
|
|
133
|
+
}
|
|
134
|
+
// An ordered map: a list that is exactly a map's keys orders that map and
|
|
135
|
+
// is not stored. One list per map, first come.
|
|
136
|
+
const orderOf = new Map();
|
|
137
|
+
const listKeysOf = new Map();
|
|
138
|
+
for (const { name, items } of lists) {
|
|
139
|
+
for (const [mapName, object] of maps) {
|
|
140
|
+
if (orderOf.has(mapName))
|
|
141
|
+
continue;
|
|
142
|
+
if (isKeyPermutation(items, object)) {
|
|
143
|
+
orderOf.set(mapName, items);
|
|
144
|
+
listKeysOf.set(name, mapName);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const roots = [];
|
|
150
|
+
for (const name of Object.keys(content)) {
|
|
151
|
+
const value = content[name];
|
|
152
|
+
const kind = sealedRootKindOf(value);
|
|
153
|
+
if (kind === 'map') {
|
|
154
|
+
const object = value;
|
|
155
|
+
const keys = orderOf.get(name) ?? ownDefinedKeys(object);
|
|
156
|
+
roots.push({ kind: 'map', name, entries: entriesFromObject(object, keys) });
|
|
157
|
+
}
|
|
158
|
+
else if (kind === 'list') {
|
|
159
|
+
const mapName = listKeysOf.get(name);
|
|
160
|
+
roots.push(mapName !== undefined
|
|
161
|
+
? { kind: 'list', name, keysOf: mapName }
|
|
162
|
+
: { kind: 'list', name, items: value });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
roots.push({ kind: 'text', name, text: value });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return roots;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The roots a record store handed over as triples is stored as.
|
|
172
|
+
*
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export function planSealedStore(source) {
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
kind: 'map',
|
|
179
|
+
name: META_KEY,
|
|
180
|
+
entries: entriesFromObject({ [SCHEMA_VERSION_KEY]: source.schemaVersion }, [
|
|
181
|
+
SCHEMA_VERSION_KEY,
|
|
182
|
+
]),
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
kind: 'list',
|
|
186
|
+
name: HEADER_KEY,
|
|
187
|
+
items: source.header.map((e) => recordValueOf(e.tag, e.body)),
|
|
188
|
+
},
|
|
189
|
+
{ kind: 'map', name: RECORDS_KEY, entries: entriesFromTriples(source.records) },
|
|
190
|
+
{ kind: 'list', name: ORDER_KEY, keysOf: RECORDS_KEY },
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
/** The keys of an object whose value is not `undefined`, which JSON would
|
|
194
|
+
* drop and which `release` leaves behind. */
|
|
195
|
+
function ownDefinedKeys(object) {
|
|
196
|
+
return Object.keys(object).filter((k) => object[k] !== undefined);
|
|
197
|
+
}
|
|
198
|
+
function isKeyPermutation(items, object) {
|
|
199
|
+
const keys = ownDefinedKeys(object);
|
|
200
|
+
if (keys.length !== items.length)
|
|
201
|
+
return false;
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
if (typeof item !== 'string' || seen.has(item))
|
|
205
|
+
return false;
|
|
206
|
+
if (!Object.prototype.hasOwnProperty.call(object, item))
|
|
207
|
+
return false;
|
|
208
|
+
if (object[item] === undefined)
|
|
209
|
+
return false;
|
|
210
|
+
seen.add(item);
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
// --- Decoding ---------------------------------------------------------------
|
|
215
|
+
/** Whether a byte run opens as frozen content THIS reader can read. Cheap
|
|
216
|
+
* enough to ask of any object, since it reads the first five bytes. */
|
|
217
|
+
export function isSealedStore(bytes) {
|
|
218
|
+
const version = frameVersion(bytes);
|
|
219
|
+
return version === VERSION_ARROW || version === VERSION_ARROW_STAGED;
|
|
220
|
+
}
|
|
221
|
+
/** The frame version stamped on frozen content, or null for bytes that are
|
|
222
|
+
* not frozen content. */
|
|
223
|
+
export function frameVersion(bytes) {
|
|
224
|
+
if (bytes.byteLength < PROLOGUE_BYTES)
|
|
225
|
+
return null;
|
|
226
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
227
|
+
return view.getUint32(0) === MAGIC ? view.getUint8(4) : null;
|
|
228
|
+
}
|
|
229
|
+
/** Frozen content that could not be read. The message names the content
|
|
230
|
+
* rather than the field that failed, because every way this throws means the
|
|
231
|
+
* bytes are not content this reader can open. */
|
|
232
|
+
export class SealedStoreError extends Error {
|
|
233
|
+
underlying;
|
|
234
|
+
constructor(message,
|
|
235
|
+
/** The error the decoder raised, where this wraps one. */
|
|
236
|
+
underlying) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.underlying = underlying;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** `out[key] = value`, as an own property even where the key is `__proto__`. */
|
|
242
|
+
function setOwn(out, key, value) {
|
|
243
|
+
if (key === '__proto__') {
|
|
244
|
+
Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
out[key] = value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** A map root held in memory: its keys in stored order and each value's JSON
|
|
251
|
+
* text, parsed afresh on every read. */
|
|
252
|
+
class SealedJsonMap {
|
|
253
|
+
_keys;
|
|
254
|
+
_lines;
|
|
255
|
+
_index = null;
|
|
256
|
+
constructor(_keys, _lines) {
|
|
257
|
+
this._keys = _keys;
|
|
258
|
+
this._lines = _lines;
|
|
259
|
+
}
|
|
260
|
+
size() {
|
|
261
|
+
return this._keys.length;
|
|
262
|
+
}
|
|
263
|
+
has(key) {
|
|
264
|
+
return this._at(key) >= 0;
|
|
265
|
+
}
|
|
266
|
+
get(key) {
|
|
267
|
+
const at = this._at(key);
|
|
268
|
+
return at < 0 ? undefined : JSON.parse(this._lines[at]);
|
|
269
|
+
}
|
|
270
|
+
keys() {
|
|
271
|
+
return [...this._keys];
|
|
272
|
+
}
|
|
273
|
+
forEach(visit) {
|
|
274
|
+
for (let i = 0; i < this._keys.length; i += 1)
|
|
275
|
+
visit(this._keys[i], JSON.parse(this._lines[i]));
|
|
276
|
+
}
|
|
277
|
+
*entries() {
|
|
278
|
+
for (let i = 0; i < this._keys.length; i += 1) {
|
|
279
|
+
yield [this._keys[i], JSON.parse(this._lines[i])];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
valueBytes() {
|
|
283
|
+
let bytes = 0;
|
|
284
|
+
for (const line of this._lines)
|
|
285
|
+
bytes += line.length;
|
|
286
|
+
return bytes;
|
|
287
|
+
}
|
|
288
|
+
toJSON() {
|
|
289
|
+
const out = {};
|
|
290
|
+
this.forEach((key, value) => setOwn(out, key, value));
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
releaseDecoded() { }
|
|
294
|
+
_at(key) {
|
|
295
|
+
if (this._index === null) {
|
|
296
|
+
this._index = new Map();
|
|
297
|
+
for (let i = 0; i < this._keys.length; i += 1)
|
|
298
|
+
this._index.set(this._keys[i], i);
|
|
299
|
+
}
|
|
300
|
+
return this._index.get(key) ?? -1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** A map's entries as `SealedJsonMap` holds them. A key repeated keeps its
|
|
304
|
+
* first entry. CONSUMES the values. */
|
|
305
|
+
function jsonMapOf(entries) {
|
|
306
|
+
const keys = [];
|
|
307
|
+
const lines = [];
|
|
308
|
+
const seen = new Set();
|
|
309
|
+
for (let i = 0; i < entries.keys.length; i += 1) {
|
|
310
|
+
const key = entries.keys[i];
|
|
311
|
+
if (!seen.has(key)) {
|
|
312
|
+
seen.add(key);
|
|
313
|
+
keys.push(key);
|
|
314
|
+
lines.push(JSON.stringify(entries.value(i)) ?? 'null');
|
|
315
|
+
}
|
|
316
|
+
entries.release(i);
|
|
317
|
+
}
|
|
318
|
+
return new SealedJsonMap(keys, lines);
|
|
319
|
+
}
|
|
320
|
+
/** Content held in memory, from the roots it is stored as. */
|
|
321
|
+
function contentOfPlan(plan, builtBy) {
|
|
322
|
+
const roots = new Map();
|
|
323
|
+
for (const root of plan) {
|
|
324
|
+
if (root.kind === 'map')
|
|
325
|
+
roots.set(root.name, { kind: 'map', map: jsonMapOf(root.entries) });
|
|
326
|
+
else if (root.kind === 'list') {
|
|
327
|
+
roots.set(root.name, 'keysOf' in root
|
|
328
|
+
? { kind: 'list', keysOf: root.keysOf }
|
|
329
|
+
: { kind: 'list', items: JSON.parse(listJson(root.items)) });
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
roots.set(root.name, { kind: 'text', value: root.text });
|
|
333
|
+
}
|
|
334
|
+
return SealedContent._of(new Uint8Array(0), roots, builtBy);
|
|
335
|
+
}
|
|
336
|
+
/** A list's items as one JSON array, an `undefined` item read as null. */
|
|
337
|
+
function listJson(items) {
|
|
338
|
+
return '[' + items.map((item) => JSON.stringify(item === undefined ? null : item) ?? 'null').join(',') + ']';
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Decoded frozen content: the frame read and every root located, or content
|
|
342
|
+
* held in memory (`fromJSON`, `fromRecords`) with the same roots, so a reader
|
|
343
|
+
* never asks which one it holds.
|
|
344
|
+
*/
|
|
345
|
+
export class SealedContent {
|
|
346
|
+
_bytes;
|
|
347
|
+
_roots;
|
|
348
|
+
_builtBy;
|
|
349
|
+
constructor(_bytes, _roots, _builtBy) {
|
|
350
|
+
this._bytes = _bytes;
|
|
351
|
+
this._roots = _roots;
|
|
352
|
+
this._builtBy = _builtBy;
|
|
353
|
+
}
|
|
354
|
+
/** Read the frame. Throws `SealedStoreError` for bytes that are not frozen
|
|
355
|
+
* content, or whose frame does not fit inside them. */
|
|
356
|
+
static decode(bytes) {
|
|
357
|
+
const version = frameVersion(bytes);
|
|
358
|
+
if (version === null)
|
|
359
|
+
throw new SealedStoreError('Not frozen content');
|
|
360
|
+
if (version === VERSION_ARROW || version === VERSION_ARROW_STAGED) {
|
|
361
|
+
return decodeArrowFrame(bytes, version);
|
|
362
|
+
}
|
|
363
|
+
throw new SealedStoreError(`Frozen content version ${version}, which this build reads up to ` +
|
|
364
|
+
`${NEWEST_VERSION}`);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Content held in memory, as `decode` reads the same content stored.
|
|
368
|
+
* CONSUMES the map values, as `sealedTablesOfImport` does. Throws for a
|
|
369
|
+
* root that is not a map, a list or a string.
|
|
370
|
+
*/
|
|
371
|
+
static fromJSON(content, builtBy) {
|
|
372
|
+
return contentOfPlan(planSealedContent(content), builtBy ?? null);
|
|
373
|
+
}
|
|
374
|
+
/** A record store handed over as triples, held in memory. CONSUMES
|
|
375
|
+
* `source.records`. */
|
|
376
|
+
static fromRecords(source) {
|
|
377
|
+
return contentOfPlan(planSealedStore(source), source.builtBy ?? null);
|
|
378
|
+
}
|
|
379
|
+
/** @internal */
|
|
380
|
+
static fromPlan(plan, builtBy) {
|
|
381
|
+
return contentOfPlan(plan, builtBy);
|
|
382
|
+
}
|
|
383
|
+
/** @internal */
|
|
384
|
+
static _of(bytes, roots, builtBy) {
|
|
385
|
+
return new SealedContent(bytes, roots, builtBy);
|
|
386
|
+
}
|
|
387
|
+
/** The content's own size, which is what it costs to hold and transfer. */
|
|
388
|
+
get byteLength() {
|
|
389
|
+
return this._bytes.byteLength;
|
|
390
|
+
}
|
|
391
|
+
/** The importer that produced the content, or null for content that names
|
|
392
|
+
* none. */
|
|
393
|
+
builtBy() {
|
|
394
|
+
return this._builtBy;
|
|
395
|
+
}
|
|
396
|
+
rootNames() {
|
|
397
|
+
return [...this._roots.keys()];
|
|
398
|
+
}
|
|
399
|
+
kindOf(name) {
|
|
400
|
+
return this._roots.get(name)?.kind;
|
|
401
|
+
}
|
|
402
|
+
/** A map root as a lazy reader, or null when the content holds no such
|
|
403
|
+
* map. */
|
|
404
|
+
mapRoot(name) {
|
|
405
|
+
const root = this._roots.get(name);
|
|
406
|
+
return root?.kind === 'map' ? root.map : null;
|
|
407
|
+
}
|
|
408
|
+
/** The map a list root is the key walk of (an ordered map's order list), or
|
|
409
|
+
* null for a list stored with items of its own. */
|
|
410
|
+
keyWalkOf(name) {
|
|
411
|
+
const root = this._roots.get(name);
|
|
412
|
+
return root?.kind === 'list' && 'keysOf' in root ? root.keysOf : null;
|
|
413
|
+
}
|
|
414
|
+
/** Every entry of a map root, in stored order, without building the
|
|
415
|
+
* object. */
|
|
416
|
+
forEachMapEntry(name, visit) {
|
|
417
|
+
this.mapRoot(name)?.forEach(visit);
|
|
418
|
+
}
|
|
419
|
+
/** A map root as an object, or null when the content holds no such map. */
|
|
420
|
+
map(name) {
|
|
421
|
+
return this.mapRoot(name)?.toJSON() ?? null;
|
|
422
|
+
}
|
|
423
|
+
/** A list root's items, or null when the content holds no such list. */
|
|
424
|
+
list(name) {
|
|
425
|
+
const root = this._roots.get(name);
|
|
426
|
+
if (root?.kind !== 'list')
|
|
427
|
+
return null;
|
|
428
|
+
if ('items' in root)
|
|
429
|
+
return [...root.items];
|
|
430
|
+
return [...(this.mapRoot(root.keysOf)?.keys() ?? [])];
|
|
431
|
+
}
|
|
432
|
+
/** A text root, or null when the content holds no such text. */
|
|
433
|
+
text(name) {
|
|
434
|
+
const root = this._roots.get(name);
|
|
435
|
+
return root?.kind === 'text' ? root.value : null;
|
|
436
|
+
}
|
|
437
|
+
/** The whole content as the JSON it was encoded from. */
|
|
438
|
+
toJSON() {
|
|
439
|
+
const out = {};
|
|
440
|
+
for (const name of this._roots.keys()) {
|
|
441
|
+
const kind = this.kindOf(name);
|
|
442
|
+
if (kind === 'map')
|
|
443
|
+
setOwn(out, name, this.map(name));
|
|
444
|
+
else if (kind === 'list')
|
|
445
|
+
setOwn(out, name, this.list(name));
|
|
446
|
+
else if (kind === 'text')
|
|
447
|
+
setOwn(out, name, this.text(name));
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The most a frame-4 or staging-frame body expands to. The encoder refuses to
|
|
454
|
+
* write tables past it and the decoder refuses a body that would expand past
|
|
455
|
+
* it, so every stored line is one every reader decodes, and a body an
|
|
456
|
+
* uploader shaped to expand further is refused before it is decompressed.
|
|
457
|
+
* The densest import measured (2.85M IFC geometry records) expands to 68 MB,
|
|
458
|
+
* and a 128 MiB CSV of single-digit cells to about 285 MB.
|
|
459
|
+
*/
|
|
460
|
+
export const SEALED_INNER_MAX_BYTES = 256 * 1024 * 1024;
|
|
461
|
+
/** Content whose tables would expand past `SEALED_INNER_MAX_BYTES`. */
|
|
462
|
+
export class SealedContentTooLargeError extends SealedStoreError {
|
|
463
|
+
constructor() {
|
|
464
|
+
super(`Sealed content expands past ${SEALED_INNER_MAX_BYTES} bytes`);
|
|
465
|
+
this.name = 'SealedContentTooLargeError';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const ZSTD_MAGIC = 0xfd2fb528;
|
|
469
|
+
const ZSTD_BLOCK_MAX = 128 * 1024;
|
|
470
|
+
/**
|
|
471
|
+
* The most a ZSTD stream can write or allocate, read from its frame and block
|
|
472
|
+
* headers without decompressing it: per frame its window, and its declared
|
|
473
|
+
* content size, which a decoder holds the frame's output to. A frame that
|
|
474
|
+
* declares none is bounded by its blocks: each raw or RLE block's stated size
|
|
475
|
+
* plus the format's 128 KiB ceiling per compressed block, which overstates a
|
|
476
|
+
* level-19 frame (its blocks average a few KB). Both encoders here declare the
|
|
477
|
+
* size. Throws on a stream that is not ZSTD frames end to end.
|
|
478
|
+
*/
|
|
479
|
+
export function zstdOutputBound(bytes) {
|
|
480
|
+
let at = 0;
|
|
481
|
+
let output = 0;
|
|
482
|
+
let largest = 0;
|
|
483
|
+
const need = (n) => {
|
|
484
|
+
if (at + n > bytes.length)
|
|
485
|
+
throw new RangeError('zstd stream is truncated');
|
|
486
|
+
};
|
|
487
|
+
const le = (n) => {
|
|
488
|
+
need(n);
|
|
489
|
+
let v = 0;
|
|
490
|
+
for (let i = n - 1; i >= 0; i -= 1)
|
|
491
|
+
v = v * 256 + bytes[at + i];
|
|
492
|
+
at += n;
|
|
493
|
+
return v;
|
|
494
|
+
};
|
|
495
|
+
while (at < bytes.length) {
|
|
496
|
+
const magic = le(4);
|
|
497
|
+
if ((magic & 0xfffffff0) >>> 0 === 0x184d2a50) {
|
|
498
|
+
const size = le(4);
|
|
499
|
+
need(size);
|
|
500
|
+
at += size;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (magic !== ZSTD_MAGIC)
|
|
504
|
+
throw new RangeError('not a zstd frame');
|
|
505
|
+
const fhd = le(1);
|
|
506
|
+
if (fhd & 8)
|
|
507
|
+
throw new RangeError('zstd frame header sets a reserved bit');
|
|
508
|
+
const fcsFlag = fhd >> 6;
|
|
509
|
+
const single = (fhd >> 5) & 1;
|
|
510
|
+
const checksum = (fhd >> 2) & 1;
|
|
511
|
+
let window = 0;
|
|
512
|
+
if (single === 0) {
|
|
513
|
+
const wd = le(1);
|
|
514
|
+
const base = 2 ** (10 + (wd >> 3));
|
|
515
|
+
window = base + (base / 8) * (wd & 7);
|
|
516
|
+
}
|
|
517
|
+
le([0, 1, 2, 4][fhd & 3]);
|
|
518
|
+
const fcsBytes = fcsFlag === 0 ? single : 2 ** fcsFlag;
|
|
519
|
+
const fcs = fcsBytes === 0 ? 0 : le(fcsBytes) + (fcsBytes === 2 ? 256 : 0);
|
|
520
|
+
largest = Math.max(largest, window, fcs);
|
|
521
|
+
let blocks = 0;
|
|
522
|
+
for (;;) {
|
|
523
|
+
const header = le(3);
|
|
524
|
+
const type = (header >> 1) & 3;
|
|
525
|
+
const size = header >>> 3;
|
|
526
|
+
if (type === 0) {
|
|
527
|
+
need(size);
|
|
528
|
+
at += size;
|
|
529
|
+
blocks += size;
|
|
530
|
+
}
|
|
531
|
+
else if (type === 1) {
|
|
532
|
+
need(1);
|
|
533
|
+
at += 1;
|
|
534
|
+
blocks += size;
|
|
535
|
+
}
|
|
536
|
+
else if (type === 2) {
|
|
537
|
+
need(size);
|
|
538
|
+
at += size;
|
|
539
|
+
blocks += ZSTD_BLOCK_MAX;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
throw new RangeError('zstd block type is reserved');
|
|
543
|
+
}
|
|
544
|
+
if (header & 1)
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
output += fcsBytes === 0 ? blocks : fcs;
|
|
548
|
+
if (checksum)
|
|
549
|
+
le(4);
|
|
550
|
+
}
|
|
551
|
+
return Math.max(output, largest);
|
|
552
|
+
}
|
|
553
|
+
/** A ZSTD body decompressed, refused before it is decompressed where its
|
|
554
|
+
* headers allow more than `max`. */
|
|
555
|
+
function unzstdCapped(unzstd, body, max) {
|
|
556
|
+
let bound;
|
|
557
|
+
try {
|
|
558
|
+
bound = zstdOutputBound(body);
|
|
559
|
+
}
|
|
560
|
+
catch (cause) {
|
|
561
|
+
throw new SealedStoreError('Frozen content is unreadable', cause);
|
|
562
|
+
}
|
|
563
|
+
if (bound > max)
|
|
564
|
+
throw new SealedContentTooLargeError();
|
|
565
|
+
return unzstd(body, max);
|
|
566
|
+
}
|
|
567
|
+
/** A gzip body inflated a slice at a time, refused once it passes `max`.
|
|
568
|
+
* A slice inflates to at most 1032 times its own size. */
|
|
569
|
+
function gunzipStreamed(body, max) {
|
|
570
|
+
const chunks = [];
|
|
571
|
+
let total = 0;
|
|
572
|
+
const inflate = new Gunzip((chunk) => {
|
|
573
|
+
total += chunk.byteLength;
|
|
574
|
+
if (total > max)
|
|
575
|
+
throw new SealedContentTooLargeError();
|
|
576
|
+
chunks.push(chunk);
|
|
577
|
+
});
|
|
578
|
+
const step = 16 * 1024;
|
|
579
|
+
for (let at = 0; at < body.length; at += step) {
|
|
580
|
+
inflate.push(body.subarray(at, at + step), at + step >= body.length);
|
|
581
|
+
}
|
|
582
|
+
const out = new Uint8Array(total);
|
|
583
|
+
let at = 0;
|
|
584
|
+
for (const chunk of chunks) {
|
|
585
|
+
out.set(chunk, at);
|
|
586
|
+
at += chunk.byteLength;
|
|
587
|
+
}
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
let sealedCodec = null;
|
|
591
|
+
let sealedCodecLoading = null;
|
|
592
|
+
/** Whether decoding these bytes needs `loadSealedCodec` first. */
|
|
593
|
+
export function needsSealedCodec(bytes) {
|
|
594
|
+
const version = frameVersion(bytes);
|
|
595
|
+
return (version === VERSION_ARROW || version === VERSION_ARROW_STAGED) && sealedCodec === null;
|
|
596
|
+
}
|
|
597
|
+
/** Node's error for output past `maxOutputLength`. */
|
|
598
|
+
function overMax(err) {
|
|
599
|
+
return err?.code === 'ERR_BUFFER_TOO_LARGE';
|
|
600
|
+
}
|
|
601
|
+
/** Load what a frame-4 decode needs. Under a Node that carries zstd, the
|
|
602
|
+
* built-in decompresses; anywhere else, `fzstd`. */
|
|
603
|
+
export function loadSealedCodec() {
|
|
604
|
+
if (sealedCodec !== null)
|
|
605
|
+
return Promise.resolve();
|
|
606
|
+
sealedCodecLoading ??= (async () => {
|
|
607
|
+
let native;
|
|
608
|
+
try {
|
|
609
|
+
const proc = globalThis.process;
|
|
610
|
+
native = proc?.getBuiltinModule?.('node:zlib');
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
native = undefined;
|
|
614
|
+
}
|
|
615
|
+
const nativeUnzstd = native?.zstdDecompressSync?.bind(native);
|
|
616
|
+
const nativeGunzip = native?.gunzipSync?.bind(native);
|
|
617
|
+
const [arrow, unzstd, reader] = await Promise.all([
|
|
618
|
+
import('apache-arrow'),
|
|
619
|
+
nativeUnzstd !== undefined
|
|
620
|
+
? Promise.resolve((bytes, max) => {
|
|
621
|
+
try {
|
|
622
|
+
return new Uint8Array(nativeUnzstd(bytes, { maxOutputLength: max }));
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
if (overMax(err))
|
|
626
|
+
throw new SealedContentTooLargeError();
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
629
|
+
})
|
|
630
|
+
: // The frame headers bound what `fzstd` allocates and writes
|
|
631
|
+
// (`unzstdCapped` checks them first), so it runs uncapped.
|
|
632
|
+
import('fzstd').then((m) => (bytes) => m.decompress(bytes)),
|
|
633
|
+
import('./sealed-arrow.js'),
|
|
634
|
+
]);
|
|
635
|
+
const gunzip = nativeGunzip !== undefined
|
|
636
|
+
? (bytes, max) => {
|
|
637
|
+
try {
|
|
638
|
+
return new Uint8Array(nativeGunzip(bytes, { maxOutputLength: max }));
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
if (overMax(err))
|
|
642
|
+
throw new SealedContentTooLargeError();
|
|
643
|
+
throw err;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
: gunzipStreamed;
|
|
647
|
+
sealedCodec = { arrow: arrow, unzstd, gunzip, reader };
|
|
648
|
+
})().catch((err) => {
|
|
649
|
+
sealedCodecLoading = null;
|
|
650
|
+
throw err;
|
|
651
|
+
});
|
|
652
|
+
return sealedCodecLoading;
|
|
653
|
+
}
|
|
654
|
+
/** Decode frozen bytes, loading the frame-4 codec first when they need it.
|
|
655
|
+
* A frame-4 decode awaits `pause` before each table it parses, so a large
|
|
656
|
+
* decode leaves the page responsive. */
|
|
657
|
+
export async function decodeSealedContent(bytes, pause = slicedYield()) {
|
|
658
|
+
if (needsSealedCodec(bytes))
|
|
659
|
+
await loadSealedCodec();
|
|
660
|
+
const version = frameVersion(bytes);
|
|
661
|
+
if (version !== VERSION_ARROW && version !== VERSION_ARROW_STAGED)
|
|
662
|
+
return SealedContent.decode(bytes);
|
|
663
|
+
const steps = arrowFrameSteps(bytes, version);
|
|
664
|
+
for (let step = steps.next();; step = steps.next()) {
|
|
665
|
+
if (step.done === true)
|
|
666
|
+
return step.value;
|
|
667
|
+
await pause();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/** Work between yields. A yield costs a few milliseconds on a phone, and a
|
|
671
|
+
* frame holds hundreds of tables, so yielding before every one adds up. */
|
|
672
|
+
const YIELD_SLICE_MS = 25;
|
|
673
|
+
/** A pause that hands the event loop back once `sliceMs` of work has run
|
|
674
|
+
* since the last time it did. */
|
|
675
|
+
export function slicedYield(sliceMs = YIELD_SLICE_MS) {
|
|
676
|
+
let since = performance.now();
|
|
677
|
+
return async () => {
|
|
678
|
+
if (performance.now() - since < sliceMs)
|
|
679
|
+
return;
|
|
680
|
+
await yieldToEventLoop();
|
|
681
|
+
since = performance.now();
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/** Hands the event loop back for one turn: `scheduler.yield` where the
|
|
685
|
+
* platform has it, else a message, which no timer clamp delays. */
|
|
686
|
+
export function yieldToEventLoop() {
|
|
687
|
+
const scheduler = globalThis.scheduler;
|
|
688
|
+
if (typeof scheduler?.yield === 'function')
|
|
689
|
+
return scheduler.yield();
|
|
690
|
+
if (typeof MessageChannel === 'function') {
|
|
691
|
+
return new Promise((resolve) => {
|
|
692
|
+
const channel = new MessageChannel();
|
|
693
|
+
channel.port1.onmessage = () => {
|
|
694
|
+
channel.port1.close();
|
|
695
|
+
resolve();
|
|
696
|
+
};
|
|
697
|
+
channel.port2.postMessage(null);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
701
|
+
}
|
|
702
|
+
/** A frame-4 object around its ZSTD-compressed parts, or with
|
|
703
|
+
* `VERSION_ARROW_STAGED`, the staging frame around its gzipped parts. */
|
|
704
|
+
export function arrowFrameBytes(compressed, version = VERSION_ARROW) {
|
|
705
|
+
const out = new Uint8Array(PROLOGUE_BYTES + compressed.byteLength);
|
|
706
|
+
const view = new DataView(out.buffer);
|
|
707
|
+
view.setUint32(0, MAGIC);
|
|
708
|
+
view.setUint8(4, version);
|
|
709
|
+
out.set(compressed, PROLOGUE_BYTES);
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
/** A frame-4 or staging-frame object's compressed parts. */
|
|
713
|
+
export function arrowFrameBody(bytes) {
|
|
714
|
+
return bytes.subarray(PROLOGUE_BYTES);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* A frame-4 or staging-frame object's parts, decompressed. Throws
|
|
718
|
+
* `SealedContentTooLargeError` for a body that expands past
|
|
719
|
+
* `SEALED_INNER_MAX_BYTES`, and needs `loadSealedCodec` first.
|
|
720
|
+
*/
|
|
721
|
+
export function arrowFrameInner(bytes) {
|
|
722
|
+
const version = frameVersion(bytes);
|
|
723
|
+
if (sealedCodec === null) {
|
|
724
|
+
throw new SealedStoreError(`Frozen content version ${version} needs its codec loaded first`);
|
|
725
|
+
}
|
|
726
|
+
const body = bytes.subarray(PROLOGUE_BYTES);
|
|
727
|
+
try {
|
|
728
|
+
return version === VERSION_ARROW_STAGED
|
|
729
|
+
? sealedCodec.gunzip(body, SEALED_INNER_MAX_BYTES)
|
|
730
|
+
: unzstdCapped(sealedCodec.unzstd, body, SEALED_INNER_MAX_BYTES);
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
if (err instanceof SealedStoreError)
|
|
734
|
+
throw err;
|
|
735
|
+
throw new SealedStoreError('Frozen content is unreadable', err);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/** The manifest part's name; table parts are named by their position. */
|
|
739
|
+
export const ARROW_MANIFEST_PART = 'manifest';
|
|
740
|
+
function decodeArrowFrame(bytes, version) {
|
|
741
|
+
const steps = arrowFrameSteps(bytes, version);
|
|
742
|
+
for (let step = steps.next();; step = steps.next())
|
|
743
|
+
if (step.done === true)
|
|
744
|
+
return step.value;
|
|
745
|
+
}
|
|
746
|
+
/** A frame-4 decode, stepping before each table it parses. */
|
|
747
|
+
function* arrowFrameSteps(bytes, version) {
|
|
748
|
+
if (sealedCodec === null) {
|
|
749
|
+
throw new SealedStoreError(`Frozen content version ${version} needs its codec loaded first`);
|
|
750
|
+
}
|
|
751
|
+
const codec = sealedCodec;
|
|
752
|
+
const inner = arrowFrameInner(bytes);
|
|
753
|
+
let parts;
|
|
754
|
+
try {
|
|
755
|
+
parts = codec.reader.unpackParts(inner);
|
|
756
|
+
}
|
|
757
|
+
catch (cause) {
|
|
758
|
+
throw new SealedStoreError('Frozen content is unreadable', cause);
|
|
759
|
+
}
|
|
760
|
+
const [manifestName, manifestBytes] = parts[0] ?? [];
|
|
761
|
+
if (manifestName !== ARROW_MANIFEST_PART || manifestBytes === undefined) {
|
|
762
|
+
throw new SealedStoreError('Frozen content descriptor is unreadable');
|
|
763
|
+
}
|
|
764
|
+
let manifest;
|
|
765
|
+
try {
|
|
766
|
+
manifest = JSON.parse(new TextDecoder().decode(manifestBytes));
|
|
767
|
+
}
|
|
768
|
+
catch (cause) {
|
|
769
|
+
throw new SealedStoreError('Frozen content descriptor is unreadable', cause);
|
|
770
|
+
}
|
|
771
|
+
if (!Array.isArray(manifest) || !Array.isArray(manifest[1])) {
|
|
772
|
+
throw new SealedStoreError('Frozen content descriptor is unreadable');
|
|
773
|
+
}
|
|
774
|
+
const tablesOf = new Map();
|
|
775
|
+
for (const [, ipc] of parts.slice(1)) {
|
|
776
|
+
yield;
|
|
777
|
+
let table;
|
|
778
|
+
let meta;
|
|
779
|
+
try {
|
|
780
|
+
codec.reader.checkIpcFile(ipc);
|
|
781
|
+
table = codec.arrow.tableFromIPC(ipc);
|
|
782
|
+
meta = JSON.parse(table.schema.metadata.get(codec.reader.TABLE_META_KEY) ?? '');
|
|
783
|
+
for (const batch of table.batches)
|
|
784
|
+
codec.reader.checkTableData(batch.data);
|
|
785
|
+
}
|
|
786
|
+
catch (cause) {
|
|
787
|
+
throw new SealedStoreError('Frozen table is unreadable', cause);
|
|
788
|
+
}
|
|
789
|
+
let list = tablesOf.get(meta.root);
|
|
790
|
+
if (list === undefined)
|
|
791
|
+
tablesOf.set(meta.root, (list = []));
|
|
792
|
+
for (const batch of table.batches)
|
|
793
|
+
list.push({ meta, data: batch.data });
|
|
794
|
+
}
|
|
795
|
+
const by = manifest[0];
|
|
796
|
+
const builtBy = Array.isArray(by) && typeof by[0] === 'string' && typeof by[1] === 'string'
|
|
797
|
+
? { id: by[0], version: by[1] }
|
|
798
|
+
: null;
|
|
799
|
+
const roots = new Map();
|
|
800
|
+
for (const d of manifest[1]) {
|
|
801
|
+
if (!Array.isArray(d) || typeof d[1] !== 'string') {
|
|
802
|
+
throw new SealedStoreError('Frozen content descriptor is unreadable');
|
|
803
|
+
}
|
|
804
|
+
const name = d[1];
|
|
805
|
+
if (d[0] === 'map') {
|
|
806
|
+
roots.set(name, { kind: 'map', map: new SealedArrowMap(codec.reader, tablesOf.get(name) ?? []) });
|
|
807
|
+
}
|
|
808
|
+
else if (d[0] === 'list') {
|
|
809
|
+
if (typeof d[3] === 'string')
|
|
810
|
+
roots.set(name, { kind: 'list', keysOf: d[3] });
|
|
811
|
+
else if (Array.isArray(d[2]))
|
|
812
|
+
roots.set(name, { kind: 'list', items: d[2] });
|
|
813
|
+
else
|
|
814
|
+
throw new SealedStoreError('Frozen content descriptor is unreadable');
|
|
815
|
+
}
|
|
816
|
+
else if (d[0] === 'text' && typeof d[2] === 'string') {
|
|
817
|
+
roots.set(name, { kind: 'text', value: d[2] });
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
throw new SealedStoreError(`Frozen content root "${name}" has an unknown kind`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return SealedContent._of(bytes, roots, builtBy);
|
|
824
|
+
}
|
|
825
|
+
/** A row's value, with any failure to build it read as unreadable content. */
|
|
826
|
+
function readValue(t, row) {
|
|
827
|
+
try {
|
|
828
|
+
return t.value(row);
|
|
829
|
+
}
|
|
830
|
+
catch (cause) {
|
|
831
|
+
throw new SealedStoreError('Frozen table is unreadable', cause);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/** A row's key, with any failure to read it as a string read as unreadable
|
|
835
|
+
* content. */
|
|
836
|
+
function readKey(t, row) {
|
|
837
|
+
let key;
|
|
838
|
+
try {
|
|
839
|
+
key = t.key(row);
|
|
840
|
+
}
|
|
841
|
+
catch (cause) {
|
|
842
|
+
throw new SealedStoreError('Frozen table is unreadable', cause);
|
|
843
|
+
}
|
|
844
|
+
if (typeof key !== 'string')
|
|
845
|
+
throw new SealedStoreError('Frozen table is unreadable');
|
|
846
|
+
return key;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Records a hash per key as a walk meets them, in position order, and throws
|
|
850
|
+
* from `finish` if two keys were the same: the hashes are sorted, and only
|
|
851
|
+
* where two agree are the keys themselves rebuilt and compared. Kept to a
|
|
852
|
+
* typed array, so the check costs one sort of four bytes a key.
|
|
853
|
+
*/
|
|
854
|
+
class KeyCheck {
|
|
855
|
+
_keyAt;
|
|
856
|
+
_hashes;
|
|
857
|
+
constructor(count, _keyAt) {
|
|
858
|
+
this._keyAt = _keyAt;
|
|
859
|
+
this._hashes = new Uint32Array(count);
|
|
860
|
+
}
|
|
861
|
+
add(key, at) {
|
|
862
|
+
this._hashes[at] = keyHash(key);
|
|
863
|
+
}
|
|
864
|
+
finish() {
|
|
865
|
+
const sorted = this._hashes.slice().sort();
|
|
866
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
867
|
+
if (sorted[i] !== sorted[i - 1])
|
|
868
|
+
continue;
|
|
869
|
+
const h = sorted[i];
|
|
870
|
+
const seen = new Set();
|
|
871
|
+
for (let at = 0; at < this._hashes.length; at += 1) {
|
|
872
|
+
if (this._hashes[at] !== h)
|
|
873
|
+
continue;
|
|
874
|
+
const key = this._keyAt(at);
|
|
875
|
+
if (seen.has(key))
|
|
876
|
+
throw new SealedStoreError('Frozen table is unreadable');
|
|
877
|
+
seen.add(key);
|
|
878
|
+
}
|
|
879
|
+
while (i + 1 < sorted.length && sorted[i + 1] === h)
|
|
880
|
+
i += 1;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function keyHash(key) {
|
|
885
|
+
let h = 0x811c9dc5;
|
|
886
|
+
for (let i = 0; i < key.length; i += 1)
|
|
887
|
+
h = Math.imul(h ^ key.charCodeAt(i), 0x01000193);
|
|
888
|
+
return h >>> 0;
|
|
889
|
+
}
|
|
890
|
+
/** Keyed reads a frame-4 map answers by scanning its key columns before it
|
|
891
|
+
* builds an index of every key. A scan touches only typed arrays and costs
|
|
892
|
+
* milliseconds at a million records; the index costs a string and a map
|
|
893
|
+
* entry per record, which a few keyed reads never repay. */
|
|
894
|
+
const SCANNED_LOOKUPS = 64;
|
|
895
|
+
/** A version 4 map root. Its tables are prepared on first use; a value is
|
|
896
|
+
* built when it is read, so a keyed read builds the rows it asks for. Until a
|
|
897
|
+
* walk prepares every table, a keyed read prepares only the table it hits,
|
|
898
|
+
* decoding its dictionaries' entries as they are read. */
|
|
899
|
+
class SealedArrowMap {
|
|
900
|
+
_reader;
|
|
901
|
+
_stored;
|
|
902
|
+
_count;
|
|
903
|
+
_cx;
|
|
904
|
+
_ready;
|
|
905
|
+
_lazyCx = null;
|
|
906
|
+
_lazyReady = null;
|
|
907
|
+
_tables = null;
|
|
908
|
+
_tableAt = null;
|
|
909
|
+
_rowAt = null;
|
|
910
|
+
_keys = null;
|
|
911
|
+
_index = null;
|
|
912
|
+
_lookups = 0;
|
|
913
|
+
/** Whether a whole walk has found every key distinct. */
|
|
914
|
+
_unique = false;
|
|
915
|
+
constructor(_reader, _stored) {
|
|
916
|
+
this._reader = _reader;
|
|
917
|
+
this._stored = _stored;
|
|
918
|
+
let count = 0;
|
|
919
|
+
for (const t of _stored)
|
|
920
|
+
count += t.data.length;
|
|
921
|
+
this._count = count;
|
|
922
|
+
this._cx = new _reader.ReadContext();
|
|
923
|
+
this._ready = Array.from({ length: _stored.length });
|
|
924
|
+
}
|
|
925
|
+
size() {
|
|
926
|
+
return this._count;
|
|
927
|
+
}
|
|
928
|
+
has(key) {
|
|
929
|
+
return this._find(key) !== null;
|
|
930
|
+
}
|
|
931
|
+
get(key) {
|
|
932
|
+
const found = this._find(key);
|
|
933
|
+
return found === null ? undefined : readValue(this._keyedTable(found[0]), found[1]);
|
|
934
|
+
}
|
|
935
|
+
keys() {
|
|
936
|
+
if (this._keys === null) {
|
|
937
|
+
this._order();
|
|
938
|
+
const check = this._unique ? null : new KeyCheck(this._count, (at) => this._keyAt(at));
|
|
939
|
+
const keys = Array.from({ length: this._count });
|
|
940
|
+
for (let at = 0; at < this._count; at += 1) {
|
|
941
|
+
const key = this._keyAt(at);
|
|
942
|
+
check?.add(key, at);
|
|
943
|
+
keys[at] = key;
|
|
944
|
+
}
|
|
945
|
+
check?.finish();
|
|
946
|
+
this._unique = true;
|
|
947
|
+
this._keys = keys;
|
|
948
|
+
}
|
|
949
|
+
return this._keys;
|
|
950
|
+
}
|
|
951
|
+
forEach(visit) {
|
|
952
|
+
for (const [key, value] of this.entries())
|
|
953
|
+
visit(key, value);
|
|
954
|
+
}
|
|
955
|
+
*entries() {
|
|
956
|
+
this._order();
|
|
957
|
+
const tables = this._tables;
|
|
958
|
+
const tableAt = this._tableAt;
|
|
959
|
+
const rowAt = this._rowAt;
|
|
960
|
+
// A repeated key is reported once the walk has met both.
|
|
961
|
+
const check = this._unique ? null : new KeyCheck(this._count, (at) => this._keyAt(at));
|
|
962
|
+
for (let at = 0; at < this._count; at += 1) {
|
|
963
|
+
const t = tables[tableAt[at]];
|
|
964
|
+
const row = rowAt[at];
|
|
965
|
+
const key = readKey(t, row);
|
|
966
|
+
check?.add(key, at);
|
|
967
|
+
yield [key, readValue(t, row)];
|
|
968
|
+
}
|
|
969
|
+
check?.finish();
|
|
970
|
+
this._unique = true;
|
|
971
|
+
}
|
|
972
|
+
valueBytes() {
|
|
973
|
+
let bytes = 0;
|
|
974
|
+
for (const [, value] of this.entries())
|
|
975
|
+
bytes += (JSON.stringify(value) ?? 'null').length;
|
|
976
|
+
return bytes;
|
|
977
|
+
}
|
|
978
|
+
toJSON() {
|
|
979
|
+
const out = {};
|
|
980
|
+
for (const [key, value] of this.entries())
|
|
981
|
+
setOwn(out, key, value);
|
|
982
|
+
return out;
|
|
983
|
+
}
|
|
984
|
+
releaseDecoded() {
|
|
985
|
+
this._keys = null;
|
|
986
|
+
this._index = null;
|
|
987
|
+
this._lookups = 0;
|
|
988
|
+
}
|
|
989
|
+
/** The table and row holding `key`. */
|
|
990
|
+
_find(key) {
|
|
991
|
+
if (this._index === null && this._lookups >= SCANNED_LOOKUPS) {
|
|
992
|
+
const keys = this.keys();
|
|
993
|
+
const index = new Map();
|
|
994
|
+
for (let at = 0; at < keys.length; at += 1)
|
|
995
|
+
index.set(keys[at], at);
|
|
996
|
+
this._index = index;
|
|
997
|
+
}
|
|
998
|
+
if (this._index !== null) {
|
|
999
|
+
const at = this._index.get(key);
|
|
1000
|
+
return at === undefined ? null : [this._tableAt[at], this._rowAt[at]];
|
|
1001
|
+
}
|
|
1002
|
+
this._lookups += 1;
|
|
1003
|
+
const cx = this._tables === null ? this._lazy() : this._cx;
|
|
1004
|
+
const matches = [];
|
|
1005
|
+
for (let ti = 0; ti < this._stored.length; ti += 1) {
|
|
1006
|
+
const t = this._stored[ti];
|
|
1007
|
+
let rows;
|
|
1008
|
+
try {
|
|
1009
|
+
rows = this._reader.rowsWithKey(t.meta, t.data.children[0], key, cx);
|
|
1010
|
+
}
|
|
1011
|
+
catch (cause) {
|
|
1012
|
+
throw new SealedStoreError('Frozen table is unreadable', cause);
|
|
1013
|
+
}
|
|
1014
|
+
for (const row of rows)
|
|
1015
|
+
matches.push([ti, row]);
|
|
1016
|
+
}
|
|
1017
|
+
// The encoder writes each key once, so tables repeating one are not its.
|
|
1018
|
+
if (matches.length > 1)
|
|
1019
|
+
throw new SealedStoreError('Frozen table is unreadable');
|
|
1020
|
+
return matches[0] ?? null;
|
|
1021
|
+
}
|
|
1022
|
+
_keyAt(at) {
|
|
1023
|
+
return readKey(this._tables[this._tableAt[at]], this._rowAt[at]);
|
|
1024
|
+
}
|
|
1025
|
+
_lazy() {
|
|
1026
|
+
return (this._lazyCx ??= new this._reader.ReadContext(true));
|
|
1027
|
+
}
|
|
1028
|
+
/** A table for a keyed read: the walk's where it is prepared, else one
|
|
1029
|
+
* prepared lazily on its own. */
|
|
1030
|
+
_keyedTable(ti) {
|
|
1031
|
+
const ready = this._ready[ti];
|
|
1032
|
+
if (ready !== undefined)
|
|
1033
|
+
return ready;
|
|
1034
|
+
const lazy = (this._lazyReady ??= Array.from({ length: this._stored.length }));
|
|
1035
|
+
let t = lazy[ti];
|
|
1036
|
+
if (t === undefined) {
|
|
1037
|
+
const stored = this._stored[ti];
|
|
1038
|
+
try {
|
|
1039
|
+
t = this._reader.readyTable(stored.meta, stored.data, this._lazy());
|
|
1040
|
+
}
|
|
1041
|
+
catch (cause) {
|
|
1042
|
+
throw new SealedStoreError('Frozen table is unreadable', cause);
|
|
1043
|
+
}
|
|
1044
|
+
lazy[ti] = t;
|
|
1045
|
+
}
|
|
1046
|
+
return t;
|
|
1047
|
+
}
|
|
1048
|
+
_table(ti) {
|
|
1049
|
+
let t = this._ready[ti];
|
|
1050
|
+
if (t === undefined) {
|
|
1051
|
+
const stored = this._stored[ti];
|
|
1052
|
+
try {
|
|
1053
|
+
t = this._reader.readyTable(stored.meta, stored.data, this._cx);
|
|
1054
|
+
}
|
|
1055
|
+
catch (cause) {
|
|
1056
|
+
throw new SealedStoreError('Frozen table is unreadable', cause);
|
|
1057
|
+
}
|
|
1058
|
+
this._ready[ti] = t;
|
|
1059
|
+
}
|
|
1060
|
+
return t;
|
|
1061
|
+
}
|
|
1062
|
+
/** Each position's table and row. Every position in the map is held by
|
|
1063
|
+
* exactly one row, or the tables do not describe the map. */
|
|
1064
|
+
_order() {
|
|
1065
|
+
if (this._tables !== null)
|
|
1066
|
+
return;
|
|
1067
|
+
const tables = this._stored.map((_, ti) => this._table(ti));
|
|
1068
|
+
const tableAt = new Int32Array(this._count).fill(-1);
|
|
1069
|
+
const rowAt = new Int32Array(this._count);
|
|
1070
|
+
tables.forEach((t, ti) => {
|
|
1071
|
+
const positions = t.positions;
|
|
1072
|
+
for (let row = 0; row < positions.length; row += 1) {
|
|
1073
|
+
const at = positions[row];
|
|
1074
|
+
if (!(at >= 0 && at < this._count) || tableAt[at] !== -1) {
|
|
1075
|
+
throw new SealedStoreError('Frozen table is unreadable');
|
|
1076
|
+
}
|
|
1077
|
+
tableAt[at] = ti;
|
|
1078
|
+
rowAt[at] = row;
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
this._tableAt = tableAt;
|
|
1082
|
+
this._rowAt = rowAt;
|
|
1083
|
+
this._tables = tables;
|
|
1084
|
+
this._lazyCx = null;
|
|
1085
|
+
this._lazyReady = null;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Frozen content read as a record store: the `instances` map, its order, the
|
|
1090
|
+
* schema version and the header. Every server-side reader that walks records
|
|
1091
|
+
* (an exporter, a view) reads through this, and it answers for any frozen
|
|
1092
|
+
* content: a root the content does not hold reads as empty.
|
|
1093
|
+
*/
|
|
1094
|
+
export class SealedStore {
|
|
1095
|
+
_content;
|
|
1096
|
+
_records;
|
|
1097
|
+
constructor(_content) {
|
|
1098
|
+
this._content = _content;
|
|
1099
|
+
this._records = _content.mapRoot(RECORDS_KEY);
|
|
1100
|
+
}
|
|
1101
|
+
static decode(bytes) {
|
|
1102
|
+
return new SealedStore(SealedContent.decode(bytes));
|
|
1103
|
+
}
|
|
1104
|
+
static of(content) {
|
|
1105
|
+
return new SealedStore(content);
|
|
1106
|
+
}
|
|
1107
|
+
/** The general form this view reads. */
|
|
1108
|
+
get content() {
|
|
1109
|
+
return this._content;
|
|
1110
|
+
}
|
|
1111
|
+
get byteLength() {
|
|
1112
|
+
return this._content.byteLength;
|
|
1113
|
+
}
|
|
1114
|
+
/** The schema version, the `meta` root's one key. */
|
|
1115
|
+
schemaVersion() {
|
|
1116
|
+
const v = this._content.mapRoot(META_KEY)?.get(SCHEMA_VERSION_KEY);
|
|
1117
|
+
return typeof v === 'string' ? v : '';
|
|
1118
|
+
}
|
|
1119
|
+
/** The header entries in the record shape, in order. */
|
|
1120
|
+
header() {
|
|
1121
|
+
const out = [];
|
|
1122
|
+
for (const item of this._content.list(HEADER_KEY) ?? []) {
|
|
1123
|
+
if (isRecordShaped(item))
|
|
1124
|
+
out.push({ tag: item.type, body: item.params });
|
|
1125
|
+
}
|
|
1126
|
+
return out;
|
|
1127
|
+
}
|
|
1128
|
+
builtBy() {
|
|
1129
|
+
return this._content.builtBy();
|
|
1130
|
+
}
|
|
1131
|
+
keys() {
|
|
1132
|
+
return this._records?.keys() ?? [];
|
|
1133
|
+
}
|
|
1134
|
+
recordCount() {
|
|
1135
|
+
return this._records?.size() ?? 0;
|
|
1136
|
+
}
|
|
1137
|
+
has(key) {
|
|
1138
|
+
return this._records?.has(key) ?? false;
|
|
1139
|
+
}
|
|
1140
|
+
get(key) {
|
|
1141
|
+
if (this._records === null || !this._records.has(key))
|
|
1142
|
+
return undefined;
|
|
1143
|
+
return sealedRecordOf(this._records.get(key));
|
|
1144
|
+
}
|
|
1145
|
+
forEachRecord(visit) {
|
|
1146
|
+
this._records?.forEach((key, value) => {
|
|
1147
|
+
const record = sealedRecordOf(value);
|
|
1148
|
+
visit(key, record.tag, record.body);
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
releaseDecoded() {
|
|
1152
|
+
this._records?.releaseDecoded();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
//# sourceMappingURL=sealed-store.js.map
|