orez 0.1.6 → 0.1.8

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 (69) hide show
  1. package/README.md +186 -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 +96 -46
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.d.ts +1 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +1 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +158 -23
  19. package/dist/index.js.map +1 -1
  20. package/dist/integration/test-permissions.d.ts +7 -0
  21. package/dist/integration/test-permissions.d.ts.map +1 -0
  22. package/dist/integration/test-permissions.js +117 -0
  23. package/dist/integration/test-permissions.js.map +1 -0
  24. package/dist/pg-proxy.js +2 -2
  25. package/dist/pg-proxy.js.map +1 -1
  26. package/dist/replication/change-tracker.d.ts.map +1 -1
  27. package/dist/replication/change-tracker.js +15 -13
  28. package/dist/replication/change-tracker.js.map +1 -1
  29. package/dist/replication/handler.d.ts.map +1 -1
  30. package/dist/replication/handler.js +27 -2
  31. package/dist/replication/handler.js.map +1 -1
  32. package/dist/sqlite-mode/index.d.ts +1 -0
  33. package/dist/sqlite-mode/index.d.ts.map +1 -1
  34. package/dist/sqlite-mode/index.js +1 -0
  35. package/dist/sqlite-mode/index.js.map +1 -1
  36. package/dist/sqlite-mode/native-binary.d.ts +11 -0
  37. package/dist/sqlite-mode/native-binary.d.ts.map +1 -0
  38. package/dist/sqlite-mode/native-binary.js +67 -0
  39. package/dist/sqlite-mode/native-binary.js.map +1 -0
  40. package/dist/sqlite-mode/package-resolve.d.ts +6 -0
  41. package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
  42. package/dist/sqlite-mode/package-resolve.js +20 -0
  43. package/dist/sqlite-mode/package-resolve.js.map +1 -0
  44. package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
  45. package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
  46. package/dist/sqlite-mode/resolve-mode.js +27 -23
  47. package/dist/sqlite-mode/resolve-mode.js.map +1 -1
  48. package/package.json +8 -2
  49. package/src/admin/log-store.ts +19 -9
  50. package/src/admin/server.ts +12 -0
  51. package/src/cli.ts +99 -44
  52. package/src/config.ts +2 -0
  53. package/src/index.ts +186 -24
  54. package/src/integration/integration.test.ts +93 -15
  55. package/src/integration/native-binary.guard.test.ts +13 -0
  56. package/src/integration/native-startup.test.ts +44 -0
  57. package/src/integration/restore-live-stress.test.ts +433 -0
  58. package/src/integration/restore-reset.test.ts +136 -20
  59. package/src/integration/test-permissions.ts +147 -0
  60. package/src/pg-proxy.ts +2 -2
  61. package/src/replication/change-tracker.test.ts +1 -1
  62. package/src/replication/change-tracker.ts +16 -13
  63. package/src/replication/handler.test.ts +2 -2
  64. package/src/replication/handler.ts +30 -2
  65. package/src/sqlite-mode/index.ts +1 -0
  66. package/src/sqlite-mode/native-binary.ts +89 -0
  67. package/src/sqlite-mode/package-resolve.ts +17 -0
  68. package/src/sqlite-mode/resolve-mode.ts +31 -21
  69. package/src/sqlite-mode/sqlite-mode.test.ts +11 -5
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,32 @@ 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
+ --force-wasm-sqlite force wasm sqlite even if native is available
63
+ --disable-wasm-sqlite force native sqlite (fail if not available)
64
+ --on-db-ready=CMD command to run after db+proxy ready, before zero-cache
65
+ --on-healthy=CMD command to run once all services healthy
66
+ --disable-admin disable admin dashboard
67
+ --admin-port=6477 admin dashboard port (default: 6477)
45
68
  ```
46
69
 
47
70
  Ports auto-increment if already in use.
48
71
 
49
72
  ## Admin Dashboard
50
73
 
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:
74
+ Enabled by default at `http://localhost:6477`.
58
75
 
59
76
  - **Logs** — live-streaming logs from zero-cache, filterable by source and level
77
+ - **HTTP** — request/response inspector for zero-cache traffic
60
78
  - **Env** — environment variables passed to zero-cache
61
- - **Actions** — restart zero-cache, reset (wipe replica + resync), clear logs
79
+ - **Actions** — restart zero-cache, reset (wipe replica + resync), full reset (wipe CVR/CDB too)
62
80
 
63
- The dashboard polls every second for new logs and updates uptime/port status every 5 seconds.
81
+ Logs are also written to separate files in your data directory: `zero.log`, `proxy.log`, `pglite.log`, etc.
64
82
 
