@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
package/src/store.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store manages one or more Replicas (a litestream instance).
|
|
3
|
+
*
|
|
4
|
+
* MVP: a thin orchestration layer that opens replicas for database paths with
|
|
5
|
+
* their replica clients, syncs/snapshots them, and closes. Monitor loops,
|
|
6
|
+
* scheduling, and retention are future work.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Replica, RetentionOptions, ReplicaRunOptions } from './replica.ts';
|
|
10
|
+
import { Replica as ReplicaClass } from './replica.ts';
|
|
11
|
+
import type { ReplicaClient } from './replica-client.ts';
|
|
12
|
+
|
|
13
|
+
/** Store coordinates multiple replicated databases. */
|
|
14
|
+
export class Store {
|
|
15
|
+
private items: Replica[] = [];
|
|
16
|
+
|
|
17
|
+
/** OpenDB opens a replica for a database path and wires it to a client. */
|
|
18
|
+
async openDB(path: string, client: ReplicaClient): Promise<Replica> {
|
|
19
|
+
const replica = new ReplicaClass(path, client);
|
|
20
|
+
this.items.push(replica);
|
|
21
|
+
return replica;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Replicas returns all open replicas. */
|
|
25
|
+
replicas(): Replica[] {
|
|
26
|
+
return [...this.items];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** SyncAll syncs every database (local writes then replica upload). */
|
|
30
|
+
async syncAll(): Promise<void> {
|
|
31
|
+
for (const replica of this.items) {
|
|
32
|
+
await replica.sync();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** SnapshotAll writes & uploads level-9 snapshots for every database. */
|
|
37
|
+
async snapshotAll(): Promise<void> {
|
|
38
|
+
for (const replica of this.items) {
|
|
39
|
+
await replica.snapshot();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** CompactAll compacts pending LTX files for every database. */
|
|
44
|
+
async compactAll(): Promise<void> {
|
|
45
|
+
for (const replica of this.items) {
|
|
46
|
+
await replica.compact();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** EnforceRetentionAll prunes old snapshots/files for every database. */
|
|
51
|
+
async enforceRetentionAll(options: RetentionOptions = {}): Promise<void> {
|
|
52
|
+
for (const replica of this.items) {
|
|
53
|
+
await replica.enforceRetention(options);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run starts each replica's continuous sync/compaction/retention loop and
|
|
59
|
+
* resolves once `options.signal` is aborted. Pass per-replica intervals and a
|
|
60
|
+
* retention policy via {@link ReplicaRunOptions}.
|
|
61
|
+
*/
|
|
62
|
+
async run(options: ReplicaRunOptions = {}): Promise<void> {
|
|
63
|
+
await Promise.all(this.items.map((replica) => replica.run(options)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Close releases all replicas' internal connections. */
|
|
67
|
+
close(): void {
|
|
68
|
+
for (const replica of this.items) replica.close();
|
|
69
|
+
this.items = [];
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/util/fs.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** File-system helpers for atomic writes and directory fsync. */
|
|
2
|
+
|
|
3
|
+
import { writeSync } from 'node:fs';
|
|
4
|
+
import { mkdir, open, type FileHandle } from 'node:fs/promises';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
import type { ByteSink } from '../ltx/sink.ts';
|
|
7
|
+
|
|
8
|
+
/** Ensures a directory (and parents) exists; no-op if already present. */
|
|
9
|
+
export async function mkdirAll(path: string): Promise<void> {
|
|
10
|
+
await mkdir(path, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Fsyncs a directory so a preceding rename within it is durable. */
|
|
14
|
+
export async function fsyncDir(path: string): Promise<void> {
|
|
15
|
+
const handle = await open(path, 'r');
|
|
16
|
+
try {
|
|
17
|
+
await handle.sync();
|
|
18
|
+
} finally {
|
|
19
|
+
await handle.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns the parent directory of a path (portable). */
|
|
24
|
+
export function parentDir(path: string): string {
|
|
25
|
+
return dirname(path);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A file-backed {@link ByteSink}. It batches bytes into fixed-size blocks and
|
|
30
|
+
* writes them synchronously, so the LTX encoder can stream a large file to disk
|
|
31
|
+
* without buffering the whole file (or the whole database) in memory.
|
|
32
|
+
*/
|
|
33
|
+
export class FileSink implements ByteSink {
|
|
34
|
+
private fh: FileHandle;
|
|
35
|
+
private buf: Uint8Array;
|
|
36
|
+
private pos = 0;
|
|
37
|
+
private filePos = 0;
|
|
38
|
+
|
|
39
|
+
constructor(fh: FileHandle, chunkSize = 256 * 1024) {
|
|
40
|
+
this.fh = fh;
|
|
41
|
+
this.buf = new Uint8Array(chunkSize);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
write(bytes: Uint8Array): void {
|
|
45
|
+
let o = 0;
|
|
46
|
+
while (o < bytes.length) {
|
|
47
|
+
if (this.pos === this.buf.length) this.flushSync();
|
|
48
|
+
const take = Math.min(this.buf.length - this.pos, bytes.length - o);
|
|
49
|
+
this.buf.set(bytes.subarray(o, o + take), this.pos);
|
|
50
|
+
this.pos += take;
|
|
51
|
+
o += take;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Returns the number of bytes written so far. */
|
|
56
|
+
get length(): number {
|
|
57
|
+
return this.filePos + this.pos;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private flushSync(): void {
|
|
61
|
+
if (this.pos === 0) return;
|
|
62
|
+
writeAllSync(this.fh, this.buf, this.pos, this.filePos);
|
|
63
|
+
this.filePos += this.pos;
|
|
64
|
+
this.pos = 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Flushes any remaining buffered bytes and fsyncs the file. */
|
|
68
|
+
async flush(): Promise<void> {
|
|
69
|
+
this.flushSync();
|
|
70
|
+
await this.fh.sync();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Writes all of `buf[0..len)` to `handle`'s fd starting at `position`. */
|
|
75
|
+
function writeAllSync(handle: FileHandle, buf: Uint8Array, len: number, position: number): void {
|
|
76
|
+
const fd = handle.fd;
|
|
77
|
+
let offset = 0;
|
|
78
|
+
while (offset < len) {
|
|
79
|
+
const written = writeSync(fd, buf, offset, len - offset, position + offset);
|
|
80
|
+
offset += written;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Minimal structured logger interface (stand-in for Go's slog.Logger). */
|
|
2
|
+
export interface Logger {
|
|
3
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
4
|
+
info(msg: string, ...args: unknown[]): void;
|
|
5
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
6
|
+
error(msg: string, ...args: unknown[]): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A no-op logger used when none is provided. */
|
|
10
|
+
export const noopLogger: Logger = {
|
|
11
|
+
debug() {},
|
|
12
|
+
info() {},
|
|
13
|
+
warn() {},
|
|
14
|
+
error() {},
|
|
15
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Stream helpers for consuming/capturing LTX byte streams. */
|
|
2
|
+
|
|
3
|
+
/** Concatenates an array of byte chunks into a single Uint8Array. */
|
|
4
|
+
export function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
|
|
5
|
+
let total = 0;
|
|
6
|
+
for (const c of chunks) total += c.length;
|
|
7
|
+
const out = new Uint8Array(total);
|
|
8
|
+
let o = 0;
|
|
9
|
+
for (const c of chunks) {
|
|
10
|
+
out.set(c, o);
|
|
11
|
+
o += c.length;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads an entire async iterable of byte chunks into a single Uint8Array.
|
|
18
|
+
* String chunks are encoded as UTF-8.
|
|
19
|
+
*/
|
|
20
|
+
export async function readAll(
|
|
21
|
+
stream: AsyncIterable<Uint8Array | string> | { [Symbol.asyncIterator](): AsyncIterator<Uint8Array | string> },
|
|
22
|
+
): Promise<Uint8Array> {
|
|
23
|
+
const chunks: Uint8Array[] = [];
|
|
24
|
+
for await (const chunk of stream) {
|
|
25
|
+
chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : Uint8Array.from(chunk));
|
|
26
|
+
}
|
|
27
|
+
return concatBytes(chunks);
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite WAL checksum.
|
|
3
|
+
*
|
|
4
|
+
* Matches litestream's `WALChecksum` and SQLite's wal-checksum algorithm.
|
|
5
|
+
* The byte order is chosen from the WAL magic: 0x377f0682 => little-endian,
|
|
6
|
+
* 0x377f0683 => big-endian. The checksum runs over 8-byte units:
|
|
7
|
+
* s0 += u32[a] + s1 ; s1 += u32[a+4] + s0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readU32BE, readU32LE } from '../ltx/bytes.ts';
|
|
11
|
+
import { WALFormatError } from '../errors.ts';
|
|
12
|
+
|
|
13
|
+
export type ByteOrder = 'le' | 'be';
|
|
14
|
+
|
|
15
|
+
/** Reads a u32 from `buf` at `off` using the given byte order. */
|
|
16
|
+
export function readWalU32(bo: ByteOrder, buf: Uint8Array, off: number): number {
|
|
17
|
+
if (bo === 'le') return readU32LE(buf, off);
|
|
18
|
+
return readU32BE(buf, off);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* walChecksum computes a running SQLite WAL checksum over a byte slice whose
|
|
23
|
+
* length is a multiple of 8. Returns the updated (s0, s1) pair.
|
|
24
|
+
*/
|
|
25
|
+
export function walChecksum(
|
|
26
|
+
bo: ByteOrder,
|
|
27
|
+
s0: number,
|
|
28
|
+
s1: number,
|
|
29
|
+
b: Uint8Array,
|
|
30
|
+
): [number, number] {
|
|
31
|
+
if (b.length % 8 !== 0) {
|
|
32
|
+
throw new WALFormatError('misaligned checksum byte slice');
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i < b.length; i += 8) {
|
|
35
|
+
s0 = (s0 + readWalU32(bo, b, i) + s1) >>> 0;
|
|
36
|
+
s1 = (s1 + readWalU32(bo, b, i + 4) + s0) >>> 0;
|
|
37
|
+
}
|
|
38
|
+
return [s0, s1];
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** SQLite WAL (Write-Ahead Log) file constants. */
|
|
2
|
+
|
|
3
|
+
export const WALHeaderSize = 32;
|
|
4
|
+
export const WALFrameHeaderSize = 24;
|
|
5
|
+
|
|
6
|
+
/** WAL magic values (always read big-endian; determines checksum byte order). */
|
|
7
|
+
export const WALMagicLE = 0x377f0682; // checksums are computed little-endian
|
|
8
|
+
export const WALMagicBE = 0x377f0683; // checksums are computed big-endian
|
|
9
|
+
|
|
10
|
+
export const WALVersion = 3007000;
|
package/src/wal/index.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WALReader parses a SQLite WAL file and verifies salt & checksum integrity
|
|
3
|
+
* while reading. It does not enforce transaction boundaries; the caller is
|
|
4
|
+
* responsible for handling uncommitted frames.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors litestream's `WALReader` (wal_reader.go).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WALFrameHeaderSize, WALHeaderSize, WALMagicBE, WALMagicLE, WALVersion } from './constants.ts';
|
|
10
|
+
import type { ByteOrder } from './checksum.ts';
|
|
11
|
+
import { walChecksum } from './checksum.ts';
|
|
12
|
+
import { readU32BE } from '../ltx/bytes.ts';
|
|
13
|
+
import { WALFormatError } from '../errors.ts';
|
|
14
|
+
|
|
15
|
+
/** Raised when the WAL ends, or when a salt/checksum mismatch is detected. */
|
|
16
|
+
export class WALEOFError extends Error {}
|
|
17
|
+
|
|
18
|
+
/** Raised when starting mid-WAL and the previous frame does not match. */
|
|
19
|
+
export class PrevFrameMismatchError extends Error {
|
|
20
|
+
readonly cause: Error;
|
|
21
|
+
constructor(cause: Error) {
|
|
22
|
+
super(`prev frame mismatch: ${cause.message}`);
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WALFrame {
|
|
28
|
+
pgno: number;
|
|
29
|
+
commit: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WALPageMap {
|
|
33
|
+
/** Latest committed offset (file offset) for each page. */
|
|
34
|
+
pageIndex: Map<number, number>;
|
|
35
|
+
/** Offset just past the last read frame. */
|
|
36
|
+
maxOffset: number;
|
|
37
|
+
/** Final database size in pages (from the last commit frame). */
|
|
38
|
+
commit: number;
|
|
39
|
+
/** Whether the scan was bounded by maxBytes. */
|
|
40
|
+
limited: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** WALReader reads and validates frames from an in-memory WAL byte buffer. */
|
|
44
|
+
export class WALReader {
|
|
45
|
+
private data: Uint8Array;
|
|
46
|
+
private bo!: ByteOrder;
|
|
47
|
+
private pageSizeVal = 0;
|
|
48
|
+
private seqVal = 0;
|
|
49
|
+
private salt1Value = 0;
|
|
50
|
+
private salt2Value = 0;
|
|
51
|
+
private chksum1 = 0;
|
|
52
|
+
private chksum2 = 0;
|
|
53
|
+
private frameN = 0;
|
|
54
|
+
|
|
55
|
+
constructor(data: Uint8Array) {
|
|
56
|
+
this.data = data;
|
|
57
|
+
this.readHeader();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Creates a reader positioned at `offset`, loading salts from the caller. */
|
|
61
|
+
static withOffset(data: Uint8Array, offset: number, salt1: number, salt2: number): WALReader {
|
|
62
|
+
if (offset <= WALHeaderSize) {
|
|
63
|
+
throw new WALFormatError(
|
|
64
|
+
`offset (${offset}) must be greater than the wal header size (${WALHeaderSize})`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const r = new WALReader(data);
|
|
68
|
+
|
|
69
|
+
// Load the salt from the caller; the beginning of the file may be gone.
|
|
70
|
+
r.salt1Value = salt1;
|
|
71
|
+
r.salt2Value = salt2;
|
|
72
|
+
|
|
73
|
+
// Ensure the offset is positioned on a frame start.
|
|
74
|
+
const frameSize = r.pageSizeVal + WALFrameHeaderSize;
|
|
75
|
+
if ((offset - WALHeaderSize) % frameSize !== 0) {
|
|
76
|
+
throw new WALFormatError(`unaligned wal offset ${offset} for page size ${r.pageSizeVal}`);
|
|
77
|
+
}
|
|
78
|
+
r.frameN = Math.floor((offset - WALHeaderSize) / frameSize);
|
|
79
|
+
|
|
80
|
+
// Read the previous page to load the checksum state.
|
|
81
|
+
r.frameN--;
|
|
82
|
+
try {
|
|
83
|
+
r.readFrameInternal(new Uint8Array(r.pageSizeVal), false);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err instanceof WALEOFError) {
|
|
86
|
+
throw new PrevFrameMismatchError(err);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** PageSize returns the page size from the header. */
|
|
95
|
+
pageSize(): number {
|
|
96
|
+
return this.pageSizeVal;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Seq returns the checkpoint sequence from the header. */
|
|
100
|
+
seq(): number {
|
|
101
|
+
return this.seqVal;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Salt1 returns the header salt-1 value. */
|
|
105
|
+
salt1(): number {
|
|
106
|
+
return this.salt1Value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Salt2 returns the header salt-2 value. */
|
|
110
|
+
salt2(): number {
|
|
111
|
+
return this.salt2Value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Offset returns the file offset of the last read frame (0 if none). */
|
|
115
|
+
offset(): number {
|
|
116
|
+
if (this.frameN === 0) return 0;
|
|
117
|
+
return WALHeaderSize + (this.frameN - 1) * (WALFrameHeaderSize + this.pageSizeVal);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** ReadFrame reads the next frame, verifying salt & checksum. */
|
|
121
|
+
readFrame(data: Uint8Array): WALFrame {
|
|
122
|
+
return this.readFrameInternal(data, true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** PageMap scans committed frames and returns the latest page offsets. */
|
|
126
|
+
pageMap(maxBytes = 0): WALPageMap {
|
|
127
|
+
const m = new Map<number, number>();
|
|
128
|
+
const txMap = new Map<number, number>();
|
|
129
|
+
const data = new Uint8Array(this.pageSizeVal);
|
|
130
|
+
const frameSize = WALFrameHeaderSize + this.pageSizeVal;
|
|
131
|
+
const startOffset = WALHeaderSize + this.frameN * frameSize;
|
|
132
|
+
let commit = 0;
|
|
133
|
+
let limited = false;
|
|
134
|
+
|
|
135
|
+
for (;;) {
|
|
136
|
+
let frame: WALFrame;
|
|
137
|
+
try {
|
|
138
|
+
frame = this.readFrame(data);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof WALEOFError) break;
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Buffer the latest offset for each page in the current transaction.
|
|
145
|
+
const offset = this.offset();
|
|
146
|
+
txMap.set(frame.pgno, offset);
|
|
147
|
+
|
|
148
|
+
// On a commit record, flush pending pages into the full map.
|
|
149
|
+
if (frame.commit !== 0) {
|
|
150
|
+
for (const [pgno, off] of txMap) m.set(pgno, off);
|
|
151
|
+
txMap.clear();
|
|
152
|
+
commit = frame.commit;
|
|
153
|
+
|
|
154
|
+
if (maxBytes > 0 && this.offset() + frameSize - startOffset >= maxBytes) {
|
|
155
|
+
limited = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Drop pages beyond the final commit size (DB may have shrunk via VACUUM).
|
|
162
|
+
for (const pgno of [...m.keys()]) {
|
|
163
|
+
if (pgno > commit) m.delete(pgno);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (m.size === 0) {
|
|
167
|
+
return { pageIndex: m, maxOffset: 0, commit: 0, limited };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Compute the highest frame offset and extend past its data.
|
|
171
|
+
let end = 0;
|
|
172
|
+
for (const offset of m.values()) {
|
|
173
|
+
if (end === 0 || offset > end) end = offset;
|
|
174
|
+
}
|
|
175
|
+
end += frameSize;
|
|
176
|
+
|
|
177
|
+
return { pageIndex: m, maxOffset: end, commit, limited };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private readHeader(): void {
|
|
181
|
+
if (this.data.length < WALHeaderSize) {
|
|
182
|
+
throw new WALEOFError();
|
|
183
|
+
}
|
|
184
|
+
const hdr = this.data.subarray(0, WALHeaderSize);
|
|
185
|
+
|
|
186
|
+
const magic = readU32BE(hdr, 0);
|
|
187
|
+
if (magic === WALMagicLE) this.bo = 'le';
|
|
188
|
+
else if (magic === WALMagicBE) this.bo = 'be';
|
|
189
|
+
else throw new WALFormatError(`invalid wal header magic: ${magic.toString(16)}`);
|
|
190
|
+
|
|
191
|
+
const chksum1 = readU32BE(hdr, 24);
|
|
192
|
+
const chksum2 = readU32BE(hdr, 28);
|
|
193
|
+
const [v0, v1] = walChecksum(this.bo, 0, 0, hdr.subarray(0, 24));
|
|
194
|
+
if (v0 !== chksum1 || v1 !== chksum2) {
|
|
195
|
+
throw new WALEOFError();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const version = readU32BE(hdr, 4);
|
|
199
|
+
if (version !== WALVersion) {
|
|
200
|
+
throw new WALFormatError(`unsupported wal version: ${version}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.pageSizeVal = readU32BE(hdr, 8);
|
|
204
|
+
this.seqVal = readU32BE(hdr, 12);
|
|
205
|
+
this.salt1Value = readU32BE(hdr, 16);
|
|
206
|
+
this.salt2Value = readU32BE(hdr, 20);
|
|
207
|
+
this.chksum1 = chksum1;
|
|
208
|
+
this.chksum2 = chksum2;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private readFrameInternal(data: Uint8Array, verifyChecksum: boolean): WALFrame {
|
|
212
|
+
if (data.length !== this.pageSizeVal) {
|
|
213
|
+
throw new WALFormatError(
|
|
214
|
+
`buffer size (${data.length}) must match page size (${this.pageSizeVal})`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const frameSize = this.pageSizeVal + WALFrameHeaderSize;
|
|
219
|
+
const offset = WALHeaderSize + this.frameN * frameSize;
|
|
220
|
+
if (offset + frameSize > this.data.length) {
|
|
221
|
+
throw new WALEOFError();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const hdr = this.data.subarray(offset, offset + WALFrameHeaderSize);
|
|
225
|
+
const page = this.data.subarray(offset + WALFrameHeaderSize, offset + frameSize);
|
|
226
|
+
|
|
227
|
+
// Verify the frame salt matches the header salt.
|
|
228
|
+
const salt1 = readU32BE(hdr, 8);
|
|
229
|
+
const salt2 = readU32BE(hdr, 12);
|
|
230
|
+
if (this.salt1Value !== salt1 || this.salt2Value !== salt2) {
|
|
231
|
+
throw new WALEOFError();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const fchksum1 = readU32BE(hdr, 16);
|
|
235
|
+
const fchksum2 = readU32BE(hdr, 20);
|
|
236
|
+
|
|
237
|
+
if (verifyChecksum) {
|
|
238
|
+
[this.chksum1, this.chksum2] = walChecksum(
|
|
239
|
+
this.bo,
|
|
240
|
+
this.chksum1,
|
|
241
|
+
this.chksum2,
|
|
242
|
+
hdr.subarray(0, 8),
|
|
243
|
+
);
|
|
244
|
+
[this.chksum1, this.chksum2] = walChecksum(
|
|
245
|
+
this.bo,
|
|
246
|
+
this.chksum1,
|
|
247
|
+
this.chksum2,
|
|
248
|
+
page,
|
|
249
|
+
);
|
|
250
|
+
if (this.chksum1 !== fchksum1 || this.chksum2 !== fchksum2) {
|
|
251
|
+
throw new WALEOFError();
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
this.chksum1 = fchksum1;
|
|
255
|
+
this.chksum2 = fchksum2;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const pgno = readU32BE(hdr, 0);
|
|
259
|
+
const commit = readU32BE(hdr, 4);
|
|
260
|
+
this.frameN++;
|
|
261
|
+
|
|
262
|
+
return { pgno, commit };
|
|
263
|
+
}
|
|
264
|
+
}
|