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.
- package/README.md +116 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +195 -0
- package/dist/index.js.map +1 -0
- package/dist/pg-proxy.d.ts +14 -0
- package/dist/pg-proxy.d.ts.map +1 -0
- package/dist/pg-proxy.js +385 -0
- package/dist/pg-proxy.js.map +1 -0
- package/dist/pglite-manager.d.ts +5 -0
- package/dist/pglite-manager.d.ts.map +1 -0
- package/dist/pglite-manager.js +71 -0
- package/dist/pglite-manager.js.map +1 -0
- package/dist/replication/change-tracker.d.ts +14 -0
- package/dist/replication/change-tracker.d.ts.map +1 -0
- package/dist/replication/change-tracker.js +86 -0
- package/dist/replication/change-tracker.js.map +1 -0
- package/dist/replication/handler.d.ts +24 -0
- package/dist/replication/handler.d.ts.map +1 -0
- package/dist/replication/handler.js +300 -0
- package/dist/replication/handler.js.map +1 -0
- package/dist/replication/pgoutput-encoder.d.ts +26 -0
- package/dist/replication/pgoutput-encoder.d.ts.map +1 -0
- package/dist/replication/pgoutput-encoder.js +204 -0
- package/dist/replication/pgoutput-encoder.js.map +1 -0
- package/dist/s3-local.d.ts +8 -0
- package/dist/s3-local.d.ts.map +1 -0
- package/dist/s3-local.js +131 -0
- package/dist/s3-local.js.map +1 -0
- package/package.json +56 -0
- package/src/config.ts +40 -0
- package/src/index.ts +255 -0
- package/src/pg-proxy.ts +474 -0
- package/src/pglite-manager.ts +105 -0
- package/src/replication/change-tracker.test.ts +179 -0
- package/src/replication/change-tracker.ts +115 -0
- package/src/replication/handler.test.ts +331 -0
- package/src/replication/handler.ts +378 -0
- package/src/replication/pgoutput-encoder.test.ts +381 -0
- package/src/replication/pgoutput-encoder.ts +252 -0
- package/src/replication/tcp-replication.test.ts +824 -0
- package/src/replication/zero-compat.test.ts +882 -0
- package/src/s3-local.ts +179 -0
package/dist/s3-local.js
ADDED
|
@@ -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
|
+
}
|