@platformatic/sqlite-js-stream 0.0.0-stage → 0.1.1

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 (162) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +22 -0
  4. package/README.md +482 -2
  5. package/dist/backends/file/index.d.ts +4 -0
  6. package/dist/backends/file/index.d.ts.map +1 -0
  7. package/dist/backends/file/index.js +3 -0
  8. package/dist/backends/file/leaser.d.ts +20 -0
  9. package/dist/backends/file/leaser.d.ts.map +1 -0
  10. package/dist/backends/file/leaser.js +123 -0
  11. package/dist/backends/file/replica-client.d.ts +38 -0
  12. package/dist/backends/file/replica-client.d.ts.map +1 -0
  13. package/dist/backends/file/replica-client.js +209 -0
  14. package/dist/db.d.ts +80 -0
  15. package/dist/db.d.ts.map +1 -0
  16. package/dist/db.js +484 -0
  17. package/dist/errors.d.ts +42 -0
  18. package/dist/errors.d.ts.map +1 -0
  19. package/dist/errors.js +66 -0
  20. package/dist/index.d.ts +19 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +22 -0
  23. package/dist/leaser.d.ts +55 -0
  24. package/dist/leaser.d.ts.map +1 -0
  25. package/dist/leaser.js +60 -0
  26. package/dist/ltx/byte-reader.d.ts +18 -0
  27. package/dist/ltx/byte-reader.d.ts.map +1 -0
  28. package/dist/ltx/byte-reader.js +38 -0
  29. package/dist/ltx/bytes.d.ts +20 -0
  30. package/dist/ltx/bytes.d.ts.map +1 -0
  31. package/dist/ltx/bytes.js +69 -0
  32. package/dist/ltx/checksum.d.ts +20 -0
  33. package/dist/ltx/checksum.d.ts.map +1 -0
  34. package/dist/ltx/checksum.js +42 -0
  35. package/dist/ltx/compaction-level.d.ts +58 -0
  36. package/dist/ltx/compaction-level.d.ts.map +1 -0
  37. package/dist/ltx/compaction-level.js +115 -0
  38. package/dist/ltx/compactor.d.ts +33 -0
  39. package/dist/ltx/compactor.d.ts.map +1 -0
  40. package/dist/ltx/compactor.js +137 -0
  41. package/dist/ltx/constants.d.ts +49 -0
  42. package/dist/ltx/constants.d.ts.map +1 -0
  43. package/dist/ltx/constants.js +58 -0
  44. package/dist/ltx/crc64.d.ts +29 -0
  45. package/dist/ltx/crc64.d.ts.map +1 -0
  46. package/dist/ltx/crc64.js +57 -0
  47. package/dist/ltx/decoder.d.ts +55 -0
  48. package/dist/ltx/decoder.d.ts.map +1 -0
  49. package/dist/ltx/decoder.js +236 -0
  50. package/dist/ltx/encoder.d.ts +49 -0
  51. package/dist/ltx/encoder.d.ts.map +1 -0
  52. package/dist/ltx/encoder.js +159 -0
  53. package/dist/ltx/file-info.d.ts +24 -0
  54. package/dist/ltx/file-info.d.ts.map +1 -0
  55. package/dist/ltx/file-info.js +74 -0
  56. package/dist/ltx/filename.d.ts +13 -0
  57. package/dist/ltx/filename.d.ts.map +1 -0
  58. package/dist/ltx/filename.js +23 -0
  59. package/dist/ltx/header.d.ts +17 -0
  60. package/dist/ltx/header.d.ts.map +1 -0
  61. package/dist/ltx/header.js +115 -0
  62. package/dist/ltx/index.d.ts +24 -0
  63. package/dist/ltx/index.d.ts.map +1 -0
  64. package/dist/ltx/index.js +25 -0
  65. package/dist/ltx/lz4.d.ts +41 -0
  66. package/dist/ltx/lz4.d.ts.map +1 -0
  67. package/dist/ltx/lz4.js +264 -0
  68. package/dist/ltx/page-index.d.ts +26 -0
  69. package/dist/ltx/page-index.d.ts.map +1 -0
  70. package/dist/ltx/page-index.js +118 -0
  71. package/dist/ltx/page.d.ts +27 -0
  72. package/dist/ltx/page.d.ts.map +1 -0
  73. package/dist/ltx/page.js +66 -0
  74. package/dist/ltx/sink.d.ts +17 -0
  75. package/dist/ltx/sink.d.ts.map +1 -0
  76. package/dist/ltx/sink.js +21 -0
  77. package/dist/ltx/stream.d.ts +64 -0
  78. package/dist/ltx/stream.d.ts.map +1 -0
  79. package/dist/ltx/stream.js +248 -0
  80. package/dist/ltx/streaming-compactor.d.ts +34 -0
  81. package/dist/ltx/streaming-compactor.d.ts.map +1 -0
  82. package/dist/ltx/streaming-compactor.js +140 -0
  83. package/dist/ltx/trailer.d.ts +12 -0
  84. package/dist/ltx/trailer.d.ts.map +1 -0
  85. package/dist/ltx/trailer.js +47 -0
  86. package/dist/ltx/types.d.ts +79 -0
  87. package/dist/ltx/types.d.ts.map +1 -0
  88. package/dist/ltx/types.js +70 -0
  89. package/dist/replica-client.d.ts +92 -0
  90. package/dist/replica-client.d.ts.map +1 -0
  91. package/dist/replica-client.js +122 -0
  92. package/dist/replica-url.d.ts +20 -0
  93. package/dist/replica-url.d.ts.map +1 -0
  94. package/dist/replica-url.js +49 -0
  95. package/dist/replica.d.ts +379 -0
  96. package/dist/replica.d.ts.map +1 -0
  97. package/dist/replica.js +1253 -0
  98. package/dist/store.d.ts +34 -0
  99. package/dist/store.d.ts.map +1 -0
  100. package/dist/store.js +60 -0
  101. package/dist/util/fs.d.ts +28 -0
  102. package/dist/util/fs.d.ts.map +1 -0
  103. package/dist/util/fs.js +73 -0
  104. package/dist/util/logger.d.ts +10 -0
  105. package/dist/util/logger.d.ts.map +1 -0
  106. package/dist/util/logger.js +7 -0
  107. package/dist/util/stream.d.ts +11 -0
  108. package/dist/util/stream.d.ts.map +1 -0
  109. package/dist/util/stream.js +25 -0
  110. package/dist/wal/checksum.d.ts +17 -0
  111. package/dist/wal/checksum.d.ts.map +1 -0
  112. package/dist/wal/checksum.js +30 -0
  113. package/dist/wal/constants.d.ts +8 -0
  114. package/dist/wal/constants.d.ts.map +1 -0
  115. package/dist/wal/constants.js +7 -0
  116. package/dist/wal/index.d.ts +5 -0
  117. package/dist/wal/index.d.ts.map +1 -0
  118. package/dist/wal/index.js +4 -0
  119. package/dist/wal/reader.d.ts +61 -0
  120. package/dist/wal/reader.d.ts.map +1 -0
  121. package/dist/wal/reader.js +207 -0
  122. package/package.json +68 -3
  123. package/src/backends/file/index.ts +3 -0
  124. package/src/backends/file/leaser.ts +145 -0
  125. package/src/backends/file/replica-client.ts +246 -0
  126. package/src/db.ts +553 -0
  127. package/src/errors.ts +74 -0
  128. package/src/index.ts +26 -0
  129. package/src/leaser.ts +86 -0
  130. package/src/ltx/byte-reader.ts +46 -0
  131. package/src/ltx/bytes.ts +87 -0
  132. package/src/ltx/checksum.ts +46 -0
  133. package/src/ltx/compaction-level.ts +135 -0
  134. package/src/ltx/compactor.ts +167 -0
  135. package/src/ltx/constants.ts +71 -0
  136. package/src/ltx/crc64.ts +65 -0
  137. package/src/ltx/decoder.ts +282 -0
  138. package/src/ltx/encoder.ts +200 -0
  139. package/src/ltx/file-info.ts +81 -0
  140. package/src/ltx/filename.ts +27 -0
  141. package/src/ltx/header.ts +126 -0
  142. package/src/ltx/index.ts +27 -0
  143. package/src/ltx/lz4.ts +279 -0
  144. package/src/ltx/page-index.ts +140 -0
  145. package/src/ltx/page.ts +80 -0
  146. package/src/ltx/sink.ts +33 -0
  147. package/src/ltx/stream.ts +286 -0
  148. package/src/ltx/streaming-compactor.ts +169 -0
  149. package/src/ltx/trailer.ts +58 -0
  150. package/src/ltx/types.ts +152 -0
  151. package/src/lz4js.d.ts +27 -0
  152. package/src/replica-client.ts +222 -0
  153. package/src/replica-url.ts +72 -0
  154. package/src/replica.ts +1480 -0
  155. package/src/store.ts +71 -0
  156. package/src/util/fs.ts +82 -0
  157. package/src/util/logger.ts +15 -0
  158. package/src/util/stream.ts +28 -0
  159. package/src/wal/checksum.ts +39 -0
  160. package/src/wal/constants.ts +10 -0
  161. package/src/wal/index.ts +5 -0
  162. package/src/wal/reader.ts +264 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * LZ4 block-codec adapter.