65
- ## Programmatic
83
+ ```
84
+ bunx orez --disable-admin # disable dashboard
85
+ ```
86
+
87
+ ## Programmatic API
66
88
 
67
89
  ```
68
90
  bun install orez
@@ -76,6 +98,7 @@ const { config, stop, db, instances } = await startZeroLite({
76
98
  zeroPort: 5849,
77
99
  migrationsDir: 'src/database/migrations',
78
100
  seedFile: 'src/database/seed.sql',
101
+ adminPort: 6477, // set to 0 to disable
79
102
  })
80
103
 
81
104
  // your app connects to zero-cache at localhost:5849
@@ -84,11 +107,10 @@ const { config, stop, db, instances } = await startZeroLite({
84
107
  // db is the postgres PGlite instance (for direct queries)
85
108
  // instances has all three: { postgres, cvr, cdb }
86
109
 
87
- // when done
88
110
  await stop()
89
111
  ```
90
112
 
91
- All options are optional with sensible defaults. Ports auto-find if in use.
113
+ All options are optional with sensible defaults.
92
114
 
93
115
  ### Lifecycle hooks
94
116
 
@@ -97,9 +119,9 @@ All options are optional with sensible defaults. Ports auto-find if in use.
97
119
  | on-db-ready | `--on-db-ready=CMD` | `onDbReady: 'CMD'` or `onDbReady: fn` | after db + proxy ready, before zero |
98
120
  | on-healthy | `--on-healthy=CMD` | `onHealthy: 'CMD'` or `onHealthy: fn` | after all services ready |
99
121
 
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.
122
+ Shell commands receive env vars: `DATABASE_URL`, `OREZ_PG_PORT`, `OREZ_ZERO_PORT`. Change tracking triggers are re-installed after `onDbReady`.
101
123
 
102
- ## Vite plugin
124
+ ## Vite Plugin
103
125
 
104
126
  ```typescript
105
127
  import { orezPlugin } from 'orez/vite'
@@ -110,7 +132,6 @@ export default {
110
132
  pgPort: 6434,
111
133
  zeroPort: 5849,
112
134
  migrationsDir: 'src/database/migrations',
113
- // lifecycle hooks (optional)
114
135
  onDbReady: () => console.log('db ready'),
115
136
  onHealthy: () => console.log('all services healthy'),
116
137
  }),
@@ -118,280 +139,220 @@ export default {
118
139
  }
119
140
  ```
120
141
 
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:
142
+ Starts oreZ when vite dev starts, stops on close. Supports all `startZeroLite` options plus `s3` and `s3Port`.
126
143
 
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.
144
+ ## Backup & Restore
134
145
 
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.
146
+ Dump and restore your local database — no native Postgres install needed.
136
147
 
