orez 0.1.5 → 0.1.7

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.
Files changed (58) hide show
  1. package/README.md +185 -225
  2. package/dist/admin/log-store.d.ts.map +1 -1
  3. package/dist/admin/log-store.js +17 -6
  4. package/dist/admin/log-store.js.map +1 -1
  5. package/dist/admin/server.d.ts +1 -0
  6. package/dist/admin/server.d.ts.map +1 -1
  7. package/dist/admin/server.js +10 -0
  8. package/dist/admin/server.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +89 -45
  11. package/dist/cli.js.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +164 -35
  15. package/dist/index.js.map +1 -1
  16. package/dist/integration/test-permissions.d.ts +5 -0
  17. package/dist/integration/test-permissions.d.ts.map +1 -0
  18. package/dist/integration/test-permissions.js +89 -0
  19. package/dist/integration/test-permissions.js.map +1 -0
  20. package/dist/pg-proxy.js +2 -2
  21. package/dist/pg-proxy.js.map +1 -1
  22. package/dist/replication/change-tracker.d.ts.map +1 -1
  23. package/dist/replication/change-tracker.js +15 -13
  24. package/dist/replication/change-tracker.js.map +1 -1
  25. package/dist/replication/handler.d.ts.map +1 -1
  26. package/dist/replication/handler.js +27 -2
  27. package/dist/replication/handler.js.map +1 -1
  28. package/dist/sqlite-mode/index.d.ts +1 -0
  29. package/dist/sqlite-mode/index.d.ts.map +1 -1
  30. package/dist/sqlite-mode/index.js +1 -0
  31. package/dist/sqlite-mode/index.js.map +1 -1
  32. package/dist/sqlite-mode/native-binary.d.ts +11 -0
  33. package/dist/sqlite-mode/native-binary.d.ts.map +1 -0
  34. package/dist/sqlite-mode/native-binary.js +67 -0
  35. package/dist/sqlite-mode/native-binary.js.map +1 -0
  36. package/dist/sqlite-mode/types.d.ts.map +1 -1
  37. package/dist/sqlite-mode/types.js +3 -4
  38. package/dist/sqlite-mode/types.js.map +1 -1
  39. package/package.json +8 -2
  40. package/src/admin/log-store.ts +19 -9
  41. package/src/admin/server.ts +12 -0
  42. package/src/cli.ts +92 -43
  43. package/src/index.ts +186 -37
  44. package/src/integration/integration.test.ts +86 -15
  45. package/src/integration/native-binary.guard.test.ts +13 -0
  46. package/src/integration/native-startup.test.ts +44 -0
  47. package/src/integration/restore-live-stress.test.ts +437 -0
  48. package/src/integration/restore-reset.test.ts +135 -16
  49. package/src/integration/test-permissions.ts +111 -0
  50. package/src/pg-proxy.ts +2 -2
  51. package/src/replication/change-tracker.test.ts +1 -1
  52. package/src/replication/change-tracker.ts +16 -13
  53. package/src/replication/handler.test.ts +2 -2
  54. package/src/replication/handler.ts +30 -2
  55. package/src/sqlite-mode/index.ts +1 -0
  56. package/src/sqlite-mode/native-binary.ts +89 -0
  57. package/src/sqlite-mode/sqlite-mode.test.ts +7 -9
  58. package/src/sqlite-mode/types.ts +3 -4
package/README.md CHANGED
@@ -1,8 +1,30 @@
1
1
  # oreZ
2
2
 
