orez 0.2.26 → 0.2.29
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/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +9 -1
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +2 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +49 -7
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
- package/dist/pg-sqlite-compiler/index.d.ts +12 -0
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/index.js +59 -0
- package/dist/pg-sqlite-compiler/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.js +39 -0
- package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.js +103 -0
- package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
- package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
- package/dist/pg-sqlite-compiler/types.d.ts +55 -0
- package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/types.js +2 -0
- package/dist/pg-sqlite-compiler/types.js.map +1 -0
- package/package.json +8 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/ARCHITECTURE.md +0 -83
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1033
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -38
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7157
- package/src/pg-proxy.ts +0 -1087
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- package/src/worker/zero-cache-embed.ts +0 -277
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
HttpServiceAdapter,
|
|
5
|
-
createHttpServiceAdapter,
|
|
6
|
-
type InjectableFastify,
|
|
7
|
-
type WebSocketHandler,
|
|
8
|
-
type WebSocketUpgradeResult,
|
|
9
|
-
} from './http-service.js'
|
|
10
|
-
|
|
11
|
-
/** minimal mock fastify that records inject() calls and returns canned responses */
|
|
12
|
-
function createMockFastify(
|
|
13
|
-
responses: Record<
|
|
14
|
-
string,
|
|
15
|
-
{ status: number; body: string; headers?: Record<string, string> }
|
|
16
|
-
>
|
|
17
|
-
): InjectableFastify {
|
|
18
|
-
return {
|
|
19
|
-
async ready() {},
|
|
20
|
-
async inject(opts) {
|
|
21
|
-
const key = `${opts.method} ${opts.url}`
|
|
22
|
-
const resp = responses[key]
|
|
23
|
-
if (!resp) {
|
|
24
|
-
return { statusCode: 404, headers: {}, body: 'not found' }
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
statusCode: resp.status,
|
|
28
|
-
headers: resp.headers ?? { 'content-type': 'text/plain' },
|
|
29
|
-
body: resp.body,
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** helper to create a Request (works in node/bun/vitest with fetch globals) */
|
|
36
|
-
function makeRequest(
|
|
37
|
-
url: string,
|
|
38
|
-
opts?: { method?: string; headers?: Record<string, string>; body?: string }
|
|
39
|
-
): Request {
|
|
40
|
-
return new Request(url, {
|
|
41
|
-
method: opts?.method ?? 'GET',
|
|
42
|
-
headers: opts?.headers,
|
|
43
|
-
body: opts?.body,
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe('HttpServiceAdapter', () => {
|
|
48
|
-
let adapter: HttpServiceAdapter
|
|
49
|
-
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
adapter = new HttpServiceAdapter()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('initialization', () => {
|
|
55
|
-
it('is not ready before initialize', () => {
|
|
56
|
-
expect(adapter.isReady).toBe(false)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('is ready after initialize', async () => {
|
|
60
|
-
await adapter.initialize(createMockFastify({}))
|
|
61
|
-
expect(adapter.isReady).toBe(true)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('returns 503 when not initialized', async () => {
|
|
65
|
-
const resp = await adapter.handleRequest(makeRequest('http://localhost/'))
|
|
66
|
-
expect(resp.status).toBe(503)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('HTTP routing via inject()', () => {
|
|
71
|
-
beforeEach(async () => {
|
|
72
|
-
await adapter.initialize(
|
|
73
|
-
createMockFastify({
|
|
74
|
-
'GET /': { status: 200, body: 'ok' },
|
|
75
|
-
'GET /keepalive': { status: 200, body: 'alive' },
|
|
76
|
-
'GET /statz': {
|
|
77
|
-
status: 200,
|
|
78
|
-
body: '{"uptime":123}',
|
|
79
|
-
headers: { 'content-type': 'application/json' },
|
|
80
|
-
},
|
|
81
|
-
'POST /data': { status: 201, body: 'created' },
|
|
82
|
-
})
|
|
83
|
-
)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('routes GET / to fastify', async () => {
|
|
87
|
-
const resp = await adapter.handleRequest(makeRequest('http://localhost/'))
|
|
88
|
-
expect(resp.status).toBe(200)
|
|
89
|
-
expect(await resp.text()).toBe('ok')
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('routes GET /keepalive to fastify', async () => {
|
|
93
|
-
const resp = await adapter.handleRequest(makeRequest('http://localhost/keepalive'))
|
|
94
|
-
expect(resp.status).toBe(200)
|
|
95
|
-
expect(await resp.text()).toBe('alive')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('routes GET /statz with correct content-type', async () => {
|
|
99
|
-
const resp = await adapter.handleRequest(makeRequest('http://localhost/statz'))
|
|
100
|
-
expect(resp.status).toBe(200)
|
|
101
|
-
expect(resp.headers.get('content-type')).toBe('application/json')
|
|
102
|
-
expect(await resp.text()).toBe('{"uptime":123}')
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('handles POST with body', async () => {
|
|
106
|
-
const resp = await adapter.handleRequest(
|
|
107
|
-
makeRequest('http://localhost/data', {
|
|
108
|
-
method: 'POST',
|
|
109
|
-
body: '{"key":"value"}',
|
|
110
|
-
headers: { 'content-type': 'application/json' },
|
|
111
|
-
})
|
|
112
|
-
)
|
|
113
|
-
expect(resp.status).toBe(201)
|
|
114
|
-
expect(await resp.text()).toBe('created')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('returns 404 for unknown routes', async () => {
|
|
118
|
-
const resp = await adapter.handleRequest(makeRequest('http://localhost/nope'))
|
|
119
|
-
expect(resp.status).toBe(404)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('preserves query string', async () => {
|
|
123
|
-
const fastify = createMockFastify({
|
|
124
|
-
'GET /search?q=hello': { status: 200, body: 'found' },
|
|
125
|
-
})
|
|
126
|
-
await adapter.initialize(fastify)
|
|
127
|
-
const resp = await adapter.handleRequest(
|
|
128
|
-
makeRequest('http://localhost/search?q=hello')
|
|
129
|
-
)
|
|
130
|
-
expect(resp.status).toBe(200)
|
|
131
|
-
expect(await resp.text()).toBe('found')
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
describe('WebSocket route matching', () => {
|
|
136
|
-
const noopHandler: WebSocketHandler = () => {}
|
|
137
|
-
|
|
138
|
-
it('matches exact WebSocket routes', () => {
|
|
139
|
-
adapter.addWsRoute('/replication/v1/changes', noopHandler)
|
|
140
|
-
const match = adapter.matchWsRoute('/replication/v1/changes')
|
|
141
|
-
expect(match).not.toBeNull()
|
|
142
|
-
expect(match!.pattern).toBe('/replication/v1/changes')
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('returns null for unmatched paths', () => {
|
|
146
|
-
adapter.addWsRoute('/replication/v1/changes', noopHandler)
|
|
147
|
-
expect(adapter.matchWsRoute('/other/path')).toBeNull()
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('matches wildcard patterns', () => {
|
|
151
|
-
adapter.addWsRoute('/replication/v*/changes', noopHandler)
|
|
152
|
-
|
|
153
|
-
expect(adapter.matchWsRoute('/replication/v1/changes')).not.toBeNull()
|
|
154
|
-
expect(adapter.matchWsRoute('/replication/v2/changes')).not.toBeNull()
|
|
155
|
-
expect(adapter.matchWsRoute('/replication/v99/changes')).not.toBeNull()
|
|
156
|
-
|
|
157
|
-
// should not match different structure
|
|
158
|
-
expect(adapter.matchWsRoute('/replication/v1/snapshot')).toBeNull()
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('matches multiple wildcard patterns', () => {
|
|
162
|
-
const changesHandler: WebSocketHandler = () => {}
|
|
163
|
-
const snapshotHandler: WebSocketHandler = () => {}
|
|
164
|
-
|
|
165
|
-
adapter.addWsRoute('/replication/v*/changes', changesHandler)
|
|
166
|
-
adapter.addWsRoute('/replication/v*/snapshot', snapshotHandler)
|
|
167
|
-
|
|
168
|
-
const changesMatch = adapter.matchWsRoute('/replication/v1/changes')
|
|
169
|
-
expect(changesMatch).not.toBeNull()
|
|
170
|
-
expect(changesMatch!.handler).toBe(changesHandler)
|
|
171
|
-
|
|
172
|
-
const snapshotMatch = adapter.matchWsRoute('/replication/v2/snapshot')
|
|
173
|
-
expect(snapshotMatch).not.toBeNull()
|
|
174
|
-
expect(snapshotMatch!.handler).toBe(snapshotHandler)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('prefers exact match over pattern', () => {
|
|
178
|
-
const exactHandler: WebSocketHandler = () => {}
|
|
179
|
-
const patternHandler: WebSocketHandler = () => {}
|
|
180
|
-
|
|
181
|
-
adapter.addWsRoute('/replication/v1/changes', exactHandler)
|
|
182
|
-
adapter.addWsRoute('/replication/v*/changes', patternHandler)
|
|
183
|
-
|
|
184
|
-
const match = adapter.matchWsRoute('/replication/v1/changes')
|
|
185
|
-
expect(match).not.toBeNull()
|
|
186
|
-
expect(match!.handler).toBe(exactHandler)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('wildcard does not match across slashes', () => {
|
|
190
|
-
adapter.addWsRoute('/api/v*/data', noopHandler)
|
|
191
|
-
expect(adapter.matchWsRoute('/api/v1/extra/data')).toBeNull()
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
describe('WebSocket upgrade detection', () => {
|
|
196
|
-
beforeEach(async () => {
|
|
197
|
-
await adapter.initialize(createMockFastify({}))
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('detects upgrade header', async () => {
|
|
201
|
-
adapter.addWsRoute('/ws', () => {})
|
|
202
|
-
|
|
203
|
-
// WebSocketPair is a CF global — not available in vitest.
|
|
204
|
-
// we verify the adapter detects the upgrade and tries to use it.
|
|
205
|
-
const resp = await adapter.handleRequest(
|
|
206
|
-
makeRequest('http://localhost/ws', {
|
|
207
|
-
headers: { upgrade: 'websocket' },
|
|
208
|
-
})
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
// without WebSocketPair in the runtime, we get a 500
|
|
212
|
-
// this confirms the adapter correctly detected the upgrade
|
|
213
|
-
// and attempted ws handling (rather than routing to fastify)
|
|
214
|
-
expect(resp.status).toBe(500)
|
|
215
|
-
expect(await resp.text()).toBe('WebSocketPair not available in this runtime')
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('returns 404 for ws upgrade with no matching route', async () => {
|
|
219
|
-
const resp = await adapter.handleRequest(
|
|
220
|
-
makeRequest('http://localhost/unknown', {
|
|
221
|
-
headers: { upgrade: 'websocket' },
|
|
222
|
-
})
|
|
223
|
-
)
|
|
224
|
-
expect(resp.status).toBe(404)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('routes non-upgrade requests to fastify even if ws route exists', async () => {
|
|
228
|
-
adapter.addWsRoute('/dual', () => {})
|
|
229
|
-
|
|
230
|
-
// regular GET (no upgrade header) should go to fastify inject
|
|
231
|
-
const resp = await adapter.handleRequest(makeRequest('http://localhost/dual'))
|
|
232
|
-
// fastify returns 404 since we didn't register an HTTP route for /dual
|
|
233
|
-
expect(resp.status).toBe(404)
|
|
234
|
-
})
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
describe('WebSocket upgrade preparation with mock WebSocketPair', () => {
|
|
238
|
-
// tests use prepareWebSocketUpgrade() directly because the CF Workers
|
|
239
|
-
// Response constructor (status 101 + webSocket property) is not available
|
|
240
|
-
// in Node.js. this tests the full setup logic without the CF-specific part.
|
|
241
|
-
|
|
242
|
-
let originalWebSocketPair: unknown
|
|
243
|
-
|
|
244
|
-
beforeEach(async () => {
|
|
245
|
-
originalWebSocketPair = (globalThis as any).WebSocketPair
|
|
246
|
-
await adapter.initialize(createMockFastify({}))
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
afterEach(() => {
|
|
250
|
-
if (originalWebSocketPair !== undefined) {
|
|
251
|
-
;(globalThis as any).WebSocketPair = originalWebSocketPair
|
|
252
|
-
} else {
|
|
253
|
-
delete (globalThis as any).WebSocketPair
|
|
254
|
-
}
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('creates WebSocket pair and returns upgrade result', () => {
|
|
258
|
-
const mockServer = {
|
|
259
|
-
accept: vi.fn(),
|
|
260
|
-
close: vi.fn(),
|
|
261
|
-
send: vi.fn(),
|
|
262
|
-
addEventListener: vi.fn(),
|
|
263
|
-
}
|
|
264
|
-
const mockClient = { close: vi.fn() }
|
|
265
|
-
|
|
266
|
-
;(globalThis as any).WebSocketPair = class {
|
|
267
|
-
0 = mockClient
|
|
268
|
-
1 = mockServer
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const handler = vi.fn()
|
|
272
|
-
adapter.addWsRoute('/ws/test', handler)
|
|
273
|
-
|
|
274
|
-
const req = makeRequest('http://localhost/ws/test', {
|
|
275
|
-
headers: { upgrade: 'websocket' },
|
|
276
|
-
})
|
|
277
|
-
const url = new URL(req.url)
|
|
278
|
-
const result = adapter.prepareWebSocketUpgrade(req, url)
|
|
279
|
-
|
|
280
|
-
// should be an upgrade result, not a Response or null
|
|
281
|
-
expect(result).not.toBeNull()
|
|
282
|
-
expect(result).not.toBeInstanceOf(Response)
|
|
283
|
-
|
|
284
|
-
const upgrade = result as WebSocketUpgradeResult
|
|
285
|
-
expect(upgrade.client).toBe(mockClient)
|
|
286
|
-
expect(upgrade.server).toBe(mockServer)
|
|
287
|
-
expect(upgrade.handler).toBe(handler)
|
|
288
|
-
expect(upgrade.url.pathname).toBe('/ws/test')
|
|
289
|
-
expect(mockServer.accept).toHaveBeenCalled()
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
it('returns null when no route matches', () => {
|
|
293
|
-
const req = makeRequest('http://localhost/nope')
|
|
294
|
-
const url = new URL(req.url)
|
|
295
|
-
expect(adapter.prepareWebSocketUpgrade(req, url)).toBeNull()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('returns Response(500) when WebSocketPair is unavailable', () => {
|
|
299
|
-
delete (globalThis as any).WebSocketPair
|
|
300
|
-
|
|
301
|
-
adapter.addWsRoute('/ws/test', () => {})
|
|
302
|
-
|
|
303
|
-
const req = makeRequest('http://localhost/ws/test')
|
|
304
|
-
const url = new URL(req.url)
|
|
305
|
-
const result = adapter.prepareWebSocketUpgrade(req, url)
|
|
306
|
-
|
|
307
|
-
expect(result).toBeInstanceOf(Response)
|
|
308
|
-
expect((result as Response).status).toBe(500)
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
it('handler invocation catches errors and closes socket', async () => {
|
|
312
|
-
const mockServer = {
|
|
313
|
-
accept: vi.fn(),
|
|
314
|
-
close: vi.fn(),
|
|
315
|
-
send: vi.fn(),
|
|
316
|
-
addEventListener: vi.fn(),
|
|
317
|
-
}
|
|
318
|
-
const mockClient = { close: vi.fn() }
|
|
319
|
-
|
|
320
|
-
;(globalThis as any).WebSocketPair = class {
|
|
321
|
-
0 = mockClient
|
|
322
|
-
1 = mockServer
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const handler = vi.fn().mockRejectedValue(new Error('handler boom'))
|
|
326
|
-
adapter.addWsRoute('/ws/fail', handler)
|
|
327
|
-
|
|
328
|
-
const req = makeRequest('http://localhost/ws/fail')
|
|
329
|
-
const url = new URL(req.url)
|
|
330
|
-
const result = adapter.prepareWebSocketUpgrade(req, url) as WebSocketUpgradeResult
|
|
331
|
-
|
|
332
|
-
// simulate what handleWebSocket does: invoke handler and catch errors
|
|
333
|
-
await Promise.resolve(
|
|
334
|
-
result.handler(result.server as any, result.request, result.url)
|
|
335
|
-
).catch((err) => {
|
|
336
|
-
try {
|
|
337
|
-
;(result.server as any).close(1011, String(err))
|
|
338
|
-
} catch {
|
|
339
|
-
// socket may already be closed
|
|
340
|
-
}
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
expect(handler).toHaveBeenCalled()
|
|
344
|
-
expect(mockServer.close).toHaveBeenCalledWith(1011, 'Error: handler boom')
|
|
345
|
-
})
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
describe('createHttpServiceAdapter factory', () => {
|
|
349
|
-
it('returns a valid adapter', () => {
|
|
350
|
-
const adapter = createHttpServiceAdapter()
|
|
351
|
-
expect(adapter).toBeInstanceOf(HttpServiceAdapter)
|
|
352
|
-
expect(adapter.isReady).toBe(false)
|
|
353
|
-
})
|
|
354
|
-
})
|
|
355
|
-
})
|
|
@@ -1,293 +0,0 @@
|
|
|
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
|
-
* http service adapter for CF Workers / Durable Objects.
|
|
6
|
-
*
|
|
7
|
-
* bridges between a Durable Object's fetch() handler and zero-cache's
|
|
8
|
-
* Fastify-based HTTP service. two routing modes:
|
|
9
|
-
*
|
|
10
|
-
* 1. HTTP routes — delegated to Fastify via inject() (health, keepalive, statz, etc.)
|
|
11
|
-
* 2. WebSocket routes — handled via DO native WebSocket pairs, bypassing Fastify
|
|
12
|
-
* entirely since inject() cannot do upgrades.
|
|
13
|
-
*
|
|
14
|
-
* usage:
|
|
15
|
-
* const adapter = createHttpServiceAdapter()
|
|
16
|
-
* adapter.addWsRoute('/replication/v1/changes', handler)
|
|
17
|
-
* await adapter.initialize(fastifyInstance)
|
|
18
|
-
* // in DO fetch():
|
|
19
|
-
* return adapter.handleRequest(request)
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
// CF Workers globals — not in Node.js types
|
|
23
|
-
declare const WebSocketPair: (new () => { 0: WebSocket; 1: WebSocket }) | undefined
|
|
24
|
-
|
|
25
|
-
// -- types for the adapter, kept minimal so we don't import fastify at module level --
|
|
26
|
-
|
|
27
|
-
/** handler for a WebSocket connection routed from a DO */
|
|
28
|
-
export type WebSocketHandler = (
|
|
29
|
-
server: WebSocket,
|
|
30
|
-
request: Request,
|
|
31
|
-
url: URL
|
|
32
|
-
) => void | Promise<void>
|
|
33
|
-
|
|
34
|
-
/** minimal interface for what we need from fastify (avoids hard dep) */
|
|
35
|
-
export interface InjectableFastify {
|
|
36
|
-
inject(opts: {
|
|
37
|
-
method: string
|
|
38
|
-
url: string
|
|
39
|
-
headers?: Record<string, string>
|
|
40
|
-
payload?: string | null
|
|
41
|
-
}): Promise<{
|
|
42
|
-
statusCode: number
|
|
43
|
-
headers: Record<string, string>
|
|
44
|
-
body: string
|
|
45
|
-
}>
|
|
46
|
-
ready(): Promise<void>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** route match result */
|
|
50
|
-
interface RouteMatch {
|
|
51
|
-
handler: WebSocketHandler
|
|
52
|
-
pattern: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** result of preparing a WebSocket upgrade (before CF Response creation) */
|
|
56
|
-
export interface WebSocketUpgradeResult {
|
|
57
|
-
client: WebSocket
|
|
58
|
-
server: WebSocket
|
|
59
|
-
handler: WebSocketHandler
|
|
60
|
-
request: Request
|
|
61
|
-
url: URL
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* the http service adapter. manages routing between Fastify inject()
|
|
66
|
-
* for regular HTTP and DO-native WebSocket pairs for upgrade requests.
|
|
67
|
-
*/
|
|
68
|
-
export class HttpServiceAdapter {
|
|
69
|
-
private fastify: InjectableFastify | null = null
|
|
70
|
-
private wsRoutes: Map<string, WebSocketHandler> = new Map()
|
|
71
|
-
private wsPatterns: Array<{
|
|
72
|
-
regex: RegExp
|
|
73
|
-
pattern: string
|
|
74
|
-
handler: WebSocketHandler
|
|
75
|
-
}> = []
|
|
76
|
-
private initialized = false
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* register a WebSocket route handler.
|
|
80
|
-
* supports exact paths and simple patterns with wildcard segments.
|
|
81
|
-
*
|
|
82
|
-
* call before initialize() so routes are ready when requests arrive.
|
|
83
|
-
*
|
|
84
|
-
* examples:
|
|
85
|
-
* addWsRoute('/replication/v1/changes', handler) -- exact
|
|
86
|
-
* addWsRoute('/replication/v[star]/changes', handler) -- wildcard (use *)
|
|
87
|
-
*/
|
|
88
|
-
addWsRoute(path: string, handler: WebSocketHandler): void {
|
|
89
|
-
if (path.includes('*')) {
|
|
90
|
-
// convert glob-style pattern to regex
|
|
91
|
-
const escaped = path.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
92
|
-
const regex = new RegExp('^' + escaped.replace(/\*/g, '[^/]+') + '$')
|
|
93
|
-
this.wsPatterns.push({ regex, pattern: path, handler })
|
|
94
|
-
} else {
|
|
95
|
-
this.wsRoutes.set(path, handler)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* initialize with a fastify instance. call after route registration
|
|
101
|
-
* (e.g., after zero-cache's init callback has run) but before handling requests.
|
|
102
|
-
* calls fastify.ready() to finalize route compilation.
|
|
103
|
-
*/
|
|
104
|
-
async initialize(fastify: InjectableFastify): Promise<void> {
|
|
105
|
-
this.fastify = fastify
|
|
106
|
-
await fastify.ready()
|
|
107
|
-
this.initialized = true
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** whether the adapter is ready to handle requests */
|
|
111
|
-
get isReady(): boolean {
|
|
112
|
-
return this.initialized
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* handle an incoming request from a DO's fetch() method.
|
|
117
|
-
* routes WebSocket upgrades to DO-native WebSocket handlers,
|
|
118
|
-
* everything else to Fastify inject().
|
|
119
|
-
*/
|
|
120
|
-
async handleRequest(request: Request): Promise<Response> {
|
|
121
|
-
if (!this.initialized) {
|
|
122
|
-
return new Response('service not ready', { status: 503 })
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const url = new URL(request.url)
|
|
126
|
-
|
|
127
|
-
// websocket upgrade — route to DO native handler
|
|
128
|
-
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
129
|
-
return this.handleWebSocket(request, url)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// regular HTTP — fastify inject
|
|
133
|
-
return this.handleHttp(request, url)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* route a regular HTTP request through Fastify's inject() mechanism.
|
|
138
|
-
* this works for all non-WebSocket routes (health, keepalive, statz, etc.).
|
|
139
|
-
*/
|
|
140
|
-
private async handleHttp(request: Request, url: URL): Promise<Response> {
|
|
141
|
-
if (!this.fastify) {
|
|
142
|
-
return new Response('fastify not initialized', { status: 503 })
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// build headers object from request
|
|
146
|
-
const headers: Record<string, string> = {}
|
|
147
|
-
request.headers.forEach((value, key) => {
|
|
148
|
-
headers[key] = value
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// read body for non-GET/HEAD requests
|
|
152
|
-
let payload: string | null = null
|
|
153
|
-
if (request.method !== 'GET' && request.method !== 'HEAD' && request.body) {
|
|
154
|
-
payload = await request.text()
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const result = await this.fastify.inject({
|
|
158
|
-
method: request.method,
|
|
159
|
-
url: url.pathname + url.search,
|
|
160
|
-
headers,
|
|
161
|
-
payload,
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
return new Response(result.body, {
|
|
165
|
-
status: result.statusCode,
|
|
166
|
-
headers: result.headers,
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* handle a WebSocket upgrade request using DO-native WebSocket pairs.
|
|
172
|
-
*
|
|
173
|
-
* creates a WebSocketPair, accepts the server side, and passes it
|
|
174
|
-
* to the matched route handler. returns the 101 response with the
|
|
175
|
-
* client side for the DO runtime to manage.
|
|
176
|
-
*
|
|
177
|
-
* note: WebSocketPair is a CF Workers global. in non-CF environments
|
|
178
|
-
* this will throw — callers should polyfill or avoid ws routes.
|
|
179
|
-
*/
|
|
180
|
-
private handleWebSocket(request: Request, url: URL): Response {
|
|
181
|
-
const result = this.prepareWebSocketUpgrade(request, url)
|
|
182
|
-
|
|
183
|
-
if (!result) {
|
|
184
|
-
return new Response('no websocket handler for path', { status: 404 })
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (result instanceof Response) {
|
|
188
|
-
return result
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// invoke handler (fire-and-forget; handler manages the connection lifecycle)
|
|
192
|
-
// errors in the handler should close the socket, not crash the DO
|
|
193
|
-
Promise.resolve(result.handler(result.server, result.request, result.url)).catch(
|
|
194
|
-
(err) => {
|
|
195
|
-
try {
|
|
196
|
-
result.server.close(1011, String(err))
|
|
197
|
-
} catch {
|
|
198
|
-
// socket may already be closed
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
// return 101 with client socket for DO runtime.
|
|
204
|
-
// CF Workers Response constructor accepts status 101 + webSocket property.
|
|
205
|
-
// standard fetch spec rejects status 101, so we try CF-style first and
|
|
206
|
-
// fall back to a plain 101-like response for non-CF environments.
|
|
207
|
-
try {
|
|
208
|
-
return new Response(null, {
|
|
209
|
-
status: 101,
|
|
210
|
-
// @ts-expect-error — webSocket is a CF Workers Response extension
|
|
211
|
-
webSocket: result.client,
|
|
212
|
-
})
|
|
213
|
-
} catch {
|
|
214
|
-
// non-CF runtime (Node.js, vitest) — status 101 not allowed.
|
|
215
|
-
// return a marker response that callers can detect.
|
|
216
|
-
const resp = new Response(null, { status: 200 })
|
|
217
|
-
;(resp as any).__orez_websocket = result.client
|
|
218
|
-
;(resp as any).__orez_ws_upgrade = true
|
|
219
|
-
return resp
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* prepare a WebSocket upgrade: match route, create pair, accept server side.
|
|
225
|
-
* separated from handleWebSocket so tests can verify the setup logic without
|
|
226
|
-
* needing the CF runtime's special Response(101) support.
|
|
227
|
-
*
|
|
228
|
-
* returns null if no route matches, a Response if WebSocketPair unavailable,
|
|
229
|
-
* or a WebSocketUpgradeResult with the pair + handler ready to invoke.
|
|
230
|
-
*/
|
|
231
|
-
prepareWebSocketUpgrade(
|
|
232
|
-
request: Request,
|
|
233
|
-
url: URL
|
|
234
|
-
): WebSocketUpgradeResult | Response | null {
|
|
235
|
-
const match = this.matchWsRoute(url.pathname)
|
|
236
|
-
if (!match) {
|
|
237
|
-
return null
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// WebSocketPair is a CF Workers runtime global
|
|
241
|
-
const WsPair = (globalThis as any).WebSocketPair
|
|
242
|
-
if (!WsPair) {
|
|
243
|
-
return new Response('WebSocketPair not available in this runtime', { status: 500 })
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const pair = new WsPair()
|
|
247
|
-
const [client, server] = Object.values(pair) as [WebSocket, WebSocket]
|
|
248
|
-
|
|
249
|
-
// accept the server side so we can send/receive (CF Workers WebSocket extension)
|
|
250
|
-
;(server as any).accept()
|
|
251
|
-
|
|
252
|
-
return { client, server, handler: match.handler, request, url }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* match a pathname against registered WebSocket routes.
|
|
257
|
-
* checks exact matches first, then pattern matches.
|
|
258
|
-
*/
|
|
259
|
-
matchWsRoute(pathname: string): RouteMatch | null {
|
|
260
|
-
// exact match first
|
|
261
|
-
const exact = this.wsRoutes.get(pathname)
|
|
262
|
-
if (exact) {
|
|
263
|
-
return { handler: exact, pattern: pathname }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// pattern match
|
|
267
|
-
for (const { regex, pattern, handler } of this.wsPatterns) {
|
|
268
|
-
if (regex.test(pathname)) {
|
|
269
|
-
return { handler, pattern }
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return null
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* factory function — creates a pre-configured adapter with the standard
|
|
279
|
-
* zero-cache WebSocket routes registered.
|
|
280
|
-
*
|
|
281
|
-
* handlers are placeholders that will be replaced during Phase 3 integration
|
|
282
|
-
* when we wire up the actual zero-cache WebSocket protocol handlers.
|
|
283
|
-
*/
|
|
284
|
-
export function createHttpServiceAdapter(): HttpServiceAdapter {
|
|
285
|
-
const adapter = new HttpServiceAdapter()
|
|
286
|
-
|
|
287
|
-
// register known zero-cache websocket route patterns.
|
|
288
|
-
// actual handlers will be set during integration (Phase 3).
|
|
289
|
-
// these patterns match zero-cache's replication endpoints.
|
|
290
|
-
// for now, register the patterns so path matching works.
|
|
291
|
-
|
|
292
|
-
return adapter
|
|
293
|
-
}
|