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,136 @@
1
+ import { PGlite } from '@electric-sql/pglite'
2
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
3
+
4
+ import { createOrezWorker } from './index'
5
+
6
+ import type { OrezWorker } from './types'
7
+
8
+ describe('orez/worker', () => {
9
+ let worker: OrezWorker
10
+
11
+ beforeEach(async () => {
12
+ worker = await createOrezWorker({
13
+ pgliteOptions: { dataDir: 'memory://' },
14
+ })
15
+ })
16
+
17
+ afterEach(async () => {
18
+ await worker.close()
19
+ })
20
+
21
+ it('creates worker with pgliteOptions', () => {
22
+ expect(worker.db).toBeDefined()
23
+ expect(worker.ownsInstance).toBe(true)
24
+ })
25
+
26
+ it('creates worker with pre-existing PGlite', async () => {
27
+ const pglite = new PGlite()
28
+ await pglite.waitReady
29
+ const w = await createOrezWorker({ pglite })
30
+ expect(w.db).toBe(pglite)
31
+ expect(w.ownsInstance).toBe(false)
32
+ await w.close()
33
+ // pglite should still be open since worker doesn't own it
34
+ expect(pglite.closed).toBe(false)
35
+ await pglite.close()
36
+ })
37
+
38
+ it('throws without pglite or pgliteOptions', async () => {
39
+ await expect(createOrezWorker({})).rejects.toThrow(
40
+ 'provide either pglite or pgliteOptions'
41
+ )
42
+ })
43
+
44
+ it('exec and query work', async () => {
45
+ await worker.exec(`
46
+ CREATE TABLE public.items (
47
+ id SERIAL PRIMARY KEY,
48
+ name TEXT NOT NULL
49
+ )
50
+ `)
51
+ await worker.installChangeTracking()
52
+ await worker.query('INSERT INTO public.items (name) VALUES ($1)', ['hello'])
53
+ const result = await worker.query<{ id: number; name: string }>(
54
+ 'SELECT * FROM public.items'
55
+ )
56
+ expect(result.rows).toHaveLength(1)
57
+ expect(result.rows[0].name).toBe('hello')
58
+ })
59
+
60
+ it('change tracking captures mutations', async () => {
61
+ await worker.exec(`
62
+ CREATE TABLE public.things (
63
+ id TEXT PRIMARY KEY,
64
+ val INTEGER
65
+ )
66
+ `)
67
+ // reinstall after creating table so triggers are on the new table
68
+ await worker.installChangeTracking()
69
+
70
+ await worker.exec(`INSERT INTO public.things VALUES ('a', 1)`)
71
+ await worker.exec(`UPDATE public.things SET val = 2 WHERE id = 'a'`)
72
+ await worker.exec(`DELETE FROM public.things WHERE id = 'a'`)
73
+
74
+ const changes = await worker.getChangesSince(0)
75
+ expect(changes).toHaveLength(3)
76
+ expect(changes[0].op).toBe('INSERT')
77
+ expect(changes[0].table_name).toBe('public.things')
78
+ expect(changes[0].row_data).toMatchObject({ id: 'a', val: 1 })
79
+ expect(changes[1].op).toBe('UPDATE')
80
+ expect(changes[1].row_data).toMatchObject({ id: 'a', val: 2 })
81
+ expect(changes[1].old_data).toMatchObject({ id: 'a', val: 1 })
82
+ expect(changes[2].op).toBe('DELETE')
83
+ expect(changes[2].old_data).toMatchObject({ id: 'a', val: 2 })
84
+ })
85
+
86
+ it('watermark tracking works', async () => {
87
+ await worker.exec(`
88
+ CREATE TABLE public.wm_test (id TEXT PRIMARY KEY)
89
+ `)
90
+ await worker.installChangeTracking()
91
+
92
+ const wm0 = await worker.getCurrentWatermark()
93
+ expect(wm0).toBe(0)
94
+
95
+ await worker.exec(`INSERT INTO public.wm_test VALUES ('x')`)
96
+ const wm1 = await worker.getCurrentWatermark()
97
+ expect(wm1).toBeGreaterThan(0)
98
+
99
+ await worker.exec(`INSERT INTO public.wm_test VALUES ('y')`)
100
+ const wm2 = await worker.getCurrentWatermark()
101
+ expect(wm2).toBeGreaterThan(wm1)
102
+
103
+ // getChangesSince with wm1 should only return the second insert
104
+ const changes = await worker.getChangesSince(wm1)
105
+ expect(changes).toHaveLength(1)
106
+ expect(changes[0].row_data).toMatchObject({ id: 'y' })
107
+ })
108
+
109
+ it('purgeChanges removes old entries', async () => {
110
+ await worker.exec(`CREATE TABLE public.purge_test (id TEXT PRIMARY KEY)`)
111
+ await worker.installChangeTracking()
112
+
113
+ await worker.exec(`INSERT INTO public.purge_test VALUES ('a')`)
114
+ await worker.exec(`INSERT INTO public.purge_test VALUES ('b')`)
115
+ await worker.exec(`INSERT INTO public.purge_test VALUES ('c')`)
116
+
117
+ const allChanges = await worker.getChangesSince(0)
118
+ expect(allChanges).toHaveLength(3)
119
+
120
+ // purge up to second change
121
+ const purged = await worker.purgeChanges(allChanges[1].watermark)
122
+ expect(purged).toBe(2)
123
+
124
+ // only third change remains
125
+ const remaining = await worker.getChangesSince(0)
126
+ expect(remaining).toHaveLength(1)
127
+ expect(remaining[0].row_data).toMatchObject({ id: 'c' })
128
+ })
129
+
130
+ it('close shuts down owned instance', async () => {
131
+ const w = await createOrezWorker({ pgliteOptions: { dataDir: 'memory://' } })
132
+ expect(w.db.closed).toBe(false)
133
+ await w.close()
134
+ expect(w.db.closed).toBe(true)
135
+ })
136
+ })
@@ -0,0 +1,367 @@
1
+ /**
2
+ * zero-cache embedded runner for cloudflare workers.
3
+ *
4
+ * runs zero-cache in-process with SINGLE_PROCESS=1, using bundler aliases
5
+ * to swap Node.js dependencies for CF-compatible shims:
6
+ *
7
+ * postgres → orez/worker/shims/postgres (PGlite-backed)
8
+ * @rocicorp/zero-sqlite3 → orez/worker/shims/sqlite (DO SQLite)
9
+ * fastify → orez/worker/shims/fastify (route capture)
10
+ * ws → orez/worker/shims/ws (CF WebSocket)
11
+ *
12
+ * the consumer's wrangler.toml must configure these aliases and enable
13
+ * nodejs_compat for the remaining Node.js APIs (events, stream, etc.).
14
+ *
15
+ * usage in a Durable Object:
16
+ *
17
+ * import { startZeroCacheEmbedCF } from 'orez/worker'
18
+ *
19
+ * // in ensureInitialized():
20
+ * globalThis.__orez_pglite = pglite // for postgres shim
21
+ * globalThis.__orez_do_sqlite = ctx.storage.sql // for sqlite shim
22
+ *
23
+ * const zc = await startZeroCacheEmbedCF({ ... })
24
+ *
25
+ * // in DO fetch():
26
+ * return zc.handleRequest(request)
27
+ */
28
+
29
+ import EventEmitter from 'node:events'
30
+
31
+ // static import so wrangler can follow the dependency tree and bundle
32
+ // zero-cache with all its transitive deps + our shim aliases.
33
+ // @ts-expect-error — internal zero-cache module, no type declarations
34
+ import { runWorker as _runWorker } from '@rocicorp/zero/out/zero-cache/src/server/runner/run-worker.js'
35
+
36
+ import type { PGlite } from '@electric-sql/pglite'
37
+
38
+ const runWorkerFn = _runWorker as (
39
+ parent: unknown,
40
+ env: Record<string, string>
41
+ ) => Promise<void>
42
+
43
+ export interface ZeroCacheEmbedCFOptions {
44
+ /** PGlite instance (also registered on globalThis.__orez_pglite) */
45
+ pglite: PGlite
46
+
47
+ /** DO SQLite storage (also registered on globalThis.__orez_do_sqlite) */
48
+ doSqlite: unknown
49
+
50
+ /** zero app ID (default: 'zero') */
51
+ appId?: string
52
+
53
+ /** publication names */
54
+ publications?: string[]
55
+
56
+ /** additional env vars passed to zero-cache */
57
+ env?: Record<string, string>
58
+
59
+ /** timeout in ms waiting for zero-cache ready (default: 30000) */
60
+ readyTimeout?: number
61
+ }
62
+
63
+ export interface ZeroCacheEmbedCF {
64
+ /** whether zero-cache is ready */
65
+ readonly ready: boolean
66
+
67
+ /**
68
+ * handle an incoming request from the DO's fetch() handler.
69
+ * routes HTTP to zero-cache's Fastify handlers, WebSocket
70
+ * upgrades through the zero-cache handoff mechanism.
71
+ */
72
+ handleRequest(request: Request): Promise<Response>
73
+
74
+ /** stop zero-cache */
75
+ stop(): Promise<void>
76
+ }
77
+
78
+ /**
79
+ * start zero-cache in embedded CF Workers mode.
80
+ *
81
+ * must be called AFTER setting up globalThis:
82
+ * globalThis.__orez_pglite = pgliteInstance
83
+ * globalThis.__orez_do_sqlite = ctx.storage.sql
84
+ */
85
+ export async function startZeroCacheEmbedCF(
86
+ opts: ZeroCacheEmbedCFOptions
87
+ ): Promise<ZeroCacheEmbedCF> {
88
+ const appId = opts.appId || 'zero'
89
+ const publications = opts.publications?.join(',') || `orez_${appId}_public`
90
+ const readyTimeout = opts.readyTimeout ?? 30000
91
+
92
+ // ensure globals are set for shims
93
+ ;(globalThis as any).__orez_pglite = opts.pglite
94
+ ;(globalThis as any).__orez_do_sqlite = opts.doSqlite
95
+
96
+ // ensure process.env exists (CF Workers doesn't have it natively)
97
+ ;(globalThis as any).process ??= {}
98
+ ;(globalThis as any).process.env ??= {}
99
+ ;(globalThis as any).process.pid ??= 1
100
+ ;(globalThis as any).process.argv ??= []
101
+
102
+ // CRITICAL: set SINGLE_PROCESS before importing zero-cache.
103
+ // zero-cache's childWorker() checks process.env.SINGLE_PROCESS directly.
104
+ ;(globalThis as any).process.env.SINGLE_PROCESS = '1'
105
+ ;(globalThis as any).process.env.NODE_ENV = 'development'
106
+
107
+ // shim process.kill (used by HeartbeatMonitor) to be a no-op
108
+ ;(globalThis as any).process.kill ??= () => {}
109
+
110
+ // create fake parent EventEmitter for zero-cache's runWorker()
111
+ // must be declared before process.exit shim (which references it)
112
+ const parent = new EventEmitter() as EventEmitter & {
113
+ send: (msg: unknown) => boolean
114
+ kill: (signal?: string) => void
115
+ pid: number
116
+ }
117
+
118
+ const parentEmitter = new EventEmitter()
119
+
120
+ parent.send = (message: unknown, sendHandle?: unknown) => {
121
+ parentEmitter.emit('message', message, sendHandle)
122
+ return true
123
+ }
124
+ parent.kill = (signal = 'SIGTERM') => {
125
+ parent.emit(signal, signal)
126
+ }
127
+ parent.pid = (globalThis as any).process.pid ?? 1
128
+
129
+ // shim process.exit to emit on parent instead of actually exiting
130
+ const origExit = (globalThis as any).process.exit
131
+ const origNodeEnv = (globalThis as any).process.env.NODE_ENV
132
+ const origKill = (globalThis as any).process.kill
133
+ ;(globalThis as any).process.exit = (code?: number) => {
134
+ parent.emit('exit', code ?? 0)
135
+ }
136
+
137
+ // build env for zero-cache
138
+ const env: Record<string, string> = {
139
+ ...((globalThis as any).process.env as Record<string, string>),
140
+ SINGLE_PROCESS: '1',
141
+ NODE_ENV: 'development',
142
+ // these connection strings are intercepted by the postgres shim
143
+ ZERO_UPSTREAM_DB: 'pglite://in-process',
144
+ ZERO_CVR_DB: 'pglite://in-process',
145
+ ZERO_CHANGE_DB: 'pglite://in-process',
146
+ // this path is intercepted by the sqlite shim
147
+ ZERO_REPLICA_FILE: ':do-sqlite:',
148
+ // don't bind a port — we route via inject/handoff
149
+ ZERO_PORT: '0',
150
+ ZERO_APP_ID: appId,
151
+ ZERO_APP_PUBLICATIONS: publications,
152
+ ZERO_LOG_LEVEL: opts.env?.ZERO_LOG_LEVEL || 'info',
153
+ ZERO_NUM_SYNC_WORKERS: opts.env?.ZERO_NUM_SYNC_WORKERS || '1',
154
+ ZERO_ENABLE_QUERY_PLANNER: 'false',
155
+ ...opts.env,
156
+ }
157
+
158
+ // wrap parent with onMessageType/onceMessageType helpers
159
+ // must forward sendHandle (second arg) for WebSocket handoff
160
+ const wrappedParent = new Proxy(parent, {
161
+ get(target, prop, receiver) {
162
+ if (prop === 'onMessageType') {
163
+ return (type: string, handler: (msg: unknown, sendHandle?: unknown) => void) => {
164
+ target.on('message', (data: unknown, sendHandle?: unknown) => {
165
+ if (Array.isArray(data) && data.length === 2 && data[0] === type) {
166
+ handler(data[1], sendHandle)
167
+ }
168
+ })
169
+ return receiver
170
+ }
171
+ }
172
+ if (prop === 'onceMessageType') {
173
+ return (type: string, handler: (msg: unknown, sendHandle?: unknown) => void) => {
174
+ const listener = (data: unknown, sendHandle?: unknown) => {
175
+ if (Array.isArray(data) && data.length === 2 && data[0] === type) {
176
+ target.off('message', listener)
177
+ handler(data[1], sendHandle)
178
+ }
179
+ }
180
+ target.on('message', listener)
181
+ return receiver
182
+ }
183
+ }
184
+ return Reflect.get(target, prop, receiver)
185
+ },
186
+ })
187
+
188
+ // track state
189
+ let isReady = false
190
+ let runWorkerPromise: Promise<void> | null = null
191
+
192
+ // capture the Fastify shim instance from zero-cache's HttpService.
193
+ // the fastify shim stores itself on globalThis when created.
194
+ let fastifyInstance: any = null
195
+
196
+ // wait for "ready" message
197
+ const readyPromise = new Promise<void>((resolve, reject) => {
198
+ const timeout = setTimeout(() => {
199
+ reject(
200
+ new Error(
201
+ `zero-cache CF embed: timed out waiting for ready after ${readyTimeout}ms`
202
+ )
203
+ )
204
+ }, readyTimeout)
205
+
206
+ parentEmitter.on('message', (msg: unknown) => {
207
+ if (Array.isArray(msg) && msg[0] === 'ready') {
208
+ clearTimeout(timeout)
209
+ isReady = true
210
+ resolve()
211
+ }
212
+ })
213
+ })
214
+
215
+ // start zero-cache
216
+ runWorkerPromise = runWorkerFn(wrappedParent, env).catch((err) => {
217
+ if (!isReady) {
218
+ throw err
219
+ }
220
+ // after ready, errors during shutdown are expected
221
+ })
222
+
223
+ // wait for ready
224
+ await readyPromise
225
+
226
+ // get the fastify instance (set by our shim during init)
227
+ fastifyInstance = (globalThis as any).__orez_fastify_instance
228
+
229
+ return {
230
+ get ready() {
231
+ return isReady
232
+ },
233
+
234
+ async handleRequest(request: Request): Promise<Response> {
235
+ if (!isReady) {
236
+ return new Response('zero-cache not ready', { status: 503 })
237
+ }
238
+
239
+ const url = new URL(request.url)
240
+ const isUpgrade =
241
+ request.headers.get('upgrade')?.toLowerCase() === 'websocket' ||
242
+ request.headers.get('x-soot-ws-upgrade') === 'true'
243
+
244
+ if (isUpgrade) {
245
+ return handleWebSocketUpgrade(request, url, fastifyInstance)
246
+ }
247
+
248
+ return handleHttpRequest(request, url, fastifyInstance)
249
+ },
250
+
251
+ async stop() {
252
+ isReady = false
253
+ wrappedParent.kill('SIGTERM')
254
+ if (runWorkerPromise) {
255
+ await Promise.race([runWorkerPromise, new Promise((r) => setTimeout(r, 5000))])
256
+ }
257
+ await new Promise((r) => setTimeout(r, 200))
258
+ // restore all modified globals
259
+ if (origExit) {
260
+ ;(globalThis as any).process.exit = origExit
261
+ }
262
+ if (origNodeEnv !== undefined) {
263
+ ;(globalThis as any).process.env.NODE_ENV = origNodeEnv
264
+ }
265
+ if (origKill) {
266
+ ;(globalThis as any).process.kill = origKill
267
+ }
268
+ delete (globalThis as any).process.env.SINGLE_PROCESS
269
+ },
270
+ }
271
+ }
272
+
273
+ // -- HTTP request handling --
274
+ // routes through the Fastify shim's inject() method
275
+
276
+ async function handleHttpRequest(
277
+ request: Request,
278
+ url: URL,
279
+ fastify: any
280
+ ): Promise<Response> {
281
+ if (!fastify?.inject) {
282
+ return new Response('fastify not available', { status: 503 })
283
+ }
284
+
285
+ const headers: Record<string, string> = {}
286
+ request.headers.forEach((value, key) => {
287
+ headers[key] = value
288
+ })
289
+
290
+ let payload: string | null = null
291
+ if (request.method !== 'GET' && request.method !== 'HEAD' && request.body) {
292
+ payload = await request.text()
293
+ }
294
+
295
+ const result = await fastify.inject({
296
+ method: request.method,
297
+ url: url.pathname + url.search,
298
+ headers,
299
+ payload,
300
+ })
301
+
302
+ return new Response(result.body, {
303
+ status: result.statusCode,
304
+ headers: result.headers,
305
+ })
306
+ }
307
+
308
+ // -- WebSocket upgrade handling --
309
+ // creates WebSocketPair and feeds the server socket into zero-cache's
310
+ // handoff mechanism via the Fastify shim's server EventEmitter.
311
+
312
+ function handleWebSocketUpgrade(request: Request, url: URL, fastify: any): Response {
313
+ const WsPair = (globalThis as any).WebSocketPair
314
+ if (!WsPair) {
315
+ return new Response('WebSocketPair not available', { status: 500 })
316
+ }
317
+
318
+ const pair = new WsPair()
319
+ const [client, server] = Object.values(pair) as [any, any]
320
+
321
+ // accept the server side (CF Workers requirement)
322
+ server.accept()
323
+
324
+ // build a serializable request object for the handoff
325
+ const headers: Record<string, string> = {}
326
+ request.headers.forEach((value, key) => {
327
+ headers[key] = value
328
+ })
329
+
330
+ const message = {
331
+ url: url.pathname + url.search,
332
+ headers,
333
+ method: 'GET',
334
+ }
335
+
336
+ // emit handoff on the Fastify server's EventEmitter.
337
+ // installWebSocketHandoff (non-Server branch) listens for this:
338
+ // source.onMessageType("handoff", (msg, socket) => { ... })
339
+ if (fastify?.server) {
340
+ fastify.server.emit(
341
+ 'message',
342
+ ['handoff', { message, head: new Uint8Array(0) }],
343
+ server // the CF WebSocket as sendHandle
344
+ )
345
+ }
346
+
347
+ // return 101 with client socket
348
+ // must echo Sec-WebSocket-Protocol — browsers reject the upgrade without it
349
+ const secProtocol = request.headers.get('sec-websocket-protocol')
350
+ const upgradeHeaders: Record<string, string> = {}
351
+ if (secProtocol) {
352
+ upgradeHeaders['Sec-WebSocket-Protocol'] = secProtocol
353
+ }
354
+ try {
355
+ return new Response(null, {
356
+ status: 101,
357
+ headers: upgradeHeaders,
358
+ // @ts-expect-error CF Workers Response extension
359
+ webSocket: client,
360
+ })
361
+ } catch {
362
+ const resp = new Response(null, { status: 200 })
363
+ ;(resp as any).__orez_websocket = client
364
+ ;(resp as any).__orez_ws_upgrade = true
365
+ return resp
366
+ }
367
+ }