3
+ *
4
+ * The LTX format uses LZ4 **block** format (not frame format), matching Go's
5
+ * `github.com/pierrec/lz4/v4` `CompressBlock`/`UncompressBlock`.
6
+ *
7
+ * `compressBlock` is a direct TypeScript port of `pierrec/lz4/v4`'s internal
8
+ * `lz4block.Compressor.CompressBlock` (the default Fast compressor used by
9
+ * litestream). It is **byte-identical** to Go's output for the same input, so
10
+ * LTX files we write match Go's `superfly/ltx` encoder exactly (validated
11
+ * against the Go fixtures in `test/ltx/go-interop.test.ts` and CI's
12
+ * `interop-go` job).
13
+ *
14
+ * `decompressBlock`/`decompressBlockInto` delegate to the pure-JS `lz4js`
15
+ * implementation, which decodes any conforming LZ4 block (decompression does
16
+ * not need to be byte-identical).
17
+ */
18
+ import lz4 from 'lz4js';
19
+ import { LTXFormatError } from "../errors.js";
20
+ // Compressor constants, mirrored from `internal/lz4block/block.go`.
21
+ const HASH_LOG = 16;
22
+ const HT_SIZE = 1 << HASH_LOG; // 65536 hash-table entries.
23
+ const WIN_SIZE = 1 << 16; // 64 KiB window.
24
+ const WIN_MASK = WIN_SIZE - 1;
25
+ const MIN_MATCH = 4;
26
+ const MF_LIMIT = 10 + MIN_MATCH; // Last match cannot start within the last 14 bytes.
27
+ const ADAPT_SKIP_LOG = 7;
28
+ const PRIME6 = 227718039650203n;
29
+ const U64_MASK = 0xffffffffffffffffn;
30
+ const U32_MASK = 0xffffffffn;
31
+ // Hash table state. `inUse` is a bitmap so we can clear the table cheaply.
32
+ const hashTable = new Uint16Array(HT_SIZE);
33
+ const inUse = new Uint32Array(HT_SIZE / 32);
34
+ /**
35
+ * compressBlockBound returns the maximum compressed size for `n` input bytes
36
+ * using the Fast compressor (`n + n/255 + 16`).
37
+ */
38
+ export function compressBlockBound(n) {
39
+ return n + ((n / 255) | 0) + 16;
40
+ }
41
+ /** Hashes the lower 6 bytes of a 64-bit little-endian value into a hash index. */
42
+ function blockHash(x) {
43
+ return Number((((x << 16n) * PRIME6) & U64_MASK) >> 48n);
44
+ }
45
+ /** Reads the 64-bit little-endian value at `o` (used by the Fast scan). */
46
+ function readU64(b, o) {
47
+ let x = 0n;
48
+ for (let i = 7; i >= 0; i--)
49
+ x = (x << 8n) | BigInt(b[o + i]);
50
+ return x;
51
+ }
52
+ /** Reads the 32-bit little-endian value at `o`. */
53
+ function readU32(b, o) {
54
+ return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0;
55
+ }
56
+ /** Returns the number of leading zero bytes in a 64-bit little-endian value. */
57
+ function trailingZeroBytes(x) {
58
+ let n = 0;
59
+ while ((x & 0xffn) === 0n && n < 8) {
60
+ x >>= 8n;
61
+ n++;
62
+ }
63
+ return n;
64
+ }
65
+ /**
66
+ * Look up the two 16-bit halves of a stored match offset for a hash. Mirrors
67
+ * `Compressor.get`: the table stores 16-bit offsets relative to a 64 KiB block
68
+ * boundary. Returns a position that may be before the start of the window (in
69
+ * which case `offset >= WIN_SIZE` short-circuits the candidate check).
70
+ */
71
+ function tableGet(h, si) {
72
+ h &= HT_SIZE - 1;
73
+ let i = 0;
74
+ if (inUse[h >>> 5] & (1 << (h & 31)))
75
+ i = hashTable[h];
76
+ i += si & ~WIN_MASK;
77
+ if (i >= si)
78
+ i -= WIN_SIZE;
79
+ return i;
80
+ }
81
+ function tablePut(h, si) {
82
+ h &= HT_SIZE - 1;
83
+ hashTable[h] = si & 0xffff;
84
+ inUse[h >>> 5] |= 1 << (h & 31);
85
+ }
86
+ /**
87
+ * Compresses `src` into `dst` as a Fast-mode LZ4 block, writing at offset 0.
88
+ * Mirrors `lz4block.Compressor.CompressBlock`. Returns the number of bytes
89
+ * written, or 0 if `dst` is too small (which happens only when the caller does
90
+ * not size `dst` with `compressBlockBound`).
91
+ */
92
+ function compressBlockInto(src, dst) {
93
+ // Clear the reused table so output is deterministic (issue #65).
94
+ inUse.fill(0);
95
+ // In normal use `dst` is sized by `compressBlockBound`, so the compressor
96
+ // always emits a valid block (even for incompressible input). An undersized
97
+ // `dst` is caught by the `di >= dst.length` guards below and returns 0.
98
+ let si = 0;
99
+ let di = 0;
100
+ let anchor = 0;
101
+ const sn = src.length - MF_LIMIT;
102
+ if (sn > 0) {
103
+ for (; si < sn;) {
104
+ const match = readU64(src, si);
105
+ let h = blockHash(match);
106
+ const h2 = blockHash(match >> 8n);
107
+ const ref = tableGet(h, si);
108
+ const ref2 = tableGet(h2, si + 1);
109
+ tablePut(h, si);
110
+ tablePut(h2, si + 1);
111
+ let offset = si - ref;
112
+ if (offset <= 0 || offset >= WIN_SIZE || (match & U32_MASK) !== BigInt(readU32(src, ref))) {
113
+ h = blockHash(match >> 16n);
114
+ const ref3 = tableGet(h, si + 2);
115
+ si += 1;
116
+ offset = si - ref2;
117
+ if (offset <= 0 || offset >= WIN_SIZE || ((match >> 8n) & U32_MASK) !== BigInt(readU32(src, ref2))) {
118
+ si += 1;
119
+ offset = si - ref3;
120
+ tablePut(h, si);
121
+ if (offset <= 0 || offset >= WIN_SIZE || ((match >> 16n) & U32_MASK) !== BigInt(readU32(src, ref3))) {
122
+ si += 2 + ((si - anchor) >>> ADAPT_SKIP_LOG);
123
+ continue;
124
+ }
125
+ }
126
+ }
127
+ // Match found.
128
+ let lLen = si - anchor;
129
+ let mLen = 4;
130
+ // Extend backwards, reducing literals.
131
+ let tOff = si - offset - 1;
132
+ while (lLen > 0 && tOff >= 0 && src[si - 1] === src[tOff]) {
133
+ si--;
134
+ tOff--;
135
+ lLen--;
136
+ mLen++;
137
+ }
138
+ // Continue search at the end of the match. `matchBase` is the position
139
+ // just past the first 4 matched bytes.
140
+ const matchBase = si + MIN_MATCH;
141
+ si = si + mLen;
142
+ // Find the longest match by comparing 8 bytes at a time.
143
+ while (si + 8 <= sn) {
144
+ const x = readU64(src, si) ^ readU64(src, si - offset);
145
+ if (x === 0n) {
146
+ si += 8;
147
+ }
148
+ else {
149
+ si += trailingZeroBytes(x);
150
+ break;
151
+ }
152
+ }
153
+ mLen = si - matchBase;
154
+ if (di >= dst.length)
155
+ return 0;
156
+ if (mLen < 0xf)
157
+ dst[di] = mLen;
158
+ else
159
+ dst[di] = 0xf;
160
+ // Encode literal length.
161
+ if (lLen < 0xf) {
162
+ dst[di] |= lLen << 4;
163
+ }
164
+ else {
165
+ dst[di] |= 0xf0;
166
+ di++;
167
+ let l = lLen - 0xf;
168
+ while (l >= 0xff && di < dst.length) {
169
+ dst[di] = 0xff;
170
+ di++;
171
+ l -= 0xff;
172
+ }
173
+ if (di >= dst.length)
174
+ return 0;
175
+ dst[di] = l;
176
+ }
177
+ di++;
178
+ if (di + lLen > dst.length)
179
+ return 0;
180
+ dst.set(src.subarray(anchor, anchor + lLen), di);
181
+ di += lLen + 2;
182
+ anchor = si;
183
+ if (di > dst.length)
184
+ return 0;
185
+ dst[di - 2] = offset & 0xff;
186
+ dst[di - 1] = (offset >> 8) & 0xff;
187
+ // Encode match length part 2.
188
+ if (mLen >= 0xf) {
189
+ let m = mLen - 0xf;
190
+ while (m >= 0xff && di < dst.length) {
191
+ dst[di] = 0xff;
192
+ di++;
193
+ m -= 0xff;
194
+ }
195
+ if (di >= dst.length)
196
+ return 0;
197
+ dst[di] = m;
198
+ di++;
199
+ }
200
+ if (si >= sn)
201
+ break;
202
+ // Hash match end-2.
203
+ h = blockHash(readU64(src, si - 2));
204
+ tablePut(h, si - 2);
205
+ }
206
+ }
207
+ // Last literals.
208
+ if (di >= dst.length)
209
+ return 0;
210
+ const lLen = src.length - anchor;
211
+ if (lLen < 0xf) {
212
+ dst[di] = lLen << 4;
213
+ }
214
+ else {
215
+ dst[di] = 0xf0;
216
+ di++;
217
+ let l = lLen - 0xf;
218
+ while (l >= 0xff && di < dst.length) {
219
+ dst[di] = 0xff;
220
+ di++;
221
+ l -= 0xff;
222
+ }
223
+ if (di >= dst.length)
224
+ return 0;
225
+ dst[di] = l;
226
+ }
227
+ di++;
228
+ if (di + (src.length - anchor) > dst.length)
229
+ return 0;
230
+ dst.set(src.subarray(anchor), di);
231
+ di += src.length - anchor;
232
+ return di;
233
+ }
234
+ /**
235
+ * compressBlock compresses `data` as a Fast-mode LZ4 block and returns the
236
+ * compressed bytes. The result is byte-identical to Go's `pierrec/lz4/v4`
237
+ * `CompressBlock` for the same input.
238
+ */
239
+ export function compressBlock(data) {
240
+ const dst = new Uint8Array(compressBlockBound(data.length));
241
+ const n = compressBlockInto(data, dst);
242
+ return dst.slice(0, n);
243
+ }
244
+ /**
245
+ * decompressBlock decompresses an LZ4 block into a buffer of exactly
246
+ * `uncompressedSize` bytes. Throws if the block is invalid or produces a
247
+ * different size.
248
+ */
249
+ export function decompressBlock(data, uncompressedSize) {
250
+ const dst = new Uint8Array(uncompressedSize);
251
+ const n = lz4.decompressBlock(data, dst, 0, data.length, 0);
252
+ if (n !== uncompressedSize) {
253
+ throw new LTXFormatError(`lz4 block decompressed to ${n} bytes, expected ${uncompressedSize}`);
254
+ }
255
+ return dst;
256
+ }
257
+ /**
258
+ * decompressBlockInto decompresses an LZ4 block into `dst` and returns the
259
+ * number of bytes written. Does not require knowing the uncompressed size
260
+ * ahead of time.
261
+ */
262
+ export function decompressBlockInto(data, dst) {
263
+ return lz4.decompressBlock(data, dst, 0, data.length, 0);
264
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * LTX page index encoding/decoding.
3
+ *
4
+ * The page index is a sequence of varint triples `(pgno, offset, size)` in
5
+ * ascending page order, terminated by a zero varint, followed by an 8-byte
6
+ * big-endian size of the varint bytes (excluding the size field itself).
7
+ */
8
+ import type { ByteReader } from './byte-reader.js';
9
+ import type { PageIndex, TXID } from './types.js';
10
+ /** Encodes a u64 as a little-endian LEB128 varint (same as Go's uvarint). */
11
+ export declare function encodeUvarint(v: bigint): Uint8Array;
12
+ /** Decodes a u64 varint from r, advancing its position. */
13
+ export declare function readUvarint(r: ByteReader): bigint;
14
+ /**
15
+ * Parses the page index section from r. observe, if non-null, receives every
16
+ * byte consumed (for checksumming); fn receives each record in file order.
17
+ */
18
+ export declare function parsePageIndex(r: ByteReader, observe: ((buf: Uint8Array) => void) | null, fn: (pgno: number, offset: number, size: number) => void): void;
19
+ /**
20
+ * DecodePageIndex decodes the page index from r, starting at the current
21
+ * position, into a Map keyed by page number.
22
+ */
23
+ export declare function decodePageIndex(r: ByteReader, level: number, minTXID: TXID, maxTXID: TXID): PageIndex;
24
+ /** Encodes a single page index record `(pgno, offset, size)` as a varint buffer. */
25
+ export declare function encodePageIndexRecord(pgno: number, offset: number, size: number): Uint8Array;
26
+ //# sourceMappingURL=page-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-index.d.ts","sourceRoot":"","sources":["../../src/ltx/page-index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAKlD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAanD;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAejD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,UAAU,EACb,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,IAAI,EAC3C,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GACvD,IAAI,CAqCN;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,CAAC,EAAE,UAAU,EACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,IAAI,GACZ,SAAS,CAgBX;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAe5F"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * LTX page index encoding/decoding.
3
+ *
4
+ * The page index is a sequence of varint triples `(pgno, offset, size)` in
5
+ * ascending page order, terminated by a zero varint, followed by an 8-byte
6
+ * big-endian size of the varint bytes (excluding the size field itself).
7
+ */
8
+ import { readU64BE } from "./bytes.js";
9
+ import { LTXFormatError } from "../errors.js";
10
+ const MAX_INT64 = 0x7fffffffffffffffn;
11
+ /** Encodes a u64 as a little-endian LEB128 varint (same as Go's uvarint). */
12
+ export function encodeUvarint(v) {
13
+ const out = [];
14
+ for (;;) {
15
+ const b = Number(v & 0x7fn);
16
+ v >>= 7n;
17
+ if (v !== 0n) {
18
+ out.push(b | 0x80);
19
+ }
20
+ else {
21
+ out.push(b);
22
+ break;
23
+ }
24
+ }
25
+ return Uint8Array.from(out);
26
+ }
27
+ /** Decodes a u64 varint from r, advancing its position. */
28
+ export function readUvarint(r) {
29
+ let x = 0n;
30
+ let s = 0n;
31
+ for (let i = 0; i < 10; i++) {
32
+ const b = r.readByte();
33
+ if (b < 0x80) {
34
+ if (i === 9 && b > 1) {
35
+ throw new LTXFormatError('uvarint overflows 64 bits');
36
+ }
37
+ return x | (BigInt(b) << s);
38
+ }
39
+ x |= BigInt(b & 0x7f) << s;
40
+ s += 7n;
41
+ }
42
+ throw new LTXFormatError('uvarint overflows 64 bits');
43
+ }
44
+ /**
45
+ * Parses the page index section from r. observe, if non-null, receives every
46
+ * byte consumed (for checksumming); fn receives each record in file order.
47
+ */
48
+ export function parsePageIndex(r, observe, fn) {
49
+ let consumed = 0;
50
+ let prevPgno = 0;
51
+ for (;;) {
52
+ const start = r.pos;
53
+ const pgno = readUvarint(r);
54
+ if (pgno === 0n) {
55
+ // End marker.
56
+ observe?.(r.slice(start, r.pos));
57
+ consumed += r.pos - start;
58
+ break;
59
+ }
60
+ if (pgno > 0xffffffffn) {
61
+ throw new LTXFormatError(`page index pgno ${pgno} out of range`);
62
+ }
63
+ if (pgno <= BigInt(prevPgno)) {
64
+ throw new LTXFormatError(`page index out of order: ${pgno} after ${prevPgno}`);
65
+ }
66
+ const offset = readUvarint(r);
67
+ const size = readUvarint(r);
68
+ if (offset > MAX_INT64 || size > MAX_INT64) {
69
+ throw new LTXFormatError(`page index pgno ${pgno} offset/size out of range`);
70
+ }
71
+ observe?.(r.slice(start, r.pos));
72
+ consumed += r.pos - start;
73
+ prevPgno = Number(pgno);
74
+ fn(Number(pgno), Number(offset), Number(size));
75
+ }
76
+ // Read trailing size field.
77
+ const sizeBuf = r.readFull(8);
78
+ observe?.(sizeBuf);
79
+ const indexSize = readU64BE(sizeBuf, 0);
80
+ if (indexSize !== BigInt(consumed)) {
81
+ throw new LTXFormatError(`page index size mismatch: field=${indexSize} read=${consumed}`);
82
+ }
83
+ }
84
+ /**
85
+ * DecodePageIndex decodes the page index from r, starting at the current
86
+ * position, into a Map keyed by page number.
87
+ */
88
+ export function decodePageIndex(r, level, minTXID, maxTXID) {
89
+ const index = new Map();
90
+ parsePageIndex(r, null, (pgno, offset, size) => {
91
+ index.set(pgno, {
92
+ level,
93
+ minTXID,
94
+ maxTXID,
95
+ offset,
96
+ size,
97
+ });
98
+ });
99
+ return index;
100
+ }
101
+ /** Encodes a single page index record `(pgno, offset, size)` as a varint buffer. */
102
+ export function encodePageIndexRecord(pgno, offset, size) {
103
+ const parts = [
104
+ encodeUvarint(BigInt(pgno)),
105
+ encodeUvarint(BigInt(offset)),
106
+ encodeUvarint(BigInt(size)),
107
+ ];
108
+ let total = 0;
109
+ for (const p of parts)
110
+ total += p.length;
111
+ const out = new Uint8Array(total);
112
+ let o = 0;
113
+ for (const p of parts) {
114
+ out.set(p, o);
115
+ o += p.length;
116
+ }
117
+ return out;
118
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * LTX page header marshalling.
3
+ */
4
+ import type { PageHeader } from './types.js';
5
+ /** MarshalBinary encodes a PageHeader to its 6-byte representation. */
6
+ export declare function marshalPageHeader(h: PageHeader): Uint8Array;
7
+ /** UnmarshalBinary decodes a PageHeader from a byte slice. */
8
+ export declare function unmarshalPageHeader(b: Uint8Array): PageHeader;
9
+ /** Validate returns an error (thrown) if h is invalid. */
10
+ export declare function validatePageHeader(h: PageHeader): void;
11
+ /** The decoded result of a single LTX page frame. */
12
+ export interface DecodedPage {
13
+ header: PageHeader;
14
+ data: Uint8Array;
15
+ }
16
+ /**
17
+ * DecodePageData decodes a single page frame from `b` (which must start at a
18
+ * page header). Matches Go's `ltx.DecodePageData`. For an empty (zero) page
19
+ * header, `data` is an empty buffer.
20
+ *
21
+ * Note: the uncompressed byte length is unknown here; LTX pages are at most
22
+ * `MaxPageSize`, so we decompress into a 64KiB buffer and slice to the bytes
23
+ * written. Callers that know the page size should use `DecodePage` in the
24
+ * decoder for the size-accurate path.
25
+ */
26
+ export declare function decodePageData(b: Uint8Array): DecodedPage;
27
+ //# sourceMappingURL=page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/ltx/page.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAK3D;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAM7D;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,CAOtD;AAED,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,UAAU,GAAG,WAAW,CA0BzD"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * LTX page header marshalling.
3
+ */
4
+ import { PageHeaderSize, PageHeaderFlagSize, MaxPageSize } from "./constants.js";
5
+ import { assertFull, readU16BE, readU32BE, writeU16BE, writeU32BE } from "./bytes.js";
6
+ import { decompressBlockInto } from "./lz4.js";
7
+ import { LTXFormatError } from "../errors.js";
8
+ /** MarshalBinary encodes a PageHeader to its 6-byte representation. */
9
+ export function marshalPageHeader(h) {
10
+ const b = new Uint8Array(PageHeaderSize);
11
+ writeU32BE(b, 0, h.pgno);
12
+ writeU16BE(b, 4, h.flags);
13
+ return b;
14
+ }
15
+ /** UnmarshalBinary decodes a PageHeader from a byte slice. */
16
+ export function unmarshalPageHeader(b) {
17
+ assertFull(b, 0, PageHeaderSize);
18
+ return {
19
+ pgno: readU32BE(b, 0),
20
+ flags: readU16BE(b, 4),
21
+ };
22
+ }
23
+ /** Validate returns an error (thrown) if h is invalid. */
24
+ export function validatePageHeader(h) {
25
+ if (h.pgno === 0) {
26
+ throw new LTXFormatError('page number required');
27
+ }
28
+ if ((h.flags & ~PageHeaderFlagSize) !== 0) {
29
+ throw new LTXFormatError(`invalid page header flags: 0x${h.flags.toString(16)}`);
30
+ }
31
+ }
32
+ /**
33
+ * DecodePageData decodes a single page frame from `b` (which must start at a
34
+ * page header). Matches Go's `ltx.DecodePageData`. For an empty (zero) page
35
+ * header, `data` is an empty buffer.
36
+ *
37
+ * Note: the uncompressed byte length is unknown here; LTX pages are at most
38
+ * `MaxPageSize`, so we decompress into a 64KiB buffer and slice to the bytes
39
+ * written. Callers that know the page size should use `DecodePage` in the
40
+ * decoder for the size-accurate path.
41
+ */
42
+ export function decodePageData(b) {
43
+ const header = unmarshalPageHeader(b);
44
+ if (header.pgno === 0 && header.flags === 0) {
45
+ return { header, data: new Uint8Array(0) };
46
+ }
47
+ validatePageHeader(header);
48
+ if ((header.flags & PageHeaderFlagSize) !== 0) {
49
+ if (b.length < PageHeaderSize + 4) {
50
+ throw new LTXFormatError('buffer too small for size prefix');
51
+ }
52
+ const dataSize = readU32BE(b, PageHeaderSize);
53
+ const offset = PageHeaderSize + 4;
54
+ if (b.length < offset + dataSize) {
55
+ throw new LTXFormatError(`buffer too small for data: need ${offset + dataSize}, have ${b.length}`);
56
+ }
57
+ const compressed = b.subarray(offset, offset + dataSize);
58
+ const buf = new Uint8Array(MaxPageSize);
59
+ const n = decompressBlockInto(compressed, buf);
60
+ if (n <= 0) {
61
+ throw new LTXFormatError('decompress block failed');
62
+ }
63
+ return { header, data: buf.subarray(0, n) };
64
+ }
65
+ throw new LTXFormatError('legacy LZ4 frame format not supported');
66
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Output sink abstraction for the LTX encoder and other writers.
3
+ * A sink must accept ordered byte chunks; the encoder counts total bytes via
4
+ * its own `n` counter so sinks do not need to be seekable.
5
+ */
6
+ export interface ByteSink {
7
+ write(bytes: Uint8Array): void;
8
+ }
9
+ /** In-memory sink that accumulates bytes into a single growable buffer. */
10
+ export declare class BufferSink implements ByteSink {
11
+ private chunks;
12
+ private _length;
13
+ write(bytes: Uint8Array): void;
14
+ get length(): number;
15
+ toBuffer(): Uint8Array;
16
+ }
17
+ //# sourceMappingURL=sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../../src/ltx/sink.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAED,2EAA2E;AAC3E,qBAAa,UAAW,YAAW,QAAQ;IACzC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,OAAO,CAAK;IAEpB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAK9B,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,QAAQ,IAAI,UAAU;CASvB"}
@@ -0,0 +1,21 @@
1
+ /** In-memory sink that accumulates bytes into a single growable buffer. */
2
+ export class BufferSink {
3
+ chunks = [];
4
+ _length = 0;
5
+ write(bytes) {
6
+ this.chunks.push(Uint8Array.from(bytes));
7
+ this._length += bytes.length;
8
+ }
9
+ get length() {
10
+ return this._length;
11
+ }
12
+ toBuffer() {
13
+ const out = new Uint8Array(this._length);
14
+ let o = 0;
15
+ for (const c of this.chunks) {
16
+ out.set(c, o);
17
+ o += c.length;
18
+ }
19
+ return out;
20
+ }
21
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Streaming LTX reader.
3
+ *
4
+ * The in-memory {@link Decoder} is convenient but requires the whole LTX file in
5
+ * a single buffer. For large databases we read files from a streaming source (a
6
+ * `Readable`) and decode page-by-page, so peak memory stays bounded by one page
7
+ * buffer plus the small page index, never by the full database size.
8
+ *
9
+ * {@link StreamLtxReader} mirrors the synchronous `Decoder` in shape: read the
10
+ * header, then read each page into a reusable buffer, then `verify()` the page
11
+ * index + trailer (including the rolling checksum) once the end-of-pages marker
12
+ * is reached. It is used by streaming restore and compact.
13
+ */
14
+ import type { Header, PageHeader, Trailer } from './types.js';
15
+ /**
16
+ * Pulls bytes from an async iterable of chunks into an exact-bytes reader.
17
+ * Handles chunk boundaries that split logical records (page headers, LZ4
18
+ * blocks, varints) without buffering more than one chunk at a time.
19
+ */
20
+ export declare class AsyncByteReader {
21
+ private it;
22
+ private buf;
23
+ private bufPos;
24
+ pos: number;
25
+ constructor(source: AsyncIterable<Uint8Array>);
26
+ /** Fills the internal buffer from the source. Returns false on EOF. */
27
+ private fill;
28
+ /** Reads exactly `n` bytes, advancing the position. Throws on short input. */
29
+ readFull(n: number): Promise<Uint8Array>;
30
+ /** Reads and returns every remaining byte in the source. */
31
+ readAllRemaining(): Promise<Uint8Array>;
32
+ }
33
+ /**
34
+ * Streams an LTX file from an async iterable of byte chunks, decoding the
35
+ * header and each page without holding the whole file in memory.
36
+ */
37
+ export declare class StreamLtxReader {
38
+ private r;
39
+ private header;
40
+ private trailer;
41
+ private state;
42
+ private hash;
43
+ private pageSeq;
44
+ private chksum;
45
+ private pageN;
46
+ constructor(source: AsyncIterable<Uint8Array>);
47
+ /** Header returns the decoded header (after `readHeader`). */
48
+ Header(): Header;
49
+ /** Trailer returns the trailer (valid after `verify`). */
50
+ Trailer(): Trailer;
51
+ /** PageN returns the number of pages decoded. */
52
+ PageN(): number;
53
+ /** Reads and validates the LTX header frame. */
54
+ readHeader(): Promise<void>;
55
+ /**
56
+ * Reads the next page frame into `data`. Returns the page header, or null
57
+ * when the end-of-pages marker is reached.
58
+ */
59
+ readPage(data: Uint8Array): Promise<PageHeader | null>;
60
+ /** Reads the page index + trailer and verifies the file checksum. */
61
+ verify(): Promise<void>;
62
+ private hashPgo;
63
+ }
64
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/ltx/stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAa9D;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA4B;IACtC,OAAO,CAAC,GAAG,CAAiC;IAC5C,OAAO,CAAC,MAAM,CAAK;IACnB,GAAG,SAAK;gBAEI,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC;IAI7C,uEAAuE;YACzD,IAAI;IAUlB,8EAA8E;IACxE,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAgB9C,4DAA4D;IACtD,gBAAgB,IAAI,OAAO,CAAC,UAAU,CAAC;CAyB9C;AAED;;;GAGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,CAAC,CAAkB;IAC3B,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,OAAO,CAAwD;IACvE,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAAK;gBAEN,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC;IAI7C,8DAA8D;IAC9D,MAAM,IAAI,MAAM;IAIhB,0DAA0D;IAC1D,OAAO,IAAI,OAAO;IAIlB,iDAAiD;IACjD,KAAK,IAAI,MAAM;IAIf,gDAAgD;IAC1C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAajC;;;OAGG;IACG,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAmD5D,qEAAqE;IAC/D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IA+D7B,OAAO,CAAC,OAAO;CAGhB"}