orez 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +116 -0
  2. package/dist/config.d.ts +15 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +20 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/index.d.ts +15 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +195 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/pg-proxy.d.ts +14 -0
  11. package/dist/pg-proxy.d.ts.map +1 -0
  12. package/dist/pg-proxy.js +385 -0
  13. package/dist/pg-proxy.js.map +1 -0
  14. package/dist/pglite-manager.d.ts +5 -0
  15. package/dist/pglite-manager.d.ts.map +1 -0
  16. package/dist/pglite-manager.js +71 -0
  17. package/dist/pglite-manager.js.map +1 -0
  18. package/dist/replication/change-tracker.d.ts +14 -0
  19. package/dist/replication/change-tracker.d.ts.map +1 -0
  20. package/dist/replication/change-tracker.js +86 -0
  21. package/dist/replication/change-tracker.js.map +1 -0
  22. package/dist/replication/handler.d.ts +24 -0
  23. package/dist/replication/handler.d.ts.map +1 -0
  24. package/dist/replication/handler.js +300 -0
  25. package/dist/replication/handler.js.map +1 -0
  26. package/dist/replication/pgoutput-encoder.d.ts +26 -0
  27. package/dist/replication/pgoutput-encoder.d.ts.map +1 -0
  28. package/dist/replication/pgoutput-encoder.js +204 -0
  29. package/dist/replication/pgoutput-encoder.js.map +1 -0
  30. package/dist/s3-local.d.ts +8 -0
  31. package/dist/s3-local.d.ts.map +1 -0
  32. package/dist/s3-local.js +131 -0
  33. package/dist/s3-local.js.map +1 -0
  34. package/package.json +56 -0
  35. package/src/config.ts +40 -0
  36. package/src/index.ts +255 -0
  37. package/src/pg-proxy.ts +474 -0
  38. package/src/pglite-manager.ts +105 -0
  39. package/src/replication/change-tracker.test.ts +179 -0
  40. package/src/replication/change-tracker.ts +115 -0
  41. package/src/replication/handler.test.ts +331 -0
  42. package/src/replication/handler.ts +378 -0
  43. package/src/replication/pgoutput-encoder.test.ts +381 -0
  44. package/src/replication/pgoutput-encoder.ts +252 -0
  45. package/src/replication/tcp-replication.test.ts +824 -0
  46. package/src/replication/zero-compat.test.ts +882 -0
  47. package/src/s3-local.ts +179 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * minimal local s3-compatible server.
