Skip to content

Commit dbfc103

Browse files
committed
Convert to a pnpm monorepo and add an S3 upload strategy
Restructure the repository into a pnpm workspace while keeping the core sqlite-js-stream package at the root unchanged (same API, name, and source). Upload strategies now live as addon packages under packages/. - Add pnpm-workspace.yaml + pnpm-lock.yaml; drop package-lock.json. - Core package.json gains an 'exports' map ('.' -> ./src/index.ts) so addons can import the ReplicaClient contract, plus workspace scripts (test:all / typecheck:all). Export the Logger type so addons implement ReplicaClient. - Root tsconfig now type-checks src/, test/, and packages/. - Add packages/backend-s3: a sqlite-js-stream-backend-s3 addon implementing an S3ReplicaClient (AWS SDK v3). It stores objects as {prefix}/ltx/{level}/{min}-{max}.ltx, preserves timestamps as ltx-timestamp metadata, supports range reads, batched deletes, and registers the s3:// URL scheme. An injectable 'client' (S3ClientLike) makes it testable without AWS; tests use an in-memory fake plus a full Replica.sync() -> restore() round-trip. - CI: use pnpm (pnpm/action-setup + setup-node cache:pnpm, frozen lockfile). - Document the workspace layout and S3 addon in README and ARCHITECTURE.
1 parent 4458802 commit dbfc103

15 files changed

Lines changed: 1455 additions & 69 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,22 @@ jobs:
2121
- name: Checkout
2222
uses: actions/checkout@v7
2323

24+
- name: Setup pnpm
25+
uses: pnpm/action-setup@v6
26+
with:
27+
version: 12
28+
2429
- name: Setup Node
2530
uses: actions/setup-node@v7
2631
with:
2732
node-version: ${{ matrix.node-version }}
28-
cache: npm
33+
cache: pnpm
2934

3035
- name: Install dependencies
31-
run: npm ci
36+
run: pnpm install --frozen-lockfile
3237

3338
- name: Typecheck
34-
run: npx tsc -p . --noEmit
39+
run: pnpm typecheck:all
3540

3641
- name: Run tests
37-
run: npm test
42+
run: pnpm test:all

‎README.md‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@ No build step. Test files and source run directly under Node.
2727

2828
## Installation
2929

30+
This repository is a **pnpm workspace**. The core library lives at the root and
31+
upload strategies (replica backends) live under `packages/`:
32+
3033
```bash
31-
npm install sqlite-js-stream
34+
pnpm install # link the workspace
3235
```
3336

34-
*(Not yet published; install from the repository.)*
37+
There is no build step: we run TypeScript directly via Node's native type
38+
stripping.
39+
40+
The core package is published as `sqlite-js-stream` *(not yet published; install
41+
from the repository)*, and each addon is a separate package:
42+
43+
```bash
44+
pnpm add sqlite-js-stream sqlite-js-stream-backend-s3
45+
```
3546

3647
---
3748

@@ -117,6 +128,43 @@ const client = newReplicaClientFromURL('file:///data/replica');
117128
const replica = new Replica('/data/app.db', client);
118129
```
119130

131+
The `file` scheme is built into the core. Additional schemes (S3, etc.) are
132+
registered by importing the matching addon (see below).
133+
134+
### S3 upload strategy
135+
136+
The `sqlite-js-stream-backend-s3` addon provides an S3 replica client. Import it
137+
once to register the `s3://` scheme, then create clients from URLs or directly:
138+
139+
```ts
140+
import { Replica, newReplicaClientFromURL } from 'sqlite-js-stream';
141+
import { S3ReplicaClient } from 'sqlite-js-stream-backend-s3';
142+
143+
// Via a URL (bucket from the host, prefix from the path).
144+
const client = newReplicaClientFromURL('s3://my-bucket/backup?region=us-east-1');
145+
146+
// Or directly, with explicit AWS config.
147+
const s3 = new S3ReplicaClient({
148+
bucket: 'my-bucket',
149+
prefix: 'backup',
150+
region: 'us-east-1',
151+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
152+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
153+
});
154+
155+
const replica = new Replica('/data/app.db', client);
156+
await replica.run({ intervalMs: 1000 });
157+
```
158+
159+
Objects are stored as `{prefix}/ltx/{level}/{minTXID}-{maxTXID}.ltx` — the same
160+
layout as the file backend, so a replica can move between strategies. Creation
161+
timestamps are preserved as the `ltx-timestamp` object metadata.
162+
163+
> **Testing without AWS:** `S3ReplicaClient` accepts an injected `client`
164+
> implementing `send(command)`. Pass an in-memory fake (see
165+
> `packages/backend-s3/test/helpers/memory-s3.ts`) to exercise the addon without
166+
> network access.
167+
120168
### Continuous sync
121169

122170
For ongoing replication, `replica.run()` syncs immediately and then repeats on an

