orez 0.1.36 → 0.1.38

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 (130) hide show
  1. package/dist/cli-entry.js +0 -0
  2. package/dist/cli.js +7 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +14 -11
  10. package/dist/index.js.map +1 -1
  11. package/dist/pg-proxy.d.ts.map +1 -1
  12. package/dist/pg-proxy.js +8 -4
  13. package/dist/pg-proxy.js.map +1 -1
  14. package/dist/pglite-manager.d.ts +12 -0
  15. package/dist/pglite-manager.d.ts.map +1 -1
  16. package/dist/pglite-manager.js +81 -0
  17. package/dist/pglite-manager.js.map +1 -1
  18. package/dist/recovery.js +2 -2
  19. package/dist/recovery.js.map +1 -1
  20. package/dist/replication/change-tracker.js +9 -9
  21. package/dist/replication/change-tracker.js.map +1 -1
  22. package/dist/replication/handler.d.ts +12 -0
  23. package/dist/replication/handler.d.ts.map +1 -1
  24. package/dist/replication/handler.js +34 -6
  25. package/dist/replication/handler.js.map +1 -1
  26. package/dist/worker/browser-build-config.d.ts +59 -0
  27. package/dist/worker/browser-build-config.d.ts.map +1 -0
  28. package/dist/worker/browser-build-config.js +101 -0
  29. package/dist/worker/browser-build-config.js.map +1 -0
  30. package/dist/worker/browser-embed.d.ts +58 -0
  31. package/dist/worker/browser-embed.d.ts.map +1 -0
  32. package/dist/worker/browser-embed.js +195 -0
  33. package/dist/worker/browser-embed.js.map +1 -0
  34. package/dist/worker/cf-patches.d.ts +20 -0
  35. package/dist/worker/cf-patches.d.ts.map +1 -0
  36. package/dist/worker/cf-patches.js +94 -0
  37. package/dist/worker/cf-patches.js.map +1 -0
  38. package/dist/worker/index.d.ts +12 -0
  39. package/dist/worker/index.d.ts.map +1 -0
  40. package/dist/worker/index.js +105 -0
  41. package/dist/worker/index.js.map +1 -0
  42. package/dist/worker/shims/fastify.d.ts +80 -0
  43. package/dist/worker/shims/fastify.d.ts.map +1 -0
  44. package/dist/worker/shims/fastify.js +223 -0
  45. package/dist/worker/shims/fastify.js.map +1 -0
  46. package/dist/worker/shims/http-service.d.ts +104 -0
  47. package/dist/worker/shims/http-service.d.ts.map +1 -0
  48. package/dist/worker/shims/http-service.js +198 -0
  49. package/dist/worker/shims/http-service.js.map +1 -0
  50. package/dist/worker/shims/node-stub.d.ts +147 -0
  51. package/dist/worker/shims/node-stub.d.ts.map +1 -0
  52. package/dist/worker/shims/node-stub.js +204 -0
  53. package/dist/worker/shims/node-stub.js.map +1 -0
  54. package/dist/worker/shims/postgres.d.ts +115 -0
  55. package/dist/worker/shims/postgres.d.ts.map +1 -0
  56. package/dist/worker/shims/postgres.js +1181 -0
  57. package/dist/worker/shims/postgres.js.map +1 -0
  58. package/dist/worker/shims/sqlite-browser.d.ts +54 -0
  59. package/dist/worker/shims/sqlite-browser.d.ts.map +1 -0
  60. package/dist/worker/shims/sqlite-browser.js +144 -0
  61. package/dist/worker/shims/sqlite-browser.js.map +1 -0
  62. package/dist/worker/shims/sqlite.d.ts +126 -0
  63. package/dist/worker/shims/sqlite.d.ts.map +1 -0
  64. package/dist/worker/shims/sqlite.js +599 -0
  65. package/dist/worker/shims/sqlite.js.map +1 -0
  66. package/dist/worker/shims/stream-browser.d.ts +9 -0
  67. package/dist/worker/shims/stream-browser.d.ts.map +1 -0
  68. package/dist/worker/shims/stream-browser.js +13 -0
  69. package/dist/worker/shims/stream-browser.js.map +1 -0
  70. package/dist/worker/shims/ws-browser.d.ts +50 -0
  71. package/dist/worker/shims/ws-browser.d.ts.map +1 -0
  72. package/dist/worker/shims/ws-browser.js +105 -0
  73. package/dist/worker/shims/ws-browser.js.map +1 -0
  74. package/dist/worker/shims/ws.d.ts +62 -0
  75. package/dist/worker/shims/ws.d.ts.map +1 -0
  76. package/dist/worker/shims/ws.js +310 -0
  77. package/dist/worker/shims/ws.js.map +1 -0
  78. package/dist/worker/types.d.ts +57 -0
  79. package/dist/worker/types.d.ts.map +1 -0
  80. package/dist/worker/types.js +9 -0
  81. package/dist/worker/types.js.map +1 -0
  82. package/dist/worker/zero-cache-embed-cf.d.ts +63 -0
  83. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -0
  84. package/dist/worker/zero-cache-embed-cf.js +268 -0
  85. package/dist/worker/zero-cache-embed-cf.js.map +1 -0
  86. package/dist/worker/zero-cache-embed.d.ts +66 -0
  87. package/dist/worker/zero-cache-embed.d.ts.map +1 -0
  88. package/dist/worker/zero-cache-embed.js +200 -0
  89. package/dist/worker/zero-cache-embed.js.map +1 -0
  90. package/package.json +62 -3
  91. package/src/cli-entry.ts +0 -0
  92. package/src/cli.ts +8 -1
  93. package/src/config.ts +2 -0
  94. package/src/index.ts +15 -10
  95. package/src/integration/integration.test.ts +1 -1
  96. package/src/integration/restore-live-stress.test.ts +2 -2
  97. package/src/pg-proxy.ts +9 -4
  98. package/src/pglite-manager.ts +111 -0
  99. package/src/recovery.ts +2 -2
  100. package/src/replication/change-tracker.test.ts +1 -1
  101. package/src/replication/change-tracker.ts +9 -9
  102. package/src/replication/handler.test.ts +37 -0
  103. package/src/replication/handler.ts +46 -6
  104. package/src/wasm-sqlite.test.ts +2 -1
  105. package/src/worker/browser-build-config.test.ts +59 -0
  106. package/src/worker/browser-build-config.ts +105 -0
  107. package/src/worker/browser-embed.ts +306 -0
  108. package/src/worker/cf-patches.ts +114 -0
  109. package/src/worker/embed-integration.test.ts +321 -0
  110. package/src/worker/index.ts +138 -0
  111. package/src/worker/shims/fastify.test.ts +255 -0
  112. package/src/worker/shims/fastify.ts +292 -0
  113. package/src/worker/shims/http-service.test.ts +355 -0
  114. package/src/worker/shims/http-service.ts +293 -0
  115. package/src/worker/shims/node-stub.ts +223 -0
  116. package/src/worker/shims/postgres.test.ts +364 -0
  117. package/src/worker/shims/postgres.ts +1434 -0
  118. package/src/worker/shims/sqlite-browser.test.ts +233 -0
  119. package/src/worker/shims/sqlite-browser.ts +178 -0
  120. package/src/worker/shims/sqlite.test.ts +641 -0
  121. package/src/worker/shims/sqlite.ts +731 -0
  122. package/src/worker/shims/ws-browser.test.ts +184 -0
  123. package/src/worker/shims/ws-browser.ts +125 -0
  124. package/src/worker/shims/ws.test.ts +288 -0
  125. package/src/worker/shims/ws.ts +367 -0
  126. package/src/worker/types.ts +75 -0
  127. package/src/worker/worker-integration.test.ts +223 -0
  128. package/src/worker/worker.test.ts +136 -0
  129. package/src/worker/zero-cache-embed-cf.ts +367 -0
  130. package/src/worker/zero-cache-embed.ts +277 -0
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import {
4
+ getBrowserAliases,
5
+ getBrowserDefine,
6
+ getBrowserBuildConfig,
7
+ } from './browser-build-config.js'
8
+
9
+ describe('browser build config', () => {
10
+ describe('getBrowserAliases', () => {
11
+ it('returns an alias map', () => {
12
+ const aliases = getBrowserAliases()
13
+ expect(typeof aliases).toBe('object')
14
+ })
15
+
16
+ it('includes orez shims', () => {
17
+ const aliases = getBrowserAliases()
18
+ expect(aliases.postgres).toBe('orez/worker/shims/postgres')
19
+ expect(aliases['@rocicorp/zero-sqlite3']).toBe('orez/worker/shims/sqlite')
20
+ expect(aliases.fastify).toBe('orez/worker/shims/fastify')
21
+ expect(aliases.ws).toBe('orez/worker/shims/ws')
22
+ })
23
+
24
+ it('includes Node.js polyfills', () => {
25
+ const aliases = getBrowserAliases()
26
+ expect(aliases['node:events']).toBe('events')
27
+ expect(aliases['node:stream']).toBe('stream-browserify')
28
+ expect(aliases['node:path']).toBe('path-browserify')
29
+ })
30
+
31
+ it('includes Node.js stubs', () => {
32
+ const aliases = getBrowserAliases()
33
+ expect(aliases['node:fs']).toBe('orez/worker/shims/node-stub')
34
+ expect(aliases['node:net']).toBe('orez/worker/shims/node-stub')
35
+ expect(aliases['node:child_process']).toBe('orez/worker/shims/node-stub')
36
+ expect(aliases['node:http']).toBe('orez/worker/shims/node-stub')
37
+ expect(aliases['node:crypto']).toBe('orez/worker/shims/node-stub')
38
+ })
39
+ })
40
+
41
+ describe('getBrowserDefine', () => {
42
+ it('returns define map', () => {
43
+ const define = getBrowserDefine()
44
+ expect(define['process.env.NODE_ENV']).toBe('"development"')
45
+ expect(define['process.env.SINGLE_PROCESS']).toBe('"1"')
46
+ })
47
+ })
48
+
49
+ describe('getBrowserBuildConfig', () => {
50
+ it('returns combined config', () => {
51
+ const config = getBrowserBuildConfig()
52
+ expect(config.alias).toBeDefined()
53
+ expect(config.define).toBeDefined()
54
+ expect(config.format).toBe('esm')
55
+ expect(config.platform).toBe('browser')
56
+ expect(config.bundle).toBe(true)
57
+ })
58
+ })
59
+ })
@@ -0,0 +1,105 @@
1
+ /**
2
+ * browser build configuration for zero-cache embed.
3
+ *
4
+ * provides the bundler alias map and polyfill configuration needed
5
+ * to bundle zero-cache for browser Web Workers.
6
+ *
7
+ * usage with esbuild:
8
+ *
9
+ * import { getBrowserAliases, getBrowserDefine } from 'orez/worker/browser-build-config'
10
+ *
11
+ * await esbuild.build({
12
+ * entryPoints: ['./my-worker.ts'],
13
+ * bundle: true,
14
+ * format: 'esm',
15
+ * alias: getBrowserAliases(),
16
+ * define: getBrowserDefine(),
17
+ * })
18
+ *
19
+ * usage with vite:
20
+ *
21
+ * import { getBrowserAliases } from 'orez/worker/browser-build-config'
22
+ *
23
+ * export default defineConfig({
24
+ * resolve: { alias: getBrowserAliases() },
25
+ * worker: { format: 'es' },
26
+ * })
27
+ */
28
+
29
+ /**
30
+ * bundler aliases that swap zero-cache's Node.js dependencies
31
+ * for browser-compatible shims.
32
+ *
33
+ * the consumer must have these packages installed:
34
+ * - orez (provides postgres/sqlite/fastify/ws shims)
35
+ * - events (EventEmitter polyfill)
36
+ * - buffer (Buffer polyfill)
37
+ * - process (process polyfill)
38
+ *
39
+ * optional (only needed if zero-cache code uses them):
40
+ * - crypto-browserify, stream-browserify, path-browserify, os-browserify
41
+ */
42
+ export function getBrowserAliases(): Record<string, string> {
43
+ return {
44
+ // -- orez shims for zero-cache dependencies --
45
+ postgres: 'orez/worker/shims/postgres',
46
+ '@rocicorp/zero-sqlite3': 'orez/worker/shims/sqlite',
47
+ fastify: 'orez/worker/shims/fastify',
48
+ ws: 'orez/worker/shims/ws',
49
+
50
+ // -- Node.js built-in polyfills --
51
+ // these are needed because zero-cache imports node: modules.
52
+ // the bundler replaces them with browser-compatible packages.
53
+ 'node:events': 'events',
54
+ 'node:buffer': 'buffer',
55
+ 'node:process': 'process/browser',
56
+ 'node:crypto': 'orez/worker/shims/node-stub',
57
+ 'crypto-browserify': 'orez/worker/shims/node-stub',
58
+ 'node:stream': 'stream-browserify',
59
+ 'node:path': 'path-browserify',
60
+ 'node:os': 'os-browserify/browser',
61
+
62
+ // -- stubs for Node.js modules that zero-cache imports but doesn't --
63
+ // -- use in SINGLE_PROCESS mode --
64
+ 'node:http': 'orez/worker/shims/node-stub',
65
+ 'node:net': 'orez/worker/shims/node-stub',
66
+ 'node:tls': 'orez/worker/shims/node-stub',
67
+ 'node:child_process': 'orez/worker/shims/node-stub',
68
+ 'node:fs': 'orez/worker/shims/node-stub',
69
+ 'node:fs/promises': 'orez/worker/shims/node-stub',
70
+ 'node:url': 'orez/worker/shims/node-stub',
71
+ 'node:util': 'orez/worker/shims/node-stub',
72
+ 'node:assert': 'orez/worker/shims/node-stub',
73
+ 'node:inspector/promises': 'orez/worker/shims/node-stub',
74
+ 'node:v8': 'orez/worker/shims/node-stub',
75
+ 'node:zlib': 'orez/worker/shims/node-stub',
76
+ }
77
+ }
78
+
79
+ /**
80
+ * esbuild define map for browser builds.
81
+ * replaces Node.js globals with browser equivalents.
82
+ */
83
+ export function getBrowserDefine(): Record<string, string> {
84
+ return {
85
+ 'process.env.NODE_ENV': '"development"',
86
+ 'process.env.SINGLE_PROCESS': '"1"',
87
+ 'process.versions.node': '"20.0.0"',
88
+ }
89
+ }
90
+
91
+ /**
92
+ * combined config for esbuild builds.
93
+ * merges aliases, define, and common settings.
94
+ */
95
+ export function getBrowserBuildConfig() {
96
+ return {
97
+ alias: getBrowserAliases(),
98
+ define: getBrowserDefine(),
99
+ // recommended esbuild settings for browser worker bundles
100
+ format: 'esm' as const,
101
+ platform: 'browser' as const,
102
+ target: 'es2022',
103
+ bundle: true,
104
+ }
105
+ }
@@ -0,0 +1,306 @@
1
+ // NOTE THIS IS NOT OREZ NODE THIS IS NOT A GOOD REFERENCE BECAUSE ITS OUR EARLY GUESS AT WHAT COULD WORK
2
+ // DO NOT STUDY THIS, THE OTHER STUFF IN SRC IS WHERE YOU EANT TO LOOK
3
+
4
+ /**
5
+ * zero-cache embedded runner for browser Web Workers.
6
+ *
7
+ * same pattern as the CF embed but for browser environments:
8
+ *
9
+ * postgres → orez/worker/shims/postgres (PGlite-backed)
10
+ * @rocicorp/zero-sqlite3 → orez/worker/shims/sqlite (sql.js or in-memory)
11
+ * fastify → orez/worker/shims/fastify (route capture)
12
+ * ws → orez/worker/shims/ws (MessagePort/WebSocket)
13
+ *
14
+ * the consumer's bundler (esbuild/vite) must configure these aliases
15
+ * plus Node.js polyfills. use getBrowserBuildConfig() for the alias map.
16
+ *
17
+ * usage:
18
+ *
19
+ * import { startZeroCacheEmbedBrowser } from 'orez/worker/browser-embed'
20
+ *
21
+ * const zc = await startZeroCacheEmbedBrowser({
22
+ * pglite: pg,
23
+ * sqlite: sqlJsDb, // optional — sql.js Database instance
24
+ * })
25
+ *
26
+ * // connect a Zero client via WebSocket-like object
27
+ * zc.handleWebSocket(wsOrPort)
28
+ *
29
+ * // or handle HTTP requests (push/pull)
30
+ * const result = await zc.handleHttp({ method: 'GET', url: '/' })
31
+ */
32
+
33
+ import EventEmitter from 'node:events'
34
+
35
+ // static import so the bundler can follow the dependency tree.
36
+ // @ts-expect-error — internal zero-cache module, no type declarations
37
+ import { runWorker as _runWorker } from '@rocicorp/zero/out/zero-cache/src/server/runner/run-worker.js'
38
+
39
+ import type { PGlite } from '@electric-sql/pglite'
40
+
41
+ const runWorkerFn = _runWorker as (
42
+ parent: unknown,
43
+ env: Record<string, string>
44
+ ) => Promise<void>
45
+
46
+ export interface ZeroCacheEmbedBrowserOptions {
47
+ /** PGlite instance */
48
+ pglite: PGlite
49
+
50
+ /**
51
+ * sql.js Database instance for SQLite replica storage.
52
+ * if not provided, looks for globalThis.__orez_sqljs_db,
53
+ * then falls back to an in-memory stub (limited functionality).
54
+ */
55
+ sqlite?: unknown
56
+
57
+ /** zero app ID (default: 'zero') */
58
+ appId?: string
59
+
60
+ /** publication names */
61
+ publications?: string[]
62
+
63
+ /** additional env vars passed to zero-cache */
64
+ env?: Record<string, string>
65
+
66
+ /** timeout in ms waiting for zero-cache ready (default: 30000) */
67
+ 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
+
77
+ export interface HttpResponse {
78
+ status: number
79
+ headers: Record<string, string>
80
+ body: string
81
+ }
82
+
83
+ /** WebSocket-like object — matches CF WebSocket, browser WebSocket, or MessagePort adapter */
84
+ interface WsLike {
85
+ readyState: number
86
+ send(data: string | ArrayBuffer | ArrayBufferView): void
87
+ close(code?: number, reason?: string): void
88
+ addEventListener(type: string, handler: (event: any) => void): void
89
+ removeEventListener(type: string, handler: (event: any) => void): void
90
+ }
91
+
92
+ export interface ZeroCacheEmbedBrowser {
93
+ /** whether zero-cache is ready */
94
+ readonly ready: boolean
95
+
96
+ /**
97
+ * handle a WebSocket connection from a Zero client.
98
+ * accepts any WebSocket-like object (browser WebSocket, MessagePort adapter, etc.)
99
+ * feeds it into zero-cache's handoff mechanism.
100
+ */
101
+ handleWebSocket(ws: WsLike, url?: string): void
102
+
103
+ /**
104
+ * handle an HTTP request (push/pull/health).
105
+ * for environments without the Fetch API Request/Response.
106
+ */
107
+ handleHttp(request: HttpRequest): Promise<HttpResponse>
108
+
109
+ /** stop zero-cache */
110
+ stop(): Promise<void>
111
+ }
112
+
113
+ export async function startZeroCacheEmbedBrowser(
114
+ opts: ZeroCacheEmbedBrowserOptions
115
+ ): Promise<ZeroCacheEmbedBrowser> {
116
+ const appId = opts.appId || 'zero'
117
+ const publications = opts.publications?.join(',') || `orez_${appId}_public`
118
+ const readyTimeout = opts.readyTimeout ?? 30000
119
+
120
+ // set up sqlite storage from sql.js or in-memory
121
+ if (opts.sqlite) {
122
+ // consumer provided a sql.js Database — create adapter
123
+ const { createSqlJsStorage } = await import('./shims/sqlite-browser.js')
124
+ ;(globalThis as any).__orez_do_sqlite = createSqlJsStorage(opts.sqlite as any)
125
+ } else if (!(globalThis as any).__orez_do_sqlite) {
126
+ // no sqlite provided — use in-memory stub
127
+ const { createInMemoryStorage } = await import('./shims/sqlite-browser.js')
128
+ ;(globalThis as any).__orez_do_sqlite = createInMemoryStorage()
129
+ }
130
+
131
+ // set up PGlite for postgres shim
132
+ ;(globalThis as any).__orez_pglite = opts.pglite
133
+
134
+ // ensure process globals exist (browser has no process)
135
+ ;(globalThis as any).process ??= {}
136
+ ;(globalThis as any).process.env ??= {}
137
+ ;(globalThis as any).process.pid ??= 1
138
+ ;(globalThis as any).process.argv ??= []
139
+ ;(globalThis as any).process.kill ??= () => {}
140
+
141
+ // CRITICAL: set SINGLE_PROCESS before zero-cache runs
142
+ ;(globalThis as any).process.env.SINGLE_PROCESS = '1'
143
+ ;(globalThis as any).process.env.NODE_ENV = 'development'
144
+
145
+ // create fake parent EventEmitter for zero-cache's runWorker()
146
+ const parent = new EventEmitter() as EventEmitter & {
147
+ send: (msg: unknown) => boolean
148
+ kill: (signal?: string) => void
149
+ pid: number
150
+ }
151
+
152
+ const parentEmitter = new EventEmitter()
153
+
154
+ parent.send = (message: unknown) => {
155
+ parentEmitter.emit('message', message)
156
+ return true
157
+ }
158
+ parent.kill = (signal = 'SIGTERM') => {
159
+ parent.emit(signal, signal)
160
+ }
161
+ parent.pid = 1
162
+
163
+ // shim process.exit
164
+ const origExit = (globalThis as any).process.exit
165
+ ;(globalThis as any).process.exit = (code?: number) => {
166
+ parent.emit('exit', code ?? 0)
167
+ }
168
+
169
+ // build env for zero-cache
170
+ const env: Record<string, string> = {
171
+ ...((globalThis as any).process.env as Record<string, string>),
172
+ SINGLE_PROCESS: '1',
173
+ NODE_ENV: 'development',
174
+ ZERO_UPSTREAM_DB: 'pglite://in-process',
175
+ ZERO_CVR_DB: 'pglite://in-process',
176
+ ZERO_CHANGE_DB: 'pglite://in-process',
177
+ ZERO_REPLICA_FILE: ':browser-sqlite:',
178
+ ZERO_PORT: '0',
179
+ ZERO_APP_ID: appId,
180
+ ZERO_APP_PUBLICATIONS: publications,
181
+ ZERO_LOG_LEVEL: opts.env?.ZERO_LOG_LEVEL || 'info',
182
+ ZERO_NUM_SYNC_WORKERS: opts.env?.ZERO_NUM_SYNC_WORKERS || '1',
183
+ ZERO_ENABLE_QUERY_PLANNER: 'false',
184
+ ...opts.env,
185
+ }
186
+
187
+ // wrap parent with onMessageType/onceMessageType helpers
188
+ const wrappedParent = new Proxy(parent, {
189
+ get(target, prop, receiver) {
190
+ if (prop === 'onMessageType') {
191
+ return (type: string, handler: (msg: unknown) => void) => {
192
+ target.on('message', (data: unknown) => {
193
+ if (Array.isArray(data) && data.length === 2 && data[0] === type) {
194
+ handler(data[1])
195
+ }
196
+ })
197
+ return receiver
198
+ }
199
+ }
200
+ if (prop === 'onceMessageType') {
201
+ return (type: string, handler: (msg: unknown) => void) => {
202
+ const listener = (data: unknown) => {
203
+ if (Array.isArray(data) && data.length === 2 && data[0] === type) {
204
+ target.off('message', listener)
205
+ handler(data[1])
206
+ }
207
+ }
208
+ target.on('message', listener)
209
+ return receiver
210
+ }
211
+ }
212
+ return Reflect.get(target, prop, receiver)
213
+ },
214
+ })
215
+
216
+ // state
217
+ let isReady = false
218
+ let runWorkerPromise: Promise<void> | null = null
219
+ let fastifyInstance: any = null
220
+
221
+ // wait for "ready" message
222
+ const readyPromise = new Promise<void>((resolve, reject) => {
223
+ const timeout = setTimeout(() => {
224
+ reject(
225
+ new Error(
226
+ `zero-cache browser embed: timed out waiting for ready after ${readyTimeout}ms`
227
+ )
228
+ )
229
+ }, readyTimeout)
230
+
231
+ parentEmitter.on('message', (msg: unknown) => {
232
+ if (Array.isArray(msg) && msg[0] === 'ready') {
233
+ clearTimeout(timeout)
234
+ isReady = true
235
+ resolve()
236
+ }
237
+ })
238
+ })
239
+
240
+ // start zero-cache
241
+ runWorkerPromise = runWorkerFn(wrappedParent, env).catch((err) => {
242
+ if (!isReady) {
243
+ throw err
244
+ }
245
+ })
246
+
247
+ await readyPromise
248
+
249
+ fastifyInstance = (globalThis as any).__orez_fastify_instance
250
+
251
+ return {
252
+ get ready() {
253
+ return isReady
254
+ },
255
+
256
+ handleWebSocket(ws: WsLike, url = '/', headers?: Record<string, string>) {
257
+ if (!isReady || !fastifyInstance?.server) return
258
+
259
+ const message = {
260
+ url,
261
+ headers: headers || {},
262
+ method: 'GET',
263
+ }
264
+
265
+ // feed the WebSocket into zero-cache's handoff mechanism.
266
+ // the fastify shim's server is an EventEmitter with onMessageType.
267
+ // installWebSocketHandoff (non-Server branch) listens for "handoff".
268
+ fastifyInstance.server.emit(
269
+ 'message',
270
+ ['handoff', { message, head: new Uint8Array(0) }],
271
+ ws // the WebSocket-like object as sendHandle
272
+ )
273
+ },
274
+
275
+ async handleHttp(request: HttpRequest): Promise<HttpResponse> {
276
+ if (!isReady || !fastifyInstance?.inject) {
277
+ return { status: 503, headers: {}, body: 'not ready' }
278
+ }
279
+
280
+ const result = await fastifyInstance.inject({
281
+ method: request.method,
282
+ url: request.url,
283
+ headers: request.headers || {},
284
+ payload: request.body,
285
+ })
286
+
287
+ return {
288
+ status: result.statusCode,
289
+ headers: result.headers,
290
+ body: result.body,
291
+ }
292
+ },
293
+
294
+ async stop() {
295
+ isReady = false
296
+ wrappedParent.kill('SIGTERM')
297
+ if (runWorkerPromise) {
298
+ await Promise.race([runWorkerPromise, new Promise((r) => setTimeout(r, 5000))])
299
+ }
300
+ if (origExit) {
301
+ ;(globalThis as any).process.exit = origExit
302
+ }
303
+ delete (globalThis as any).process.env.SINGLE_PROCESS
304
+ },
305
+ }
306
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * zero-cache CF Workers patches.
3
+ *
4
+ * applies patches to @rocicorp/zero's internal files so zero-cache
5
+ * can run in SINGLE_PROCESS mode on CF Workers where dynamic import()
6
+ * doesn't work.
7
+ *
8
+ * two patches:
9
+ * 1. worker-urls.js — replace file:// URLs with zero-worker:// identifiers
10
+ * 2. processes.js — replace dynamic import() with static worker module lookup
11
+ *
12
+ * usage in a post-build script:
13
+ *
14
+ * import { patchZeroCacheForCF } from 'orez/worker/cf-patches'
15
+ * patchZeroCacheForCF('./node_modules')
16
+ *
17
+ * idempotent: safe to run multiple times.
18
+ */
19
+
20
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs'
21
+ import { resolve } from 'node:path'
22
+
23
+ export function patchZeroCacheForCF(nodeModulesPath: string): void {
24
+ const zcBase = resolve(nodeModulesPath, '@rocicorp', 'zero', 'out', 'zero-cache', 'src')
25
+
26
+ patchWorkerUrls(zcBase)
27
+ patchProcesses(zcBase)
28
+ }
29
+
30
+ function patchWorkerUrls(zcBase: string): void {
31
+ const workerUrlsPath = resolve(zcBase, 'server', 'worker-urls.js')
32
+ if (!existsSync(workerUrlsPath)) {
33
+ console.warn('[orez] worker-urls.js not found at', workerUrlsPath)
34
+ return
35
+ }
36
+
37
+ const content = readFileSync(workerUrlsPath, 'utf-8')
38
+
39
+ // skip if already patched
40
+ if (content.includes('zero-worker://')) {
41
+ return
42
+ }
43
+
44
+ writeFileSync(
45
+ workerUrlsPath,
46
+ `// patched by orez for CF Workers (replaces file:// URLs with identifiers)
47
+ const u = (n) => new URL("zero-worker://" + n);
48
+ export const MAIN_URL = u("main");
49
+ export const CHANGE_STREAMER_URL = u("change-streamer");
50
+ export const REAPER_URL = u("reaper");
51
+ export const REPLICATOR_URL = u("replicator");
52
+ export const SYNCER_URL = u("syncer");
53
+ `
54
+ )
55
+ console.log('[orez] patched zero-cache worker-urls.js')
56
+ }
57
+
58
+ function patchProcesses(zcBase: string): void {
59
+ const processesPath = resolve(zcBase, 'types', 'processes.js')
60
+ if (!existsSync(processesPath)) {
61
+ console.warn('[orez] processes.js not found at', processesPath)
62
+ return
63
+ }
64
+
65
+ let code = readFileSync(processesPath, 'utf-8')
66
+
67
+ // skip if already patched
68
+ if (code.includes('__zc_workers')) {
69
+ return
70
+ }
71
+
72
+ // add static imports of all zero-cache worker modules at the top.
73
+ // these are relative to processes.js location in @rocicorp/zero.
74
+ const workerImports = `\
75
+ // patched by orez for CF Workers (static imports replace dynamic import())
76
+ import { default as __zc_main } from "../server/main.js";
77
+ import { default as __zc_change_streamer } from "../server/change-streamer.js";
78
+ import { default as __zc_reaper } from "../server/reaper.js";
79
+ import { default as __zc_replicator } from "../server/replicator.js";
80
+ import { default as __zc_syncer } from "../server/syncer.js";
81
+ const __zc_workers = {
82
+ "main": __zc_main,
83
+ "change-streamer": __zc_change_streamer,
84
+ "reaper": __zc_reaper,
85
+ "replicator": __zc_replicator,
86
+ "syncer": __zc_syncer,
87
+ };
88
+ `
89
+
90
+ // replace the dynamic import in childWorker with a synchronous lookup.
91
+ // original: import(moduleUrl.href).then(async ({ default: runWorker }) => ...
92
+ // patched: lookup __zc_workers by name, then continue as before
93
+ const dynamicImportPattern =
94
+ 'import(moduleUrl.href).then(async ({ default: runWorker })'
95
+ const staticLookup =
96
+ '((async () => { ' +
97
+ 'const _name = moduleUrl.hostname || moduleUrl.pathname.split("/").pop()?.replace(".js",""); ' +
98
+ 'const runWorker = __zc_workers[_name]; ' +
99
+ 'if (!runWorker) throw new Error("orez: unknown zero-cache worker: " + _name + " (available: " + Object.keys(__zc_workers).join(", ") + ")"); ' +
100
+ 'return { default: runWorker }; ' +
101
+ '})()).then(async ({ default: runWorker })'
102
+
103
+ if (!code.includes(dynamicImportPattern)) {
104
+ console.warn(
105
+ '[orez] could not find dynamic import pattern in processes.js. ' +
106
+ 'zero-cache version may have changed — check compatibility.'
107
+ )
108
+ return
109
+ }
110
+
111
+ code = workerImports + code.replace(dynamicImportPattern, staticLookup)
112
+ writeFileSync(processesPath, code)
113
+ console.log('[orez] patched zero-cache processes.js (static worker imports)')
114
+ }