3
- [Zero](https://zero.rocicorp.dev) is amazing, but getting started can take a lot - setting up Postgres, approving native SQLite, and then configuring the two to work together.
3
+ [![npm version](https://img.shields.io/npm/v/orez.svg)](https://www.npmjs.com/package/orez)
4
+ [![license](https://img.shields.io/npm/l/orez.svg)](https://github.com/natew/orez/blob/main/LICENSE)
4
5
 
5
- oreZ is an experiment at making Zero work on [PGlite](https://pglite.dev) and SQLite-wasm, and then packing the two together so running them is as simple as possible. It's intended as a dev-mode tool, with a CLI, programmatic API, and Vite plugin.
6
+ Run [Zero](https://zero.rocicorp.dev) locally with zero native dependencies. No Postgres install, no SQLite compilation, no Docker.
7
+
8
+ ```
9
+ bunx orez
10
+ ```
11
+
12
+ oreZ makes Zero work on [PGlite](https://pglite.dev) (Postgres in WASM) and [bedrock-sqlite](https://www.npmjs.com/package/bedrock-sqlite) (SQLite in WASM), bundled together so local development is as simple as `bun install && bunx orez`.
13
+
14
+ ## Requirements
15
+
16
+ - **Bun** 1.0+ or **Node.js** 20+
17
+ - **Zero** 0.18+ (tested with 0.18.x)
18
+
19
+ ## Limitations
20
+
21
+ This is a **development tool only**. Not suitable for production.
22
+
23
+ - **Single-session per database** — queries are serialized through a mutex. Fine for development, would bottleneck under load.
24
+ - **Trigger overhead** — every write fires change-tracking triggers.
25
+ - **Local filesystem** — no replication, no HA. Use `orez pg_dump` for backups.
26
+
27
+ ## Features
6
28
 
7
29
  ```
8
30
  bunx orez
@@ -10,13 +32,13 @@ bunx orez
10
32
 
11
33
  **What oreZ handles automatically:**
12
34
 
13
- - **Memory management** — auto-sizes Node heap based on system RAM, purges consumed WAL, batches restores with CHECKPOINTs to prevent WASM OOM
14
- - **Real-time replication** — changes sync instantly via pg_notify triggers, with adaptive polling as fallback; tracks configured/public tables and shard `clients` tables
15
- - **Auto-recovery** — finds available ports if configured ones are busy and provides reset/restart controls for zero-cache state
16
- - **PGlite compatibility** — rewrites unsupported queries, fakes wire protocol responses, filters unsupported column types, cleans session state between connections
17
- - **Admin dashboard** — live zero-cache logs, restart/reset controls, connection info (enabled by default, `--disable-admin` to turn off)
18
- - **Production restores** — `pg_dump`/`pg_restore` with COPY→INSERT conversion, skips unsupported extensions, handles oversized rows, auto-restarts zero-cache
19
- - **Zero-cache workarounds** — fixes concurrent COPY bug, disables query planner (WASM infinite loop), respects publication filtering
35
+ - **Zero native deps** — both Postgres and SQLite run as WASM. Nothing to compile, nothing platform-specific.
36
+ - **Memory management** — auto-sizes Node heap (~50% RAM, min 4GB), purges consumed WAL, batches restores with CHECKPOINTs
37
+ - **Real-time replication** — changes sync instantly via `pg_notify` triggers, with adaptive polling fallback (20ms catching up, 500ms idle)
38
+ - **Auto-recovery** — finds available ports if configured ones are busy, provides reset/restart controls
39
+ - **PGlite compatibility** — rewrites unsupported queries, fakes wire protocol responses, filters unsupported column types
40
+ - **Admin dashboard** — live logs, HTTP request inspector, restart/reset controls, env viewer
41
+ - **Production restores** — `pg_dump`/`pg_restore` with COPY→INSERT conversion, auto-coordinates with zero-cache
20
42
  - **Extensions** — pgvector and pg_trgm enabled by default
21
43
 
22
44
  ## CLI
@@ -37,32 +59,31 @@ bunx orez
37
59
  --log-level=warn error, warn, info, debug
38
60
  --s3 also start a local s3-compatible server
39
61
  --s3-port=9200 s3 server port
40
- --disable-wasm-sqlite use native @rocicorp/zero-sqlite3 instead of wasm bedrock-sqlite
41
- --on-db-ready=CMD command to run after db+proxy are ready, before zero-cache starts
42
- --on-healthy=CMD command to run once all services are healthy
43
- --disable-admin disable admin dashboard (enabled by default)
44
- --admin-port=6477 admin dashboard port
62
+ --disable-wasm-sqlite use native @rocicorp/zero-sqlite3 instead of wasm
63
+ --on-db-ready=CMD command to run after db+proxy ready, before zero-cache
64
+ --on-healthy=CMD command to run once all services healthy
65
+ --disable-admin disable admin dashboard
66
+ --admin-port=6477 admin dashboard port (default: 6477)
45
67
  ```
46
68
 
47
69
  Ports auto-increment if already in use.
48
70
 
49
71
  ## Admin Dashboard
50
72
 
51
- The admin dashboard is enabled by default on port 6477. To disable it:
52
-
53
- ```
54
- bunx orez --disable-admin
55
- ```
56
-
57
- Open `http://localhost:6477` for a real-time dashboard with:
73
+ Enabled by default at `http://localhost:6477`.
58
74
 
59
75
  - **Logs** — live-streaming logs from zero-cache, filterable by source and level
76
+ - **HTTP** — request/response inspector for zero-cache traffic
60
77
  - **Env** — environment variables passed to zero-cache
61
- - **Actions** — restart zero-cache, reset (wipe replica + resync), clear logs
78
+ - **Actions** — restart zero-cache, reset (wipe replica + resync), full reset (wipe CVR/CDB too)
62
79
 
63
- The dashboard polls every second for new logs and updates uptime/port status every 5 seconds.
80
+ Logs are also written to separate files in your data directory: `zero.log`, `proxy.log`, `pglite.log`, etc.
64
81
 
65
- ## Programmatic
82
+ ```
83
+ bunx orez --disable-admin # disable dashboard
84
+ ```
85
+
86
+ ## Programmatic API
66
87
 
67
88
  ```
68
89
  bun install orez
@@ -76,6 +97,7 @@ const { config, stop, db, instances } = await startZeroLite({
76
97
  zeroPort: 5849,
77
98
  migrationsDir: 'src/database/migrations',
78
99
  seedFile: 'src/database/seed.sql',
100
+ adminPort: 6477, // set to 0 to disable
79
101
  })
80
102
 
81
103
  // your app connects to zero-cache at localhost:5849
@@ -84,11 +106,10 @@ const { config, stop, db, instances } = await startZeroLite({
84
106
  // db is the postgres PGlite instance (for direct queries)
85
107
  // instances has all three: { postgres, cvr, cdb }
86
108
 
87
- // when done
88
109
  await stop()
89
110
  ```
90
111
 
91
- All options are optional with sensible defaults. Ports auto-find if in use.
112
+ All options are optional with sensible defaults.
92
113
 
93
114
  ### Lifecycle hooks
94
115
 
@@ -97,9 +118,9 @@ All options are optional with sensible defaults. Ports auto-find if in use.
97
118
  | on-db-ready | `--on-db-ready=CMD` | `onDbReady: 'CMD'` or `onDbReady: fn` | after db + proxy ready, before zero |
98
119
  | on-healthy | `--on-healthy=CMD` | `onHealthy: 'CMD'` or `onHealthy: fn` | after all services ready |
99
120
 
100
- Hooks can be shell command strings (CLI) or callback functions (programmatic). Shell commands receive env vars: `DATABASE_URL`, `OREZ_PG_PORT`, `OREZ_ZERO_PORT`. Change tracking triggers are re-installed after `onDbReady`, so tables created by hooks are tracked.
121
+ Shell commands receive env vars: `DATABASE_URL`, `OREZ_PG_PORT`, `OREZ_ZERO_PORT`. Change tracking triggers are re-installed after `onDbReady`.
101
122
 
102
- ## Vite plugin
123
+ ## Vite Plugin
103
124
 
104
125
  ```typescript
105
126
  import { orezPlugin } from 'orez/vite'
@@ -110,7 +131,6 @@ export default {
110
131
  pgPort: 6434,
111
132
  zeroPort: 5849,
112
133
  migrationsDir: 'src/database/migrations',
113
- // lifecycle hooks (optional)
114
134
  onDbReady: () => console.log('db ready'),
115
135
  onHealthy: () => console.log('all services healthy'),
116
136
  }),
@@ -118,280 +138,220 @@ export default {
118
138
  }
119
139
  ```
120
140
 
121
- Starts oreZ when vite dev server starts, stops on close. Supports all `startZeroLite` options plus `s3` and `s3Port` for local S3.
122
-
123
- ## How it works
124
-
125
- oreZ starts three things:
141
+ Starts oreZ when vite dev starts, stops on close. Supports all `startZeroLite` options plus `s3` and `s3Port`.
126
142
 
127
- 1. Three PGlite instances (full PostgreSQL 16 running in-process via WASM) — one for each database zero-cache expects (upstream, CVR, change)
128
- 2. A TCP proxy that speaks the PostgreSQL wire protocol, routing connections to the correct PGlite instance and handling logical replication
129
- 3. A zero-cache child process that connects to the proxy thinking it's a real Postgres server
130
-
131
- ### Multi-instance architecture
132
-
133
- zero-cache expects three separate databases: `postgres` (app data), `zero_cvr` (client view records), and `zero_cdb` (change-streamer state). In real PostgreSQL these are independent databases with separate connection pools and transaction contexts.
143
+ ## Backup & Restore
134
144
 
135
- oreZ creates a separate PGlite instance for each database, each with its own data directory and mutex. This is critical because PGlite is single-session all proxy connections to the same instance share one session. Without isolation, transactions on the CVR database get corrupted by queries on the postgres database (zero-cache's view-syncer detects this as `ConcurrentModificationException` and crashes). Separate instances eliminate cross-database interference entirely.
145
+ Dump and restore your local database — no native Postgres install needed.
136
146
 
137
- The proxy routes connections based on the database name in the startup message:
147
+ ```bash
148
+ bunx orez pg_dump > backup.sql
149
+ bunx orez pg_dump --output backup.sql
150
+ bunx orez pg_restore backup.sql
151
+ bunx orez pg_restore backup.sql --clean # drop public schema first
152
+ ```
138
153
 
139
- | Connection database | PGlite instance | Data directory |
140
- | ------------------- | --------------- | ----------------- |
141
- | `postgres` | postgres | `pgdata-postgres` |
142
- | `zero_cvr` | cvr | `pgdata-cvr` |
143
- | `zero_cdb` | cdb | `pgdata-cdb` |
154
+ ### Restoring into a running instance
144
155
 
145
- Each instance has its own mutex for serializing queries. Extensions (pgvector, pg_trgm) and app migrations only run on the postgres instance.
156
+ When oreZ is running, restore through the wire protocol:
146
157
 
147
- ### Replication
158
+ ```bash
159
+ bunx orez pg_restore backup.sql --pg-port 6434
160
+ ```
148
161
 
149
- zero-cache needs logical replication to stay in sync with the upstream database. PGlite doesn't support logical replication natively, so oreZ fakes it. Every mutation is captured by triggers into a changes table, then encoded into the pgoutput binary protocol and streamed to zero-cache through the replication connection. zero-cache can't tell the difference.
162
+ This automatically:
150
163
 
151
- Change notifications are **real-time via pg_notify** — triggers fire a notification on every write, waking the replication handler immediately. Polling is only a fallback for edge cases (e.g., bulk restores that bypass triggers). Fallback polling is adaptive: 20ms when catching up, 500ms when idle. Batch size is 2000 changes per poll. Consumed changes are purged every 10 cycles to prevent the `_zero_changes` table from growing unbounded.
164
+ 1. Stops zero-cache before restore (via admin API)
165
+ 2. Clears replication state and shard schemas
166
+ 3. Restores the dump
167
+ 4. Adds all public tables to the publication
168
+ 5. Restarts zero-cache
152
169
 
153
- Shard schemas (e.g., `chat_0`) are re-scanned periodically and change tracking is installed for shard `clients` tables.
170
+ The `--direct` flag forces direct PGlite access, skipping wire protocol.
154
171
 
155
- The replication handler also tracks shard schema tables so that `.server` promises on zero mutations resolve correctly.
172
+ ### What restore handles
156
173
 
157
- ### Zero native dependencies
174
+ - **COPY INSERT** — PGlite doesn't support COPY protocol; converted to batched multi-row INSERTs
175
+ - **Unsupported extensions** — `pg_stat_statements`, `pg_buffercache`, `pg_cron` etc. silently skipped
176
+ - **Idempotent DDL** — `CREATE SCHEMA` → `IF NOT EXISTS`, `CREATE FUNCTION` → `OR REPLACE`
177
+ - **Oversized rows** — rows >16MB skipped with warning (WASM limit)
178
+ - **Transaction batching** — 200 statements per transaction, CHECKPOINT every 3 batches
179
+ - **Dollar-quoting** — correctly parses `$$` and `$tag$` in function bodies
158
180
 
159
- The whole point of oreZ is that `bunx orez` works everywhere with no native compilation step. Postgres runs in-process as WASM via PGlite. zero-cache also needs SQLite, and `@rocicorp/zero-sqlite3` ships as a compiled C addon — so orez ships [bedrock-sqlite](https://www.npmjs.com/package/bedrock-sqlite), SQLite's [bedrock branch](https://sqlite.org/src/timeline?t=begin-concurrent) recompiled to WASM with BEGIN CONCURRENT and WAL2 support. At startup, oreZ patches `@rocicorp/zero-sqlite3` to load bedrock-sqlite instead of the native C addon. Both databases run as WASM — nothing to compile, nothing platform-specific. Just `bun install` and go.
181
+ Standard Postgres tools (`pg_dump`, `pg_restore`, `psql`) also work against the running proxy.
160
182
 
161
- ### Auto heap sizing
183
+ ## Environment Variables
162
184
 
163
- The CLI detects system memory on startup and re-spawns the process with `--max-old-space-size` set to ~50% of available RAM (minimum 4GB). PGlite WASM needs substantial heap for large datasets and restores — this prevents cryptic V8 OOM crashes without requiring manual tuning.
185
+ All `ZERO_*` env vars are forwarded to zero-cache. oreZ provides defaults:
164
186
 
165
- ## Environment variables
187
+ | Variable | Default | Overridable |
188
+ | --------------------------- | ------------------ | ----------- |
189
+ | `NODE_ENV` | `development` | yes |
190
+ | `ZERO_LOG_LEVEL` | from `--log-level` | yes |
191
+ | `ZERO_NUM_SYNC_WORKERS` | `1` | yes |
192
+ | `ZERO_ENABLE_QUERY_PLANNER` | `false` | yes |
193
+ | `ZERO_UPSTREAM_DB` | _(managed)_ | no |
194
+ | `ZERO_CVR_DB` | _(managed)_ | no |
195
+ | `ZERO_CHANGE_DB` | _(managed)_ | no |
196
+ | `ZERO_REPLICA_FILE` | _(managed)_ | no |
197
+ | `ZERO_PORT` | _(managed)_ | no |
166
198
 
167
- Your entire environment is forwarded to the zero-cache child process. This means any `ZERO_*` env vars you set are passed through automatically.
199
+ Common vars you might set:
168
200
 
169
- oreZ provides sensible defaults for a few variables:
201
+ ```bash
202
+ ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
203
+ ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
204
+ ```
170
205
 
171
- | Variable | Default | Overridable |
172
- | --------------------------- | ------------------- | ----------- |
173
- | `NODE_ENV` | `development` | yes |
174
- | `ZERO_LOG_LEVEL` | from `--log-level` | yes |
175
- | `ZERO_NUM_SYNC_WORKERS` | `1` | yes |
176
- | `ZERO_ENABLE_QUERY_PLANNER` | `false` | yes |
177
- | `ZERO_UPSTREAM_DB` | _(managed by oreZ)_ | no |
178
- | `ZERO_CVR_DB` | _(managed by oreZ)_ | no |
179
- | `ZERO_CHANGE_DB` | _(managed by oreZ)_ | no |
180
- | `ZERO_REPLICA_FILE` | _(managed by oreZ)_ | no |
181
- | `ZERO_PORT` | _(managed by oreZ)_ | no |
206
+ ## Local S3
182
207
 
183
- The `--log-level` flag controls zero-cache (`ZERO_LOG_LEVEL`) and oreZ console output. Default is `warn` to keep output quiet. Set to `info` or `debug` for troubleshooting. `ZERO_ENABLE_QUERY_PLANNER` is disabled by default because it can freeze with wasm sqlite.
208
+ Since Zero apps often need file uploads and MinIO requires Docker:
184
209
 
185
- The layering is: oreZ defaults → your env → oreZ-managed connection vars. So setting `ZERO_LOG_LEVEL=debug` in your shell overrides the `--log-level` default, but you can't override the database connection strings (oreZ needs to point zero-cache at its own proxy).
210
+ ```bash
211
+ bunx orez --s3 # with orez
212
+ bunx orez s3 # standalone
213
+ ```
186
214
 
187
- Common vars you might want to set:
215
+ ```typescript
216
+ import { startS3Local } from 'orez/s3'
188
217
 
189
- ```bash
190
- ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
191
- ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
218
+ const server = await startS3Local({ port: 9200, dataDir: '.orez' })
192
219
  ```
193
220
 
194
- ## What gets faked
221
+ Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
195
222
 
196
- The proxy intercepts several things to convince zero-cache it's talking to a real PostgreSQL server with logical replication enabled:
223
+ ---
197
224
 
198
- - `IDENTIFY_SYSTEM` returns a fake system ID and timeline
199
- - `CREATE_REPLICATION_SLOT` persists slot info in a local table and returns a valid LSN
200
- - `START_REPLICATION` enters streaming mode, encoding changes as pgoutput binary messages
201
- - `version()` returns a standard PostgreSQL 16.4 version string (PGlite's Emscripten string breaks `pg_restore` and other tools)
202
- - `current_setting('wal_level')` always returns `logical`
203
- - `pg_replication_slots` queries are redirected to a local tracking table
204
- - `SET TRANSACTION SNAPSHOT` is silently accepted (PGlite doesn't support imported snapshots)
205
- - `ALTER ROLE ... REPLICATION` returns success
206
- - `READ ONLY` is stripped from transaction starts (PGlite is single-session)
207
- - `ISOLATION LEVEL` is stripped from all queries (meaningless with a single-session database)
208
- - `SET TRANSACTION` / `SET SESSION` return synthetic success without hitting PGlite
225
+ # How It Works
209
226
 
210
- The pgoutput encoder produces spec-compliant binary messages: Begin, Relation, Insert, Update, Delete, Commit, and Keepalive. Column values are encoded as text (typeOid 25) except booleans which use typeOid 16 with `t`/`f` encoding, matching PostgreSQL's native boolean wire format.
227
+ ## Architecture
211
228
 
212
- ## Workarounds
229
+ oreZ runs three components:
213
230
 
214
- A lot of things don't "just work" when you replace Postgres with PGlite and native SQLite with WASM. Here's what oreZ does to make it seamless.
231
+ 1. **Three PGlite instances** PostgreSQL 17 in WASM, one per database zero-cache expects (postgres, zero_cvr, zero_cdb)
232
+ 2. **TCP proxy** — speaks PostgreSQL wire protocol, routes to correct PGlite, handles logical replication
233
+ 3. **zero-cache** — child process connecting to proxy, thinks it's real Postgres
215
234
 
216
- ### TCP proxy and message handling
235
+ ### Why three instances?
217
236
 
218
- The proxy runs on raw `net.Socket` and uses `pg-gateway` for connection/auth protocol handling, with oreZ intercepting and rewriting messages where needed (logical replication commands, query rewrites, replication slot views).
237
+ zero-cache expects three databases with independent transaction contexts. PGlite is single-session all connections share one session. Without isolation, CVR transactions get corrupted by postgres queries (`ConcurrentModificationException`).
219
238
 
220
- ### Session state bleed between connections
239
+ | Connection database | PGlite instance | Data directory |
240
+ | ------------------- | --------------- | ----------------- |
241
+ | `postgres` | postgres | `pgdata-postgres` |
242
+ | `zero_cvr` | cvr | `pgdata-cvr` |
243
+ | `zero_cdb` | cdb | `pgdata-cdb` |
221
244
 
222
- PGlite is single-session — all proxy connections share one session. If `pg_restore` sets `search_path = ''`, every subsequent connection inherits that. On disconnect, oreZ resets `search_path`, `statement_timeout`, `lock_timeout`, and `idle_in_transaction_session_timeout`, and rolls back any open transaction. Without this, the next connection gets a corrupted session.
245
+ ### Replication
223
246
 
224
- ### Event loop starvation from mutex chains
247
+ PGlite doesn't support logical replication, so oreZ fakes it:
225
248
 
226
- The mutex uses `setImmediate`/`setTimeout` between releases instead of resolving the next waiter as a microtask. Without this, releasing the mutex triggers a chain of synchronous PGlite executions that blocks all socket I/O — connections stall because reads and writes can't be processed between queries.
249
+ 1. Triggers capture every mutation into `_orez._zero_changes`
250
+ 2. Changes are encoded as pgoutput binary protocol
251
+ 3. Streamed to zero-cache through the replication connection
227
252
 
228
- ### PGlite errors don't kill connections
253
+ Change notifications use `pg_notify` for real-time sync. Polling (20ms/500ms adaptive) is fallback only.
229
254
 
230
- When `execProtocolRaw` throws (PGlite internal error), the proxy sends a proper ErrorResponse + ReadyForQuery over the wire instead of destroying the socket. The client sees an error message and continues working.
255
+ ### SQLite WASM
231
256
 
232
- ### SQLite shim via ESM loader hooks
257
+ zero-cache needs SQLite via `@rocicorp/zero-sqlite3` (native C addon). oreZ intercepts this at runtime using Node's ESM loader hooks, redirecting to [bedrock-sqlite](https://www.npmjs.com/package/bedrock-sqlite) — SQLite's bedrock branch compiled to WASM with BEGIN CONCURRENT and WAL2.
233
258
 
234
- zero-cache imports `@rocicorp/zero-sqlite3` (a native C addon) via ESM `import`. oreZ uses Node's `module.register()` API with `--import` to intercept resolution — ESM `resolve` and `load` hooks redirect `@rocicorp/zero-sqlite3` to bedrock-sqlite WASM at runtime. The hook templates live in `src/shim/` and are written to tmpdir with the resolved bedrock-sqlite path substituted.
259
+ The shim also polyfills the better-sqlite3 API surface zero-cache expects.
235
260
 
236
- The shim also polyfills the better-sqlite3 API surface zero-cache expects: `unsafeMode()`, `defaultSafeIntegers()`, `serialize()`, `backup()`, and `scanStatus`/`scanStatusV2`/`scanStatusReset` on Statement prototypes (zero-cache's query planner calls these for scan statistics, which WASM doesn't support).
261
+ ### Native SQLite mode
237
262
 
238
- ### Query planner disabled
263
+ For `--disable-wasm-sqlite`, bootstrap the native addon first:
239
264
 
240
- `ZERO_ENABLE_QUERY_PLANNER` is set to `false` because it relies on SQLite scan statistics that trigger infinite loops in WASM sqlite (and have caused freezes with native sqlite too). The planner is an optimization, not required for correctness.
265
+ ```bash
266
+ bun run native:bootstrap
267
+ ```
241
268
 
242
- ### Type OIDs in RELATION messages
269
+ ## Internal Schema
243
270
 
244
- Replication RELATION messages carry correct PostgreSQL type OIDs (not just text/25) so zero-cache selects the right value parsers. For example, `timestamp with time zone` gets OID 1184, which triggers `timestampToFpMillis` conversion. Without this, zero-cache misinterprets column types.
271
+ oreZ stores replication state in the `_orez` schema (survives `pg_restore --clean`):
245
272
 
246
- ### Unsupported column exclusion
273
+ - `_orez._zero_changes` change log for replication
274
+ - `_orez._zero_replication_slots` — slot tracking
275
+ - `_orez._zero_watermark` — LSN sequence
247
276
 
248
- Columns with types zero-cache can't handle (`tsvector`, `tsquery`, `USER-DEFINED`) are filtered out of replication messages. Without exclusion, zero-cache crashes on the unknown types. The columns are removed from both new and old row data.
277
+ ## Wire Protocol Compatibility
249
278
 
250
- ### Publication-aware change tracking
279
+ The proxy intercepts and rewrites to make PGlite look like real Postgres:
251
280
 
252
- If `ZERO_APP_PUBLICATIONS` is set, only tables in that publication get change-tracking triggers. This prevents streaming changes for private tables (user sessions, accounts) that zero-cache doesn't know about. Stale triggers from previous installs (before the publication existed) are cleaned up automatically.
281
+ | Query/Command | What oreZ does |
282
+ | ------------------------------- | --------------------------------------------------- |
283
+ | `version()` | Returns `PostgreSQL 17.4 on x86_64-pc-linux-gnu...` |
284
+ | `current_setting('wal_level')` | Returns `logical` |
285
+ | `IDENTIFY_SYSTEM` | Returns fake system ID and timeline |
286
+ | `CREATE_REPLICATION_SLOT` | Persists to local table, returns valid LSN |
287
+ | `START_REPLICATION` | Streams changes as pgoutput binary |
288
+ | `pg_replication_slots` | Redirects to local tracking table |
289
+ | `READ ONLY` / `ISOLATION LEVEL` | Stripped (single-session) |
253
290
 
254
- ### Replica cleanup on startup
291
+ ## Workarounds
255
292
 
256
- oreZ deletes the SQLite replica file (`zero-replica.db`) and related files (`-wal`, `-shm`, `-wal2`) on startup/reset so zero-cache performs a fresh sync.
293
+ Things that don't "just work" when replacing Postgres with PGlite and native SQLite with WASM:
257
294
 
258
- ### Data directory migration
295
+ ### Session state bleed
259
296
 
260
- Existing installs that used a single PGlite instance (`pgdata/`) are auto-migrated to the multi-instance layout (`pgdata-postgres/`) on first run. No manual intervention needed.
297
+ PGlite is single-session if `pg_restore` sets `search_path = ''`, every subsequent connection inherits it. On disconnect, oreZ resets `search_path`, `statement_timeout`, `lock_timeout`, and rolls back open transactions.
261
298
 
262
- ### Restore: dollar-quoting and statement boundaries
299
+ ### Query planner disabled
263
300
 
264
- The restore parser tracks `$$` and `$tag$` blocks to correctly identify statement boundaries in function bodies. Without this, semicolons inside `CREATE FUNCTION` bodies are misinterpreted as statement terminators.
301
+ `ZERO_ENABLE_QUERY_PLANNER=false` because it relies on SQLite scan statistics that cause infinite loops in WASM.
265
302
 
266
- ### Restore: broken trigger cleanup
303
+ ### Unsupported column types
267
304
 
268
- After restore, oreZ drops triggers whose backing functions don't exist. This happens when a filtered `pg_dump` includes triggers on public-schema tables that reference functions from excluded schemas. The triggers survive TOC filtering because they're associated with public tables, but the functions they reference weren't included.
305
+ Columns with `tsvector`, `tsquery`, `USER-DEFINED` types are filtered from replication messages.
269
306
 
270
- ### Restore: wire protocol auto-detection
307
+ ### Publication-aware tracking
271
308
 
272
- `pg_restore` tries connecting via wire protocol first (for restoring into a running oreZ instance). If the connection fails, it falls back to direct PGlite access. But if the connection succeeds and the restore itself fails, it does _not_ fall back — the error is real and should be reported, not masked by a retry.
309
+ If `ZERO_APP_PUBLICATIONS` is set, only tables in that publication get change-tracking triggers.
273
310
 
274
- ### Callback-based message loop
311
+ ### Broken trigger cleanup
275
312
 
276
- The proxy uses callback-based `socket.on('data')` events instead of async iterators for the message loop. Async iterators have unreliable behavior across runtimes (Node.js vs Bun). The callback approach with manual pause/resume works everywhere.
313
+ After restore, triggers whose backing functions don't exist are dropped (happens with filtered pg_dump).
277
314
 
278
315
  ## Tests
279
316
 
280
- Tests cover the full stack from binary encoding to TCP-level integration, including pg_restore end-to-end tests and bedrock-sqlite WASM engine tests:
281
-
282
- ```
317
+ ```bash
283
318
  bun run test # orez tests
319
+ bun run test:integration:native # native sqlite integration
284
320
  cd sqlite-wasm && bunx vitest run # bedrock-sqlite tests
285
321
  ```
286
322
 
287
- The oreZ test suite includes a zero-cache compatibility layer that decodes pgoutput messages into the same typed format that zero-cache's PgoutputParser produces, validating end-to-end compatibility.
288
-
289
- The bedrock-sqlite tests cover Database/Statement API, transactions, WAL/WAL2 modes, BEGIN CONCURRENT, FTS5, JSON functions, custom functions, aggregates, bigint handling, and file persistence.
290
-
291
- ## Limitations
292
-
293
- This is a development tool. It is not suitable for production use.
294
-
295
- - PGlite is single-session per instance. All queries to the same database are serialized through a mutex. Cross-database queries are independent (each database has its own PGlite instance and mutex). Fine for development but would bottleneck under real load.
296
- - Triggers add overhead to every write. Again, fine for development.
297
- - PGlite stores data on the local filesystem. No replication, no high availability. Use `orez pg_dump` / `orez pg_restore` for backups.
298
-
299
- ## Project structure
323
+ ## Project Structure
300
324
 
301
325
  ```
302
326
  src/
303
- cli-entry.ts thin wrapper for auto heap sizing
327
+ cli-entry.ts auto heap sizing wrapper
304
328
  cli.ts cli with citty
305
- index.ts main entry, orchestrates startup + sqlite wasm patching
329
+ index.ts main entry, orchestrates startup
306
330
  config.ts configuration with defaults
307
- log.ts colored log prefixes
308
- mutex.ts simple mutex for serializing pglite access
331
+ log.ts colored log prefixes, log files
332
+ mutex.ts serializing pglite access
309
333
  port.ts auto port finding
310
- pg-proxy.ts raw tcp proxy implementing postgresql wire protocol
311
- pglite-manager.ts multi-instance pglite creation and migration runner
312
- s3-local.ts local s3-compatible server (orez/s3)
313
- vite-plugin.ts vite dev server plugin (orez/vite)
334
+ pg-proxy.ts postgresql wire protocol proxy
335
+ pglite-manager.ts multi-instance pglite, migrations
336
+ s3-local.ts local s3 server (orez/s3)
337
+ vite-plugin.ts vite plugin (orez/vite)
338
+ admin/
339
+ server.ts admin dashboard backend
340
+ ui.ts admin dashboard frontend
341
+ log-store.ts log aggregation
342
+ http-proxy.ts http request logging
314
343
  replication/
315
- handler.ts replication protocol state machine + adaptive polling
316
- pgoutput-encoder.ts binary pgoutput message encoder
317
- change-tracker.ts trigger installation, shard tracking, change purging
344
+ handler.ts replication state machine, adaptive polling
345
+ pgoutput-encoder.ts binary pgoutput encoder
346
+ change-tracker.ts trigger installation, change purging
318
347
  integration/
319
- integration.test.ts end-to-end zero-cache sync test
320
- restore.test.ts pg_dump/restore integration test
348
+ *.test.ts end-to-end tests
321
349
  sqlite-wasm/
322
- Makefile emscripten build for bedrock-sqlite wasm binary
323
- bedrock-sqlite.d.ts typescript declarations
324
- native/
325
- api.js better-sqlite3 compatible database/statement API
326
- vfs.c custom VFS with SHM support for WAL/WAL2
327
- vfs.js javascript VFS bridge
328
- test/
329
- database.test.ts wasm sqlite engine tests
330
- ```
331
-
332
- ## Backup & Restore
333
-
334
- Dump and restore your local PGlite database using WASM-compiled `pg_dump` — no native Postgres install needed.
335
-
336
- ```
337
- bunx orez pg_dump > backup.sql
338
- bunx orez pg_dump --output backup.sql
339
- bunx orez pg_restore backup.sql
340
- bunx orez pg_restore backup.sql --clean
341
- ```
342
-
343
- ```
344
- pg_dump options:
345
- --data-dir=.orez data directory
346
- -o, --output output file path (default: stdout)
347
-
348
- pg_restore options:
349
- --data-dir=.orez data directory
350
- --clean drop and recreate public schema before restoring
351
- ```
352
-
353
- `pg_restore` also supports connecting to a running oreZ instance via wire protocol — just pass `--pg-port`:
354
-
355
- ```
356
- bunx orez pg_restore backup.sql --pg-port 6434
357
- bunx orez pg_restore backup.sql --pg-port 6434 --pg-user user --pg-password password
358
- bunx orez pg_restore backup.sql --direct # force direct PGlite access, skip wire protocol
359
- ```
360
-
361
- Restore streams the dump file line-by-line so it can handle large dumps without loading everything into memory. SQL is parsed using [pgsql-parser](https://www.npmjs.com/package/pgsql-parser) (the real PostgreSQL C parser compiled to WASM) for accurate statement classification and rewriting.
362
-
363
- ### What restore handles automatically
364
-
365
- - **COPY FROM stdin → INSERT**: PGlite WASM doesn't support the COPY protocol, so COPY blocks are converted to batched multi-row INSERTs (50 rows per statement, flushed at 1MB)
366
- - **Unsupported extensions**: `pg_stat_statements`, `pg_buffercache`, `pg_cron`, etc. — CREATE, DROP, and COMMENT ON EXTENSION statements are skipped
367
- - **Idempotent DDL**: `CREATE SCHEMA` → `IF NOT EXISTS`, `CREATE FUNCTION/VIEW` → `OR REPLACE`
368
- - **Oversized rows**: Rows larger than 16MB are skipped with a warning (PGlite WASM crashes around 24MB per value)
369
- - **Missing table references**: DDL errors from filtered dumps (e.g. ALTER TABLE on excluded tables) log a warning and continue
370
- - **Transaction batching**: Data statements are grouped 200 per transaction with CHECKPOINT every 3 batches to manage WASM memory
371
- - **PostgreSQL 18+ artifacts**: `SET transaction_timeout` silently skipped
372
- - **psql meta-commands**: `\restrict` and similar silently skipped
373
-
374
- This means you can take a `pg_dump` from a production Postgres database and restore it directly into oreZ — incompatible statements are handled automatically.
375
-
376
- When oreZ is not running, `pg_restore` opens PGlite directly. When oreZ is running, pass `--pg-port` to restore through the wire protocol. Standard Postgres tools (`pg_dump`, `pg_restore`, `psql`) also work against the running proxy since oreZ presents a standard PostgreSQL 16.4 version string over the wire.
377
-
378
- ## Extra: orez/s3
379
-
380
- Since we use this stack often with a file uploading service like MinIO which also requires docker, I threw in a tiny s3-compatible endpoint too:
381
-
382
- `bunx orez --s3` or standalone `bunx orez s3`.
383
-
384
- ```typescript
385
- import { startS3Local } from 'orez/s3'
386
-
387
- const server = await startS3Local({
388
- port: 9200,
389
- dataDir: '.orez',
390
- })
350
+ Makefile emscripten build
351
+ native/api.js better-sqlite3 compatible API
352
+ native/vfs.c custom VFS with SHM for WAL2
391
353
  ```
392
354
 
393
- Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
394
-
395
355
  ## License
396
356
 
397
357
  MIT
@@ -1 +1 @@
1
- {"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["../../src/admin/log-store.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QACjF,OAAO,EAAE,QAAQ,EAAE,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,MAAM,IAAI,QAAQ,EAAE,CAAA;IACpB,KAAK,IAAI,IAAI,CAAA;CACd;AAOD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,QAAQ,CAkG5E"}
1
+ {"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["../../src/admin/log-store.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QACjF,OAAO,EAAE,QAAQ,EAAE,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,MAAM,IAAI,QAAQ,EAAE,CAAA;IACpB,KAAK,IAAI,IAAI,CAAA;CACd;AAOD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,QAAQ,CA4G5E"}
@@ -8,20 +8,26 @@ export function createLogStore(dataDir, writeToDisk = true) {
8
8
  const entries = [];
9
9
  let nextId = 1;
10
10
  const logsDir = join(dataDir, 'logs');
11
- const logFile = join(logsDir, 'orez.log');
12
- const backupFile = join(logsDir, 'orez.log.1');
13
11
  if (writeToDisk) {
14
12
  mkdirSync(logsDir, { recursive: true });
15
13
  }
16
- function rotateIfNeeded() {
14
+ // track file sizes to rotate per-source
15
+ const fileSizes = {};
16
+ function getLogFile(source) {
17
+ return join(logsDir, `${source}.log`);
18
+ }
19
+ function rotateIfNeeded(source) {
17
20
  if (!writeToDisk)
18
21
  return;
19
22
  try {
23
+ const logFile = getLogFile(source);
20
24
  if (!existsSync(logFile))
21
25
  return;
22
26
  const stat = statSync(logFile);
27
+ fileSizes[source] = stat.size;
23
28
  if (stat.size > MAX_FILE_SIZE) {
24
- renameSync(logFile, backupFile);
29
+ renameSync(logFile, logFile + '.1');
30
+ fileSizes[source] = 0;
25
31
  }
26
32
  }
27
33
  catch { }
@@ -41,8 +47,13 @@ export function createLogStore(dataDir, writeToDisk = true) {
41
47
  if (writeToDisk) {
42
48
  try {
43
49
  const ts = new Date(entry.ts).toISOString();
44
- appendFileSync(logFile, '[' + ts + '] [' + source + '] [' + level + '] ' + entry.msg + '\n');
45
- rotateIfNeeded();
50
+ const logFile = getLogFile(source);
51
+ appendFileSync(logFile, `[${ts}] [${level}] ${entry.msg}\n`);
52
+ // check rotation every ~100 writes to this source
53
+ fileSizes[source] = (fileSizes[source] || 0) + entry.msg.length + 50;
54
+ if (fileSizes[source] > MAX_FILE_SIZE) {
55
+ rotateIfNeeded(source);
56
+ }
46
57
  }
47
58
  catch { }
48
59
  }