@elaraai/east 1.0.79 → 1.0.80

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 (276) hide show
  1. package/README.md +1 -1
  2. package/dist/src/comparison.d.ts.map +1 -1
  3. package/dist/src/comparison.js +4 -5
  4. package/dist/src/comparison.js.map +1 -1
  5. package/dist/src/compile/builtins/array.d.ts +59 -0
  6. package/dist/src/compile/builtins/array.d.ts.map +1 -0
  7. package/dist/src/compile/builtins/array.js +656 -0
  8. package/dist/src/compile/builtins/array.js.map +1 -0
  9. package/dist/src/compile/builtins/blob.d.ts +18 -0
  10. package/dist/src/compile/builtins/blob.d.ts.map +1 -0
  11. package/dist/src/compile/builtins/blob.js +150 -0
  12. package/dist/src/compile/builtins/blob.js.map +1 -0
  13. package/dist/src/compile/builtins/compare.d.ts +18 -0
  14. package/dist/src/compile/builtins/compare.d.ts.map +1 -0
  15. package/dist/src/compile/builtins/compare.js +52 -0
  16. package/dist/src/compile/builtins/compare.js.map +1 -0
  17. package/dist/src/compile/builtins/datetime.d.ts +21 -0
  18. package/dist/src/compile/builtins/datetime.d.ts.map +1 -0
  19. package/dist/src/compile/builtins/datetime.js +46 -0
  20. package/dist/src/compile/builtins/datetime.js.map +1 -0
  21. package/dist/src/compile/builtins/dict.d.ts +46 -0
  22. package/dist/src/compile/builtins/dict.d.ts.map +1 -0
  23. package/dist/src/compile/builtins/dict.js +551 -0
  24. package/dist/src/compile/builtins/dict.js.map +1 -0
  25. package/dist/src/compile/builtins/index.d.ts +9 -0
  26. package/dist/src/compile/builtins/index.d.ts.map +1 -0
  27. package/dist/src/compile/builtins/index.js +28 -0
  28. package/dist/src/compile/builtins/index.js.map +1 -0
  29. package/dist/src/compile/builtins/matrix.d.ts +31 -0
  30. package/dist/src/compile/builtins/matrix.d.ts.map +1 -0
  31. package/dist/src/compile/builtins/matrix.js +310 -0
  32. package/dist/src/compile/builtins/matrix.js.map +1 -0
  33. package/dist/src/compile/builtins/primitives.d.ts +36 -0
  34. package/dist/src/compile/builtins/primitives.d.ts.map +1 -0
  35. package/dist/src/compile/builtins/primitives.js +67 -0
  36. package/dist/src/compile/builtins/primitives.js.map +1 -0
  37. package/dist/src/compile/builtins/ref.d.ts +11 -0
  38. package/dist/src/compile/builtins/ref.d.ts.map +1 -0
  39. package/dist/src/compile/builtins/ref.js +30 -0
  40. package/dist/src/compile/builtins/ref.js.map +1 -0
  41. package/dist/src/compile/builtins/set.d.ts +41 -0
  42. package/dist/src/compile/builtins/set.d.ts.map +1 -0
  43. package/dist/src/compile/builtins/set.js +385 -0
  44. package/dist/src/compile/builtins/set.js.map +1 -0
  45. package/dist/src/compile/builtins/sparse.d.ts +19 -0
  46. package/dist/src/compile/builtins/sparse.d.ts.map +1 -0
  47. package/dist/src/compile/builtins/sparse.js +143 -0
  48. package/dist/src/compile/builtins/sparse.js.map +1 -0
  49. package/dist/src/compile/builtins/string.d.ts +34 -0
  50. package/dist/src/compile/builtins/string.d.ts.map +1 -0
  51. package/dist/src/compile/builtins/string.js +238 -0
  52. package/dist/src/compile/builtins/string.js.map +1 -0
  53. package/dist/src/compile/builtins/vector.d.ts +44 -0
  54. package/dist/src/compile/builtins/vector.d.ts.map +1 -0
  55. package/dist/src/compile/builtins/vector.js +463 -0
  56. package/dist/src/compile/builtins/vector.js.map +1 -0
  57. package/dist/src/compile/constructors.d.ts +13 -0
  58. package/dist/src/compile/constructors.d.ts.map +1 -0
  59. package/dist/src/compile/constructors.js +179 -0
  60. package/dist/src/compile/constructors.js.map +1 -0
  61. package/dist/src/compile/control.d.ts +13 -0
  62. package/dist/src/compile/control.d.ts.map +1 -0
  63. package/dist/src/compile/control.js +513 -0
  64. package/dist/src/compile/control.js.map +1 -0
  65. package/dist/src/compile/functions.d.ts +13 -0
  66. package/dist/src/compile/functions.d.ts.map +1 -0
  67. package/dist/src/compile/functions.js +492 -0
  68. package/dist/src/compile/functions.js.map +1 -0
  69. package/dist/src/compile/ir.d.ts +19 -0
  70. package/dist/src/compile/ir.d.ts.map +1 -0
  71. package/dist/src/compile/ir.js +193 -0
  72. package/dist/src/compile/ir.js.map +1 -0
  73. package/dist/src/compile/runtime.d.ts +78 -0
  74. package/dist/src/compile/runtime.d.ts.map +1 -0
  75. package/dist/src/compile/runtime.js +111 -0
  76. package/dist/src/compile/runtime.js.map +1 -0
  77. package/dist/src/compile/type_parameters.d.ts +4 -0
  78. package/dist/src/compile/type_parameters.d.ts.map +1 -0
  79. package/dist/src/compile/type_parameters.js +55 -0
  80. package/dist/src/compile/type_parameters.js.map +1 -0
  81. package/dist/src/compile/typed_arrays.d.ts +28 -0
  82. package/dist/src/compile/typed_arrays.d.ts.map +1 -0
  83. package/dist/src/compile/typed_arrays.js +77 -0
  84. package/dist/src/compile/typed_arrays.js.map +1 -0
  85. package/dist/src/compile.d.ts +6 -41
  86. package/dist/src/compile.d.ts.map +1 -1
  87. package/dist/src/compile.js +8 -4415
  88. package/dist/src/compile.js.map +1 -1
  89. package/dist/src/compile.open_beast.spec.js +5 -4
  90. package/dist/src/compile.open_beast.spec.js.map +1 -1
  91. package/dist/src/containers/sortedmap.d.ts +2 -1
  92. package/dist/src/containers/sortedmap.d.ts.map +1 -1
  93. package/dist/src/containers/sortedmap.js +4 -2
  94. package/dist/src/containers/sortedmap.js.map +1 -1
  95. package/dist/src/containers/sortedset.d.ts +2 -1
  96. package/dist/src/containers/sortedset.d.ts.map +1 -1
  97. package/dist/src/containers/sortedset.js +4 -2
  98. package/dist/src/containers/sortedset.js.map +1 -1
  99. package/dist/src/error.d.ts +15 -0
  100. package/dist/src/error.d.ts.map +1 -1
  101. package/dist/src/error.js +16 -0
  102. package/dist/src/error.js.map +1 -1
  103. package/dist/src/index.d.ts +1 -0
  104. package/dist/src/index.d.ts.map +1 -1
  105. package/dist/src/index.js +1 -0
  106. package/dist/src/index.js.map +1 -1
  107. package/dist/src/patch/index.d.ts +1 -1
  108. package/dist/src/patch/index.d.ts.map +1 -1
  109. package/dist/src/patch/index.js +1 -1
  110. package/dist/src/patch/index.js.map +1 -1
  111. package/dist/src/patch/type_of_patch.d.ts +25 -1
  112. package/dist/src/patch/type_of_patch.d.ts.map +1 -1
  113. package/dist/src/patch/type_of_patch.js +33 -10
  114. package/dist/src/patch/type_of_patch.js.map +1 -1
  115. package/dist/src/platform.spec.js +19 -1
  116. package/dist/src/platform.spec.js.map +1 -1
  117. package/dist/src/runner_protocol.d.ts +316 -0
  118. package/dist/src/runner_protocol.d.ts.map +1 -0
  119. package/dist/src/runner_protocol.js +135 -0
  120. package/dist/src/runner_protocol.js.map +1 -0
  121. package/dist/src/serialization/beast2/index.d.ts +21 -5
  122. package/dist/src/serialization/beast2/index.d.ts.map +1 -1
  123. package/dist/src/serialization/beast2/index.js +12 -2
  124. package/dist/src/serialization/beast2/index.js.map +1 -1
  125. package/dist/src/serialization/beast2/index.spec.js +84 -3
  126. package/dist/src/serialization/beast2/index.spec.js.map +1 -1
  127. package/dist/src/serialization/beast2/shared.d.ts +32 -3
  128. package/dist/src/serialization/beast2/shared.d.ts.map +1 -1
  129. package/dist/src/serialization/beast2/shared.js +135 -10
  130. package/dist/src/serialization/beast2/shared.js.map +1 -1
  131. package/dist/src/serialization/beast2/v4/container.d.ts.map +1 -1
  132. package/dist/src/serialization/beast2/v4/container.js +5 -3
  133. package/dist/src/serialization/beast2/v4/container.js.map +1 -1
  134. package/dist/src/serialization/beast2/v5/aliasing.spec.d.ts +6 -0
  135. package/dist/src/serialization/beast2/v5/aliasing.spec.d.ts.map +1 -0
  136. package/dist/src/serialization/beast2/v5/aliasing.spec.js +75 -0
  137. package/dist/src/serialization/beast2/v5/aliasing.spec.js.map +1 -0
  138. package/dist/src/serialization/beast2/v5/boundary.d.ts +176 -0
  139. package/dist/src/serialization/beast2/v5/boundary.d.ts.map +1 -0
  140. package/dist/src/serialization/beast2/v5/boundary.js +297 -0
  141. package/dist/src/serialization/beast2/v5/boundary.js.map +1 -0
  142. package/dist/src/serialization/beast2/v5/boundary.spec.d.ts +6 -0
  143. package/dist/src/serialization/beast2/v5/boundary.spec.d.ts.map +1 -0
  144. package/dist/src/serialization/beast2/v5/boundary.spec.js +479 -0
  145. package/dist/src/serialization/beast2/v5/boundary.spec.js.map +1 -0
  146. package/dist/src/serialization/beast2/v5/codec.d.ts +6 -5
  147. package/dist/src/serialization/beast2/v5/codec.d.ts.map +1 -1
  148. package/dist/src/serialization/beast2/v5/codec.js +18 -3
  149. package/dist/src/serialization/beast2/v5/codec.js.map +1 -1
  150. package/dist/src/serialization/beast2/v5/deflate.d.ts +24 -2
  151. package/dist/src/serialization/beast2/v5/deflate.d.ts.map +1 -1
  152. package/dist/src/serialization/beast2/v5/deflate.js +47 -12
  153. package/dist/src/serialization/beast2/v5/deflate.js.map +1 -1
  154. package/dist/src/serialization/beast2/v5/deflate.spec.js +15 -1
  155. package/dist/src/serialization/beast2/v5/deflate.spec.js.map +1 -1
  156. package/dist/src/serialization/beast2/v5/elements.d.ts +64 -0
  157. package/dist/src/serialization/beast2/v5/elements.d.ts.map +1 -0
  158. package/dist/src/serialization/beast2/v5/elements.js +320 -0
  159. package/dist/src/serialization/beast2/v5/elements.js.map +1 -0
  160. package/dist/src/serialization/beast2/v5/elements.spec.d.ts +6 -0
  161. package/dist/src/serialization/beast2/v5/elements.spec.d.ts.map +1 -0
  162. package/dist/src/serialization/beast2/v5/elements.spec.js +161 -0
  163. package/dist/src/serialization/beast2/v5/elements.spec.js.map +1 -0
  164. package/dist/src/serialization/beast2/v5/frame-pool.d.ts +62 -21
  165. package/dist/src/serialization/beast2/v5/frame-pool.d.ts.map +1 -1
  166. package/dist/src/serialization/beast2/v5/frame-pool.js +138 -27
  167. package/dist/src/serialization/beast2/v5/frame-pool.js.map +1 -1
  168. package/dist/src/serialization/beast2/v5/frame-pool.spec.js +198 -71
  169. package/dist/src/serialization/beast2/v5/frame-pool.spec.js.map +1 -1
  170. package/dist/src/serialization/beast2/v5/frame-worker.js +25 -15
  171. package/dist/src/serialization/beast2/v5/frame-worker.js.map +1 -1
  172. package/dist/src/serialization/beast2/v5/frames.d.ts +22 -12
  173. package/dist/src/serialization/beast2/v5/frames.d.ts.map +1 -1
  174. package/dist/src/serialization/beast2/v5/frames.js +52 -27
  175. package/dist/src/serialization/beast2/v5/frames.js.map +1 -1
  176. package/dist/src/serialization/beast2/v5/geometry.d.ts +38 -1
  177. package/dist/src/serialization/beast2/v5/geometry.d.ts.map +1 -1
  178. package/dist/src/serialization/beast2/v5/geometry.js +73 -1
  179. package/dist/src/serialization/beast2/v5/geometry.js.map +1 -1
  180. package/dist/src/serialization/beast2/v5/geometry.spec.js +34 -23
  181. package/dist/src/serialization/beast2/v5/geometry.spec.js.map +1 -1
  182. package/dist/src/serialization/beast2/v5/index.spec.js +32 -28
  183. package/dist/src/serialization/beast2/v5/index.spec.js.map +1 -1
  184. package/dist/src/serialization/beast2/v5/lazy.d.ts +42 -1
  185. package/dist/src/serialization/beast2/v5/lazy.d.ts.map +1 -1
  186. package/dist/src/serialization/beast2/v5/lazy.js +143 -28
  187. package/dist/src/serialization/beast2/v5/lazy.js.map +1 -1
  188. package/dist/src/serialization/beast2/v5/lazy.spec.js +102 -24
  189. package/dist/src/serialization/beast2/v5/lazy.spec.js.map +1 -1
  190. package/dist/src/serialization/beast2/v5/manifest-writer.d.ts +110 -0
  191. package/dist/src/serialization/beast2/v5/manifest-writer.d.ts.map +1 -0
  192. package/dist/src/serialization/beast2/v5/manifest-writer.js +118 -0
  193. package/dist/src/serialization/beast2/v5/manifest-writer.js.map +1 -0
  194. package/dist/src/serialization/beast2/v5/manifest-writer.spec.d.ts +6 -0
  195. package/dist/src/serialization/beast2/v5/manifest-writer.spec.d.ts.map +1 -0
  196. package/dist/src/serialization/beast2/v5/manifest-writer.spec.js +207 -0
  197. package/dist/src/serialization/beast2/v5/manifest-writer.spec.js.map +1 -0
  198. package/dist/src/serialization/beast2/v5/manifest.d.ts +241 -0
  199. package/dist/src/serialization/beast2/v5/manifest.d.ts.map +1 -0
  200. package/dist/src/serialization/beast2/v5/manifest.js +233 -0
  201. package/dist/src/serialization/beast2/v5/manifest.js.map +1 -0
  202. package/dist/src/serialization/beast2/v5/merge.d.ts +93 -0
  203. package/dist/src/serialization/beast2/v5/merge.d.ts.map +1 -0
  204. package/dist/src/serialization/beast2/v5/merge.js +202 -0
  205. package/dist/src/serialization/beast2/v5/merge.js.map +1 -0
  206. package/dist/src/serialization/beast2/v5/merge.spec.d.ts +6 -0
  207. package/dist/src/serialization/beast2/v5/merge.spec.d.ts.map +1 -0
  208. package/dist/src/serialization/beast2/v5/merge.spec.js +118 -0
  209. package/dist/src/serialization/beast2/v5/merge.spec.js.map +1 -0
  210. package/dist/src/serialization/beast2/v5/recut.d.ts +143 -0
  211. package/dist/src/serialization/beast2/v5/recut.d.ts.map +1 -0
  212. package/dist/src/serialization/beast2/v5/recut.js +365 -0
  213. package/dist/src/serialization/beast2/v5/recut.js.map +1 -0
  214. package/dist/src/serialization/beast2/v5/recut.spec.d.ts +6 -0
  215. package/dist/src/serialization/beast2/v5/recut.spec.d.ts.map +1 -0
  216. package/dist/src/serialization/beast2/v5/recut.spec.js +215 -0
  217. package/dist/src/serialization/beast2/v5/recut.spec.js.map +1 -0
  218. package/dist/src/serialization/beast2/v5/runs.d.ts +148 -0
  219. package/dist/src/serialization/beast2/v5/runs.d.ts.map +1 -0
  220. package/dist/src/serialization/beast2/v5/runs.js +260 -0
  221. package/dist/src/serialization/beast2/v5/runs.js.map +1 -0
  222. package/dist/src/serialization/beast2/v5/runs.spec.d.ts +6 -0
  223. package/dist/src/serialization/beast2/v5/runs.spec.d.ts.map +1 -0
  224. package/dist/src/serialization/beast2/v5/runs.spec.js +182 -0
  225. package/dist/src/serialization/beast2/v5/runs.spec.js.map +1 -0
  226. package/dist/src/serialization/beast2/v5/sha256.d.ts +19 -0
  227. package/dist/src/serialization/beast2/v5/sha256.d.ts.map +1 -0
  228. package/dist/src/serialization/beast2/v5/sha256.js +103 -0
  229. package/dist/src/serialization/beast2/v5/sha256.js.map +1 -0
  230. package/dist/src/serialization/beast2/v5/sha256.spec.d.ts +6 -0
  231. package/dist/src/serialization/beast2/v5/sha256.spec.d.ts.map +1 -0
  232. package/dist/src/serialization/beast2/v5/sha256.spec.js +42 -0
  233. package/dist/src/serialization/beast2/v5/sha256.spec.js.map +1 -0
  234. package/dist/src/serialization/beast2/v5/stream.d.ts +273 -86
  235. package/dist/src/serialization/beast2/v5/stream.d.ts.map +1 -1
  236. package/dist/src/serialization/beast2/v5/stream.js +576 -244
  237. package/dist/src/serialization/beast2/v5/stream.js.map +1 -1
  238. package/dist/src/serialization/beast2/v5/sync-reader.spec.js +24 -10
  239. package/dist/src/serialization/beast2/v5/sync-reader.spec.js.map +1 -1
  240. package/dist/src/serialization/index.d.ts +1 -1
  241. package/dist/src/serialization/index.d.ts.map +1 -1
  242. package/dist/src/serialization/index.js +1 -1
  243. package/dist/src/serialization/index.js.map +1 -1
  244. package/dist/src/serialization/json.d.ts +28 -0
  245. package/dist/src/serialization/json.d.ts.map +1 -1
  246. package/dist/src/serialization/json.js +141 -30
  247. package/dist/src/serialization/json.js.map +1 -1
  248. package/dist/src/serialization/json.spec.js +47 -2
  249. package/dist/src/serialization/json.spec.js.map +1 -1
  250. package/dist/src/serialization/json_schema.d.ts +15 -11
  251. package/dist/src/serialization/json_schema.d.ts.map +1 -1
  252. package/dist/src/serialization/json_schema.js +21 -36
  253. package/dist/src/serialization/json_schema.js.map +1 -1
  254. package/dist/src/serialization/json_schema.spec.js +26 -32
  255. package/dist/src/serialization/json_schema.spec.js.map +1 -1
  256. package/dist/src/serialization/json_schema_to_type.d.ts +10 -5
  257. package/dist/src/serialization/json_schema_to_type.d.ts.map +1 -1
  258. package/dist/src/serialization/json_schema_to_type.js +13 -6
  259. package/dist/src/serialization/json_schema_to_type.js.map +1 -1
  260. package/dist/src/serialization/json_schema_to_type.spec.js +19 -0
  261. package/dist/src/serialization/json_schema_to_type.spec.js.map +1 -1
  262. package/dist/src/string_order.d.ts +36 -0
  263. package/dist/src/string_order.d.ts.map +1 -0
  264. package/dist/src/string_order.js +64 -0
  265. package/dist/src/string_order.js.map +1 -0
  266. package/dist/src/string_order.spec.d.ts +6 -0
  267. package/dist/src/string_order.spec.d.ts.map +1 -0
  268. package/dist/src/string_order.spec.js +79 -0
  269. package/dist/src/string_order.spec.js.map +1 -0
  270. package/dist/src/type_of_type.d.ts +17 -0
  271. package/dist/src/type_of_type.d.ts.map +1 -1
  272. package/dist/src/type_of_type.js +66 -0
  273. package/dist/src/type_of_type.js.map +1 -1
  274. package/dist/src/type_of_type.to_etv.spec.js +69 -1
  275. package/dist/src/type_of_type.to_etv.spec.js.map +1 -1
  276. package/package.json +1 -1