137
- The proxy routes connections based on the database name in the startup message:
148
+ ```bash
149
+ bunx orez pg_dump > backup.sql
150
+ bunx orez pg_dump --output backup.sql
151
+ bunx orez pg_restore backup.sql
152
+ bunx orez pg_restore backup.sql --clean # drop public schema first
153
+ ```
138
154
 
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` |
155
+ ### Restoring into a running instance
144
156
 
145
- Each instance has its own mutex for serializing queries. Extensions (pgvector, pg_trgm) and app migrations only run on the postgres instance.
157
+ When oreZ is running, restore through the wire protocol:
146
158
 
147
- ### Replication
159
+ ```bash
160
+ bunx orez pg_restore backup.sql --pg-port 6434
161
+ ```
148
162
 
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.
163
+ This automatically:
150
164
 
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.
165
+ 1. Stops zero-cache before restore (via admin API)
166
+ 2. Clears replication state and shard schemas
167
+ 3. Restores the dump
168
+ 4. Adds all public tables to the publication
169
+ 5. Restarts zero-cache
152
170
 
153
- Shard schemas (e.g., `chat_0`) are re-scanned periodically and change tracking is installed for shard `clients` tables.
171
+ The `--direct` flag forces direct PGlite access, skipping wire protocol.
154
172
 
155
- The replication handler also tracks shard schema tables so that `.server` promises on zero mutations resolve correctly.
173
+ ### What restore handles
156
174
 
157
- ### Zero native dependencies
175
+ - **COPY INSERT** — PGlite doesn't support COPY protocol; converted to batched multi-row INSERTs
176
+ - **Unsupported extensions** — `pg_stat_statements`, `pg_buffercache`, `pg_cron` etc. silently skipped
177
+ - **Idempotent DDL** — `CREATE SCHEMA` → `IF NOT EXISTS`, `CREATE FUNCTION` → `OR REPLACE`
178
+ - **Oversized rows** — rows >16MB skipped with warning (WASM limit)
179
+ - **Transaction batching** — 200 statements per transaction, CHECKPOINT every 3 batches
180
+ - **Dollar-quoting** — correctly parses `$$` and `$tag$` in function bodies
158
181
 
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.
182
+ Standard Postgres tools (`pg_dump`, `pg_restore`, `psql`) also work against the running proxy.
160
183
 
161
- ### Auto heap sizing
184
+ ## Environment Variables
162
185
 
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.
186
+ All `ZERO_*` env vars are forwarded to zero-cache. oreZ provides defaults:
164
187
 
165
- ## Environment variables
188
+ | Variable | Default | Overridable |
189
+ | --------------------------- | ------------------ | ----------- |
190
+ | `NODE_ENV` | `development` | yes |
191
+ | `ZERO_LOG_LEVEL` | from `--log-level` | yes |
192
+ | `ZERO_NUM_SYNC_WORKERS` | `1` | yes |
193
+ | `ZERO_ENABLE_QUERY_PLANNER` | `false` | yes |
194
+ | `ZERO_UPSTREAM_DB` | _(managed)_ | no |
195
+ | `ZERO_CVR_DB` | _(managed)_ | no |
196
+ | `ZERO_CHANGE_DB` | _(managed)_ | no |
197
+ | `ZERO_REPLICA_FILE` | _(managed)_ | no |
198
+ | `ZERO_PORT` | _(managed)_ | no |
166
199
 
167
- Your entire environment is forwarded to the zero-cache child process. This means any `ZERO_*` env vars you set are passed through automatically.
200
+ Common vars you might set:
168
201
 
169
- oreZ provides sensible defaults for a few variables:
202
+ ```bash
203
+ ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
204
+ ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
205
+ ```
170
206
 
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 |
207
+ ## Local S3
182
208
 
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.
209
+ Since Zero apps often need file uploads and MinIO requires Docker:
184
210
 
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).
211
+ ```bash
212
+ bunx orez --s3 # with orez
213
+ bunx orez s3 # standalone
214
+ ```
186
215
 
187
- Common vars you might want to set:
216
+ ```typescript
217
+ import { startS3Local } from 'orez/s3'
188
218
 
189
- ```bash
190
- ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
191
- ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
219
+ const server = await startS3Local({ port: 9200, dataDir: '.orez' })
192
220
  ```
193
221
 
194
- ## What gets faked
222
+ Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
195
223
 
196
- The proxy intercepts several things to convince zero-cache it's talking to a real PostgreSQL server with logical replication enabled:
224
+ ---
197
225
 
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
226
+ # How It Works
209
227
 
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.
228
+ ## Architecture
211
229
 
212
- ## Workarounds
230
+ oreZ runs three components:
213
231
 
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.
232
+ 1. **Three PGlite instances** PostgreSQL 17 in WASM, one per database zero-cache expects (postgres, zero_cvr, zero_cdb)
233
+ 2. **TCP proxy** — speaks PostgreSQL wire protocol, routes to correct PGlite, handles logical replication
234
+ 3. **zero-cache** — child process connecting to proxy, thinks it's real Postgres
215
235
 
216
- ### TCP proxy and message handling
236
+ ### Why three instances?
217
237
 
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).
238
+ 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
239
 
220
- ### Session state bleed between connections
240
+ | Connection database | PGlite instance | Data directory |
241
+ | ------------------- | --------------- | ----------------- |
242
+ | `postgres` | postgres | `pgdata-postgres` |
243
+ | `zero_cvr` | cvr | `pgdata-cvr` |
244
+ | `zero_cdb` | cdb | `pgdata-cdb` |
221
245
 
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.
246
+ ### Replication
223
247
 
224
- ### Event loop starvation from mutex chains
248
+ PGlite doesn't support logical replication, so oreZ fakes it:
225
249
 
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.
250
+ 1. Triggers capture every mutation into `_orez._zero_changes`
251
+ 2. Changes are encoded as pgoutput binary protocol
252
+ 3. Streamed to zero-cache through the replication connection
227
253
 
228
- ### PGlite errors don't kill connections
254
+ Change notifications use `pg_notify` for real-time sync. Polling (20ms/500ms adaptive) is fallback only.
229
255
 
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.
256
+ ### SQLite WASM
231
257
 
232
- ### SQLite shim via ESM loader hooks
258
+ 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
259
 
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.
260
+ The shim also polyfills the better-sqlite3 API surface zero-cache expects.
235
261
 
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).
262
+ ### Native SQLite mode
237
263
 
238
- ### Query planner disabled
264
+ For `--disable-wasm-sqlite`, bootstrap the native addon first:
239
265
 
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.
266
+ ```bash
267
+ bun run native:bootstrap
268
+ ```
241
269
 
