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.
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +9 -1
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +2 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +49 -7
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
- package/dist/pg-sqlite-compiler/index.d.ts +12 -0
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/index.js +59 -0
- package/dist/pg-sqlite-compiler/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.js +39 -0
- package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.js +103 -0
- package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
- package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
- package/dist/pg-sqlite-compiler/types.d.ts +55 -0
- package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/types.js +2 -0
- package/dist/pg-sqlite-compiler/types.js.map +1 -0
- package/package.json +8 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/ARCHITECTURE.md +0 -83
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1033
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -38
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7157
- package/src/pg-proxy.ts +0 -1087
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- 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
|
-
}
|