orez 0.2.19 → 0.2.24
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/dist/browser.d.ts +5 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -0
- package/dist/browser.js.map +1 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +71 -50
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +27 -19
- package/dist/pglite-manager.js.map +1 -1
- package/dist/worker/browser-admin.d.ts +13 -0
- package/dist/worker/browser-admin.d.ts.map +1 -0
- package/dist/worker/browser-admin.js +33 -0
- package/dist/worker/browser-admin.js.map +1 -0
- package/dist/worker/browser-embed.d.ts +12 -12
- package/dist/worker/browser-embed.d.ts.map +1 -1
- package/dist/worker/browser-embed.js +7 -0
- package/dist/worker/browser-embed.js.map +1 -1
- package/package.json +14 -3
- package/src/browser.ts +7 -0
- package/src/pg-proxy.ts +83 -51
- package/src/pglite-manager.ts +28 -18
- package/src/worker/browser-admin.ts +52 -0
- package/src/worker/browser-embed-admin.test.ts +75 -0
- package/src/worker/browser-embed.ts +21 -12
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { HttpRequest, HttpResponse } from './browser-admin.js';
|
|
1
2
|
import type { PGlite } from '@electric-sql/pglite';
|
|
3
|
+
export type { HttpRequest, HttpResponse } from './browser-admin.js';
|
|
2
4
|
export interface ZeroCacheEmbedBrowserOptions {
|
|
3
5
|
/** PGlite instance */
|
|
4
6
|
pglite: PGlite;
|
|
@@ -16,17 +18,16 @@ export interface ZeroCacheEmbedBrowserOptions {
|
|
|
16
18
|
env?: Record<string, string>;
|
|
17
19
|
/** timeout in ms waiting for zero-cache ready (default: 30000) */
|
|
18
20
|
readyTimeout?: number;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
body: string;
|
|
21
|
+
/**
|
|
22
|
+
* intercept browser-mode orez admin routes before they reach zero-cache.
|
|
23
|
+
*
|
|
24
|
+
* browser embeds do not run the node admin dashboard or log store. leaving
|
|
25
|
+
* `/__orez/*` to fall through to zero-cache makes disabled admin look like
|
|
26
|
+
* an app/zero route miss. keep the contract explicit by returning empty
|
|
27
|
+
* admin responses for the small read-only surface and 404 for the rest.
|
|
28
|
+
* default: true.
|
|
29
|
+
*/
|
|
30
|
+
disableAdminApi?: boolean;
|
|
30
31
|
}
|
|
31
32
|
/** WebSocket-like object — matches CF WebSocket, browser WebSocket, or MessagePort adapter */
|
|
32
33
|
interface WsLike {
|
|
@@ -54,5 +55,4 @@ export interface ZeroCacheEmbedBrowser {
|
|
|
54
55
|
stop(): Promise<void>;
|
|
55
56
|
}
|
|
56
57
|
export declare function startZeroCacheEmbedBrowser(opts: ZeroCacheEmbedBrowserOptions): Promise<ZeroCacheEmbedBrowser>;
|
|
57
|
-
export {};
|
|
58
58
|
//# sourceMappingURL=browser-embed.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-embed.d.ts","sourceRoot":"","sources":["../../src/worker/browser-embed.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-embed.d.ts","sourceRoot":"","sources":["../../src/worker/browser-embed.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAElD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAOnE,MAAM,WAAW,4BAA4B;IAC3C,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IAEd;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IAEvB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE5B,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,8FAA8F;AAC9F,UAAU,MAAM;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,CAAA;IACxD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAA;IACnE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAA;CACvE;AAED,MAAM,WAAW,qBAAqB;IACpC,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IAEvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAE/C;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAEvD,sBAAsB;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACtB;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,qBAAqB,CAAC,CAmOhC"}
|
|
@@ -32,11 +32,13 @@ import EventEmitter from 'node:events';
|
|
|
32
32
|
// static import so the bundler can follow the dependency tree.
|
|
33
33
|
// @ts-expect-error — internal zero-cache module, no type declarations
|
|
34
34
|
import { runWorker as _runWorker } from '@rocicorp/zero/out/zero-cache/src/server/runner/run-worker.js';
|
|
35
|
+
import { handleDisabledBrowserAdminRequest } from './browser-admin.js';
|
|
35
36
|
const runWorkerFn = _runWorker;
|
|
36
37
|
export async function startZeroCacheEmbedBrowser(opts) {
|
|
37
38
|
const appId = opts.appId || 'zero';
|
|
38
39
|
const publications = opts.publications?.join(',') || `orez_${appId}_public`;
|
|
39
40
|
const readyTimeout = opts.readyTimeout ?? 30000;
|
|
41
|
+
const disableAdminApi = opts.disableAdminApi ?? true;
|
|
40
42
|
// set up sqlite storage from sql.js or in-memory
|
|
41
43
|
if (opts.sqlite) {
|
|
42
44
|
// consumer provided a sql.js Database — create adapter
|
|
@@ -196,6 +198,11 @@ export async function startZeroCacheEmbedBrowser(opts) {
|
|
|
196
198
|
}
|
|
197
199
|
},
|
|
198
200
|
async handleHttp(request) {
|
|
201
|
+
if (disableAdminApi) {
|
|
202
|
+
const adminResponse = handleDisabledBrowserAdminRequest(request);
|
|
203
|
+
if (adminResponse)
|
|
204
|
+
return adminResponse;
|
|
205
|
+
}
|
|
199
206
|
if (!isReady || !fastifyInstance?.inject) {
|
|
200
207
|
return { status: 503, headers: {}, body: 'not ready' };
|
|
201
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-embed.js","sourceRoot":"","sources":["../../src/worker/browser-embed.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,sEAAsE;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,YAAY,MAAM,aAAa,CAAA;AAEtC,+DAA+D;AAC/D,sEAAsE;AACtE,OAAO,EAAE,SAAS,IAAI,UAAU,EAAE,MAAM,+DAA+D,CAAA;
|
|
1
|
+
{"version":3,"file":"browser-embed.js","sourceRoot":"","sources":["../../src/worker/browser-embed.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "PGlite-powered zero-sync development backend. No Docker required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,12 +63,23 @@
|
|
|
63
63
|
"test:chat": "bun scripts/test-chat-integration.ts",
|
|
64
64
|
"test:chat:smoke": "bun scripts/test-chat-integration.ts --smoke",
|
|
65
65
|
"test:chat:e2e": "bun scripts/test-chat-e2e.ts",
|
|
66
|
-
"release": "bun scripts/release.ts"
|
|
66
|
+
"release": "bun scripts/release.ts",
|
|
67
|
+
"perf:load": "bun run perf/load/harness.ts",
|
|
68
|
+
"perf:memory": "bun run perf/memory/profile.ts",
|
|
69
|
+
"perf:bench": "bun run perf/scripts/bench-all.ts",
|
|
70
|
+
"perf:overhead": "bun run perf/scripts/bench-proxy-overhead.ts",
|
|
71
|
+
"perf:soak": "bun run perf/stability/soak.ts",
|
|
72
|
+
"perf:crash": "bun run perf/stability/crash-recovery.ts",
|
|
73
|
+
"perf:correctness": "bun test perf/stability/correctness.test.ts",
|
|
74
|
+
"perf:diagnose": "bun run perf/scripts/diagnose.ts",
|
|
75
|
+
"perf:quick": "bun run perf/scripts/quick-check.ts",
|
|
76
|
+
"perf:all": "bun run perf:bench && bun run perf:overhead && bun run perf:correctness && bun run perf:crash",
|
|
77
|
+
"perf:ci": "bun run perf:bench -- --output=perf/reports/bench.json && bun run perf:overhead && bun run perf:correctness"
|
|
67
78
|
},
|
|
68
79
|
"dependencies": {
|
|
69
80
|
"@electric-sql/pglite": "0.4.1",
|
|
70
81
|
"@electric-sql/pglite-tools": "^0.3.1",
|
|
71
|
-
"bedrock-sqlite": "0.2.
|
|
82
|
+
"bedrock-sqlite": "0.2.24",
|
|
72
83
|
"citty": "^0.2.0",
|
|
73
84
|
"pg-gateway": "0.3.0-beta.4",
|
|
74
85
|
"pgsql-parser": "^17.9.11",
|
package/src/browser.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface OrezBrowserConfig {
|
|
|
35
35
|
|
|
36
36
|
/** log level */
|
|
37
37
|
logLevel?: string
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* intercept browser-mode orez admin routes before they reach zero-cache.
|
|
41
|
+
* default: true.
|
|
42
|
+
*/
|
|
43
|
+
disableAdminApi?: boolean
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export interface OrezBrowserInstance {
|
|
@@ -157,6 +163,7 @@ export async function startOrezBrowser(
|
|
|
157
163
|
pglite: pgPostgres as unknown as PGlite,
|
|
158
164
|
appId,
|
|
159
165
|
publications: config.publications,
|
|
166
|
+
disableAdminApi: config.disableAdminApi ?? true,
|
|
160
167
|
env: {
|
|
161
168
|
ZERO_LOG_LEVEL: config.logLevel || 'info',
|
|
162
169
|
},
|
package/src/pg-proxy.ts
CHANGED
|
@@ -45,6 +45,17 @@ const SCHEMA_CACHE_TTL_MS = 30_000
|
|
|
45
45
|
// performance tracking
|
|
46
46
|
const proxyStats = { totalWaitMs: 0, totalExecMs: 0, count: 0, batches: 0 }
|
|
47
47
|
|
|
48
|
+
// query classification cache — avoids re-running regex on every repeated query.
|
|
49
|
+
// keys are trimmed+lowercased original query texts (before rewrites).
|
|
50
|
+
// entries are invalidated on DDL (schema changes may affect classification).
|
|
51
|
+
interface QueryClass {
|
|
52
|
+
isWrite: boolean
|
|
53
|
+
isDDL: boolean
|
|
54
|
+
isCacheable: boolean
|
|
55
|
+
}
|
|
56
|
+
const queryClassCache = new Map<string, QueryClass>()
|
|
57
|
+
const QUERY_CLASS_CACHE_MAX = 500
|
|
58
|
+
|
|
48
59
|
// query classification helpers — operate on pre-normalized (trimmed+lowercased) query strings
|
|
49
60
|
const SCHEMA_QUERY_MARKERS = [
|
|
50
61
|
'information_schema.',
|
|
@@ -86,6 +97,33 @@ function isDDLNormalized(q: string): boolean {
|
|
|
86
97
|
return false
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
/**
|
|
101
|
+
* classify a query and cache the result.
|
|
102
|
+
* repeated queries (common in app workloads) hit the cache and skip all regex.
|
|
103
|
+
* invalidated on DDL (schema changes may reclassify queries).
|
|
104
|
+
*/
|
|
105
|
+
function classifyQuery(q: string): QueryClass {
|
|
106
|
+
const cached = queryClassCache.get(q)
|
|
107
|
+
if (cached) return cached
|
|
108
|
+
|
|
109
|
+
const result: QueryClass = {
|
|
110
|
+
isWrite: isWriteNormalized(q),
|
|
111
|
+
isDDL: isDDLNormalized(q),
|
|
112
|
+
isCacheable: isCacheableNormalized(q),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// LRU-ish eviction: clear oldest half when full
|
|
116
|
+
if (queryClassCache.size >= QUERY_CLASS_CACHE_MAX) {
|
|
117
|
+
const keys = [...queryClassCache.keys()]
|
|
118
|
+
for (let i = 0; i < Math.floor(keys.length / 2); i++) {
|
|
119
|
+
queryClassCache.delete(keys[i])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
queryClassCache.set(q, result)
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
|
|
89
127
|
function extractQueryText(data: Uint8Array): string | null {
|
|
90
128
|
if (data[0] === 0x51) {
|
|
91
129
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
@@ -100,6 +138,7 @@ function extractQueryText(data: Uint8Array): string | null {
|
|
|
100
138
|
|
|
101
139
|
function invalidateSchemaCache() {
|
|
102
140
|
schemaQueryCache.clear()
|
|
141
|
+
queryClassCache.clear()
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
// abort previous replication handler when a new one starts
|
|
@@ -300,22 +339,6 @@ function interceptQuery(data: Uint8Array): Uint8Array {
|
|
|
300
339
|
return data
|
|
301
340
|
}
|
|
302
341
|
|
|
303
|
-
/**
|
|
304
|
-
* check if a query should be intercepted as a no-op.
|
|
305
|
-
*/
|
|
306
|
-
function isNoopQuery(data: Uint8Array): boolean {
|
|
307
|
-
let query: string | null = null
|
|
308
|
-
if (data[0] === 0x51) {
|
|
309
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
310
|
-
const len = view.getInt32(1)
|
|
311
|
-
query = textDecoder.decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '')
|
|
312
|
-
} else if (data[0] === 0x50) {
|
|
313
|
-
query = extractParseQuery(data)
|
|
314
|
-
}
|
|
315
|
-
if (!query) return false
|
|
316
|
-
return NOOP_QUERY_PATTERNS.some((p) => p.test(query!))
|
|
317
|
-
}
|
|
318
|
-
|
|
319
342
|
/**
|
|
320
343
|
* build a synthetic "SET" command complete response.
|
|
321
344
|
*/
|
|
@@ -495,6 +518,7 @@ function extractNoticeCode(
|
|
|
495
518
|
/**
|
|
496
519
|
* single-pass response message filter. strips ReadyForQuery messages (when
|
|
497
520
|
* stripRfq=true) and benign transaction state warnings in one scan.
|
|
521
|
+
* returns the original buffer unchanged when nothing was stripped.
|
|
498
522
|
*/
|
|
499
523
|
function stripResponseMessages(data: Uint8Array, stripRfq: boolean): Uint8Array {
|
|
500
524
|
if (data.length === 0) return data
|
|
@@ -837,43 +861,50 @@ export async function startPgProxy(
|
|
|
837
861
|
|
|
838
862
|
// Simple Query (0x51) or standalone Sync — per-message mutex
|
|
839
863
|
|
|
840
|
-
//
|
|
841
|
-
|
|
864
|
+
// extract query text ONCE for all checks (ping, noop, classification, caching)
|
|
865
|
+
let queryText: string | null = null
|
|
866
|
+
let queryClass: QueryClass | null = null
|
|
867
|
+
// cache/dedup key — the rewritten query when a rewrite applies, else
|
|
868
|
+
// the original. read and write paths MUST use the same key.
|
|
869
|
+
let schemaCacheKey: string | null = null
|
|
842
870
|
if (msgType === 0x51) {
|
|
843
|
-
|
|
871
|
+
queryText = extractQueryText(data)
|
|
844
872
|
if (queryText) {
|
|
873
|
+
// fast-path: ping queries — bypass mutex entirely
|
|
845
874
|
const pingMatch = queryText.match(PING_QUERY_RE)
|
|
846
875
|
if (pingMatch) {
|
|
847
876
|
return buildSelectIntResponse(pingMatch[1])
|
|
848
877
|
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
878
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// intercept and rewrite queries
|
|
860
|
-
data = interceptQuery(data)
|
|
861
|
-
|
|
862
|
-
// normalize query once for all classification checks
|
|
863
|
-
const isSimpleQuery = msgType === 0x51
|
|
864
|
-
const queryText = isSimpleQuery ? extractQueryText(data) : null
|
|
865
|
-
const queryNorm = queryText ? queryText.trimStart().toLowerCase() : null
|
|
866
|
-
const cacheable = queryNorm && isCacheableNormalized(queryNorm)
|
|
879
|
+
// fast-path: no-op queries — synthetic response, no mutex
|
|
880
|
+
if (NOOP_QUERY_PATTERNS.some((p) => p.test(queryText!))) {
|
|
881
|
+
return buildSetCompleteResponse()
|
|
882
|
+
}
|
|
867
883
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
884
|
+
// normalize once, classify once (cached for repeated queries)
|
|
885
|
+
queryClass = classifyQuery(queryText.trimStart().toLowerCase())
|
|
886
|
+
|
|
887
|
+
// schema query cache: identical information_schema queries deduplicated
|
|
888
|
+
if (queryClass.isCacheable) {
|
|
889
|
+
// apply rewrites before caching (version() etc. change the query)
|
|
890
|
+
const rewritten = applyRewrites(queryText)
|
|
891
|
+
schemaCacheKey = rewritten !== queryText ? rewritten : queryText
|
|
892
|
+
const cached = schemaQueryCache.get(schemaCacheKey)
|
|
893
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
894
|
+
return stripResponseMessages(cached.result, false)
|
|
895
|
+
}
|
|
896
|
+
const inflight = schemaQueryInFlight.get(schemaCacheKey)
|
|
897
|
+
if (inflight) {
|
|
898
|
+
return stripResponseMessages(await inflight, false)
|
|
899
|
+
}
|
|
900
|
+
// rewrite data for execution
|
|
901
|
+
if (rewritten !== queryText) {
|
|
902
|
+
data = rebuildSimpleQuery(rewritten)
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
// apply query rewrites for non-cacheable queries
|
|
906
|
+
data = interceptQuery(data)
|
|
907
|
+
}
|
|
877
908
|
}
|
|
878
909
|
}
|
|
879
910
|
|
|
@@ -914,21 +945,22 @@ export async function startPgProxy(
|
|
|
914
945
|
}
|
|
915
946
|
|
|
916
947
|
let result: Uint8Array
|
|
917
|
-
|
|
948
|
+
const cacheable = queryClass?.isCacheable ?? false
|
|
949
|
+
if (cacheable && schemaCacheKey) {
|
|
918
950
|
const promise = execute()
|
|
919
|
-
schemaQueryInFlight.set(
|
|
951
|
+
schemaQueryInFlight.set(schemaCacheKey, promise)
|
|
920
952
|
try {
|
|
921
953
|
result = await promise
|
|
922
|
-
schemaQueryCache.set(
|
|
954
|
+
schemaQueryCache.set(schemaCacheKey, {
|
|
923
955
|
result,
|
|
924
956
|
expiresAt: Date.now() + SCHEMA_CACHE_TTL_MS,
|
|
925
957
|
})
|
|
926
958
|
} finally {
|
|
927
|
-
schemaQueryInFlight.delete(
|
|
959
|
+
schemaQueryInFlight.delete(schemaCacheKey)
|
|
928
960
|
}
|
|
929
961
|
} else {
|
|
930
962
|
result = await execute()
|
|
931
|
-
if (
|
|
963
|
+
if (queryClass?.isDDL) {
|
|
932
964
|
invalidateSchemaCache()
|
|
933
965
|
}
|
|
934
966
|
}
|
|
@@ -937,7 +969,7 @@ export async function startPgProxy(
|
|
|
937
969
|
result = stripResponseMessages(result, stripRfq)
|
|
938
970
|
|
|
939
971
|
// signal replication handler on postgres writes for instant sync
|
|
940
|
-
if (dbName === 'postgres' &&
|
|
972
|
+
if (dbName === 'postgres' && queryClass?.isWrite) {
|
|
941
973
|
signalReplicationChange()
|
|
942
974
|
}
|
|
943
975
|
|
package/src/pglite-manager.ts
CHANGED
|
@@ -115,7 +115,6 @@ async function ensurePublication(db: {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// pglite startParams replaces defaults, so always include required flags
|
|
119
118
|
const PGLITE_BASE_FLAGS = [
|
|
120
119
|
'--single',
|
|
121
120
|
'-F',
|
|
@@ -127,6 +126,27 @@ const PGLITE_BASE_FLAGS = [
|
|
|
127
126
|
'exit_on_error=false',
|
|
128
127
|
'-c',
|
|
129
128
|
'log_checkpoints=false',
|
|
129
|
+
'-c',
|
|
130
|
+
'jit=off',
|
|
131
|
+
'-c',
|
|
132
|
+
'max_connections=5',
|
|
133
|
+
'-c',
|
|
134
|
+
'temp_buffers=1MB',
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
// main instance: tuned for development (matching soot browser config)
|
|
138
|
+
const MAIN_START_PARAMS = [
|
|
139
|
+
...PGLITE_BASE_FLAGS,
|
|
140
|
+
'-c',
|
|
141
|
+
'shared_buffers=1MB',
|
|
142
|
+
'-c',
|
|
143
|
+
'wal_buffers=64kB',
|
|
144
|
+
'-c',
|
|
145
|
+
'work_mem=1MB',
|
|
146
|
+
'-c',
|
|
147
|
+
'maintenance_work_mem=4MB',
|
|
148
|
+
'-c',
|
|
149
|
+
'effective_cache_size=16MB',
|
|
130
150
|
]
|
|
131
151
|
|
|
132
152
|
// cvr/cdb are just zero-cache bookkeeping — minimal fixed memory
|
|
@@ -135,13 +155,15 @@ const ZERO_START_PARAMS = [
|
|
|
135
155
|
'-c',
|
|
136
156
|
'shared_buffers=128kB',
|
|
137
157
|
'-c',
|
|
138
|
-
'wal_buffers=
|
|
158
|
+
'wal_buffers=32kB',
|
|
139
159
|
'-c',
|
|
140
160
|
'work_mem=64kB',
|
|
141
161
|
'-c',
|
|
142
|
-
'maintenance_work_mem=
|
|
162
|
+
'maintenance_work_mem=512kB',
|
|
163
|
+
'-c',
|
|
164
|
+
'temp_buffers=400kB',
|
|
143
165
|
'-c',
|
|
144
|
-
'
|
|
166
|
+
'max_connections=1',
|
|
145
167
|
]
|
|
146
168
|
|
|
147
169
|
// create a single pglite instance with given dataDir suffix
|
|
@@ -176,18 +198,12 @@ async function createInstance(
|
|
|
176
198
|
dataDir: dataPath,
|
|
177
199
|
debug: config.logLevel === 'debug' ? 1 : 0,
|
|
178
200
|
relaxedDurability: true,
|
|
179
|
-
initialMemory: isMain ?
|
|
201
|
+
initialMemory: isMain ? 16 * 1024 * 1024 : 8 * 1024 * 1024,
|
|
180
202
|
...(isMain ? {} : { startParams: ZERO_START_PARAMS }),
|
|
181
203
|
// main instance: user overrides via pgliteOptions, zero instances: fixed
|
|
182
204
|
...(isMain
|
|
183
205
|
? {
|
|
184
|
-
startParams:
|
|
185
|
-
...PGLITE_BASE_FLAGS,
|
|
186
|
-
'-c',
|
|
187
|
-
'shared_buffers=4MB',
|
|
188
|
-
'-c',
|
|
189
|
-
'wal_buffers=1MB',
|
|
190
|
-
],
|
|
206
|
+
startParams: MAIN_START_PARAMS,
|
|
191
207
|
...userOpts,
|
|
192
208
|
extensions: userOpts.extensions || {
|
|
193
209
|
vector,
|
|
@@ -211,14 +227,8 @@ async function createInstance(
|
|
|
211
227
|
|
|
212
228
|
if (isMain) {
|
|
213
229
|
await db.exec(`
|
|
214
|
-
SET work_mem = '4MB';
|
|
215
|
-
SET maintenance_work_mem = '16MB';
|
|
216
|
-
SET effective_cache_size = '64MB';
|
|
217
230
|
SET random_page_cost = 1.1;
|
|
218
|
-
SET jit = off;
|
|
219
231
|
`)
|
|
220
|
-
} else {
|
|
221
|
-
await db.exec(`SET jit = off;`)
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
log.debug.pglite(`${name} ready`)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface HttpRequest {
|
|
2
|
+
method: string
|
|
3
|
+
url: string
|
|
4
|
+
headers?: Record<string, string>
|
|
5
|
+
body?: string | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HttpResponse {
|
|
9
|
+
status: number
|
|
10
|
+
headers: Record<string, string>
|
|
11
|
+
body: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ADMIN_HEADERS = {
|
|
15
|
+
'access-control-allow-origin': '*',
|
|
16
|
+
'access-control-allow-methods': 'GET, OPTIONS',
|
|
17
|
+
'access-control-allow-headers': '*',
|
|
18
|
+
'content-type': 'application/json',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function jsonResponse(status: number, body: unknown): HttpResponse {
|
|
22
|
+
return {
|
|
23
|
+
status,
|
|
24
|
+
headers: ADMIN_HEADERS,
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function handleDisabledBrowserAdminRequest(
|
|
30
|
+
request: HttpRequest
|
|
31
|
+
): HttpResponse | null {
|
|
32
|
+
const url = new URL(request.url, 'http://localhost')
|
|
33
|
+
if (!url.pathname.startsWith('/__orez/')) return null
|
|
34
|
+
|
|
35
|
+
const method = request.method.toUpperCase()
|
|
36
|
+
if (method === 'OPTIONS') {
|
|
37
|
+
return { status: 200, headers: ADMIN_HEADERS, body: '' }
|
|
38
|
+
}
|
|
39
|
+
if (method !== 'GET') {
|
|
40
|
+
return jsonResponse(405, { error: 'method not allowed', admin: 'disabled' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (url.pathname === '/__orez/api/logs') {
|
|
44
|
+
return jsonResponse(200, { entries: [], cursor: 0, admin: 'disabled' })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (url.pathname === '/__orez/api/status') {
|
|
48
|
+
return jsonResponse(200, { ready: true, admin: 'disabled' })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return jsonResponse(404, { error: 'not found', admin: 'disabled' })
|
|
52
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { handleDisabledBrowserAdminRequest } from './browser-admin.js'
|
|
4
|
+
|
|
5
|
+
describe('disabled browser admin api', () => {
|
|
6
|
+
it('ignores non-admin routes', () => {
|
|
7
|
+
expect(
|
|
8
|
+
handleDisabledBrowserAdminRequest({
|
|
9
|
+
method: 'GET',
|
|
10
|
+
url: '/sync/v1/connect',
|
|
11
|
+
})
|
|
12
|
+
).toBeNull()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('serves empty logs for disabled browser admin', () => {
|
|
16
|
+
const response = handleDisabledBrowserAdminRequest({
|
|
17
|
+
method: 'GET',
|
|
18
|
+
url: '/__orez/api/logs?limit=100',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
expect(response?.status).toBe(200)
|
|
22
|
+
expect(response?.headers['content-type']).toBe('application/json')
|
|
23
|
+
expect(JSON.parse(response?.body ?? '')).toEqual({
|
|
24
|
+
entries: [],
|
|
25
|
+
cursor: 0,
|
|
26
|
+
admin: 'disabled',
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('serves status for disabled browser admin', () => {
|
|
31
|
+
const response = handleDisabledBrowserAdminRequest({
|
|
32
|
+
method: 'GET',
|
|
33
|
+
url: 'http://localhost:7849/__orez/api/status',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(response?.status).toBe(200)
|
|
37
|
+
expect(JSON.parse(response?.body ?? '')).toEqual({
|
|
38
|
+
ready: true,
|
|
39
|
+
admin: 'disabled',
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('handles preflight and disallows writes', () => {
|
|
44
|
+
expect(
|
|
45
|
+
handleDisabledBrowserAdminRequest({
|
|
46
|
+
method: 'OPTIONS',
|
|
47
|
+
url: '/__orez/api/logs',
|
|
48
|
+
})?.status
|
|
49
|
+
).toBe(200)
|
|
50
|
+
|
|
51
|
+
const response = handleDisabledBrowserAdminRequest({
|
|
52
|
+
method: 'POST',
|
|
53
|
+
url: '/__orez/api/actions/restart-zero',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(response?.status).toBe(405)
|
|
57
|
+
expect(JSON.parse(response?.body ?? '')).toEqual({
|
|
58
|
+
error: 'method not allowed',
|
|
59
|
+
admin: 'disabled',
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('keeps unknown admin routes explicit', () => {
|
|
64
|
+
const response = handleDisabledBrowserAdminRequest({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: '/__orez/api/actions/restart-zero',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(response?.status).toBe(404)
|
|
70
|
+
expect(JSON.parse(response?.body ?? '')).toEqual({
|
|
71
|
+
error: 'not found',
|
|
72
|
+
admin: 'disabled',
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -36,8 +36,13 @@ import EventEmitter from 'node:events'
|
|
|
36
36
|
// @ts-expect-error — internal zero-cache module, no type declarations
|
|
37
37
|
import { runWorker as _runWorker } from '@rocicorp/zero/out/zero-cache/src/server/runner/run-worker.js'
|
|
38
38
|
|
|
39
|
+
import { handleDisabledBrowserAdminRequest } from './browser-admin.js'
|
|
40
|
+
|
|
41
|
+
import type { HttpRequest, HttpResponse } from './browser-admin.js'
|
|
39
42
|
import type { PGlite } from '@electric-sql/pglite'
|
|
40
43
|
|
|
44
|
+
export type { HttpRequest, HttpResponse } from './browser-admin.js'
|
|
45
|
+
|
|
41
46
|
const runWorkerFn = _runWorker as (
|
|
42
47
|
parent: unknown,
|
|
43
48
|
env: Record<string, string>
|
|
@@ -65,19 +70,17 @@ export interface ZeroCacheEmbedBrowserOptions {
|
|
|
65
70
|
|
|
66
71
|
/** timeout in ms waiting for zero-cache ready (default: 30000) */
|
|
67
72
|
readyTimeout?: number
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface HttpRequest {
|
|
71
|
-
method: string
|
|
72
|
-
url: string
|
|
73
|
-
headers?: Record<string, string>
|
|
74
|
-
body?: string | null
|
|
75
|
-
}
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
/**
|
|
75
|
+
* intercept browser-mode orez admin routes before they reach zero-cache.
|
|
76
|
+
*
|
|
77
|
+
* browser embeds do not run the node admin dashboard or log store. leaving
|
|
78
|
+
* `/__orez/*` to fall through to zero-cache makes disabled admin look like
|
|
79
|
+
* an app/zero route miss. keep the contract explicit by returning empty
|
|
80
|
+
* admin responses for the small read-only surface and 404 for the rest.
|
|
81
|
+
* default: true.
|
|
82
|
+
*/
|
|
83
|
+
disableAdminApi?: boolean
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/** WebSocket-like object — matches CF WebSocket, browser WebSocket, or MessagePort adapter */
|
|
@@ -116,6 +119,7 @@ export async function startZeroCacheEmbedBrowser(
|
|
|
116
119
|
const appId = opts.appId || 'zero'
|
|
117
120
|
const publications = opts.publications?.join(',') || `orez_${appId}_public`
|
|
118
121
|
const readyTimeout = opts.readyTimeout ?? 30000
|
|
122
|
+
const disableAdminApi = opts.disableAdminApi ?? true
|
|
119
123
|
|
|
120
124
|
// set up sqlite storage from sql.js or in-memory
|
|
121
125
|
if (opts.sqlite) {
|
|
@@ -303,6 +307,11 @@ export async function startZeroCacheEmbedBrowser(
|
|
|
303
307
|
},
|
|
304
308
|
|
|
305
309
|
async handleHttp(request: HttpRequest): Promise<HttpResponse> {
|
|
310
|
+
if (disableAdminApi) {
|
|
311
|
+
const adminResponse = handleDisabledBrowserAdminRequest(request)
|
|
312
|
+
if (adminResponse) return adminResponse
|
|
313
|
+
}
|
|
314
|
+
|
|
306
315
|
if (!isReady || !fastifyInstance?.inject) {
|
|
307
316
|
return { status: 503, headers: {}, body: 'not ready' }
|
|
308
317
|
}
|