@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,843 @@
|
|
|
1
|
+
// Frame 4's reader: a stream's maps as typed Arrow tables, inferred from the
|
|
2
|
+
// values alone by the platform's encoder, compressed with the rest of the
|
|
3
|
+
// stream as one ZSTD frame.
|
|
4
|
+
//
|
|
5
|
+
// A value sits in a slot, and a slot holds one or more variants. A variant is
|
|
6
|
+
// a JSON kind, and for an object its ordered key list plus a slot per field,
|
|
7
|
+
// so values that differ in kind or key set sit side by side rather than in one
|
|
8
|
+
// widened column. A slot of several variants carries a `t` column naming each
|
|
9
|
+
// row's variant.
|
|
10
|
+
//
|
|
11
|
+
// null no column
|
|
12
|
+
// bool uint8, or a constant in the schema
|
|
13
|
+
// num int32 where every value is one, else float64; or a constant
|
|
14
|
+
// str dictionary utf8, or a constant, or (`derive`) a code into
|
|
15
|
+
// rules that rebuild the text from a sibling number field
|
|
16
|
+
// tuple a struct of position slots (one array length)
|
|
17
|
+
// list a list of one item slot
|
|
18
|
+
// obj a struct of field slots
|
|
19
|
+
// json JSON text, where no other variant can carry the value
|
|
20
|
+
//
|
|
21
|
+
// A map is one or more tables. Each holds its rows' keys, their positions in
|
|
22
|
+
// the map as gaps from the row before, and the root slot, so the reader puts
|
|
23
|
+
// every entry back at its position and the map's order is its own.
|
|
24
|
+
//
|
|
25
|
+
// What a read returns is what `JSON.parse` of the value's JSON text returns:
|
|
26
|
+
// the same keys in the same order. Objects are built by cloning a per-variant
|
|
27
|
+
// template; no generated code runs, which a page whose CSP withholds
|
|
28
|
+
// `unsafe-eval` requires.
|
|
29
|
+
//
|
|
30
|
+
// Nothing here is exported from the package. Arrow is loaded only when a
|
|
31
|
+
// frame-4 stream is opened (`loadSealedCodec` in `sealed-store.ts`).
|
|
32
|
+
|
|
33
|
+
import { DictionaryBatch } from 'apache-arrow/fb/dictionary-batch';
|
|
34
|
+
import type { Field } from 'apache-arrow/fb/field';
|
|
35
|
+
import { Footer } from 'apache-arrow/fb/footer';
|
|
36
|
+
import { Message } from 'apache-arrow/fb/message';
|
|
37
|
+
import { MessageHeader } from 'apache-arrow/fb/message-header';
|
|
38
|
+
import { RecordBatch } from 'apache-arrow/fb/record-batch';
|
|
39
|
+
import type { Schema } from 'apache-arrow/fb/schema';
|
|
40
|
+
import { ByteBuffer } from 'flatbuffers';
|
|
41
|
+
|
|
42
|
+
/** The slice of Arrow JS this reader uses. */
|
|
43
|
+
export interface ArrowData {
|
|
44
|
+
type: { typeId: number };
|
|
45
|
+
length: number;
|
|
46
|
+
offset: number;
|
|
47
|
+
values: ArrayLike<number> & { subarray?: (a: number, b: number) => Uint8Array };
|
|
48
|
+
valueOffsets: Int32Array;
|
|
49
|
+
children: ArrowData[];
|
|
50
|
+
dictionary?: { length: number; data: ArrowData[] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ArrowReader {
|
|
54
|
+
tableFromIPC(bytes: Uint8Array): {
|
|
55
|
+
schema: { metadata: Map<string, string> };
|
|
56
|
+
batches: { data: ArrowData }[];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The variants a slot's values take, in the order the `t` column numbers
|
|
61
|
+
* them. */
|
|
62
|
+
export interface Slot {
|
|
63
|
+
v: Variant[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A string derived from a sibling number field: field index `from`, and per
|
|
67
|
+
* code an upper-case flag and a suffix. */
|
|
68
|
+
export interface Derivation {
|
|
69
|
+
from: number;
|
|
70
|
+
rules: [0 | 1, string][];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type Variant =
|
|
74
|
+
| { k: 'null' }
|
|
75
|
+
| { k: 'bool'; c?: boolean }
|
|
76
|
+
| { k: 'num'; int: boolean; c?: number }
|
|
77
|
+
| { k: 'str'; c?: string; derive?: Derivation }
|
|
78
|
+
| { k: 'tuple'; items: Slot[] }
|
|
79
|
+
| { k: 'list'; item: Slot }
|
|
80
|
+
| { k: 'obj'; keys: string[]; fields: Slot[] }
|
|
81
|
+
| { k: 'json' };
|
|
82
|
+
|
|
83
|
+
/** How a table stores its keys: a shared prefix before an int32, two
|
|
84
|
+
* dictionary halves around a separator, utf8 text, or JSON text for a key
|
|
85
|
+
* UTF-8 cannot carry. */
|
|
86
|
+
export type KeyMeta = { prefix: string } | { split: string } | { text: true } | { json: true };
|
|
87
|
+
|
|
88
|
+
export interface TableMeta {
|
|
89
|
+
root: string;
|
|
90
|
+
key: KeyMeta;
|
|
91
|
+
slot: Slot;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The derived-string code for a row whose text no rule rebuilds. */
|
|
95
|
+
export const HELD = 255;
|
|
96
|
+
|
|
97
|
+
/** The table metadata's key in an IPC file's schema metadata. */
|
|
98
|
+
export const TABLE_META_KEY = 'brass:table';
|
|
99
|
+
|
|
100
|
+
/** Whether a variant stores a column. */
|
|
101
|
+
export function variantStored(v: Variant): boolean {
|
|
102
|
+
switch (v.k) {
|
|
103
|
+
case 'null':
|
|
104
|
+
return false;
|
|
105
|
+
case 'bool':
|
|
106
|
+
case 'num':
|
|
107
|
+
case 'str':
|
|
108
|
+
return v.c === undefined;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- parts ---------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
// Each row decodes on its own, so a decoder that strips a leading BOM would
|
|
116
|
+
// take U+FEFF off the start of every value that begins with one.
|
|
117
|
+
const utf8Decoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
118
|
+
|
|
119
|
+
/** Named parts behind a JSON table of names and lengths. */
|
|
120
|
+
export function unpackParts(bytes: Uint8Array): [string, Uint8Array][] {
|
|
121
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
122
|
+
const headLength = view.getUint32(0, true);
|
|
123
|
+
const table = JSON.parse(utf8Decoder.decode(bytes.subarray(4, 4 + headLength))) as [
|
|
124
|
+
string,
|
|
125
|
+
number,
|
|
126
|
+
][];
|
|
127
|
+
let at = 4 + headLength;
|
|
128
|
+
return table.map(([name, length]) => {
|
|
129
|
+
const part = bytes.subarray(at, at + length);
|
|
130
|
+
at += length;
|
|
131
|
+
if (part.byteLength !== length) throw new RangeError(`part ${name} is truncated`);
|
|
132
|
+
return [name, part];
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- checking ------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const ARROW_MAGIC = [0x41, 0x52, 0x52, 0x4f, 0x57, 0x31];
|
|
139
|
+
|
|
140
|
+
/** Throws unless `count` items of `stride` bytes fit in the buffer. */
|
|
141
|
+
function fits(count: number, stride: number, bb: ByteBuffer): void {
|
|
142
|
+
if (!(count >= 0 && count * stride <= bb.capacity())) {
|
|
143
|
+
throw new RangeError('an IPC vector runs past its bytes');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Fields visited, bounded by the buffer, so a field tree whose children
|
|
148
|
+
* point back at shared tables cannot multiply the walk. */
|
|
149
|
+
interface FieldBudget {
|
|
150
|
+
left: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkField(f: Field, bb: ByteBuffer, depth: number, budget: FieldBudget): void {
|
|
154
|
+
if (depth > 64 || (budget.left -= 1) < 0) throw new RangeError('an IPC schema nests too deep');
|
|
155
|
+
fits(f.childrenLength(), 4, bb);
|
|
156
|
+
fits(f.customMetadataLength(), 4, bb);
|
|
157
|
+
for (let i = 0; i < f.childrenLength(); i += 1) {
|
|
158
|
+
const child = f.children(i);
|
|
159
|
+
if (child === null) throw new RangeError('an IPC field is missing');
|
|
160
|
+
checkField(child, bb, depth + 1, budget);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function checkSchema(s: Schema, bb: ByteBuffer): void {
|
|
165
|
+
fits(s.fieldsLength(), 4, bb);
|
|
166
|
+
fits(s.customMetadataLength(), 4, bb);
|
|
167
|
+
fits(s.featuresLength(), 8, bb);
|
|
168
|
+
const budget = { left: bb.capacity() >> 2 };
|
|
169
|
+
for (let i = 0; i < s.fieldsLength(); i += 1) {
|
|
170
|
+
const field = s.fields(i);
|
|
171
|
+
if (field === null) throw new RangeError('an IPC field is missing');
|
|
172
|
+
checkField(field, bb, 0, budget);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function checkBatch(rb: RecordBatch, bb: ByteBuffer): void {
|
|
177
|
+
fits(rb.nodesLength(), 16, bb);
|
|
178
|
+
fits(rb.buffersLength(), 16, bb);
|
|
179
|
+
fits(rb.variadicBufferCountsLength(), 8, bb);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Throws unless an Arrow IPC file's footer and every message it points at
|
|
184
|
+
* stay inside the file. Arrow reads a file by its own offsets and vector
|
|
185
|
+
* lengths, so a footer block naming a message of the wrong kind makes its
|
|
186
|
+
* reader loop, and a declared length makes it allocate that much; both are
|
|
187
|
+
* held to the bytes here before Arrow reads anything.
|
|
188
|
+
*/
|
|
189
|
+
export function checkIpcFile(bytes: Uint8Array): void {
|
|
190
|
+
const n = bytes.byteLength;
|
|
191
|
+
if (n < 22) throw new RangeError('an IPC file is truncated');
|
|
192
|
+
for (let i = 0; i < ARROW_MAGIC.length; i += 1) {
|
|
193
|
+
if (bytes[i] !== ARROW_MAGIC[i] || bytes[n - 6 + i] !== ARROW_MAGIC[i]) {
|
|
194
|
+
throw new RangeError('an IPC file lacks its magic');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, n);
|
|
198
|
+
const footerLength = view.getInt32(n - 10, true);
|
|
199
|
+
if (!(footerLength > 0 && footerLength <= n - 18)) {
|
|
200
|
+
throw new RangeError('an IPC footer runs past its file');
|
|
201
|
+
}
|
|
202
|
+
const bodyEnd = n - 10 - footerLength;
|
|
203
|
+
const fbb = new ByteBuffer(bytes.subarray(bodyEnd, n - 10));
|
|
204
|
+
const footer = Footer.getRootAsFooter(fbb);
|
|
205
|
+
const schema = footer.schema();
|
|
206
|
+
if (schema === null) throw new RangeError('an IPC file has no schema');
|
|
207
|
+
checkSchema(schema, fbb);
|
|
208
|
+
fits(footer.recordBatchesLength(), 24, fbb);
|
|
209
|
+
fits(footer.dictionariesLength(), 24, fbb);
|
|
210
|
+
fits(footer.customMetadataLength(), 4, fbb);
|
|
211
|
+
const blocks: [MessageHeader, number, (i: number) => ReturnType<Footer['dictionaries']>][] = [
|
|
212
|
+
[MessageHeader.DictionaryBatch, footer.dictionariesLength(), (i) => footer.dictionaries(i)],
|
|
213
|
+
[MessageHeader.RecordBatch, footer.recordBatchesLength(), (i) => footer.recordBatches(i)],
|
|
214
|
+
];
|
|
215
|
+
for (const [want, count, blockAt] of blocks) {
|
|
216
|
+
for (let i = 0; i < count; i += 1) {
|
|
217
|
+
const block = blockAt(i);
|
|
218
|
+
if (block === null) throw new RangeError('an IPC block is missing');
|
|
219
|
+
const offset = Number(block.offset());
|
|
220
|
+
const metaLength = block.metaDataLength();
|
|
221
|
+
const bodyLength = Number(block.bodyLength());
|
|
222
|
+
if (
|
|
223
|
+
!(offset >= 8 && metaLength > 8 && bodyLength >= 0 && offset + metaLength + bodyLength <= bodyEnd)
|
|
224
|
+
) {
|
|
225
|
+
throw new RangeError('an IPC block runs past its file');
|
|
226
|
+
}
|
|
227
|
+
if (view.getInt32(offset, true) !== -1) throw new RangeError('an IPC block names no message');
|
|
228
|
+
const length = view.getInt32(offset + 4, true);
|
|
229
|
+
if (!(length > 0 && length <= metaLength - 8)) {
|
|
230
|
+
throw new RangeError('an IPC message runs past its block');
|
|
231
|
+
}
|
|
232
|
+
const mbb = new ByteBuffer(bytes.subarray(offset + 8, offset + 8 + length));
|
|
233
|
+
const message = Message.getRootAsMessage(mbb);
|
|
234
|
+
if (message.headerType() !== want) throw new RangeError('an IPC block names the wrong message');
|
|
235
|
+
if (!(Number(message.bodyLength()) <= bodyLength)) {
|
|
236
|
+
throw new RangeError('an IPC message body runs past its block');
|
|
237
|
+
}
|
|
238
|
+
fits(message.customMetadataLength(), 4, mbb);
|
|
239
|
+
const rb =
|
|
240
|
+
want === MessageHeader.RecordBatch
|
|
241
|
+
? (message.header(new RecordBatch()) as RecordBatch | null)
|
|
242
|
+
: (message.header(new DictionaryBatch()) as DictionaryBatch | null)?.data() ?? null;
|
|
243
|
+
if (rb === null) throw new RangeError('an IPC message has no batch');
|
|
244
|
+
checkBatch(rb, mbb);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
// Arrow's type ids for the column types a sealed table is written with.
|
|
251
|
+
const TYPE_INT = 2;
|
|
252
|
+
const TYPE_FLOAT = 3;
|
|
253
|
+
const TYPE_UTF8 = 5;
|
|
254
|
+
const TYPE_LIST = 12;
|
|
255
|
+
const TYPE_STRUCT = 13;
|
|
256
|
+
const TYPE_DICTIONARY = -1;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Throws unless a stored table's buffers carry every row it declares. A
|
|
260
|
+
* column's length is the uploader's claim, and the reader allocates and loops
|
|
261
|
+
* on it, so each claim is held to the bytes behind it: a fixed-width column
|
|
262
|
+
* has a value per row, offsets rise and stay inside the bytes or child they
|
|
263
|
+
* index (so no two rows share a range, and decoding every row reads each
|
|
264
|
+
* byte once), a struct's fields are each as long as it, and a dictionary is
|
|
265
|
+
* no longer than the column that indexes it. A table's own struct carries
|
|
266
|
+
* the key, gap and value columns.
|
|
267
|
+
*/
|
|
268
|
+
export function checkTableData(data: ArrowData): void {
|
|
269
|
+
if (data.type.typeId !== TYPE_STRUCT || data.children.length !== 3) {
|
|
270
|
+
throw new RangeError('a table is not a key, gap and value struct');
|
|
271
|
+
}
|
|
272
|
+
checkData(data);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function checkData(d: ArrowData): void {
|
|
276
|
+
const n = d.length;
|
|
277
|
+
if (d.offset !== 0 || !(n >= 0)) throw new RangeError('a column starts past its buffer');
|
|
278
|
+
switch (d.type.typeId) {
|
|
279
|
+
case TYPE_INT:
|
|
280
|
+
case TYPE_FLOAT:
|
|
281
|
+
if (d.values.length < n) throw new RangeError('a column is shorter than its rows');
|
|
282
|
+
return;
|
|
283
|
+
case TYPE_UTF8:
|
|
284
|
+
checkOffsets(d.valueOffsets, n, d.values.length);
|
|
285
|
+
return;
|
|
286
|
+
case TYPE_LIST: {
|
|
287
|
+
const child = d.children[0];
|
|
288
|
+
if (child === undefined) throw new RangeError('a list has no items');
|
|
289
|
+
checkOffsets(d.valueOffsets, n, child.length);
|
|
290
|
+
checkData(child);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
case TYPE_STRUCT:
|
|
294
|
+
for (const child of d.children) {
|
|
295
|
+
if (child.length < n) throw new RangeError('a field is shorter than its struct');
|
|
296
|
+
checkData(child);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
case TYPE_DICTIONARY: {
|
|
300
|
+
const dictionary = d.dictionary;
|
|
301
|
+
if (d.values.length < n || dictionary === undefined || dictionary.length > n) {
|
|
302
|
+
throw new RangeError('a dictionary column does not cover its rows');
|
|
303
|
+
}
|
|
304
|
+
checkIndices(d.values, n, dictionary.length);
|
|
305
|
+
for (const chunk of dictionary.data) {
|
|
306
|
+
if (chunk.type.typeId !== TYPE_UTF8) throw new RangeError('a dictionary is not text');
|
|
307
|
+
checkData(chunk);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
throw new RangeError(`a column has type ${d.type.typeId}, which no sealed table is written with`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Throws unless each of the first `n` values indexes into `extent`. */
|
|
316
|
+
function checkIndices(values: ArrayLike<number>, n: number, extent: number): void {
|
|
317
|
+
for (let row = 0; row < n; row += 1) {
|
|
318
|
+
const i = values[row]!;
|
|
319
|
+
if (!(i >= 0 && i < extent)) throw new RangeError('an index runs past what it indexes');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function checkOffsets(offsets: Int32Array, n: number, extent: number): void {
|
|
324
|
+
if (offsets.length < n + 1) throw new RangeError('a column is shorter than its rows');
|
|
325
|
+
let last = offsets[0]!;
|
|
326
|
+
if (last < 0) throw new RangeError('an offset is negative');
|
|
327
|
+
for (let row = 1; row <= n; row += 1) {
|
|
328
|
+
const next = offsets[row]!;
|
|
329
|
+
if (next < last) throw new RangeError('offsets fall');
|
|
330
|
+
last = next;
|
|
331
|
+
}
|
|
332
|
+
if (last > extent) throw new RangeError('an offset runs past its bytes');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- reading -------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
type Obj = Record<string, unknown>;
|
|
338
|
+
export type Reader = (row: number) => unknown;
|
|
339
|
+
|
|
340
|
+
/** What one decode shares across its tables: each object key list's
|
|
341
|
+
* construction site, and each dictionary decoded once. A `lazy` context
|
|
342
|
+
* decodes a dictionary's entries as reads reach them, which suits a keyed
|
|
343
|
+
* read; a walk reads every entry, and decoding them up front is faster. */
|
|
344
|
+
export class ReadContext {
|
|
345
|
+
private readonly shapes = new Map<string, number>();
|
|
346
|
+
private readonly dictionaries = new WeakMap<object, string[]>();
|
|
347
|
+
private readonly lazyDictionaries = new WeakMap<object, LazyDictionary>();
|
|
348
|
+
|
|
349
|
+
constructor(readonly lazy = false) {}
|
|
350
|
+
|
|
351
|
+
strings(dictionary: { data: ArrowData[] }): string[] {
|
|
352
|
+
let strings = this.dictionaries.get(dictionary);
|
|
353
|
+
if (strings === undefined) {
|
|
354
|
+
strings = [];
|
|
355
|
+
for (const chunk of dictionary.data) {
|
|
356
|
+
const at = utf8At(chunk);
|
|
357
|
+
for (let row = 0; row < chunk.length; row += 1) strings.push(at(row));
|
|
358
|
+
}
|
|
359
|
+
this.dictionaries.set(dictionary, strings);
|
|
360
|
+
}
|
|
361
|
+
return strings;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
lazyStrings(dictionary: { data: ArrowData[] }): LazyDictionary {
|
|
365
|
+
let strings = this.lazyDictionaries.get(dictionary);
|
|
366
|
+
if (strings === undefined) {
|
|
367
|
+
strings = new LazyDictionary(dictionary.data);
|
|
368
|
+
this.lazyDictionaries.set(dictionary, strings);
|
|
369
|
+
}
|
|
370
|
+
return strings;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** A flag per dictionary entry equal to `s`, or null where none is. */
|
|
374
|
+
matches(dictionary: { data: ArrowData[] }, s: string): Uint8Array | null {
|
|
375
|
+
return this.lazy ? this.lazyStrings(dictionary).matches(s) : dictionaryMatches(this.strings(dictionary), s);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
shapeId(keys: readonly string[]): number {
|
|
379
|
+
const sig = keys.join('\u0000');
|
|
380
|
+
let id = this.shapes.get(sig);
|
|
381
|
+
if (id === undefined) {
|
|
382
|
+
id = this.shapes.size;
|
|
383
|
+
this.shapes.set(sig, id);
|
|
384
|
+
}
|
|
385
|
+
return id;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** A dictionary whose entries are decoded on first read and kept. */
|
|
390
|
+
export class LazyDictionary {
|
|
391
|
+
readonly length: number;
|
|
392
|
+
private readonly decoded: (string | undefined)[];
|
|
393
|
+
/** Each chunk's first entry. */
|
|
394
|
+
private readonly starts: number[] = [];
|
|
395
|
+
|
|
396
|
+
constructor(private readonly chunks: readonly ArrowData[]) {
|
|
397
|
+
let length = 0;
|
|
398
|
+
for (const chunk of chunks) {
|
|
399
|
+
this.starts.push(length);
|
|
400
|
+
length += chunk.length;
|
|
401
|
+
}
|
|
402
|
+
this.length = length;
|
|
403
|
+
this.decoded = Array.from({ length });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
at(i: number): string {
|
|
407
|
+
let s = this.decoded[i];
|
|
408
|
+
if (s === undefined) {
|
|
409
|
+
let c = this.chunks.length - 1;
|
|
410
|
+
while (c > 0 && this.starts[c]! > i) c -= 1;
|
|
411
|
+
const chunk = this.chunks[c]!;
|
|
412
|
+
const row = i - this.starts[c]!;
|
|
413
|
+
const offsets = chunk.valueOffsets;
|
|
414
|
+
s = utf8Decoder.decode((chunk.values as Uint8Array).subarray(offsets[row]!, offsets[row + 1]!));
|
|
415
|
+
this.decoded[i] = s;
|
|
416
|
+
}
|
|
417
|
+
return s;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** A flag per entry equal to `s`, found by comparing bytes, or null where
|
|
421
|
+
* none is. */
|
|
422
|
+
matches(s: string): Uint8Array | null {
|
|
423
|
+
// An entry reads with replacement characters where its bytes are not
|
|
424
|
+
// UTF-8, so a wanted string carrying one is compared as text.
|
|
425
|
+
if (s.includes('\uFFFD')) return dictionaryMatches(Array.from({ length: this.length }, (_, i) => this.at(i)), s);
|
|
426
|
+
// Every entry decodes well formed, so a string that is not equals none.
|
|
427
|
+
if (!wellFormed(s)) return null;
|
|
428
|
+
const want = utf8Encoder.encode(s);
|
|
429
|
+
const length = want.length;
|
|
430
|
+
let flags: Uint8Array | null = null;
|
|
431
|
+
this.chunks.forEach((chunk, c) => {
|
|
432
|
+
const bytes = chunk.values as Uint8Array;
|
|
433
|
+
const offsets = chunk.valueOffsets;
|
|
434
|
+
for (let row = 0; row < chunk.length; row += 1) {
|
|
435
|
+
const start = offsets[row]!;
|
|
436
|
+
if (offsets[row + 1]! - start !== length) continue;
|
|
437
|
+
let i = 0;
|
|
438
|
+
while (i < length && bytes[start + i] === want[i]) i += 1;
|
|
439
|
+
if (i === length) (flags ??= new Uint8Array(this.length))[this.starts[c]! + row] = 1;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
return flags;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function utf8At(d: ArrowData): (row: number) => string {
|
|
447
|
+
const bytes = d.values as Uint8Array;
|
|
448
|
+
const offsets = d.valueOffsets;
|
|
449
|
+
return (row) => utf8Decoder.decode(bytes.subarray(offsets[row]!, offsets[row + 1]!));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Keys are compared by their bytes on a keyed read, so a key whose bytes are
|
|
453
|
+
// not UTF-8 is refused rather than read with replacement characters.
|
|
454
|
+
const keyDecoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* A table's keys as text, row by row. Each key reads only in the form the
|
|
458
|
+
* encoder writes it (a non-negative integer id, a separator that falls after
|
|
459
|
+
* the first half, UTF-8 text, JSON text as `JSON.stringify` writes it), so a
|
|
460
|
+
* walk and a keyed read (`rowsWithKey`) agree on every key; any other form
|
|
461
|
+
* throws.
|
|
462
|
+
*/
|
|
463
|
+
export function keyReader(meta: TableMeta, d: ArrowData, cx: ReadContext): (row: number) => string {
|
|
464
|
+
const key = meta.key;
|
|
465
|
+
if ('prefix' in key) {
|
|
466
|
+
const prefix = key.prefix;
|
|
467
|
+
const ids = d.values;
|
|
468
|
+
if (typeof prefix !== 'string') throw new RangeError('a key prefix is not text');
|
|
469
|
+
for (let row = 0; row < d.length; row += 1) {
|
|
470
|
+
const id = ids[row]!;
|
|
471
|
+
if (!(Number.isInteger(id) && id >= 0)) throw new RangeError('a key id is not a whole number');
|
|
472
|
+
}
|
|
473
|
+
return (row) => prefix + String(ids[row]);
|
|
474
|
+
}
|
|
475
|
+
if ('split' in key) {
|
|
476
|
+
const [a, b] = d.children as [ArrowData, ArrowData];
|
|
477
|
+
if (cx.lazy) return lazySplitKeyReader(key.split, a, b, cx);
|
|
478
|
+
const as = cx.strings(a.dictionary!);
|
|
479
|
+
const bs = cx.strings(b.dictionary!);
|
|
480
|
+
const ai = a.values;
|
|
481
|
+
const bi = b.values;
|
|
482
|
+
const sep = key.split;
|
|
483
|
+
if (typeof sep !== 'string' || sep === '' || as.some((s) => (s + sep).indexOf(sep) !== s.length)) {
|
|
484
|
+
throw new RangeError('a key separator does not split its keys');
|
|
485
|
+
}
|
|
486
|
+
return (row) => as[ai[row]!] + sep + bs[bi[row]!];
|
|
487
|
+
}
|
|
488
|
+
const bytes = d.values as Uint8Array;
|
|
489
|
+
const offsets = d.valueOffsets;
|
|
490
|
+
const at = (row: number): string => keyDecoder.decode(bytes.subarray(offsets[row]!, offsets[row + 1]!));
|
|
491
|
+
if ('json' in key) {
|
|
492
|
+
return (row) => {
|
|
493
|
+
const text = at(row);
|
|
494
|
+
const k: unknown = JSON.parse(text);
|
|
495
|
+
if (typeof k !== 'string' || JSON.stringify(k) !== text) throw new RangeError('a JSON key is not as written');
|
|
496
|
+
return k;
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return at;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Split keys read through lazy dictionaries. Each first half is checked
|
|
503
|
+
* against the separator when a read first reaches it. */
|
|
504
|
+
function lazySplitKeyReader(sep: unknown, a: ArrowData, b: ArrowData, cx: ReadContext): (row: number) => string {
|
|
505
|
+
if (typeof sep !== 'string' || sep === '') throw new RangeError('a key separator does not split its keys');
|
|
506
|
+
const as = cx.lazyStrings(a.dictionary!);
|
|
507
|
+
const bs = cx.lazyStrings(b.dictionary!);
|
|
508
|
+
const ai = a.values;
|
|
509
|
+
const bi = b.values;
|
|
510
|
+
const checked = new Uint8Array(as.length);
|
|
511
|
+
return (row) => {
|
|
512
|
+
const i = ai[row]!;
|
|
513
|
+
const first = as.at(i);
|
|
514
|
+
if (checked[i] === 0) {
|
|
515
|
+
if ((first + sep).indexOf(sep) !== first.length) throw new RangeError('a key separator does not split its keys');
|
|
516
|
+
checked[i] = 1;
|
|
517
|
+
}
|
|
518
|
+
return first + sep + bs.at(bi[row]!);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const utf8Encoder = new TextEncoder();
|
|
523
|
+
|
|
524
|
+
/** Whether a string is well-formed UTF-16, which UTF-8 can carry. */
|
|
525
|
+
const wellFormed = (s: string): boolean => (s as unknown as { isWellFormed(): boolean }).isWellFormed();
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* The rows of a table whose key reads as `key`, found from the key column
|
|
529
|
+
* alone: an id compared as a number, a dictionary half by its index, text by
|
|
530
|
+
* its bytes. Nothing is decoded for a row that does not match.
|
|
531
|
+
*/
|
|
532
|
+
export function rowsWithKey(meta: TableMeta, d: ArrowData, key: string, cx: ReadContext): number[] {
|
|
533
|
+
const rows: number[] = [];
|
|
534
|
+
const n = d.length;
|
|
535
|
+
const k = meta.key;
|
|
536
|
+
if ('prefix' in k) {
|
|
537
|
+
if (!key.startsWith(k.prefix)) return rows;
|
|
538
|
+
const digits = key.slice(k.prefix.length);
|
|
539
|
+
const id = Number(digits);
|
|
540
|
+
if (digits === '' || String(id) !== digits) return rows;
|
|
541
|
+
const ids = d.values;
|
|
542
|
+
for (let row = 0; row < n; row += 1) if (ids[row] === id) rows.push(row);
|
|
543
|
+
return rows;
|
|
544
|
+
}
|
|
545
|
+
if ('split' in k) {
|
|
546
|
+
const at = key.indexOf(k.split);
|
|
547
|
+
if (at < 0) return rows;
|
|
548
|
+
const [a, b] = d.children as [ArrowData, ArrowData];
|
|
549
|
+
const inA = cx.matches(a.dictionary!, key.slice(0, at));
|
|
550
|
+
const inB = cx.matches(b.dictionary!, key.slice(at + k.split.length));
|
|
551
|
+
if (inA === null || inB === null) return rows;
|
|
552
|
+
const ai = a.values;
|
|
553
|
+
const bi = b.values;
|
|
554
|
+
for (let row = 0; row < n; row += 1) if (inA[ai[row]!] === 1 && inB[bi[row]!] === 1) rows.push(row);
|
|
555
|
+
return rows;
|
|
556
|
+
}
|
|
557
|
+
// Text keys are written well formed, and JSON keys as `JSON.stringify` writes them.
|
|
558
|
+
if ('text' in k && !wellFormed(key)) return rows;
|
|
559
|
+
const want = utf8Encoder.encode('json' in k ? JSON.stringify(key) : key);
|
|
560
|
+
const bytes = d.values as Uint8Array;
|
|
561
|
+
const offsets = d.valueOffsets;
|
|
562
|
+
const length = want.length;
|
|
563
|
+
for (let row = 0; row < n; row += 1) {
|
|
564
|
+
const start = offsets[row]!;
|
|
565
|
+
if (offsets[row + 1]! - start !== length) continue;
|
|
566
|
+
let i = 0;
|
|
567
|
+
while (i < length && bytes[start + i] === want[i]) i += 1;
|
|
568
|
+
if (i === length) rows.push(row);
|
|
569
|
+
}
|
|
570
|
+
return rows;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** A flag per dictionary entry equal to `s`, or null where none is. */
|
|
574
|
+
function dictionaryMatches(strings: readonly string[], s: string): Uint8Array | null {
|
|
575
|
+
let flags: Uint8Array | null = null;
|
|
576
|
+
for (let i = 0; i < strings.length; i += 1) {
|
|
577
|
+
if (strings[i] === s) (flags ??= new Uint8Array(strings.length))[i] = 1;
|
|
578
|
+
}
|
|
579
|
+
return flags;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function readSlot(slot: Slot, d: ArrowData, cx: ReadContext): Reader {
|
|
583
|
+
if (d.offset !== 0) throw new RangeError('a slot column starts past its buffer');
|
|
584
|
+
let c = 0;
|
|
585
|
+
const tags = slot.v.length > 1 ? d.children[c++]!.values : null;
|
|
586
|
+
const readers = slot.v.map((v) => readVariant(v, variantStored(v) ? d.children[c++]! : null, cx));
|
|
587
|
+
if (tags === null) return readers[0]!;
|
|
588
|
+
checkIndices(tags, d.length, readers.length);
|
|
589
|
+
return (row) => readers[tags[row]!]!(row);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function readVariant(v: Variant, d: ArrowData | null, cx: ReadContext): Reader {
|
|
593
|
+
switch (v.k) {
|
|
594
|
+
case 'null':
|
|
595
|
+
return () => null;
|
|
596
|
+
case 'bool': {
|
|
597
|
+
if (v.c !== undefined) {
|
|
598
|
+
const c = v.c;
|
|
599
|
+
return () => c;
|
|
600
|
+
}
|
|
601
|
+
const values = d!.values;
|
|
602
|
+
return (row) => values[row] === 1;
|
|
603
|
+
}
|
|
604
|
+
case 'num': {
|
|
605
|
+
if (v.c !== undefined) {
|
|
606
|
+
const c = v.c;
|
|
607
|
+
return () => c;
|
|
608
|
+
}
|
|
609
|
+
const values = d!.values;
|
|
610
|
+
return (row) => values[row];
|
|
611
|
+
}
|
|
612
|
+
case 'str': {
|
|
613
|
+
if (v.c !== undefined) {
|
|
614
|
+
const c = v.c;
|
|
615
|
+
return () => c;
|
|
616
|
+
}
|
|
617
|
+
if (v.derive) throw new TypeError('a derived string is read through its object');
|
|
618
|
+
const idx = d!.values;
|
|
619
|
+
if (cx.lazy) {
|
|
620
|
+
const lazy = cx.lazyStrings(d!.dictionary!);
|
|
621
|
+
return (row) => lazy.at(idx[row]!);
|
|
622
|
+
}
|
|
623
|
+
const strings = cx.strings(d!.dictionary!);
|
|
624
|
+
return (row) => strings[idx[row]!];
|
|
625
|
+
}
|
|
626
|
+
case 'json': {
|
|
627
|
+
const at = utf8At(d!);
|
|
628
|
+
return (row) => JSON.parse(at(row)) as unknown;
|
|
629
|
+
}
|
|
630
|
+
case 'tuple':
|
|
631
|
+
return tupleReader(v.items.map((s, p) => readSlot(s, d!.children[p]!, cx)));
|
|
632
|
+
case 'list': {
|
|
633
|
+
const offsets = d!.valueOffsets;
|
|
634
|
+
const item = readSlot(v.item, d!.children[0]!, cx);
|
|
635
|
+
return (row) => {
|
|
636
|
+
const out: unknown[] = [];
|
|
637
|
+
for (let j = offsets[row]!; j < offsets[row + 1]!; j += 1) out.push(item(j));
|
|
638
|
+
return out;
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
case 'obj': {
|
|
642
|
+
// The encoder writes an object whose keys assign onto `{}` in order as
|
|
643
|
+
// JSON text instead, so a field named `__proto__` is not a table's.
|
|
644
|
+
if (v.keys.includes('__proto__') || v.keys.length !== v.fields.length) {
|
|
645
|
+
throw new RangeError('an object variant names a key no table carries');
|
|
646
|
+
}
|
|
647
|
+
// Fields every row agrees on ride in the template the object is cloned
|
|
648
|
+
// from; the rest are read per row.
|
|
649
|
+
const template: Obj = {};
|
|
650
|
+
const varying: string[] = [];
|
|
651
|
+
const readers: Reader[] = [];
|
|
652
|
+
v.fields.forEach((s, f) => {
|
|
653
|
+
const key = v.keys[f]!;
|
|
654
|
+
const only = s.v.length === 1 ? s.v[0]! : null;
|
|
655
|
+
if (only?.k === 'null') template[key] = null;
|
|
656
|
+
else if (
|
|
657
|
+
(only?.k === 'num' || only?.k === 'str' || only?.k === 'bool') &&
|
|
658
|
+
only.c !== undefined
|
|
659
|
+
) {
|
|
660
|
+
template[key] = only.c;
|
|
661
|
+
} else {
|
|
662
|
+
template[key] = null;
|
|
663
|
+
varying.push(key);
|
|
664
|
+
const derive = only?.k === 'str' ? only.derive : undefined;
|
|
665
|
+
readers.push(
|
|
666
|
+
derive === undefined
|
|
667
|
+
? readSlot(s, d!.children[f]!, cx)
|
|
668
|
+
: derivedReader(
|
|
669
|
+
derive,
|
|
670
|
+
d!.children[f]!.children[0]!,
|
|
671
|
+
d!.children[derive.from]!.children[0]!.values,
|
|
672
|
+
),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
return objectReader(cx.shapeId(v.keys), template, varying, readers);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function derivedReader(derive: Derivation, d: ArrowData, numbers: ArrayLike<number>): Reader {
|
|
682
|
+
const codes = d.children[0]!.values;
|
|
683
|
+
for (let row = 0; row < d.length; row += 1) {
|
|
684
|
+
const code = codes[row]!;
|
|
685
|
+
if (code !== HELD && !(code >= 0 && code < derive.rules.length)) {
|
|
686
|
+
throw new RangeError('a derived string names no rule');
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const held = utf8At(d.children[1]!);
|
|
690
|
+
const upper = derive.rules.map((r) => r[0] === 1);
|
|
691
|
+
const suffix = derive.rules.map((r) => r[1]);
|
|
692
|
+
return (row) => {
|
|
693
|
+
const code = codes[row]!;
|
|
694
|
+
if (code === HELD) return held(row);
|
|
695
|
+
const s = String(numbers[row]);
|
|
696
|
+
return (upper[code] ? s.toUpperCase() : s) + suffix[code]!;
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// An array literal allocates at its length, where pushing grows the array.
|
|
701
|
+
const TUPLES: ((rs: Reader[]) => Reader)[] = [
|
|
702
|
+
() => () => [],
|
|
703
|
+
([r0]) => (row) => [r0!(row)],
|
|
704
|
+
([r0, r1]) => (row) => [r0!(row), r1!(row)],
|
|
705
|
+
([r0, r1, r2]) => (row) => [r0!(row), r1!(row), r2!(row)],
|
|
706
|
+
([r0, r1, r2, r3]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row)],
|
|
707
|
+
([r0, r1, r2, r3, r4]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row)],
|
|
708
|
+
([r0, r1, r2, r3, r4, r5]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row)],
|
|
709
|
+
([r0, r1, r2, r3, r4, r5, r6]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row), r6!(row)],
|
|
710
|
+
([r0, r1, r2, r3, r4, r5, r6, r7]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row), r6!(row), r7!(row)],
|
|
711
|
+
([r0, r1, r2, r3, r4, r5, r6, r7, r8]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row), r6!(row), r7!(row), r8!(row)],
|
|
712
|
+
([r0, r1, r2, r3, r4, r5, r6, r7, r8, r9]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row), r6!(row), r7!(row), r8!(row), r9!(row)],
|
|
713
|
+
([r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row), r6!(row), r7!(row), r8!(row), r9!(row), r10!(row)],
|
|
714
|
+
([r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11]) => (row) => [r0!(row), r1!(row), r2!(row), r3!(row), r4!(row), r5!(row), r6!(row), r7!(row), r8!(row), r9!(row), r10!(row), r11!(row)],
|
|
715
|
+
];
|
|
716
|
+
|
|
717
|
+
function tupleReader(rs: Reader[]): Reader {
|
|
718
|
+
const literal = TUPLES[rs.length];
|
|
719
|
+
if (literal !== undefined) return literal(rs);
|
|
720
|
+
const n = rs.length;
|
|
721
|
+
return (row) => {
|
|
722
|
+
const out: unknown[] = [];
|
|
723
|
+
for (let p = 0; p < n; p += 1) out.push(rs[p]!(row));
|
|
724
|
+
return out;
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
type Maker = (template: Obj, keys: string[], readers: Reader[]) => Reader;
|
|
729
|
+
|
|
730
|
+
// One construction site per object shape. V8 keeps type feedback per source
|
|
731
|
+
// site, and a site that clones one template and stores one set of keys stays
|
|
732
|
+
// monomorphic, where one shared site sees every shape in the stream and runs
|
|
733
|
+
// at about half the speed. Each array holds distinct copies of one site, by
|
|
734
|
+
// the number of fields a row reads; shape `id` takes copy `id`, and a shape
|
|
735
|
+
// past the copies takes the shared loop.
|
|
736
|
+
const SITES0: Maker[] = [
|
|
737
|
+
(t) => () => ({ ...t }),
|
|
738
|
+
(t) => () => ({ ...t }),
|
|
739
|
+
(t) => () => ({ ...t }),
|
|
740
|
+
(t) => () => ({ ...t }),
|
|
741
|
+
(t) => () => ({ ...t }),
|
|
742
|
+
(t) => () => ({ ...t }),
|
|
743
|
+
(t) => () => ({ ...t }),
|
|
744
|
+
(t) => () => ({ ...t }),
|
|
745
|
+
(t) => () => ({ ...t }),
|
|
746
|
+
(t) => () => ({ ...t }),
|
|
747
|
+
(t) => () => ({ ...t }),
|
|
748
|
+
(t) => () => ({ ...t }),
|
|
749
|
+
(t) => () => ({ ...t }),
|
|
750
|
+
(t) => () => ({ ...t }),
|
|
751
|
+
(t) => () => ({ ...t }),
|
|
752
|
+
(t) => () => ({ ...t }),
|
|
753
|
+
];
|
|
754
|
+
const SITES1: Maker[] = [
|
|
755
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
756
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
757
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
758
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
759
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
760
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
761
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
762
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
763
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
764
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
765
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
766
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
767
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
768
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
769
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
770
|
+
(t, [k0], [r0]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); return o; },
|
|
771
|
+
];
|
|
772
|
+
const SITES2: Maker[] = [
|
|
773
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
774
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
775
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
776
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
777
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
778
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
779
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
780
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
781
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
782
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
783
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
784
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
785
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
786
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
787
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
788
|
+
(t, [k0, k1], [r0, r1]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); return o; },
|
|
789
|
+
];
|
|
790
|
+
const SITES3: Maker[] = [
|
|
791
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
792
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
793
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
794
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
795
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
796
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
797
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
798
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
799
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
800
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
801
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
802
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
803
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
804
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
805
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
806
|
+
(t, [k0, k1, k2], [r0, r1, r2]) => (row) => { const o = { ...t }; o[k0!] = r0!(row); o[k1!] = r1!(row); o[k2!] = r2!(row); return o; },
|
|
807
|
+
];
|
|
808
|
+
const SITES: Maker[][] = [SITES0, SITES1, SITES2, SITES3];
|
|
809
|
+
|
|
810
|
+
function objectReader(id: number, template: Obj, varying: string[], rs: Reader[]): Reader {
|
|
811
|
+
const site = SITES[varying.length]?.[id];
|
|
812
|
+
if (site !== undefined) return site(template, varying, rs);
|
|
813
|
+
const n = varying.length;
|
|
814
|
+
return (row) => {
|
|
815
|
+
const o: Obj = { ...template };
|
|
816
|
+
for (let f = 0; f < n; f += 1) o[varying[f]!] = rs[f]!(row);
|
|
817
|
+
return o;
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// --- a map's tables ------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
/** One table of a map, ready to read: each row's position in the map, its
|
|
824
|
+
* key, and its value. */
|
|
825
|
+
export interface ReadyTable {
|
|
826
|
+
positions: Int32Array;
|
|
827
|
+
key: (row: number) => string;
|
|
828
|
+
value: Reader;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** A stored table's rows prepared for reading. Positions are absolute, and a
|
|
832
|
+
* table's gap column runs from zero. */
|
|
833
|
+
export function readyTable(meta: TableMeta, data: ArrowData, cx: ReadContext): ReadyTable {
|
|
834
|
+
const [keyCol, gapCol, rootCol] = data.children as [ArrowData, ArrowData, ArrowData];
|
|
835
|
+
const positions = new Int32Array(data.length);
|
|
836
|
+
const gaps = gapCol.values;
|
|
837
|
+
let at = 0;
|
|
838
|
+
for (let row = 0; row < data.length; row += 1) {
|
|
839
|
+
at += gaps[row]!;
|
|
840
|
+
positions[row] = at;
|
|
841
|
+
}
|
|
842
|
+
return { positions, key: keyReader(meta, keyCol, cx), value: readSlot(meta.slot, rootCol, cx) };
|
|
843
|
+
}
|