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