3
+ * handles GET/PUT/DELETE/HEAD for object storage, replacing minio.
4
+ */
5
+ import { createServer, } from 'node:http';
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync, } from 'node:fs';
7
+ import { join, dirname, extname, resolve } from 'node:path';
8
+ const MIME_TYPES = {
9
+ '.jpg': 'image/jpeg',
10
+ '.jpeg': 'image/jpeg',
11
+ '.png': 'image/png',
12
+ '.gif': 'image/gif',
13
+ '.webp': 'image/webp',
14
+ '.svg': 'image/svg+xml',
15
+ '.json': 'application/json',
16
+ '.txt': 'text/plain',
17
+ };
18
+ function corsHeaders() {
19
+ return {
20
+ 'Access-Control-Allow-Origin': '*',
21
+ 'Access-Control-Allow-Methods': 'GET, PUT, DELETE, HEAD, OPTIONS',
22
+ 'Access-Control-Allow-Headers': '*',
23
+ 'Access-Control-Expose-Headers': 'ETag, Content-Length',
24
+ };
25
+ }
26
+ export function startS3Server(config) {
27
+ const storageDir = join(config.dataDir, 's3');
28
+ mkdirSync(storageDir, { recursive: true });
29
+ const server = createServer((req, res) => {
30
+ const headers = corsHeaders();
31
+ if (req.method === 'OPTIONS') {
32
+ res.writeHead(200, headers);
33
+ res.end();
34
+ return;
35
+ }
36
+ const url = new URL(req.url || '/', `http://localhost:${config.s3Port}`);
37
+ const filePath = resolve(join(storageDir, url.pathname));
38
+ if (!filePath.startsWith(resolve(storageDir))) {
39
+ res.writeHead(403, headers);
40
+ res.end();
41
+ return;
42
+ }
43
+ try {
44
+ switch (req.method) {
45
+ case 'GET': {
46
+ if (!existsSync(filePath) ||
47
+ statSync(filePath).isDirectory()) {
48
+ res.writeHead(404, {
49
+ ...headers,
50
+ 'Content-Type': 'application/xml',
51
+ });
52
+ res.end('<Error><Code>NoSuchKey</Code></Error>');
53
+ return;
54
+ }
55
+ const data = readFileSync(filePath);
56
+ const ext = extname(filePath);
57
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
58
+ res.writeHead(200, {
59
+ ...headers,
60
+ 'Content-Type': contentType,
61
+ 'Content-Length': data.length.toString(),
62
+ ETag: `"${Buffer.from(data).length}"`,
63
+ });
64
+ res.end(data);
65
+ break;
66
+ }
67
+ case 'PUT': {
68
+ const chunks = [];
69
+ req.on('data', (chunk) => chunks.push(chunk));
70
+ req.on('end', () => {
71
+ mkdirSync(dirname(filePath), {
72
+ recursive: true,
73
+ });
74
+ const body = Buffer.concat(chunks);
75
+ writeFileSync(filePath, body);
76
+ res.writeHead(200, {
77
+ ...headers,
78
+ ETag: `"${body.length}"`,
79
+ });
80
+ res.end();
81
+ });
82
+ break;
83
+ }
84
+ case 'DELETE': {
85
+ if (existsSync(filePath)) {
86
+ unlinkSync(filePath);
87
+ }
88
+ res.writeHead(204, headers);
89
+ res.end();
90
+ break;
91
+ }
92
+ case 'HEAD': {
93
+ if (!existsSync(filePath) ||
94
+ statSync(filePath).isDirectory()) {
95
+ res.writeHead(404, headers);
96
+ res.end();
97
+ return;
98
+ }
99
+ const stat = statSync(filePath);
100
+ const ext = extname(filePath);
101
+ res.writeHead(200, {
102
+ ...headers,
103
+ 'Content-Type': MIME_TYPES[ext] ||
104
+ 'application/octet-stream',
105
+ 'Content-Length': stat.size.toString(),
106
+ });
107
+ res.end();
108
+ break;
109
+ }
110
+ default:
111
+ res.writeHead(405, headers);
112
+ res.end();
113
+ }
114
+ }
115
+ catch (err) {
116
+ res.writeHead(500, {
117
+ ...headers,
118
+ 'Content-Type': 'application/xml',
119
+ });
120
+ res.end('<Error><Code>InternalError</Code></Error>');
121
+ }
122
+ });
123
+ return new Promise((resolve, reject) => {
124
+ server.listen(config.s3Port, '127.0.0.1', () => {
125
+ console.info(`[orez] local s3 listening on port ${config.s3Port}`);
126
+ resolve(server);
127
+ });
128
+ server.on('error', reject);
129
+ });
130
+ }
131
+ //# sourceMappingURL=s3-local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-local.js","sourceRoot":"","sources":["../src/s3-local.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "orez",
3
+ "version": "0.0.1",
4
+ "description": "PGlite-powered zero-sync development backend. No Docker required.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "lint": "oxlint --import-plugin",
25
+ "format": "oxfmt .",
26
+ "format:check": "oxfmt --check .",
27
+ "check": "tsc --noEmit",
28
+ "demo:test": "bun demo/src/test/run-e2e.ts",
29
+ "check:all": "bun run lint && bun run format:check && bun run check && bun run test",
30
+ "release": "bun scripts/release.ts"
31
+ },
32
+ "dependencies": {
33
+ "@electric-sql/pglite": "^0.2.17",
34
+ "pg-gateway": "^0.3.0-beta.4"
35
+ },
36
+ "devDependencies": {
37
+ "oxlint": "latest",
38
+ "oxfmt": "latest",
39
+ "typescript": "^5.7.0",
40
+ "vitest": "^3.0.0",
41
+ "@types/node": "^22.0.0"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/natew/zero-lite"
46
+ },
47
+ "keywords": [
48
+ "zero",
49
+ "pglite",
50
+ "postgres",
51
+ "replication",
52
+ "sync",
53
+ "zero-cache",
54
+ "development"
55
+ ]
56
+ }
package/src/config.ts ADDED
@@ -0,0 +1,40 @@
1
+ export interface ZeroLiteConfig {
2
+ dataDir: string
3
+ pgPort: number
4
+ zeroPort: number
5
+ s3Port: number
6
+ webPort: number
7
+ pgUser: string
8
+ pgPassword: string
9
+ migrationsDir: string
10
+ seedFile: string
11
+ skipZeroCache: boolean
12
+ }
13
+
14
+ export function getConfig(
15
+ overrides: Partial<ZeroLiteConfig> = {}
16
+ ): ZeroLiteConfig {
17
+ return {
18
+ dataDir: overrides.dataDir || '.zero-lite',
19
+ pgPort: overrides.pgPort || 6434,
20
+ zeroPort: overrides.zeroPort || 5849,
21
+ s3Port: overrides.s3Port || 10201,
22
+ webPort:
23
+ overrides.webPort ||
24
+ Number(process.env.VITE_PORT_WEB) ||
25
+ 8081,
26
+ pgUser: overrides.pgUser || 'user',
27
+ pgPassword: overrides.pgPassword || 'password',
28
+ migrationsDir:
29
+ overrides.migrationsDir || 'src/database/migrations',
30
+ seedFile: overrides.seedFile || 'src/database/seed.sql',
31
+ skipZeroCache: overrides.skipZeroCache || false,
32
+ }
33
+ }
34
+
35
+ export function getConnectionString(
36
+ config: ZeroLiteConfig,
37
+ dbName = 'postgres'
38
+ ): string {
39
+ return `postgresql://${config.pgUser}:${config.pgPassword}@127.0.0.1:${config.pgPort}/${dbName}`
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * orez: pglite-powered zero-sync development backend.
3
+ *
4
+ * starts a pglite instance, tcp proxy, s3 server, and zero-cache
5
+ * process. replaces docker-based postgresql, zero-cache, and minio
6
+ * with a single `bun run` command.
7
+ */
8
+
9
+ import { resolve, join } from 'node:path'
10
+ import {
11
+ existsSync,
12
+ readFileSync,
13
+ mkdirSync,
14
+ writeFileSync,
15
+ unlinkSync,
16
+ } from 'node:fs'
17
+ import { spawn, type ChildProcess } from 'node:child_process'
18
+
19
+ import type { PGlite } from '@electric-sql/pglite'
20
+ import type { Server } from 'node:net'
21
+ import type { Server as HttpServer } from 'node:http'
22
+
23
+ import { getConfig, getConnectionString } from './config'
24
+ import { installChangeTracking } from './replication/change-tracker'
25
+ import { createPGliteInstance, runMigrations } from './pglite-manager'
26
+ import { startPgProxy } from './pg-proxy'
27
+ import { startS3Server } from './s3-local'
28
+
29
+ import type { ZeroLiteConfig } from './config'
30
+
31
+ export type { ZeroLiteConfig } from './config'
32
+ export { getConfig, getConnectionString } from './config'
33
+
34
+ export async function startZeroLite(overrides: Partial<ZeroLiteConfig> = {}) {
35
+ const config = getConfig(overrides)
36
+ console.info('[orez] starting...')
37
+ console.info(`[orez] data dir: ${resolve(config.dataDir)}`)
38
+
39
+ mkdirSync(config.dataDir, { recursive: true })
40
+
41
+ // start pglite
42
+ const db = await createPGliteInstance(config)
43
+
44
+ // run migrations
45
+ await runMigrations(db, config)
46
+
47
+ // install change tracking
48
+ console.info('[orez] installing change tracking')
49
+ await installChangeTracking(db)
50
+
51
+ // start tcp proxy
52
+ const pgServer = await startPgProxy(db, config)
53
+
54
+ // start s3 server
55
+ const s3Server = await startS3Server(config)
56
+
57
+ // seed data if needed
58
+ await seedIfNeeded(db, config)
59
+
60
+ // write .env.local so `bun dev` connects to orez ports
61
+ writeEnvLocal(config)
62
+
63
+ // start zero-cache
64
+ let zeroCacheProcess: ChildProcess | null = null
65
+ if (!config.skipZeroCache) {
66
+ console.info('[orez] starting zero-cache...')
67
+ zeroCacheProcess = await startZeroCache(config)
68
+ await waitForZeroCache(config)
69
+ } else {
70
+ console.info('[orez] skipping zero-cache (skipZeroCache=true)')
71
+ }
72
+
73
+ const stop = async () => {
74
+ console.info('[orez] shutting down...')
75
+ if (zeroCacheProcess) {
76
+ zeroCacheProcess.kill('SIGTERM')
77
+ }
78
+ pgServer.close()
79
+ s3Server.close()
80
+ await db.close()
81
+ cleanupEnvLocal()
82
+ console.info('[orez] stopped')
83
+ }
84
+
85
+ return { config, stop }
86
+ }
87
+
88
+ const ENV_LOCAL_PATH = resolve('.env.local')
89
+ const ENV_LOCAL_MARKER = '# auto-generated by orez'
90
+
91
+ function writeEnvLocal(config: ZeroLiteConfig): void {
92
+ const upstreamUrl = getConnectionString(config, 'postgres')
93
+ const cvrUrl = getConnectionString(config, 'zero_cvr')
94
+ const cdbUrl = getConnectionString(config, 'zero_cdb')
95
+
96
+ const content = `${ENV_LOCAL_MARKER}
97
+ VITE_PORT_POSTGRES=${config.pgPort}
98
+ VITE_PORT_ZERO=${config.zeroPort}
99
+ VITE_PORT_MINIO=${config.s3Port}
100
+ VITE_PUBLIC_ZERO_SERVER="http://localhost:${config.zeroPort}"
101
+ ZERO_UPSTREAM_DB="${upstreamUrl}"
102
+ ZERO_CVR_DB="${cvrUrl}"
103
+ ZERO_CHANGE_DB="${cdbUrl}"
104
+ CLOUDFLARE_R2_ENDPOINT="http://localhost:${config.s3Port}"
105
+ CLOUDFLARE_R2_PUBLIC_URL="http://localhost:${config.s3Port}"
106
+ `
107
+ writeFileSync(ENV_LOCAL_PATH, content)
108
+ console.info('[orez] wrote .env.local for dev server')
109
+ }
110
+
111
+ function cleanupEnvLocal(): void {
112
+ try {
113
+ if (existsSync(ENV_LOCAL_PATH)) {
114
+ const content = readFileSync(ENV_LOCAL_PATH, 'utf-8')
115
+ if (content.startsWith(ENV_LOCAL_MARKER)) {
116
+ unlinkSync(ENV_LOCAL_PATH)
117
+ console.info('[orez] cleaned up .env.local')
118
+ }
119
+ }
120
+ } catch {
121
+ // ignore cleanup errors
122
+ }
123
+ }
124
+
125
+ async function seedIfNeeded(
126
+ db: PGlite,
127
+ config: ZeroLiteConfig
128
+ ): Promise<void> {
129
+ // check if we already have data
130
+ try {
131
+ const result = await db.query<{ count: string }>(
132
+ 'SELECT count(*) as count FROM public."user"'
133
+ )
134
+ if (Number(result.rows[0].count) > 0) {
135
+ return
136
+ }
137
+ } catch {
138
+ // table might not exist yet
139
+ }
140
+
141
+ console.info('[orez] seeding demo data...')
142
+ const seedFile = resolve(config.seedFile)
143
+ if (!existsSync(seedFile)) {
144
+ console.info('[orez] no seed file found, skipping')
145
+ return
146
+ }
147
+
148
+ const sql = readFileSync(seedFile, 'utf-8')
149
+ const statements = sql
150
+ .split('--> statement-breakpoint')
151
+ .map((s) => s.trim())
152
+ .filter(Boolean)
153
+
154
+ for (const stmt of statements) {
155
+ await db.exec(stmt)
156
+ }
157
+ console.info('[orez] seeded demo data')
158
+ }
159
+
160
+ async function startZeroCache(
161
+ config: ZeroLiteConfig
162
+ ): Promise<ChildProcess> {
163
+ // find zero-cache binary
164
+ const zeroCachePaths = [
165
+ resolve('node_modules/.bin/zero-cache'),
166
+ resolve(
167
+ 'node_modules/@rocicorp/zero/out/zero-cache/src/bin/main.js'
168
+ ),
169
+ ]
170
+
171
+ let zeroCacheBin = ''
172
+ for (const p of zeroCachePaths) {
173
+ if (existsSync(p)) {
174
+ zeroCacheBin = p
175
+ break
176
+ }
177
+ }
178
+
179
+ if (!zeroCacheBin) {
180
+ throw new Error(
181
+ 'zero-cache binary not found. install @rocicorp/zero'
182
+ )
183
+ }
184
+
185
+ const upstreamUrl = getConnectionString(config, 'postgres')
186
+ const cvrUrl = getConnectionString(config, 'zero_cvr')
187
+ const cdbUrl = getConnectionString(config, 'zero_cdb')
188
+
189
+ const env: Record<string, string> = {
190
+ ...(process.env as Record<string, string>),
191
+ NODE_ENV: 'development',
192
+ ZERO_UPSTREAM_DB: upstreamUrl,
193
+ ZERO_CVR_DB: cvrUrl,
194
+ ZERO_CHANGE_DB: cdbUrl,
195
+ ZERO_REPLICA_FILE: resolve(config.dataDir, 'zero-replica.db'),
196
+ ZERO_PORT: String(config.zeroPort),
197
+ ZERO_MUTATE_URL: `http://localhost:${config.webPort}/api/zero/push`,
198
+ ZERO_QUERY_URL: `http://localhost:${config.webPort}/api/zero/pull`,
199
+ ZERO_LOG_LEVEL: 'info',
200
+ ZERO_NUM_SYNC_WORKERS: '1',
201
+ DO_NOT_TRACK: '1',
202
+ }
203
+
204
+ const child = spawn(zeroCacheBin, [], {
205
+ env,
206
+ stdio: ['ignore', 'pipe', 'pipe'],
207
+ })
208
+
209
+ child.stdout?.on('data', (data: Buffer) => {
210
+ const lines = data.toString().trim().split('\n')
211
+ for (const line of lines) {
212
+ console.info(`[zero-cache] ${line}`)
213
+ }
214
+ })
215
+
216
+ child.stderr?.on('data', (data: Buffer) => {
217
+ const lines = data.toString().trim().split('\n')
218
+ for (const line of lines) {
219
+ console.info(`[zero-cache] ${line}`)
220
+ }
221
+ })
222
+
223
+ child.on('exit', (code) => {
224
+ if (code !== 0 && code !== null) {
225
+ console.info(`[zero-cache] exited with code ${code}`)
226
+ }
227
+ })
228
+
229
+ return child
230
+ }
231
+
232
+ async function waitForZeroCache(
233
+ config: ZeroLiteConfig,
234
+ timeoutMs = 60000
235
+ ): Promise<void> {
236
+ const start = Date.now()
237
+ const url = `http://127.0.0.1:${config.zeroPort}/`
238
+
239
+ while (Date.now() - start < timeoutMs) {
240
+ try {
241
+ const res = await fetch(url)
242
+ if (res.ok) {
243
+ console.info('[orez] zero-cache is ready')
244
+ return
245
+ }
246
+ } catch {
247
+ // not ready yet
248
+ }
249
+ await new Promise((r) => setTimeout(r, 500))
250
+ }
251
+
252
+ console.info(
253
+ '[orez] warning: zero-cache health check timed out, continuing anyway'
254
+ )
255
+ }