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,876 +0,0 @@
1
- /**
2
- * tcp-level integration test for the full replication stack.
3
- *
4
- * connects to pg-proxy over tcp, speaks the pg wire protocol,
5
- * runs the replication handshake, and verifies streamed changes
6
- * match what a real pg consumer expects.
7
- *
8
- * this catches integration bugs (socket handling, framing, auth,
9
- * query routing) that unit tests on individual components miss.
10
- */
11
-
12
- import { createConnection, type Socket } from 'node:net'
13
-
14
- import { PGlite } from '@electric-sql/pglite'
15
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
16
-
17
- import { getConfig } from '../config'
18
- import { startPgProxy } from '../pg-proxy'
19
- import { installChangeTracking } from './change-tracker'
20
- import { signalReplicationChange, resetReplicationState } from './handler'
21
-
22
- import type { Server, AddressInfo } from 'node:net'
23
-
24
- // --- pgoutput decoder (validates against pg protocol spec) ---
25
-
26
- interface DecodedMessage {
27
- type: string
28
- raw: Uint8Array
29
- }
30
-
31
- interface BeginMessage extends DecodedMessage {
32
- type: 'Begin'
33
- lsn: bigint
34
- timestamp: bigint
35
- xid: number
36
- }
37
-
38
- interface CommitMessage extends DecodedMessage {
39
- type: 'Commit'
40
- flags: number
41
- lsn: bigint
42
- endLsn: bigint
43
- timestamp: bigint
44
- }
45
-
46
- interface RelationColumn {
47
- flags: number
48
- name: string
49
- typeOid: number
50
- typeMod: number
51
- }
52
-
53
- interface RelationMessage extends DecodedMessage {
54
- type: 'Relation'
55
- tableOid: number
56
- schema: string
57
- tableName: string
58
- replicaIdentity: number
59
- columns: RelationColumn[]
60
- }
61
-
62
- interface InsertMessage extends DecodedMessage {
63
- type: 'Insert'
64
- tableOid: number
65
- tupleData: TupleData
66
- }
67
-
68
- interface UpdateMessage extends DecodedMessage {
69
- type: 'Update'
70
- tableOid: number
71
- oldTupleData?: TupleData
72
- newTupleData: TupleData
73
- }
74
-
75
- interface DeleteMessage extends DecodedMessage {
76
- type: 'Delete'
77
- tableOid: number
78
- keyTupleData: TupleData
79
- }
80
-
81
- interface TupleData {
82
- columns: Array<{ type: 'null' | 'text'; value: string | null }>
83
- }
84
-
85
- interface KeepaliveMessage extends DecodedMessage {
86
- type: 'Keepalive'
87
- walEnd: bigint
88
- timestamp: bigint
89
- replyRequested: boolean
90
- }
91
-
92
- type PgOutputMessage =
93
- | BeginMessage
94
- | CommitMessage
95
- | RelationMessage
96
- | InsertMessage
97
- | UpdateMessage
98
- | DeleteMessage
99
- | KeepaliveMessage
100
-
101
- function r16(buf: Uint8Array, off: number) {
102
- return new DataView(buf.buffer, buf.byteOffset).getInt16(off)
103
- }
104
- function r32(buf: Uint8Array, off: number) {
105
- return new DataView(buf.buffer, buf.byteOffset).getInt32(off)
106
- }
107
- function r64(buf: Uint8Array, off: number) {
108
- return new DataView(buf.buffer, buf.byteOffset).getBigInt64(off)
109
- }
110
- function rCStr(buf: Uint8Array, off: number): [string, number] {
111
- let end = off
112
- while (end < buf.length && buf[end] !== 0) end++
113
- return [new TextDecoder().decode(buf.subarray(off, end)), end + 1]
114
- }
115
-
116
- function decodeTupleData(buf: Uint8Array, off: number): [TupleData, number] {
117
- const numCols = r16(buf, off)
118
- off += 2
119
- const columns: TupleData['columns'] = []
120
- for (let i = 0; i < numCols; i++) {
121
- const colType = buf[off++]
122
- if (colType === 0x6e) {
123
- // 'n' null
124
- columns.push({ type: 'null', value: null })
125
- } else if (colType === 0x74) {
126
- // 't' text
127
- const len = r32(buf, off)
128
- off += 4
129
- const value = new TextDecoder().decode(buf.subarray(off, off + len))
130
- off += len
131
- columns.push({ type: 'text', value })
132
- } else {
133
- throw new Error(`unknown tuple column type: 0x${colType.toString(16)}`)
134
- }
135
- }
136
- return [{ columns }, off]
137
- }
138
-
139
- function decodePgOutput(data: Uint8Array): PgOutputMessage {
140
- const msgType = data[0]
141
-
142
- switch (msgType) {
143
- case 0x42: {
144
- // Begin
145
- return {
146
- type: 'Begin',
147
- raw: data,
148
- lsn: r64(data, 1),
149
- timestamp: r64(data, 9),
150
- xid: r32(data, 17),
151
- }
152
- }
153
- case 0x43: {
154
- // Commit
155
- return {
156
- type: 'Commit',
157
- raw: data,
158
- flags: data[1],
159
- lsn: r64(data, 2),
160
- endLsn: r64(data, 10),
161
- timestamp: r64(data, 18),
162
- }
163
- }
164
- case 0x52: {
165
- // Relation
166
- const tableOid = r32(data, 1)
167
- let pos = 5
168
- const [schema, p1] = rCStr(data, pos)
169
- pos = p1
170
- const [tableName, p2] = rCStr(data, pos)
171
- pos = p2
172
- const replicaIdentity = data[pos++]
173
- const numCols = r16(data, pos)
174
- pos += 2
175
- const columns: RelationColumn[] = []
176
- for (let i = 0; i < numCols; i++) {
177
- const flags = data[pos++]
178
- const [name, np] = rCStr(data, pos)
179
- pos = np
180
- const typeOid = r32(data, pos)
181
- pos += 4
182
- const typeMod = r32(data, pos)
183
- pos += 4
184
- columns.push({ flags, name, typeOid, typeMod })
185
- }
186
- return {
187
- type: 'Relation',
188
- raw: data,
189
- tableOid,
190
- schema,
191
- tableName,
192
- replicaIdentity,
193
- columns,
194
- }
195
- }
196
- case 0x49: {
197
- // Insert
198
- const tableOid = r32(data, 1)
199
- const marker = data[5] // should be 'N'
200
- if (marker !== 0x4e)
201
- throw new Error(`insert: expected 'N' marker, got 0x${marker.toString(16)}`)
202
- const [tupleData] = decodeTupleData(data, 6)
203
- return { type: 'Insert', raw: data, tableOid, tupleData }
204
- }
205
- case 0x55: {
206
- // Update
207
- const tableOid = r32(data, 1)
208
- let pos = 5
209
- let oldTupleData: TupleData | undefined
210
- if (data[pos] === 0x4f) {
211
- // 'O' old tuple
212
- pos++
213
- const [old, np] = decodeTupleData(data, pos)
214
- oldTupleData = old
215
- pos = np
216
- }
217
- if (data[pos] !== 0x4e) throw new Error(`update: expected 'N' marker at ${pos}`)
218
- pos++
219
- const [newTupleData] = decodeTupleData(data, pos)
220
- return { type: 'Update', raw: data, tableOid, oldTupleData, newTupleData }
221
- }
222
- case 0x44: {
223
- // Delete
224
- const tableOid = r32(data, 1)
225
- const marker = data[5]
226
- if (marker !== 0x4b && marker !== 0x4f)
227
- throw new Error(
228
- `delete: expected 'K' or 'O' marker, got 0x${marker.toString(16)}`
229
- )
230
- const [keyTupleData] = decodeTupleData(data, 6)
231
- return { type: 'Delete', raw: data, tableOid, keyTupleData }
232
- }
233
- default:
234
- throw new Error(`unknown pgoutput message type: 0x${msgType.toString(16)}`)
235
- }
236
- }
237
-
238
- // decode a CopyData frame, returning either an XLogData payload or a Keepalive
239
- function decodeCopyData(frame: Uint8Array): PgOutputMessage | KeepaliveMessage | null {
240
- if (frame[0] !== 0x64) return null // not CopyData
241
- const innerType = frame[5]
242
- if (innerType === 0x77) {
243
- // XLogData: walStart(8) + walEnd(8) + timestamp(8) + data
244
- const payload = frame.subarray(30)
245
- return decodePgOutput(payload)
246
- }
247
- if (innerType === 0x6b) {
248
- // Keepalive
249
- return {
250
- type: 'Keepalive',
251
- raw: frame,
252
- walEnd: r64(frame, 6),
253
- timestamp: r64(frame, 14),
254
- replyRequested: frame[22] === 1,
255
- }
256
- }
257
- return null
258
- }
259
-
260
- // --- minimal pg wire protocol client ---
261
-
262
- function buildStartupMessage(params: Record<string, string>): Buffer {
263
- const pairs: Buffer[] = []
264
- for (const [k, v] of Object.entries(params)) {
265
- pairs.push(Buffer.from(`${k}\0${v}\0`, 'utf8'))
266
- }
267
- pairs.push(Buffer.from('\0', 'utf8'))
268
-
269
- const bodyLen = pairs.reduce((s, b) => s + b.length, 0)
270
- const buf = Buffer.alloc(4 + 4 + bodyLen)
271
- buf.writeInt32BE(4 + 4 + bodyLen, 0) // length
272
- buf.writeInt32BE(196608, 4) // protocol version 3.0
273
- let pos = 8
274
- for (const p of pairs) {
275
- p.copy(buf, pos)
276
- pos += p.length
277
- }
278
- return buf
279
- }
280
-
281
- function buildPasswordMessage(password: string): Buffer {
282
- const pwBuf = Buffer.from(password + '\0', 'utf8')
283
- const buf = Buffer.alloc(1 + 4 + pwBuf.length)
284
- buf[0] = 0x70 // 'p'
285
- buf.writeInt32BE(4 + pwBuf.length, 1)
286
- pwBuf.copy(buf, 5)
287
- return buf
288
- }
289
-
290
- function buildQuery(sql: string): Buffer {
291
- const sqlBuf = Buffer.from(sql + '\0', 'utf8')
292
- const buf = Buffer.alloc(1 + 4 + sqlBuf.length)
293
- buf[0] = 0x51 // 'Q'
294
- buf.writeInt32BE(4 + sqlBuf.length, 1)
295
- sqlBuf.copy(buf, 5)
296
- return buf
297
- }
298
-
299
- interface PgMessage {
300
- type: number
301
- data: Buffer
302
- }
303
-
304
- // reads exactly one PG message from a buffer, returns [message, remainingBuffer]
305
- function parseMessage(buf: Buffer): [PgMessage | null, Buffer] {
306
- if (buf.length < 5) return [null, buf]
307
- const type = buf[0]
308
- const len = buf.readInt32BE(1)
309
- const totalLen = 1 + len
310
- if (buf.length < totalLen) return [null, buf]
311
- return [{ type, data: buf.subarray(0, totalLen) }, buf.subarray(totalLen)]
312
- }
313
-
314
- // higher-level client that connects, authenticates, and can send queries
315
- class TestPgClient {
316
- private socket!: Socket
317
- private buffer = Buffer.alloc(0)
318
- private waiters: Array<(msg: PgMessage) => void> = []
319
- private messages: PgMessage[] = []
320
- port: number
321
-
322
- constructor(port: number) {
323
- this.port = port
324
- }
325
-
326
- async connect(opts: {
327
- user: string
328
- password: string
329
- database: string
330
- replication?: boolean
331
- }): Promise<void> {
332
- this.socket = createConnection({ port: this.port, host: '127.0.0.1' })
333
-
334
- await new Promise<void>((resolve, reject) => {
335
- this.socket.once('connect', resolve)
336
- this.socket.once('error', reject)
337
- })
338
-
339
- this.socket.on('data', (chunk: Buffer) => {
340
- this.buffer = Buffer.concat([this.buffer, chunk])
341
- this.drain()
342
- })
343
-
344
- const params: Record<string, string> = {
345
- user: opts.user,
346
- database: opts.database,
347
- }
348
- if (opts.replication) {
349
- params.replication = 'database'
350
- }
351
-
352
- this.socket.write(buildStartupMessage(params))
353
-
354
- // wait for auth request
355
- const authReq = await this.nextMessage()
356
- expect(authReq.type).toBe(0x52) // 'R' Authentication
357
-
358
- const authType = authReq.data.readInt32BE(5)
359
- if (authType === 3) {
360
- // cleartext password
361
- this.socket.write(buildPasswordMessage(opts.password))
362
- const authOk = await this.nextMessage()
363
- expect(authOk.type).toBe(0x52)
364
- expect(authOk.data.readInt32BE(5)).toBe(0) // AuthenticationOk
365
- }
366
-
367
- // consume parameter status + backend key data + ready for query
368
- while (true) {
369
- const msg = await this.nextMessage()
370
- if (msg.type === 0x5a) break // ReadyForQuery
371
- }
372
- }
373
-
374
- // send simple query and collect all response messages until ReadyForQuery
375
- async query(sql: string): Promise<PgMessage[]> {
376
- this.socket.write(buildQuery(sql))
377
- const responses: PgMessage[] = []
378
- while (true) {
379
- const msg = await this.nextMessage()
380
- responses.push(msg)
381
- if (msg.type === 0x5a) break // ReadyForQuery
382
- }
383
- return responses
384
- }
385
-
386
- // send START_REPLICATION and return CopyBothResponse, then collect stream messages
387
- async startReplication(query: string): Promise<void> {
388
- this.socket.write(buildQuery(query))
389
- }
390
-
391
- // collect streaming messages for a duration
392
- async collectStream(durationMs: number): Promise<PgMessage[]> {
393
- const collected: PgMessage[] = []
394
- const deadline = Date.now() + durationMs
395
- while (Date.now() < deadline) {
396
- try {
397
- const msg = await this.nextMessage(Math.max(50, deadline - Date.now()))
398
- collected.push(msg)
399
- } catch {
400
- // timeout, keep going
401
- }
402
- }
403
- return collected
404
- }
405
-
406
- // send raw data to inject into connection (e.g. for data connection)
407
- sendRaw(data: Buffer) {
408
- this.socket.write(data)
409
- }
410
-
411
- close() {
412
- this.socket?.destroy()
413
- }
414
-
415
- private drain() {
416
- while (true) {
417
- const [msg, remaining] = parseMessage(this.buffer)
418
- if (!msg) break
419
- this.buffer = remaining
420
- const waiter = this.waiters.shift()
421
- if (waiter) {
422
- waiter(msg)
423
- } else {
424
- this.messages.push(msg)
425
- }
426
- }
427
- }
428
-
429
- private nextMessage(timeoutMs = 5000): Promise<PgMessage> {
430
- const queued = this.messages.shift()
431
- if (queued) return Promise.resolve(queued)
432
-
433
- return new Promise<PgMessage>((resolve, reject) => {
434
- const timer = setTimeout(() => {
435
- const idx = this.waiters.indexOf(resolve)
436
- if (idx >= 0) this.waiters.splice(idx, 1)
437
- reject(new Error(`timeout waiting for message (${timeoutMs}ms)`))
438
- }, timeoutMs)
439
-
440
- this.waiters.push((msg) => {
441
- clearTimeout(timer)
442
- resolve(msg)
443
- })
444
- })
445
- }
446
- }
447
-
448
- // --- tests ---
449
-
450
- describe('tcp replication', () => {
451
- let db: PGlite
452
- let server: Server
453
- let port: number
454
-
455
- beforeEach(async () => {
456
- resetReplicationState()
457
- db = new PGlite()
458
- await db.waitReady
459
-
460
- await db.exec(`
461
- CREATE TABLE public.items (
462
- id SERIAL PRIMARY KEY,
463
- name TEXT NOT NULL,
464
- value INTEGER
465
- )
466
- `)
467
-
468
- // publication for zero-cache
469
- await db.exec(`CREATE PUBLICATION zero_takeout FOR ALL TABLES`)
470
-
471
- await installChangeTracking(db)
472
-
473
- const config = {
474
- ...getConfig(),
475
- pgPort: 0, // random port
476
- }
477
- server = await startPgProxy(db, config)
478
- port = (server.address() as AddressInfo).port
479
- })
480
-
481
- afterEach(async () => {
482
- server?.close()
483
- await db?.close()
484
- })
485
-
486
- it('accepts connection and authenticates', async () => {
487
- const client = new TestPgClient(port)
488
- await client.connect({
489
- user: 'user',
490
- password: 'password',
491
- database: 'postgres',
492
- })
493
- client.close()
494
- })
495
-
496
- it('rejects wrong password', async () => {
497
- const client = new TestPgClient(port)
498
- await expect(
499
- client.connect({
500
- user: 'user',
501
- password: 'wrong',
502
- database: 'postgres',
503
- })
504
- ).rejects.toThrow()
505
- client.close()
506
- })
507
-
508
- it('handles IDENTIFY_SYSTEM over tcp', async () => {
509
- const client = new TestPgClient(port)
510
- await client.connect({
511
- user: 'user',
512
- password: 'password',
513
- database: 'postgres',
514
- replication: true,
515
- })
516
-
517
- const response = await client.query('IDENTIFY_SYSTEM')
518
- // should have RowDescription + DataRow + CommandComplete + ReadyForQuery
519
- const types = response.map((m) => m.type)
520
- expect(types).toContain(0x54) // RowDescription
521
- expect(types).toContain(0x44) // DataRow
522
- expect(types).toContain(0x43) // CommandComplete
523
- expect(types).toContain(0x5a) // ReadyForQuery
524
-
525
- client.close()
526
- })
527
-
528
- it('handles CREATE_REPLICATION_SLOT over tcp', async () => {
529
- const client = new TestPgClient(port)
530
- await client.connect({
531
- user: 'user',
532
- password: 'password',
533
- database: 'postgres',
534
- replication: true,
535
- })
536
-
537
- const response = await client.query(
538
- 'CREATE_REPLICATION_SLOT "tcp_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
539
- )
540
- const types = response.map((m) => m.type)
541
- expect(types).toContain(0x54) // RowDescription
542
- expect(types).toContain(0x44) // DataRow
543
-
544
- client.close()
545
- })
546
-
547
- it('streams replication changes over tcp', async () => {
548
- const replClient = new TestPgClient(port)
549
- await replClient.connect({
550
- user: 'user',
551
- password: 'password',
552
- database: 'postgres',
553
- replication: true,
554
- })
555
-
556
- await replClient.query(
557
- 'CREATE_REPLICATION_SLOT "stream_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
558
- )
559
-
560
- await replClient.startReplication(
561
- "START_REPLICATION SLOT \"stream_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
562
- )
563
-
564
- // insert data right away - signal so the handler picks it up immediately
565
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('tcp_streamed', 42)`)
566
- signalReplicationChange()
567
-
568
- // collect everything for long enough to catch the change
569
- const allRaw = await replClient.collectStream(3000)
570
-
571
- // decode all CopyData frames
572
- const decoded: PgOutputMessage[] = []
573
- for (const msg of allRaw) {
574
- if (msg.type === 0x64) {
575
- const result = decodeCopyData(new Uint8Array(msg.data))
576
- if (result) decoded.push(result)
577
- }
578
- }
579
-
580
- // should have keepalives
581
- const keepalives = decoded.filter((m) => m.type === 'Keepalive')
582
- expect(keepalives.length).toBeGreaterThan(0)
583
-
584
- // should have BEGIN, RELATION, INSERT, COMMIT
585
- const msgTypes = decoded.map((m) => m.type)
586
- expect(msgTypes).toContain('Begin')
587
- expect(msgTypes).toContain('Relation')
588
- expect(msgTypes).toContain('Insert')
589
- expect(msgTypes).toContain('Commit')
590
-
591
- // validate the RELATION message
592
- const relation = decoded.find((m) => m.type === 'Relation') as RelationMessage
593
- expect(relation.schema).toBe('public')
594
- expect(relation.tableName).toBe('items')
595
- expect(relation.columns.length).toBeGreaterThanOrEqual(3) // id, name, value
596
-
597
- // validate the INSERT message
598
- const insert = decoded.find((m) => m.type === 'Insert') as InsertMessage
599
- expect(insert.tableOid).toBe(relation.tableOid) // same table
600
- const values = insert.tupleData.columns.map((c) => c.value)
601
- expect(values).toContain('tcp_streamed')
602
- expect(values).toContain('42')
603
-
604
- // validate transaction structure: BEGIN before INSERT before COMMIT
605
- const beginIdx = decoded.findIndex((m) => m.type === 'Begin')
606
- const insertIdx = decoded.findIndex((m) => m.type === 'Insert')
607
- const commitIdx = decoded.findIndex((m) => m.type === 'Commit')
608
- expect(beginIdx).toBeLessThan(insertIdx)
609
- expect(insertIdx).toBeLessThan(commitIdx)
610
-
611
- replClient.close()
612
- }, 15_000)
613
-
614
- it('streams updates with old + new tuple data', async () => {
615
- const replClient = new TestPgClient(port)
616
- await replClient.connect({
617
- user: 'user',
618
- password: 'password',
619
- database: 'postgres',
620
- replication: true,
621
- })
622
- await replClient.query(
623
- 'CREATE_REPLICATION_SLOT "upd_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
624
- )
625
- await replClient.startReplication(
626
- "START_REPLICATION SLOT \"upd_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
627
- )
628
-
629
- await replClient.collectStream(200) // skip CopyBothResponse
630
-
631
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('upd_target', 10)`)
632
- signalReplicationChange()
633
- await replClient.collectStream(1500)
634
-
635
- await db.exec(`UPDATE public.items SET value = 20 WHERE name = 'upd_target'`)
636
- signalReplicationChange()
637
- const stream = await replClient.collectStream(1500)
638
-
639
- const decoded: PgOutputMessage[] = []
640
- for (const msg of stream) {
641
- if (msg.type === 0x64) {
642
- const result = decodeCopyData(new Uint8Array(msg.data))
643
- if (result) decoded.push(result)
644
- }
645
- }
646
-
647
- const update = decoded.find((m) => m.type === 'Update') as UpdateMessage
648
- expect(update).toBeDefined()
649
- // new data should have value=20
650
- const newValues = update.newTupleData.columns.map((c) => c.value)
651
- expect(newValues).toContain('20')
652
- // old data should have value=10
653
- expect(update.oldTupleData).toBeDefined()
654
- const oldValues = update.oldTupleData!.columns.map((c) => c.value)
655
- expect(oldValues).toContain('10')
656
-
657
- replClient.close()
658
- }, 15_000)
659
-
660
- it('streams deletes with key data', async () => {
661
- const replClient = new TestPgClient(port)
662
- await replClient.connect({
663
- user: 'user',
664
- password: 'password',
665
- database: 'postgres',
666
- replication: true,
667
- })
668
- await replClient.query(
669
- 'CREATE_REPLICATION_SLOT "del_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
670
- )
671
- await replClient.startReplication(
672
- "START_REPLICATION SLOT \"del_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
673
- )
674
-
675
- await replClient.collectStream(200)
676
-
677
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('del_target', 99)`)
678
- signalReplicationChange()
679
- await replClient.collectStream(2000)
680
-
681
- await db.exec(`DELETE FROM public.items WHERE name = 'del_target'`)
682
- signalReplicationChange()
683
-
684
- const allDecoded: PgOutputMessage[] = []
685
- const deadline = Date.now() + 5000
686
- while (Date.now() < deadline) {
687
- const stream = await replClient.collectStream(500)
688
- for (const msg of stream) {
689
- if (msg.type === 0x64) {
690
- const result = decodeCopyData(new Uint8Array(msg.data))
691
- if (result) allDecoded.push(result)
692
- }
693
- }
694
- if (allDecoded.some((m) => m.type === 'Delete')) break
695
- }
696
-
697
- const del = allDecoded.find((m) => m.type === 'Delete') as DeleteMessage
698
- expect(del).toBeDefined()
699
- const keyValues = del.keyTupleData.columns.map((c) => c.value)
700
- expect(keyValues).toContain('del_target')
701
-
702
- replClient.close()
703
- }, 15_000)
704
-
705
- it('handles multiple tables in same stream', async () => {
706
- // create a second table and re-install tracking for both
707
- await db.exec(`CREATE TABLE public.products (id SERIAL PRIMARY KEY, label TEXT)`)
708
- await installChangeTracking(db)
709
-
710
- const replClient = new TestPgClient(port)
711
- await replClient.connect({
712
- user: 'user',
713
- password: 'password',
714
- database: 'postgres',
715
- replication: true,
716
- })
717
- await replClient.query(
718
- 'CREATE_REPLICATION_SLOT "multi_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
719
- )
720
- await replClient.startReplication(
721
- "START_REPLICATION SLOT \"multi_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
722
- )
723
-
724
- await replClient.collectStream(200)
725
-
726
- // insert into both tables
727
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('multi1', 1)`)
728
- await db.exec(`INSERT INTO public.products (label) VALUES ('multi2')`)
729
- signalReplicationChange()
730
-
731
- // collect until we see both relations (with timeout)
732
- const allDecoded: PgOutputMessage[] = []
733
- const deadline = Date.now() + 10000
734
- while (Date.now() < deadline) {
735
- const stream = await replClient.collectStream(500)
736
- for (const msg of stream) {
737
- if (msg.type === 0x64) {
738
- const result = decodeCopyData(new Uint8Array(msg.data))
739
- if (result) allDecoded.push(result)
740
- }
741
- }
742
- const relations = allDecoded.filter(
743
- (m) => m.type === 'Relation'
744
- ) as RelationMessage[]
745
- const tableNames = relations.map((r) => r.tableName)
746
- if (tableNames.includes('items') && tableNames.includes('products')) break
747
- signalReplicationChange()
748
- }
749
-
750
- const relations = allDecoded.filter((m) => m.type === 'Relation') as RelationMessage[]
751
- const tableNames = relations.map((r) => r.tableName)
752
- expect(tableNames).toContain('items')
753
- expect(tableNames).toContain('products')
754
-
755
- replClient.close()
756
- }, 15_000)
757
-
758
- it('handles rapid inserts over tcp', async () => {
759
- const replClient = new TestPgClient(port)
760
- await replClient.connect({
761
- user: 'user',
762
- password: 'password',
763
- database: 'postgres',
764
- replication: true,
765
- })
766
- await replClient.query(
767
- 'CREATE_REPLICATION_SLOT "rapid_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
768
- )
769
- await replClient.startReplication(
770
- "START_REPLICATION SLOT \"rapid_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
771
- )
772
-
773
- await replClient.collectStream(200)
774
-
775
- const count = 15
776
- for (let i = 0; i < count; i++) {
777
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('rapid${i}', ${i})`)
778
- }
779
- signalReplicationChange()
780
-
781
- // poll until all inserts arrive
782
- const allDecoded: PgOutputMessage[] = []
783
- const deadline = Date.now() + 5000
784
- while (Date.now() < deadline) {
785
- const stream = await replClient.collectStream(500)
786
- for (const msg of stream) {
787
- if (msg.type === 0x64) {
788
- const result = decodeCopyData(new Uint8Array(msg.data))
789
- if (result) allDecoded.push(result)
790
- }
791
- }
792
- const inserts = allDecoded.filter((m) => m.type === 'Insert')
793
- if (inserts.length >= count) break
794
- signalReplicationChange()
795
- }
796
-
797
- const inserts = allDecoded.filter((m) => m.type === 'Insert')
798
- expect(inserts.length).toBe(count)
799
-
800
- replClient.close()
801
- }, 15_000)
802
-
803
- it('regular (non-replication) queries work over tcp', async () => {
804
- const client = new TestPgClient(port)
805
- await client.connect({
806
- user: 'user',
807
- password: 'password',
808
- database: 'postgres',
809
- })
810
-
811
- // insert via tcp
812
- await client.query(`INSERT INTO public.items (name, value) VALUES ('tcp_direct', 77)`)
813
-
814
- // select back
815
- const response = await client.query(
816
- `SELECT name, value FROM public.items WHERE name = 'tcp_direct'`
817
- )
818
- const dataRow = response.find((m) => m.type === 0x44) // DataRow
819
- expect(dataRow).toBeDefined()
820
-
821
- client.close()
822
- })
823
-
824
- it('concurrent replication + regular connections', async () => {
825
- // start replication client
826
- const replClient = new TestPgClient(port)
827
- await replClient.connect({
828
- user: 'user',
829
- password: 'password',
830
- database: 'postgres',
831
- replication: true,
832
- })
833
- await replClient.query(
834
- 'CREATE_REPLICATION_SLOT "concurrent_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT'
835
- )
836
- await replClient.startReplication(
837
- "START_REPLICATION SLOT \"concurrent_test\" LOGICAL 0/0 (proto_version '1', publication_names 'zero_takeout')"
838
- )
839
- await replClient.collectStream(200)
840
-
841
- // regular client inserts data
842
- const dataClient = new TestPgClient(port)
843
- await dataClient.connect({
844
- user: 'user',
845
- password: 'password',
846
- database: 'postgres',
847
- })
848
- await dataClient.query(
849
- `INSERT INTO public.items (name, value) VALUES ('concurrent', 123)`
850
- )
851
- signalReplicationChange()
852
-
853
- // poll until the replication stream picks up the change
854
- const allDecoded: PgOutputMessage[] = []
855
- const deadline = Date.now() + 5000
856
- while (Date.now() < deadline) {
857
- const stream = await replClient.collectStream(500)
858
- for (const msg of stream) {
859
- if (msg.type === 0x64) {
860
- const result = decodeCopyData(new Uint8Array(msg.data))
861
- if (result) allDecoded.push(result)
862
- }
863
- }
864
- const inserts = allDecoded.filter((m) => m.type === 'Insert')
865
- if (inserts.length >= 1) break
866
- }
867
-
868
- const inserts = allDecoded.filter((m) => m.type === 'Insert') as InsertMessage[]
869
- expect(inserts.length).toBe(1)
870
- const values = inserts[0].tupleData.columns.map((c) => c.value)
871
- expect(values).toContain('concurrent')
872
-
873
- dataClient.close()
874
- replClient.close()
875
- }, 15_000)
876
- })