orez 0.2.26 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/cf-do/worker.d.ts.map +1 -1
  2. package/dist/cf-do/worker.js +9 -1
  3. package/dist/cf-do/worker.js.map +1 -1
  4. package/dist/pg-proxy-do-backend.d.ts +2 -0
  5. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  6. package/dist/pg-proxy-do-backend.js +49 -7
  7. package/dist/pg-proxy-do-backend.js.map +1 -1
  8. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  9. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  10. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  11. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  12. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  13. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  14. package/dist/pg-sqlite-compiler/index.js +59 -0
  15. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  16. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  17. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  18. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  19. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  20. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  21. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  22. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  23. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  24. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  25. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  27. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  29. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  31. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  33. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  35. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  39. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  41. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/types.js +2 -0
  43. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  44. package/package.json +8 -4
  45. package/src/admin/admin-data.test.ts +0 -348
  46. package/src/admin/http-proxy.ts +0 -252
  47. package/src/admin/log-store.ts +0 -192
  48. package/src/admin/server.ts +0 -471
  49. package/src/admin/ui.ts +0 -1322
  50. package/src/bench/proxy-throughput.bench.ts +0 -343
  51. package/src/bench/serial-mutations.bench.ts +0 -270
  52. package/src/browser.ts +0 -203
  53. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  54. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  55. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  56. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  57. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  58. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  59. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  60. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  61. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  62. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  63. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  64. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  65. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  66. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  67. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  69. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  75. package/src/cf-do/ARCHITECTURE.md +0 -83
  76. package/src/cf-do/watermark.test.ts +0 -103
  77. package/src/cf-do/watermark.ts +0 -118
  78. package/src/cf-do/worker.ts +0 -1033
  79. package/src/cf-do/wrangler.toml +0 -11
  80. package/src/cf-pglite/README.md +0 -19
  81. package/src/change-tracking.ts +0 -25
  82. package/src/child-process.test.ts +0 -147
  83. package/src/child-process.ts +0 -90
  84. package/src/cli-entry.ts +0 -72
  85. package/src/cli.test.ts +0 -38
  86. package/src/cli.ts +0 -1214
  87. package/src/config.ts +0 -150
  88. package/src/do-sql-tracking.test.ts +0 -19
  89. package/src/do-sql-tracking.ts +0 -19
  90. package/src/index.ts +0 -1215
  91. package/src/integration/integration.test.ts +0 -517
  92. package/src/integration/native-binary.guard.test.ts +0 -13
  93. package/src/integration/native-startup.test.ts +0 -44
  94. package/src/integration/replication-latency.test.ts +0 -428
  95. package/src/integration/restore-live-stress.test.ts +0 -433
  96. package/src/integration/restore-reset.test.ts +0 -400
  97. package/src/integration/restore.test.ts +0 -274
  98. package/src/integration/test-permissions.ts +0 -147
  99. package/src/load-config.ts +0 -46
  100. package/src/log.ts +0 -96
  101. package/src/mutex.ts +0 -47
  102. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  103. package/src/pg-proxy-browser.ts +0 -2022
  104. package/src/pg-proxy-do-backend.test.ts +0 -3890
  105. package/src/pg-proxy-do-backend.ts +0 -7157
  106. package/src/pg-proxy.ts +0 -1087
  107. package/src/pglite-ipc.test.ts +0 -116
  108. package/src/pglite-ipc.ts +0 -266
  109. package/src/pglite-manager.ts +0 -557
  110. package/src/pglite-web-proxy.test.ts +0 -57
  111. package/src/pglite-web-proxy.ts +0 -221
  112. package/src/pglite-web-worker.ts +0 -152
  113. package/src/pglite-worker-thread.ts +0 -253
  114. package/src/port.ts +0 -25
  115. package/src/process-title.ts +0 -9
  116. package/src/recovery.ts +0 -155
  117. package/src/replication/change-tracker.test.ts +0 -357
  118. package/src/replication/change-tracker.ts +0 -279
  119. package/src/replication/handler.test.ts +0 -511
  120. package/src/replication/handler.ts +0 -1190
  121. package/src/replication/pgoutput-encoder.test.ts +0 -697
  122. package/src/replication/pgoutput-encoder.ts +0 -373
  123. package/src/replication/tcp-replication.test.ts +0 -876
  124. package/src/replication/zero-compat.test.ts +0 -1150
  125. package/src/restore-stress.test.ts +0 -188
  126. package/src/s3-local.ts +0 -203
  127. package/src/shim/hooks.mjs +0 -120
  128. package/src/shim/register.mjs +0 -4
  129. package/src/sqlite-mode/apply-mode.ts +0 -224
  130. package/src/sqlite-mode/index.ts +0 -15
  131. package/src/sqlite-mode/native-binary.ts +0 -89
  132. package/src/sqlite-mode/package-resolve.ts +0 -17
  133. package/src/sqlite-mode/resolve-mode.ts +0 -80
  134. package/src/sqlite-mode/shim-template.ts +0 -159
  135. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  136. package/src/sqlite-mode/types.ts +0 -30
  137. package/src/vite-plugin.ts +0 -67
  138. package/src/wasm-sqlite.test.ts +0 -537
  139. package/src/worker/browser-admin.ts +0 -52
  140. package/src/worker/browser-build-config.test.ts +0 -71
  141. package/src/worker/browser-build-config.ts +0 -109
  142. package/src/worker/browser-embed-admin.test.ts +0 -75
  143. package/src/worker/browser-embed.ts +0 -345
  144. package/src/worker/cf-patches.ts +0 -384
  145. package/src/worker/embed-integration.test.ts +0 -321
  146. package/src/worker/index.ts +0 -138
  147. package/src/worker/shims/fastify.test.ts +0 -255
  148. package/src/worker/shims/fastify.ts +0 -306
  149. package/src/worker/shims/http-service.test.ts +0 -355
  150. package/src/worker/shims/http-service.ts +0 -293
  151. package/src/worker/shims/node-stub.ts +0 -290
  152. package/src/worker/shims/oxfmt.ts +0 -3
  153. package/src/worker/shims/postgres-browser.ts +0 -59
  154. package/src/worker/shims/postgres-socket.test.ts +0 -576
  155. package/src/worker/shims/postgres-socket.ts +0 -310
  156. package/src/worker/shims/postgres.test.ts +0 -364
  157. package/src/worker/shims/postgres.ts +0 -1454
  158. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  159. package/src/worker/shims/sqlite-browser.ts +0 -175
  160. package/src/worker/shims/sqlite.test.ts +0 -786
  161. package/src/worker/shims/sqlite.ts +0 -978
  162. package/src/worker/shims/stream-browser.ts +0 -15
  163. package/src/worker/shims/ws-browser.test.ts +0 -205
  164. package/src/worker/shims/ws-browser.ts +0 -248
  165. package/src/worker/shims/ws.test.ts +0 -288
  166. package/src/worker/shims/ws.ts +0 -467
  167. package/src/worker/shims/zero-process-env.ts +0 -11
  168. package/src/worker/types.ts +0 -75
  169. package/src/worker/worker-integration.test.ts +0 -223
  170. package/src/worker/worker.test.ts +0 -136
  171. package/src/worker/zero-cache-embed-cf.ts +0 -463
  172. 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 }