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.
- package/dist/cli-entry.js +0 -0
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +8 -4
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +12 -0
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +81 -0
- package/dist/pglite-manager.js.map +1 -1
- package/dist/recovery.js +2 -2
- package/dist/recovery.js.map +1 -1
- package/dist/replication/change-tracker.js +9 -9
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts +12 -0
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +34 -6
- package/dist/replication/handler.js.map +1 -1
- package/dist/worker/browser-build-config.d.ts +59 -0
- package/dist/worker/browser-build-config.d.ts.map +1 -0
- package/dist/worker/browser-build-config.js +101 -0
- package/dist/worker/browser-build-config.js.map +1 -0
- package/dist/worker/browser-embed.d.ts +58 -0
- package/dist/worker/browser-embed.d.ts.map +1 -0
- package/dist/worker/browser-embed.js +195 -0
- package/dist/worker/browser-embed.js.map +1 -0
- package/dist/worker/cf-patches.d.ts +20 -0
- package/dist/worker/cf-patches.d.ts.map +1 -0
- package/dist/worker/cf-patches.js +94 -0
- package/dist/worker/cf-patches.js.map +1 -0
- package/dist/worker/index.d.ts +12 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +105 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/shims/fastify.d.ts +80 -0
- package/dist/worker/shims/fastify.d.ts.map +1 -0
- package/dist/worker/shims/fastify.js +223 -0
- package/dist/worker/shims/fastify.js.map +1 -0
- package/dist/worker/shims/http-service.d.ts +104 -0
- package/dist/worker/shims/http-service.d.ts.map +1 -0
- package/dist/worker/shims/http-service.js +198 -0
- package/dist/worker/shims/http-service.js.map +1 -0
- package/dist/worker/shims/node-stub.d.ts +147 -0
- package/dist/worker/shims/node-stub.d.ts.map +1 -0
- package/dist/worker/shims/node-stub.js +204 -0
- package/dist/worker/shims/node-stub.js.map +1 -0
- package/dist/worker/shims/postgres.d.ts +115 -0
- package/dist/worker/shims/postgres.d.ts.map +1 -0
- package/dist/worker/shims/postgres.js +1181 -0
- package/dist/worker/shims/postgres.js.map +1 -0
- package/dist/worker/shims/sqlite-browser.d.ts +54 -0
- package/dist/worker/shims/sqlite-browser.d.ts.map +1 -0
- package/dist/worker/shims/sqlite-browser.js +144 -0
- package/dist/worker/shims/sqlite-browser.js.map +1 -0
- package/dist/worker/shims/sqlite.d.ts +126 -0
- package/dist/worker/shims/sqlite.d.ts.map +1 -0
- package/dist/worker/shims/sqlite.js +599 -0
- package/dist/worker/shims/sqlite.js.map +1 -0
- package/dist/worker/shims/stream-browser.d.ts +9 -0
- package/dist/worker/shims/stream-browser.d.ts.map +1 -0
- package/dist/worker/shims/stream-browser.js +13 -0
- package/dist/worker/shims/stream-browser.js.map +1 -0
- package/dist/worker/shims/ws-browser.d.ts +50 -0
- package/dist/worker/shims/ws-browser.d.ts.map +1 -0
- package/dist/worker/shims/ws-browser.js +105 -0
- package/dist/worker/shims/ws-browser.js.map +1 -0
- package/dist/worker/shims/ws.d.ts +62 -0
- package/dist/worker/shims/ws.d.ts.map +1 -0
- package/dist/worker/shims/ws.js +310 -0
- package/dist/worker/shims/ws.js.map +1 -0
- package/dist/worker/types.d.ts +57 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +9 -0
- package/dist/worker/types.js.map +1 -0
- package/dist/worker/zero-cache-embed-cf.d.ts +63 -0
- package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -0
- package/dist/worker/zero-cache-embed-cf.js +268 -0
- package/dist/worker/zero-cache-embed-cf.js.map +1 -0
- package/dist/worker/zero-cache-embed.d.ts +66 -0
- package/dist/worker/zero-cache-embed.d.ts.map +1 -0
- package/dist/worker/zero-cache-embed.js +200 -0
- package/dist/worker/zero-cache-embed.js.map +1 -0
- package/package.json +62 -3
- package/src/cli-entry.ts +0 -0
- package/src/cli.ts +8 -1
- package/src/config.ts +2 -0
- package/src/index.ts +15 -10
- package/src/integration/integration.test.ts +1 -1
- package/src/integration/restore-live-stress.test.ts +2 -2
- package/src/pg-proxy.ts +9 -4
- package/src/pglite-manager.ts +111 -0
- package/src/recovery.ts +2 -2
- package/src/replication/change-tracker.test.ts +1 -1
- package/src/replication/change-tracker.ts +9 -9
- package/src/replication/handler.test.ts +37 -0
- package/src/replication/handler.ts +46 -6
- package/src/wasm-sqlite.test.ts +2 -1
- package/src/worker/browser-build-config.test.ts +59 -0
- package/src/worker/browser-build-config.ts +105 -0
- package/src/worker/browser-embed.ts +306 -0
- package/src/worker/cf-patches.ts +114 -0
- package/src/worker/embed-integration.test.ts +321 -0
- package/src/worker/index.ts +138 -0
- package/src/worker/shims/fastify.test.ts +255 -0
- package/src/worker/shims/fastify.ts +292 -0
- package/src/worker/shims/http-service.test.ts +355 -0
- package/src/worker/shims/http-service.ts +293 -0
- package/src/worker/shims/node-stub.ts +223 -0
- package/src/worker/shims/postgres.test.ts +364 -0
- package/src/worker/shims/postgres.ts +1434 -0
- package/src/worker/shims/sqlite-browser.test.ts +233 -0
- package/src/worker/shims/sqlite-browser.ts +178 -0
- package/src/worker/shims/sqlite.test.ts +641 -0
- package/src/worker/shims/sqlite.ts +731 -0
- package/src/worker/shims/ws-browser.test.ts +184 -0
- package/src/worker/shims/ws-browser.ts +125 -0
- package/src/worker/shims/ws.test.ts +288 -0
- package/src/worker/shims/ws.ts +367 -0
- package/src/worker/types.ts +75 -0
- package/src/worker/worker-integration.test.ts +223 -0
- package/src/worker/worker.test.ts +136 -0
- package/src/worker/zero-cache-embed-cf.ts +367 -0
- 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
|
+
}
|