@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/README.md
CHANGED
|
@@ -1,3 +1,483 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sqlite-js-stream
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A **Node.js port of [Litestream](https://github.com/benbjohnson/litestream)** — SQLite streaming
|
|
4
|
+
replication and disaster recovery — written in **TypeScript** and built on the built-in `node:sqlite`.
|
|
5
|
+
|
|
6
|
+
SQLite is the most widely deployed database on the planet but it only lives on a single machine.
|
|
7
|
+
If that machine dies, your database is gone. This library replicates a SQLite database's **WAL
|
|
8
|
+
(Write-Ahead Log)** file into immutable, chronological **LTX (Log Transaction)** files on a remote
|
|
9
|
+
replica (S3, a local directory, etc.), and can **restore** a database from those files.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- **Node.js `>= 24`** — required for the built-in `node:sqlite`.
|
|
16
|
+
- Runtime dependency: `lz4js` (pure-JS LZ4 **block** decoder), matching the LTX format.
|
|
17
|
+
Compression is a byte-identical TypeScript port of Go's `pierrec/lz4/v4` Fast block compressor.
|
|
18
|
+
|
|
19
|
+
The **published** package ships compiled JavaScript plus TypeScript declarations
|
|
20
|
+
(`dist/*.js` + `dist/*.d.ts`), built at publish time via `pnpm build`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
This repository is a **pnpm workspace**. The core library lives at the root and
|
|
27
|
+
upload strategies (replica backends) live under `packages/`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm install # link the workspace
|
|
31
|
+
pnpm build # transpile src -> dist (.js + .d.ts), used by addons/tests
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The core package is published as **`@platformatic/sqlite-js-stream`** and the S3
|
|
35
|
+
addon as **`@platformatic/sqlite-js-stream-s3`**:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add @platformatic/sqlite-js-stream
|
|
39
|
+
pnpm add @platformatic/sqlite-js-stream-s3 # optional addon
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Importing the addon registers the `s3` URL scheme with
|
|
43
|
+
`newReplicaClientFromURL('s3://bucket/prefix')`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { Replica, FileReplicaClient } from '@platformatic/sqlite-js-stream';
|
|
51
|
+
|
|
52
|
+
// 1. Point the replica at your database path and a replica destination.
|
|
53
|
+
// The library reads the database + `-wal` directly and is agnostic to
|
|
54
|
+
// whichever SQLite driver your application uses.
|
|
55
|
+
const client = new FileReplicaClient('/data/replica');
|
|
56
|
+
const replica = new Replica('/data/app.db', client);
|
|
57
|
+
|
|
58
|
+
// 2. Sync builds a local LTX file (into <db>-litestream/ltx/0/) and uploads it.
|
|
59
|
+
await replica.sync();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The database is written to by your **own** SQLite connection (using `node:sqlite`, `better-sqlite3`,
|
|
63
|
+
or anything else). After more writes, just `sync()` again:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// ... your app writes rows with whatever driver you use ...
|
|
67
|
+
await replica.sync();
|
|
68
|
+
|
|
69
|
+
const status = await replica.syncStatus();
|
|
70
|
+
console.log(status.inSync, status.localTXID, status.remoteTXID); // true 3n 3n
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Take a full snapshot
|
|
74
|
+
|
|
75
|
+
Normal `sync()` writes **incremental** level-0 files. For a complete, point-in-time backup, take a
|
|
76
|
+
**level-9 snapshot** (a full database copy as an LTX file with `MinTXID = 1`):
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await replica.snapshot();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Restore a database
|
|
83
|
+
|
|
84
|
+
Restore reconstructs a SQLite database by compacting a level-9 snapshot with the subsequent
|
|
85
|
+
level-0 files:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await replica.restore({ outputPath: '/data/restored.db' });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Restore to a specific transaction ID:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await replica.restore({ outputPath: '/data/app-at-txid.db', txID: 42n });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Continuously restore (follow)
|
|
98
|
+
|
|
99
|
+
`follow()` restores the latest backup, then keeps the database up to date as new backups are
|
|
100
|
+
replicated — maintaining a read-only replica (litestream's `restore -f`):
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const ac = new AbortController();
|
|
104
|
+
await replica.follow({ outputPath: '/data/replica.db', intervalMs: 1000, signal: ac.signal });
|
|
105
|
+
// later: ac.abort() to stop
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The applied TXID is recorded in a `-txid` sidecar (default `<outputPath>-txid`) so a later `follow()`
|
|
109
|
+
resumes instead of re-restoring. The output database should only be opened **read-only**.
|
|
110
|
+
|
|
111
|
+
### Restore the local database if it is missing
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
await replica.ensureExists();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Manage many databases
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { Store } from '@platformatic/sqlite-js-stream';
|
|
121
|
+
|
|
122
|
+
const store = new Store();
|
|
123
|
+
await store.openDB('/data/a.db', new FileReplicaClient('/data/replica-a'));
|
|
124
|
+
await store.openDB('/data/b.db', new FileReplicaClient('/data/replica-b'));
|
|
125
|
+
|
|
126
|
+
await store.syncAll(); // local sync + upload for every database
|
|
127
|
+
await store.snapshotAll(); // level-9 snapshots for every database
|
|
128
|
+
|
|
129
|
+
store.close();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Replica URL
|
|
133
|
+
|
|
134
|
+
Replica clients are created from URLs via the factory registry:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { newReplicaClientFromURL, Replica } from '@platformatic/sqlite-js-stream';
|
|
138
|
+
|
|
139
|
+
const client = newReplicaClientFromURL('file:///data/replica');
|
|
140
|
+
const replica = new Replica('/data/app.db', client);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The `file` scheme is built into the core. Additional schemes (S3, etc.) are
|
|
144
|
+
registered by importing the matching addon (see below).
|
|
145
|
+
|
|
146
|
+
### S3 upload strategy
|
|
147
|
+
|
|
148
|
+
The `@platformatic/sqlite-js-stream-s3` addon provides an S3 replica client. Import it
|
|
149
|
+
once to register the `s3://` scheme, then create clients from URLs or directly:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { Replica, newReplicaClientFromURL } from '@platformatic/sqlite-js-stream';
|
|
153
|
+
import { S3ReplicaClient } from '@platformatic/sqlite-js-stream-s3';
|
|
154
|
+
|
|
155
|
+
// Via a URL (bucket from the host, prefix from the path).
|
|
156
|
+
const client = newReplicaClientFromURL('s3://my-bucket/backup?region=us-east-1');
|
|
157
|
+
|
|
158
|
+
// Or directly, with explicit AWS config.
|
|
159
|
+
const s3 = new S3ReplicaClient({
|
|
160
|
+
bucket: 'my-bucket',
|
|
161
|
+
prefix: 'backup',
|
|
162
|
+
region: 'us-east-1',
|
|
163
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
164
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const replica = new Replica('/data/app.db', client);
|
|
168
|
+
await replica.run({ intervalMs: 1000 });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Objects are stored as `{prefix}/ltx/{level}/{minTXID}-{maxTXID}.ltx` — the same
|
|
172
|
+
layout as the file backend, so a replica can move between strategies. Creation
|
|
173
|
+
timestamps are preserved as the `ltx-timestamp` object metadata.
|
|
174
|
+
|
|
175
|
+
> **Testing without AWS:** `S3ReplicaClient` accepts an injected `client`
|
|
176
|
+
> implementing `send(command)`. Pass an in-memory fake (see
|
|
177
|
+
> `packages/backend-s3/test/helpers/memory-s3.ts`) to exercise the addon without
|
|
178
|
+
> network access.
|
|
179
|
+
|
|
180
|
+
### Continuous sync
|
|
181
|
+
|
|
182
|
+
For ongoing replication, `replica.run()` syncs immediately and then repeats on an
|
|
183
|
+
interval until the supplied `AbortSignal` is aborted. Sync errors are logged and the
|
|
184
|
+
loop keeps going, so a transient failure doesn't stop replication:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { Replica, FileReplicaClient } from '@platformatic/sqlite-js-stream';
|
|
188
|
+
|
|
189
|
+
const replica = new Replica('/data/app.db', new FileReplicaClient('/data/replica'));
|
|
190
|
+
const ac = new AbortController();
|
|
191
|
+
|
|
192
|
+
// Run continuously (sync now, then every 1s); resolves when aborted.
|
|
193
|
+
const runP = replica.run({ intervalMs: 1000, signal: ac.signal });
|
|
194
|
+
|
|
195
|
+
// ... later, to stop:
|
|
196
|
+
ac.abort();
|
|
197
|
+
await runP;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
If you want to keep replicating in the background without awaiting, call `run()` and
|
|
201
|
+
`catch` its promise (it resolves on abort):
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
replica.run({ intervalMs: 1000, signal: ac.signal }).catch((err) => {
|
|
205
|
+
console.error('continuous sync stopped', err);
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`Store` also exposes `syncAll()`/`snapshotAll()` for one-shot operations; a continuous
|
|
210
|
+
loop can be driven by calling `replica.run()` per database, or via your own scheduler.
|
|
211
|
+
|
|
212
|
+
### Replication from a worker thread
|
|
213
|
+
|
|
214
|
+
Run `replica.run()` in a **worker thread** so continuous replication doesn't compete with
|
|
215
|
+
your application's main thread. The worker opens its own **read-only** connection (via the
|
|
216
|
+
library) and reads the database + `-wal` files, while your app keeps writing from the main
|
|
217
|
+
thread on its own connection. Only the **path**s need to cross threads.
|
|
218
|
+
|
|
219
|
+
`replica-worker.ts`:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { parentPort } from 'node:worker_threads';
|
|
223
|
+
import { Replica, FileReplicaClient } from '@platformatic/sqlite-js-stream';
|
|
224
|
+
|
|
225
|
+
parentPort.on('message', async (msg) => {
|
|
226
|
+
if (msg?.type !== 'start') return;
|
|
227
|
+
const { dbPath, clientPath, intervalMs = 1000 } = msg;
|
|
228
|
+
|
|
229
|
+
const replica = new Replica(dbPath, new FileReplicaClient(clientPath));
|
|
230
|
+
const ac = new AbortController();
|
|
231
|
+
const runP = replica.run({ intervalMs, signal: ac.signal });
|
|
232
|
+
|
|
233
|
+
parentPort.postMessage({ type: 'started' });
|
|
234
|
+
parentPort.on('message', async (m) => {
|
|
235
|
+
if (m?.type === 'stop') {
|
|
236
|
+
ac.abort();
|
|
237
|
+
await runP;
|
|
238
|
+
parentPort.postMessage({ type: 'done' });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Main thread:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { Worker } from 'node:worker_threads';
|
|
248
|
+
|
|
249
|
+
const worker = new Worker(new URL('./replica-worker.ts', import.meta.url));
|
|
250
|
+
const started = new Promise((resolve) => worker.on('message', (m) => m?.type === 'started' && resolve()));
|
|
251
|
+
const done = new Promise((resolve) => worker.on('message', (m) => m?.type === 'done' && resolve()));
|
|
252
|
+
|
|
253
|
+
worker.postMessage({ type: 'start', dbPath: '/data/app.db', clientPath: '/data/replica', intervalMs: 1000 });
|
|
254
|
+
await started;
|
|
255
|
+
|
|
256
|
+
// Your app keeps using node:sqlite (or better-sqlite3) from the main thread.
|
|
257
|
+
// ... write rows ...
|
|
258
|
+
|
|
259
|
+
// Shut the replication worker down gracefully.
|
|
260
|
+
worker.postMessage({ type: 'stop' });
|
|
261
|
+
await done;
|
|
262
|
+
await worker.terminate();
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
> **Important:** the worker and your app each use their **own** connection to the same database
|
|
266
|
+
> file. Never hand the worker the same `DatabaseSync` handle your app writes with — a single
|
|
267
|
+
> connection holds one transaction at a time, so sharing a writable handle would merge the read
|
|
268
|
+
> snapshot with your writes (see *Concurrency & snapshot consistency* above).
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## How it works
|
|
273
|
+
|
|
274
|
+
Litestream (and this port) never touches the database through the SQLite WAL/VFS as part of
|
|
275
|
+
replication. Instead it:
|
|
276
|
+
|
|
277
|
+
1. **Reads the `-wal` file directly** (parsing frames, verifying the WAL salt + checksum) and the
|
|
278
|
+
**database file directly** via `fs`.
|
|
279
|
+
2. **Builds an LTX file** per sync — a sequence of LZ4-compressed, checksummed pages that capture
|
|
280
|
+
the changed pages, tagged with a sequential **transaction ID**.
|
|
281
|
+
3. **Uploads** those immutable LTX files to a replica.
|
|
282
|
+
4. **Restores** by compacting a snapshot with the stream of level-0 files and writing a valid
|
|
283
|
+
SQLite file.
|
|
284
|
+
|
|
285
|
+
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full design and
|
|
286
|
+
[`docs/LTX_FORMAT.md`](docs/LTX_FORMAT.md) for the on-disk format.
|
|
287
|
+
|
|
288
|
+
### Concurrency & snapshot consistency
|
|
289
|
+
|
|
290
|
+
To keep snapshots consistent, `sync()`/`snapshot()` open a **separate, read-only** SQLite
|
|
291
|
+
connection to the same database and hold a short-lived read transaction while they read the
|
|
292
|
+
`-wal` + database files. SQLite will not checkpoint/truncate the WAL past an active reader, and the
|
|
293
|
+
pinned reader's `PRAGMA page_count` reports the commit including uncheckpointed WAL pages.
|
|
294
|
+
|
|
295
|
+
Your application is free to use its own SQLite driver and connections (`node:sqlite`,
|
|
296
|
+
`better-sqlite3`, …) independently — the library reads only the files and never touches them.
|
|
297
|
+
|
|
298
|
+
> **Do not point the library at your application's writable connection.**
|
|
299
|
+
>
|
|
300
|
+
> A single SQLite connection holds **one transaction at a time**. If the library were to use the
|
|
301
|
+
> same `DatabaseSync` handle as your writer:
|
|
302
|
+
> - its read snapshot would **not be isolated** from concurrent app writes (they'd merge into one
|
|
303
|
+
> transaction, and the library could read *uncommitted* rows or commit the app's writes), and
|
|
304
|
+
> - if your app had a transaction open, the library could not `BEGIN` its snapshot pin at all.
|
|
305
|
+
>
|
|
306
|
+
> The library has no API to accept an app-owned writable connection, and it deliberately opens its
|
|
307
|
+
> own read-only reader instead. Keep the library's reader and your app's writer on separate
|
|
308
|
+
> connections to the same database file.
|
|
309
|
+
|
|
310
|
+
### Large databases & streaming
|
|
311
|
+
|
|
312
|
+
The replication path is **streaming** end-to-end, so peak memory is bounded by one page buffer
|
|
313
|
+
(plus the small LTX page index) rather than the database size:
|
|
314
|
+
|
|
315
|
+
- `sync()`/`snapshot()` read database **pages one at a time from a file handle** and stream the
|
|
316
|
+
encoded LTX straight to a temp file (never buffering the whole DB/WAL).
|
|
317
|
+
- `restore()` streams each LTX file's pages into the output database, overlaying incremental files
|
|
318
|
+
on the snapshot base — it never materializes a full merged file or the whole DB in memory.
|
|
319
|
+
- The S3 addon uploads objects larger than **5 MiB** with the **multipart** API, so a large snapshot
|
|
320
|
+
is never buffered on the upload side either.
|
|
321
|
+
|
|
322
|
+
### Benchmarks
|
|
323
|
+
|
|
324
|
+
Reproducible, configurable benchmarks live in [`bench/bench-sync-restore.ts`](bench/bench-sync-restore.ts)
|
|
325
|
+
and measure `sync()`/`restore()` throughput plus peak process RSS:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
pnpm bench
|
|
329
|
+
# another size / batch:
|
|
330
|
+
ROWS=200000 BATCH=1000 pnpm bench
|
|
331
|
+
# demonstrate the streaming memory bound (peak RSS vs DB size):
|
|
332
|
+
SCALE=4 pnpm bench
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Conditions are disclosed in the output (rows, batch/transactions, source WAL bytes). Peak RSS is
|
|
336
|
+
process-wide (it includes the Node baseline), so the meaningful claim is that it stays **flat** as the
|
|
337
|
+
database grows — see the `SCALE` mode, which shows WAL bytes and peak RSS at two sizes.
|
|
338
|
+
|
|
339
|
+
### Compaction & retention
|
|
340
|
+
|
|
341
|
+
Over time `sync()` writes one level-0 LTX file per transaction. To keep the replica bounded and
|
|
342
|
+
restore fast, compact those into higher levels and prune data that has been superseded:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
// Merge pending level-0 files into a level-1 file (and 1→2→3 up to maxCompactionLevel).
|
|
346
|
+
await replica.compact({ dstLevel: 1 });
|
|
347
|
+
|
|
348
|
+
// Time-based retention: delete snapshots older than 24h (always keeping the
|
|
349
|
+
// newest), cascade the boundary down through the other levels, and delete
|
|
350
|
+
// level-0 files that were compacted into level-1 once they age past 5m.
|
|
351
|
+
const result = await replica.enforceRetention({
|
|
352
|
+
snapshotRetentionMs: 24 * 60 * 60 * 1000,
|
|
353
|
+
l0RetentionMs: 5 * 60 * 1000,
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`Replica.run()` can drive all of this on their own schedules, and `Store` orchestrates it across
|
|
358
|
+
every replica.
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { CompactionLevels } from '@platformatic/sqlite-js-stream';
|
|
362
|
+
|
|
363
|
+
// Each level is compacted at most once per wall-clock bucket (L1 every 30s,
|
|
364
|
+
// L2 every 5m, L3 every 1h) — mirrors litestream's per-level pacing.
|
|
365
|
+
const levels = new CompactionLevels([
|
|
366
|
+
{ level: 0, intervalMs: 0 },
|
|
367
|
+
{ level: 1, intervalMs: 30_000 },
|
|
368
|
+
{ level: 2, intervalMs: 300_000 },
|
|
369
|
+
{ level: 3, intervalMs: 3_600_000 },
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
await store.run({
|
|
373
|
+
intervalMs: 1000,
|
|
374
|
+
compactionLevels: levels,
|
|
375
|
+
snapshotIntervalMs: 24 * 60 * 60 * 1000,
|
|
376
|
+
snapshotRetentionMs: 24 * 60 * 60 * 1000,
|
|
377
|
+
l0RetentionMs: 5 * 60 * 1000,
|
|
378
|
+
signal,
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Compaction is **streaming** (bounded memory) and only deletes a lower-level file after the higher
|
|
383
|
+
level is written *and* uploaded, so a mid-compaction failure never loses data. Retention never deletes
|
|
384
|
+
files needed to restore any point within the retention window, and the live-head level-0 file is
|
|
385
|
+
always retained.
|
|
386
|
+
|
|
387
|
+
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md#compaction--retention) for the full model.
|
|
388
|
+
|
|
389
|
+
### Distributed leasing
|
|
390
|
+
|
|
391
|
+
When several replicas point at the same storage, only one may write at a time. Give the replica a
|
|
392
|
+
`Leaser` (a `lock.json` lease) and `run()` will make exactly one replica the active **primary**;
|
|
393
|
+
the others become idle **followers** that poll until the lease is free. Storage writes are gated on
|
|
394
|
+
lease ownership, so a secondary can never split the brain.
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { FileLeaser } from '@platformatic/sqlite-js-stream';
|
|
398
|
+
|
|
399
|
+
replica.leaser = new FileLeaser('/data/replica'); // becomes primary if the lease is free
|
|
400
|
+
await replica.run({ intervalMs: 1000, signal });
|
|
401
|
+
|
|
402
|
+
// Query which mode we are in:
|
|
403
|
+
if (replica.isPrimary()) {
|
|
404
|
+
// we are writing
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The file backend uses an exclusive-create lease with a content-hash token; the S3 addon
|
|
409
|
+
(`import { S3Leaser } from '@platformatic/sqlite-js-stream-s3'`) uses S3 `If-None-Match`/`If-Match`
|
|
410
|
+
conditional writes so racy acquirers cannot both win.
|
|
411
|
+
|
|
412
|
+
### ⚠️ This is a backup tool, not a distributed database
|
|
413
|
+
|
|
414
|
+
The lease coordinates **which host writes to storage**. It does **not** merge, compare, or reconcile
|
|
415
|
+
the actual data in your databases. Be very clear about what you're pointing at the same prefix.
|
|
416
|
+
|
|
417
|
+
**Multiple hosts are only safe if they hold the *same* database content** (e.g. a DR/standby host that
|
|
418
|
+
was restored from the same backup). The lease then prevents two hosts from both writing, and gives
|
|
419
|
+
you automatic failover when the primary stops.
|
|
420
|
+
|
|
421
|
+
**Do NOT point hosts that hold *different* data at the same prefix.** SQLite is a
|
|
422
|
+
single-writer, single-file database. If host A has `row id=1 → "X"` and host B has
|
|
423
|
+
`row id=1 → "Y"` and they share one prefix, here is what happens:
|
|
424
|
+
|
|
425
|
+
| Host A local DB | Host B local DB | What lands in S3 |
|
|
426
|
+
|----------------------|---------------------|------------------|
|
|
427
|
+
| `row id=1 → "X"` | `row id=1 → "Y"` | `"X"` (only if **A** is the primary) |
|
|
428
|
+
|
|
429
|
+
- Whichever host is the **primary** gets backed up. The other host's value is **never uploaded**, is
|
|
430
|
+
not merged, and is effectively **lost** from the backup's perspective.
|
|
431
|
+
- When you restore, you get exactly one host's state. There is **no conflict resolution**. `"Y"` is
|
|
432
|
+
simply gone.
|
|
433
|
+
|
|
434
|
+
**Choose the right setup:**
|
|
435
|
+
|
|
436
|
+
| If you need… | Do this |
|
|
437
|
+
|---------------------------------------------------------|----------|
|
|
438
|
+
| Each host owns a **different** database | Give each its own prefix, e.g. `app/${tenantId}` — separate backup per DB, no conflict. |
|
|
439
|
+
| Multiple app instances sharing one consistent dataset | Don't use SQLite + this replication. Use a client-server DB (Postgres/MySQL) or a real shared-file lock + single writer. |
|
|
440
|
+
| Same data on every host (failover / DR) | Keep exactly one writer. On failover, **restore the standby from S3 first**, then let it take the lease — don't let a stale local file start writing. |
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Deferred features
|
|
445
|
+
|
|
446
|
+
Not implemented — listed here so the roadmap is clear. None of these are required for the library
|
|
447
|
+
core to replicate, snapshot, compact, lease, and restore a SQLite database.
|
|
448
|
+
|
|
449
|
+
- **CLI + YAML config** — this is a *library*. The `litestream replicate/restore/ltx/reset/snapshots`
|
|
450
|
+
CLI and a `litestream.yml`-style config are out of scope for now.
|
|
451
|
+
- **Metrics/telemetry** — no Prometheus/OpenTelemetry instrumentation yet.
|
|
452
|
+
- **Point-in-time restore edge cases** — `txID`/timestamp restore is supported, but litestream's
|
|
453
|
+
full edge-case matrix is not exhaustively covered.
|
|
454
|
+
- **VFS real-time mirroring** — hooking into SQLite's VFS to mirror writes live is not implemented.
|
|
455
|
+
- **IPC control server** — litestream's Unix-socket IPC server is not implemented.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Documentation
|
|
460
|
+
|
|
461
|
+
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — system design (DB, Replica, ReplicaClient,
|
|
462
|
+
LTX, WAL, backends).
|
|
463
|
+
- [`docs/LTX_FORMAT.md`](docs/LTX_FORMAT.md) — the LTX binary format.
|
|
464
|
+
- [`test/`](test) — TDD test suite (`npm test`).
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Credits
|
|
469
|
+
|
|
470
|
+
This project is a **port of [Litestream](https://github.com/benbjohnson/litestream)**
|
|
471
|
+
by [Ben Johnson](https://github.com/benbjohnson) (Apache-2.0). The LTX file format and much of the
|
|
472
|
+
design come from the [`superfly/ltx`](https://github.com/superfly/ltx) library and the litestream
|
|
473
|
+
documentation (`docs/ARCHITECTURE.md`, `docs/SQLITE_INTERNALS.md`, `docs/LTX_FORMAT.md`,
|
|
474
|
+
`docs/PATTERNS.md`), which are adapted here with attribution.
|
|
475
|
+
|
|
476
|
+
The SQLite **WAL** and **LTX** formats are replicated faithfully so that this library can read LTX
|
|
477
|
+
files produced by Litestream and vice-versa.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## License
|
|
482
|
+
|
|
483
|
+
Apache License 2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/backends/file/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backend Leaser.
|
|
3
|
+
*
|
|
4
|
+
* Stores the lease as `<path>/lock.json` and uses an exclusive create + a
|
|
5
|
+
* content hash as the conditional-write token. The file backend is primarily
|
|
6
|
+
* for local/single-host use; for real multi-host safety use the S3 leaser
|
|
7
|
+
* (which uses S3 `If-None-Match`/`If-Match` conditional writes).
|
|
8
|
+
*/
|
|
9
|
+
import { Lease, type Leaser } from '../../leaser.js';
|
|
10
|
+
/** FileLeaser implements {@link Leaser} for a local directory. */
|
|
11
|
+
export declare class FileLeaser implements Leaser {
|
|
12
|
+
private lockPath;
|
|
13
|
+
private ttlMs;
|
|
14
|
+
constructor(path: string, lockName?: string, ttlMs?: number);
|
|
15
|
+
acquireLease(): Promise<Lease>;
|
|
16
|
+
renewLease(lease: Lease): Promise<Lease>;
|
|
17
|
+
releaseLease(lease: Lease): Promise<void>;
|
|
18
|
+
private owner;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=leaser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leaser.d.ts","sourceRoot":"","sources":["../../../src/backends/file/leaser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAIL,KAAK,EAEL,KAAK,MAAM,EACZ,MAAM,iBAAiB,CAAC;AA4CzB,kEAAkE;AAClE,qBAAa,UAAW,YAAW,MAAM;IACvC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,KAAK,CAAS;gBAEV,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAmB,EAAE,KAAK,SAAkB;IAKxE,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC;IA6C9B,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAexC,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/C,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backend Leaser.
|
|
3
|
+
*
|
|
4
|
+
* Stores the lease as `<path>/lock.json` and uses an exclusive create + a
|
|
5
|
+
* content hash as the conditional-write token. The file backend is primarily
|
|
6
|
+
* for local/single-host use; for real multi-host safety use the S3 leaser
|
|
7
|
+
* (which uses S3 `If-None-Match`/`If-Match` conditional writes).
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { link, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
11
|
+
import nodePath from 'node:path';
|
|
12
|
+
import { DefaultLeasePath, DefaultLeaseTTL, ErrLeaseNotHeld, Lease, LeaseExistsError, } from "../../leaser.js";
|
|
13
|
+
import { FileReplicaError } from "../../errors.js";
|
|
14
|
+
// Disambiguates temp files written in the same millisecond by this process.
|
|
15
|
+
let tmpSeq = 0;
|
|
16
|
+
function sha256(data) {
|
|
17
|
+
return createHash('sha256').update(data).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
function encodeLease(l) {
|
|
20
|
+
return new TextEncoder().encode(JSON.stringify({
|
|
21
|
+
generation: l.generation,
|
|
22
|
+
expires_at: new Date(l.expiresAt).toISOString(),
|
|
23
|
+
owner: l.owner,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
function decodeLease(data, etag) {
|
|
27
|
+
const parsed = JSON.parse(new TextDecoder().decode(data));
|
|
28
|
+
const expiresAt = Date.parse(parsed.expires_at ?? '');
|
|
29
|
+
if (Number.isNaN(expiresAt)) {
|
|
30
|
+
throw new FileReplicaError('invalid lease file: bad expires_at');
|
|
31
|
+
}
|
|
32
|
+
return new Lease(parsed.generation ?? 1, expiresAt, parsed.owner ?? '', etag);
|
|
33
|
+
}
|
|
34
|
+
/** Reads and decodes the lease file, returning null if it does not exist. */
|
|
35
|
+
async function readLease(lockPath) {
|
|
36
|
+
try {
|
|
37
|
+
const data = await readFile(lockPath);
|
|
38
|
+
return decodeLease(data, sha256(data));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.code === 'ENOENT')
|
|
42
|
+
return null;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** FileLeaser implements {@link Leaser} for a local directory. */
|
|
47
|
+
export class FileLeaser {
|
|
48
|
+
lockPath;
|
|
49
|
+
ttlMs;
|
|
50
|
+
constructor(path, lockName = DefaultLeasePath, ttlMs = DefaultLeaseTTL) {
|
|
51
|
+
this.lockPath = nodePath.join(path, lockName);
|
|
52
|
+
this.ttlMs = ttlMs;
|
|
53
|
+
}
|
|
54
|
+
async acquireLease() {
|
|
55
|
+
const existing = await readLease(this.lockPath);
|
|
56
|
+
if (existing !== null && !existing.isExpired()) {
|
|
57
|
+
throw new LeaseExistsError(existing.owner, existing.expiresAt);
|
|
58
|
+
}
|
|
59
|
+
let generation = 1;
|
|
60
|
+
if (existing !== null)
|
|
61
|
+
generation = existing.generation + 1;
|
|
62
|
+
const lease = new Lease(generation, Date.now() + this.ttlMs, this.owner());
|
|
63
|
+
const bytes = encodeLease(lease);
|
|
64
|
+
if (existing === null) {
|
|
65
|
+
// Exclusive create so we do not clobber a concurrent acquirer. Write a
|
|
66
|
+
// temp file and hard-link it into place so the lock appears atomically
|
|
67
|
+
// with its full contents; a racing reader never sees an empty file.
|
|
68
|
+
const tmp = `${this.lockPath}.tmp-${process.pid}-${Date.now()}-${tmpSeq++}`;
|
|
69
|
+
await writeFile(tmp, bytes);
|
|
70
|
+
try {
|
|
71
|
+
await link(tmp, this.lockPath);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const code = err.code;
|
|
75
|
+
if (code === 'EEXIST') {
|
|
76
|
+
const current = await readLease(this.lockPath);
|
|
77
|
+
if (current !== null && !current.isExpired()) {
|
|
78
|
+
throw new LeaseExistsError(current.owner, current.expiresAt);
|
|
79
|
+
}
|
|
80
|
+
// The other writer lost the race; retry the expired-overwrite path.
|
|
81
|
+
return this.acquireLease();
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await rm(tmp, { force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Existing lease expired: atomically replace it.
|
|
91
|
+
const tmp = `${this.lockPath}.tmp-${process.pid}-${Date.now()}-${tmpSeq++}`;
|
|
92
|
+
await writeFile(tmp, bytes);
|
|
93
|
+
await rename(tmp, this.lockPath);
|
|
94
|
+
}
|
|
95
|
+
const written = await readLease(this.lockPath);
|
|
96
|
+
return written ?? lease;
|
|
97
|
+
}
|
|
98
|
+
async renewLease(lease) {
|
|
99
|
+
const current = await readLease(this.lockPath);
|
|
100
|
+
if (current === null)
|
|
101
|
+
throw new ErrLeaseNotHeld();
|
|
102
|
+
if (current.etag !== lease.etag)
|
|
103
|
+
throw new ErrLeaseNotHeld();
|
|
104
|
+
const renewed = new Lease(lease.generation, Date.now() + this.ttlMs, lease.owner);
|
|
105
|
+
const bytes = encodeLease(renewed);
|
|
106
|
+
const tmp = `${this.lockPath}.tmp-${process.pid}-${Date.now()}-${tmpSeq++}`;
|
|
107
|
+
await writeFile(tmp, bytes);
|
|
108
|
+
await rename(tmp, this.lockPath);
|
|
109
|
+
const written = await readLease(this.lockPath);
|
|
110
|
+
return written ?? renewed;
|
|
111
|
+
}
|
|
112
|
+
async releaseLease(lease) {
|
|
113
|
+
const current = await readLease(this.lockPath);
|
|
114
|
+
if (current === null)
|
|
115
|
+
return; // already released
|
|
116
|
+
if (current.etag !== lease.etag)
|
|
117
|
+
throw new ErrLeaseNotHeld();
|
|
118
|
+
await rm(this.lockPath, { force: true });
|
|
119
|
+
}
|
|
120
|
+
owner() {
|
|
121
|
+
return `${process.pid}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File replica client — replicates LTX files to a local directory.
|
|
3
|
+
*
|
|
4
|
+
* Layout: `<root>/ltx/<level>/<minTXID>-<maxTXID>.ltx`
|
|
5
|
+
* Writes are atomic: data is streamed to a `.tmp` file, fsync'd, then renamed,
|
|
6
|
+
* the directory is fsync'd, and the file mtime is set from the LTX header
|
|
7
|
+
* timestamp to preserve original creation time.
|
|
8
|
+
*/
|
|
9
|
+
import { Readable } from 'node:stream';
|
|
10
|
+
import { type OpenLTXFileOptions, type ReplicaClient, type WriteLTXFileOptions } from '../../replica-client.js';
|
|
11
|
+
import type { FileInfo } from '../../ltx/index.js';
|
|
12
|
+
import type { Logger } from '../../util/logger.js';
|
|
13
|
+
export declare const ReplicaClientType = "file";
|
|
14
|
+
/** FileReplicaClient writes LTX files to a local directory. */
|
|
15
|
+
export declare class FileReplicaClient implements ReplicaClient {
|
|
16
|
+
private pathVal;
|
|
17
|
+
private logger;
|
|
18
|
+
constructor(path: string);
|
|
19
|
+
type(): string;
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
setLogger(logger: Logger): void;
|
|
22
|
+
/** Returns the destination root path. */
|
|
23
|
+
path(): string;
|
|
24
|
+
ltxLevelDir(level: number): string;
|
|
25
|
+
ltxFilePath(level: number, minTXID: bigint, maxTXID: bigint): string;
|
|
26
|
+
ltxFiles(opts: {
|
|
27
|
+
level: number;
|
|
28
|
+
seek: bigint;
|
|
29
|
+
useMetadata: boolean;
|
|
30
|
+
}): AsyncIterable<FileInfo>;
|
|
31
|
+
openLtxFile(opts: OpenLTXFileOptions): Promise<Readable>;
|
|
32
|
+
writeLtxFile(opts: WriteLTXFileOptions, body: AsyncIterable<Uint8Array | string>): Promise<FileInfo>;
|
|
33
|
+
deleteLtxFiles(infos: FileInfo[]): Promise<void>;
|
|
34
|
+
deleteAll(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** NewReplicaClientFromURL creates a FileReplicaClient from a URL path. */
|
|
37
|
+
export declare function NewReplicaClientFromURL(_scheme: string, _host: string, urlPath: string, _query: URLSearchParams | null, _userinfo: string | null): ReplicaClient;
|
|
38
|
+
//# sourceMappingURL=replica-client.d.ts.map
|