@@ -5,9 +5,15 @@
5
5
  /**
6
6
  * Beast2 v5 streaming and paging APIs.
7
7
  *
8
+ * - {@link Beast2ElementWriter} — the canonical writer: elements in, segments
9
+ * out where the content-defined cut rule places them, so a collection's
10
+ * bytes are a function of its value alone — as one blob, or as the
11
+ * standalone segment blobs a manifest names. Every writer of a stored
12
+ * collection writes through it.
13
+ * - {@link encodeBeast2PagedFor} — one whole value through the element writer.
8
14
  * - {@link Beast2Writer} — append-only streaming writer: each `write(batch)`
9
15
  * becomes one root segment (one frame), so writer memory is O(batch), never
10
- * the whole collection.
16
+ * the whole collection. The geometry is the caller's.
11
17
  * - {@link encodeBeast2SegmentsFor} — in-memory convenience over the writer.
12
18
  * - {@link iterBeast2SegmentsFor} — sequential segment iterator: yields one
13
19
  * decoded collection per root segment with O(segment) decoded memory.
@@ -15,9 +21,10 @@
15
21
  * `elementCount`, per-segment decode via footer + index seeks.
16
22
  *
17
23
  * Streaming roots are collections (Array/Set/Dict). Writers default to
18
- * self-contained segments (aliasing scoped per segment) so their output is
19
- * pageable and parallel-decodable; pass `selfContained: false` to keep
20
- * whole-stream aliasing at the cost of random access.
24
+ * self-contained output — aliasing scoped per root element — so segments are
25
+ * pageable and parallel-decodable and an element's bytes depend on the element
26
+ * alone; pass `selfContained: false` to keep whole-stream aliasing at the cost
27
+ * of random access.
21
28
  */
