@elaraai/east 1.0.45 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/containers/sortedmap.d.ts +22 -0
- package/dist/src/containers/sortedmap.d.ts.map +1 -1
- package/dist/src/containers/sortedmap.js +31 -2
- package/dist/src/containers/sortedmap.js.map +1 -1
- package/dist/src/containers/sortedset.d.ts +22 -0
- package/dist/src/containers/sortedset.d.ts.map +1 -1
- package/dist/src/containers/sortedset.js +31 -2
- package/dist/src/containers/sortedset.js.map +1 -1
- package/dist/src/expr/blob.d.ts +6 -1
- package/dist/src/expr/blob.d.ts.map +1 -1
- package/dist/src/expr/blob.js +6 -1
- package/dist/src/expr/blob.js.map +1 -1
- package/dist/src/expr/libs/blob.d.ts +10 -1
- package/dist/src/expr/libs/blob.d.ts.map +1 -1
- package/dist/src/expr/libs/blob.js +10 -1
- package/dist/src/expr/libs/blob.js.map +1 -1
- package/dist/src/fuzz.d.ts.map +1 -1
- package/dist/src/fuzz.js +11 -2
- package/dist/src/fuzz.js.map +1 -1
- package/dist/src/serialization/beast2/index.d.ts +76 -22
- package/dist/src/serialization/beast2/index.d.ts.map +1 -1
- package/dist/src/serialization/beast2/index.js +87 -720
- package/dist/src/serialization/beast2/index.js.map +1 -1
- package/dist/src/serialization/beast2/index.spec.js +147 -4
- package/dist/src/serialization/beast2/index.spec.js.map +1 -1
- package/dist/src/serialization/beast2/shared.d.ts +79 -0
- package/dist/src/serialization/beast2/shared.d.ts.map +1 -0
- package/dist/src/serialization/beast2/shared.js +143 -0
- package/dist/src/serialization/beast2/shared.js.map +1 -0
- package/dist/src/serialization/beast2/v4/container.d.ts +67 -0
- package/dist/src/serialization/beast2/v4/container.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v4/container.js +727 -0
- package/dist/src/serialization/beast2/v4/container.js.map +1 -0
- package/dist/src/serialization/beast2/{sourcemap-table.d.ts → v4/sourcemap-table.d.ts} +2 -2
- package/dist/src/serialization/beast2/v4/sourcemap-table.d.ts.map +1 -0
- package/dist/src/serialization/beast2/{sourcemap-table.js → v4/sourcemap-table.js} +2 -2
- package/dist/src/serialization/beast2/v4/sourcemap-table.js.map +1 -0
- package/dist/src/serialization/beast2/{string-table.d.ts → v4/string-table.d.ts} +1 -1
- package/dist/src/serialization/beast2/v4/string-table.d.ts.map +1 -0
- package/dist/src/serialization/beast2/{string-table.js → v4/string-table.js} +1 -1
- package/dist/src/serialization/beast2/v4/string-table.js.map +1 -0
- package/dist/src/serialization/beast2/{type-table.d.ts → v4/type-table.d.ts} +6 -4
- package/dist/src/serialization/beast2/v4/type-table.d.ts.map +1 -0
- package/dist/src/serialization/beast2/{type-table.js → v4/type-table.js} +103 -10
- package/dist/src/serialization/beast2/v4/type-table.js.map +1 -0
- package/dist/src/serialization/beast2/{value-table.d.ts → v4/value-table.d.ts} +2 -2
- package/dist/src/serialization/beast2/v4/value-table.d.ts.map +1 -0
- package/dist/src/serialization/beast2/{value-table.js → v4/value-table.js} +3 -3
- package/dist/src/serialization/beast2/v4/value-table.js.map +1 -0
- package/dist/src/serialization/beast2/v5/codec.d.ts +213 -0
- package/dist/src/serialization/beast2/v5/codec.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/codec.js +954 -0
- package/dist/src/serialization/beast2/v5/codec.js.map +1 -0
- package/dist/src/serialization/beast2/v5/deflate.d.ts +16 -0
- package/dist/src/serialization/beast2/v5/deflate.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/deflate.js +172 -0
- package/dist/src/serialization/beast2/v5/deflate.js.map +1 -0
- package/dist/src/serialization/beast2/v5/frames.d.ts +150 -0
- package/dist/src/serialization/beast2/v5/frames.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/frames.js +284 -0
- package/dist/src/serialization/beast2/v5/frames.js.map +1 -0
- package/dist/src/serialization/beast2/v5/index.spec.d.ts +6 -0
- package/dist/src/serialization/beast2/v5/index.spec.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/index.spec.js +484 -0
- package/dist/src/serialization/beast2/v5/index.spec.js.map +1 -0
- package/dist/src/serialization/beast2/v5/stream.d.ts +177 -0
- package/dist/src/serialization/beast2/v5/stream.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/stream.js +447 -0
- package/dist/src/serialization/beast2/v5/stream.js.map +1 -0
- package/dist/src/serialization/beast2/v5/type-section.d.ts +81 -0
- package/dist/src/serialization/beast2/v5/type-section.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/type-section.js +211 -0
- package/dist/src/serialization/beast2/v5/type-section.js.map +1 -0
- package/dist/src/serialization/beast2/version.d.ts +40 -0
- package/dist/src/serialization/beast2/version.d.ts.map +1 -0
- package/dist/src/serialization/beast2/version.js +23 -0
- package/dist/src/serialization/beast2/version.js.map +1 -0
- package/dist/src/serialization/index.d.ts +1 -1
- package/dist/src/serialization/index.d.ts.map +1 -1
- package/dist/src/serialization/index.js +1 -1
- package/dist/src/serialization/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/serialization/beast2/sourcemap-table.d.ts.map +0 -1
- package/dist/src/serialization/beast2/sourcemap-table.js.map +0 -1
- package/dist/src/serialization/beast2/string-table.d.ts.map +0 -1
- package/dist/src/serialization/beast2/string-table.js.map +0 -1
- package/dist/src/serialization/beast2/type-table.d.ts.map +0 -1
- package/dist/src/serialization/beast2/type-table.js.map +0 -1
- package/dist/src/serialization/beast2/value-table.d.ts.map +0 -1
- package/dist/src/serialization/beast2/value-table.js.map +0 -1
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Beast2 v5 codec — the single-pass, segment-terminated record stream.
|
|
7
|
+
*
|
|
8
|
+
* Blob layout:
|
|
9
|
+
*
|
|
10
|
+
* magic[8] 0x89 "East" 0x0D 0x0A 0x05
|
|
11
|
+
* type_section well-known id + hash, or structural (v4 table)
|
|
12
|
+
* source_map_section varint(len) + stacks with inline filenames
|
|
13
|
+
* value stream frames carrying the logical value encoding
|
|
14
|
+
* [index_section] optional — segment offsets/counts (see stream.ts)
|
|
15
|
+
* [footer] u64-LE index offset + footer magic
|
|
16
|
+
*
|
|
17
|
+
* Logical value encoding (type-directed, positional):
|
|
18
|
+
* - immutable scalars encode as in v4, except strings are inline
|
|
19
|
+
* (`varint(len) + utf8`) — there is no string table.
|
|
20
|
+
* - mutable containers (Array/Set/Dict/Ref) carry a tag byte: `0x00 NEW`
|
|
21
|
+
* defines the container (registered in definition-start preorder) and is
|
|
22
|
+
* followed by its content; `0x01 REF` + `varint(delta)` aliases the
|
|
23
|
+
* container defined `delta` definitions ago. Relative deltas make decode
|
|
24
|
+
* independent of segment scoping — a self-contained segment resolves the
|
|
25
|
+
* same deltas with a fresh table that a sequential reader resolves with a
|
|
26
|
+
* global one.
|
|
27
|
+
* - Array/Set/Dict content is segment-terminated:
|
|
28
|
+
* `repeat[varint(n>0) + n elements] varint(0)` — no up-front totals.
|
|
29
|
+
* - Function values emit a source-map delta (`varint(n_new)` + stacks not yet
|
|
30
|
+
* written to this stream), then their IR, then `varint(capture_count)` +
|
|
31
|
+
* captures. Location ids inside IR stay plain integer data, so round-trips
|
|
32
|
+
* preserve them exactly.
|
|
33
|
+
*
|
|
34
|
+
* See v5/SPEC.md for the full wire specification.
|
|
35
|
+
*/
|
|
36
|
+
import {} from "../../../type_of_type.js";
|
|
37
|
+
import { variant } from "../../../containers/variant.js";
|
|
38
|
+
import { ref } from "../../../containers/ref.js";
|
|
39
|
+
import { SortedSet } from "../../../containers/sortedset.js";
|
|
40
|
+
import { SortedMap } from "../../../containers/sortedmap.js";
|
|
41
|
+
import { matrix } from "../../../containers/matrix.js";
|
|
42
|
+
import { BufferWriter, BufferReader } from "../../binary-utils.js";
|
|
43
|
+
import { compareFor } from "../../../comparison.js";
|
|
44
|
+
import { EAST_IR_SYMBOL, EAST_CAPTURES_SYMBOL, EAST_SOURCE_MAP_SYMBOL } from "../../../compile.js";
|
|
45
|
+
import { InternalError } from "../../../error.js";
|
|
46
|
+
import { SourceMap } from "../../../location.js";
|
|
47
|
+
import { buildPlatformContext, describeNoIrValue, finishDecodedFunction, irTypeValue } from "../shared.js";
|
|
48
|
+
import { writeTypeSection, readTypeSection, asTypeValue } from "./type-section.js";
|
|
49
|
+
import { FrameReader, writeFrame, preInflateFrames } from "./frames.js";
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Wire constants
|
|
52
|
+
// =============================================================================
|
|
53
|
+
/** The v5 container magic: the beast2 magic with version byte 0x05. */
|
|
54
|
+
export const MAGIC_BYTES_V5 = new Uint8Array([0x89, 0x45, 0x61, 0x73, 0x74, 0x0D, 0x0A, 0x05]);
|
|
55
|
+
/** Container tag: defines a new mutable container at this position. */
|
|
56
|
+
export const TAG_NEW = 0x00;
|
|
57
|
+
/** Container tag: aliases a previously defined container by relative delta. */
|
|
58
|
+
export const TAG_REF = 0x01;
|
|
59
|
+
/** Creates a fresh v5 encode context.
|
|
60
|
+
*
|
|
61
|
+
* @param sourceMap - the pre-resolved header source map, if any
|
|
62
|
+
* @param selfContained - whether the writer scopes aliasing per segment
|
|
63
|
+
* @returns the initialized context
|
|
64
|
+
*/
|
|
65
|
+
export function createV5EncodeContext(sourceMap, selfContained) {
|
|
66
|
+
return {
|
|
67
|
+
containerIndex: new Map(),
|
|
68
|
+
containerCount: 0,
|
|
69
|
+
segmentBaseDef: 0,
|
|
70
|
+
crossSegmentRef: false,
|
|
71
|
+
sourceMap,
|
|
72
|
+
sourceMapEmitted: sourceMap ? Number(sourceMap.size) : 1,
|
|
73
|
+
selfContained,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Source map section (v5: inline filenames)
|
|
78
|
+
// =============================================================================
|
|
79
|
+
function writeStack(stack, writer) {
|
|
80
|
+
writer.writeVarint(stack.length);
|
|
81
|
+
for (const frame of stack) {
|
|
82
|
+
writer.writeStringUtf8Varint(frame.filename);
|
|
83
|
+
writer.writeVarint(Number(frame.line));
|
|
84
|
+
writer.writeVarint(Number(frame.column));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function readStack(reader) {
|
|
88
|
+
const frameCount = reader.readVarint();
|
|
89
|
+
const stack = new Array(frameCount);
|
|
90
|
+
for (let i = 0; i < frameCount; i++) {
|
|
91
|
+
const filename = reader.readStringUtf8Varint();
|
|
92
|
+
const line = reader.readVarint();
|
|
93
|
+
const column = reader.readVarint();
|
|
94
|
+
stack[i] = { filename, line: BigInt(line), column: BigInt(column) };
|
|
95
|
+
}
|
|
96
|
+
return stack;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Writes the v5 source_map_section: `varint(payload_len)` + `varint(count)` +
|
|
100
|
+
* stacks with inline filenames. Entry 0 (the empty sentinel) is implicit.
|
|
101
|
+
*
|
|
102
|
+
* @param sourceMap - the header source map, or `null` for an empty section
|
|
103
|
+
* @param writer - the wire-level writer
|
|
104
|
+
*/
|
|
105
|
+
export function writeSourceMapSectionV5(sourceMap, writer) {
|
|
106
|
+
const payload = new BufferWriter();
|
|
107
|
+
if (!sourceMap || sourceMap.size <= 1n) {
|
|
108
|
+
payload.writeVarint(0);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const entries = sourceMap.entries();
|
|
112
|
+
payload.writeVarint(entries.length - 1);
|
|
113
|
+
for (let i = 1; i < entries.length; i++) {
|
|
114
|
+
writeStack(entries[i], payload);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const bytes = payload.toUint8Array();
|
|
118
|
+
writer.writeVarint(bytes.length);
|
|
119
|
+
writer.writeBytes(bytes);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reads the v5 source_map_section into a fresh {@link SourceMap}.
|
|
123
|
+
*
|
|
124
|
+
* @param reader - the wire-level reader positioned at the section start
|
|
125
|
+
* @returns the decoded source map (stack ids match encode-side ids)
|
|
126
|
+
*/
|
|
127
|
+
export function readSourceMapSectionV5(reader) {
|
|
128
|
+
const payloadLen = reader.readVarint();
|
|
129
|
+
const end = reader.offset + payloadLen;
|
|
130
|
+
const map = new SourceMap();
|
|
131
|
+
const stackCount = reader.readVarint();
|
|
132
|
+
for (let i = 0; i < stackCount; i++) {
|
|
133
|
+
map.intern_stack(readStack(reader));
|
|
134
|
+
}
|
|
135
|
+
if (reader.offset !== end) {
|
|
136
|
+
throw new Error(`beast2 v5: source map section size mismatch: expected offset ${end}, got ${reader.offset}`);
|
|
137
|
+
}
|
|
138
|
+
return map;
|
|
139
|
+
}
|
|
140
|
+
/** Emits the NEW tag and registers a container definition; returns false when
|
|
141
|
+
* the container was already defined and a REF was written instead. */
|
|
142
|
+
function beginContainer(value, writer, ctx) {
|
|
143
|
+
const idx = ctx.containerIndex.get(value);
|
|
144
|
+
if (idx !== undefined) {
|
|
145
|
+
writer.writeUint8(TAG_REF);
|
|
146
|
+
writer.writeVarint(ctx.containerCount - idx);
|
|
147
|
+
if (idx < ctx.segmentBaseDef)
|
|
148
|
+
ctx.crossSegmentRef = true;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
writer.writeUint8(TAG_NEW);
|
|
152
|
+
ctx.containerIndex.set(value, ctx.containerCount++);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Builds a v5 value encoder closure tree for the given type.
|
|
157
|
+
*
|
|
158
|
+
* @param type - the type to encode
|
|
159
|
+
* @param typeCtx - recursive-type resolution context shared across the tree
|
|
160
|
+
* @returns the encoder closure
|
|
161
|
+
*/
|
|
162
|
+
export function buildV5Encoder(type, typeCtx = new Map()) {
|
|
163
|
+
switch (type.type) {
|
|
164
|
+
case "Never":
|
|
165
|
+
return () => { throw new Error("Cannot encode value of type Never"); };
|
|
166
|
+
case "Null":
|
|
167
|
+
return () => { };
|
|
168
|
+
case "Boolean":
|
|
169
|
+
return (value, writer) => writer.writeUint8(value ? 1 : 0);
|
|
170
|
+
case "Integer":
|
|
171
|
+
return (value, writer) => writer.writeZigzag(value);
|
|
172
|
+
case "Float":
|
|
173
|
+
return (value, writer) => writer.writeFloat64LE(value);
|
|
174
|
+
case "String":
|
|
175
|
+
return (value, writer) => writer.writeStringUtf8Varint(value);
|
|
176
|
+
case "DateTime":
|
|
177
|
+
return (value, writer) => writer.writeZigzag(BigInt(value.valueOf()));
|
|
178
|
+
case "Blob":
|
|
179
|
+
return (value, writer) => {
|
|
180
|
+
writer.writeVarint(value.length);
|
|
181
|
+
writer.writeBytes(value);
|
|
182
|
+
};
|
|
183
|
+
case "Array": {
|
|
184
|
+
let elem;
|
|
185
|
+
const ret = (value, writer, ctx) => {
|
|
186
|
+
if (!beginContainer(value, writer, ctx))
|
|
187
|
+
return;
|
|
188
|
+
const n = value.length;
|
|
189
|
+
if (n > 0) {
|
|
190
|
+
writer.writeVarint(n);
|
|
191
|
+
for (const item of value)
|
|
192
|
+
elem(item, writer, ctx);
|
|
193
|
+
}
|
|
194
|
+
writer.writeVarint(0);
|
|
195
|
+
};
|
|
196
|
+
elem = buildV5Encoder(type.value, typeCtx);
|
|
197
|
+
return ret;
|
|
198
|
+
}
|
|
199
|
+
case "Set": {
|
|
200
|
+
let elem;
|
|
201
|
+
const ret = (value, writer, ctx) => {
|
|
202
|
+
if (!beginContainer(value, writer, ctx))
|
|
203
|
+
return;
|
|
204
|
+
const n = value.size;
|
|
205
|
+
if (n > 0) {
|
|
206
|
+
writer.writeVarint(n);
|
|
207
|
+
for (const item of value)
|
|
208
|
+
elem(item, writer, ctx);
|
|
209
|
+
}
|
|
210
|
+
writer.writeVarint(0);
|
|
211
|
+
};
|
|
212
|
+
elem = buildV5Encoder(type.value, typeCtx);
|
|
213
|
+
return ret;
|
|
214
|
+
}
|
|
215
|
+
case "Dict": {
|
|
216
|
+
let key;
|
|
217
|
+
let val;
|
|
218
|
+
const ret = (value, writer, ctx) => {
|
|
219
|
+
if (!beginContainer(value, writer, ctx))
|
|
220
|
+
return;
|
|
221
|
+
const n = value.size;
|
|
222
|
+
if (n > 0) {
|
|
223
|
+
writer.writeVarint(n);
|
|
224
|
+
for (const [k, v] of value) {
|
|
225
|
+
key(k, writer, ctx);
|
|
226
|
+
val(v, writer, ctx);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
writer.writeVarint(0);
|
|
230
|
+
};
|
|
231
|
+
key = buildV5Encoder(type.value.key, typeCtx);
|
|
232
|
+
val = buildV5Encoder(type.value.value, typeCtx);
|
|
233
|
+
return ret;
|
|
234
|
+
}
|
|
235
|
+
case "Ref": {
|
|
236
|
+
let inner;
|
|
237
|
+
const ret = (value, writer, ctx) => {
|
|
238
|
+
if (!beginContainer(value, writer, ctx))
|
|
239
|
+
return;
|
|
240
|
+
inner(value.value, writer, ctx);
|
|
241
|
+
};
|
|
242
|
+
inner = buildV5Encoder(type.value, typeCtx);
|
|
243
|
+
return ret;
|
|
244
|
+
}
|
|
245
|
+
case "Struct": {
|
|
246
|
+
const fieldEncoders = [];
|
|
247
|
+
const ret = (value, writer, ctx) => {
|
|
248
|
+
for (const [name, enc] of fieldEncoders)
|
|
249
|
+
enc(value[name], writer, ctx);
|
|
250
|
+
};
|
|
251
|
+
for (const { name, type: fieldType } of type.value) {
|
|
252
|
+
fieldEncoders.push([name, buildV5Encoder(fieldType, typeCtx)]);
|
|
253
|
+
}
|
|
254
|
+
return ret;
|
|
255
|
+
}
|
|
256
|
+
case "Variant": {
|
|
257
|
+
const caseEncoders = {};
|
|
258
|
+
const caseTags = {};
|
|
259
|
+
const ret = (value, writer, ctx) => {
|
|
260
|
+
writer.writeVarint(caseTags[value.type]);
|
|
261
|
+
caseEncoders[value.type](value.value, writer, ctx);
|
|
262
|
+
};
|
|
263
|
+
for (let i = 0; i < type.value.length; i++) {
|
|
264
|
+
const { name, type: caseType } = type.value[i];
|
|
265
|
+
caseTags[name] = i;
|
|
266
|
+
caseEncoders[name] = buildV5Encoder(caseType, typeCtx);
|
|
267
|
+
}
|
|
268
|
+
return ret;
|
|
269
|
+
}
|
|
270
|
+
case "Recursive": {
|
|
271
|
+
if (type.value.type === "wrapper") {
|
|
272
|
+
let inner;
|
|
273
|
+
const ret = (value, writer, ctx) => inner(value, writer, ctx);
|
|
274
|
+
typeCtx.set(type.value.value.id, ret);
|
|
275
|
+
inner = buildV5Encoder(type.value.value.inner, typeCtx);
|
|
276
|
+
return ret;
|
|
277
|
+
}
|
|
278
|
+
const target = typeCtx.get(type.value.value);
|
|
279
|
+
if (!target)
|
|
280
|
+
throw new InternalError("Recursive type context not found during encoder build");
|
|
281
|
+
return target;
|
|
282
|
+
}
|
|
283
|
+
case "Function":
|
|
284
|
+
case "AsyncFunction": {
|
|
285
|
+
const fnIrEncoder = buildV5Encoder(irTypeValue, typeCtx);
|
|
286
|
+
const captureEncoderCache = new Map();
|
|
287
|
+
return (value, writer, ctx) => {
|
|
288
|
+
const ir = value[EAST_IR_SYMBOL];
|
|
289
|
+
if (!ir)
|
|
290
|
+
throw new Error(`Cannot serialize function: no IR attached (${describeNoIrValue(value)})`);
|
|
291
|
+
// Source-map delta: stacks of the stream's map not yet on the wire.
|
|
292
|
+
const sm = value[EAST_SOURCE_MAP_SYMBOL] ?? null;
|
|
293
|
+
if (ctx.sourceMap === null && sm)
|
|
294
|
+
ctx.sourceMap = sm;
|
|
295
|
+
if (ctx.sourceMap !== null && sm === ctx.sourceMap && Number(ctx.sourceMap.size) > ctx.sourceMapEmitted) {
|
|
296
|
+
if (ctx.selfContained) {
|
|
297
|
+
throw new Error(`beast2 v5: self-contained streams cannot add function source maps mid-stream; ` +
|
|
298
|
+
`pass the source map to the writer up front or disable selfContained`);
|
|
299
|
+
}
|
|
300
|
+
const entries = ctx.sourceMap.entries();
|
|
301
|
+
writer.writeVarint(entries.length - ctx.sourceMapEmitted);
|
|
302
|
+
for (let i = ctx.sourceMapEmitted; i < entries.length; i++) {
|
|
303
|
+
writeStack(entries[i], writer);
|
|
304
|
+
}
|
|
305
|
+
ctx.sourceMapEmitted = entries.length;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
writer.writeVarint(0);
|
|
309
|
+
}
|
|
310
|
+
fnIrEncoder(ir, writer, ctx);
|
|
311
|
+
const captures = value[EAST_CAPTURES_SYMBOL];
|
|
312
|
+
const captureList = ir.value.captures;
|
|
313
|
+
writer.writeVarint(captureList.length);
|
|
314
|
+
for (const captureVar of captureList) {
|
|
315
|
+
const name = captureVar.value.name;
|
|
316
|
+
const captureType = captureVar.value.type;
|
|
317
|
+
if (!captures)
|
|
318
|
+
throw new InternalError("Function has captures but no EAST_CAPTURES_SYMBOL");
|
|
319
|
+
const entry = captures[name];
|
|
320
|
+
if (!entry)
|
|
321
|
+
throw new InternalError(`Capture '${name}' not found`);
|
|
322
|
+
let enc = captureEncoderCache.get(captureType);
|
|
323
|
+
if (!enc) {
|
|
324
|
+
enc = buildV5Encoder(captureType, typeCtx);
|
|
325
|
+
captureEncoderCache.set(captureType, enc);
|
|
326
|
+
}
|
|
327
|
+
enc(entry.value, writer, ctx);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
case "Vector":
|
|
332
|
+
return (value, writer) => {
|
|
333
|
+
writer.writeVarint(value.length);
|
|
334
|
+
writer.writeBytes(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
335
|
+
};
|
|
336
|
+
case "Matrix":
|
|
337
|
+
return (value, writer) => {
|
|
338
|
+
writer.writeVarint(value.rows);
|
|
339
|
+
writer.writeVarint(value.cols);
|
|
340
|
+
writer.writeBytes(new Uint8Array(value.data.buffer, value.data.byteOffset, value.data.byteLength));
|
|
341
|
+
};
|
|
342
|
+
default:
|
|
343
|
+
throw new Error(`Unknown type: ${type.type}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// =============================================================================
|
|
347
|
+
// Value decoder factory
|
|
348
|
+
// =============================================================================
|
|
349
|
+
/** Reads a container tag; resolves and returns the aliased container for REF,
|
|
350
|
+
* or `undefined` for NEW (the caller then defines the container). */
|
|
351
|
+
function readContainerTag(reader, ctx) {
|
|
352
|
+
const tag = reader.readUint8();
|
|
353
|
+
if (tag === TAG_NEW)
|
|
354
|
+
return undefined;
|
|
355
|
+
if (tag !== TAG_REF) {
|
|
356
|
+
throw new Error(`beast2 v5: invalid container tag 0x${tag.toString(16)}`);
|
|
357
|
+
}
|
|
358
|
+
const delta = reader.readVarint();
|
|
359
|
+
if (delta < 1 || delta > ctx.containers.length) {
|
|
360
|
+
throw new Error(`beast2 v5: container backref delta ${delta} out of range (${ctx.containers.length} definitions visible)`);
|
|
361
|
+
}
|
|
362
|
+
return ctx.containers[ctx.containers.length - delta];
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Builds a v5 value decoder closure tree for the given type.
|
|
366
|
+
*
|
|
367
|
+
* @param type - the type to decode
|
|
368
|
+
* @param typeCtx - recursive-type resolution context shared across the tree
|
|
369
|
+
* @returns the decoder closure
|
|
370
|
+
*/
|
|
371
|
+
export function buildV5Decoder(type, typeCtx = new Map()) {
|
|
372
|
+
switch (type.type) {
|
|
373
|
+
case "Never":
|
|
374
|
+
return () => { throw new Error("Cannot decode value of type Never"); };
|
|
375
|
+
case "Null":
|
|
376
|
+
return () => null;
|
|
377
|
+
case "Boolean":
|
|
378
|
+
return (reader) => reader.readBoolean();
|
|
379
|
+
case "Integer":
|
|
380
|
+
return (reader) => reader.readZigzag();
|
|
381
|
+
case "Float":
|
|
382
|
+
return (reader) => reader.readFloat64LE();
|
|
383
|
+
case "String":
|
|
384
|
+
return (reader) => reader.readStringUtf8Varint();
|
|
385
|
+
case "DateTime":
|
|
386
|
+
return (reader) => new Date(Number(reader.readZigzag()));
|
|
387
|
+
case "Blob":
|
|
388
|
+
return (reader) => reader.readBytes(reader.readVarint());
|
|
389
|
+
case "Array": {
|
|
390
|
+
let elem;
|
|
391
|
+
const ret = (reader, ctx) => {
|
|
392
|
+
const aliased = readContainerTag(reader, ctx);
|
|
393
|
+
if (aliased !== undefined)
|
|
394
|
+
return aliased;
|
|
395
|
+
const arr = [];
|
|
396
|
+
ctx.containers.push(arr);
|
|
397
|
+
for (;;) {
|
|
398
|
+
const n = reader.readVarint();
|
|
399
|
+
if (n === 0)
|
|
400
|
+
break;
|
|
401
|
+
for (let i = 0; i < n; i++)
|
|
402
|
+
arr.push(elem(reader, ctx));
|
|
403
|
+
}
|
|
404
|
+
return arr;
|
|
405
|
+
};
|
|
406
|
+
elem = buildV5Decoder(type.value, typeCtx);
|
|
407
|
+
return ret;
|
|
408
|
+
}
|
|
409
|
+
case "Set": {
|
|
410
|
+
let elem;
|
|
411
|
+
const cmp = compareFor(type.value);
|
|
412
|
+
const ret = (reader, ctx) => {
|
|
413
|
+
const aliased = readContainerTag(reader, ctx);
|
|
414
|
+
if (aliased !== undefined)
|
|
415
|
+
return aliased;
|
|
416
|
+
// A SortedSet keeps East's total order, which also gives the
|
|
417
|
+
// cross-segment merge semantics for free: inserts sort and dedup.
|
|
418
|
+
const set = new SortedSet(undefined, cmp);
|
|
419
|
+
ctx.containers.push(set);
|
|
420
|
+
for (;;) {
|
|
421
|
+
const n = reader.readVarint();
|
|
422
|
+
if (n === 0)
|
|
423
|
+
break;
|
|
424
|
+
for (let i = 0; i < n; i++)
|
|
425
|
+
set.add(elem(reader, ctx));
|
|
426
|
+
}
|
|
427
|
+
return set;
|
|
428
|
+
};
|
|
429
|
+
elem = buildV5Decoder(type.value, typeCtx);
|
|
430
|
+
return ret;
|
|
431
|
+
}
|
|
432
|
+
case "Dict": {
|
|
433
|
+
let key;
|
|
434
|
+
let val;
|
|
435
|
+
const cmpKey = compareFor(type.value.key);
|
|
436
|
+
const ret = (reader, ctx) => {
|
|
437
|
+
const aliased = readContainerTag(reader, ctx);
|
|
438
|
+
if (aliased !== undefined)
|
|
439
|
+
return aliased;
|
|
440
|
+
// A SortedMap keeps East's total order; a later set() on an existing
|
|
441
|
+
// key overwrites, which IS the cross-segment last-wins rule.
|
|
442
|
+
const map = new SortedMap(undefined, cmpKey);
|
|
443
|
+
ctx.containers.push(map);
|
|
444
|
+
for (;;) {
|
|
445
|
+
const n = reader.readVarint();
|
|
446
|
+
if (n === 0)
|
|
447
|
+
break;
|
|
448
|
+
for (let i = 0; i < n; i++) {
|
|
449
|
+
const k = key(reader, ctx);
|
|
450
|
+
const v = val(reader, ctx);
|
|
451
|
+
map.set(k, v);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return map;
|
|
455
|
+
};
|
|
456
|
+
key = buildV5Decoder(type.value.key, typeCtx);
|
|
457
|
+
val = buildV5Decoder(type.value.value, typeCtx);
|
|
458
|
+
return ret;
|
|
459
|
+
}
|
|
460
|
+
case "Ref": {
|
|
461
|
+
let inner;
|
|
462
|
+
const ret = (reader, ctx) => {
|
|
463
|
+
const aliased = readContainerTag(reader, ctx);
|
|
464
|
+
if (aliased !== undefined)
|
|
465
|
+
return aliased;
|
|
466
|
+
const cell = ref(undefined);
|
|
467
|
+
ctx.containers.push(cell);
|
|
468
|
+
cell.value = inner(reader, ctx);
|
|
469
|
+
return cell;
|
|
470
|
+
};
|
|
471
|
+
inner = buildV5Decoder(type.value, typeCtx);
|
|
472
|
+
return ret;
|
|
473
|
+
}
|
|
474
|
+
case "Struct": {
|
|
475
|
+
const fields = type.value;
|
|
476
|
+
const names = [];
|
|
477
|
+
const decoders = [];
|
|
478
|
+
const ret = (reader, ctx) => {
|
|
479
|
+
const result = {};
|
|
480
|
+
for (let i = 0; i < names.length; i++)
|
|
481
|
+
result[names[i]] = decoders[i](reader, ctx);
|
|
482
|
+
return result;
|
|
483
|
+
};
|
|
484
|
+
for (const { name, type: fieldType } of fields) {
|
|
485
|
+
names.push(name);
|
|
486
|
+
decoders.push(buildV5Decoder(fieldType, typeCtx));
|
|
487
|
+
}
|
|
488
|
+
return ret;
|
|
489
|
+
}
|
|
490
|
+
case "Variant": {
|
|
491
|
+
const caseDecoders = [];
|
|
492
|
+
const ret = (reader, ctx) => {
|
|
493
|
+
const tagIndex = reader.readVarint();
|
|
494
|
+
if (tagIndex >= caseDecoders.length)
|
|
495
|
+
throw new Error(`Invalid variant tag ${tagIndex}`);
|
|
496
|
+
const [caseName, caseDec] = caseDecoders[tagIndex];
|
|
497
|
+
return variant(caseName, caseDec(reader, ctx));
|
|
498
|
+
};
|
|
499
|
+
for (const { name, type: caseType } of type.value) {
|
|
500
|
+
caseDecoders.push([name, buildV5Decoder(caseType, typeCtx)]);
|
|
501
|
+
}
|
|
502
|
+
return ret;
|
|
503
|
+
}
|
|
504
|
+
case "Recursive": {
|
|
505
|
+
if (type.value.type === "wrapper") {
|
|
506
|
+
let inner;
|
|
507
|
+
const ret = (reader, ctx) => inner(reader, ctx);
|
|
508
|
+
typeCtx.set(type.value.value.id, ret);
|
|
509
|
+
inner = buildV5Decoder(type.value.value.inner, typeCtx);
|
|
510
|
+
return ret;
|
|
511
|
+
}
|
|
512
|
+
const target = typeCtx.get(type.value.value);
|
|
513
|
+
if (!target)
|
|
514
|
+
throw new InternalError("Recursive type context not found during decoder build");
|
|
515
|
+
return target;
|
|
516
|
+
}
|
|
517
|
+
case "Function":
|
|
518
|
+
case "AsyncFunction": {
|
|
519
|
+
const isAsync = type.type === "AsyncFunction";
|
|
520
|
+
const fnType = type;
|
|
521
|
+
const fnIrDecoder = buildV5Decoder(irTypeValue, typeCtx);
|
|
522
|
+
const captureDecoderCache = new Map();
|
|
523
|
+
return (reader, ctx) => {
|
|
524
|
+
// Inline source-map delta.
|
|
525
|
+
const newStacks = reader.readVarint();
|
|
526
|
+
for (let i = 0; i < newStacks; i++) {
|
|
527
|
+
ctx.sourceMap.intern_stack(readStack(reader));
|
|
528
|
+
}
|
|
529
|
+
const ir = fnIrDecoder(reader, ctx);
|
|
530
|
+
if (ir.type !== (isAsync ? "AsyncFunction" : "Function")) {
|
|
531
|
+
throw new Error(`Expected ${fnType.type} IR, got ${ir.type}`);
|
|
532
|
+
}
|
|
533
|
+
const captureCount = reader.readVarint();
|
|
534
|
+
if (captureCount !== ir.value.captures.length) {
|
|
535
|
+
throw new Error(`Capture count mismatch: IR has ${ir.value.captures.length}, data has ${captureCount}`);
|
|
536
|
+
}
|
|
537
|
+
const captureContext = {};
|
|
538
|
+
const typeContext = {};
|
|
539
|
+
for (const captureVar of ir.value.captures) {
|
|
540
|
+
const name = captureVar.value.name;
|
|
541
|
+
const captureType = captureVar.value.type;
|
|
542
|
+
let dec = captureDecoderCache.get(captureType);
|
|
543
|
+
if (!dec) {
|
|
544
|
+
dec = buildV5Decoder(captureType, typeCtx);
|
|
545
|
+
captureDecoderCache.set(captureType, dec);
|
|
546
|
+
}
|
|
547
|
+
const captureValue = dec(reader, ctx);
|
|
548
|
+
captureContext[name] = captureVar.value.mutable
|
|
549
|
+
? variant("boxed", captureValue)
|
|
550
|
+
: variant("value", captureValue);
|
|
551
|
+
typeContext[name] = captureType;
|
|
552
|
+
}
|
|
553
|
+
return finishDecodedFunction(ir, isAsync, captureContext, typeContext, ctx, ctx.sourceMap);
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
case "Vector": {
|
|
557
|
+
const elemType = type.value.type;
|
|
558
|
+
const bpe = elemType === "Float" ? 8 : elemType === "Integer" ? 8 : 1;
|
|
559
|
+
return (reader) => {
|
|
560
|
+
const len = reader.readVarint();
|
|
561
|
+
const raw = new Uint8Array(reader.readBytesView(len * bpe));
|
|
562
|
+
if (elemType === "Float")
|
|
563
|
+
return new Float64Array(raw.buffer, 0, len);
|
|
564
|
+
if (elemType === "Integer")
|
|
565
|
+
return new BigInt64Array(raw.buffer, 0, len);
|
|
566
|
+
return new Uint8ClampedArray(raw.buffer, 0, len);
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
case "Matrix": {
|
|
570
|
+
const elemType = type.value.type;
|
|
571
|
+
const bpe = elemType === "Float" ? 8 : elemType === "Integer" ? 8 : 1;
|
|
572
|
+
return (reader) => {
|
|
573
|
+
const rows = reader.readVarint();
|
|
574
|
+
const cols = reader.readVarint();
|
|
575
|
+
const raw = new Uint8Array(reader.readBytesView(rows * cols * bpe));
|
|
576
|
+
if (elemType === "Float")
|
|
577
|
+
return matrix(new Float64Array(raw.buffer, 0, rows * cols), rows, cols);
|
|
578
|
+
if (elemType === "Integer")
|
|
579
|
+
return matrix(new BigInt64Array(raw.buffer, 0, rows * cols), rows, cols);
|
|
580
|
+
return matrix(new Uint8ClampedArray(raw.buffer, 0, rows * cols), rows, cols);
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
default:
|
|
584
|
+
throw new Error(`Unknown type: ${type.type}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/** Whether a root type takes the segmented container framing. */
|
|
588
|
+
export function isSegmentedRoot(type) {
|
|
589
|
+
return type.type === "Array" || type.type === "Set" || type.type === "Dict";
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Builds a v5 whole-value encoder closure for the given type.
|
|
593
|
+
*
|
|
594
|
+
* @param type - the root East type (as `EastType` or `EastTypeValue`)
|
|
595
|
+
* @param options - encode options (source map, codec, index)
|
|
596
|
+
* @returns a reusable function encoding values of `type` to v5 beast2 bytes
|
|
597
|
+
*/
|
|
598
|
+
export function encodeBeast2V5For(type, options) {
|
|
599
|
+
const typeValue = asTypeValue(type);
|
|
600
|
+
const codec = options?.codec ?? "deflate";
|
|
601
|
+
const withIndex = (options?.index ?? false) && isSegmentedRoot(typeValue);
|
|
602
|
+
const setupTypeCtx = new Map();
|
|
603
|
+
const valueEncoder = buildV5Encoder(typeValue, setupTypeCtx);
|
|
604
|
+
// Header bytes up to the source-map section are value-independent — build
|
|
605
|
+
// the magic + type section once per closure.
|
|
606
|
+
const headWriter = new BufferWriter();
|
|
607
|
+
headWriter.writeBytes(MAGIC_BYTES_V5);
|
|
608
|
+
writeTypeSection(typeValue, headWriter);
|
|
609
|
+
const headBytes = headWriter.toUint8Array();
|
|
610
|
+
return (value) => {
|
|
611
|
+
const sourceMap = options?.sourceMap
|
|
612
|
+
?? value?.[EAST_SOURCE_MAP_SYMBOL]
|
|
613
|
+
?? null;
|
|
614
|
+
const ctx = createV5EncodeContext(sourceMap, false);
|
|
615
|
+
const writer = new BufferWriter();
|
|
616
|
+
writer.writeBytes(headBytes);
|
|
617
|
+
writeSourceMapSectionV5(sourceMap, writer);
|
|
618
|
+
if (!withIndex) {
|
|
619
|
+
// Single frame carrying the whole logical value encoding.
|
|
620
|
+
const logical = new BufferWriter();
|
|
621
|
+
valueEncoder(value, logical, ctx);
|
|
622
|
+
writeFrame(writer, logical.toUint8Array(), codec);
|
|
623
|
+
return writer.toUint8Array();
|
|
624
|
+
}
|
|
625
|
+
// Indexed layout: tag frame, one segment frame, terminator frame — every
|
|
626
|
+
// indexed segment frame holds exactly `varint(n) + n elements`.
|
|
627
|
+
ctx.containerCount++;
|
|
628
|
+
ctx.containerIndex.set(value, 0);
|
|
629
|
+
ctx.segmentBaseDef = 1;
|
|
630
|
+
writeFrame(writer, new Uint8Array([TAG_NEW]), "none");
|
|
631
|
+
const count = typeValue.type === "Array" ? value.length : value.size;
|
|
632
|
+
const segments = [];
|
|
633
|
+
if (count > 0) {
|
|
634
|
+
const logical = new BufferWriter();
|
|
635
|
+
logical.writeVarint(count);
|
|
636
|
+
encodeSegmentElements(typeValue, value, logical, ctx, setupTypeCtx);
|
|
637
|
+
segments.push({ offset: writer.size, count });
|
|
638
|
+
writeFrame(writer, logical.toUint8Array(), codec);
|
|
639
|
+
}
|
|
640
|
+
writeFrame(writer, new Uint8Array([0x00]), "none");
|
|
641
|
+
writeIndexAndFooter(writer, segments, !ctx.crossSegmentRef);
|
|
642
|
+
return writer.toUint8Array();
|
|
643
|
+
};
|
|
644
|
+
function encodeSegmentElements(rootType, value, logical, ctx, typeCtx) {
|
|
645
|
+
if (rootType.type === "Array" || rootType.type === "Set") {
|
|
646
|
+
const elem = getElemEncoder(rootType.value, typeCtx);
|
|
647
|
+
for (const item of value)
|
|
648
|
+
elem(item, logical, ctx);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
const key = getElemEncoder(rootType.value.key, typeCtx);
|
|
652
|
+
const val = getElemEncoder(rootType.value.value, typeCtx);
|
|
653
|
+
for (const [k, v] of value) {
|
|
654
|
+
key(k, logical, ctx);
|
|
655
|
+
val(v, logical, ctx);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const elemEncoderCache = new WeakMap();
|
|
661
|
+
/** Builds (and caches per type context) an element encoder. */
|
|
662
|
+
function getElemEncoder(elemType, typeCtx) {
|
|
663
|
+
let cache = elemEncoderCache.get(typeCtx);
|
|
664
|
+
if (!cache) {
|
|
665
|
+
cache = new Map();
|
|
666
|
+
elemEncoderCache.set(typeCtx, cache);
|
|
667
|
+
}
|
|
668
|
+
let enc = cache.get(elemType);
|
|
669
|
+
if (!enc) {
|
|
670
|
+
enc = buildV5Encoder(elemType, typeCtx);
|
|
671
|
+
cache.set(elemType, enc);
|
|
672
|
+
}
|
|
673
|
+
return enc;
|
|
674
|
+
}
|
|
675
|
+
// =============================================================================
|
|
676
|
+
// Index + footer
|
|
677
|
+
// =============================================================================
|
|
678
|
+
/** The v5 footer magic: the beast2 magic family with terminal byte 0xF5. */
|
|
679
|
+
export const FOOTER_MAGIC_V5 = new Uint8Array([0x89, 0x45, 0x61, 0x73, 0x74, 0x0D, 0x0A, 0xF5]);
|
|
680
|
+
/** Index flag bit: segments are independently decodable (no cross-segment
|
|
681
|
+
* aliasing), so paging readers may seek. */
|
|
682
|
+
export const INDEX_FLAG_SELF_CONTAINED = 0x01;
|
|
683
|
+
/**
|
|
684
|
+
* Writes the index_section and footer.
|
|
685
|
+
*
|
|
686
|
+
* @param writer - the wire-level writer (its current size is the index offset)
|
|
687
|
+
* @param segments - per-segment absolute frame offsets and element counts
|
|
688
|
+
* @param selfContained - whether segments are independently decodable
|
|
689
|
+
*/
|
|
690
|
+
export function writeIndexAndFooter(writer, segments, selfContained) {
|
|
691
|
+
const indexOffset = writer.size;
|
|
692
|
+
writer.writeVarint(selfContained ? INDEX_FLAG_SELF_CONTAINED : 0);
|
|
693
|
+
writer.writeVarint(segments.length);
|
|
694
|
+
let prev = 0;
|
|
695
|
+
for (const seg of segments) {
|
|
696
|
+
writer.writeVarint(seg.offset - prev);
|
|
697
|
+
writer.writeVarint(seg.count);
|
|
698
|
+
prev = seg.offset;
|
|
699
|
+
}
|
|
700
|
+
writeU64LE(writer, indexOffset);
|
|
701
|
+
writer.writeBytes(FOOTER_MAGIC_V5);
|
|
702
|
+
}
|
|
703
|
+
function writeU64LE(writer, value) {
|
|
704
|
+
let v = BigInt(value);
|
|
705
|
+
for (let i = 0; i < 8; i++) {
|
|
706
|
+
writer.writeUint8(Number(v & 0xffn));
|
|
707
|
+
v >>= 8n;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Reads the index_section + footer from the tail of a blob.
|
|
712
|
+
*
|
|
713
|
+
* @param data - the whole blob
|
|
714
|
+
* @returns the parsed index, or `null` when the blob carries no footer
|
|
715
|
+
* @throws {Error} When a footer is present but the index is malformed.
|
|
716
|
+
*/
|
|
717
|
+
export function readIndex(data) {
|
|
718
|
+
if (data.length < 16)
|
|
719
|
+
return null;
|
|
720
|
+
const footerStart = data.length - 16;
|
|
721
|
+
for (let i = 0; i < 8; i++) {
|
|
722
|
+
if (data[footerStart + 8 + i] !== FOOTER_MAGIC_V5[i])
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
let indexOffset = 0n;
|
|
726
|
+
for (let i = 7; i >= 0; i--) {
|
|
727
|
+
indexOffset = (indexOffset << 8n) | BigInt(data[footerStart + i]);
|
|
728
|
+
}
|
|
729
|
+
const offset = Number(indexOffset);
|
|
730
|
+
if (offset < 8 || offset >= footerStart) {
|
|
731
|
+
throw new Error(`beast2 v5: footer index offset ${offset} out of range`);
|
|
732
|
+
}
|
|
733
|
+
const reader = new BufferReader(data, offset);
|
|
734
|
+
const flags = reader.readVarint();
|
|
735
|
+
if ((flags & ~INDEX_FLAG_SELF_CONTAINED) !== 0) {
|
|
736
|
+
throw new Error(`beast2 v5: unknown index flags 0x${flags.toString(16)}`);
|
|
737
|
+
}
|
|
738
|
+
const segmentCount = reader.readVarint();
|
|
739
|
+
const offsets = new Array(segmentCount);
|
|
740
|
+
const counts = new Array(segmentCount);
|
|
741
|
+
let prev = 0;
|
|
742
|
+
let totalCount = 0;
|
|
743
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
744
|
+
prev += reader.readVarint();
|
|
745
|
+
offsets[i] = prev;
|
|
746
|
+
counts[i] = reader.readVarint();
|
|
747
|
+
totalCount += counts[i];
|
|
748
|
+
if (prev >= offset) {
|
|
749
|
+
throw new Error(`beast2 v5: index segment offset ${prev} overlaps the index section`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (reader.offset !== footerStart) {
|
|
753
|
+
throw new Error(`beast2 v5: index section size mismatch (ends at ${reader.offset}, footer at ${footerStart})`);
|
|
754
|
+
}
|
|
755
|
+
return { selfContained: (flags & INDEX_FLAG_SELF_CONTAINED) !== 0, offsets, counts, totalCount };
|
|
756
|
+
}
|
|
757
|
+
/** Parses the v5 header and returns the wire root type plus a reader
|
|
758
|
+
* positioned at the first value-stream frame. */
|
|
759
|
+
function readHeader(data) {
|
|
760
|
+
if (data.length < 8) {
|
|
761
|
+
throw new Error(`Data too short for Beast2 format: ${data.length} bytes`);
|
|
762
|
+
}
|
|
763
|
+
for (let i = 0; i < 8; i++) {
|
|
764
|
+
if (data[i] !== MAGIC_BYTES_V5[i]) {
|
|
765
|
+
throw new Error(`Invalid Beast2 v5 magic at offset ${i}: expected 0x${MAGIC_BYTES_V5[i].toString(16)}, got 0x${data[i].toString(16)}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const reader = new BufferReader(data, MAGIC_BYTES_V5.length);
|
|
769
|
+
const { rootType } = readTypeSection(reader);
|
|
770
|
+
const sourceMap = readSourceMapSectionV5(reader);
|
|
771
|
+
return { rootType, sourceMap, frameOffset: reader.offset };
|
|
772
|
+
}
|
|
773
|
+
/** Decodes a whole v5 blob with the given decode type (`null` = use the wire
|
|
774
|
+
* root type), enforcing whole-stream strictness. */
|
|
775
|
+
function decodeV5(data, decodeType, options, inflate) {
|
|
776
|
+
const { rootType, sourceMap, frameOffset } = readHeader(data);
|
|
777
|
+
const typeValue = decodeType ?? rootType;
|
|
778
|
+
const ctx = {
|
|
779
|
+
containers: [],
|
|
780
|
+
sourceMap,
|
|
781
|
+
...buildPlatformContext(options),
|
|
782
|
+
};
|
|
783
|
+
const cursor = new FrameReader(data, frameOffset, inflate);
|
|
784
|
+
const segmentCounts = isSegmentedRoot(typeValue) ? [] : null;
|
|
785
|
+
let value;
|
|
786
|
+
if (segmentCounts) {
|
|
787
|
+
value = decodeSegmentedRoot(typeValue, cursor, ctx, segmentCounts);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
const reader = cursor.next();
|
|
791
|
+
const dec = cachedRootDecoder(typeValue);
|
|
792
|
+
value = dec(reader, ctx);
|
|
793
|
+
if (reader.offset !== reader.buffer.length) {
|
|
794
|
+
throw new Error(`beast2 v5: ${reader.buffer.length - reader.offset} logical bytes after the root value`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
verifyTrailing(data, cursor.wireOffset, segmentCounts);
|
|
798
|
+
return { value, rootType, sourceMap };
|
|
799
|
+
}
|
|
800
|
+
// Decoder closure trees, shared across decodes of the same root type.
|
|
801
|
+
//
|
|
802
|
+
// Building them per decode is the dominant cost for a large recursive schema
|
|
803
|
+
// — a UIComponentType blob spent ~200 ms per decode in buildDecoder and the
|
|
804
|
+
// GC churn of the closures it allocates, versus 0.04 ms in east-c, whose
|
|
805
|
+
// decoder is a switch over types and allocates nothing. Decoders take their
|
|
806
|
+
// DecodeContext as a parameter and capture nothing per-decode, so sharing
|
|
807
|
+
// them is safe; the key is the root type object, which is a module-level
|
|
808
|
+
// singleton for well-known schemas and the #417-cached instance otherwise.
|
|
809
|
+
const rootDecoderCache = new WeakMap();
|
|
810
|
+
const elementDecoderCache = new WeakMap();
|
|
811
|
+
function cachedRootDecoder(typeValue) {
|
|
812
|
+
const hit = rootDecoderCache.get(typeValue);
|
|
813
|
+
if (hit)
|
|
814
|
+
return hit;
|
|
815
|
+
const dec = buildV5Decoder(typeValue);
|
|
816
|
+
rootDecoderCache.set(typeValue, dec);
|
|
817
|
+
return dec;
|
|
818
|
+
}
|
|
819
|
+
function cachedElementDecoders(typeValue, kind, typeCtx) {
|
|
820
|
+
const hit = elementDecoderCache.get(typeValue);
|
|
821
|
+
if (hit)
|
|
822
|
+
return hit;
|
|
823
|
+
const built = {
|
|
824
|
+
elemDec: kind === "Dict" ? null : buildV5Decoder(typeValue.value, typeCtx),
|
|
825
|
+
keyDec: kind === "Dict" ? buildV5Decoder(typeValue.value.key, typeCtx) : null,
|
|
826
|
+
valDec: kind === "Dict" ? buildV5Decoder(typeValue.value.value, typeCtx) : null,
|
|
827
|
+
};
|
|
828
|
+
elementDecoderCache.set(typeValue, built);
|
|
829
|
+
return built;
|
|
830
|
+
}
|
|
831
|
+
/** Decodes a segmented (Array/Set/Dict) root across frames. */
|
|
832
|
+
function decodeSegmentedRoot(typeValue, cursor, ctx, segmentCounts) {
|
|
833
|
+
let reader = cursor.next();
|
|
834
|
+
const tag = reader.readUint8();
|
|
835
|
+
if (tag !== TAG_NEW) {
|
|
836
|
+
throw new Error(`beast2 v5: root container must be NEW (tag 0x${tag.toString(16)})`);
|
|
837
|
+
}
|
|
838
|
+
const kind = typeValue.type;
|
|
839
|
+
// Sorted containers keep East's total order and give the cross-segment
|
|
840
|
+
// merge rules for free (Set inserts dedup; a later Dict set() overwrites).
|
|
841
|
+
const container = kind === "Array" ? []
|
|
842
|
+
: kind === "Set" ? new SortedSet(undefined, compareFor(typeValue.value))
|
|
843
|
+
: new SortedMap(undefined, compareFor(typeValue.value.key));
|
|
844
|
+
ctx.containers.push(container);
|
|
845
|
+
const typeCtx = new Map();
|
|
846
|
+
const { elemDec, keyDec, valDec } = cachedElementDecoders(typeValue, kind, typeCtx);
|
|
847
|
+
for (;;) {
|
|
848
|
+
if (reader.offset === reader.buffer.length)
|
|
849
|
+
reader = cursor.next();
|
|
850
|
+
const n = reader.readVarint();
|
|
851
|
+
if (n === 0)
|
|
852
|
+
break;
|
|
853
|
+
segmentCounts.push(n);
|
|
854
|
+
if (kind === "Array") {
|
|
855
|
+
for (let i = 0; i < n; i++)
|
|
856
|
+
container.push(elemDec(reader, ctx));
|
|
857
|
+
}
|
|
858
|
+
else if (kind === "Set") {
|
|
859
|
+
for (let i = 0; i < n; i++)
|
|
860
|
+
container.add(elemDec(reader, ctx));
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
for (let i = 0; i < n; i++) {
|
|
864
|
+
const k = keyDec(reader, ctx);
|
|
865
|
+
const v = valDec(reader, ctx);
|
|
866
|
+
container.set(k, v);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (reader.offset !== reader.buffer.length) {
|
|
871
|
+
throw new Error(`beast2 v5: ${reader.buffer.length - reader.offset} logical bytes after the root terminator`);
|
|
872
|
+
}
|
|
873
|
+
return container;
|
|
874
|
+
}
|
|
875
|
+
/** Enforces whole-stream strictness after the value stream: the remaining
|
|
876
|
+
* bytes must be nothing, or a consistent index + footer. */
|
|
877
|
+
function verifyTrailing(data, wireOffset, segmentCounts) {
|
|
878
|
+
if (wireOffset === data.length)
|
|
879
|
+
return;
|
|
880
|
+
if (segmentCounts === null) {
|
|
881
|
+
throw new Error(`beast2 v5: ${data.length - wireOffset} trailing bytes at offset ${wireOffset}`);
|
|
882
|
+
}
|
|
883
|
+
const index = readIndex(data);
|
|
884
|
+
if (!index) {
|
|
885
|
+
throw new Error(`beast2 v5: ${data.length - wireOffset} trailing bytes at offset ${wireOffset} (no footer)`);
|
|
886
|
+
}
|
|
887
|
+
// The footer's index offset must sit exactly where the value stream ended.
|
|
888
|
+
const footerStart = data.length - 16;
|
|
889
|
+
let indexOffset = 0n;
|
|
890
|
+
for (let i = 7; i >= 0; i--) {
|
|
891
|
+
indexOffset = (indexOffset << 8n) | BigInt(data[footerStart + i]);
|
|
892
|
+
}
|
|
893
|
+
if (Number(indexOffset) !== wireOffset) {
|
|
894
|
+
throw new Error(`beast2 v5: index offset ${Number(indexOffset)} does not match end of value stream ${wireOffset}`);
|
|
895
|
+
}
|
|
896
|
+
if (index.counts.length !== segmentCounts.length) {
|
|
897
|
+
throw new Error(`beast2 v5: index declares ${index.counts.length} segments, stream has ${segmentCounts.length}`);
|
|
898
|
+
}
|
|
899
|
+
for (let i = 0; i < segmentCounts.length; i++) {
|
|
900
|
+
if (index.counts[i] !== segmentCounts[i]) {
|
|
901
|
+
throw new Error(`beast2 v5: index segment ${i} count ${index.counts[i]} disagrees with stream count ${segmentCounts[i]}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Builds a v5 decoder closure for the given type.
|
|
907
|
+
*
|
|
908
|
+
* @param type - the expected root East type (as `EastType` or `EastTypeValue`)
|
|
909
|
+
* @param options - decode options (platform functions for decoded functions)
|
|
910
|
+
* @returns a reusable function decoding v5 beast2 bytes to values of `type`
|
|
911
|
+
*/
|
|
912
|
+
export function decodeBeast2V5For(type, options) {
|
|
913
|
+
const typeValue = asTypeValue(type);
|
|
914
|
+
return (data) => decodeV5(data, typeValue, options).value;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Builds an async v5 decoder closure — pre-inflates deflate frames with the
|
|
918
|
+
* platform's async decompressor, so it works in browsers without zlib.
|
|
919
|
+
*
|
|
920
|
+
* @param type - the expected root East type (as `EastType` or `EastTypeValue`)
|
|
921
|
+
* @param options - decode options (platform functions for decoded functions)
|
|
922
|
+
* @returns a reusable async function decoding v5 beast2 bytes
|
|
923
|
+
*/
|
|
924
|
+
export function decodeBeast2V5ForAsync(type, options) {
|
|
925
|
+
const typeValue = asTypeValue(type);
|
|
926
|
+
return async (data) => {
|
|
927
|
+
const { frameOffset } = readHeader(data);
|
|
928
|
+
const inflate = await preInflateFrames(data, frameOffset);
|
|
929
|
+
return decodeV5(data, typeValue, options, inflate).value;
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Decodes a self-describing v5 blob using its embedded root type.
|
|
934
|
+
*
|
|
935
|
+
* @param data - a v5 beast2 blob
|
|
936
|
+
* @param options - decode options (platform functions for decoded functions)
|
|
937
|
+
* @returns the blob's root type and decoded value
|
|
938
|
+
*/
|
|
939
|
+
export function decodeBeast2V5(data, options) {
|
|
940
|
+
const result = decodeV5(data, null, options);
|
|
941
|
+
return { type: result.rootType, value: result.value };
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Decodes a v5 IRType blob and returns both the IR value and the decoded
|
|
945
|
+
* source map. Backs the version-agnostic `decodeEastIR` / `decodeAsyncEastIR`.
|
|
946
|
+
*
|
|
947
|
+
* @param data - a v5 beast2 blob whose root is a Function/AsyncFunction IR
|
|
948
|
+
* @returns the decoded IR value and the blob's source map
|
|
949
|
+
*/
|
|
950
|
+
export function decodeIRWithSourceMapV5(data) {
|
|
951
|
+
const result = decodeV5(data, irTypeValue, undefined);
|
|
952
|
+
return { ir: result.value, sourceMap: result.sourceMap };
|
|
953
|
+
}
|
|
954
|
+
//# sourceMappingURL=codec.js.map
|