@brass-build/client 0.4.1 → 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 +124 -45
- package/CHANGELOG.md +80 -4
- package/README.md +666 -179
- 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 +48 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +918 -115
- 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/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 +1022 -112
- 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,876 @@
|
|
|
1
|
+
// The stored form's tables, built from sealed content or from an importer's
|
|
2
|
+
// output directly. Portable: no Node API, so the same encoder runs
|
|
3
|
+
// wherever an importer runs (the platform's worker, a browser tab, the CLI),
|
|
4
|
+
// and only the compression around its output differs by place.
|
|
5
|
+
//
|
|
6
|
+
// It sees each map's (key, plain JSON value) pairs in stored order and infers
|
|
7
|
+
// typed Arrow tables from them (the slot and variant model is described in
|
|
8
|
+
// `sealed-arrow.ts`, which reads what this writes). Nothing here names a
|
|
9
|
+
// format. A map is encoded in chunks of stored positions, each chunk its own
|
|
10
|
+
// tables, so the values held parsed at once are one chunk's however large the
|
|
11
|
+
// map.
|
|
12
|
+
//
|
|
13
|
+
// @internal
|
|
14
|
+
import { Dictionary, Field, Float64, Int32, List, RecordBatch, Schema, Struct, Table, Uint8, Utf8, makeData, makeVector, tableToIPC, } from 'apache-arrow';
|
|
15
|
+
import { ARROW_MANIFEST_PART, SEALED_INNER_MAX_BYTES, SealedContentTooLargeError, VERSION_ARROW_STAGED, arrowFrameBytes, planSealedContent, planSealedStore, } from './sealed-store.js';
|
|
16
|
+
import { UNPAGED_MAX_RECORDS } from './record-paging.js';
|
|
17
|
+
import { gzipSync } from 'fflate';
|
|
18
|
+
import { HELD, TABLE_META_KEY, } from './sealed-arrow.js';
|
|
19
|
+
/** Map entries a chunk holds. A chunk's values are held parsed while its
|
|
20
|
+
* tables are built; the next chunk's tables follow in the same frame. */
|
|
21
|
+
export const SEALED_CHUNK_ENTRIES = 1 << 17;
|
|
22
|
+
const MAX_VARIANTS = 16;
|
|
23
|
+
const MAX_TUPLE = 64;
|
|
24
|
+
const MAX_DEPTH = 32;
|
|
25
|
+
const MAX_TABLES = 1024;
|
|
26
|
+
const MIN_TABLE_ROWS = 8;
|
|
27
|
+
const MAX_RULES = 255;
|
|
28
|
+
const MAX_SUFFIX = 16;
|
|
29
|
+
function kindOf(v) {
|
|
30
|
+
if (v === null)
|
|
31
|
+
return 'null';
|
|
32
|
+
switch (typeof v) {
|
|
33
|
+
case 'boolean':
|
|
34
|
+
return 'bool';
|
|
35
|
+
case 'number':
|
|
36
|
+
return 'num';
|
|
37
|
+
case 'string':
|
|
38
|
+
return 'str';
|
|
39
|
+
case 'object':
|
|
40
|
+
return Array.isArray(v) ? 'arr' : 'obj';
|
|
41
|
+
}
|
|
42
|
+
throw new TypeError(`a ${typeof v} is not a JSON value`);
|
|
43
|
+
}
|
|
44
|
+
/** A value's variant signature: its kind, and for an object its keys, with
|
|
45
|
+
* each field's kind where `fieldKinds`. */
|
|
46
|
+
function signature(v, fieldKinds) {
|
|
47
|
+
const kind = kindOf(v);
|
|
48
|
+
if (kind !== 'obj')
|
|
49
|
+
return kind;
|
|
50
|
+
const o = v;
|
|
51
|
+
// Each key is length-prefixed, so no key can read as a boundary.
|
|
52
|
+
let s = 'obj';
|
|
53
|
+
for (const key in o)
|
|
54
|
+
s += fieldKinds ? `${key.length}:${key}${kindOf(o[key])};` : `${key.length}:${key};`;
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
/** Indices of `values` by signature, in first-seen order. */
|
|
58
|
+
function groupBy(values, fieldKinds) {
|
|
59
|
+
const groups = new Map();
|
|
60
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
61
|
+
const s = signature(values[i], fieldKinds);
|
|
62
|
+
let list = groups.get(s);
|
|
63
|
+
if (list === undefined)
|
|
64
|
+
groups.set(s, (list = []));
|
|
65
|
+
list.push(i);
|
|
66
|
+
}
|
|
67
|
+
return groups;
|
|
68
|
+
}
|
|
69
|
+
/** Whether a string is well-formed UTF-16, which UTF-8 can carry. */
|
|
70
|
+
const wellFormed = (s) => s.isWellFormed();
|
|
71
|
+
/** Whether an object's keys survive assignment in order onto `{}`. */
|
|
72
|
+
const assignable = (keys) => keys.every((k) => k !== '__proto__' && wellFormed(k));
|
|
73
|
+
const utf8 = new TextEncoder();
|
|
74
|
+
function utf8Column(strings) {
|
|
75
|
+
const offsets = new Int32Array(strings.length + 1);
|
|
76
|
+
const parts = strings.map((s) => utf8.encode(s));
|
|
77
|
+
parts.forEach((p, i) => (offsets[i + 1] = offsets[i] + p.byteLength));
|
|
78
|
+
const bytes = new Uint8Array(offsets[strings.length]);
|
|
79
|
+
parts.forEach((p, i) => bytes.set(p, offsets[i]));
|
|
80
|
+
const type = new Utf8();
|
|
81
|
+
return [type, makeData({ type, length: strings.length, valueOffsets: offsets, data: bytes })];
|
|
82
|
+
}
|
|
83
|
+
function int32Column(data) {
|
|
84
|
+
const type = new Int32();
|
|
85
|
+
return [type, makeData({ type, length: data.length, data })];
|
|
86
|
+
}
|
|
87
|
+
function uint8Column(data) {
|
|
88
|
+
const type = new Uint8();
|
|
89
|
+
return [type, makeData({ type, length: data.length, data })];
|
|
90
|
+
}
|
|
91
|
+
function float64Column(data) {
|
|
92
|
+
const type = new Float64();
|
|
93
|
+
return [type, makeData({ type, length: data.length, data })];
|
|
94
|
+
}
|
|
95
|
+
function struct(names, columns, length) {
|
|
96
|
+
const type = new Struct(columns.map((c, i) => new Field(names[i], c[0], true)));
|
|
97
|
+
return [
|
|
98
|
+
type,
|
|
99
|
+
makeData({ type, length, children: columns.map((c) => c[1]) }),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/** A row's position in the slot's columns: `at[i]`, or `i` where dense. */
|
|
103
|
+
const rowOf = (at, i) => (at === null ? i : at[i]);
|
|
104
|
+
class Writer {
|
|
105
|
+
nextDictionary = 0;
|
|
106
|
+
stats = { tables: 0, values: 0, jsonValues: 0, rawBytes: 0 };
|
|
107
|
+
dictionary(strings) {
|
|
108
|
+
const index = new Map();
|
|
109
|
+
const indices = new Int32Array(strings.length);
|
|
110
|
+
strings.forEach((s, row) => {
|
|
111
|
+
if (s === undefined)
|
|
112
|
+
return;
|
|
113
|
+
let at = index.get(s);
|
|
114
|
+
if (at === undefined)
|
|
115
|
+
index.set(s, (at = index.size));
|
|
116
|
+
indices[row] = at;
|
|
117
|
+
});
|
|
118
|
+
const type = new Dictionary(new Utf8(), new Int32(), this.nextDictionary++);
|
|
119
|
+
const dictionary = makeVector(utf8Column([...index.keys()])[1]);
|
|
120
|
+
return [type, makeData({ type, length: strings.length, data: indices, dictionary })];
|
|
121
|
+
}
|
|
122
|
+
json(values, idx, at, length) {
|
|
123
|
+
const strings = Array.from({ length }, () => '');
|
|
124
|
+
for (const i of idx) {
|
|
125
|
+
strings[rowOf(at, i)] = JSON.stringify(values[i]);
|
|
126
|
+
this.stats.jsonValues += 1;
|
|
127
|
+
}
|
|
128
|
+
return utf8Column(strings);
|
|
129
|
+
}
|
|
130
|
+
slot(values, at, length, depth) {
|
|
131
|
+
const all = values.map((_, i) => i);
|
|
132
|
+
if (depth > MAX_DEPTH)
|
|
133
|
+
return this.jsonSlot(values, all, at, length);
|
|
134
|
+
let groups = groupBy(values, true);
|
|
135
|
+
if (groups.size > MAX_VARIANTS)
|
|
136
|
+
groups = groupBy(values, false);
|
|
137
|
+
if (groups.size > MAX_VARIANTS)
|
|
138
|
+
return this.jsonSlot(values, all, at, length);
|
|
139
|
+
const variants = [];
|
|
140
|
+
const names = [];
|
|
141
|
+
const columns = [];
|
|
142
|
+
let tags = null;
|
|
143
|
+
if (groups.size > 1) {
|
|
144
|
+
tags = new Uint8Array(length);
|
|
145
|
+
names.push('t');
|
|
146
|
+
columns.push(uint8Column(tags));
|
|
147
|
+
}
|
|
148
|
+
for (const [sig, idx] of groups) {
|
|
149
|
+
const vi = variants.length;
|
|
150
|
+
if (tags)
|
|
151
|
+
for (const i of idx)
|
|
152
|
+
tags[rowOf(at, i)] = vi;
|
|
153
|
+
const built = this.variant(sig.startsWith('obj') ? 'obj' : sig, values, idx, at, length, depth);
|
|
154
|
+
variants.push(built.variant);
|
|
155
|
+
if (built.column) {
|
|
156
|
+
names.push(`v${vi}`);
|
|
157
|
+
columns.push(built.column);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { slot: { v: variants }, column: struct(names, columns, length) };
|
|
161
|
+
}
|
|
162
|
+
jsonSlot(values, idx, at, length) {
|
|
163
|
+
return { slot: { v: [{ k: 'json' }] }, column: struct(['v0'], [this.json(values, idx, at, length)], length) };
|
|
164
|
+
}
|
|
165
|
+
variant(kind, values, idx, at, length, depth) {
|
|
166
|
+
const first = values[idx[0]];
|
|
167
|
+
const same = () => idx.every((i) => values[i] === first);
|
|
168
|
+
switch (kind) {
|
|
169
|
+
case 'null':
|
|
170
|
+
return { variant: { k: 'null' } };
|
|
171
|
+
case 'bool': {
|
|
172
|
+
if (same())
|
|
173
|
+
return { variant: { k: 'bool', c: first } };
|
|
174
|
+
const col = new Uint8Array(length);
|
|
175
|
+
for (const i of idx)
|
|
176
|
+
col[rowOf(at, i)] = values[i] ? 1 : 0;
|
|
177
|
+
return { variant: { k: 'bool' }, column: uint8Column(col) };
|
|
178
|
+
}
|
|
179
|
+
case 'num': {
|
|
180
|
+
if (same())
|
|
181
|
+
return { variant: { k: 'num', int: false, c: first } };
|
|
182
|
+
const int = idx.every((i) => {
|
|
183
|
+
const n = values[i];
|
|
184
|
+
return Number.isInteger(n) && n >= -0x80000000 && n <= 0x7fffffff && !Object.is(n, -0);
|
|
185
|
+
});
|
|
186
|
+
if (int) {
|
|
187
|
+
const col = new Int32Array(length);
|
|
188
|
+
for (const i of idx)
|
|
189
|
+
col[rowOf(at, i)] = values[i];
|
|
190
|
+
return { variant: { k: 'num', int }, column: int32Column(col) };
|
|
191
|
+
}
|
|
192
|
+
const col = new Float64Array(length);
|
|
193
|
+
for (const i of idx)
|
|
194
|
+
col[rowOf(at, i)] = values[i];
|
|
195
|
+
return { variant: { k: 'num', int }, column: float64Column(col) };
|
|
196
|
+
}
|
|
197
|
+
case 'str': {
|
|
198
|
+
if (!idx.every((i) => wellFormed(values[i]))) {
|
|
199
|
+
return { variant: { k: 'json' }, column: this.json(values, idx, at, length) };
|
|
200
|
+
}
|
|
201
|
+
if (same())
|
|
202
|
+
return { variant: { k: 'str', c: first } };
|
|
203
|
+
const strings = Array.from({ length }, () => undefined);
|
|
204
|
+
for (const i of idx)
|
|
205
|
+
strings[rowOf(at, i)] = values[i];
|
|
206
|
+
return { variant: { k: 'str' }, column: this.dictionary(strings) };
|
|
207
|
+
}
|
|
208
|
+
case 'arr':
|
|
209
|
+
return this.array(values, idx, at, length, depth);
|
|
210
|
+
case 'obj':
|
|
211
|
+
return this.object(values, idx, at, length, depth);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
array(values, idx, at, length, depth) {
|
|
215
|
+
const width = values[idx[0]].length;
|
|
216
|
+
const rows = Int32Array.from(idx, (i) => rowOf(at, i));
|
|
217
|
+
if (width <= MAX_TUPLE && idx.every((i) => values[i].length === width)) {
|
|
218
|
+
const items = [];
|
|
219
|
+
const columns = [];
|
|
220
|
+
for (let p = 0; p < width; p += 1) {
|
|
221
|
+
const built = this.slot(idx.map((i) => values[i][p]), rows, length, depth + 1);
|
|
222
|
+
items.push(built.slot);
|
|
223
|
+
columns.push(built.column);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
variant: { k: 'tuple', items },
|
|
227
|
+
column: struct(items.map((_, p) => `p${p}`), columns, length),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Rows rise through `idx`, so a row's items follow the row before's.
|
|
231
|
+
const offsets = new Int32Array(length + 1);
|
|
232
|
+
const counts = new Int32Array(length);
|
|
233
|
+
const flat = [];
|
|
234
|
+
for (const i of idx) {
|
|
235
|
+
counts[rowOf(at, i)] = values[i].length;
|
|
236
|
+
for (const item of values[i])
|
|
237
|
+
flat.push(item);
|
|
238
|
+
}
|
|
239
|
+
for (let row = 0; row < length; row += 1)
|
|
240
|
+
offsets[row + 1] = offsets[row] + counts[row];
|
|
241
|
+
const item = this.slot(flat, null, flat.length, depth + 1);
|
|
242
|
+
const type = new List(new Field('item', item.column[0], true));
|
|
243
|
+
return {
|
|
244
|
+
variant: { k: 'list', item: item.slot },
|
|
245
|
+
column: [
|
|
246
|
+
type,
|
|
247
|
+
makeData({ type, length, valueOffsets: offsets, child: item.column[1] }),
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
object(values, idx, at, length, depth) {
|
|
252
|
+
const keys = Object.keys(values[idx[0]]);
|
|
253
|
+
if (!assignable(keys))
|
|
254
|
+
return { variant: { k: 'json' }, column: this.json(values, idx, at, length) };
|
|
255
|
+
const rows = Int32Array.from(idx, (i) => rowOf(at, i));
|
|
256
|
+
const fieldValues = keys.map((k) => idx.map((i) => values[i][k]));
|
|
257
|
+
const derived = derivations(fieldValues);
|
|
258
|
+
const fields = [];
|
|
259
|
+
const columns = [];
|
|
260
|
+
keys.forEach((_, f) => {
|
|
261
|
+
const from = derived.get(f);
|
|
262
|
+
const built = from !== undefined
|
|
263
|
+
? this.derived(fieldValues[f], fieldValues[from], from, rows, length)
|
|
264
|
+
: this.slot(fieldValues[f], rows, length, depth + 1);
|
|
265
|
+
fields.push(built.slot);
|
|
266
|
+
columns.push(built.column);
|
|
267
|
+
});
|
|
268
|
+
return { variant: { k: 'obj', keys, fields }, column: struct(keys.map((_, f) => `f${f}`), columns, length) };
|
|
269
|
+
}
|
|
270
|
+
/** A string field as codes against its sibling number field. */
|
|
271
|
+
derived(strings, numbers, from, rows, length) {
|
|
272
|
+
const rules = new Map();
|
|
273
|
+
const list = [];
|
|
274
|
+
const codes = new Uint8Array(length);
|
|
275
|
+
const held = Array.from({ length }, () => '');
|
|
276
|
+
strings.forEach((s, i) => {
|
|
277
|
+
const rule = ruleOf(s, numbers[i]);
|
|
278
|
+
let code;
|
|
279
|
+
if (rule !== null) {
|
|
280
|
+
const name = `${rule[0]}${rule[1]}`;
|
|
281
|
+
code = rules.get(name);
|
|
282
|
+
if (code === undefined && list.length < MAX_RULES) {
|
|
283
|
+
rules.set(name, (code = list.length));
|
|
284
|
+
list.push(rule);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (code === undefined) {
|
|
288
|
+
codes[rows[i]] = HELD;
|
|
289
|
+
held[rows[i]] = s;
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
codes[rows[i]] = code;
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
slot: { v: [{ k: 'str', derive: { from, rules: list } }] },
|
|
296
|
+
column: struct(['v0'], [struct(['d', 'h'], [uint8Column(codes), utf8Column(held)], length)], length),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
table(root, keys, values, positions) {
|
|
300
|
+
this.stats.tables += 1;
|
|
301
|
+
this.stats.values += values.length;
|
|
302
|
+
const n = keys.length;
|
|
303
|
+
const key = keyColumn(keys, (strings) => this.dictionary(strings));
|
|
304
|
+
// A table's positions rise, so each is stored as the gap from the row
|
|
305
|
+
// before, the first from zero.
|
|
306
|
+
const gaps = new Int32Array(n);
|
|
307
|
+
for (let row = 0; row < n; row += 1)
|
|
308
|
+
gaps[row] = positions[row] - (row === 0 ? 0 : positions[row - 1]);
|
|
309
|
+
const built = this.slot(values, null, n, 0);
|
|
310
|
+
const columns = [
|
|
311
|
+
['key', key.column],
|
|
312
|
+
['ordGap', int32Column(gaps)],
|
|
313
|
+
['r', built.column],
|
|
314
|
+
];
|
|
315
|
+
const meta = { root, key: key.meta, slot: built.slot };
|
|
316
|
+
const fields = columns.map(([name, c]) => new Field(name, c[0], false));
|
|
317
|
+
const schema = new Schema(fields, new Map([[TABLE_META_KEY, JSON.stringify(meta)]]));
|
|
318
|
+
const data = makeData({
|
|
319
|
+
type: new Struct(fields),
|
|
320
|
+
length: n,
|
|
321
|
+
children: columns.map(([, c]) => c[1]),
|
|
322
|
+
});
|
|
323
|
+
return tableToIPC(new Table([new RecordBatch(schema, data)]), 'file');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** `s` as `String(n)`, upper-cased or not, plus a short suffix; or null. */
|
|
327
|
+
function ruleOf(s, n) {
|
|
328
|
+
const plain = String(n);
|
|
329
|
+
if (s.startsWith(plain) && s.length - plain.length <= MAX_SUFFIX)
|
|
330
|
+
return [0, s.slice(plain.length)];
|
|
331
|
+
const upper = plain.toUpperCase();
|
|
332
|
+
if (upper !== plain && s.startsWith(upper) && s.length - upper.length <= MAX_SUFFIX)
|
|
333
|
+
return [1, s.slice(upper.length)];
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
/** The rows `derivations` reads across every pair of a string field and a
|
|
337
|
+
* number field together, so an object with many fields costs a smaller
|
|
338
|
+
* sample rather than a longer encode. A smaller sample only decides which
|
|
339
|
+
* fields are derived; a row no rule derives is held either way. */
|
|
340
|
+
const DERIVATION_SAMPLE_BUDGET = 200_000;
|
|
341
|
+
const DERIVATION_SAMPLE_MIN = 16;
|
|
342
|
+
/** Which string fields derive from which number field: a field of strings
|
|
343
|
+
* that differ, beside a field of numbers that differ, where a rule derives at
|
|
344
|
+
* least half the strings of a sample. */
|
|
345
|
+
function derivations(fields) {
|
|
346
|
+
const out = new Map();
|
|
347
|
+
const kinds = fields.map((values) => {
|
|
348
|
+
const first = values[0];
|
|
349
|
+
if (values.every((v) => v === first))
|
|
350
|
+
return null;
|
|
351
|
+
if (values.every((v) => typeof v === 'number'))
|
|
352
|
+
return 'num';
|
|
353
|
+
if (values.every((v) => typeof v === 'string' && wellFormed(v)))
|
|
354
|
+
return 'str';
|
|
355
|
+
return null;
|
|
356
|
+
});
|
|
357
|
+
const strs = kinds.reduce((n, k) => n + (k === 'str' ? 1 : 0), 0);
|
|
358
|
+
const nums = kinds.reduce((n, k) => n + (k === 'num' ? 1 : 0), 0);
|
|
359
|
+
if (strs === 0 || nums === 0)
|
|
360
|
+
return out;
|
|
361
|
+
const sample = Math.min(fields[0].length, 1000, Math.max(DERIVATION_SAMPLE_MIN, Math.floor(DERIVATION_SAMPLE_BUDGET / (strs * nums))));
|
|
362
|
+
kinds.forEach((k, s) => {
|
|
363
|
+
if (k !== 'str')
|
|
364
|
+
return;
|
|
365
|
+
let best = -1;
|
|
366
|
+
let bestHits = 0;
|
|
367
|
+
kinds.forEach((kn, n) => {
|
|
368
|
+
if (kn !== 'num')
|
|
369
|
+
return;
|
|
370
|
+
let hits = 0;
|
|
371
|
+
for (let i = 0; i < sample; i += 1) {
|
|
372
|
+
if (ruleOf(fields[s][i], fields[n][i]) !== null)
|
|
373
|
+
hits += 1;
|
|
374
|
+
}
|
|
375
|
+
if (hits * 2 >= sample && hits > bestHits) {
|
|
376
|
+
best = n;
|
|
377
|
+
bestHits = hits;
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
if (best >= 0)
|
|
381
|
+
out.set(s, best);
|
|
382
|
+
});
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
/** Separators `keySeparator` tries, so its cost stays linear in the keys. */
|
|
386
|
+
const MAX_SEPARATOR_CANDIDATES = 8;
|
|
387
|
+
/** The separator that splits every key in two at its first occurrence with
|
|
388
|
+
* the fewest distinct halves, where the halves number at most half the keys;
|
|
389
|
+
* or null. Only a character that is not a letter or digit is tried, from the
|
|
390
|
+
* first `MAX_SEPARATOR_CANDIDATES` of the first key. A key splits at its
|
|
391
|
+
* first separator, so its first half never contains one and the join
|
|
392
|
+
* restores it exactly. */
|
|
393
|
+
function keySeparator(keys) {
|
|
394
|
+
if (keys.length < 2)
|
|
395
|
+
return null;
|
|
396
|
+
const candidates = [...new Set(keys[0])]
|
|
397
|
+
.filter((c) => !/[\p{L}\p{N}]/u.test(c))
|
|
398
|
+
.slice(0, MAX_SEPARATOR_CANDIDATES);
|
|
399
|
+
const limit = keys.length / 2;
|
|
400
|
+
let best = null;
|
|
401
|
+
for (const sep of candidates) {
|
|
402
|
+
const a = new Set();
|
|
403
|
+
const b = new Set();
|
|
404
|
+
let ok = true;
|
|
405
|
+
for (const k of keys) {
|
|
406
|
+
const at = k.indexOf(sep);
|
|
407
|
+
if (at === -1) {
|
|
408
|
+
ok = false;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
a.add(k.slice(0, at));
|
|
412
|
+
b.add(k.slice(at + sep.length));
|
|
413
|
+
// Past half the keys, this separator cannot qualify.
|
|
414
|
+
if (a.size + b.size > limit) {
|
|
415
|
+
ok = false;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const distinct = a.size + b.size;
|
|
420
|
+
if (ok && (best === null || distinct < best.distinct))
|
|
421
|
+
best = { sep, distinct };
|
|
422
|
+
}
|
|
423
|
+
return best?.sep ?? null;
|
|
424
|
+
}
|
|
425
|
+
/** Keys as int32 under a shared prefix where each is that prefix and a
|
|
426
|
+
* canonical non-negative integer; as two dictionary halves where a separator
|
|
427
|
+
* at least halves the distinct values; else as utf8 text, or JSON text where
|
|
428
|
+
* a key is not well formed. */
|
|
429
|
+
function keyColumn(keys, dictionary) {
|
|
430
|
+
const split = (k) => {
|
|
431
|
+
let at = k.length;
|
|
432
|
+
while (at > 0 && k.charCodeAt(at - 1) >= 48 && k.charCodeAt(at - 1) <= 57)
|
|
433
|
+
at -= 1;
|
|
434
|
+
return [k.slice(0, at), k.slice(at)];
|
|
435
|
+
};
|
|
436
|
+
if (keys.length > 0) {
|
|
437
|
+
const prefix = split(keys[0])[0];
|
|
438
|
+
const ids = new Int32Array(keys.length);
|
|
439
|
+
const fits = keys.every((k, row) => {
|
|
440
|
+
const [p, digits] = split(k);
|
|
441
|
+
if (p !== prefix || digits === '')
|
|
442
|
+
return false;
|
|
443
|
+
const n = Number(digits);
|
|
444
|
+
if (n > 0x7fffffff || String(n) !== digits)
|
|
445
|
+
return false;
|
|
446
|
+
ids[row] = n;
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
if (fits && wellFormed(prefix))
|
|
450
|
+
return { meta: { prefix }, column: int32Column(ids) };
|
|
451
|
+
}
|
|
452
|
+
if (!keys.every((k) => wellFormed(k))) {
|
|
453
|
+
return { meta: { json: true }, column: utf8Column(keys.map((k) => JSON.stringify(k))) };
|
|
454
|
+
}
|
|
455
|
+
const sep = keySeparator(keys);
|
|
456
|
+
if (sep !== null) {
|
|
457
|
+
const halves = keys.map((k) => k.indexOf(sep));
|
|
458
|
+
return {
|
|
459
|
+
meta: { split: sep },
|
|
460
|
+
column: struct(['a', 'b'], [dictionary(keys.map((k, i) => k.slice(0, halves[i]))), dictionary(keys.map((k, i) => k.slice(halves[i] + sep.length)))], keys.length),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return { meta: { text: true }, column: utf8Column(keys) };
|
|
464
|
+
}
|
|
465
|
+
// --- grouping ------------------------------------------------------------
|
|
466
|
+
/** A chunk's entries grouped into tables: each table's members as indices
|
|
467
|
+
* into the chunk, rising. */
|
|
468
|
+
function tablesOf(values) {
|
|
469
|
+
const groups = [...groupBy(values, true).values()];
|
|
470
|
+
const out = [];
|
|
471
|
+
for (const members of groups) {
|
|
472
|
+
const head = values[members[0]];
|
|
473
|
+
const split = kindOf(head) === 'obj' ? discriminate(values, members) : null;
|
|
474
|
+
if (split === null)
|
|
475
|
+
out.push(members);
|
|
476
|
+
else
|
|
477
|
+
for (const part of split)
|
|
478
|
+
out.push(part);
|
|
479
|
+
}
|
|
480
|
+
// Tables averaging fewer than MIN_TABLE_ROWS rows (objects whose key sets
|
|
481
|
+
// barely repeat) cost a schema each, so the chunk is then one table.
|
|
482
|
+
if (out.length > MAX_TABLES || out.length * MIN_TABLE_ROWS > values.length)
|
|
483
|
+
return [values.map((_, i) => i)];
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
/** The group split by the string field with the fewest distinct values,
|
|
487
|
+
* where that is between 2 and an eighth of the group; or null. */
|
|
488
|
+
function discriminate(values, members) {
|
|
489
|
+
const limit = Math.min(MAX_TABLES, Math.max(2, Math.floor(members.length / 8)));
|
|
490
|
+
let best = null;
|
|
491
|
+
for (const key of Object.keys(values[members[0]])) {
|
|
492
|
+
const parts = new Map();
|
|
493
|
+
let ok = true;
|
|
494
|
+
for (const i of members) {
|
|
495
|
+
const v = values[i][key];
|
|
496
|
+
if (typeof v !== 'string') {
|
|
497
|
+
ok = false;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
let list = parts.get(v);
|
|
501
|
+
if (list === undefined) {
|
|
502
|
+
if (parts.size >= limit) {
|
|
503
|
+
ok = false;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
parts.set(v, (list = []));
|
|
507
|
+
}
|
|
508
|
+
list.push(i);
|
|
509
|
+
}
|
|
510
|
+
if (ok && parts.size >= 2 && (best === null || parts.size < best.size))
|
|
511
|
+
best = parts;
|
|
512
|
+
}
|
|
513
|
+
return best === null ? null : [...best.values()];
|
|
514
|
+
}
|
|
515
|
+
// --- importer output as JSON ---------------------------------------------
|
|
516
|
+
const OMIT = Symbol('omit');
|
|
517
|
+
/**
|
|
518
|
+
* `JSON.parse(JSON.stringify(value))`, without the text: `toJSON` is called
|
|
519
|
+
* with the key JSON would pass, boxed primitives unwrap, a non-finite number
|
|
520
|
+
* and -0 read as JSON writes them, and a member JSON drops is dropped (null
|
|
521
|
+
* in an array). Throws where `JSON.stringify` throws on a BigInt.
|
|
522
|
+
*/
|
|
523
|
+
export function jsonValue(value, key = '') {
|
|
524
|
+
const out = plain(value, key);
|
|
525
|
+
return out === OMIT ? null : out;
|
|
526
|
+
}
|
|
527
|
+
function plain(value, key) {
|
|
528
|
+
if ((typeof value === 'object' && value !== null) ||
|
|
529
|
+
typeof value === 'function' ||
|
|
530
|
+
typeof value === 'bigint') {
|
|
531
|
+
const toJSON = value.toJSON;
|
|
532
|
+
if (typeof toJSON === 'function') {
|
|
533
|
+
value = toJSON.call(value, String(key));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
switch (typeof value) {
|
|
537
|
+
case 'string':
|
|
538
|
+
case 'boolean':
|
|
539
|
+
return value;
|
|
540
|
+
case 'number':
|
|
541
|
+
return Number.isFinite(value) ? (value === 0 ? 0 : value) : null;
|
|
542
|
+
case 'bigint':
|
|
543
|
+
throw new TypeError('Do not know how to serialize a BigInt');
|
|
544
|
+
case 'object':
|
|
545
|
+
break;
|
|
546
|
+
default:
|
|
547
|
+
return OMIT;
|
|
548
|
+
}
|
|
549
|
+
if (value === null)
|
|
550
|
+
return null;
|
|
551
|
+
// A subtree JSON would write unchanged is kept as it is, and a container is
|
|
552
|
+
// copied only once a member reads differently, so a body an importer built
|
|
553
|
+
// as plain JSON is not built twice.
|
|
554
|
+
if (Array.isArray(value)) {
|
|
555
|
+
const n = value.length;
|
|
556
|
+
let out = Object.getPrototypeOf(value) === Array.prototype ? null : [];
|
|
557
|
+
for (let i = 0; i < n; i += 1) {
|
|
558
|
+
const item = value[i];
|
|
559
|
+
let read = plain(item, i);
|
|
560
|
+
if (read === OMIT)
|
|
561
|
+
read = null;
|
|
562
|
+
if (out === null) {
|
|
563
|
+
if (Object.is(read, item))
|
|
564
|
+
continue;
|
|
565
|
+
out = Array.prototype.slice.call(value, 0, i);
|
|
566
|
+
}
|
|
567
|
+
out.push(read);
|
|
568
|
+
}
|
|
569
|
+
return out ?? value;
|
|
570
|
+
}
|
|
571
|
+
if (value instanceof Number)
|
|
572
|
+
return plain(Number(value), key);
|
|
573
|
+
if (value instanceof String)
|
|
574
|
+
return String(value);
|
|
575
|
+
if (value instanceof Boolean)
|
|
576
|
+
return value.valueOf();
|
|
577
|
+
const source = value;
|
|
578
|
+
const keys = Object.keys(source);
|
|
579
|
+
let out = Object.getPrototypeOf(source) === Object.prototype ? null : {};
|
|
580
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
581
|
+
const k = keys[i];
|
|
582
|
+
const member = source[k];
|
|
583
|
+
const read = plain(member, k);
|
|
584
|
+
if (out === null) {
|
|
585
|
+
if (Object.is(read, member))
|
|
586
|
+
continue;
|
|
587
|
+
out = {};
|
|
588
|
+
for (let j = 0; j < i; j += 1)
|
|
589
|
+
define(out, keys[j], source[keys[j]]);
|
|
590
|
+
}
|
|
591
|
+
if (read !== OMIT)
|
|
592
|
+
define(out, k, read);
|
|
593
|
+
}
|
|
594
|
+
return out ?? source;
|
|
595
|
+
}
|
|
596
|
+
function define(out, k, value) {
|
|
597
|
+
if (k === '__proto__') {
|
|
598
|
+
Object.defineProperty(out, k, { value, enumerable: true, writable: true, configurable: true });
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
out[k] = value;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/** The length of `JSON.stringify(value)` for a value `jsonValue` returned. */
|
|
605
|
+
export function jsonLength(value) {
|
|
606
|
+
switch (typeof value) {
|
|
607
|
+
case 'string':
|
|
608
|
+
return quotedLength(value);
|
|
609
|
+
case 'number':
|
|
610
|
+
return String(value).length;
|
|
611
|
+
case 'boolean':
|
|
612
|
+
return value ? 4 : 5;
|
|
613
|
+
default:
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
if (value === null)
|
|
617
|
+
return 4;
|
|
618
|
+
if (Array.isArray(value)) {
|
|
619
|
+
let n = 2 + Math.max(0, value.length - 1);
|
|
620
|
+
for (const item of value)
|
|
621
|
+
n += jsonLength(item);
|
|
622
|
+
return n;
|
|
623
|
+
}
|
|
624
|
+
const object = value;
|
|
625
|
+
const keys = Object.keys(object);
|
|
626
|
+
let n = 2 + Math.max(0, keys.length - 1);
|
|
627
|
+
for (const k of keys)
|
|
628
|
+
n += quotedLength(k) + 1 + jsonLength(object[k]);
|
|
629
|
+
return n;
|
|
630
|
+
}
|
|
631
|
+
function quotedLength(s) {
|
|
632
|
+
let n = 2;
|
|
633
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
634
|
+
const c = s.charCodeAt(i);
|
|
635
|
+
if (c === 0x22 || c === 0x5c || c === 0x08 || c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d) {
|
|
636
|
+
n += 2;
|
|
637
|
+
}
|
|
638
|
+
else if (c < 0x20) {
|
|
639
|
+
n += 6;
|
|
640
|
+
}
|
|
641
|
+
else if (c >= 0xd800 && c <= 0xdbff) {
|
|
642
|
+
const next = s.charCodeAt(i + 1);
|
|
643
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
644
|
+
n += 2;
|
|
645
|
+
i += 1;
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
n += 6;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else if (c >= 0xdc00 && c <= 0xdfff) {
|
|
652
|
+
n += 6;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
n += 1;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return n;
|
|
659
|
+
}
|
|
660
|
+
// --- the frame -----------------------------------------------------------
|
|
661
|
+
function packParts(parts) {
|
|
662
|
+
const head = utf8.encode(JSON.stringify(parts.map(([n, b]) => [n, b.byteLength])));
|
|
663
|
+
const out = new Uint8Array(4 + head.byteLength + parts.reduce((n, [, b]) => n + b.byteLength, 0));
|
|
664
|
+
new DataView(out.buffer).setUint32(0, head.byteLength, true);
|
|
665
|
+
out.set(head, 4);
|
|
666
|
+
let at = 4 + head.byteLength;
|
|
667
|
+
for (const [, b] of parts) {
|
|
668
|
+
out.set(b, at);
|
|
669
|
+
at += b.byteLength;
|
|
670
|
+
}
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Sealed content's tables.
|
|
675
|
+
*
|
|
676
|
+
* Walks each map once, in stored order, holding one chunk of its values at a
|
|
677
|
+
* time. A key the content repeats keeps its first entry. The bytes depend on
|
|
678
|
+
* the content alone.
|
|
679
|
+
*/
|
|
680
|
+
export function sealedTablesOfContent(content, options = {}) {
|
|
681
|
+
function* roots() {
|
|
682
|
+
for (const name of content.rootNames()) {
|
|
683
|
+
const kind = content.kindOf(name);
|
|
684
|
+
if (kind === 'map') {
|
|
685
|
+
yield { kind: 'map', name, entries: content.mapRoot(name).entries() };
|
|
686
|
+
}
|
|
687
|
+
else if (kind === 'list') {
|
|
688
|
+
const keysOf = content.keyWalkOf(name);
|
|
689
|
+
yield keysOf !== null
|
|
690
|
+
? { kind: 'list', name, keysOf }
|
|
691
|
+
: { kind: 'list', name, items: content.list(name) ?? [] };
|
|
692
|
+
}
|
|
693
|
+
else if (kind === 'text') {
|
|
694
|
+
yield { kind: 'text', name, text: content.text(name) ?? '' };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return encodeRoots(roots(), content.builtBy(), options);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* An importer's content, straight from the importer: the same tables
|
|
702
|
+
* `sealedTablesOfContent` builds for the same content staged and decoded.
|
|
703
|
+
*
|
|
704
|
+
* Each value is read as `JSON.parse(JSON.stringify(value))` would read it
|
|
705
|
+
* (`jsonValue`) when its chunk reaches it, and released from `content` once
|
|
706
|
+
* read, so a value built lazily is built one chunk at a time.
|
|
707
|
+
*
|
|
708
|
+
* CONSUMES the map values.
|
|
709
|
+
*/
|
|
710
|
+
export function sealedTablesOfImport(content, builtBy, options = {}) {
|
|
711
|
+
return sealedTablesOfPlan(planSealedContent(content), builtBy, options);
|
|
712
|
+
}
|
|
713
|
+
/** A record store handed over as triples (`SealedStoreSource`): the tables
|
|
714
|
+
* `sealedTablesOfImport` builds for the same store's JSON. CONSUMES
|
|
715
|
+
* `source.records`. */
|
|
716
|
+
export function sealedTablesOfRecords(source, options = {}) {
|
|
717
|
+
return sealedTablesOfPlan(planSealedStore(source), source.builtBy, options);
|
|
718
|
+
}
|
|
719
|
+
function sealedTablesOfPlan(plan, builtBy, options) {
|
|
720
|
+
const observe = options.observe;
|
|
721
|
+
function* mapEntries(entries) {
|
|
722
|
+
const paged = entries.keys.length > UNPAGED_MAX_RECORDS;
|
|
723
|
+
if (paged)
|
|
724
|
+
observe?.pagedMap?.(entries.keys.length);
|
|
725
|
+
for (let i = 0; i < entries.keys.length; i += 1) {
|
|
726
|
+
const value = jsonValue(entries.value(i));
|
|
727
|
+
entries.release(i);
|
|
728
|
+
observe?.value?.(value, paged);
|
|
729
|
+
yield [entries.keys[i], value];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function* roots() {
|
|
733
|
+
for (const root of plan) {
|
|
734
|
+
if (root.kind === 'map') {
|
|
735
|
+
yield { kind: 'map', name: root.name, entries: mapEntries(root.entries) };
|
|
736
|
+
}
|
|
737
|
+
else if (root.kind === 'list') {
|
|
738
|
+
if ('keysOf' in root) {
|
|
739
|
+
yield root;
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const items = root.items.map((item, index) => {
|
|
743
|
+
const value = jsonValue(item, String(index));
|
|
744
|
+
observe?.value?.(value, false);
|
|
745
|
+
return value;
|
|
746
|
+
});
|
|
747
|
+
yield { kind: 'list', name: root.name, items };
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
observe?.text?.(root.text);
|
|
751
|
+
yield root;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return encodeRoots(roots(), builtBy ?? null, options);
|
|
756
|
+
}
|
|
757
|
+
function encodeRoots(sealedRoots, by, options) {
|
|
758
|
+
const chunkEntries = options.chunkEntries ?? SEALED_CHUNK_ENTRIES;
|
|
759
|
+
const started = performance.now();
|
|
760
|
+
const writer = new Writer();
|
|
761
|
+
const roots = [];
|
|
762
|
+
const parts = [];
|
|
763
|
+
let innerBytes = 0;
|
|
764
|
+
// Refused as the tables are built, so content past what a reader decodes
|
|
765
|
+
// stops before the rest of it is encoded.
|
|
766
|
+
const pushTable = (bytes) => {
|
|
767
|
+
innerBytes += bytes.byteLength;
|
|
768
|
+
if (innerBytes > SEALED_INNER_MAX_BYTES)
|
|
769
|
+
throw new SealedContentTooLargeError();
|
|
770
|
+
parts.push([String(parts.length + 1), bytes]);
|
|
771
|
+
};
|
|
772
|
+
for (const root of sealedRoots) {
|
|
773
|
+
const name = root.name;
|
|
774
|
+
if (root.kind === 'map') {
|
|
775
|
+
roots.push(['map', name]);
|
|
776
|
+
const seen = new Set();
|
|
777
|
+
let keys = [];
|
|
778
|
+
let values = [];
|
|
779
|
+
let positions = [];
|
|
780
|
+
let at = 0;
|
|
781
|
+
const flush = () => {
|
|
782
|
+
if (keys.length === 0)
|
|
783
|
+
return;
|
|
784
|
+
for (const members of tablesOf(values)) {
|
|
785
|
+
pushTable(writer.table(name, members.map((i) => keys[i]), members.map((i) => values[i]), Int32Array.from(members, (i) => positions[i])));
|
|
786
|
+
}
|
|
787
|
+
keys = [];
|
|
788
|
+
values = [];
|
|
789
|
+
positions = [];
|
|
790
|
+
};
|
|
791
|
+
for (const [key, value] of root.entries) {
|
|
792
|
+
if (seen.has(key))
|
|
793
|
+
continue;
|
|
794
|
+
seen.add(key);
|
|
795
|
+
keys.push(key);
|
|
796
|
+
values.push(value);
|
|
797
|
+
positions.push(at);
|
|
798
|
+
at += 1;
|
|
799
|
+
if (keys.length >= chunkEntries)
|
|
800
|
+
flush();
|
|
801
|
+
}
|
|
802
|
+
flush();
|
|
803
|
+
// An empty map is one table of no rows, so its root reads back.
|
|
804
|
+
if (at === 0)
|
|
805
|
+
pushTable(writer.table(name, [], [], new Int32Array(0)));
|
|
806
|
+
}
|
|
807
|
+
else if (root.kind === 'list') {
|
|
808
|
+
roots.push('keysOf' in root ? ['list', name, null, root.keysOf] : ['list', name, root.items]);
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
roots.push(['text', name, root.text]);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const manifest = [by !== null ? [by.id, by.version] : null, roots];
|
|
815
|
+
const manifestBytes = utf8.encode(JSON.stringify(manifest));
|
|
816
|
+
if (innerBytes + manifestBytes.byteLength > SEALED_INNER_MAX_BYTES)
|
|
817
|
+
throw new SealedContentTooLargeError();
|
|
818
|
+
const inner = packParts([[ARROW_MANIFEST_PART, manifestBytes], ...parts]);
|
|
819
|
+
writer.stats.rawBytes = inner.byteLength;
|
|
820
|
+
return { inner, stats: writer.stats, encodeMs: performance.now() - started };
|
|
821
|
+
}
|
|
822
|
+
/** The ZSTD level the stored form is compressed at. */
|
|
823
|
+
export const SEALED_ZSTD_LEVEL = 19;
|
|
824
|
+
function nativeZstd() {
|
|
825
|
+
try {
|
|
826
|
+
const proc = globalThis
|
|
827
|
+
.process;
|
|
828
|
+
const zlib = proc?.getBuiltinModule?.('node:zlib');
|
|
829
|
+
return zlib?.zstdCompressSync !== undefined ? zlib : undefined;
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/** A frame body as frame 4, where the runtime carries a ZSTD compressor. */
|
|
836
|
+
export function zstdFrame(inner) {
|
|
837
|
+
const zlib = nativeZstd();
|
|
838
|
+
if (zlib === undefined)
|
|
839
|
+
return null;
|
|
840
|
+
const compressed = zlib.zstdCompressSync(inner, {
|
|
841
|
+
params: {
|
|
842
|
+
[zlib.constants.ZSTD_c_compressionLevel]: SEALED_ZSTD_LEVEL,
|
|
843
|
+
[zlib.constants.ZSTD_c_checksumFlag]: 1,
|
|
844
|
+
},
|
|
845
|
+
pledgedSrcSize: inner.byteLength,
|
|
846
|
+
});
|
|
847
|
+
return arrowFrameBytes(new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength));
|
|
848
|
+
}
|
|
849
|
+
async function gzip(bytes) {
|
|
850
|
+
if (typeof CompressionStream === 'function') {
|
|
851
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'));
|
|
852
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
853
|
+
}
|
|
854
|
+
return gzipSync(bytes);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* An importer's stream in the form its producer uploads: the stored frame
|
|
858
|
+
* where the runtime compresses ZSTD (Node), else the same tables gzipped as
|
|
859
|
+
* the staging frame, which the seal recompresses. Either way the platform
|
|
860
|
+
* reads every value before it stores the line.
|
|
861
|
+
*
|
|
862
|
+
* CONSUMES the map values, as `sealedTablesOfImport` does.
|
|
863
|
+
*/
|
|
864
|
+
export async function encodeSealedUpload(content, builtBy, options = {}) {
|
|
865
|
+
return uploadFrame(sealedTablesOfImport(content, builtBy).inner, options);
|
|
866
|
+
}
|
|
867
|
+
/** `encodeSealedUpload` for a record store handed over as triples. CONSUMES
|
|
868
|
+
* `source.records`. */
|
|
869
|
+
export async function encodeSealedRecordsUpload(source, options = {}) {
|
|
870
|
+
return uploadFrame(sealedTablesOfRecords(source).inner, options);
|
|
871
|
+
}
|
|
872
|
+
async function uploadFrame(inner, options) {
|
|
873
|
+
const stored = options.staged === true ? null : zstdFrame(inner);
|
|
874
|
+
return stored ?? arrowFrameBytes(await gzip(inner), VERSION_ARROW_STAGED);
|
|
875
|
+
}
|
|
876
|
+
//# sourceMappingURL=sealed-encode.js.map
|