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,348 +0,0 @@
1
- /**
2
- * integration tests for the admin data explorer endpoints.
3
- *
4
- * spins up pglite instances + admin server directly (no zero-cache)
5
- * and exercises the /api/db/* and /api/sqlite/* endpoints.
6
- */
7
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
8
- import { resolve } from 'node:path'
9
-
10
- import { PGlite } from '@electric-sql/pglite'
11
- import { afterAll, beforeAll, describe, expect, test } from 'vitest'
12
-
13
- import { startAdminServer } from './server.js'
14
-
15
- import type { ZeroLiteConfig } from '../config.js'
16
- import type { LogStore } from './log-store.js'
17
- import type { Server } from 'node:http'
18
-
19
- const TEST_PORT = 16400 + Math.floor(Math.random() * 500)
20
- const DATA_DIR = `.orez-admin-data-test-${Date.now()}`
21
-
22
- function stubLogStore(): LogStore {
23
- const entries: any[] = []
24
- return {
25
- push() {},
26
- query() {
27
- return { entries, cursor: 0 }
28
- },
29
- getAll() {
30
- return entries
31
- },
32
- clear() {
33
- entries.length = 0
34
- },
35
- }
36
- }
37
-
38
- function stubConfig(): ZeroLiteConfig {
39
- return {
40
- dataDir: resolve(DATA_DIR),
41
- pgPort: 0,
42
- zeroPort: 0,
43
- adminPort: TEST_PORT,
44
- pgUser: 'test',
45
- pgPassword: 'test',
46
- migrationsDir: '',
47
- seedFile: '',
48
- skipZeroCache: true,
49
- disableWasmSqlite: false,
50
- forceWasmSqlite: false,
51
- useWorkerThreads: false,
52
- singleDb: false,
53
- readReplicas: 0,
54
- logLevel: 'info',
55
- pgliteOptions: {},
56
- checkpointIntervalMs: 0,
57
- maxLogFileSize: 0,
58
- disableDiskLogs: true,
59
- }
60
- }
61
-
62
- describe('admin data explorer', { timeout: 60_000 }, () => {
63
- let server: Server
64
- let postgres: PGlite
65
- let cvr: PGlite
66
- let cdb: PGlite
67
- const base = `http://127.0.0.1:${TEST_PORT}`
68
-
69
- beforeAll(async () => {
70
- mkdirSync(DATA_DIR, { recursive: true })
71
-
72
- postgres = new PGlite()
73
- cvr = new PGlite()
74
- cdb = new PGlite()
75
- await Promise.all([postgres.waitReady, cvr.waitReady, cdb.waitReady])
76
-
77
- // create test tables
78
- await postgres.exec(`
79
- CREATE TABLE public.users (
80
- id serial PRIMARY KEY,
81
- name text NOT NULL,
82
- email text,
83
- active boolean DEFAULT true
84
- );
85
- INSERT INTO public.users (name, email) VALUES
86
- ('alice', 'alice@test.com'),
87
- ('bob', 'bob@test.com'),
88
- ('charlie', 'charlie@test.com');
89
- `)
90
-
91
- await postgres.exec(`
92
- CREATE TABLE public.posts (
93
- id serial PRIMARY KEY,
94
- user_id int REFERENCES users(id),
95
- title text NOT NULL,
96
- body text
97
- );
98
- INSERT INTO public.posts (user_id, title, body) VALUES
99
- (1, 'hello world', 'first post'),
100
- (1, 'second post', 'more content'),
101
- (2, 'bob writes', NULL);
102
- `)
103
-
104
- server = await startAdminServer({
105
- port: TEST_PORT,
106
- logStore: stubLogStore(),
107
- config: stubConfig(),
108
- zeroEnv: {},
109
- startTime: Date.now(),
110
- db: { postgres, cvr, cdb, postgresReplicas: [] } as any,
111
- })
112
- })
113
-
114
- afterAll(async () => {
115
- server?.close()
116
- await Promise.all([postgres?.close(), cvr?.close(), cdb?.close()])
117
- rmSync(DATA_DIR, { recursive: true, force: true })
118
- })
119
-
120
- // --- html ---
121
-
122
- test('GET / serves html', async () => {
123
- const res = await fetch(`${base}/`)
124
- expect(res.status).toBe(200)
125
- expect(res.headers.get('content-type')).toContain('text/html')
126
- const html = await res.text()
127
- expect(html).toContain('oreZ admin')
128
- expect(html).toContain('data-db="sqlite"')
129
- })
130
-
131
- test('GET /data serves html', async () => {
132
- const res = await fetch(`${base}/data`)
133
- expect(res.status).toBe(200)
134
- const html = await res.text()
135
- expect(html).toContain('sql-editor')
136
- })
137
-
138
- // --- /api/db/tables ---
139
-
140
- test('lists postgres tables', async () => {
141
- const res = await fetch(`${base}/api/db/tables?db=postgres`)
142
- expect(res.status).toBe(200)
143
- const data = await res.json()
144
- expect(data.tables).toBeDefined()
145
- const names = data.tables.map((t: any) => t.table_name)
146
- expect(names).toContain('users')
147
- expect(names).toContain('posts')
148
- })
149
-
150
- test('lists cvr tables (empty)', async () => {
151
- const res = await fetch(`${base}/api/db/tables?db=cvr`)
152
- expect(res.status).toBe(200)
153
- const data = await res.json()
154
- expect(data.tables).toBeDefined()
155
- expect(data.tables.length).toBe(0)
156
- })
157
-
158
- test('rejects unknown db name', async () => {
159
- const res = await fetch(`${base}/api/db/tables?db=nope`)
160
- expect(res.status).toBe(400)
161
- const data = await res.json()
162
- expect(data.error).toContain('unknown db')
163
- })
164
-
165
- // --- /api/db/table-data ---
166
-
167
- test('browses table data', async () => {
168
- const res = await fetch(`${base}/api/db/table-data?db=postgres&table=users`)
169
- expect(res.status).toBe(200)
170
- const data = await res.json()
171
- expect(data.columns).toBeDefined()
172
- expect(data.columns.length).toBeGreaterThanOrEqual(4)
173
- expect(data.rows.length).toBe(3)
174
- expect(data.total).toBe(3)
175
- // check column metadata
176
- const colNames = data.columns.map((c: any) => c.name)
177
- expect(colNames).toContain('id')
178
- expect(colNames).toContain('name')
179
- expect(colNames).toContain('email')
180
- })
181
-
182
- test('table-data supports search', async () => {
183
- const res = await fetch(
184
- `${base}/api/db/table-data?db=postgres&table=users&search=alice`
185
- )
186
- const data = await res.json()
187
- expect(data.rows.length).toBe(1)
188
- expect(data.rows[0].name).toBe('alice')
189
- expect(data.total).toBe(1)
190
- })
191
-
192
- test('table-data supports pagination', async () => {
193
- const res = await fetch(
194
- `${base}/api/db/table-data?db=postgres&table=users&limit=2&offset=0`
195
- )
196
- const data = await res.json()
197
- expect(data.rows.length).toBe(2)
198
- expect(data.total).toBe(3)
199
-
200
- const page2 = await fetch(
201
- `${base}/api/db/table-data?db=postgres&table=users&limit=2&offset=2`
202
- )
203
- const data2 = await page2.json()
204
- expect(data2.rows.length).toBe(1)
205
- })
206
-
207
- test('table-data with schema-qualified name', async () => {
208
- const res = await fetch(`${base}/api/db/table-data?db=postgres&table=public.posts`)
209
- const data = await res.json()
210
- expect(data.rows.length).toBe(3)
211
- // check NULL values come through
212
- const bobPost = data.rows.find((r: any) => r.title === 'bob writes')
213
- expect(bobPost).toBeDefined()
214
- expect(bobPost.body).toBeNull()
215
- })
216
-
217
- test('table-data missing table param', async () => {
218
- const res = await fetch(`${base}/api/db/table-data?db=postgres`)
219
- expect(res.status).toBe(400)
220
- const data = await res.json()
221
- expect(data.error).toContain('missing table')
222
- })
223
-
224
- // --- /api/db/query ---
225
-
226
- test('runs arbitrary SQL', async () => {
227
- const res = await fetch(`${base}/api/db/query`, {
228
- method: 'POST',
229
- headers: { 'Content-Type': 'application/json' },
230
- body: JSON.stringify({
231
- db: 'postgres',
232
- sql: 'SELECT name, email FROM users ORDER BY name',
233
- }),
234
- })
235
- expect(res.status).toBe(200)
236
- const data = await res.json()
237
- expect(data.fields).toEqual(['name', 'email'])
238
- expect(data.rowCount).toBe(3)
239
- expect(data.rows[0].name).toBe('alice')
240
- expect(data.durationMs).toBeGreaterThanOrEqual(0)
241
- })
242
-
243
- test('returns error for bad SQL', async () => {
244
- const res = await fetch(`${base}/api/db/query`, {
245
- method: 'POST',
246
- headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify({ db: 'postgres', sql: 'SELECT * FROM nonexistent' }),
248
- })
249
- expect(res.status).toBe(400)
250
- const data = await res.json()
251
- expect(data.error).toBeTruthy()
252
- })
253
-
254
- test('query with joins', async () => {
255
- const res = await fetch(`${base}/api/db/query`, {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/json' },
258
- body: JSON.stringify({
259
- db: 'postgres',
260
- sql: `SELECT u.name, p.title FROM users u JOIN posts p ON p.user_id = u.id ORDER BY p.id`,
261
- }),
262
- })
263
- const data = await res.json()
264
- expect(data.rowCount).toBe(3)
265
- expect(data.fields).toEqual(['name', 'title'])
266
- })
267
-
268
- test('query missing sql', async () => {
269
- const res = await fetch(`${base}/api/db/query`, {
270
- method: 'POST',
271
- headers: { 'Content-Type': 'application/json' },
272
- body: JSON.stringify({ db: 'postgres' }),
273
- })
274
- expect(res.status).toBe(400)
275
- const data = await res.json()
276
- expect(data.error).toContain('missing sql')
277
- })
278
-
279
- // --- sqlite ---
280
-
281
- test('sqlite tables returns 404 when no replica', async () => {
282
- const res = await fetch(`${base}/api/sqlite/tables`)
283
- expect(res.status).toBe(404)
284
- const data = await res.json()
285
- expect(data.error).toContain('not found')
286
- })
287
-
288
- test('sqlite endpoints work when replica file exists', async () => {
289
- // create a fake zero-replica.db using bedrock-sqlite
290
- // @ts-expect-error - CJS module
291
- const bedrock: any = await import('bedrock-sqlite')
292
- const Ctor = bedrock.Database || bedrock.default?.Database || bedrock.default
293
- const replicaPath = resolve(DATA_DIR, 'zero-replica.db')
294
- const setupDb = new Ctor(replicaPath)
295
- setupDb.exec(`
296
- CREATE TABLE widgets (
297
- id INTEGER PRIMARY KEY,
298
- label TEXT NOT NULL,
299
- count INTEGER
300
- );
301
- INSERT INTO widgets (label, count) VALUES
302
- ('alpha', 1),
303
- ('beta', 2),
304
- ('gamma', 3);
305
- `)
306
- setupDb.close()
307
-
308
- // list tables
309
- const tablesRes = await fetch(`${base}/api/sqlite/tables`)
310
- expect(tablesRes.status).toBe(200)
311
- const tables = await tablesRes.json()
312
- expect(tables.tables.some((t: any) => t.name === 'widgets')).toBe(true)
313
-
314
- // browse table data
315
- const browseRes = await fetch(`${base}/api/sqlite/table-data?table=widgets`)
316
- expect(browseRes.status).toBe(200)
317
- const browse = await browseRes.json()
318
- expect(browse.rows.length).toBe(3)
319
- expect(browse.total).toBe(3)
320
-
321
- // search
322
- const searchRes = await fetch(
323
- `${base}/api/sqlite/table-data?table=widgets&search=beta`
324
- )
325
- const search = await searchRes.json()
326
- expect(search.rows.length).toBe(1)
327
- expect(search.rows[0].label).toBe('beta')
328
-
329
- // raw query
330
- const queryRes = await fetch(`${base}/api/sqlite/query`, {
331
- method: 'POST',
332
- headers: { 'Content-Type': 'application/json' },
333
- body: JSON.stringify({ sql: 'SELECT count(*) as c FROM widgets' }),
334
- })
335
- expect(queryRes.status).toBe(200)
336
- const q = await queryRes.json()
337
- expect(q.rows[0].c).toBe(3)
338
- expect(q.fields).toContain('c')
339
- })
340
-
341
- // --- CORS ---
342
-
343
- test('OPTIONS returns CORS headers', async () => {
344
- const res = await fetch(`${base}/api/db/tables`, { method: 'OPTIONS' })
345
- expect(res.status).toBe(200)
346
- expect(res.headers.get('access-control-allow-origin')).toBe('*')
347
- })
348
- })
@@ -1,252 +0,0 @@
1
- import { createServer, connect, type Socket, type Server } from 'node:net'
2
-
3
- import type { ZeroLiteConfig } from '../config.js'
4
- import type { LogStore } from './log-store.js'
5
-
6
- export interface HttpLogEntry {
7
- id: number
8
- ts: number
9
- method: string
10
- path: string
11
- status: number
12
- duration: number
13
- reqSize: number
14
- resSize: number
15
- reqHeaders: Record<string, string>
16
- resHeaders: Record<string, string>
17
- }
18
-
19
- export interface HttpLogStore {
20
- push(entry: Omit<HttpLogEntry, 'id'>): void
21
- query(opts?: { since?: number; path?: string }): {
22
- entries: HttpLogEntry[]
23
- cursor: number
24
- }
25
- clear(): void
26
- }
27
-
28
- const MAX_ENTRIES = 10_000
29
- const TRIM_BATCH = Math.floor(MAX_ENTRIES * 0.1)
30
-
31
- export function createHttpLogStore(): HttpLogStore {
32
- const entries: HttpLogEntry[] = []
33
- let nextId = 1
34
-
35
- function push(entry: Omit<HttpLogEntry, 'id'>) {
36
- const full: HttpLogEntry = { ...entry, id: nextId++ }
37
- entries.push(full)
38
- if (entries.length > MAX_ENTRIES + TRIM_BATCH) {
39
- entries.splice(0, entries.length - MAX_ENTRIES)
40
- }
41
- }
42
-
43
- function query(opts?: { since?: number; path?: string }) {
44
- let result: HttpLogEntry[] = entries
45
- if (opts?.since) {
46
- const since = opts.since
47
- let lo = 0
48
- let hi = result.length
49
- while (lo < hi) {
50
- const mid = (lo + hi) >>> 1
51
- if (result[mid].id <= since) lo = mid + 1
52
- else hi = mid
53
- }
54
- result = result.slice(lo)
55
- }
56
- if (opts?.path) {
57
- const p = opts.path
58
- result = result.filter((e) => e.path.includes(p))
59
- }
60
- return {
61
- entries: result,
62
- cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
63
- }
64
- }
65
-
66
- function clear() {
67
- entries.length = 0
68
- }
69
-
70
- return { push, query, clear }
71
- }
72
-
73
- function parseHeaders(raw: string): Record<string, string> {
74
- const out: Record<string, string> = {}
75
- const lines = raw.split('\r\n')
76
- for (let i = 1; i < lines.length; i++) {
77
- if (lines[i] === '') break
78
- const idx = lines[i].indexOf(': ')
79
- if (idx > 0) {
80
- out[lines[i].slice(0, idx).toLowerCase()] = lines[i].slice(idx + 2)
81
- }
82
- }
83
- return out
84
- }
85
-
86
- // public API routes served directly by the proxy (read-only, no auth)
87
- // these are available at the sprite's public URL under /__orez/
88
- const CORS =
89
- 'Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, OPTIONS\r\nAccess-Control-Allow-Headers: *'
90
-
91
- function httpResponse(
92
- status: number,
93
- body: string,
94
- contentType = 'application/json'
95
- ): Buffer {
96
- const headers = `HTTP/1.1 ${status} ${status === 200 ? 'OK' : 'Error'}\r\nContent-Type: ${contentType}\r\nContent-Length: ${Buffer.byteLength(body)}\r\n${CORS}\r\nConnection: close\r\n\r\n`
97
- return Buffer.from(headers + body)
98
- }
99
-
100
- function handleOrezRoute(
101
- path: string,
102
- method: string,
103
- logStore?: LogStore,
104
- config?: ZeroLiteConfig,
105
- startTime?: number
106
- ): Buffer | null {
107
- if (method === 'OPTIONS') {
108
- return httpResponse(200, '')
109
- }
110
-
111
- if (method !== 'GET') {
112
- return httpResponse(405, JSON.stringify({ error: 'method not allowed' }))
113
- }
114
-
115
- const url = new URL(path, 'http://localhost')
116
- const route = url.pathname.replace(/^\/__orez/, '')
117
-
118
- if (route === '/api/logs' && logStore) {
119
- const source = url.searchParams.get('source') || undefined
120
- const level = url.searchParams.get('level') || undefined
121
- const sinceStr = url.searchParams.get('since')
122
- const limitStr = url.searchParams.get('limit')
123
- const since = sinceStr ? Number(sinceStr) : undefined
124
- const limit = limitStr ? Number(limitStr) : undefined
125
- return httpResponse(
126
- 200,
127
- JSON.stringify(logStore.query({ source, level, since, limit }))
128
- )
129
- }
130
-
131
- if (route === '/api/status' && config) {
132
- return httpResponse(
133
- 200,
134
- JSON.stringify({
135
- uptime: Math.floor((Date.now() - (startTime || Date.now())) / 1000),
136
- logLevel: config.logLevel,
137
- sqliteMode: config.disableWasmSqlite ? 'native' : 'wasm',
138
- })
139
- )
140
- }
141
-
142
- return httpResponse(404, JSON.stringify({ error: 'not found' }))
143
- }
144
-
145
- // raw tcp proxy that avoids bun's broken node:http upgrade handling.
146
- // bun silently drops socket.write() data in http server upgrade events,
147
- // so we do everything at the net level instead.
148
- //
149
- // intercepts /__orez/* paths to serve read-only API (logs, status)
150
- // directly without forwarding to zero-cache.
151
- export function startHttpProxy(opts: {
152
- listenPort: number
153
- targetPort: number
154
- httpLog: HttpLogStore
155
- logStore?: LogStore
156
- config?: ZeroLiteConfig
157
- startTime?: number
158
- }): Promise<Server> {
159
- const { listenPort, targetPort, httpLog, logStore, config, startTime } = opts
160
-
161
- const server = createServer((client: Socket) => {
162
- const start = Date.now()
163
-
164
- let logged = false
165
- let reqMethod = ''
166
- let reqPath = ''
167
- let reqHeaders: Record<string, string> = {}
168
-
169
- // intercept first client chunk to extract request info
170
- client.once('data', (chunk: Buffer) => {
171
- const str = chunk.toString('utf8')
172
- const firstLine = str.split('\r\n')[0] || ''
173
- const parts = firstLine.split(' ')
174
- reqMethod = parts[0] || 'GET'
175
- reqPath = parts[1] || '/'
176
- reqHeaders = parseHeaders(str)
177
-
178
- // intercept /__orez/ paths — serve directly, don't forward to zero-cache
179
- // check char 0 first to skip the startsWith on hot-path sync/ws traffic
180
- if (
181
- reqPath.charCodeAt(0) === 47 &&
182
- reqPath.charCodeAt(1) === 95 &&
183
- reqPath.startsWith('/__orez/')
184
- ) {
185
- const response = handleOrezRoute(reqPath, reqMethod, logStore, config, startTime)
186
- if (response) {
187
- client.write(response)
188
- client.end()
189
- httpLog.push({
190
- ts: start,
191
- method: reqMethod,
192
- path: reqPath,
193
- status: 200,
194
- duration: Date.now() - start,
195
- reqSize: chunk.length,
196
- resSize: response.length,
197
- reqHeaders,
198
- resHeaders: {},
199
- })
200
- return
201
- }
202
- }
203
-
204
- // forward to zero-cache
205
- const target = connect(targetPort, '127.0.0.1')
206
-
207
- target.setKeepAlive(true, 30_000)
208
- target.setTimeout(0)
209
- client.setKeepAlive(true, 30_000)
210
- client.setTimeout(0)
211
-
212
- target.write(chunk)
213
- client.pipe(target)
214
-
215
- // intercept first target chunk to extract response info and log
216
- target.once('data', (resChunk: Buffer) => {
217
- const resStr = resChunk.toString('utf8')
218
- const resFirstLine = resStr.split('\r\n')[0] || ''
219
- const status = parseInt(resFirstLine.split(' ')[1]) || 0
220
- const resHeaders = parseHeaders(resStr)
221
-
222
- if (!logged) {
223
- logged = true
224
- httpLog.push({
225
- ts: start,
226
- method: status === 101 ? 'WS' : reqMethod,
227
- path: reqPath,
228
- status,
229
- duration: Date.now() - start,
230
- reqSize: 0,
231
- resSize: resChunk.length,
232
- reqHeaders,
233
- resHeaders,
234
- })
235
- }
236
-
237
- client.write(resChunk)
238
- target.pipe(client)
239
- })
240
-
241
- target.on('error', () => client.destroy())
242
- client.on('error', () => target.destroy())
243
- target.on('close', () => client.destroy())
244
- client.on('close', () => target.destroy())
245
- })
246
- })
247
-
248
- return new Promise((resolve, reject) => {
249
- server.listen(listenPort, '127.0.0.1', () => resolve(server as any))
250
- server.on('error', reject)
251
- })
252
- }