orez 0.2.27 → 0.2.30
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 +3 -0
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +37 -15
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -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/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.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/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
- package/src/cf-do/ARCHITECTURE.md +0 -93
- package/src/cf-do/CHAT_E2E.md +0 -213
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1041
- 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 -40
- 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 -7191
- package/src/pg-proxy.ts +0 -1087
- package/src/pg-sqlite-compiler/README.md +0 -53
- package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
- package/src/pg-sqlite-compiler/index.ts +0 -73
- package/src/pg-sqlite-compiler/integration.test.ts +0 -136
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
- package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
- package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
- package/src/pg-sqlite-compiler/passes/index.ts +0 -49
- package/src/pg-sqlite-compiler/passes/types.ts +0 -156
- package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
- package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
- package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
- package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
- package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
- package/src/pg-sqlite-compiler/types.ts +0 -63
- 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
package/src/worker/index.ts
DELETED
|
@@ -1,138 +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
|
-
* orez/worker: embeddable PGlite + change tracking.
|
|
6
|
-
*
|
|
7
|
-
* runs without Node.js dependencies — works in CF Workers, browsers,
|
|
8
|
-
* vitest, bun, deno. provides the PGlite database layer with change
|
|
9
|
-
* tracking and replication encoding that zero-cache needs.
|
|
10
|
-
*
|
|
11
|
-
* usage:
|
|
12
|
-
* import { createOrezWorker } from 'orez/worker'
|
|
13
|
-
*
|
|
14
|
-
* const orez = await createOrezWorker({
|
|
15
|
-
* pgliteOptions: { dataDir: 'memory://' },
|
|
16
|
-
* })
|
|
17
|
-
* await orez.exec('CREATE TABLE foo (id TEXT PRIMARY KEY, name TEXT)')
|
|
18
|
-
* await orez.installChangeTracking()
|
|
19
|
-
* await orez.query('INSERT INTO foo VALUES ($1, $2)', ['1', 'bar'])
|
|
20
|
-
* const changes = await orez.getChangesSince(0)
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { Mutex } from '../mutex.js'
|
|
24
|
-
import {
|
|
25
|
-
installChangeTracking,
|
|
26
|
-
getChangesSince,
|
|
27
|
-
getCurrentWatermark,
|
|
28
|
-
purgeConsumedChanges,
|
|
29
|
-
} from '../replication/change-tracker.js'
|
|
30
|
-
import { handleStartReplication } from '../replication/handler.js'
|
|
31
|
-
|
|
32
|
-
import type { ChangeRecord } from '../replication/change-tracker.js'
|
|
33
|
-
import type { ReplicationWriter } from '../replication/handler.js'
|
|
34
|
-
import type { OrezWorkerOptions, OrezWorker } from './types.js'
|
|
35
|
-
import type { PGlite, Results } from '@electric-sql/pglite'
|
|
36
|
-
|
|
37
|
-
export type { OrezWorkerOptions, OrezWorker } from './types.js'
|
|
38
|
-
export type { ChangeRecord } from '../replication/change-tracker.js'
|
|
39
|
-
export type { ReplicationWriter } from '../replication/handler.js'
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* create an orez worker instance.
|
|
43
|
-
*
|
|
44
|
-
* accepts either a pre-created PGlite instance or PGliteOptions to
|
|
45
|
-
* create one. installs the _orez schema and change tracking infrastructure.
|
|
46
|
-
*/
|
|
47
|
-
export async function createOrezWorker(opts: OrezWorkerOptions): Promise<OrezWorker> {
|
|
48
|
-
let db: PGlite
|
|
49
|
-
let ownsInstance: boolean
|
|
50
|
-
|
|
51
|
-
if (opts.pglite) {
|
|
52
|
-
db = opts.pglite
|
|
53
|
-
ownsInstance = false
|
|
54
|
-
} else if (opts.pgliteOptions) {
|
|
55
|
-
// dynamic import so PGlite isn't required at module load time.
|
|
56
|
-
// this lets the worker module be imported in environments where
|
|
57
|
-
// PGlite is provided externally (CF Workers with custom WASM).
|
|
58
|
-
const { PGlite: PGliteCtor } = await import('@electric-sql/pglite')
|
|
59
|
-
db = new PGliteCtor(opts.pgliteOptions)
|
|
60
|
-
await db.waitReady
|
|
61
|
-
ownsInstance = true
|
|
62
|
-
} else {
|
|
63
|
-
throw new Error('orez/worker: provide either pglite or pgliteOptions')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const mutex = new Mutex()
|
|
67
|
-
|
|
68
|
-
// set up publication env if provided (change-tracker reads this)
|
|
69
|
-
if (opts.publications?.length) {
|
|
70
|
-
// change-tracker reads ZERO_APP_PUBLICATIONS to decide which tables to track.
|
|
71
|
-
// in non-Node environments globalThis may not have process.env, so we
|
|
72
|
-
// set it defensively.
|
|
73
|
-
if (typeof globalThis !== 'undefined') {
|
|
74
|
-
;(globalThis as any).process ??= {}
|
|
75
|
-
;(globalThis as any).process.env ??= {}
|
|
76
|
-
;(globalThis as any).process.env.ZERO_APP_PUBLICATIONS = opts.publications.join(',')
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// install core schema (plpgsql, _orez schema)
|
|
81
|
-
await db.exec('CREATE EXTENSION IF NOT EXISTS plpgsql')
|
|
82
|
-
|
|
83
|
-
// install change tracking (creates _orez schema, tables, trigger function)
|
|
84
|
-
await installChangeTracking(db)
|
|
85
|
-
|
|
86
|
-
const worker: OrezWorker = {
|
|
87
|
-
get db() {
|
|
88
|
-
return db
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
get mutex() {
|
|
92
|
-
return mutex
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
get ownsInstance() {
|
|
96
|
-
return ownsInstance
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
async query<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
100
|
-
sql: string,
|
|
101
|
-
params?: unknown[]
|
|
102
|
-
): Promise<Results<T>> {
|
|
103
|
-
return db.query<T>(sql, params)
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
async exec(sql: string): Promise<void> {
|
|
107
|
-
await db.exec(sql)
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
async installChangeTracking(): Promise<void> {
|
|
111
|
-
await installChangeTracking(db)
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
async getChangesSince(watermark: number, limit?: number): Promise<ChangeRecord[]> {
|
|
115
|
-
return getChangesSince(db, watermark, limit)
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
async getCurrentWatermark(): Promise<number> {
|
|
119
|
-
return getCurrentWatermark(db)
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
async purgeChanges(watermark: number): Promise<number> {
|
|
123
|
-
return purgeConsumedChanges(db, watermark)
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
async startReplication(writer: ReplicationWriter): Promise<void> {
|
|
127
|
-
await handleStartReplication('START_REPLICATION', writer, db, mutex)
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
async close(): Promise<void> {
|
|
131
|
-
if (ownsInstance && !db.closed) {
|
|
132
|
-
await db.close()
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return worker
|
|
138
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import Fastify, { type FastifyShim } from './fastify.js'
|
|
4
|
-
|
|
5
|
-
describe('Fastify shim', () => {
|
|
6
|
-
let app: FastifyShim
|
|
7
|
-
let origGlobal: unknown
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
origGlobal = (globalThis as any).__orez_fastify_instance
|
|
11
|
-
app = Fastify()
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
if (origGlobal !== undefined) {
|
|
16
|
-
;(globalThis as any).__orez_fastify_instance = origGlobal
|
|
17
|
-
} else {
|
|
18
|
-
delete (globalThis as any).__orez_fastify_instance
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
describe('constructor', () => {
|
|
23
|
-
it('creates an instance', () => {
|
|
24
|
-
expect(app).toBeDefined()
|
|
25
|
-
expect(app.server).toBeDefined()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('registers itself on globalThis', () => {
|
|
29
|
-
expect((globalThis as any).__orez_fastify_instance).toBe(app)
|
|
30
|
-
})
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
describe('route registration', () => {
|
|
34
|
-
it('registers GET routes', async () => {
|
|
35
|
-
app.get('/', (_req, reply) => reply.send('ok'))
|
|
36
|
-
const result = await app.inject({ method: 'GET', url: '/' })
|
|
37
|
-
expect(result.statusCode).toBe(200)
|
|
38
|
-
expect(result.body).toBe('ok')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('registers POST routes', async () => {
|
|
42
|
-
app.post('/data', (_req, reply) => reply.send('created'))
|
|
43
|
-
const result = await app.inject({ method: 'POST', url: '/data' })
|
|
44
|
-
expect(result.statusCode).toBe(200)
|
|
45
|
-
expect(result.body).toBe('created')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('registers PUT routes', async () => {
|
|
49
|
-
app.put('/item', (_req, reply) => {
|
|
50
|
-
reply.code(200).send('updated')
|
|
51
|
-
})
|
|
52
|
-
const result = await app.inject({ method: 'PUT', url: '/item' })
|
|
53
|
-
expect(result.statusCode).toBe(200)
|
|
54
|
-
expect(result.body).toBe('updated')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('registers DELETE routes', async () => {
|
|
58
|
-
app.delete('/item', (_req, reply) => {
|
|
59
|
-
reply.code(204).send('')
|
|
60
|
-
})
|
|
61
|
-
const result = await app.inject({ method: 'DELETE', url: '/item' })
|
|
62
|
-
expect(result.statusCode).toBe(204)
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe('inject()', () => {
|
|
67
|
-
it('returns 404 for unregistered routes', async () => {
|
|
68
|
-
const result = await app.inject({ method: 'GET', url: '/nope' })
|
|
69
|
-
expect(result.statusCode).toBe(404)
|
|
70
|
-
expect(result.body).toBe('Not Found')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('passes request headers to handler', async () => {
|
|
74
|
-
let capturedHeaders: Record<string, string | undefined> = {}
|
|
75
|
-
app.get('/headers', (req, reply) => {
|
|
76
|
-
capturedHeaders = req.headers
|
|
77
|
-
reply.send('ok')
|
|
78
|
-
})
|
|
79
|
-
await app.inject({
|
|
80
|
-
method: 'GET',
|
|
81
|
-
url: '/headers',
|
|
82
|
-
headers: { 'x-custom': 'test-value' },
|
|
83
|
-
})
|
|
84
|
-
expect(capturedHeaders['x-custom']).toBe('test-value')
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('passes query parameters', async () => {
|
|
88
|
-
let capturedQuery: Record<string, string> = {}
|
|
89
|
-
app.get('/search', (req, reply) => {
|
|
90
|
-
capturedQuery = req.query || {}
|
|
91
|
-
reply.send('ok')
|
|
92
|
-
})
|
|
93
|
-
await app.inject({ method: 'GET', url: '/search?q=hello&page=2' })
|
|
94
|
-
expect(capturedQuery.q).toBe('hello')
|
|
95
|
-
expect(capturedQuery.page).toBe('2')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('passes parsed JSON body', async () => {
|
|
99
|
-
let capturedBody: unknown
|
|
100
|
-
app.post('/json', (req, reply) => {
|
|
101
|
-
capturedBody = req.body
|
|
102
|
-
reply.send('ok')
|
|
103
|
-
})
|
|
104
|
-
await app.inject({
|
|
105
|
-
method: 'POST',
|
|
106
|
-
url: '/json',
|
|
107
|
-
payload: '{"name":"test"}',
|
|
108
|
-
})
|
|
109
|
-
expect(capturedBody).toEqual({ name: 'test' })
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('passes raw string body if not JSON', async () => {
|
|
113
|
-
let capturedBody: unknown
|
|
114
|
-
app.post('/raw', (req, reply) => {
|
|
115
|
-
capturedBody = req.body
|
|
116
|
-
reply.send('ok')
|
|
117
|
-
})
|
|
118
|
-
await app.inject({
|
|
119
|
-
method: 'POST',
|
|
120
|
-
url: '/raw',
|
|
121
|
-
payload: 'not json',
|
|
122
|
-
})
|
|
123
|
-
expect(capturedBody).toBe('not json')
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('reply.code() sets status code', async () => {
|
|
127
|
-
app.get('/created', (_req, reply) => {
|
|
128
|
-
reply.code(201).send('done')
|
|
129
|
-
})
|
|
130
|
-
const result = await app.inject({ method: 'GET', url: '/created' })
|
|
131
|
-
expect(result.statusCode).toBe(201)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('reply.header() sets response headers', async () => {
|
|
135
|
-
app.get('/custom', (_req, reply) => {
|
|
136
|
-
reply.header('X-Custom', 'value').send('ok')
|
|
137
|
-
})
|
|
138
|
-
const result = await app.inject({ method: 'GET', url: '/custom' })
|
|
139
|
-
expect(result.headers['x-custom']).toBe('value')
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('reply.type() sets content-type', async () => {
|
|
143
|
-
app.get('/typed', (_req, reply) => {
|
|
144
|
-
reply.type('text/html').send('<h1>hi</h1>')
|
|
145
|
-
})
|
|
146
|
-
const result = await app.inject({ method: 'GET', url: '/typed' })
|
|
147
|
-
expect(result.headers['content-type']).toBe('text/html')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('auto-serializes object responses as JSON', async () => {
|
|
151
|
-
app.get('/obj', (_req, reply) => {
|
|
152
|
-
reply.send({ foo: 'bar' })
|
|
153
|
-
})
|
|
154
|
-
const result = await app.inject({ method: 'GET', url: '/obj' })
|
|
155
|
-
expect(result.headers['content-type']).toBe('application/json')
|
|
156
|
-
expect(JSON.parse(result.body)).toEqual({ foo: 'bar' })
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('uses handler return value if reply.send() not called', async () => {
|
|
160
|
-
app.get('/return', () => 'returned')
|
|
161
|
-
const result = await app.inject({ method: 'GET', url: '/return' })
|
|
162
|
-
expect(result.body).toBe('returned')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('returns 500 on handler error', async () => {
|
|
166
|
-
app.get('/boom', () => {
|
|
167
|
-
throw new Error('handler error')
|
|
168
|
-
})
|
|
169
|
-
const result = await app.inject({ method: 'GET', url: '/boom' })
|
|
170
|
-
expect(result.statusCode).toBe(500)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('handles async handlers', async () => {
|
|
174
|
-
app.get('/async', async (_req, reply) => {
|
|
175
|
-
await new Promise((r) => setTimeout(r, 5))
|
|
176
|
-
reply.send('async ok')
|
|
177
|
-
})
|
|
178
|
-
const result = await app.inject({ method: 'GET', url: '/async' })
|
|
179
|
-
expect(result.statusCode).toBe(200)
|
|
180
|
-
expect(result.body).toBe('async ok')
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('is case-insensitive on method matching', async () => {
|
|
184
|
-
app.get('/test', (_req, reply) => reply.send('ok'))
|
|
185
|
-
const result = await app.inject({ method: 'get', url: '/test' })
|
|
186
|
-
expect(result.statusCode).toBe(200)
|
|
187
|
-
})
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
describe('lifecycle', () => {
|
|
191
|
-
it('listen() resolves to an address string', async () => {
|
|
192
|
-
const addr = await app.listen({ host: '::', port: 4848 })
|
|
193
|
-
expect(typeof addr).toBe('string')
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('ready() resolves', async () => {
|
|
197
|
-
await expect(app.ready()).resolves.toBeUndefined()
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('close() resolves', async () => {
|
|
201
|
-
await expect(app.close()).resolves.toBeUndefined()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('register() returns this for chaining', () => {
|
|
205
|
-
const result = app.register(() => {})
|
|
206
|
-
expect(result).toBe(app)
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('FakeHttpServer', () => {
|
|
211
|
-
it('has address() method', () => {
|
|
212
|
-
const addr = app.server.address()
|
|
213
|
-
expect(addr).toHaveProperty('address')
|
|
214
|
-
expect(addr).toHaveProperty('port')
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('supports onMessageType for EventEmitter IPC', () => {
|
|
218
|
-
let received: unknown = null
|
|
219
|
-
let receivedHandle: unknown = null
|
|
220
|
-
|
|
221
|
-
app.server.onMessageType('handoff', (msg: unknown, handle?: unknown) => {
|
|
222
|
-
received = msg
|
|
223
|
-
receivedHandle = handle
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
const payload = { message: { url: '/test' }, head: new Uint8Array(0) }
|
|
227
|
-
const fakeSocket = { accept: () => {} }
|
|
228
|
-
|
|
229
|
-
app.server.emit('message', ['handoff', payload], fakeSocket)
|
|
230
|
-
|
|
231
|
-
expect(received).toEqual(payload)
|
|
232
|
-
expect(receivedHandle).toBe(fakeSocket)
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it('onMessageType ignores non-matching types', () => {
|
|
236
|
-
let called = false
|
|
237
|
-
app.server.onMessageType('handoff', () => {
|
|
238
|
-
called = true
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
app.server.emit('message', ['ready', { ready: true }])
|
|
242
|
-
expect(called).toBe(false)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('onMessageType ignores non-array messages', () => {
|
|
246
|
-
let called = false
|
|
247
|
-
app.server.onMessageType('handoff', () => {
|
|
248
|
-
called = true
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
app.server.emit('message', 'not an array')
|
|
252
|
-
expect(called).toBe(false)
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
})
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* fastify shim for cloudflare workers.
|
|
3
|
-
*
|
|
4
|
-
* minimal fastify replacement that captures route registrations and
|
|
5
|
-
* exposes them via inject() for request processing. zero-cache's
|
|
6
|
-
* HttpService creates a Fastify instance, registers routes, and calls
|
|
7
|
-
* listen(). on CF Workers we skip listen() and route DO fetch()
|
|
8
|
-
* through inject().
|
|
9
|
-
*
|
|
10
|
-
* supports { websocket: true } routes: when a handoff event arrives on
|
|
11
|
-
* the server, matches against websocket routes and calls the handler
|
|
12
|
-
* with the socket directly. this enables the serving-replicator's
|
|
13
|
-
* in-process WebSocket connection to the change-streamer.
|
|
14
|
-
*
|
|
15
|
-
* usage with bundler alias:
|
|
16
|
-
* alias: { 'fastify': './src/worker/shims/fastify.js' }
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import EventEmitter from 'node:events'
|
|
20
|
-
|
|
21
|
-
import { WebSocket as WsShim, WebSocketServer as WsServerShim } from './ws.js'
|
|
22
|
-
|
|
23
|
-
// -- types matching fastify's minimal surface used by zero-cache --
|
|
24
|
-
|
|
25
|
-
interface FastifyRequest {
|
|
26
|
-
headers: Record<string, string | undefined>
|
|
27
|
-
url: string
|
|
28
|
-
method: string
|
|
29
|
-
body?: unknown
|
|
30
|
-
query?: Record<string, string>
|
|
31
|
-
params?: Record<string, string>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface FastifyReply {
|
|
35
|
-
code(statusCode: number): FastifyReply
|
|
36
|
-
header(name: string, value: string): FastifyReply
|
|
37
|
-
send(payload?: unknown): void
|
|
38
|
-
type(contentType: string): FastifyReply
|
|
39
|
-
status(statusCode: number): FastifyReply
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type RouteHandler = (
|
|
43
|
-
request: FastifyRequest,
|
|
44
|
-
reply: FastifyReply
|
|
45
|
-
) => unknown | Promise<unknown>
|
|
46
|
-
|
|
47
|
-
interface InjectOptions {
|
|
48
|
-
method: string
|
|
49
|
-
url: string
|
|
50
|
-
headers?: Record<string, string>
|
|
51
|
-
payload?: string | null
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface InjectResult {
|
|
55
|
-
statusCode: number
|
|
56
|
-
headers: Record<string, string>
|
|
57
|
-
body: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// -- fake http.Server replacement --
|
|
61
|
-
// uses EventEmitter with onMessageType for zero-cache's
|
|
62
|
-
// installWebSocketHandoff non-Server branch.
|
|
63
|
-
|
|
64
|
-
class FakeHttpServer extends EventEmitter {
|
|
65
|
-
#address = { address: '0.0.0.0', port: 0, family: 'IPv4' as const }
|
|
66
|
-
|
|
67
|
-
address() {
|
|
68
|
-
return this.#address
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** match the onMessageType pattern from zero-cache processes.js */
|
|
72
|
-
onMessageType(
|
|
73
|
-
type: string,
|
|
74
|
-
handler: (msg: unknown, sendHandle?: unknown) => void
|
|
75
|
-
): this {
|
|
76
|
-
this.on('message', (data: unknown, sendHandle?: unknown) => {
|
|
77
|
-
if (Array.isArray(data) && data.length === 2 && data[0] === type) {
|
|
78
|
-
handler(data[1], sendHandle)
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
return this
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
listen() {
|
|
85
|
-
/* no-op on CF */
|
|
86
|
-
}
|
|
87
|
-
close() {
|
|
88
|
-
/* no-op on CF */
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// use the real WebSocketServer from the WS shim — it wraps raw sockets
|
|
93
|
-
// in a proper WebSocket class with ping/pong/on/emit etc.
|
|
94
|
-
|
|
95
|
-
// -- route pattern matching --
|
|
96
|
-
// converts fastify route patterns like "/replication/:version/changes"
|
|
97
|
-
// to regex for matching incoming URLs
|
|
98
|
-
|
|
99
|
-
function patternToRegex(pattern: string): RegExp {
|
|
100
|
-
const escaped = pattern
|
|
101
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
102
|
-
.replace(/:(\w+)/g, '(?<$1>[^/]+)')
|
|
103
|
-
return new RegExp(`^${escaped}$`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// -- fastify shim instance --
|
|
107
|
-
|
|
108
|
-
class FastifyShim {
|
|
109
|
-
server: FakeHttpServer
|
|
110
|
-
websocketServer: WsServerShim
|
|
111
|
-
#routes = new Map<string, RouteHandler>()
|
|
112
|
-
#wsRoutes: Array<{ pattern: RegExp; handler: (ws: unknown, req: any) => void }> = []
|
|
113
|
-
#readyResolvers: Array<() => void> = []
|
|
114
|
-
|
|
115
|
-
constructor() {
|
|
116
|
-
this.server = new FakeHttpServer()
|
|
117
|
-
this.websocketServer = new WsServerShim()
|
|
118
|
-
this.#installWsHandoffHandler()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// listen for in-process WebSocket handoff events on the server.
|
|
122
|
-
// when the WS shim creates an in-process connection, it emits a handoff
|
|
123
|
-
// event. we match the URL against registered { websocket: true } routes
|
|
124
|
-
// and call the handler with the socket.
|
|
125
|
-
#installWsHandoffHandler() {
|
|
126
|
-
this.server.onMessageType('handoff', (msg: any, socket?: any) => {
|
|
127
|
-
this.tryHandoff(msg, socket)
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// try to match a handoff message against registered websocket routes.
|
|
132
|
-
// returns true if a route matched, false otherwise.
|
|
133
|
-
// this is public so callers (ws shim, browser-embed) can iterate
|
|
134
|
-
// all fastify instances and stop at the first match.
|
|
135
|
-
tryHandoff(msg: any, socket?: any): boolean {
|
|
136
|
-
if (!socket || !msg?.message?.url) return false
|
|
137
|
-
const url = msg.message.url
|
|
138
|
-
const parsedUrl = new URL(url, 'http://localhost')
|
|
139
|
-
const pathname = parsedUrl.pathname
|
|
140
|
-
|
|
141
|
-
for (const route of this.#wsRoutes) {
|
|
142
|
-
if (route.pattern.test(pathname)) {
|
|
143
|
-
const req = {
|
|
144
|
-
url,
|
|
145
|
-
headers: msg.message.headers || {},
|
|
146
|
-
method: msg.message.method || 'GET',
|
|
147
|
-
}
|
|
148
|
-
// wrap socket through handleUpgrade so it gets the full WS API
|
|
149
|
-
// (ping, on, once, terminate, etc.) needed by zero-cache's streamOut
|
|
150
|
-
this.websocketServer.handleUpgrade(
|
|
151
|
-
req,
|
|
152
|
-
socket,
|
|
153
|
-
Buffer.from(new Uint8Array(0)),
|
|
154
|
-
(ws: any) => {
|
|
155
|
-
route.handler(ws, req)
|
|
156
|
-
}
|
|
157
|
-
)
|
|
158
|
-
return true
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return false
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// route registration — supports optional { websocket: true } option
|
|
165
|
-
get(path: string, optsOrHandler: any, handler?: any) {
|
|
166
|
-
if (typeof optsOrHandler === 'function') {
|
|
167
|
-
this.#routes.set(`GET:${path}`, optsOrHandler)
|
|
168
|
-
} else if (optsOrHandler?.websocket && handler) {
|
|
169
|
-
// websocket route — register for handoff matching
|
|
170
|
-
this.#wsRoutes.push({
|
|
171
|
-
pattern: patternToRegex(path),
|
|
172
|
-
handler,
|
|
173
|
-
})
|
|
174
|
-
} else if (handler) {
|
|
175
|
-
this.#routes.set(`GET:${path}`, handler)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
post(path: string, handler: RouteHandler) {
|
|
179
|
-
this.#routes.set(`POST:${path}`, handler)
|
|
180
|
-
}
|
|
181
|
-
put(path: string, handler: RouteHandler) {
|
|
182
|
-
this.#routes.set(`PUT:${path}`, handler)
|
|
183
|
-
}
|
|
184
|
-
delete(path: string, handler: RouteHandler) {
|
|
185
|
-
this.#routes.set(`DELETE:${path}`, handler)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// plugin registration (no-op — zero-cache registers @fastify/websocket here)
|
|
189
|
-
register(_plugin: unknown, _opts?: unknown): this {
|
|
190
|
-
return this
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// lifecycle
|
|
194
|
-
async ready(): Promise<void> {
|
|
195
|
-
for (const resolve of this.#readyResolvers) resolve()
|
|
196
|
-
this.#readyResolvers = []
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async listen(_opts?: { host?: string; port?: number }): Promise<string> {
|
|
200
|
-
await this.ready()
|
|
201
|
-
return '0.0.0.0:0'
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async close(): Promise<void> {
|
|
205
|
-
// no-op on CF
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// inject — process a request through registered routes
|
|
209
|
-
async inject(opts: InjectOptions): Promise<InjectResult> {
|
|
210
|
-
const method = opts.method.toUpperCase()
|
|
211
|
-
const urlObj = new URL(opts.url, 'http://localhost')
|
|
212
|
-
const pathname = urlObj.pathname
|
|
213
|
-
|
|
214
|
-
// find matching route
|
|
215
|
-
const handler = this.#routes.get(`${method}:${pathname}`)
|
|
216
|
-
if (!handler) {
|
|
217
|
-
return { statusCode: 404, headers: {}, body: 'Not Found' }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// build fake request
|
|
221
|
-
const request: FastifyRequest = {
|
|
222
|
-
headers: opts.headers || {},
|
|
223
|
-
url: opts.url,
|
|
224
|
-
method,
|
|
225
|
-
body: opts.payload ? tryParseJson(opts.payload) : undefined,
|
|
226
|
-
query: Object.fromEntries(urlObj.searchParams),
|
|
227
|
-
params: {},
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// build fake reply
|
|
231
|
-
let statusCode = 200
|
|
232
|
-
const headers: Record<string, string> = {}
|
|
233
|
-
let body = ''
|
|
234
|
-
let sent = false
|
|
235
|
-
|
|
236
|
-
const reply: FastifyReply = {
|
|
237
|
-
code(code: number) {
|
|
238
|
-
statusCode = code
|
|
239
|
-
return reply
|
|
240
|
-
},
|
|
241
|
-
status(code: number) {
|
|
242
|
-
statusCode = code
|
|
243
|
-
return reply
|
|
244
|
-
},
|
|
245
|
-
header(name: string, value: string) {
|
|
246
|
-
headers[name.toLowerCase()] = value
|
|
247
|
-
return reply
|
|
248
|
-
},
|
|
249
|
-
type(contentType: string) {
|
|
250
|
-
headers['content-type'] = contentType
|
|
251
|
-
return reply
|
|
252
|
-
},
|
|
253
|
-
send(payload?: unknown) {
|
|
254
|
-
sent = true
|
|
255
|
-
if (payload === undefined || payload === null) {
|
|
256
|
-
body = ''
|
|
257
|
-
} else if (typeof payload === 'string') {
|
|
258
|
-
body = payload
|
|
259
|
-
} else {
|
|
260
|
-
body = JSON.stringify(payload)
|
|
261
|
-
if (!headers['content-type']) {
|
|
262
|
-
headers['content-type'] = 'application/json'
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
const result = await handler(request, reply)
|
|
270
|
-
// if handler returned a value and didn't call reply.send()
|
|
271
|
-
if (!sent && result !== undefined) {
|
|
272
|
-
reply.send(result)
|
|
273
|
-
}
|
|
274
|
-
} catch (err) {
|
|
275
|
-
statusCode = 500
|
|
276
|
-
body = String(err)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return { statusCode, headers, body }
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function tryParseJson(str: string): unknown {
|
|
284
|
-
try {
|
|
285
|
-
return JSON.parse(str)
|
|
286
|
-
} catch {
|
|
287
|
-
return str
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// -- default export matching fastify's API --
|
|
292
|
-
|
|
293
|
-
function Fastify(_opts?: unknown): FastifyShim {
|
|
294
|
-
const instance = new FastifyShim()
|
|
295
|
-
// always overwrite — the ZeroDispatcher (which has the WS handoff routes)
|
|
296
|
-
// is created LAST, so the final instance is the one handleWebSocket needs.
|
|
297
|
-
;(globalThis as any).__orez_fastify_instance = instance
|
|
298
|
-
// track all instances so callers can try handoff against each one
|
|
299
|
-
;(globalThis as any).__orez_fastify_instances =
|
|
300
|
-
(globalThis as any).__orez_fastify_instances || []
|
|
301
|
-
;(globalThis as any).__orez_fastify_instances.push(instance)
|
|
302
|
-
return instance
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export default Fastify
|
|
306
|
-
export type { FastifyRequest, FastifyReply, FastifyShim }
|