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.
Files changed (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. package/src/worker/zero-cache-embed.ts +0 -277
@@ -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 }