turbodata 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +27 -0
  3. package/README.md +215 -0
  4. package/dist/compression.d.ts +8 -0
  5. package/dist/compression.d.ts.map +1 -0
  6. package/dist/compression.js +19 -0
  7. package/dist/compression.js.map +1 -0
  8. package/dist/index.d.ts +16 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +23 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/io.d.ts +37 -0
  13. package/dist/io.d.ts.map +1 -0
  14. package/dist/io.js +174 -0
  15. package/dist/io.js.map +1 -0
  16. package/dist/iterators/message_heap.d.ts +17 -0
  17. package/dist/iterators/message_heap.d.ts.map +1 -0
  18. package/dist/iterators/message_heap.js +83 -0
  19. package/dist/iterators/message_heap.js.map +1 -0
  20. package/dist/iterators/preloaded_topics_group_iterator.d.ts +38 -0
  21. package/dist/iterators/preloaded_topics_group_iterator.d.ts.map +1 -0
  22. package/dist/iterators/preloaded_topics_group_iterator.js +109 -0
  23. package/dist/iterators/preloaded_topics_group_iterator.js.map +1 -0
  24. package/dist/iterators/topics_group_iterator.d.ts +45 -0
  25. package/dist/iterators/topics_group_iterator.d.ts.map +1 -0
  26. package/dist/iterators/topics_group_iterator.js +132 -0
  27. package/dist/iterators/topics_group_iterator.js.map +1 -0
  28. package/dist/loaded_bytes.d.ts +12 -0
  29. package/dist/loaded_bytes.d.ts.map +1 -0
  30. package/dist/loaded_bytes.js +43 -0
  31. package/dist/loaded_bytes.js.map +1 -0
  32. package/dist/msgpack.d.ts +10 -0
  33. package/dist/msgpack.d.ts.map +1 -0
  34. package/dist/msgpack.js +44 -0
  35. package/dist/msgpack.js.map +1 -0
  36. package/dist/multi_reader.d.ts +56 -0
  37. package/dist/multi_reader.d.ts.map +1 -0
  38. package/dist/multi_reader.js +281 -0
  39. package/dist/multi_reader.js.map +1 -0
  40. package/dist/read_fetcher.d.ts +9 -0
  41. package/dist/read_fetcher.d.ts.map +1 -0
  42. package/dist/read_fetcher.js +59 -0
  43. package/dist/read_fetcher.js.map +1 -0
  44. package/dist/read_options.d.ts +40 -0
  45. package/dist/read_options.d.ts.map +1 -0
  46. package/dist/read_options.js +16 -0
  47. package/dist/read_options.js.map +1 -0
  48. package/dist/read_planner.d.ts +37 -0
  49. package/dist/read_planner.d.ts.map +1 -0
  50. package/dist/read_planner.js +110 -0
  51. package/dist/read_planner.js.map +1 -0
  52. package/dist/read_source.d.ts +19 -0
  53. package/dist/read_source.d.ts.map +1 -0
  54. package/dist/read_source.js +15 -0
  55. package/dist/read_source.js.map +1 -0
  56. package/dist/read_strategy.d.ts +52 -0
  57. package/dist/read_strategy.d.ts.map +1 -0
  58. package/dist/read_strategy.js +70 -0
  59. package/dist/read_strategy.js.map +1 -0
  60. package/dist/reader.d.ts +209 -0
  61. package/dist/reader.d.ts.map +1 -0
  62. package/dist/reader.js +649 -0
  63. package/dist/reader.js.map +1 -0
  64. package/dist/sample.d.ts +41 -0
  65. package/dist/sample.d.ts.map +1 -0
  66. package/dist/sample.js +528 -0
  67. package/dist/sample.js.map +1 -0
  68. package/dist/sort_and_filter.d.ts +25 -0
  69. package/dist/sort_and_filter.d.ts.map +1 -0
  70. package/dist/sort_and_filter.js +126 -0
  71. package/dist/sort_and_filter.js.map +1 -0
  72. package/dist/sources/blob.d.ts +8 -0
  73. package/dist/sources/blob.d.ts.map +1 -0
  74. package/dist/sources/blob.js +38 -0
  75. package/dist/sources/blob.js.map +1 -0
  76. package/dist/sources/http.d.ts +17 -0
  77. package/dist/sources/http.d.ts.map +1 -0
  78. package/dist/sources/http.js +90 -0
  79. package/dist/sources/http.js.map +1 -0
  80. package/dist/types.d.ts +75 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +29 -0
  83. package/dist/types.js.map +1 -0
  84. package/package.json +61 -0
  85. package/src/compression.ts +28 -0
  86. package/src/index.ts +70 -0
  87. package/src/io.ts +214 -0
  88. package/src/iterators/message_heap.ts +104 -0
  89. package/src/iterators/preloaded_topics_group_iterator.ts +143 -0
  90. package/src/iterators/topics_group_iterator.ts +180 -0
  91. package/src/loaded_bytes.ts +61 -0
  92. package/src/msgpack.ts +46 -0
  93. package/src/multi_reader.ts +337 -0
  94. package/src/read_fetcher.ts +74 -0
  95. package/src/read_options.ts +58 -0
  96. package/src/read_planner.ts +153 -0
  97. package/src/read_source.ts +33 -0
  98. package/src/read_strategy.ts +111 -0
  99. package/src/reader.ts +910 -0
  100. package/src/sample.ts +715 -0
  101. package/src/sort_and_filter.ts +167 -0
  102. package/src/sources/blob.ts +52 -0
  103. package/src/sources/http.ts +121 -0
  104. package/src/types.ts +110 -0
