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,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
- }
@@ -1,192 +0,0 @@
1
- import { mkdirSync, appendFile, stat, rename, unlink, readdirSync } from 'node:fs'
2
- import { join } from 'node:path'
3
-
4
- export interface LogEntry {
5
- id: number
6
- ts: number
7
- source: string
8
- level: string
9
- msg: string
10
- }
11
-
12
- export interface LogStore {
13
- push(source: string, level: string, msg: string): void
14
- query(opts?: { source?: string; level?: string; since?: number; limit?: number }): {
15
- entries: LogEntry[]
16
- cursor: number
17
- }
18
- getAll(): LogEntry[]
19
- clear(): void
20
- }
21
-
22
- const ANSI_RE = /\x1b\[[0-9;]*m/g
23
- const MAX_ENTRIES = 20_000
24
- // trim in batches of 10% to avoid O(n) splice on every single push
25
- const TRIM_BATCH = Math.floor(MAX_ENTRIES * 0.1)
26
- const MAX_FILE_SIZE = 2 * 1024 * 1024
27
- const MAX_QUERY_LIMIT = 5000
28
- const LEVEL_PRIORITY: Record<string, number> = { error: 0, warn: 1, info: 2, debug: 3 }
29
- const VALID_SOURCES = new Set(['orez', 'zero', 'pglite', 'proxy', 's3'])
30
- const VALID_LEVELS = new Set(['error', 'warn', 'info', 'debug'])
31
-
32
- export function createLogStore(
33
- dataDir: string,
34
- writeToDisk = true,
35
- maxFileSize = MAX_FILE_SIZE
36
- ): LogStore {
37
- const entries: LogEntry[] = []
38
- let nextId = 1
39
-
40
- const logsDir = join(dataDir, 'logs')
41
-
42
- if (writeToDisk) {
43
- mkdirSync(logsDir, { recursive: true })
44
- // clean up old rotated log files on startup
45
- try {
46
- for (const f of readdirSync(logsDir)) {
47
- if (/\.log\.\d+$/.test(f)) {
48
- unlink(join(logsDir, f), () => {})
49
- }
50
- }
51
- } catch {}
52
- }
53
-
54
- // track file sizes and rotation state per-source
55
- const fileSizes: Record<string, number> = {}
56
- const rotating: Record<string, boolean> = {}
57
-
58
- // buffered async disk writes — avoids appendFileSync blocking the event loop
59
- const writeBuffers: Record<string, string[]> = {}
60
- const MAX_BUFFER_SIZE = 10_000
61
- const FLUSH_INTERVAL_MS = 3000
62
-
63
- function getLogFile(source: string): string {
64
- return join(logsDir, `${source}.log`)
65
- }
66
-
67
- function rotateIfNeeded(source: string) {
68
- if (!writeToDisk || rotating[source]) return
69
- rotating[source] = true
70
- const logFile = getLogFile(source)
71
- stat(logFile, (err, stats) => {
72
- if (err) {
73
- rotating[source] = false
74
- return
75
- }
76
- fileSizes[source] = stats.size
77
- if (stats.size > maxFileSize) {
78
- // delete old backup first, then rename current
79
- unlink(logFile + '.1', () => {
80
- rename(logFile, logFile + '.1', () => {
81
- fileSizes[source] = 0
82
- rotating[source] = false
83
- })
84
- })
85
- } else {
86
- rotating[source] = false
87
- }
88
- })
89
- }
90
-
91
- function flushBuffers() {
92
- for (const source in writeBuffers) {
93
- const buf = writeBuffers[source]
94
- if (buf.length === 0) continue
95
- const data = buf.join('')
96
- buf.length = 0
97
- const logFile = getLogFile(source)
98
- appendFile(logFile, data, (err) => {
99
- if (err) return
100
- fileSizes[source] = (fileSizes[source] || 0) + data.length
101
- if (fileSizes[source] > maxFileSize) {
102
- rotateIfNeeded(source)
103
- }
104
- })
105
- }
106
- }
107
-
108
- if (writeToDisk) {
109
- const timer = setInterval(flushBuffers, FLUSH_INTERVAL_MS)
110
- if (timer.unref) timer.unref()
111
- }
112
-
113
- function push(source: string, level: string, msg: string) {
114
- const entry: LogEntry = {
115
- id: nextId++,
116
- ts: Date.now(),
117
- source,
118
- level,
119
- msg: msg.replace(ANSI_RE, ''),
120
- }
121
- entries.push(entry)
122
- // trim in batches to amortize the O(n) splice cost — instead of shifting
123
- // 50k elements on every push, we shift once every ~5k pushes
124
- if (entries.length > MAX_ENTRIES + TRIM_BATCH) {
125
- entries.splice(0, entries.length - MAX_ENTRIES)
126
- }
127
- if (writeToDisk) {
128
- const ts = new Date(entry.ts).toISOString()
129
- const line = `[${ts}] [${level}] ${entry.msg}\n`
130
- if (!writeBuffers[source]) writeBuffers[source] = []
131
- const buf = writeBuffers[source]
132
- buf.push(line)
133
- // cap buffer size to prevent unbounded growth if flushBuffers is delayed
134
- if (buf.length > MAX_BUFFER_SIZE) {
135
- buf.splice(0, buf.length - MAX_BUFFER_SIZE)
136
- }
137
- }
138
- }
139
-
140
- function query(opts?: {
141
- source?: string
142
- level?: string
143
- since?: number
144
- limit?: number
145
- }) {
146
- let result = entries
147
- // clamp limit to prevent oversized responses
148
- const limit = Math.min(Math.max(opts?.limit ?? 1000, 1), MAX_QUERY_LIMIT)
149
-
150
- if (opts?.since) {
151
- const since = opts.since
152
- let lo = 0
153
- let hi = result.length
154
- while (lo < hi) {
155
- const mid = (lo + hi) >>> 1
156
- if (result[mid].id <= since) lo = mid + 1
157
- else hi = mid
158
- }
159
- result = result.slice(lo)
160
- }
161
-
162
- if (opts?.source && VALID_SOURCES.has(opts.source)) {
163
- const source = opts.source
164
- result = result.filter((e) => e.source === source)
165
- }
166
-
167
- if (opts?.level && VALID_LEVELS.has(opts.level)) {
168
- const maxPriority = LEVEL_PRIORITY[opts.level] ?? 3
169
- result = result.filter((e) => (LEVEL_PRIORITY[e.level] ?? 3) <= maxPriority)
170
- }
171
-
172
- // limit results to prevent UI slowdown
173
- if (result.length > limit) {
174
- result = result.slice(-limit)
175
- }
176
-
177
- return {
178
- entries: result,
179
- cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
180
- }
181
- }
182
-
183
- function getAll() {
184
- return [...entries]
185
- }
186
-
187
- function clear() {
188
- entries.length = 0
189
- }
190
-
191
- return { push, query, getAll, clear }
192
- }