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,428 +0,0 @@
1
- /**
2
- * replication latency stress test.
3
- *
4
- * measures the end-to-end time from a proxy write to the zero-cache
5
- * websocket poke arriving at the client. this is the critical path
6
- * that determines whether UI re-renders overlap with user interactions.
7
- *
8
- * run: vitest run src/integration/replication-latency.test.ts
9
- */
10
-
11
- import postgres from 'postgres'
12
- import { describe, expect, test, beforeAll, afterAll } from 'vitest'
13
- import WebSocket from 'ws'
14
-
15
- import { startZeroLite } from '../index.js'
16
- import { installChangeTracking } from '../replication/change-tracker.js'
17
- import {
18
- ensureTablesInPublications,
19
- installAllowAllPermissions,
20
- } from './test-permissions.js'
21
-
22
- import type { PGlite } from '@electric-sql/pglite'
23
-
24
- const SYNC_PROTOCOL_VERSION = 49
25
-
26
- function encodeSecProtocols(
27
- initConnectionMessage: unknown,
28
- authToken: string | undefined
29
- ): string {
30
- const payload = JSON.stringify({ initConnectionMessage, authToken })
31
- return encodeURIComponent(Buffer.from(payload, 'utf-8').toString('base64'))
32
- }
33
-
34
- class Queue<T> {
35
- private items: T[] = []
36
- private waiters: Array<{
37
- resolve: (v: T) => void
38
- timer?: ReturnType<typeof setTimeout>
39
- }> = []
40
-
41
- enqueue(item: T) {
42
- const waiter = this.waiters.shift()
43
- if (waiter) {
44
- if (waiter.timer) clearTimeout(waiter.timer)
45
- waiter.resolve(item)
46
- } else {
47
- this.items.push(item)
48
- }
49
- }
50
-
51
- dequeue(fallback?: T, timeoutMs = 10000): Promise<T> {
52
- if (this.items.length > 0) {
53
- return Promise.resolve(this.items.shift()!)
54
- }
55
- return new Promise<T>((resolve) => {
56
- const waiter: { resolve: (v: T) => void; timer?: ReturnType<typeof setTimeout> } = {
57
- resolve,
58
- }
59
- if (fallback !== undefined) {
60
- waiter.timer = setTimeout(() => {
61
- const idx = this.waiters.indexOf(waiter)
62
- if (idx >= 0) this.waiters.splice(idx, 1)
63
- resolve(fallback)
64
- }, timeoutMs)
65
- }
66
- this.waiters.push(waiter)
67
- })
68
- }
69
- }
70
-
71
- describe('replication latency', { timeout: 120000 }, () => {
72
- let db: PGlite
73
- let zeroPort: number
74
- let pgPort: number
75
- let shutdown: () => Promise<void>
76
- let resetZeroFull: (() => Promise<void>) | undefined
77
- let dataDir: string
78
- let sql: ReturnType<typeof postgres>
79
-
80
- beforeAll(async () => {
81
- const testPgPort = 24000 + Math.floor(Math.random() * 1000)
82
- const testZeroPort = testPgPort + 100
83
-
84
- dataDir = `.orez-latency-test-${Date.now()}`
85
- const result = await startZeroLite({
86
- pgPort: testPgPort,
87
- zeroPort: testZeroPort,
88
- dataDir,
89
- logLevel: 'info',
90
- skipZeroCache: false,
91
- })
92
-
93
- db = result.db
94
- zeroPort = result.zeroPort
95
- pgPort = result.pgPort
96
- shutdown = result.stop
97
- resetZeroFull = result.resetZeroFull
98
-
99
- // create test table
100
- await db.exec(`
101
- CREATE TABLE IF NOT EXISTS latency_test (
102
- id TEXT PRIMARY KEY,
103
- value TEXT,
104
- ts BIGINT
105
- );
106
- `)
107
- await ensureTablesInPublications(db, ['latency_test'])
108
- const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
109
- if (pubName) {
110
- const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
111
- await db
112
- .exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public"."latency_test"`)
113
- .catch(() => {})
114
- await installChangeTracking(db)
115
- }
116
- await installAllowAllPermissions(db, ['latency_test'])
117
- if (resetZeroFull) await resetZeroFull()
118
-
119
- // wait for zero-cache ready
120
- await waitForZero(zeroPort, 90000)
121
-
122
- // connect via wire protocol (like a real app would)
123
- sql = postgres(`postgresql://user:password@127.0.0.1:${pgPort}/postgres`, {
124
- max: 1,
125
- idle_timeout: 0,
126
- })
127
- }, 120000)
128
-
129
- afterAll(async () => {
130
- if (sql) await sql.end()
131
- if (shutdown) await shutdown()
132
- if (dataDir) {
133
- const { rmSync } = await import('node:fs')
134
- try {
135
- rmSync(dataDir, { recursive: true, force: true })
136
- } catch {}
137
- }
138
- })
139
-
140
- test('measure write-to-poke latency (single inserts)', async () => {
141
- const downstream = new Queue<unknown>()
142
- const ws = connectAndSubscribe(zeroPort, downstream)
143
- await drainInitialPokes(downstream)
144
-
145
- const NUM_WRITES = 20
146
- const latencies: number[] = []
147
-
148
- for (let i = 0; i < NUM_WRITES; i++) {
149
- const id = `latency-${i}-${Date.now()}`
150
- const writeStart = performance.now()
151
-
152
- // write through the wire protocol proxy (like a real app)
153
- await sql`INSERT INTO latency_test (id, value, ts) VALUES (${id}, ${'test'}, ${Date.now()})`
154
-
155
- // wait for the poke containing our row
156
- const poke = await waitForPokeWithRow(downstream, 'latency_test', id, 10000)
157
- const latencyMs = performance.now() - writeStart
158
-
159
- expect(poke).toBeTruthy()
160
- latencies.push(latencyMs)
161
- }
162
-
163
- ws.close()
164
-
165
- // report
166
- latencies.sort((a, b) => a - b)
167
- const avg = latencies.reduce((s, v) => s + v, 0) / latencies.length
168
- const p50 = latencies[Math.floor(latencies.length * 0.5)]
169
- const p95 = latencies[Math.floor(latencies.length * 0.95)]
170
- const p99 = latencies[Math.floor(latencies.length * 0.99)]
171
- const max = latencies[latencies.length - 1]
172
-
173
- console.log(`\n[replication latency] ${NUM_WRITES} single inserts via wire protocol:`)
174
- console.log(
175
- ` avg=${avg.toFixed(1)}ms p50=${p50.toFixed(1)}ms p95=${p95.toFixed(1)}ms p99=${p99.toFixed(1)}ms max=${max.toFixed(1)}ms`
176
- )
177
- console.log(` all: ${latencies.map((l) => l.toFixed(0)).join(', ')}ms`)
178
-
179
- // assert reasonable latency — under 200ms avg means the UI re-render
180
- // arrives before a user can interact with the element
181
- expect(avg).toBeLessThan(200)
182
- // no single write should take more than 500ms
183
- expect(max).toBeLessThan(500)
184
- })
185
-
186
- test('count poke batches per single write', async () => {
187
- // theory: orez causes 2+ poke batches per write because zero-cache
188
- // writes shard updates back through the proxy, creating a separate
189
- // replication batch. real postgres doesn't have this round-trip.
190
- const downstream = new Queue<unknown>()
191
- const ws = connectAndSubscribe(zeroPort, downstream)
192
- await drainInitialPokes(downstream)
193
-
194
- const id = `poke-count-${Date.now()}`
195
- await sql`INSERT INTO latency_test (id, value, ts) VALUES (${id}, ${'count-test'}, ${Date.now()})`
196
-
197
- // collect ALL messages for 2 seconds after the write
198
- const messages: any[] = []
199
- const deadline = Date.now() + 2000
200
- while (Date.now() < deadline) {
201
- const remaining = Math.max(100, deadline - Date.now())
202
- const msg = (await downstream.dequeue('timeout' as any, remaining)) as any
203
- if (msg !== 'timeout') messages.push(msg)
204
- }
205
-
206
- const pokeStarts = messages.filter((m) => Array.isArray(m) && m[0] === 'pokeStart')
207
- const pokeEnds = messages.filter((m) => Array.isArray(m) && m[0] === 'pokeEnd')
208
- const pokeParts = messages.filter((m) => Array.isArray(m) && m[0] === 'pokePart')
209
-
210
- console.log(`\n[poke batches] after 1 INSERT:`)
211
- console.log(
212
- ` pokeStart=${pokeStarts.length} pokePart=${pokeParts.length} pokeEnd=${pokeEnds.length}`
213
- )
214
- console.log(` total messages: ${messages.length}`)
215
- for (const msg of messages) {
216
- if (Array.isArray(msg)) {
217
- const type = msg[0]
218
- if (type === 'pokePart' && msg[1]?.rowsPatch) {
219
- const tables = msg[1].rowsPatch
220
- .map((r: any) => `${r.op}:${r.tableName}`)
221
- .join(', ')
222
- console.log(` pokePart: ${tables}`)
223
- } else {
224
- console.log(` ${type}`)
225
- }
226
- }
227
- }
228
-
229
- // ideally just 1 poke cycle per write, but we want to measure reality
230
- expect(pokeStarts.length).toBeGreaterThanOrEqual(1)
231
-
232
- ws.close()
233
- })
234
-
235
- test('count poke batches when shard tables update', async () => {
236
- // simulate what happens in the real app: zero-cache writes to shard
237
- // tables (clients.lastMutationID) after processing a mutation.
238
- // these shard writes go through the proxy and trigger replication.
239
- const downstream = new Queue<unknown>()
240
- const ws = connectAndSubscribe(zeroPort, downstream)
241
- await drainInitialPokes(downstream)
242
-
243
- const id = `shard-test-${Date.now()}`
244
- // insert via proxy (triggers replication)
245
- await sql`INSERT INTO latency_test (id, value, ts) VALUES (${id}, ${'shard'}, ${Date.now()})`
246
-
247
- // now simulate a shard write (like zero-cache updating clients table)
248
- // check if any shard schemas exist
249
- const shardSchemas = await sql`
250
- SELECT nspname FROM pg_namespace
251
- WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public', '_orez')
252
- AND nspname NOT LIKE 'pg_%'
253
- AND nspname NOT LIKE 'zero_%'
254
- AND nspname NOT LIKE '_zero_%'
255
- AND nspname NOT LIKE '%/%'
256
- `
257
-
258
- // collect messages for 3 seconds
259
- const messages: any[] = []
260
- const deadline = Date.now() + 3000
261
- while (Date.now() < deadline) {
262
- const remaining = Math.max(100, deadline - Date.now())
263
- const msg = (await downstream.dequeue('timeout' as any, remaining)) as any
264
- if (msg !== 'timeout') messages.push(msg)
265
- }
266
-
267
- const pokeStarts = messages.filter((m) => Array.isArray(m) && m[0] === 'pokeStart')
268
- const pokeParts = messages.filter((m) => Array.isArray(m) && m[0] === 'pokePart')
269
-
270
- console.log(
271
- `\n[shard poke batches] after INSERT + shard schemas=${shardSchemas.length}:`
272
- )
273
- console.log(` pokeStart=${pokeStarts.length} pokePart=${pokeParts.length}`)
274
- for (const msg of messages) {
275
- if (Array.isArray(msg) && msg[0] === 'pokePart' && msg[1]?.rowsPatch) {
276
- const tables = msg[1].rowsPatch
277
- .map((r: any) => `${r.op}:${r.tableName}`)
278
- .join(', ')
279
- console.log(` pokePart: ${tables}`)
280
- }
281
- }
282
-
283
- expect(pokeStarts.length).toBeGreaterThanOrEqual(1)
284
- ws.close()
285
- })
286
-
287
- test('measure rapid sequential write latency', async () => {
288
- const downstream = new Queue<unknown>()
289
- const ws = connectAndSubscribe(zeroPort, downstream)
290
- await drainInitialPokes(downstream)
291
-
292
- // simulate rapid sequential writes (like a chat app sending messages)
293
- const NUM_WRITES = 10
294
- const ids: string[] = []
295
- const writeStart = performance.now()
296
-
297
- for (let i = 0; i < NUM_WRITES; i++) {
298
- const id = `rapid-${i}-${Date.now()}`
299
- ids.push(id)
300
- await sql`INSERT INTO latency_test (id, value, ts) VALUES (${id}, ${'rapid'}, ${Date.now()})`
301
- }
302
-
303
- const writeEnd = performance.now()
304
-
305
- // wait for ALL rows to arrive
306
- const receivedIds = new Set<string>()
307
- const deadline = Date.now() + 30000
308
- while (receivedIds.size < NUM_WRITES && Date.now() < deadline) {
309
- const msg = (await downstream.dequeue('timeout' as any, 5000)) as any
310
- if (msg === 'timeout') continue
311
- if (Array.isArray(msg) && msg[0] === 'pokePart' && msg[1]?.rowsPatch) {
312
- for (const row of msg[1].rowsPatch) {
313
- if (row.op === 'put' && row.tableName === 'latency_test' && row.value?.id) {
314
- receivedIds.add(row.value.id)
315
- }
316
- }
317
- }
318
- }
319
-
320
- const totalMs = performance.now() - writeStart
321
- const writeMs = writeEnd - writeStart
322
- const replicationMs = totalMs - writeMs
323
-
324
- console.log(`\n[replication latency] ${NUM_WRITES} rapid sequential inserts:`)
325
- console.log(
326
- ` write=${writeMs.toFixed(1)}ms replication=${replicationMs.toFixed(1)}ms total=${totalMs.toFixed(1)}ms`
327
- )
328
- console.log(` received ${receivedIds.size}/${NUM_WRITES} rows`)
329
-
330
- expect(receivedIds.size).toBe(NUM_WRITES)
331
- for (const id of ids) {
332
- expect(receivedIds.has(id)).toBe(true)
333
- }
334
- // all 10 writes + replication should complete in under 3s
335
- expect(totalMs).toBeLessThan(3000)
336
- })
337
-
338
- // --- helpers ---
339
-
340
- function connectAndSubscribe(port: number, downstream: Queue<unknown>): WebSocket {
341
- const cg = `latency-cg-${Date.now()}`
342
- const cid = `latency-client-${Date.now()}`
343
- const secProtocol = encodeSecProtocols(
344
- [
345
- 'initConnection',
346
- {
347
- desiredQueriesPatch: [
348
- {
349
- op: 'put',
350
- hash: 'q1',
351
- ast: {
352
- table: 'latency_test',
353
- orderBy: [['id', 'asc']],
354
- },
355
- },
356
- ],
357
- clientSchema: {
358
- tables: {
359
- latency_test: {
360
- columns: {
361
- id: { type: 'string' },
362
- value: { type: 'string' },
363
- ts: { type: 'number' },
364
- },
365
- primaryKey: ['id'],
366
- },
367
- },
368
- },
369
- },
370
- ],
371
- undefined
372
- )
373
- const ws = new WebSocket(
374
- `ws://localhost:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
375
- `?clientGroupID=${cg}&clientID=${cid}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
376
- secProtocol
377
- )
378
- ws.on('message', (data) => downstream.enqueue(JSON.parse(data.toString())))
379
- return ws
380
- }
381
-
382
- async function drainInitialPokes(downstream: Queue<unknown>) {
383
- let settled = false
384
- const timeout = Date.now() + 30000
385
- while (!settled && Date.now() < timeout) {
386
- const msg = (await downstream.dequeue('timeout' as any, 3000)) as any
387
- if (msg === 'timeout') {
388
- settled = true
389
- } else if (Array.isArray(msg) && msg[0] === 'pokeEnd') {
390
- const next = (await downstream.dequeue('timeout' as any, 2000)) as any
391
- if (next === 'timeout') settled = true
392
- }
393
- }
394
- }
395
-
396
- async function waitForPokeWithRow(
397
- downstream: Queue<unknown>,
398
- tableName: string,
399
- rowId: string,
400
- timeoutMs = 10000
401
- ): Promise<Record<string, any> | null> {
402
- const deadline = Date.now() + timeoutMs
403
- while (Date.now() < deadline) {
404
- const remaining = Math.max(500, deadline - Date.now())
405
- const msg = (await downstream.dequeue('timeout' as any, remaining)) as any
406
- if (msg === 'timeout') return null
407
- if (Array.isArray(msg) && msg[0] === 'pokePart' && msg[1]?.rowsPatch) {
408
- const match = msg[1].rowsPatch.find(
409
- (r: any) => r.op === 'put' && r.tableName === tableName && r.value?.id === rowId
410
- )
411
- if (match) return match
412
- }
413
- }
414
- return null
415
- }
416
- })
417
-
418
- async function waitForZero(port: number, timeoutMs = 60000): Promise<void> {
419
- const deadline = Date.now() + timeoutMs
420
- while (Date.now() < deadline) {
421
- try {
422
- const res = await fetch(`http://localhost:${port}/`)
423
- if (res.ok) return
424
- } catch {}
425
- await new Promise((r) => setTimeout(r, 500))
426
- }
427
- throw new Error(`zero-cache did not become ready within ${timeoutMs}ms`)
428
- }