‎docs/ARCHITECTURE.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ prevents the WAL from being checkpointed past our position.
3636
| **WAL module** (`src/wal/`) | Parse SQLite WAL header/frames; verify salt + checksum; produce `pageMap()` (latest committed page offsets). |
3737
| **LocalDatabase** (`src/db.ts`, internal) | Reads a database + `-wal` directly from disk, builds LTX files (base snapshot, incremental L0, level-9 snapshot), and tracks the local position. It holds a short-lived **read-only** `node:sqlite` read transaction to **pin the WAL snapshot** (SQLite won't checkpoint/truncate past an active reader), then reads the files via `fs`. It is an internal implementation detail, not part of the public API. |
3838
| **Replica** (`src/replica.ts`) | Upload level-0/level-9 LTX files to a client; track the replicated position; `restore()`, `ensureExists()`, `syncStatus()`. |
39-
| **ReplicaClient** (`src/replica-client.ts`) | An interface to a storage backend (`LTXFiles`, `OpenLTXFile`, `WriteLTXFile`, `DeleteLTXFiles`, `DeleteAll`). |
39+
| **ReplicaClient** (`src/replica-client.ts`) | An interface to a storage backend (`LTXFiles`, `OpenLTXFile`, `WriteLTXFile`, `DeleteLTXFiles`, `DeleteAll`). Addons implement this to add upload strategies. |
4040
| **File backend** (`src/backends/file/`) | The simplest backend: store LTX files in `<root>/ltx/<level>/<min>-<max>.ltx`, atomic writes. |
4141
| **Store** (`src/store.ts`) | Coordinate multiple `Replica`s. |
42+
| **Addons** (`packages/`) | Upload strategies live as separate workspace packages. Each addon depends on the core `sqlite-js-stream` and registers a URL scheme (e.g. `packages/backend-s3` registers `s3://`). |
43+
44+
> The repository is a **pnpm workspace**: the core package is at the root and
45+
> upload-strategy addons live under `packages/*`. Addons import the `ReplicaClient`
46+
> contract (and `registerReplicaClientFactory`) from the core.
4247
4348
---
4449

‎package-lock.json‎

Lines changed: 0 additions & 60 deletions
This file was deleted.

‎package.json‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"description": "Node.js port of Litestream: SQLite streaming replication & disaster recovery via LTX files",
55
"type": "module",
6+
"packageManager": "pnpm@12.4.2",
67
"engines": {
78
"node": ">=24"
89
},
@@ -16,9 +17,14 @@
1617
"bugs": {
1718
"url": "https://github.com/platformatic/sqlite-js-stream/issues"
1819
},
20+
"exports": {
21+
".": "./src/index.ts"
22+
},
1923
"scripts": {
2024
"test": "node --test 'test/**/*.test.ts'",
21-
"typecheck": "tsc -p . --noEmit"
25+
"typecheck": "tsc -p . --noEmit",
26+
"test:all": "pnpm test && pnpm -r test",
27+
"typecheck:all": "pnpm typecheck"
2228
},
2329
"dependencies": {
2430
"lz4js": "^0.2.0"

‎packages/backend-s3/package.json‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "sqlite-js-stream-backend-s3",
3+
"version": "0.1.0",
4+
"description": "Amazon S3 upload strategy (replica client) for sqlite-js-stream",
5+
"type": "module",
6+
"engines": {
7+
"node": ">=24"
8+
},
9+
"license": "Apache-2.0",
10+
"exports": {
11+
".": "./src/index.ts"
12+
},
13+
"scripts": {
14+
"test": "node --test 'test/**/*.test.ts'"
15+
},
16+
"dependencies": {
17+
"@aws-sdk/client-s3": "^3.1135.0",
18+
"sqlite-js-stream": "workspace:*"
19+
},
20+
"devDependencies": {
21+
"@types/node": "^24.0.0",
22+
"typescript": "^5.7.0"
23+
}
24+
}

‎packages/backend-s3/src/index.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* sqlite-js-stream-backend-s3 — Amazon S3 upload strategy.
3+
*
4+
* Importing this module registers the `s3` scheme with the core's replica URL
5+
* factory, so `newReplicaClientFromURL('s3://bucket/prefix')` resolves after
6+
* this addon is loaded.
7+
*/
8+
9+
export {
10+
S3ReplicaClient,
11+
newReplicaClientFromURL,
12+
ReplicaClientType,
13+
type S3ClientLike,
14+
type S3ReplicaClientOptions,
15+
} from './replica-client.ts';
16+
17+
import { registerReplicaClientFactory } from 'sqlite-js-stream';
18+
import { newReplicaClientFromURL } from './replica-client.ts';
19+
20+
// Register the 's3' URL scheme when the addon is imported.
21+
registerReplicaClientFactory('s3', newReplicaClientFromURL);

0 commit comments

Comments
 (0)