orez 0.2.27 → 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 (150) hide show
  1. package/package.json +3 -4
  2. package/src/admin/admin-data.test.ts +0 -348
  3. package/src/admin/http-proxy.ts +0 -252
  4. package/src/admin/log-store.ts +0 -192
  5. package/src/admin/server.ts +0 -471
  6. package/src/admin/ui.ts +0 -1322
  7. package/src/bench/proxy-throughput.bench.ts +0 -343
  8. package/src/bench/serial-mutations.bench.ts +0 -270
  9. package/src/browser.ts +0 -203
  10. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  11. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  12. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  13. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  14. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  15. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  16. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  17. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  18. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  19. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  20. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  21. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  22. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  23. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  24. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  25. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  26. package/src/cf-do/ARCHITECTURE.md +0 -93
  27. package/src/cf-do/CHAT_E2E.md +0 -213
  28. package/src/cf-do/watermark.test.ts +0 -103
  29. package/src/cf-do/watermark.ts +0 -118
  30. package/src/cf-do/worker.ts +0 -1041
  31. package/src/cf-do/wrangler.toml +0 -11
  32. package/src/cf-pglite/README.md +0 -19
  33. package/src/change-tracking.ts +0 -25
  34. package/src/child-process.test.ts +0 -147
  35. package/src/child-process.ts +0 -90
  36. package/src/cli-entry.ts +0 -72
  37. package/src/cli.test.ts +0 -40
  38. package/src/cli.ts +0 -1214
  39. package/src/config.ts +0 -150
  40. package/src/do-sql-tracking.test.ts +0 -19
  41. package/src/do-sql-tracking.ts +0 -19
  42. package/src/index.ts +0 -1215
  43. package/src/integration/integration.test.ts +0 -517
  44. package/src/integration/native-binary.guard.test.ts +0 -13
  45. package/src/integration/native-startup.test.ts +0 -44
  46. package/src/integration/replication-latency.test.ts +0 -428
  47. package/src/integration/restore-live-stress.test.ts +0 -433
  48. package/src/integration/restore-reset.test.ts +0 -400
  49. package/src/integration/restore.test.ts +0 -274
  50. package/src/integration/test-permissions.ts +0 -147
  51. package/src/load-config.ts +0 -46
  52. package/src/log.ts +0 -96
  53. package/src/mutex.ts +0 -47
  54. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  55. package/src/pg-proxy-browser.ts +0 -2022
  56. package/src/pg-proxy-do-backend.test.ts +0 -3890
  57. package/src/pg-proxy-do-backend.ts +0 -7191
  58. package/src/pg-proxy.ts +0 -1087
  59. package/src/pg-sqlite-compiler/README.md +0 -53
  60. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  61. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  62. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  63. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  64. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  65. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  66. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  67. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  71. package/src/pg-sqlite-compiler/index.ts +0 -73
  72. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  73. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  74. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  75. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  76. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  77. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  78. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  79. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  80. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  81. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  82. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  83. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  84. package/src/pg-sqlite-compiler/types.ts +0 -63
  85. package/src/pglite-ipc.test.ts +0 -116
  86. package/src/pglite-ipc.ts +0 -266
  87. package/src/pglite-manager.ts +0 -557
  88. package/src/pglite-web-proxy.test.ts +0 -57
  89. package/src/pglite-web-proxy.ts +0 -221
  90. package/src/pglite-web-worker.ts +0 -152
  91. package/src/pglite-worker-thread.ts +0 -253
  92. package/src/port.ts +0 -25
  93. package/src/process-title.ts +0 -9
  94. package/src/recovery.ts +0 -155
  95. package/src/replication/change-tracker.test.ts +0 -357
  96. package/src/replication/change-tracker.ts +0 -279
  97. package/src/replication/handler.test.ts +0 -511
  98. package/src/replication/handler.ts +0 -1190
  99. package/src/replication/pgoutput-encoder.test.ts +0 -697
  100. package/src/replication/pgoutput-encoder.ts +0 -373
  101. package/src/replication/tcp-replication.test.ts +0 -876
  102. package/src/replication/zero-compat.test.ts +0 -1150
  103. package/src/restore-stress.test.ts +0 -188
  104. package/src/s3-local.ts +0 -203
  105. package/src/shim/hooks.mjs +0 -120
  106. package/src/shim/register.mjs +0 -4
  107. package/src/sqlite-mode/apply-mode.ts +0 -224
  108. package/src/sqlite-mode/index.ts +0 -15
  109. package/src/sqlite-mode/native-binary.ts +0 -89
  110. package/src/sqlite-mode/package-resolve.ts +0 -17
  111. package/src/sqlite-mode/resolve-mode.ts +0 -80
  112. package/src/sqlite-mode/shim-template.ts +0 -159
  113. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  114. package/src/sqlite-mode/types.ts +0 -30
  115. package/src/vite-plugin.ts +0 -67
  116. package/src/wasm-sqlite.test.ts +0 -537
  117. package/src/worker/browser-admin.ts +0 -52
  118. package/src/worker/browser-build-config.test.ts +0 -71
  119. package/src/worker/browser-build-config.ts +0 -109
  120. package/src/worker/browser-embed-admin.test.ts +0 -75
  121. package/src/worker/browser-embed.ts +0 -345
  122. package/src/worker/cf-patches.ts +0 -384
  123. package/src/worker/embed-integration.test.ts +0 -321
  124. package/src/worker/index.ts +0 -138
  125. package/src/worker/shims/fastify.test.ts +0 -255
  126. package/src/worker/shims/fastify.ts +0 -306
  127. package/src/worker/shims/http-service.test.ts +0 -355
  128. package/src/worker/shims/http-service.ts +0 -293
  129. package/src/worker/shims/node-stub.ts +0 -290
  130. package/src/worker/shims/oxfmt.ts +0 -3
  131. package/src/worker/shims/postgres-browser.ts +0 -59
  132. package/src/worker/shims/postgres-socket.test.ts +0 -576
  133. package/src/worker/shims/postgres-socket.ts +0 -310
  134. package/src/worker/shims/postgres.test.ts +0 -364
  135. package/src/worker/shims/postgres.ts +0 -1454
  136. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  137. package/src/worker/shims/sqlite-browser.ts +0 -175
  138. package/src/worker/shims/sqlite.test.ts +0 -786
  139. package/src/worker/shims/sqlite.ts +0 -978
  140. package/src/worker/shims/stream-browser.ts +0 -15
  141. package/src/worker/shims/ws-browser.test.ts +0 -205
  142. package/src/worker/shims/ws-browser.ts +0 -248
  143. package/src/worker/shims/ws.test.ts +0 -288
  144. package/src/worker/shims/ws.ts +0 -467
  145. package/src/worker/shims/zero-process-env.ts +0 -11
  146. package/src/worker/types.ts +0 -75
  147. package/src/worker/worker-integration.test.ts +0 -223
  148. package/src/worker/worker.test.ts +0 -136
  149. package/src/worker/zero-cache-embed-cf.ts +0 -463
  150. 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
- }