orez 0.2.26 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/cf-do/worker.d.ts.map +1 -1
  2. package/dist/cf-do/worker.js +9 -1
  3. package/dist/cf-do/worker.js.map +1 -1
  4. package/dist/pg-proxy-do-backend.d.ts +2 -0
  5. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  6. package/dist/pg-proxy-do-backend.js +49 -7
  7. package/dist/pg-proxy-do-backend.js.map +1 -1
  8. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  9. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  10. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  11. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  12. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  13. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  14. package/dist/pg-sqlite-compiler/index.js +59 -0
  15. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  16. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  17. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  18. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  19. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  20. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  21. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  22. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  23. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  24. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  25. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  27. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  29. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  31. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  33. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  35. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  39. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  41. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/types.js +2 -0
  43. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  44. package/package.json +8 -4
  45. package/src/admin/admin-data.test.ts +0 -348
  46. package/src/admin/http-proxy.ts +0 -252
  47. package/src/admin/log-store.ts +0 -192
  48. package/src/admin/server.ts +0 -471
  49. package/src/admin/ui.ts +0 -1322
  50. package/src/bench/proxy-throughput.bench.ts +0 -343
  51. package/src/bench/serial-mutations.bench.ts +0 -270
  52. package/src/browser.ts +0 -203
  53. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  54. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  55. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  56. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  57. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  58. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  59. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  60. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  61. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  62. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  63. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  64. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  65. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  66. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  67. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  69. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  75. package/src/cf-do/ARCHITECTURE.md +0 -83
  76. package/src/cf-do/watermark.test.ts +0 -103
  77. package/src/cf-do/watermark.ts +0 -118
  78. package/src/cf-do/worker.ts +0 -1033
  79. package/src/cf-do/wrangler.toml +0 -11
  80. package/src/cf-pglite/README.md +0 -19
  81. package/src/change-tracking.ts +0 -25
  82. package/src/child-process.test.ts +0 -147
  83. package/src/child-process.ts +0 -90
  84. package/src/cli-entry.ts +0 -72
  85. package/src/cli.test.ts +0 -38
  86. package/src/cli.ts +0 -1214
  87. package/src/config.ts +0 -150
  88. package/src/do-sql-tracking.test.ts +0 -19
  89. package/src/do-sql-tracking.ts +0 -19
  90. package/src/index.ts +0 -1215
  91. package/src/integration/integration.test.ts +0 -517
  92. package/src/integration/native-binary.guard.test.ts +0 -13
  93. package/src/integration/native-startup.test.ts +0 -44
  94. package/src/integration/replication-latency.test.ts +0 -428
  95. package/src/integration/restore-live-stress.test.ts +0 -433
  96. package/src/integration/restore-reset.test.ts +0 -400
  97. package/src/integration/restore.test.ts +0 -274
  98. package/src/integration/test-permissions.ts +0 -147
  99. package/src/load-config.ts +0 -46
  100. package/src/log.ts +0 -96
  101. package/src/mutex.ts +0 -47
  102. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  103. package/src/pg-proxy-browser.ts +0 -2022
  104. package/src/pg-proxy-do-backend.test.ts +0 -3890
  105. package/src/pg-proxy-do-backend.ts +0 -7157
  106. package/src/pg-proxy.ts +0 -1087
  107. package/src/pglite-ipc.test.ts +0 -116
  108. package/src/pglite-ipc.ts +0 -266
  109. package/src/pglite-manager.ts +0 -557
  110. package/src/pglite-web-proxy.test.ts +0 -57
  111. package/src/pglite-web-proxy.ts +0 -221
  112. package/src/pglite-web-worker.ts +0 -152
  113. package/src/pglite-worker-thread.ts +0 -253
  114. package/src/port.ts +0 -25
  115. package/src/process-title.ts +0 -9
  116. package/src/recovery.ts +0 -155
  117. package/src/replication/change-tracker.test.ts +0 -357
  118. package/src/replication/change-tracker.ts +0 -279
  119. package/src/replication/handler.test.ts +0 -511
  120. package/src/replication/handler.ts +0 -1190
  121. package/src/replication/pgoutput-encoder.test.ts +0 -697
  122. package/src/replication/pgoutput-encoder.ts +0 -373
  123. package/src/replication/tcp-replication.test.ts +0 -876
  124. package/src/replication/zero-compat.test.ts +0 -1150
  125. package/src/restore-stress.test.ts +0 -188
  126. package/src/s3-local.ts +0 -203
  127. package/src/shim/hooks.mjs +0 -120
  128. package/src/shim/register.mjs +0 -4
  129. package/src/sqlite-mode/apply-mode.ts +0 -224
  130. package/src/sqlite-mode/index.ts +0 -15
  131. package/src/sqlite-mode/native-binary.ts +0 -89
  132. package/src/sqlite-mode/package-resolve.ts +0 -17
  133. package/src/sqlite-mode/resolve-mode.ts +0 -80
  134. package/src/sqlite-mode/shim-template.ts +0 -159
  135. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  136. package/src/sqlite-mode/types.ts +0 -30
  137. package/src/vite-plugin.ts +0 -67
  138. package/src/wasm-sqlite.test.ts +0 -537
  139. package/src/worker/browser-admin.ts +0 -52
  140. package/src/worker/browser-build-config.test.ts +0 -71
  141. package/src/worker/browser-build-config.ts +0 -109
  142. package/src/worker/browser-embed-admin.test.ts +0 -75
  143. package/src/worker/browser-embed.ts +0 -345
  144. package/src/worker/cf-patches.ts +0 -384
  145. package/src/worker/embed-integration.test.ts +0 -321
  146. package/src/worker/index.ts +0 -138
  147. package/src/worker/shims/fastify.test.ts +0 -255
  148. package/src/worker/shims/fastify.ts +0 -306
  149. package/src/worker/shims/http-service.test.ts +0 -355
  150. package/src/worker/shims/http-service.ts +0 -293
  151. package/src/worker/shims/node-stub.ts +0 -290
  152. package/src/worker/shims/oxfmt.ts +0 -3
  153. package/src/worker/shims/postgres-browser.ts +0 -59
  154. package/src/worker/shims/postgres-socket.test.ts +0 -576
  155. package/src/worker/shims/postgres-socket.ts +0 -310
  156. package/src/worker/shims/postgres.test.ts +0 -364
  157. package/src/worker/shims/postgres.ts +0 -1454
  158. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  159. package/src/worker/shims/sqlite-browser.ts +0 -175
  160. package/src/worker/shims/sqlite.test.ts +0 -786
  161. package/src/worker/shims/sqlite.ts +0 -978
  162. package/src/worker/shims/stream-browser.ts +0 -15
  163. package/src/worker/shims/ws-browser.test.ts +0 -205
  164. package/src/worker/shims/ws-browser.ts +0 -248
  165. package/src/worker/shims/ws.test.ts +0 -288
  166. package/src/worker/shims/ws.ts +0 -467
  167. package/src/worker/shims/zero-process-env.ts +0 -11
  168. package/src/worker/types.ts +0 -75
  169. package/src/worker/worker-integration.test.ts +0 -223
  170. package/src/worker/worker.test.ts +0 -136
  171. package/src/worker/zero-cache-embed-cf.ts +0 -463
  172. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,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
- })