22
29
  import { EastTypeValueType, isTypeValueEqual } from "../../../type_of_type.js";
23
30
  import { printFor } from "../../east.js";
@@ -29,9 +36,11 @@ import { SortedMap } from "../../../containers/sortedmap.js";
29
36
  import { buildPlatformContext } from "../shared.js";
30
37
  import { writeTypeSection, readTypeSection, asTypeValue } from "./type-section.js";
31
38
  import { FrameReader, openFramePrefix, writeFrame } from "./frames.js";
32
- import { FRAME_HEADER_MAX, framePool } from "./frame-pool.js";
39
+ import { framePool } from "./frame-pool.js";
33
40
  import { MAGIC_BYTES_V5, TAG_NEW, createV5EncodeContext, buildV5Encoder, buildV5Decoder, writeSourceMapSectionV5, readSourceMapSectionV5, writeIndexAndFooter, readIndex, isSegmentedRoot, } from "./codec.js";
34
41
  import { TAG_OR_TERMINATOR_FRAME, bytesReader, isBeast2SyncRangeReader, readExact, readU64LE, readBeast2ExtentsSync } from "./range.js";
42
+ import { SegmentCutter, decodeBeast2FenceFor } from "./boundary.js";
43
+ import { isBeast2ManifestSource } from "./manifest.js";
35
44
  /** Validates that a root type is streamable and returns its kind. */