242
- ### Type OIDs in RELATION messages
270
+ ## Internal Schema
243
271
 
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.
272
+ oreZ stores replication state in the `_orez` schema (survives `pg_restore --clean`):
245
273
 
246
- ### Unsupported column exclusion
274
+ - `_orez._zero_changes` change log for replication
275
+ - `_orez._zero_replication_slots` — slot tracking
276
+ - `_orez._zero_watermark` — LSN sequence
247
277
 
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.
278
+ ## Wire Protocol Compatibility
249
279
 
250
- ### Publication-aware change tracking
280
+ The proxy intercepts and rewrites to make PGlite look like real Postgres:
251
281
 
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.
282
+ | Query/Command | What oreZ does |
283
+ | ------------------------------- | --------------------------------------------------- |
284
+ | `version()` | Returns `PostgreSQL 17.4 on x86_64-pc-linux-gnu...` |
285
+ | `current_setting('wal_level')` | Returns `logical` |
286
+ | `IDENTIFY_SYSTEM` | Returns fake system ID and timeline |
287
+ | `CREATE_REPLICATION_SLOT` | Persists to local table, returns valid LSN |
288
+ | `START_REPLICATION` | Streams changes as pgoutput binary |
289
+ | `pg_replication_slots` | Redirects to local tracking table |
290
+ | `READ ONLY` / `ISOLATION LEVEL` | Stripped (single-session) |
253
291
 
254
- ### Replica cleanup on startup
292
+ ## Workarounds
255
293
 
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.
294
+ Things that don't "just work" when replacing Postgres with PGlite and native SQLite with WASM:
257
295
 
258
- ### Data directory migration
296
+ ### Session state bleed
259
297
 
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.
298
+ 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
299
 
262
- ### Restore: dollar-quoting and statement boundaries
300
+ ### Query planner disabled
263
301
 
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.
302
+ `ZERO_ENABLE_QUERY_PLANNER=false` because it relies on SQLite scan statistics that cause infinite loops in WASM.
265
303
 
266
- ### Restore: broken trigger cleanup
304
+ ### Unsupported column types
267
305
 
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.
306
+ Columns with `tsvector`, `tsquery`, `USER-DEFINED` types are filtered from replication messages.
269
307
 
270
- ### Restore: wire protocol auto-detection
308
+ ### Publication-aware tracking
271
309
 
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.
310
+ If `ZERO_APP_PUBLICATIONS` is set, only tables in that publication get change-tracking triggers.
273
311
 
274
- ### Callback-based message loop
312
+ ### Broken trigger cleanup
275
313
 
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.
314
+ After restore, triggers whose backing functions don't exist are dropped (happens with filtered pg_dump).
277
315
 
278
316
  ## Tests
279
317
 
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
- ```
318
+ ```bash
283
319
  bun run test # orez tests
320
+ bun run test:integration:native # native sqlite integration
284
321
  cd sqlite-wasm && bunx vitest run # bedrock-sqlite tests
285
322
  ```
286
323
 
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
324
+ ## Project Structure
300
325
 
301
326
  ```
302
327
  src/
303
- cli-entry.ts thin wrapper for auto heap sizing
328
+ cli-entry.ts auto heap sizing wrapper
304
329
  cli.ts cli with citty
305
- index.ts main entry, orchestrates startup + sqlite wasm patching
330
+ index.ts main entry, orchestrates startup
306
331
  config.ts configuration with defaults
307
- log.ts colored log prefixes
308
- mutex.ts simple mutex for serializing pglite access
332
+ log.ts colored log prefixes, log files
333
+ mutex.ts serializing pglite access
309
334
  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)
335
+ pg-proxy.ts postgresql wire protocol proxy
336
+ pglite-manager.ts multi-instance pglite, migrations
337
+ s3-local.ts local s3 server (orez/s3)
338
+ vite-plugin.ts vite plugin (orez/vite)
339
+ admin/
340
+ server.ts admin dashboard backend
341
+ ui.ts admin dashboard frontend
342
+ log-store.ts log aggregation
343
+ http-proxy.ts http request logging
314
344
  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
345
+ handler.ts replication state machine, adaptive polling
346
+ pgoutput-encoder.ts binary pgoutput encoder
347
+ change-tracker.ts trigger installation, change purging
318
348
  integration/
319
- integration.test.ts end-to-end zero-cache sync test
320
- restore.test.ts pg_dump/restore integration test
349
+ *.test.ts end-to-end tests
321
350
  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
- })
351
+ Makefile emscripten build
352
+ native/api.js better-sqlite3 compatible API
353
+ native/vfs.c custom VFS with SHM for WAL2
391
354
  ```
392
355
 
393
- Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
394
-
395
356
  ## License
396
357
 
397
358
  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
  }