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,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
- }