36
45
  function checkSegmented(typeValue) {
37
46
  if (!isSegmentedRoot(typeValue)) {
@@ -50,9 +59,120 @@ function orderCmpFor(typeValue, kind) {
50
59
  * frame pool: workers cost tens of milliseconds to start, and a value this
51
60
  * small deflates faster than that on one thread. */
52
61
  const POOL_MIN_LOGICAL_BYTES = 8 * 1024 * 1024;
53
- /** Segments a pooled writer writes before it may demote itself to inline
54
- * framing because its caller keeps settling (see `Beast2Writer.settle`). */
55
- const POOL_DEMOTE_MIN_SEGMENTS = 8;
62
+ // =============================================================================
63
+ // Framing
64
+ // =============================================================================
65
+ /**
66
+ * Frames segments' logical bytes — inline, or on the frame pool — and lands
67
+ * each frame in the order its segment was submitted, with what the submitter
68
+ * said about it. Both writers frame through one of these.
69
+ */
70
+ class FramePipeline {
71
+ codec;
72
+ parallel;
73
+ land;
74
+ /** `undefined` until decided; `null` when frames are written inline. */
75
+ pool;
76
+ logicalWritten = 0;
77
+ /** Frames submitted to the pool and not yet landed, in order. */
78
+ inflight = [];
79
+ constructor(codec, parallel, land) {
80
+ this.codec = codec;
81
+ this.parallel = parallel;
82
+ this.land = land;
83
+ }
84
+ /** Frames one segment, or hands it to the pool; its frame lands once every
85
+ * frame submitted before it has. */
86
+ submit(logical, info) {
87
+ this.logicalWritten += logical.length;
88
+ const pool = this.poolFor();
89
+ if (pool === null) {
90
+ const frame = new BufferWriter();
91
+ writeFrame(frame, logical, this.codec);
92
+ this.land(frame.toUint8Array(), info);
93
+ return;
94
+ }
95
+ // Back-pressure: at most two frames per worker in flight, so the
96
+ // writer's memory stays O(workers x segment).
97
+ while (this.inflight.length >= pool.workers * 2)
98
+ this.appendFrames(1);
99
+ this.inflight.push({ frame: pool.submit(logical, this.codec), info });
100
+ this.appendFrames(0);
101
+ }
102
+ /** Lands every in-flight frame. */
103
+ settle() {
104
+ this.appendFrames(Infinity);
105
+ }
106
+ /** The pool to frame on — decided once a parallel writer has produced
107
+ * enough bytes to be worth it. While frames are in flight the writer keeps
108
+ * the pool they are on, so frames cannot interleave out of order; with none
109
+ * in flight it follows the process's current pool — a new one after an idle
110
+ * pool retired its workers, `null` once a pool has lost a worker — so a
111
+ * writer never submits to terminated workers. */
112
+ poolFor() {
113
+ if (this.pool === undefined) {
114
+ if (!this.parallel || this.logicalWritten < POOL_MIN_LOGICAL_BYTES)
115
+ return null;
116
+ this.pool = framePool();
117
+ }
118
+ else if (this.pool !== null && this.inflight.length === 0) {
119
+ this.pool = framePool();
120
+ }
121
+ return this.pool;
122
+ }
123
+ /** Lands the done frames at the head of the in-flight queue, in order;
124
+ * blocks until at least `minimum` have landed (`Infinity` settles the
125
+ * queue). */
126
+ appendFrames(minimum) {
127
+ let appended = 0;
128
+ while (this.inflight.length > 0) {
129
+ const head = this.inflight[0];
130
+ if (appended >= minimum && !head.frame.ready())
131
+ return;
132
+ const bytes = head.frame.take();
133
+ this.inflight.shift();
134
+ this.land(bytes, head.info);
135
+ appended++;
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * The header a collection blob of `typeValue` starts with: magic, type
141
+ * section, source-map section, then the root tag as its own frame so every
142
+ * indexed segment frame is pure. A caller-provided prefix (splice tooling) is
143
+ * the header instead — after verifying its wire type IS the declared type,
144
+ * since a mismatched prefix would write a blob whose header lies about its
145
+ * contents.
146
+ */
147
+ function headerFor(typeValue, sourceMap, headerPrefix) {
148
+ if (headerPrefix !== undefined) {
149
+ verifyV5Magic(headerPrefix);
150
+ const prefixReader = new BufferReader(headerPrefix, MAGIC_BYTES_V5.length);
151
+ const { rootType } = readTypeSection(prefixReader);
152
+ if (!isTypeValueEqual(rootType, typeValue)) {
153
+ const printType = printFor(EastTypeValueType);
154
+ throw new TypeError(`beast2 v5: headerPrefix declares wire type ${printType(rootType)}, not the writer's ${printType(typeValue)} — the prefix must come from a blob of the same wire type`);
155
+ }
156
+ return headerPrefix;
157
+ }
158
+ const head = new BufferWriter();
159
+ head.writeBytes(MAGIC_BYTES_V5);
160
+ writeTypeSection(typeValue, head);
161
+ writeSourceMapSectionV5(sourceMap, head);
162
+ writeFrame(head, new Uint8Array([TAG_NEW]), "none");
163
+ return head.toUint8Array();
164
+ }
165
+ /** A segment as a standalone blob: the collection's header, the segment's
166
+ * frame, the terminator, and an index naming the one segment — byte for byte
167
+ * what carving it out of the whole blob gives. */
168
+ function segmentBlob(head, frame, count) {
169
+ const blob = new BufferWriter(head.length + frame.length + TAG_OR_TERMINATOR_FRAME.length + 32);
170
+ blob.writeBytes(head);
171
+ blob.writeBytes(frame);
172
+ blob.writeBytes(TAG_OR_TERMINATOR_FRAME);
173
+ writeIndexAndFooter(blob, [{ offset: head.length, count }], true);
174
+ return blob.toUint8Array();
175
+ }
56
176
  // =============================================================================
57
177
  // Streaming writer
58
178
  // =============================================================================
@@ -66,6 +186,10 @@ const POOL_DEMOTE_MIN_SEGMENTS = 8;
66
186
  * {@link finish} appends the terminator (plus index and footer by default),
67
187
  * so the byte stream is append-only end to end.
68
188
  *
189
+ * Where the segments fall is the caller's choice here. A collection that is
190
+ * stored is written through {@link Beast2ElementWriter} instead, which cuts
191
+ * where the content-defined rule says, so equal values store as equal bytes.
192
+ *
69
193
  * @example
70
194
  * ```ts
71
195
  * const chunks: Uint8Array[] = [];
@@ -82,7 +206,6 @@ export class Beast2Writer {
82
206
  segments = 0;
83
207
  kind;
84
208
  sink;
85
- codec;
86
209
  selfContained;
87
210
  withIndex;
88
211
  ctx;
@@ -93,15 +216,9 @@ export class Beast2Writer {
93
216
  hasLast = false;
94
217
  bytesWritten = 0;
95
218
  finished = false;
96
- parallel;
97
- /** `undefined` until decided; `null` when frames are written inline. */
98
- pool;
99
- logicalWritten = 0;
100
- /** Frames submitted to the pool and not yet on the sink, in order. */
101
- inflight = [];
102
- inflightLogical = 0;
103
- /** {@link settle} calls that found frames in flight. */
104
- settlesInflight = 0;
219
+ /** Frames each segment and puts it on the sink as it lands, recording its
220
+ * index entry there. */
221
+ frames;
105
222
  /**
106
223
  * @param type - the collection type this stream holds (Array/Set/Dict)
107
224
  * @param sink - receives output bytes as they are produced
@@ -113,19 +230,29 @@ export class Beast2Writer {
113
230
  const typeValue = asTypeValue(type);
114
231
  this.kind = checkSegmented(typeValue);
115
232
  this.sink = sink;
116
- this.codec = options?.codec ?? "deflate";
117
233
  this.selfContained = options?.selfContained ?? true;
118
234
  this.withIndex = options?.index ?? true;
119
- this.parallel = options?.parallel ?? false;
120
235
  this.orderCmp = orderCmpFor(typeValue, this.kind);
236
+ this.frames = new FramePipeline(options?.codec ?? "deflate", options?.parallel ?? false, (frame, count) => {
237
+ this.index.push({ offset: this.bytesWritten, count });
238
+ this.emit(frame);
239
+ });
121
240
  const sourceMap = options?.sourceMap ?? null;
122
241
  this.ctx = createV5EncodeContext(sourceMap, this.selfContained);
242
+ // A self-contained stream starts every root element with an empty identity
243
+ // map, so no REF reaches past the element it sits in: sharing inside an
244
+ // element is kept, sharing between elements is written out again.
245
+ const scoped = this.selfContained;
123
246
  const typeCtx = new Map();
124
247
  if (this.kind === "Dict") {
125
248
  const key = buildV5Encoder(typeValue.value.key, typeCtx);
126
249
  const val = buildV5Encoder(typeValue.value.value, typeCtx);
127
250
  this.encodeElems = (value, logical) => {
128
251
  for (const [k, v] of value) {
252
+ if (scoped) {
253
+ this.ctx.containerIndex.clear();
254
+ this.ctx.segmentBaseDef = this.ctx.containerCount;
255
+ }
129
256
  key(k, logical, this.ctx);
130
257
  val(v, logical, this.ctx);
131
258
  }
@@ -134,39 +261,22 @@ export class Beast2Writer {
134
261
  else {
135
262
  const elem = buildV5Encoder(typeValue.value, typeCtx);
136
263
  this.encodeElems = (value, logical) => {
137
- for (const item of value)
264
+ for (const item of value) {
265
+ if (scoped) {
266
+ this.ctx.containerIndex.clear();
267
+ this.ctx.segmentBaseDef = this.ctx.containerCount;
268
+ }
138
269
  elem(item, logical, this.ctx);
270
+ }
139
271
  };
140
272
  }
141
- // Header: magic + type section + source map section, then the root tag
142
- // as its own frame so every indexed segment frame is pure. A caller-
143
- // provided prefix (splice tooling) is emitted verbatim instead — after
144
- // verifying its wire type IS the declared type, since a mismatched
145
- // prefix would write a blob whose header lies about its contents.
146
- if (options?.headerPrefix !== undefined) {
147
- verifyV5Magic(options.headerPrefix);
148
- const prefixReader = new BufferReader(options.headerPrefix, MAGIC_BYTES_V5.length);
149
- const { rootType } = readTypeSection(prefixReader);
150
- if (!isTypeValueEqual(rootType, typeValue)) {
151
- const printType = printFor(EastTypeValueType);
152
- throw new TypeError(`beast2 v5: headerPrefix declares wire type ${printType(rootType)}, not the writer's ${printType(typeValue)} — the prefix must come from a blob of the same wire type`);
153
- }
154
- this.ctx.containerCount = 1;
155
- this.ctx.segmentBaseDef = 1;
156
- this.emit(options.headerPrefix);
157
- return;
158
- }
159
- const head = new BufferWriter();
160
- head.writeBytes(MAGIC_BYTES_V5);
161
- writeTypeSection(typeValue, head);
162
- writeSourceMapSectionV5(sourceMap, head);
163
- writeFrame(head, new Uint8Array([TAG_NEW]), "none");
273
+ const head = headerFor(typeValue, sourceMap, options?.headerPrefix);
164
274
  // The root container consumes definition 0 (no root object exists on the
165
275
  // encode side — batches are independent values, so nothing can alias it);
166
276
  // segments scope from definition 1 to match the decoder's numbering.
167
277
  this.ctx.containerCount = 1;
168
278
  this.ctx.segmentBaseDef = 1;
169
- this.emit(head.toUint8Array());
279
+ this.emit(head);
170
280
  }
171
281
  /**
172
282
  * Encodes one batch as one root segment.
@@ -195,106 +305,53 @@ export class Beast2Writer {
195
305
  return;
196
306
  if (this.orderCmp)
197
307
  this.checkAscent(batch);
198
- if (this.selfContained) {
199
- this.ctx.containerIndex.clear();
200
- this.ctx.segmentBaseDef = this.ctx.containerCount;
201
- }
202
308
  const logical = new BufferWriter();
203
309
  logical.writeVarint(count);
204
310
  this.encodeElems(batch, logical);
205
- const logicalBytes = logical.toUint8Array();
206
- this.segments++;
207
- this.logicalWritten += logicalBytes.length;
208
- const pool = this.poolFor();
209
- if (pool === null) {
210
- const frame = new BufferWriter();
211
- writeFrame(frame, logicalBytes, this.codec);
212
- this.index.push({ offset: this.bytesWritten, count });
213
- this.emit(frame.toUint8Array());
214
- return;
215
- }
216
- // A caller whose batch decisions keep landing between the bounds settles
217
- // on nearly every segment, which makes the pool pure overhead. Framing
218
- // strategy never changes a byte, so demote: drain in order, then frame
219
- // inline for the rest of the stream.
220
- if (this.segments >= POOL_DEMOTE_MIN_SEGMENTS && this.settlesInflight * 2 >= this.segments) {
221
- this.appendFrames(Infinity);
222
- this.pool = null;
223
- const frame = new BufferWriter();
224
- writeFrame(frame, logicalBytes, this.codec);
225
- this.index.push({ offset: this.bytesWritten, count });
226
- this.emit(frame.toUint8Array());
227
- return;
228
- }
229
- // Back-pressure: at most two frames per worker in flight, so the
230
- // writer's memory stays O(workers x segment).
231
- while (this.inflight.length >= pool.workers * 2)
232
- this.appendFrames(1);
233
- this.inflight.push({ frame: pool.submit(logicalBytes, this.codec), count });
234
- this.inflightLogical += logicalBytes.length;
235
- this.appendFrames(0);
311
+ this.frameSegment(count, logical.toUint8Array());
236
312
  }
237
313
  /**
238
- * Brackets the total bytes the writer will have passed to its sink once
239
- * every in-flight frame lands.
314
+ * Writes one root segment from elements that are already encoded — each in
315
+ * its canonical bytes, with aliasing scoped to itself — which is how
316
+ * {@link Beast2ElementWriter} writes the segments it cuts.
240
317
  *
241
- * `lo` is exactly the bytes the sink has received; `hi` adds each
242
- * in-flight frame's logical bytes plus a header bound (a frame's payload
243
- * never exceeds its logical bytes). A serial writer has `lo === hi`. A
244
- * decision that is monotone in the byte count — the paged encoders' batch
245
- * refinement — is exact when it agrees at both bounds; otherwise
246
- * {@link settle} first.
318
+ * The elements are copied before this returns, so the caller may reuse the
319
+ * buffer. Their order is the caller's to keep: nothing here decodes them to
320
+ * check a Set or Dict's ascent.
247
321
  *
248
- * @returns the bounds, in bytes
322
+ * @param count - the elements (pairs, for a Dict) the bytes hold; a
323
+ * zero-count segment is skipped
324
+ * @param elements - the elements' bytes, back to back
325
+ * @throws {Error} When called after {@link finish}, or — for a parallel
326
+ * writer — when a frame worker failed (see {@link write}).
249
327
  */
250
- emittedBounds() {
251
- this.appendFrames(0);
252
- return {
253
- lo: this.bytesWritten,
254
- hi: this.bytesWritten + this.inflightLogical + this.inflight.length * FRAME_HEADER_MAX,
255
- };
256
- }
257
- /** Waits for every in-flight frame and passes it to the sink, after which
258
- * {@link emittedBounds} is exact. A no-op for a serial writer. */
259
- settle() {
260
- if (this.inflight.length > 0)
261
- this.settlesInflight++;
262
- this.appendFrames(Infinity);
328
+ writeEncodedSegment(count, elements) {
329
+ if (this.finished)
330
+ throw new Error("write() after finish()");
331
+ if (count === 0)
332
+ return;
333
+ const logical = new BufferWriter(elements.length + 10);
334
+ logical.writeVarint(count);
335
+ logical.writeBytes(elements);
336
+ this.frameSegment(count, logical.toUint8Array());
263
337
  }
264
- /** The pool to frame on — decided once a parallel writer has produced
265
- * enough bytes to be worth it. While frames are in flight the writer keeps
266
- * the pool they are on, so frames cannot interleave out of order; with none
267
- * in flight it follows the process's current pool — a new one after an idle
268
- * pool retired its workers, `null` once a pool has lost a worker — so a
269
- * writer never submits to terminated workers. A writer demoted to inline
270
- * framing stays inline. */
271
- poolFor() {
272
- if (this.pool === undefined) {
273
- if (!this.parallel || this.logicalWritten < POOL_MIN_LOGICAL_BYTES)
274
- return null;
275
- this.pool = framePool();
276
- }
277
- else if (this.pool !== null && this.inflight.length === 0) {
278
- this.pool = framePool();
279
- }
280
- return this.pool;
338
+ /** Frames one segment's logical bytes — inline, or on the frame pool — and
339
+ * records its index entry as its frame lands. */
340
+ frameSegment(count, logicalBytes) {
341
+ this.segments++;
342
+ this.frames.submit(logicalBytes, count);
281
343
  }
282
- /** Passes the done frames at the head of the in-flight queue to the sink,
283
- * in order, assigning index offsets as they land; blocks until at least
284
- * `minimum` have been passed (`Infinity` settles the queue). */
285
- appendFrames(minimum) {
286
- let appended = 0;
287
- while (this.inflight.length > 0) {
288
- const head = this.inflight[0];
289
- if (appended >= minimum && !head.frame.ready())
290
- return;
291
- const bytes = head.frame.take();
292
- this.inflight.shift();
293
- this.inflightLogical -= head.frame.logicalLength;
294
- this.index.push({ offset: this.bytesWritten, count: head.count });
295
- this.emit(bytes);
296
- appended++;
297
- }
344
+ /**
345
+ * Waits for every in-flight frame and passes it to the sink, so the sink
346
+ * holds every segment written so far. A writer about to go quiet then holds
347
+ * nothing on the pool, which lets an idle pool retire its workers. A no-op
348
+ * for a serial writer.
349
+ *
350
+ * @throws {Error} For a parallel writer, when an in-flight frame's worker
351
+ * failed or stopped responding (see {@link write}).
352
+ */
353
+ settle() {
354
+ this.frames.settle();
298
355
  }
299
356
  /**
300
357
  * Terminates the stream: writes the terminator frame and, unless disabled,
@@ -306,7 +363,7 @@ export class Beast2Writer {
306
363
  finish() {
307
364
  if (this.finished)
308
365
  return;
309
- this.appendFrames(Infinity);
366
+ this.frames.settle();
310
367
  this.finished = true;
311
368
  const tail = new BufferWriter();
312
369
  writeFrame(tail, new Uint8Array([0x00]), "none");
@@ -386,30 +443,291 @@ export function encodeBeast2SegmentsFor(type, options) {
386
443
  return out;
387
444
  };
388
445
  }
389
- // =============================================================================
390
- // Paged whole-value encode
391
- // =============================================================================
392
- /** Default element cap per segment for {@link encodeBeast2PagedFor}. Small
393
- * enough that one segment decodes cheaply, large enough that frames stay far
394
- * above the compression threshold and per-segment overhead (frame header +
395
- * index entry) is negligible. */
396
- export const BEAST2_PAGED_BATCH_DEFAULT = 1_000;
397
- /** Default wire-byte target per segment for {@link encodeBeast2PagedFor}.
398
- * Wide rows would otherwise make element-capped segments arbitrarily large —
399
- * and a paging reader decodes whole segments, so segment size IS the random-
400
- * access cost. Batching adapts toward this target from measured output. */
401
- export const BEAST2_PAGED_TARGET_BYTES_DEFAULT = 2 * 1024 * 1024;
402
- /** The probe batch that seeds the byte-adaptive batching — small, so one
403
- * pathologically wide first batch cannot blow past the target unmeasured.
404
- * Exported so every writer of a collection blob seeds the same way: an
405
- * emit sink that opened at the element cap instead segmented a wide-rowed
406
- * value differently from this encoder, giving one value two hashes. */
407
- export const BEAST2_PAGED_PROBE_BATCH = 16;
408
- const PAGED_PROBE_BATCH = BEAST2_PAGED_PROBE_BATCH;
446
+ /**
447
+ * Builds an encoder of a collection's root elements, one at a time, each in
448
+ * its canonical bytes: aliasing is scoped to the element, so no REF reaches a
449
+ * neighbour and the element's bytes depend on it alone.
450
+ *
451
+ * @param typeValue - the collection type (Array/Set/Dict)
452
+ * @param sourceMap - the stream's source map, for function values
453
+ * @returns a function appending one element's bytes to a writer and returning
454
+ * the length of its key: a Set element's whole length, a Dict pair's key, 0
455
+ * for an Array element, which has none
456
+ * @throws {TypeError} When `typeValue` is not an Array, Set or Dict type.
457
+ * @internal
458
+ */
459
+ export function elementEncoderFor(typeValue, sourceMap) {
460
+ const kind = checkSegmented(typeValue);
461
+ const ctx = createV5EncodeContext(sourceMap, true);
462
+ // The root container is definition 0, so elements define from 1.
463
+ ctx.containerCount = 1;
464
+ const typeCtx = new Map();
465
+ if (kind === "Dict") {
466
+ const key = buildV5Encoder(typeValue.value.key, typeCtx);
467
+ const value = buildV5Encoder(typeValue.value.value, typeCtx);
468
+ return (element, writer) => {
469
+ ctx.containerIndex.clear();
470
+ ctx.segmentBaseDef = ctx.containerCount;
471
+ const start = writer.size;
472
+ key(element[0], writer, ctx);
473
+ const keyLength = writer.size - start;
474
+ value(element[1], writer, ctx);
475
+ return keyLength;
476
+ };
477
+ }
478
+ const elem = buildV5Encoder(typeValue.value, typeCtx);
479
+ const keyed = kind === "Set";
480
+ return (element, writer) => {
481
+ ctx.containerIndex.clear();
482
+ ctx.segmentBaseDef = ctx.containerCount;
483
+ const start = writer.size;
484
+ elem(element, writer, ctx);
485
+ return keyed ? writer.size - start : 0;
486
+ };
487
+ }
488
+ /**
489
+ * The canonical writer of a collection: elements go in one at a time, in
490
+ * canonical order, and segments come out wherever the content-defined cut rule
491
+ * ({@link SegmentCutter}) places them — as one blob, or as the standalone
492
+ * segment blobs a manifest names, one at a time to a {@link Beast2SegmentSink}.
493
+ *
494
+ * Each element is encoded as it arrives, with aliasing scoped to itself, so
495
+ * its bytes depend on the element alone — and the cut rule reads nothing but
496
+ * those bytes. The blob is therefore a function of the value: whichever
497
+ * process writes a collection, in whichever runtime, and however its elements
498
+ * were produced, it writes the same bytes. That is what lets a
499
+ * content-addressed store keep an equal value once and diff two values by
500
+ * segment.
501
+ *
502
+ * Memory is one open segment. Elements that are already encoded — sorted runs,
503
+ * a merge, a re-cut — go in through {@link addEncoded}, without a decode.
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * const chunks: Uint8Array[] = [];
508
+ * const type = DictType(StringType, IntegerType);
509
+ * const writer = new Beast2ElementWriter(type, (b) => chunks.push(b));
510
+ * writer.add(["a", 1n]);
511
+ * writer.add(["b", 2n]);
512
+ * writer.finish();
513
+ * decodeBeast2For(type)(Buffer.concat(chunks)); // Map { "a" => 1n, "b" => 2n }
514
+ *
515
+ * const segments: Beast2Segment[] = [];
516
+ * const split = new Beast2ElementWriter(type, { segment: (s) => segments.push(s) });
517
+ * split.add(["a", 1n]);
518
+ * split.finish();
519
+ * segments[0].blob; // segment 0 as a standalone blob, under split.header
520
+ * ```
521
+ */
522
+ export class Beast2ElementWriter {
523
+ kind;
524
+ /** The writer of the one blob, when that is the output. */
525
+ blob;
526
+ /** The framing of the segment blobs, when those are the output. */
527
+ frames;
528
+ head;
529
+ encodeElement;
530
+ orderCmp;
531
+ cutter = new SegmentCutter();
532
+ /** The open segment's elements, back to back. */
533
+ open = new BufferWriter();
534
+ /** Elements in the open segment. */
535
+ count = 0;
536
+ /** The length of the open segment's first key — its fence. */
537
+ firstKeyLength = 0;
538
+ written = 0;
539
+ lastKey;
540
+ hasLast = false;
541
+ finished = false;
542
+ /**
543
+ * @param type - the collection type (Array/Set/Dict)
544
+ * @param sink - receives the blob's bytes as they are produced; or, as a
545
+ * {@link Beast2SegmentSink}, each segment as a standalone blob
546
+ * @param options - codec, source map, header prefix and parallel framing
547
+ * @throws {TypeError} When `type` is not an Array, Set or Dict type, or
548
+ * when `options.headerPrefix` is not a v5 header of exactly `type`.
549
+ */
550
+ constructor(type, sink, options) {
551
+ const typeValue = asTypeValue(type);
552
+ this.kind = checkSegmented(typeValue);
553
+ this.orderCmp = orderCmpFor(typeValue, this.kind);
554
+ this.encodeElement = elementEncoderFor(typeValue, options?.sourceMap ?? null);
555
+ const head = headerFor(typeValue, options?.sourceMap ?? null, options?.headerPrefix);
556
+ this.head = head;
557
+ if (typeof sink === "function") {
558
+ this.blob = new Beast2Writer(typeValue, sink, options);
559
+ this.frames = null;
560
+ }
561
+ else {
562
+ this.blob = null;
563
+ this.frames = new FramePipeline(options?.codec ?? "deflate", options?.parallel ?? false, (frame, facts) => sink.segment({ ...facts, blob: segmentBlob(head, frame, facts.count) }));
564
+ }
565
+ }
566
+ /** Segments written so far; the open segment is not among them until the
567
+ * cut rule closes it or {@link finish} does. */
568
+ get segments() {
569
+ return this.written;
570
+ }
571
+ /** The header every segment is written under — magic, type section,
572
+ * source-map section and root tag — which a manifest names as its header. */
573
+ get header() {
574
+ return this.head;
575
+ }
576
+ /**
577
+ * Encodes one element and appends it to the collection.
578
+ *
579
+ * @param element - an Array or Set element, or a Dict's `[key, value]` pair
580
+ * @throws {Error} When called after {@link finish}, when a Set element or
581
+ * Dict key does not ascend strictly from the last in East order, or when
582
+ * the element cannot be encoded — which leaves the writer as it was.
583
+ */
584
+ add(element) {
585
+ if (this.finished)
586
+ throw new Error("add() after finish()");
587
+ let key;
588
+ if (this.orderCmp !== null) {
589
+ key = this.kind === "Dict" ? element[0] : element;
590
+ if (this.hasLast && this.orderCmp(this.lastKey, key) >= 0) {
591
+ throw new Error(`beast2 v5: ${this.kind} ${this.kind === "Dict" ? "keys" : "elements"} must arrive strictly ascending in East order — ` +
592
+ `the blob holds the canonical value; sort them first, or write arrival order as an Array`);
593
+ }
594
+ }
595
+ const start = this.open.size;
596
+ let keyLength;
597
+ try {
598
+ keyLength = this.encodeElement(element, this.open);
599
+ }
600
+ catch (err) {
601
+ const kept = this.open.toUint8Array().slice(0, start);
602
+ this.open.pop();
603
+ this.open.writeBytes(kept);
604
+ throw err;
605
+ }
606
+ if (this.orderCmp !== null) {
607
+ this.lastKey = key;
608
+ this.hasLast = true;
609
+ }
610
+ this.place(start, keyLength);
611
+ }
612
+ /**
613
+ * Appends one element that is already in its canonical bytes — encoded
614
+ * with aliasing scoped to itself, as {@link add} encodes one — without
615
+ * decoding it.
616
+ *
617
+ * Order is the caller's to keep: a Set or Dict's elements must arrive
618
+ * strictly ascending, and nothing here decodes them to check.
619
+ *
620
+ * @param element - the element's canonical bytes (a Dict pair's key, then
621
+ * its value)
622
+ * @param keyLength - the length of the key at the front of `element`: a Set
623
+ * element's whole length, a Dict pair's key; ignored for an Array
624
+ * @throws {Error} When called after {@link finish}.
625
+ */
626
+ addEncoded(element, keyLength) {
627
+ if (this.finished)
628
+ throw new Error("add() after finish()");
629
+ const start = this.open.size;
630
+ this.open.writeBytes(element);
631
+ this.place(start, keyLength);
632
+ }
633
+ /** Accounts for the element just appended at `start`, and when the cut rule
634
+ * starts a segment at it, writes out the segment that closes. */
635
+ place(start, keyLength) {
636
+ const bytes = this.open.toUint8Array();
637
+ const element = bytes.subarray(start);
638
+ // An Array element has no key, so the rule hashes it whole.
639
+ const hashed = this.kind === "Array" ? element : element.subarray(0, keyLength);
640
+ if (this.cutter.startsSegment(element.length, hashed)) {
641
+ this.writeSegment(bytes.subarray(0, start));
642
+ const carried = element.slice();
643
+ this.open.pop();
644
+ this.open.writeBytes(carried);
645
+ this.count = 0;
646
+ }
647
+ if (this.count === 0)
648
+ this.firstKeyLength = keyLength;
649
+ this.count++;
650
+ }
651
+ /**
652
+ * Waits for every in-flight frame and passes it to the sink, so the sink
653
+ * holds every segment written so far. A no-op for a serial writer.
654
+ *
655
+ * @throws {Error} For a parallel writer, when an in-flight frame's worker
656
+ * failed or stopped responding (see {@link Beast2Writer.write}).
657
+ */
658
+ settle() {
659
+ if (this.blob !== null)
660
+ this.blob.settle();
661
+ else
662
+ this.frames.settle();
663
+ }
664
+ /** Elements in the open segment.
665
+ * @internal */
666
+ get openCount() {
667
+ return this.cutter.openCount;
668
+ }
669
+ /** Logical bytes in the open segment.
670
+ * @internal */
671
+ get openBytes() {
672
+ return this.cutter.openBytes;
673
+ }
674
+ /**
675
+ * Ends the open segment here, for a caller that knows the cut rule starts
676
+ * one at what comes next without handing it over: writes the open segment,
677
+ * and makes the next element the first of a segment.
678
+ *
679
+ * @throws {Error} When called after {@link finish}.
680
+ * @internal
681
+ */
682
+ closeSegment() {
683
+ if (this.finished)
684
+ throw new Error("add() after finish()");
685
+ this.writeSegment(this.open.toUint8Array());
686
+ this.open.pop();
687
+ this.count = 0;
688
+ this.cutter = new SegmentCutter();
689
+ }
690
+ /**
691
+ * Writes the open segment and — for a blob — the terminator, index and
692
+ * footer. Idempotent.
693
+ *
694
+ * @throws {Error} For a parallel writer, when an in-flight frame's worker
695
+ * failed or stopped responding (see {@link Beast2Writer.write}).
696
+ */
697
+ finish() {
698
+ if (this.finished)
699
+ return;
700
+ this.finished = true;
701
+ this.writeSegment(this.open.toUint8Array());
702
+ if (this.blob !== null)
703
+ this.blob.finish();
704
+ else
705
+ this.frames.settle();
706
+ }
707
+ /** Writes the open segment, whose elements are `elements`, unless it is
708
+ * empty. */
709
+ writeSegment(elements) {
710
+ if (this.count === 0)
711
+ return;
712
+ this.written++;
713
+ if (this.blob !== null) {
714
+ this.blob.writeEncodedSegment(this.count, elements);
715
+ return;
716
+ }
717
+ const logical = new BufferWriter(elements.length + 10);
718
+ logical.writeVarint(this.count);
719
+ logical.writeBytes(elements);
720
+ this.frames.submit(logical.toUint8Array(), {
721
+ count: this.count,
722
+ fence: this.kind === "Array" ? new Uint8Array(0) : elements.slice(0, this.firstKeyLength),
723
+ logicalBytes: elements.length,
724
+ });
725
+ }
726
+ }
409
727
  /**
410
728
  * Builds a curried paged encoder: `encode(value)` writes one whole collection
411
- * value as a segmented, self-contained, indexed v5 blob — `batchSize` elements
412
- * per segment.
729
+ * value as a segmented, self-contained, indexed v5 blob, cut by the
730
+ * content-defined rule through {@link Beast2ElementWriter}.
413
731
  *
414
732
  * The write-side sibling of {@link openBeast2PagesFor}: a blob written this
415
733
  * way supports random access ({@link Beast2Pages.segment} /
@@ -420,7 +738,7 @@ const PAGED_PROBE_BATCH = BEAST2_PAGED_PROBE_BATCH;
420
738
  * bytes), so content-addressed stores hash the two forms differently.
421
739
  *
422
740
  * @param type - the collection type (Array/Set/Dict)
423
- * @param options - batch size, codec, and source map options
741
+ * @param options - codec and source map options
424
742
  * @returns a function encoding a collection value to an indexed v5 blob
425
743
  * @throws {TypeError} When `type` is not an Array, Set or Dict type.
426
744
  */
@@ -428,20 +746,10 @@ export function encodeBeast2PagedFor(type, options) {
428
746
  const typeValue = asTypeValue(type);
429
747
  const kind = checkSegmented(typeValue);
430
748
  const cmp = orderCmpFor(typeValue, kind);
431
- const batchCap = Math.max(1, Math.floor(options?.batchSize ?? BEAST2_PAGED_BATCH_DEFAULT));
432
- const targetBytes = Math.max(1, Math.floor(options?.targetSegmentBytes ?? BEAST2_PAGED_TARGET_BYTES_DEFAULT));
433
- const writerOptions = {
434
- ...(options?.codec !== undefined && { codec: options.codec }),
435
- ...(options?.sourceMap !== undefined && { sourceMap: options.sourceMap }),
436
- };
437
749
  return (value) => {
438
- const makeBatch = kind === "Array" ? (items) => items
439
- : kind === "Set" ? (items) => new Set(items)
440
- : (items) => new Map(items);
441
750
  // Canonical source order: SortedSet/SortedMap iterate in East order
442
- // already; a plain Set/Map (insertion order) is sorted first — segments
443
- // must hold the canonical value, and the writer validates the ascent.
444
- const iterable = kind === "Dict"
751
+ // already; a plain Set/Map (insertion order) is sorted first.
752
+ const elements = kind === "Dict"
445
753
  ? (value instanceof SortedMap
446
754
  ? value.entries()
447
755
  : [...value.entries()].sort((a, b) => cmp(a[0], b[0])))
@@ -450,71 +758,14 @@ export function encodeBeast2PagedFor(type, options) {
450
758
  ? value
451
759
  : [...value].sort(cmp))
452
760
  : value;
453
- // Byte-adaptive batching: a throwaway scratch encode of the first few
454
- // elements measures the average wire size, and batches then target
455
- // `targetSegmentBytes` (never above the element cap). Re-encoding the
456
- // probe costs a handful of elements; the real stream starts with
457
- // full-size, right-sized segments. Batching is a pure function of the
458
- // value, so the bytes stay deterministic for content-addressing.
459
- const items = iterable[Symbol.iterator]();
460
- const probe = [];
461
- while (probe.length < PAGED_PROBE_BATCH) {
462
- const n = items.next();
463
- if (n.done)
464
- break;
465
- probe.push(n.value);
466
- }
467
- let nextBatch = batchCap;
468
- if (probe.length > 0) {
469
- let scratchBytes = 0;
470
- let scratchHeader = 0;
471
- const scratch = new Beast2Writer(typeValue, (b) => { scratchBytes += b.length; }, writerOptions);
472
- scratchHeader = scratchBytes;
473
- scratch.write(makeBatch(probe));
474
- const avg = Math.max(1, (scratchBytes - scratchHeader) / probe.length);
475
- nextBatch = Math.max(1, Math.min(batchCap, Math.floor(targetBytes / avg)));
476
- }
477
761
  const chunks = [];
478
- let bodyBytes = 0;
762
+ let total = 0;
479
763
  // Frames deflate on worker threads where the runtime has them (#763).
480
- const writer = new Beast2Writer(typeValue, (b) => {
481
- chunks.push(b);
482
- bodyBytes += b.length;
483
- }, { ...writerOptions, parallel: true });
484
- const headerBytes = bodyBytes;
485
- let written = 0;
486
- let batch = [];
487
- const refine = (body) => Math.max(1, Math.min(batchCap, Math.floor(targetBytes / Math.max(1, body / written))));
488
- const flush = () => {
489
- if (batch.length === 0)
490
- return;
491
- writer.write(makeBatch(batch));
492
- written += batch.length;
493
- batch = [];
494
- // Refine toward the target as real output accumulates (drifting data).
495
- // With frames still deflating, only bounds on that output are known;
496
- // the refinement is monotone in it, so agreeing bounds ARE the serial
497
- // decision, and disagreeing ones wait for the frames. Either way the
498
- // segmentation — and every byte — is what a serial writer produces.
499
- const { lo, hi } = writer.emittedBounds();
500
- nextBatch = refine(lo - headerBytes);
501
- if (nextBatch !== refine(hi - headerBytes)) {
502
- writer.settle();
503
- nextBatch = refine(writer.emittedBounds().lo - headerBytes);
504
- }
505
- };
506
- const pump = (item) => {
507
- batch.push(item);
508
- if (batch.length >= nextBatch)
509
- flush();
510
- };
511
- for (const p of probe)
512
- pump(p);
513
- for (let n = items.next(); !n.done; n = items.next())
514
- pump(n.value);
515
- flush();
764
+ const writer = new Beast2ElementWriter(typeValue, (b) => { chunks.push(b); total += b.length; }, { ...options, parallel: true });
765
+ for (const element of elements)
766
+ writer.add(element);
516
767
  writer.finish();
517
- const out = new Uint8Array(bodyBytes);
768
+ const out = new Uint8Array(total);
518
769
  let pos = 0;
519
770
  for (const c of chunks) {
520
771
  out.set(c, pos);
@@ -555,7 +806,11 @@ function openSegmented(data, typeValue) {
555
806
  * decoded element/key must exceed `order.prev`. Passing one state across
556
807
  * consecutive segments extends the check over the segment boundary; a fresh
557
808
  * state validates a single segment in isolation. Violations are corruption
558
- * (the wire must hold the canonical value), never data to repair. */
809
+ * (the wire must hold the canonical value), never data to repair.
810
+ *
811
+ * A Set/Dict segment is a SortedSet/SortedMap under the East comparator, as a
812
+ * whole-value decode is: a plain JS Set or Map compares keys by SameValueZero,
813
+ * which reads a `-0` key as `0` and merges it with a `0` beside it. */
559
814
  function buildSegmentDecoder(typeValue, kind) {
560
815
  const typeCtx = new Map();
561
816
  const cmp = orderCmpFor(typeValue, kind);
@@ -563,7 +818,7 @@ function buildSegmentDecoder(typeValue, kind) {
563
818
  const key = buildV5Decoder(typeValue.value.key, typeCtx);
564
819
  const val = buildV5Decoder(typeValue.value.value, typeCtx);
565
820
  return (reader, ctx, n, order) => {
566
- const map = new Map();
821
+ const map = new SortedMap(undefined, cmp);
567
822
  for (let i = 0; i < n; i++) {
568
823
  const k = key(reader, ctx);
569
824
  if (order) {
@@ -584,7 +839,7 @@ function buildSegmentDecoder(typeValue, kind) {
584
839
  const elem = buildV5Decoder(typeValue.value, typeCtx);
585
840
  if (kind === "Set") {
586
841
  return (reader, ctx, n, order) => {
587
- const set = new Set();
842
+ const set = new SortedSet(undefined, cmp);
588
843
  for (let i = 0; i < n; i++) {
589
844
  const item = elem(reader, ctx);
590
845
  if (order) {
@@ -709,11 +964,16 @@ export class Beast2Pages {
709
964
  /** Whether segments are independently decodable. */
710
965
  selfContained;
711
966
  source;
967
+ /** The manifest behind this reader, when the segments are separate blobs
968
+ * rather than runs of one. */
969
+ manifestSource = null;
712
970
  /** Wire offset of the terminator frame — where the last segment's frame ends. */
713
971
  segmentsEnd;
714
972
  indexData;
715
973
  kind;
716
974
  typeValue;
975
+ /** The stream's source map. For a manifest it starts empty and is replaced
976
+ * by the first segment's own header map, which every segment shares. */
717
977
  sourceMap;
718
978
  decodeSegment;
719
979
  platform;
@@ -722,6 +982,8 @@ export class Beast2Pages {
722
982
  /** First key/element of each segment, in segment order (Set/Dict only). */
723
983
  fences = null;
724
984
  fenceDec = null;
985
+ /** The decoder of a manifest's stored fence bytes, built on first use. */
986
+ manifestFenceDec = null;
725
987
  /** Decoded segments kept hot for the element and keyed read paths, keyed
726
988
  * by segment index in LRU order (mirrors east-c's `B2V5_PAGES_LRU`).
727
989
  * Only {@link element} and {@link get} route through it — the public
@@ -735,7 +997,25 @@ export class Beast2Pages {
735
997
  let kind;
736
998
  let sourceMap;
737
999
  let index;
738
- if (!isBeast2SyncRangeReader(source)) {
1000
+ if (isBeast2ManifestSource(source)) {
1001
+ // A manifest names its segments; there is no one blob to take geometry
1002
+ // from. Counts and fences come from the entries, and a segment is its
1003
+ // own blob — so no offset in this index is ever read, and the header
1004
+ // (with it the source map) is parsed from the first segment opened.
1005
+ kind = checkSegmented(typeValue);
1006
+ sourceMap = new SourceMap();
1007
+ const entries = source.manifest.entries;
1008
+ index = {
1009
+ selfContained: true,
1010
+ offsets: entries.map(() => 0),
1011
+ counts: entries.map((e) => Number(e.count)),
1012
+ totalCount: entries.reduce((sum, e) => sum + Number(e.count), 0),
1013
+ };
1014
+ this.manifestSource = source;
1015
+ this.segmentsEnd = 0;
1016
+ this.source = bytesReader(new Uint8Array(0));
1017
+ }
1018
+ else if (!isBeast2SyncRangeReader(source)) {
739
1019
  ({ kind, sourceMap } = openSegmented(source, typeValue));
740
1020
  const whole = readIndex(source);
741
1021
  if (!whole) {
@@ -780,12 +1060,31 @@ export class Beast2Pages {
780
1060
  /** Reads segment `i`'s frame — exactly its wire bytes, from its index
781
1061
  * offset to the next segment's (or the terminator) — and opens its
782
1062
  * logical chunk. The only place segment bytes are fetched, so a ranged
783
- * source touches one frame per decode. */
1063
+ * source touches one frame per decode, and a manifest source touches one
1064
+ * segment blob. */
784
1065
  frameReader(i) {
1066
+ if (this.manifestSource !== null)
1067
+ return this.manifestFrameReader(i);
785
1068
  const start = this.indexData.offsets[i];
786
1069
  const end = i + 1 < this.indexData.offsets.length ? this.indexData.offsets[i + 1] : this.segmentsEnd;
787
1070
  return new FrameReader(readExact(this.source, start, end - start), 0).next();
788
1071
  }
1072
+ /** Segment `i`'s frame out of its own standalone blob: the blob's geometry
1073
+ * names exactly one frame, and its header carries the source map every
1074
+ * segment of this collection shares. */
1075
+ manifestFrameReader(i) {
1076
+ const blob = this.manifestSource.segment(i);
1077
+ const reader = isBeast2SyncRangeReader(blob) ? blob : bytesReader(blob);
1078
+ const extents = readBeast2ExtentsSync(reader);
1079
+ if (extents.offsets.length !== 1) {
1080
+ throw new Error(`beast2 v5: manifest entry ${i} holds ${extents.offsets.length} segments, not one`);
1081
+ }
1082
+ if (this.sourceMap.size <= 1n) {
1083
+ this.sourceMap = openSegmented(extents.head, this.typeValue).sourceMap;
1084
+ }
1085
+ const start = extents.offsets[0];
1086
+ return new FrameReader(readExact(reader, start, extents.segmentsEnd - start), 0).next();
1087
+ }
789
1088
  /**
790
1089
  * Decodes one segment by index.
791
1090
  *
@@ -863,6 +1162,15 @@ export class Beast2Pages {
863
1162
  * final attempt reads the frame whole, so corruption is still reported
864
1163
  * with the frame's own error. */
865
1164
  firstKey(i) {
1165
+ // A manifest stores every fence already, so a bisect over one reads no
1166
+ // segment bytes at all — the whole point of carrying them.
1167
+ if (this.manifestSource !== null) {
1168
+ if (!this.manifestFenceDec) {
1169
+ const keyType = this.kind === "Dict" ? this.typeValue.value.key : this.typeValue.value;
1170
+ this.manifestFenceDec = decodeBeast2FenceFor(keyType, this.platform);
1171
+ }
1172
+ return this.manifestFenceDec(this.manifestSource.manifest.entries[i].fence);
1173
+ }
866
1174
  if (!this.fenceDec) {
867
1175
  const keyType = this.kind === "Dict" ? this.typeValue.value.key : this.typeValue.value;
868
1176
  this.fenceDec = buildV5Decoder(keyType);
@@ -983,7 +1291,7 @@ export class Beast2Pages {
983
1291
  * Rows address stream order — for Array roots the element order, for
984
1292
  * Set/Dict roots the canonical East (key) order, since segments are
985
1293
  * disjoint ascending ranges. Returns a collection value of the root kind
986
- * holding the window (an array, `Set`, or `Map` in that order).
1294
+ * holding the window (an array, `SortedSet`, or `SortedMap` in that order).
987
1295
  *
988
1296
  * Clamps like `Array.prototype.slice`: a window past the end returns the
989
1297
  * available tail (or an empty collection), never throws for being short.
@@ -999,7 +1307,8 @@ export class Beast2Pages {
999
1307
  if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 0) {
1000
1308
  throw new Error(`beast2 v5: slice(${offset}, ${limit}) — offset and limit must be non-negative integers`);
1001
1309
  }
1002
- const empty = () => this.kind === "Array" ? [] : this.kind === "Set" ? new Set() : new Map();
1310
+ const empty = () => this.kind === "Array" ? []
1311
+ : this.kind === "Set" ? new SortedSet(undefined, this.orderCmp) : new SortedMap(undefined, this.orderCmp);
1003
1312
  if (limit === 0 || offset >= this.elementCount)
1004
1313
  return empty();
1005
1314
  if (this.kind === "Array") {
@@ -1021,7 +1330,7 @@ export class Beast2Pages {
1021
1330
  // against the fence of the first untouched segment.
1022
1331
  const fences = this.verifyFences();
1023
1332
  const isSet = this.kind === "Set";
1024
- const out = (isSet ? new Set() : new Map());
1333
+ const out = empty();
1025
1334
  let taken = 0;
1026
1335
  let { seg, base } = this.rowSegment(offset);
1027
1336
  const order = { prev: undefined, has: false };
@@ -1092,6 +1401,28 @@ export class Beast2Pages {
1092
1401
  }
1093
1402
  return lo;
1094
1403
  }
1404
+ /**
1405
+ * Decodes one segment of a Set or Dict for a walk in canonical order
1406
+ * (Set/Dict roots only), as east-c's paged loops read one: the fences are
1407
+ * probed and verified to ascend strictly first, once per reader, and the
1408
+ * segment's last key is checked to fall below the next segment's fence. A
1409
+ * walk from segment 0 over a blob that violates the canonical-order contract
1410
+ * therefore fails at the first segment it reads: before any element when the
1411
+ * fences are out of order, and before the elements of the first segment
1412
+ * that overlaps the next.
1413
+ *
1414
+ * @param i - zero-based segment index
1415
+ * @returns the segment's decoded collection
1416
+ * @throws {Error} When the root is an Array, `i` is out of range, the blob
1417
+ * is not self-contained, or the blob violates the canonical-order
1418
+ * contract.
1419
+ */
1420
+ segmentDisjoint(i) {
1421
+ if (this.kind === "Array") {
1422
+ throw new Error(`beast2 v5: segmentDisjoint() addresses Set and Dict roots; this blob holds Array — use segment()`);
1423
+ }
1424
+ return this.decodeDisjoint(i, { prev: undefined, has: false }, this.verifyFences());
1425
+ }
1095
1426
  /**
1096
1427
  * Looks up one Set element or Dict value by key (Set/Dict roots only):
1097
1428
  * binary-searches the verified segment fences for the only segment whose
@@ -1133,9 +1464,10 @@ export class Beast2Pages {
1133
1464
  * Builds a curried pages opener: `open(source)` parses the header, footer and
1134
1465
  * index once and returns a {@link Beast2Pages} for random access.
1135
1466
  *
1136
- * `source` is the whole blob, or a {@link Beast2SyncRangeReader} over it —
1137
- * then only the tail, the head and the segments actually read are ever
1138
- * fetched.
1467
+ * `source` is the whole blob, a {@link Beast2SyncRangeReader} over it — then
1468
+ * only the tail, the head and the segments actually read are ever fetched —
1469
+ * or a {@link Beast2ManifestSource}, whose segments are separate blobs and
1470
+ * whose fences are already decoded, so a keyed read touches exactly one.
1139
1471
  *
1140
1472
  * @param type - the collection type (Array/Set/Dict)
1141
1473
  * @param options - decode options (platform functions for decoded functions)