@@ -0,0 +1,167 @@
1
+ // Copyright 2026 Wanjia He
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // sortAndFilter: pure version that turns a decoded IndexChunk into a flat,
16
+ // timestamp-ordered list of kept (topicId, timestamp, offsetInChunk) plus
17
+ // per-message lengths. Mirrors go/sort_and_filter.go.
18
+ //
19
+ // The "sort" is by per-message OffsetInChunk; the writer interleaves messages
20
+ // in timestamp order, so offset order == timestamp order within a chunk. We
21
+ // preserve that invariant rather than re-sorting by timestamp explicitly.
22
+
23
+ import { MessageHeap } from "./iterators/message_heap.js";
24
+ import type { IndexChunk, MessageIndex, TopicIndex } from "./types.js";
25
+
26
+ export interface MessageRef {
27
+ timestamp: bigint;
28
+ topicId: number;
29
+ offsetInChunk: bigint;
30
+ }
31
+
32
+ interface ItemWithTopic {
33
+ topicId: number;
34
+ mi: MessageIndex;
35
+ }
36
+
37
+ export function sortAndFilter(
38
+ topicIndexes: TopicIndex[],
39
+ totalLen: bigint,
40
+ topicIds: Set<number>,
41
+ startTimestamp: bigint,
42
+ endTimestamp: bigint,
43
+ videoDecodable = false,
44
+ ): { msgs: MessageRef[]; lens: bigint[] } {
45
+ let total = 0;
46
+ for (const ti of topicIndexes) {
47
+ total += ti.messageIndexes.length;
48
+ }
49
+ if (total === 0) {
50
+ return { msgs: [], lens: [] };
51
+ }
52
+
53
+ const sorted: ItemWithTopic[] = new Array(total);
54
+ let writeIdx = 0;
55
+
56
+ if (topicIndexes.length === 1) {
57
+ // Fast path: single topic, messages are already in offset order.
58
+ const ti = topicIndexes[0]!;
59
+ for (const mi of ti.messageIndexes) {
60
+ sorted[writeIdx++] = { topicId: ti.id, mi };
61
+ }
62
+ } else {
63
+ // K-way merge by offsetInChunk across topics, via a min-heap.
64
+ // Mirrors go/sort_and_filter.go's MessageIndexHeap-based merge: O(N log K)
65
+ // regardless of how many topics share a group.
66
+ const heap = new MessageHeap<{ topicSlot: number; cursor: number }>(false);
67
+ for (let t = 0; t < topicIndexes.length; t++) {
68
+ const mis = topicIndexes[t]!.messageIndexes;
69
+ if (mis.length === 0) {
70
+ continue;
71
+ }
72
+ heap.push(mis[0]!.offsetInChunk, { topicSlot: t, cursor: 0 });
73
+ }
74
+ while (heap.size > 0) {
75
+ const top = heap.pop()!;
76
+ const { topicSlot, cursor } = top.value;
77
+ const ti = topicIndexes[topicSlot]!;
78
+ sorted[writeIdx++] = { topicId: ti.id, mi: ti.messageIndexes[cursor]! };
79
+ const next = cursor + 1;
80
+ if (next < ti.messageIndexes.length) {
81
+ heap.push(ti.messageIndexes[next]!.offsetInChunk, {
82
+ topicSlot,
83
+ cursor: next,
84
+ });
85
+ }
86
+ }
87
+ }
88
+
89
+ // Per-entry lengths from offset deltas.
90
+ const allLens = new Array<bigint>(sorted.length);
91
+ for (let i = 0; i < sorted.length - 1; i++) {
92
+ allLens[i] = sorted[i + 1]!.mi.offsetInChunk - sorted[i]!.mi.offsetInChunk;
93
+ }
94
+ allLens[allLens.length - 1] =
95
+ totalLen - sorted[sorted.length - 1]!.mi.offsetInChunk;
96
+
97
+ // For video-decodable groups, snap the lower bound back to the anchoring
98
+ // key frame so the kept sequence is decodable cold.
99
+ let effectiveStart = startTimestamp;
100
+ if (videoDecodable) {
101
+ effectiveStart = keyFrameStart(topicIndexes, startTimestamp);
102
+ }
103
+
104
+ // Filter by topic + timestamp range.
105
+ const msgs: MessageRef[] = [];
106
+ const lens: bigint[] = [];
107
+ for (let i = 0; i < sorted.length; i++) {
108
+ const item = sorted[i]!;
109
+ if (!topicIds.has(item.topicId)) {
110
+ continue;
111
+ }
112
+ const ts = item.mi.timestamp;
113
+ if (ts < effectiveStart || ts > endTimestamp) {
114
+ continue;
115
+ }
116
+ msgs.push({
117
+ timestamp: ts,
118
+ topicId: item.topicId,
119
+ offsetInChunk: item.mi.offsetInChunk,
120
+ });
121
+ lens.push(allLens[i]!);
122
+ }
123
+ return { msgs, lens };
124
+ }
125
+
126
+ /**
127
+ * Timestamp of the latest key frame whose timestamp is <= startTimestamp, or
128
+ * startTimestamp unchanged if no such key frame exists in this chunk. Mirrors
129
+ * py key_frame_start / go keyFrameStart.
130
+ *
131
+ * Video-decodable groups always hold exactly one topic. keyFrameIndexes are
132
+ * ascending positions into that topic's timestamp-ordered messageIndexes.
133
+ */
134
+ export function keyFrameStart(
135
+ topicIndexes: TopicIndex[],
136
+ startTimestamp: bigint,
137
+ ): bigint {
138
+ if (topicIndexes.length === 0) {
139
+ return startTimestamp;
140
+ }
141
+ const ti = topicIndexes[0]!;
142
+ let anchor = startTimestamp;
143
+ for (const kfIdx of ti.keyFrameIndexes) {
144
+ const ts = ti.messageIndexes[kfIdx]!.timestamp;
145
+ if (ts > startTimestamp) {
146
+ break;
147
+ }
148
+ anchor = ts;
149
+ }
150
+ return anchor;
151
+ }
152
+
153
+ /** Convenience: sort+filter directly from a decoded IndexChunk. */
154
+ export function sortAndFilterChunk(
155
+ chunk: IndexChunk,
156
+ topicIds: Set<number>,
157
+ startTimestamp: bigint,
158
+ endTimestamp: bigint,
159
+ ): { msgs: MessageRef[]; lens: bigint[] } {
160
+ return sortAndFilter(
161
+ chunk.topicIndexes,
162
+ chunk.uncompressedLen,
163
+ topicIds,
164
+ startTimestamp,
165
+ endTimestamp,
166
+ );
167
+ }
@@ -0,0 +1,52 @@
1
+ // Copyright 2026 Wanjia He
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // BlobReadSource: ReadSource backed by a Blob (or File, which extends Blob).
16
+ //
17
+ // Blob.slice() is synchronous, so read concurrency is implicit at the browser
18
+ // level — the only async work is decoding to Uint8Array via arrayBuffer().
19
+
20
+ import type { ReadSource } from "../read_source.js";
21
+
22
+ export class BlobReadSource implements ReadSource {
23
+ private readonly blob: Blob;
24
+
25
+ constructor(blob: Blob) {
26
+ this.blob = blob;
27
+ }
28
+
29
+ async size(): Promise<bigint> {
30
+ return BigInt(this.blob.size);
31
+ }
32
+
33
+ async read(offset: bigint, length: bigint): Promise<Uint8Array> {
34
+ // Blob.slice takes number offsets. We accept that browsers can't host a
35
+ // single Blob > 2^53 bytes anyway; clamp via Number() and validate.
36
+ const start = Number(offset);
37
+ const end = Number(offset + length);
38
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end)) {
39
+ throw new Error(
40
+ `BlobReadSource.read: offset/length exceed Number.MAX_SAFE_INTEGER (offset=${offset}, length=${length})`,
41
+ );
42
+ }
43
+ const slice = this.blob.slice(start, end);
44
+ const buf = await slice.arrayBuffer();
45
+ if (buf.byteLength !== Number(length)) {
46
+ throw new Error(
47
+ `BlobReadSource.read: short read at offset ${offset}, expected ${length}, got ${buf.byteLength}`,
48
+ );
49
+ }
50
+ return new Uint8Array(buf);
51
+ }
52
+ }
@@ -0,0 +1,121 @@
1
+ // Copyright 2026 Wanjia He
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // HttpRangeReadSource: ReadSource backed by HTTP Range requests.
16
+ //
17
+ // The browser's fetch caps concurrent connections per origin (~6 on HTTP/1.1,
18
+ // effectively unlimited mux on HTTP/2). The Reader's cost-aware path bounds
19
+ // concurrency to `maxConcurrency`; HTTP-level limits are an additional cap.
20
+ //
21
+ // `size()` is resolved lazily via a HEAD (or GET Range: bytes=0-0). We prefer
22
+ // HEAD because most static-file servers respond cheaply, but fall back to a
23
+ // 1-byte GET if the server forbids HEAD.
24
+
25
+ import type { ReadSource } from "../read_source.js";
26
+
27
+ export interface HttpRangeReadSourceOptions {
28
+ /** Custom fetch implementation (e.g. for tests or auth headers). */
29
+ fetch?: typeof fetch;
30
+ /** Extra request init applied to every request (e.g. credentials, headers). */
31
+ init?: RequestInit;
32
+ }
33
+
34
+ export class HttpRangeReadSource implements ReadSource {
35
+ private readonly url: string;
36
+ private readonly fetchImpl: typeof fetch;
37
+ private readonly init: RequestInit;
38
+ private cachedSize: bigint | undefined;
39
+
40
+ constructor(url: string, opts: HttpRangeReadSourceOptions = {}) {
41
+ this.url = url;
42
+ this.fetchImpl = opts.fetch ?? fetch.bind(globalThis);
43
+ this.init = opts.init ?? {};
44
+ }
45
+
46
+ async size(): Promise<bigint> {
47
+ if (this.cachedSize !== undefined) {
48
+ return this.cachedSize;
49
+ }
50
+ // Try HEAD first.
51
+ let resp: Response;
52
+ try {
53
+ resp = await this.fetchImpl(this.url, { ...this.init, method: "HEAD" });
54
+ } catch (err) {
55
+ throw new Error(
56
+ `HttpRangeReadSource: HEAD failed for ${this.url}: ${(err as Error).message}`,
57
+ );
58
+ }
59
+ if (resp.ok) {
60
+ const cl = resp.headers.get("content-length");
61
+ if (cl !== null) {
62
+ const v = BigInt(cl);
63
+ this.cachedSize = v;
64
+ return v;
65
+ }
66
+ }
67
+ // Fallback: a 1-byte ranged GET, parse Content-Range total.
68
+ const probe = await this.fetchImpl(this.url, {
69
+ ...this.init,
70
+ method: "GET",
71
+ headers: { ...(this.init.headers ?? {}), Range: "bytes=0-0" },
72
+ });
73
+ if (!probe.ok && probe.status !== 206) {
74
+ throw new Error(
75
+ `HttpRangeReadSource: probe GET failed: ${probe.status} ${probe.statusText}`,
76
+ );
77
+ }
78
+ const cr = probe.headers.get("content-range");
79
+ if (cr === null) {
80
+ throw new Error(
81
+ `HttpRangeReadSource: server returned no Content-Length or Content-Range`,
82
+ );
83
+ }
84
+ // Format: "bytes 0-0/12345"
85
+ const m = /\/(\d+)$/.exec(cr);
86
+ if (m === null) {
87
+ throw new Error(`HttpRangeReadSource: malformed Content-Range: ${cr}`);
88
+ }
89
+ const v = BigInt(m[1]!);
90
+ this.cachedSize = v;
91
+ // Drain the probe body so the connection can be reused.
92
+ await probe.arrayBuffer();
93
+ return v;
94
+ }
95
+
96
+ async read(offset: bigint, length: bigint): Promise<Uint8Array> {
97
+ if (length <= 0n) {
98
+ return new Uint8Array(0);
99
+ }
100
+ const end = offset + length - 1n;
101
+ const headers = new Headers(this.init.headers);
102
+ headers.set("Range", `bytes=${offset.toString()}-${end.toString()}`);
103
+ const resp = await this.fetchImpl(this.url, {
104
+ ...this.init,
105
+ method: "GET",
106
+ headers,
107
+ });
108
+ if (!resp.ok && resp.status !== 206) {
109
+ throw new Error(
110
+ `HttpRangeReadSource: GET range ${offset}-${end} failed: ${resp.status} ${resp.statusText}`,
111
+ );
112
+ }
113
+ const buf = new Uint8Array(await resp.arrayBuffer());
114
+ if (BigInt(buf.byteLength) !== length) {
115
+ throw new Error(
116
+ `HttpRangeReadSource: short read at ${offset}, expected ${length}, got ${buf.byteLength}`,
117
+ );
118
+ }
119
+ return buf;
120
+ }
121
+ }
package/src/types.ts ADDED
@@ -0,0 +1,110 @@
1
+ // Copyright 2026 Wanjia He
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // Domain types for turbodata. Mirrors go/types.go.
16
+ //
17
+ // Numeric type convention (matches @mcap/core):
18
+ // - Go int64 -> bigint
19
+ // - Go uint8 / uint16 / uint32 -> number
20
+
21
+ export interface Footer {
22
+ /** uncompressed: int64. Length of the (zstd-compressed) summary block. */
23
+ summaryLen: bigint;
24
+ /** Always equals "7URB0" for valid files. */
25
+ magic: Uint8Array;
26
+ }
27
+
28
+ export interface Summary {
29
+ topicsInfos: TopicsInfo[];
30
+ }
31
+
32
+ export interface TopicsInfo {
33
+ topicMetadatas: TopicMetadata[];
34
+ indexChunkInfoList: IndexChunkInfo[];
35
+ /**
36
+ * int64. Total length of all index chunks for this topic group, in bytes
37
+ * (compressed). Used by the reader to compute the length of the *last*
38
+ * index chunk in the group.
39
+ */
40
+ totalLen: bigint;
41
+ }
42
+
43
+ export interface TopicMetadata {
44
+ /** uint16 */
45
+ id: number;
46
+ name: string;
47
+ /**
48
+ * Decoded msgpack map. Int64 values are decoded as bigint (via
49
+ * `useBigInt64: true`); smaller integers stay as number.
50
+ */
51
+ metadata: Map<string, unknown>;
52
+ /** uint32. Number of messages written into this topic. */
53
+ messageCount: number;
54
+ }
55
+
56
+ export interface IndexChunkInfo {
57
+ /** int64 */
58
+ startTimestamp: bigint;
59
+ /** int64 */
60
+ endTimestamp: bigint;
61
+ /** int64. Absolute file offset where the (compressed) index chunk starts. */
62
+ offset: bigint;
63
+ }
64
+
65
+ export interface IndexChunk {
66
+ topicIndexes: TopicIndex[];
67
+ /** int64. Absolute file offset of this index chunk's data. */
68
+ chunkOffset: bigint;
69
+ /** int64. On-disk (possibly compressed) data length in bytes. */
70
+ chunkLen: bigint;
71
+ /** int64. Decompressed data length, used to compute the last message's length. */
72
+ uncompressedLen: bigint;
73
+ }
74
+
75
+ export interface TopicIndex {
76
+ /** uint16 */
77
+ id: number;
78
+ messageIndexes: MessageIndex[];
79
+ /**
80
+ * uint32 entries: ascending positions into `messageIndexes` marking key
81
+ * frames. Consumed by the video-decodable read/sample paths to anchor GOPs.
82
+ */
83
+ keyFrameIndexes: Uint32Array;
84
+ }
85
+
86
+ export interface MessageIndex {
87
+ /** int64 */
88
+ timestamp: bigint;
89
+ /** int64. Offset relative to the start of the (uncompressed) data chunk. */
90
+ offsetInChunk: bigint;
91
+ }
92
+
93
+ /** The 5 magic bytes that close every turbodata file: "7URB0". */
94
+ export const MAGIC: Uint8Array = new Uint8Array([0x37, 0x55, 0x52, 0x42, 0x30]);
95
+
96
+ /** Footer is fixed at 13 bytes: 8 (summaryLen) + 5 (magic). */
97
+ export const FOOTER_LEN = 13;
98
+
99
+ // Common per-topic schema metadata keys. SCHEMA_DATA holds raw schema bytes.
100
+ export const META_KEY_SCHEMA_NAME = "schema_name";
101
+ export const META_KEY_SCHEMA_ENCODING = "schema_encoding";
102
+ export const META_KEY_SCHEMA_DATA = "schema_data";
103
+
104
+ // Internal per-topic metadata keys used to carry write-format control flags
105
+ // alongside caller-supplied metadata. The __td_ prefix namespaces them away
106
+ // from user keys. The reader reads these back to drive decompression and
107
+ // video-aware sampling.
108
+ export const META_KEY_COMPRESSED = "__td_is_compressed";
109
+ export const META_KEY_VIDEO = "__td_is_video";
110
+ export const META_KEY_CHUNK_CONFIG = "__td_chunk_config";