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