@sproutboat/runtime 0.21.1 → 0.22.0
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 +12 -0
- package/LTX_REPLICATION.md +124 -0
- package/README.md +7 -0
- package/package.json +2 -2
- package/src/transport-embedded.js +1077 -4
- package/test/native-ltx.ts +359 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3f95234: Add experimental bounded local LTX replication to the embedded SQLite transport.
|
|
8
|
+
Opt in with compatibility date 2026-10-22 and SB_LTX_ENABLE=1. A single writer
|
|
9
|
+
owns the database, commits are captured durably before checkpoint, and the WAL
|
|
10
|
+
has an enforced byte ceiling. Default behavior and the broker are unchanged.
|
|
11
|
+
Verified clean-checkpoint restarts reuse their generation without rewriting a base.
|
|
12
|
+
|
|
13
|
+
Developed with AI assistance from OpenAI Codex (GPT-6.1).
|
|
14
|
+
|
|
3
15
|
## 0.21.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Experimental local LTX replication
|
|
2
|
+
|
|
3
|
+
AI assistance: OpenAI Codex (GPT-6.1).
|
|
4
|
+
|
|
5
|
+
This feature is for an embedded D1 database owned by one native executable.
|
|
6
|
+
Enable it with compatibility date `2026-10-22` or later, `SB_LTX_ENABLE=1`,
|
|
7
|
+
and `SB_LTX_DATABASE=DB` (the exact D1 binding name). Earlier compatibility dates,
|
|
8
|
+
unset flags, other bindings and the broker keep their existing behavior.
|
|
9
|
+
No Node or Bun runtime is needed by the executable.
|
|
10
|
+
|
|
11
|
+
## Capture and bounds
|
|
12
|
+
|
|
13
|
+
SQLite uses WAL, synchronous FULL, no auto-checkpoint, and exclusive locking.
|
|
14
|
+
An advisory lock also rejects another copy of the executable using the same
|
|
15
|
+
path. Other SQLite connections, including readers, are excluded while it runs.
|
|
16
|
+
Do not use this feature with the SO_REUSEPORT multi-process deployment recipe.
|
|
17
|
+
The raw page-read descriptor stays open for the SQLite connection's lifetime:
|
|
18
|
+
closing any descriptor for the database inode would release process-level POSIX
|
|
19
|
+
locks, even if SQLite's own descriptor remained open.
|
|
20
|
+
|
|
21
|
+
The commit hook validates the WAL header, salts and rolling checksum through the
|
|
22
|
+
commit frame. It sorts a bounded array of `(pgno, frame)` entries and emits the
|
|
23
|
+
newest version of each page in ascending order. The prior database checksum is
|
|
24
|
+
updated using the old page version from the main file or the captured WAL prefix.
|
|
25
|
+
Truncation removes the checksums of discarded pages. No full database buffer or
|
|
26
|
+
in-memory page-index map is retained.
|
|
27
|
+
|
|
28
|
+
Each commit creates one LTX v3 file (magic `LTX1`), with a single logical TXID,
|
|
29
|
+
pre-apply checksum, committed database size, salts, and WAL frame range. TXIDs
|
|
30
|
+
start at one inside each generation; they are backup sequence numbers, not an
|
|
31
|
+
application transaction ID. The base contains exact checkpointed live pages
|
|
32
|
+
under a read transaction. It does not use VACUUM INTO. Page compression currently
|
|
33
|
+
uses interoperable literal-only LZ4 blocks; these are fast and have bounded memory,
|
|
34
|
+
but do not reduce file size. The prototype's JS word-based LZ4 compressor is a
|
|
35
|
+
separate path, not a claim that this C writer compresses data.
|
|
36
|
+
|
|
37
|
+
`SB_LTX_MAX_WAL_BYTES` defaults to 16 MiB, with an allowed range of 128 KiB to
|
|
38
|
+
1 GiB. A VFS wrapper rejects a WAL write or truncate that would cross the ceiling
|
|
39
|
+
with SQLITE_FULL, including inside a single huge transaction. That transaction
|
|
40
|
+
rolls back. Reduce transaction size to fit the ceiling; this implementation does
|
|
41
|
+
not split a transaction. Frame metadata is bounded by that same ceiling, about
|
|
42
|
+
8 bytes per WAL frame. SQLite's normal page cache remains capped at 2 MiB.
|
|
43
|
+
|
|
44
|
+
After each statement resets, checkpoint TRUNCATE runs. Multi-statement `exec`
|
|
45
|
+
steps and finalizes each statement separately under this opt-in, so every commit
|
|
46
|
+
gets a checkpoint opportunity. Inside an explicit open transaction, BUSY/LOCKED
|
|
47
|
+
can defer checkpointing; the VFS ceiling still applies. SQL that changes the WAL,
|
|
48
|
+
locking, synchronization or page-size invariants, or attaches another database,
|
|
49
|
+
is rejected by an authorizer. `DB.backup()` uses VACUUM INTO and is not supported
|
|
50
|
+
for this opted-in binding. Use the base LTX file for replication.
|
|
51
|
+
|
|
52
|
+
## Files and durability
|
|
53
|
+
|
|
54
|
+
Files live under `<database-path>.ltx/epoch-XXXXXX/`. Completed names are
|
|
55
|
+
`<minTXID as 16 hex digits>-<maxTXID as 16 hex digits>.ltx`.
|
|
56
|
+
The encoder streams one page at a time and spools index records with `tmpfile()`.
|
|
57
|
+
|
|
58
|
+
For every LTX file:
|
|
59
|
+
|
|
60
|
+
1. Write a `.tmp` file, flush it and fsync it.
|
|
61
|
+
2. Publish the completed filename with an atomic, no-replace hard link, unlink
|
|
62
|
+
the temporary name, and fsync the generation directory. A finished file is
|
|
63
|
+
never replaced. This has rename-style publication with no overwrite race.
|
|
64
|
+
3. Write a versioned, checksummed position record to `position.tmp`, fsync it,
|
|
65
|
+
rename it to `position`, and fsync the root directory. The record includes
|
|
66
|
+
generation, TXID, salts, next WAL byte offset, database checksum, WAL checksum
|
|
67
|
+
state, page size and committed page count.
|
|
68
|
+
4. Only then checkpoint the source database. After checkpoint TRUNCATE succeeds,
|
|
69
|
+
atomically persist a second position record marking the database checkpointed.
|
|
70
|
+
The same file/directory fsync sequence applies to that record. A crash between
|
|
71
|
+
checkpoint and publication of the marker retains the conservative fallback.
|
|
72
|
+
|
|
73
|
+
Capture, fsync and publication failures latch the database closed to subsequent
|
|
74
|
+
operations until restart. The SQL commit has already happened when a hook error
|
|
75
|
+
is returned. A failed request can therefore represent a committed transaction;
|
|
76
|
+
applications must use idempotent transaction identifiers for retries.
|
|
77
|
+
|
|
78
|
+
## Qualification status
|
|
79
|
+
|
|
80
|
+
Capture is tested against the exact inline C through `bun run test:native-ltx`,
|
|
81
|
+
including independent BigInt CRC and index verification, a 102-file chain,
|
|
82
|
+
byte-exact restore, integrity checking, oversized transaction rollback, database truncation, explicit rollback and late position-write failure latching.
|
|
83
|
+
The workspace prototype additionally tests the compiled Porffor executable,
|
|
84
|
+
including external writer exclusion and invariant PRAGMA rejection.
|
|
85
|
+
Restart validation now checks the position CRC, completed LTX checksum, WAL
|
|
86
|
+
header/salts, commit frame and cumulative frame checksum. A matching prefix
|
|
87
|
+
resumes in the same generation; any later complete commits are captured before
|
|
88
|
+
serving. Incomplete/torn trailing WAL frames are excluded. An orphan finished
|
|
89
|
+
file ahead of position forces a new generation, preserving the old files.
|
|
90
|
+
Missing, truncated, reset or mismatched WAL takes a fresh page-exact base when
|
|
91
|
+
it contradicts the recorded state. Position format LTXPOS2 distinguishes W
|
|
92
|
+
(captured WAL prefix) from C (checkpoint complete, expected absent/empty WAL).
|
|
93
|
+
A C record also requires a valid completed LTX, unchanged file size and matching
|
|
94
|
+
full database page checksum before the generation is reused. Clean restarts do
|
|
95
|
+
not create a base or rewrite the position. Any unexpected nonempty WAL, offline
|
|
96
|
+
database modification or record mismatch retains the fresh-base fallback.
|
|
97
|
+
Older LTXPOS1 records are conservatively replaced by a new base on first start.
|
|
98
|
+
All state and file descriptors are still created lazily on first DB open.
|
|
99
|
+
|
|
100
|
+
Verification is a bounded streaming read, but it is O(database size), not constant
|
|
101
|
+
time. It also checks the last LTX file. Five native restarts each at 4.2 MB and
|
|
102
|
+
51.1 MB retained the same generation and unchanged base inode/mtime and position.
|
|
103
|
+
The 51.1 MB verification took 399 to 446 ms on macOS arm64 (plus about 13 ms to
|
|
104
|
+
health readiness); this is not a 9 ms cold-start claim for large databases.
|
|
105
|
+
|
|
106
|
+
The regression SIGKILLs at ten durability boundaries, including both sides of
|
|
107
|
+
clean-marker publication, and verifies matching-prefix resume and fallback cases.
|
|
108
|
+
It also recovers a committed tail beyond the durable cursor. The full Porffor executable passed 200 seeded random SIGKILL iterations on macOS
|
|
109
|
+
arm64: 1,675 acknowledged writes retained, 88 same-generation resumes and 112
|
|
110
|
+
fresh-base recoveries. Every finished chain passed file/database CRC verification,
|
|
111
|
+
integrity_check, deterministic source comparison at the last durable TXID, and
|
|
112
|
+
comparison after restart. The test also recovered commits whose HTTP response
|
|
113
|
+
was interrupted. Evidence and the runnable harness are in the linked prototype
|
|
114
|
+
workspace; the self-contained durability-boundary regression runs in this repo's
|
|
115
|
+
native CI. These are process-crash tests, not power-cut or storage-failure tests.
|
|
116
|
+
Linux CI exercises the same C implementation; a randomized full executable Linux
|
|
117
|
+
campaign and a power-loss campaign are not yet qualified.
|
|
118
|
+
|
|
119
|
+
No uploader is included. Finished local files are retained without a retention
|
|
120
|
+
policy; disk usage of the backup chain is not bounded by the WAL ceiling.
|
|
121
|
+
|
|
122
|
+
Format and API references: [SQLite WAL](https://www.sqlite.org/fileformat2.html#walformat),
|
|
123
|
+
[commit hook](https://www.sqlite.org/c3ref/wal_hook.html),
|
|
124
|
+
[checkpoint](https://www.sqlite.org/c3ref/wal_checkpoint_v2.html).
|
package/README.md
CHANGED
|
@@ -78,3 +78,10 @@ generation with its matching size and etag.
|
|
|
78
78
|
Unfinished uploads expire after seven days. The broker removes expired parts
|
|
79
79
|
when another multipart upload starts; a standalone binary removes them when it
|
|
80
80
|
initializes its embedded store.
|
|
81
|
+
|
|
82
|
+
## Experimental SQLite replication
|
|
83
|
+
|
|
84
|
+
The embedded transport can capture bounded, durable local LTX files for one D1
|
|
85
|
+
binding. It requires explicit opt-in and compatibility date 2026-10-22 or later.
|
|
86
|
+
See [LTX replication](LTX_REPLICATION.md) for bounds, recovery, file durability,
|
|
87
|
+
qualification evidence and current limitations. No remote uploader is included.
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutboat/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"repository": {
|
|
7
6
|
"type": "git",
|
|
8
7
|
"url": "git+https://github.com/baronunread/sproutboat-packages.git",
|
|
9
8
|
"directory": "packages/runtime"
|
|
10
9
|
},
|
|
10
|
+
"type": "module",
|
|
11
11
|
"main": "./src/index.ts",
|
|
12
12
|
"types": "./src/index.ts",
|
|
13
13
|
"scripts": {
|