@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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +202 -0
- package/NOTICE +22 -0
- package/README.md +482 -2
- package/dist/backends/file/index.d.ts +4 -0
- package/dist/backends/file/index.d.ts.map +1 -0
- package/dist/backends/file/index.js +3 -0
- package/dist/backends/file/leaser.d.ts +20 -0
- package/dist/backends/file/leaser.d.ts.map +1 -0
- package/dist/backends/file/leaser.js +123 -0
- package/dist/backends/file/replica-client.d.ts +38 -0
- package/dist/backends/file/replica-client.d.ts.map +1 -0
- package/dist/backends/file/replica-client.js +209 -0
- package/dist/db.d.ts +80 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +484 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/leaser.d.ts +55 -0
- package/dist/leaser.d.ts.map +1 -0
- package/dist/leaser.js +60 -0
- package/dist/ltx/byte-reader.d.ts +18 -0
- package/dist/ltx/byte-reader.d.ts.map +1 -0
- package/dist/ltx/byte-reader.js +38 -0
- package/dist/ltx/bytes.d.ts +20 -0
- package/dist/ltx/bytes.d.ts.map +1 -0
- package/dist/ltx/bytes.js +69 -0
- package/dist/ltx/checksum.d.ts +20 -0
- package/dist/ltx/checksum.d.ts.map +1 -0
- package/dist/ltx/checksum.js +42 -0
- package/dist/ltx/compaction-level.d.ts +58 -0
- package/dist/ltx/compaction-level.d.ts.map +1 -0
- package/dist/ltx/compaction-level.js +115 -0
- package/dist/ltx/compactor.d.ts +33 -0
- package/dist/ltx/compactor.d.ts.map +1 -0
- package/dist/ltx/compactor.js +137 -0
- package/dist/ltx/constants.d.ts +49 -0
- package/dist/ltx/constants.d.ts.map +1 -0
- package/dist/ltx/constants.js +58 -0
- package/dist/ltx/crc64.d.ts +29 -0
- package/dist/ltx/crc64.d.ts.map +1 -0
- package/dist/ltx/crc64.js +57 -0
- package/dist/ltx/decoder.d.ts +55 -0
- package/dist/ltx/decoder.d.ts.map +1 -0
- package/dist/ltx/decoder.js +236 -0
- package/dist/ltx/encoder.d.ts +49 -0
- package/dist/ltx/encoder.d.ts.map +1 -0
- package/dist/ltx/encoder.js +159 -0
- package/dist/ltx/file-info.d.ts +24 -0
- package/dist/ltx/file-info.d.ts.map +1 -0
- package/dist/ltx/file-info.js +74 -0
- package/dist/ltx/filename.d.ts +13 -0
- package/dist/ltx/filename.d.ts.map +1 -0
- package/dist/ltx/filename.js +23 -0
- package/dist/ltx/header.d.ts +17 -0
- package/dist/ltx/header.d.ts.map +1 -0
- package/dist/ltx/header.js +115 -0
- package/dist/ltx/index.d.ts +24 -0
- package/dist/ltx/index.d.ts.map +1 -0
- package/dist/ltx/index.js +25 -0
- package/dist/ltx/lz4.d.ts +41 -0
- package/dist/ltx/lz4.d.ts.map +1 -0
- package/dist/ltx/lz4.js +264 -0
- package/dist/ltx/page-index.d.ts +26 -0
- package/dist/ltx/page-index.d.ts.map +1 -0
- package/dist/ltx/page-index.js +118 -0
- package/dist/ltx/page.d.ts +27 -0
- package/dist/ltx/page.d.ts.map +1 -0
- package/dist/ltx/page.js +66 -0
- package/dist/ltx/sink.d.ts +17 -0
- package/dist/ltx/sink.d.ts.map +1 -0
- package/dist/ltx/sink.js +21 -0
- package/dist/ltx/stream.d.ts +64 -0
- package/dist/ltx/stream.d.ts.map +1 -0
- package/dist/ltx/stream.js +248 -0
- package/dist/ltx/streaming-compactor.d.ts +34 -0
- package/dist/ltx/streaming-compactor.d.ts.map +1 -0
- package/dist/ltx/streaming-compactor.js +140 -0
- package/dist/ltx/trailer.d.ts +12 -0
- package/dist/ltx/trailer.d.ts.map +1 -0
- package/dist/ltx/trailer.js +47 -0
- package/dist/ltx/types.d.ts +79 -0
- package/dist/ltx/types.d.ts.map +1 -0
- package/dist/ltx/types.js +70 -0
- package/dist/replica-client.d.ts +92 -0
- package/dist/replica-client.d.ts.map +1 -0
- package/dist/replica-client.js +122 -0
- package/dist/replica-url.d.ts +20 -0
- package/dist/replica-url.d.ts.map +1 -0
- package/dist/replica-url.js +49 -0
- package/dist/replica.d.ts +379 -0
- package/dist/replica.d.ts.map +1 -0
- package/dist/replica.js +1253 -0
- package/dist/store.d.ts +34 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +60 -0
- package/dist/util/fs.d.ts +28 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +73 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +7 -0
- package/dist/util/stream.d.ts +11 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +25 -0
- package/dist/wal/checksum.d.ts +17 -0
- package/dist/wal/checksum.d.ts.map +1 -0
- package/dist/wal/checksum.js +30 -0
- package/dist/wal/constants.d.ts +8 -0
- package/dist/wal/constants.d.ts.map +1 -0
- package/dist/wal/constants.js +7 -0
- package/dist/wal/index.d.ts +5 -0
- package/dist/wal/index.d.ts.map +1 -0
- package/dist/wal/index.js +4 -0
- package/dist/wal/reader.d.ts +61 -0
- package/dist/wal/reader.d.ts.map +1 -0
- package/dist/wal/reader.js +207 -0
- package/package.json +68 -3
- package/src/backends/file/index.ts +3 -0
- package/src/backends/file/leaser.ts +145 -0
- package/src/backends/file/replica-client.ts +246 -0
- package/src/db.ts +553 -0
- package/src/errors.ts +74 -0
- package/src/index.ts +26 -0
- package/src/leaser.ts +86 -0
- package/src/ltx/byte-reader.ts +46 -0
- package/src/ltx/bytes.ts +87 -0
- package/src/ltx/checksum.ts +46 -0
- package/src/ltx/compaction-level.ts +135 -0
- package/src/ltx/compactor.ts +167 -0
- package/src/ltx/constants.ts +71 -0
- package/src/ltx/crc64.ts +65 -0
- package/src/ltx/decoder.ts +282 -0
- package/src/ltx/encoder.ts +200 -0
- package/src/ltx/file-info.ts +81 -0
- package/src/ltx/filename.ts +27 -0
- package/src/ltx/header.ts +126 -0
- package/src/ltx/index.ts +27 -0
- package/src/ltx/lz4.ts +279 -0
- package/src/ltx/page-index.ts +140 -0
- package/src/ltx/page.ts +80 -0
- package/src/ltx/sink.ts +33 -0
- package/src/ltx/stream.ts +286 -0
- package/src/ltx/streaming-compactor.ts +169 -0
- package/src/ltx/trailer.ts +58 -0
- package/src/ltx/types.ts +152 -0
- package/src/lz4js.d.ts +27 -0
- package/src/replica-client.ts +222 -0
- package/src/replica-url.ts +72 -0
- package/src/replica.ts +1480 -0
- package/src/store.ts +71 -0
- package/src/util/fs.ts +82 -0
- package/src/util/logger.ts +15 -0
- package/src/util/stream.ts +28 -0
- package/src/wal/checksum.ts +39 -0
- package/src/wal/constants.ts +10 -0
- package/src/wal/index.ts +5 -0
- package/src/wal/reader.ts +264 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LTX header frame marshalling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HeaderSize, Magic, Version } from './constants.ts';
|
|
6
|
+
import {
|
|
7
|
+
ChecksumFlag,
|
|
8
|
+
isValidHeaderFlags,
|
|
9
|
+
isValidPageSize,
|
|
10
|
+
} from './constants.ts';
|
|
11
|
+
import { assertFull, readU32BE, readU64BE, writeU32BE, writeU64BE } from './bytes.ts';
|
|
12
|
+
import type { Header } from './types.ts';
|
|
13
|
+
import { isSnapshot } from './types.ts';
|
|
14
|
+
import { lockPgno } from './constants.ts';
|
|
15
|
+
import { LTXFormatError } from '../errors.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MarshalBinary encodes a header to its 100-byte representation.
|
|
19
|
+
*/
|
|
20
|
+
export function marshalHeader(h: Header): Uint8Array {
|
|
21
|
+
const b = new Uint8Array(HeaderSize);
|
|
22
|
+
for (let i = 0; i < 4; i++) b[i] = Magic.charCodeAt(i);
|
|
23
|
+
writeU32BE(b, 4, h.flags);
|
|
24
|
+
writeU32BE(b, 8, h.pageSize);
|
|
25
|
+
writeU32BE(b, 12, h.commit);
|
|
26
|
+
writeU64BE(b, 16, h.minTXID);
|
|
27
|
+
writeU64BE(b, 24, h.maxTXID);
|
|
28
|
+
writeU64BE(b, 32, h.timestamp);
|
|
29
|
+
writeU64BE(b, 40, h.preApplyChecksum);
|
|
30
|
+
writeU64BE(b, 48, h.walOffset);
|
|
31
|
+
writeU64BE(b, 56, h.walSize);
|
|
32
|
+
writeU32BE(b, 64, h.walSalt1);
|
|
33
|
+
writeU32BE(b, 68, h.walSalt2);
|
|
34
|
+
writeU64BE(b, 72, h.nodeID);
|
|
35
|
+
return b;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* UnmarshalBinary decodes a header from a byte slice.
|
|
40
|
+
* Sets `version` from the magic and returns the header.
|
|
41
|
+
*/
|
|
42
|
+
export function unmarshalHeader(b: Uint8Array): Header {
|
|
43
|
+
assertFull(b, 0, HeaderSize);
|
|
44
|
+
const magic = String.fromCharCode(b[0], b[1], b[2], b[3]);
|
|
45
|
+
if (magic !== Magic) {
|
|
46
|
+
throw new LTXFormatError('invalid LTX file');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: Version,
|
|
50
|
+
flags: readU32BE(b, 4),
|
|
51
|
+
pageSize: readU32BE(b, 8),
|
|
52
|
+
commit: readU32BE(b, 12),
|
|
53
|
+
minTXID: readU64BE(b, 16),
|
|
54
|
+
maxTXID: readU64BE(b, 24),
|
|
55
|
+
timestamp: readU64BE(b, 32),
|
|
56
|
+
preApplyChecksum: readU64BE(b, 40),
|
|
57
|
+
walOffset: readU64BE(b, 48),
|
|
58
|
+
walSize: readU64BE(b, 56),
|
|
59
|
+
walSalt1: readU32BE(b, 64),
|
|
60
|
+
walSalt2: readU32BE(b, 68),
|
|
61
|
+
nodeID: readU64BE(b, 72),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function noChecksum(flags: number): boolean {
|
|
66
|
+
return (flags & 0x00000002) !== 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Validate returns an error (thrown) if h is invalid. */
|
|
70
|
+
export function validateHeader(h: Header): void {
|
|
71
|
+
if (h.version !== Version) {
|
|
72
|
+
throw new LTXFormatError('invalid version');
|
|
73
|
+
}
|
|
74
|
+
if (!isValidHeaderFlags(h.flags)) {
|
|
75
|
+
throw new LTXFormatError(`invalid flags: 0x${h.flags.toString(16)}`);
|
|
76
|
+
}
|
|
77
|
+
if (!isValidPageSize(h.pageSize)) {
|
|
78
|
+
throw new LTXFormatError(`invalid page size: ${h.pageSize}`);
|
|
79
|
+
}
|
|
80
|
+
if (h.minTXID === 0n) {
|
|
81
|
+
throw new LTXFormatError('minimum transaction id required');
|
|
82
|
+
}
|
|
83
|
+
if (h.maxTXID === 0n) {
|
|
84
|
+
throw new LTXFormatError('maximum transaction id required');
|
|
85
|
+
}
|
|
86
|
+
if (h.minTXID > h.maxTXID) {
|
|
87
|
+
throw new LTXFormatError(`transaction ids out of order: (${h.minTXID},${h.maxTXID})`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (h.walOffset < 0n) {
|
|
91
|
+
throw new LTXFormatError(`wal offset cannot be negative: ${h.walOffset}`);
|
|
92
|
+
}
|
|
93
|
+
if (h.walSize < 0n) {
|
|
94
|
+
throw new LTXFormatError(`wal size cannot be negative: ${h.walSize}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ((h.walSalt1 !== 0 || h.walSalt2 !== 0) && h.walOffset === 0n) {
|
|
98
|
+
throw new LTXFormatError('wal offset required if salt exists');
|
|
99
|
+
}
|
|
100
|
+
if (h.walOffset === 0n && h.walSize !== 0n) {
|
|
101
|
+
throw new LTXFormatError('wal offset required if wal size exists');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const snap = isSnapshot(h);
|
|
105
|
+
const nc = noChecksum(h.flags);
|
|
106
|
+
if (snap) {
|
|
107
|
+
if (h.preApplyChecksum !== 0n) {
|
|
108
|
+
throw new LTXFormatError('pre-apply checksum must be zero on snapshots');
|
|
109
|
+
}
|
|
110
|
+
} else if (nc) {
|
|
111
|
+
if (h.preApplyChecksum !== 0n) {
|
|
112
|
+
throw new LTXFormatError('pre-apply checksum not allowed');
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
if (h.preApplyChecksum === 0n) {
|
|
116
|
+
throw new LTXFormatError('pre-apply checksum required on non-snapshot files');
|
|
117
|
+
}
|
|
118
|
+
if ((h.preApplyChecksum & ChecksumFlag) === 0n) {
|
|
119
|
+
throw new LTXFormatError('invalid pre-apply checksum format');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function headerLockPgno(h: Header): number {
|
|
125
|
+
return lockPgno(h.pageSize);
|
|
126
|
+
}
|
package/src/ltx/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LTX format module — public surface for reading/writing LTX files.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './constants.ts';
|
|
6
|
+
export * from './types.ts';
|
|
7
|
+
export * from './crc64.ts';
|
|
8
|
+
export * from './checksum.ts';
|
|
9
|
+
export * from './bytes.ts';
|
|
10
|
+
export * from './byte-reader.ts';
|
|
11
|
+
export * from './filename.ts';
|
|
12
|
+
export * from './page-index.ts';
|
|
13
|
+
export * from './header.ts';
|
|
14
|
+
export * from './page.ts';
|
|
15
|
+
export * from './trailer.ts';
|
|
16
|
+
export * from './sink.ts';
|
|
17
|
+
export * from './encoder.ts';
|
|
18
|
+
export * from './decoder.ts';
|
|
19
|
+
export * from './stream.ts';
|
|
20
|
+
export * from './compactor.ts';
|
|
21
|
+
export * from './streaming-compactor.ts';
|
|
22
|
+
export * from './file-info.ts';
|
|
23
|
+
export * from './compaction-level.ts';
|
|
24
|
+
|
|
25
|
+
// LZ4 block codec is internal (not part of the public LTX surface) but
|
|
26
|
+
// exposed for the WAL/DB layers.
|
|
27
|
+
export { compressBlock, decompressBlock } from './lz4.ts';
|
package/src/ltx/lz4.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
|
|
19
|
+
import lz4 from 'lz4js';
|
|
20
|
+
import { LTXFormatError } from '../errors.ts';
|
|
21
|
+
|
|
22
|
+
// Compressor constants, mirrored from `internal/lz4block/block.go`.
|
|
23
|
+
const HASH_LOG = 16;
|
|
24
|
+
const HT_SIZE = 1 << HASH_LOG; // 65536 hash-table entries.
|
|
25
|
+
const WIN_SIZE = 1 << 16; // 64 KiB window.
|
|
26
|
+
const WIN_MASK = WIN_SIZE - 1;
|
|
27
|
+
const MIN_MATCH = 4;
|
|
28
|
+
const MF_LIMIT = 10 + MIN_MATCH; // Last match cannot start within the last 14 bytes.
|
|
29
|
+
const ADAPT_SKIP_LOG = 7;
|
|
30
|
+
const PRIME6 = 227718039650203n;
|
|
31
|
+
const U64_MASK = 0xffffffffffffffffn;
|
|
32
|
+
const U32_MASK = 0xffffffffn;
|
|
33
|
+
|
|
34
|
+
// Hash table state. `inUse` is a bitmap so we can clear the table cheaply.
|
|
35
|
+
const hashTable = new Uint16Array(HT_SIZE);
|
|
36
|
+
const inUse = new Uint32Array(HT_SIZE / 32);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* compressBlockBound returns the maximum compressed size for `n` input bytes
|
|
40
|
+
* using the Fast compressor (`n + n/255 + 16`).
|
|
41
|
+
*/
|
|
42
|
+
export function compressBlockBound(n: number): number {
|
|
43
|
+
return n + ((n / 255) | 0) + 16;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Hashes the lower 6 bytes of a 64-bit little-endian value into a hash index. */
|
|
47
|
+
function blockHash(x: bigint): number {
|
|
48
|
+
return Number((((x << 16n) * PRIME6) & U64_MASK) >> 48n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Reads the 64-bit little-endian value at `o` (used by the Fast scan). */
|
|
52
|
+
function readU64(b: Uint8Array, o: number): bigint {
|
|
53
|
+
let x = 0n;
|
|
54
|
+
for (let i = 7; i >= 0; i--) x = (x << 8n) | BigInt(b[o + i]);
|
|
55
|
+
return x;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reads the 32-bit little-endian value at `o`. */
|
|
59
|
+
function readU32(b: Uint8Array, o: number): number {
|
|
60
|
+
return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Returns the number of leading zero bytes in a 64-bit little-endian value. */
|
|
64
|
+
function trailingZeroBytes(x: bigint): number {
|
|
65
|
+
let n = 0;
|
|
66
|
+
while ((x & 0xffn) === 0n && n < 8) {
|
|
67
|
+
x >>= 8n;
|
|
68
|
+
n++;
|
|
69
|
+
}
|
|
70
|
+
return n;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Look up the two 16-bit halves of a stored match offset for a hash. Mirrors
|
|
75
|
+
* `Compressor.get`: the table stores 16-bit offsets relative to a 64 KiB block
|
|
76
|
+
* boundary. Returns a position that may be before the start of the window (in
|
|
77
|
+
* which case `offset >= WIN_SIZE` short-circuits the candidate check).
|
|
78
|
+
*/
|
|
79
|
+
function tableGet(h: number, si: number): number {
|
|
80
|
+
h &= HT_SIZE - 1;
|
|
81
|
+
let i = 0;
|
|
82
|
+
if (inUse[h >>> 5] & (1 << (h & 31))) i = hashTable[h];
|
|
83
|
+
i += si & ~WIN_MASK;
|
|
84
|
+
if (i >= si) i -= WIN_SIZE;
|
|
85
|
+
return i;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tablePut(h: number, si: number): void {
|
|
89
|
+
h &= HT_SIZE - 1;
|
|
90
|
+
hashTable[h] = si & 0xffff;
|
|
91
|
+
inUse[h >>> 5] |= 1 << (h & 31);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compresses `src` into `dst` as a Fast-mode LZ4 block, writing at offset 0.
|
|
96
|
+
* Mirrors `lz4block.Compressor.CompressBlock`. Returns the number of bytes
|
|
97
|
+
* written, or 0 if `dst` is too small (which happens only when the caller does
|
|
98
|
+
* not size `dst` with `compressBlockBound`).
|
|
99
|
+
*/
|
|
100
|
+
function compressBlockInto(src: Uint8Array, dst: Uint8Array): number {
|
|
101
|
+
// Clear the reused table so output is deterministic (issue #65).
|
|
102
|
+
inUse.fill(0);
|
|
103
|
+
|
|
104
|
+
// In normal use `dst` is sized by `compressBlockBound`, so the compressor
|
|
105
|
+
// always emits a valid block (even for incompressible input). An undersized
|
|
106
|
+
// `dst` is caught by the `di >= dst.length` guards below and returns 0.
|
|
107
|
+
|
|
108
|
+
let si = 0;
|
|
109
|
+
let di = 0;
|
|
110
|
+
let anchor = 0;
|
|
111
|
+
const sn = src.length - MF_LIMIT;
|
|
112
|
+
|
|
113
|
+
if (sn > 0) {
|
|
114
|
+
for (; si < sn;) {
|
|
115
|
+
const match = readU64(src, si);
|
|
116
|
+
let h = blockHash(match);
|
|
117
|
+
const h2 = blockHash(match >> 8n);
|
|
118
|
+
const ref = tableGet(h, si);
|
|
119
|
+
const ref2 = tableGet(h2, si + 1);
|
|
120
|
+
tablePut(h, si);
|
|
121
|
+
tablePut(h2, si + 1);
|
|
122
|
+
|
|
123
|
+
let offset = si - ref;
|
|
124
|
+
|
|
125
|
+
if (offset <= 0 || offset >= WIN_SIZE || (match & U32_MASK) !== BigInt(readU32(src, ref))) {
|
|
126
|
+
h = blockHash(match >> 16n);
|
|
127
|
+
const ref3 = tableGet(h, si + 2);
|
|
128
|
+
si += 1;
|
|
129
|
+
offset = si - ref2;
|
|
130
|
+
if (offset <= 0 || offset >= WIN_SIZE || ((match >> 8n) & U32_MASK) !== BigInt(readU32(src, ref2))) {
|
|
131
|
+
si += 1;
|
|
132
|
+
offset = si - ref3;
|
|
133
|
+
tablePut(h, si);
|
|
134
|
+
if (offset <= 0 || offset >= WIN_SIZE || ((match >> 16n) & U32_MASK) !== BigInt(readU32(src, ref3))) {
|
|
135
|
+
si += 2 + ((si - anchor) >>> ADAPT_SKIP_LOG);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Match found.
|
|
142
|
+
let lLen = si - anchor;
|
|
143
|
+
let mLen = 4;
|
|
144
|
+
|
|
145
|
+
// Extend backwards, reducing literals.
|
|
146
|
+
let tOff = si - offset - 1;
|
|
147
|
+
while (lLen > 0 && tOff >= 0 && src[si - 1] === src[tOff]) {
|
|
148
|
+
si--;
|
|
149
|
+
tOff--;
|
|
150
|
+
lLen--;
|
|
151
|
+
mLen++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Continue search at the end of the match. `matchBase` is the position
|
|
155
|
+
// just past the first 4 matched bytes.
|
|
156
|
+
const matchBase = si + MIN_MATCH;
|
|
157
|
+
si = si + mLen;
|
|
158
|
+
|
|
159
|
+
// Find the longest match by comparing 8 bytes at a time.
|
|
160
|
+
while (si + 8 <= sn) {
|
|
161
|
+
const x = readU64(src, si) ^ readU64(src, si - offset);
|
|
162
|
+
if (x === 0n) {
|
|
163
|
+
si += 8;
|
|
164
|
+
} else {
|
|
165
|
+
si += trailingZeroBytes(x);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
mLen = si - matchBase;
|
|
171
|
+
|
|
172
|
+
if (di >= dst.length) return 0;
|
|
173
|
+
if (mLen < 0xf) dst[di] = mLen;
|
|
174
|
+
else dst[di] = 0xf;
|
|
175
|
+
|
|
176
|
+
// Encode literal length.
|
|
177
|
+
if (lLen < 0xf) {
|
|
178
|
+
dst[di] |= lLen << 4;
|
|
179
|
+
} else {
|
|
180
|
+
dst[di] |= 0xf0;
|
|
181
|
+
di++;
|
|
182
|
+
let l = lLen - 0xf;
|
|
183
|
+
while (l >= 0xff && di < dst.length) {
|
|
184
|
+
dst[di] = 0xff;
|
|
185
|
+
di++;
|
|
186
|
+
l -= 0xff;
|
|
187
|
+
}
|
|
188
|
+
if (di >= dst.length) return 0;
|
|
189
|
+
dst[di] = l;
|
|
190
|
+
}
|
|
191
|
+
di++;
|
|
192
|
+
|
|
193
|
+
if (di + lLen > dst.length) return 0;
|
|
194
|
+
dst.set(src.subarray(anchor, anchor + lLen), di);
|
|
195
|
+
di += lLen + 2;
|
|
196
|
+
anchor = si;
|
|
197
|
+
|
|
198
|
+
if (di > dst.length) return 0;
|
|
199
|
+
dst[di - 2] = offset & 0xff;
|
|
200
|
+
dst[di - 1] = (offset >> 8) & 0xff;
|
|
201
|
+
|
|
202
|
+
// Encode match length part 2.
|
|
203
|
+
if (mLen >= 0xf) {
|
|
204
|
+
let m = mLen - 0xf;
|
|
205
|
+
while (m >= 0xff && di < dst.length) {
|
|
206
|
+
dst[di] = 0xff;
|
|
207
|
+
di++;
|
|
208
|
+
m -= 0xff;
|
|
209
|
+
}
|
|
210
|
+
if (di >= dst.length) return 0;
|
|
211
|
+
dst[di] = m;
|
|
212
|
+
di++;
|
|
213
|
+
}
|
|
214
|
+
if (si >= sn) break;
|
|
215
|
+
|
|
216
|
+
// Hash match end-2.
|
|
217
|
+
h = blockHash(readU64(src, si - 2));
|
|
218
|
+
tablePut(h, si - 2);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Last literals.
|
|
223
|
+
if (di >= dst.length) return 0;
|
|
224
|
+
const lLen = src.length - anchor;
|
|
225
|
+
if (lLen < 0xf) {
|
|
226
|
+
dst[di] = lLen << 4;
|
|
227
|
+
} else {
|
|
228
|
+
dst[di] = 0xf0;
|
|
229
|
+
di++;
|
|
230
|
+
let l = lLen - 0xf;
|
|
231
|
+
while (l >= 0xff && di < dst.length) {
|
|
232
|
+
dst[di] = 0xff;
|
|
233
|
+
di++;
|
|
234
|
+
l -= 0xff;
|
|
235
|
+
}
|
|
236
|
+
if (di >= dst.length) return 0;
|
|
237
|
+
dst[di] = l;
|
|
238
|
+
}
|
|
239
|
+
di++;
|
|
240
|
+
|
|
241
|
+
if (di + (src.length - anchor) > dst.length) return 0;
|
|
242
|
+
dst.set(src.subarray(anchor), di);
|
|
243
|
+
di += src.length - anchor;
|
|
244
|
+
return di;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* compressBlock compresses `data` as a Fast-mode LZ4 block and returns the
|
|
249
|
+
* compressed bytes. The result is byte-identical to Go's `pierrec/lz4/v4`
|
|
250
|
+
* `CompressBlock` for the same input.
|
|
251
|
+
*/
|
|
252
|
+
export function compressBlock(data: Uint8Array): Uint8Array {
|
|
253
|
+
const dst = new Uint8Array(compressBlockBound(data.length));
|
|
254
|
+
const n = compressBlockInto(data, dst);
|
|
255
|
+
return dst.slice(0, n);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* decompressBlock decompresses an LZ4 block into a buffer of exactly
|
|
260
|
+
* `uncompressedSize` bytes. Throws if the block is invalid or produces a
|
|
261
|
+
* different size.
|
|
262
|
+
*/
|
|
263
|
+
export function decompressBlock(data: Uint8Array, uncompressedSize: number): Uint8Array {
|
|
264
|
+
const dst = new Uint8Array(uncompressedSize);
|
|
265
|
+
const n = lz4.decompressBlock(data, dst, 0, data.length, 0);
|
|
266
|
+
if (n !== uncompressedSize) {
|
|
267
|
+
throw new LTXFormatError(`lz4 block decompressed to ${n} bytes, expected ${uncompressedSize}`);
|
|
268
|
+
}
|
|
269
|
+
return dst;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* decompressBlockInto decompresses an LZ4 block into `dst` and returns the
|
|
274
|
+
* number of bytes written. Does not require knowing the uncompressed size
|
|
275
|
+
* ahead of time.
|
|
276
|
+
*/
|
|
277
|
+
export function decompressBlockInto(data: Uint8Array, dst: Uint8Array): number {
|
|
278
|
+
return lz4.decompressBlock(data, dst, 0, data.length, 0);
|
|
279
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
|
|
9
|
+
import { readU64BE } from './bytes.ts';
|
|
10
|
+
import type { ByteReader } from './byte-reader.ts';
|
|
11
|
+
import type { PageIndex, TXID } from './types.ts';
|
|
12
|
+
import { LTXFormatError } from '../errors.ts';
|
|
13
|
+
|
|
14
|
+
const MAX_INT64 = 0x7fffffffffffffffn;
|
|
15
|
+
|
|
16
|
+
/** Encodes a u64 as a little-endian LEB128 varint (same as Go's uvarint). */
|
|
17
|
+
export function encodeUvarint(v: bigint): Uint8Array {
|
|
18
|
+
const out: number[] = [];
|
|
19
|
+
for (;;) {
|
|
20
|
+
const b = Number(v & 0x7fn);
|
|
21
|
+
v >>= 7n;
|
|
22
|
+
if (v !== 0n) {
|
|
23
|
+
out.push(b | 0x80);
|
|
24
|
+
} else {
|
|
25
|
+
out.push(b);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Uint8Array.from(out);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Decodes a u64 varint from r, advancing its position. */
|
|
33
|
+
export function readUvarint(r: ByteReader): bigint {
|
|
34
|
+
let x = 0n;
|
|
35
|
+
let s = 0n;
|
|
36
|
+
for (let i = 0; i < 10; i++) {
|
|
37
|
+
const b = r.readByte();
|
|
38
|
+
if (b < 0x80) {
|
|
39
|
+
if (i === 9 && b > 1) {
|
|
40
|
+
throw new LTXFormatError('uvarint overflows 64 bits');
|
|
41
|
+
}
|
|
42
|
+
return x | (BigInt(b) << s);
|
|
43
|
+
}
|
|
44
|
+
x |= BigInt(b & 0x7f) << s;
|
|
45
|
+
s += 7n;
|
|
46
|
+
}
|
|
47
|
+
throw new LTXFormatError('uvarint overflows 64 bits');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parses the page index section from r. observe, if non-null, receives every
|
|
52
|
+
* byte consumed (for checksumming); fn receives each record in file order.
|
|
53
|
+
*/
|
|
54
|
+
export function parsePageIndex(
|
|
55
|
+
r: ByteReader,
|
|
56
|
+
observe: ((buf: Uint8Array) => void) | null,
|
|
57
|
+
fn: (pgno: number, offset: number, size: number) => void,
|
|
58
|
+
): void {
|
|
59
|
+
let consumed = 0;
|
|
60
|
+
let prevPgno = 0;
|
|
61
|
+
|
|
62
|
+
for (;;) {
|
|
63
|
+
const start = r.pos;
|
|
64
|
+
const pgno = readUvarint(r);
|
|
65
|
+
if (pgno === 0n) {
|
|
66
|
+
// End marker.
|
|
67
|
+
observe?.(r.slice(start, r.pos));
|
|
68
|
+
consumed += r.pos - start;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (pgno > 0xffffffffn) {
|
|
72
|
+
throw new LTXFormatError(`page index pgno ${pgno} out of range`);
|
|
73
|
+
}
|
|
74
|
+
if (pgno <= BigInt(prevPgno)) {
|
|
75
|
+
throw new LTXFormatError(`page index out of order: ${pgno} after ${prevPgno}`);
|
|
76
|
+
}
|
|
77
|
+
const offset = readUvarint(r);
|
|
78
|
+
const size = readUvarint(r);
|
|
79
|
+
if (offset > MAX_INT64 || size > MAX_INT64) {
|
|
80
|
+
throw new LTXFormatError(`page index pgno ${pgno} offset/size out of range`);
|
|
81
|
+
}
|
|
82
|
+
observe?.(r.slice(start, r.pos));
|
|
83
|
+
consumed += r.pos - start;
|
|
84
|
+
prevPgno = Number(pgno);
|
|
85
|
+
fn(Number(pgno), Number(offset), Number(size));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Read trailing size field.
|
|
89
|
+
const sizeBuf = r.readFull(8);
|
|
90
|
+
observe?.(sizeBuf);
|
|
91
|
+
const indexSize = readU64BE(sizeBuf, 0);
|
|
92
|
+
if (indexSize !== BigInt(consumed)) {
|
|
93
|
+
throw new LTXFormatError(`page index size mismatch: field=${indexSize} read=${consumed}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* DecodePageIndex decodes the page index from r, starting at the current
|
|
99
|
+
* position, into a Map keyed by page number.
|
|
100
|
+
*/
|
|
101
|
+
export function decodePageIndex(
|
|
102
|
+
r: ByteReader,
|
|
103
|
+
level: number,
|
|
104
|
+
minTXID: TXID,
|
|
105
|
+
maxTXID: TXID,
|
|
106
|
+
): PageIndex {
|
|
107
|
+
const index: PageIndex = new Map();
|
|
108
|
+
parsePageIndex(
|
|
109
|
+
r,
|
|
110
|
+
null,
|
|
111
|
+
(pgno, offset, size) => {
|
|
112
|
+
index.set(pgno, {
|
|
113
|
+
level,
|
|
114
|
+
minTXID,
|
|
115
|
+
maxTXID,
|
|
116
|
+
offset,
|
|
117
|
+
size,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
return index;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Encodes a single page index record `(pgno, offset, size)` as a varint buffer. */
|
|
125
|
+
export function encodePageIndexRecord(pgno: number, offset: number, size: number): Uint8Array {
|
|
126
|
+
const parts = [
|
|
127
|
+
encodeUvarint(BigInt(pgno)),
|
|
128
|
+
encodeUvarint(BigInt(offset)),
|
|
129
|
+
encodeUvarint(BigInt(size)),
|
|
130
|
+
];
|
|
131
|
+
let total = 0;
|
|
132
|
+
for (const p of parts) total += p.length;
|
|
133
|
+
const out = new Uint8Array(total);
|
|
134
|
+
let o = 0;
|
|
135
|
+
for (const p of parts) {
|
|
136
|
+
out.set(p, o);
|
|
137
|
+
o += p.length;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
package/src/ltx/page.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LTX page header marshalling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PageHeaderSize, PageHeaderFlagSize, MaxPageSize } from './constants.ts';
|
|
6
|
+
import { assertFull, readU16BE, readU32BE, writeU16BE, writeU32BE } from './bytes.ts';
|
|
7
|
+
import { decompressBlockInto } from './lz4.ts';
|
|
8
|
+
import type { PageHeader } from './types.ts';
|
|
9
|
+
import { LTXFormatError } from '../errors.ts';
|
|
10
|
+
|
|
11
|
+
/** MarshalBinary encodes a PageHeader to its 6-byte representation. */
|
|
12
|
+
export function marshalPageHeader(h: PageHeader): Uint8Array {
|
|
13
|
+
const b = new Uint8Array(PageHeaderSize);
|
|
14
|
+
writeU32BE(b, 0, h.pgno);
|
|
15
|
+
writeU16BE(b, 4, h.flags);
|
|
16
|
+
return b;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** UnmarshalBinary decodes a PageHeader from a byte slice. */
|
|
20
|
+
export function unmarshalPageHeader(b: Uint8Array): PageHeader {
|
|
21
|
+
assertFull(b, 0, PageHeaderSize);
|
|
22
|
+
return {
|
|
23
|
+
pgno: readU32BE(b, 0),
|
|
24
|
+
flags: readU16BE(b, 4),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Validate returns an error (thrown) if h is invalid. */
|
|
29
|
+
export function validatePageHeader(h: PageHeader): void {
|
|
30
|
+
if (h.pgno === 0) {
|
|
31
|
+
throw new LTXFormatError('page number required');
|
|
32
|
+
}
|
|
33
|
+
if ((h.flags & ~PageHeaderFlagSize) !== 0) {
|
|
34
|
+
throw new LTXFormatError(`invalid page header flags: 0x${h.flags.toString(16)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The decoded result of a single LTX page frame. */
|
|
39
|
+
export interface DecodedPage {
|
|
40
|
+
header: PageHeader;
|
|
41
|
+
data: Uint8Array;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* DecodePageData decodes a single page frame from `b` (which must start at a
|
|
46
|
+
* page header). Matches Go's `ltx.DecodePageData`. For an empty (zero) page
|
|
47
|
+
* header, `data` is an empty buffer.
|
|
48
|
+
*
|
|
49
|
+
* Note: the uncompressed byte length is unknown here; LTX pages are at most
|
|
50
|
+
* `MaxPageSize`, so we decompress into a 64KiB buffer and slice to the bytes
|
|
51
|
+
* written. Callers that know the page size should use `DecodePage` in the
|
|
52
|
+
* decoder for the size-accurate path.
|
|
53
|
+
*/
|
|
54
|
+
export function decodePageData(b: Uint8Array): DecodedPage {
|
|
55
|
+
const header = unmarshalPageHeader(b);
|
|
56
|
+
if (header.pgno === 0 && header.flags === 0) {
|
|
57
|
+
return { header, data: new Uint8Array(0) };
|
|
58
|
+
}
|
|
59
|
+
validatePageHeader(header);
|
|
60
|
+
|
|
61
|
+
if ((header.flags & PageHeaderFlagSize) !== 0) {
|
|
62
|
+
if (b.length < PageHeaderSize + 4) {
|
|
63
|
+
throw new LTXFormatError('buffer too small for size prefix');
|
|
64
|
+
}
|
|
65
|
+
const dataSize = readU32BE(b, PageHeaderSize);
|
|
66
|
+
const offset = PageHeaderSize + 4;
|
|
67
|
+
if (b.length < offset + dataSize) {
|
|
68
|
+
throw new LTXFormatError(`buffer too small for data: need ${offset + dataSize}, have ${b.length}`);
|
|
69
|
+
}
|
|
70
|
+
const compressed = b.subarray(offset, offset + dataSize);
|
|
71
|
+
const buf = new Uint8Array(MaxPageSize);
|
|
72
|
+
const n = decompressBlockInto(compressed, buf);
|
|
73
|
+
if (n <= 0) {
|
|
74
|
+
throw new LTXFormatError('decompress block failed');
|
|
75
|
+
}
|
|
76
|
+
return { header, data: buf.subarray(0, n) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new LTXFormatError('legacy LZ4 frame format not supported');
|
|
80
|
+
}
|
package/src/ltx/sink.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
|
|
10
|
+
/** In-memory sink that accumulates bytes into a single growable buffer. */
|
|
11
|
+
export class BufferSink implements ByteSink {
|
|
12
|
+
private chunks: Uint8Array[] = [];
|
|
13
|
+
private _length = 0;
|
|
14
|
+
|
|
15
|
+
write(bytes: Uint8Array): void {
|
|
16
|
+
this.chunks.push(Uint8Array.from(bytes));
|
|
17
|
+
this._length += bytes.length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get length(): number {
|
|
21
|
+
return this._length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toBuffer(): Uint8Array {
|
|
25
|
+
const out = new Uint8Array(this._length);
|
|
26
|
+
let o = 0;
|
|
27
|
+
for (const c of this.chunks) {
|
|
28
|
+
out.set(c, o);
|
|
29
|
+
o += c.length;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
}
|