orez 0.0.48 → 0.0.49
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/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -112
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -280
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +0 -9
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -24
- package/dist/log.js.map +1 -1
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +2 -13
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts +2 -3
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +167 -377
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +0 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +2 -8
- package/dist/pglite-manager.js.map +1 -1
- package/dist/replication/change-tracker.d.ts +0 -6
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +1 -62
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +7 -66
- package/dist/replication/handler.js.map +1 -1
- package/dist/vite-plugin.d.ts +0 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +0 -24
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +5 -4
- package/src/cli.ts +18 -124
- package/src/config.ts +0 -10
- package/src/index.ts +92 -309
- package/src/integration/integration.test.ts +264 -133
- package/src/log.ts +1 -25
- package/src/mutex.ts +2 -12
- package/src/pg-proxy.ts +187 -451
- package/src/pglite-manager.ts +2 -9
- package/src/replication/change-tracker.ts +1 -83
- package/src/replication/handler.ts +6 -79
- package/src/replication/pgoutput-encoder.test.ts +0 -217
- package/src/replication/zero-compat.test.ts +1 -232
- package/src/shim/hooks.mjs +1 -1
- package/src/vite-plugin.ts +0 -28
- package/src/wasm-sqlite.test.ts +1 -2
|
@@ -701,22 +701,17 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
|
|
|
701
701
|
const s = await stream()
|
|
702
702
|
const q = s.messages
|
|
703
703
|
|
|
704
|
-
// insert sequentially with waits to avoid batching
|
|
705
704
|
await db.exec(`INSERT INTO public.foo (id) VALUES ('t1')`)
|
|
706
705
|
await db.exec(`INSERT INTO public.foo (id) VALUES ('t2')`)
|
|
707
706
|
await db.exec(`INSERT INTO public.foo (id) VALUES ('t3')`)
|
|
708
707
|
|
|
709
|
-
// orez may batch changes into fewer transactions, so just verify
|
|
710
|
-
// all inserts arrive and every begin has a matching commit
|
|
711
708
|
const all: ZcMessage[] = []
|
|
712
709
|
const deadline = Date.now() + 8000
|
|
713
710
|
while (Date.now() < deadline) {
|
|
714
711
|
const m = await q.dequeue(2000).catch(() => null)
|
|
715
712
|
if (!m) break
|
|
716
713
|
if (m.tag !== 'keepalive') all.push(m)
|
|
717
|
-
|
|
718
|
-
const commits = all.filter((x) => x.tag === 'commit')
|
|
719
|
-
if (inserts.length >= 3 && commits.length >= 1) break
|
|
714
|
+
if (all.filter((x) => x.tag === 'commit').length >= 3) break
|
|
720
715
|
}
|
|
721
716
|
|
|
722
717
|
const begins = all.filter((m) => m.tag === 'begin')
|
|
@@ -946,229 +941,3 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
|
|
|
946
941
|
s.close()
|
|
947
942
|
})
|
|
948
943
|
})
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* postgres.js replication stream test.
|
|
952
|
-
*
|
|
953
|
-
* uses the same postgres library and code path that zero-cache uses
|
|
954
|
-
* in its stream.js subscribe function. validates that orez's CopyData
|
|
955
|
-
* frames are correctly parsed by postgres.js's wire protocol handler
|
|
956
|
-
* and that zero-cache's PgoutputParser can consume the payloads.
|
|
957
|
-
*/
|
|
958
|
-
describe('postgres.js replication stream (zero-cache code path)', { timeout: 30000 }, () => {
|
|
959
|
-
let db: PGlite
|
|
960
|
-
let server: Server
|
|
961
|
-
let port: number
|
|
962
|
-
|
|
963
|
-
beforeEach(async () => {
|
|
964
|
-
db = new PGlite()
|
|
965
|
-
await db.waitReady
|
|
966
|
-
await db.exec(`
|
|
967
|
-
CREATE TABLE public.items (
|
|
968
|
-
id TEXT PRIMARY KEY,
|
|
969
|
-
val INTEGER,
|
|
970
|
-
note TEXT
|
|
971
|
-
)
|
|
972
|
-
`)
|
|
973
|
-
await db.exec(`CREATE PUBLICATION zero_data FOR ALL TABLES`)
|
|
974
|
-
await installChangeTracking(db)
|
|
975
|
-
|
|
976
|
-
const config = { ...getConfig(), pgPort: 0 }
|
|
977
|
-
server = await startPgProxy(db, config)
|
|
978
|
-
port = (server.address() as AddressInfo).port
|
|
979
|
-
})
|
|
980
|
-
|
|
981
|
-
afterEach(async () => {
|
|
982
|
-
server?.close()
|
|
983
|
-
await db?.close()
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
it('postgres.js receives CopyData and parseStreamMessage decodes it', { timeout: 30000 }, async () => {
|
|
987
|
-
// import postgres (same lib zero-cache uses)
|
|
988
|
-
const pg = (await import('postgres')).default
|
|
989
|
-
|
|
990
|
-
// create a regular connection for queries
|
|
991
|
-
const regular = pg({
|
|
992
|
-
host: '127.0.0.1',
|
|
993
|
-
port,
|
|
994
|
-
user: 'user',
|
|
995
|
-
password: 'password',
|
|
996
|
-
database: 'postgres',
|
|
997
|
-
max: 1,
|
|
998
|
-
})
|
|
999
|
-
|
|
1000
|
-
// create replication connection (same as zero-cache's subscribe)
|
|
1001
|
-
const session = pg({
|
|
1002
|
-
host: '127.0.0.1',
|
|
1003
|
-
port,
|
|
1004
|
-
user: 'user',
|
|
1005
|
-
password: 'password',
|
|
1006
|
-
database: 'postgres',
|
|
1007
|
-
max: 1,
|
|
1008
|
-
fetch_types: false,
|
|
1009
|
-
idle_timeout: null,
|
|
1010
|
-
max_lifetime: null,
|
|
1011
|
-
connection: { replication: 'database' },
|
|
1012
|
-
})
|
|
1013
|
-
|
|
1014
|
-
try {
|
|
1015
|
-
// create slot (same as zero-cache)
|
|
1016
|
-
await session.unsafe(
|
|
1017
|
-
`CREATE_REPLICATION_SLOT "pgjs_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT`
|
|
1018
|
-
).simple()
|
|
1019
|
-
|
|
1020
|
-
// start replication stream (same pattern as stream.js)
|
|
1021
|
-
const stream = session.unsafe(
|
|
1022
|
-
`START_REPLICATION SLOT "pgjs_test" LOGICAL 0/0 (proto_version '1', publication_names 'zero_data', messages 'true')`
|
|
1023
|
-
).execute()
|
|
1024
|
-
|
|
1025
|
-
const [readable, _writable] = await Promise.all([
|
|
1026
|
-
stream.readable(),
|
|
1027
|
-
stream.writable(),
|
|
1028
|
-
])
|
|
1029
|
-
|
|
1030
|
-
// import zero-cache's actual parser
|
|
1031
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1032
|
-
const { PgoutputParser } = require('/Users/n8/orez/node_modules/@rocicorp/zero/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js')
|
|
1033
|
-
const typeParsers = { getTypeParser: () => String }
|
|
1034
|
-
const parser = new PgoutputParser(typeParsers)
|
|
1035
|
-
|
|
1036
|
-
// parseStreamMessage from zero-cache's stream.js
|
|
1037
|
-
function parseStreamMessage(buffer: Buffer): [bigint, any] | null {
|
|
1038
|
-
if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) return null
|
|
1039
|
-
const lsn = buffer.readBigUInt64BE(1)
|
|
1040
|
-
if (buffer[0] === 0x77) {
|
|
1041
|
-
return [lsn, parser.parse(buffer.subarray(25))]
|
|
1042
|
-
}
|
|
1043
|
-
if (buffer.readInt8(17)) {
|
|
1044
|
-
return [lsn, { tag: 'keepalive' }]
|
|
1045
|
-
}
|
|
1046
|
-
return null // keepalive with shouldRespond=false
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// collect parsed messages
|
|
1050
|
-
const messages: any[] = []
|
|
1051
|
-
const collectDone = new Promise<void>((resolve) => {
|
|
1052
|
-
readable.on('data', (chunk: Buffer) => {
|
|
1053
|
-
const result = parseStreamMessage(chunk)
|
|
1054
|
-
if (result) {
|
|
1055
|
-
const [_lsn, msg] = result
|
|
1056
|
-
messages.push(msg)
|
|
1057
|
-
}
|
|
1058
|
-
})
|
|
1059
|
-
setTimeout(resolve, 3000)
|
|
1060
|
-
})
|
|
1061
|
-
|
|
1062
|
-
await new Promise((r) => setTimeout(r, 500))
|
|
1063
|
-
await regular.unsafe(`INSERT INTO public.items (id, val, note) VALUES ('pgjs', 42, 'postgres.js test')`)
|
|
1064
|
-
|
|
1065
|
-
await collectDone
|
|
1066
|
-
readable.destroy()
|
|
1067
|
-
|
|
1068
|
-
// filter out keepalives
|
|
1069
|
-
const data = messages.filter((m: any) => m.tag !== 'keepalive')
|
|
1070
|
-
|
|
1071
|
-
// should have: begin, relation, insert, commit
|
|
1072
|
-
const tags = data.map((m: any) => m.tag)
|
|
1073
|
-
expect(tags).toContain('begin')
|
|
1074
|
-
expect(tags).toContain('relation')
|
|
1075
|
-
expect(tags).toContain('insert')
|
|
1076
|
-
expect(tags).toContain('commit')
|
|
1077
|
-
|
|
1078
|
-
// validate BEGIN has commitLsn as string (not BigInt)
|
|
1079
|
-
const begin = data.find((m: any) => m.tag === 'begin')
|
|
1080
|
-
expect(typeof begin.commitLsn).toBe('string')
|
|
1081
|
-
expect(begin.commitLsn).toMatch(/^[0-9A-F]+\/[0-9A-F]+$/)
|
|
1082
|
-
|
|
1083
|
-
// validate RELATION has correct structure
|
|
1084
|
-
const rel = data.find((m: any) => m.tag === 'relation')
|
|
1085
|
-
expect(rel.schema).toBe('public')
|
|
1086
|
-
expect(rel.name).toBe('items')
|
|
1087
|
-
expect(rel.columns.length).toBe(3)
|
|
1088
|
-
|
|
1089
|
-
// validate INSERT has parsed values
|
|
1090
|
-
const ins = data.find((m: any) => m.tag === 'insert')
|
|
1091
|
-
expect(ins.relation.name).toBe('items')
|
|
1092
|
-
expect(ins.new.id).toBe('pgjs')
|
|
1093
|
-
expect(ins.new.val).toBe('42')
|
|
1094
|
-
expect(ins.new.note).toBe('postgres.js test')
|
|
1095
|
-
|
|
1096
|
-
// validate COMMIT has commitLsn and commitEndLsn
|
|
1097
|
-
const commit = data.find((m: any) => m.tag === 'commit')
|
|
1098
|
-
expect(typeof commit.commitLsn).toBe('string')
|
|
1099
|
-
expect(typeof commit.commitEndLsn).toBe('string')
|
|
1100
|
-
|
|
1101
|
-
// validate LSN ordering: commit.commitEndLsn > begin.commitLsn
|
|
1102
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1103
|
-
const { toBigInt: lsnToBigInt } = require('/Users/n8/orez/node_modules/@rocicorp/zero/out/zero-cache/src/services/change-source/pg/lsn.js')
|
|
1104
|
-
expect(lsnToBigInt(commit.commitEndLsn)).toBeGreaterThan(lsnToBigInt(begin.commitLsn))
|
|
1105
|
-
|
|
1106
|
-
// validate lexi version conversion works (storer uses this)
|
|
1107
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1108
|
-
const { versionToLexi } = require('/Users/n8/orez/node_modules/@rocicorp/zero/out/zero-cache/src/types/lexi-version.js')
|
|
1109
|
-
const beginVersion = versionToLexi(lsnToBigInt(begin.commitLsn))
|
|
1110
|
-
const commitVersion = versionToLexi(lsnToBigInt(commit.commitEndLsn))
|
|
1111
|
-
expect(typeof beginVersion).toBe('string')
|
|
1112
|
-
expect(commitVersion > beginVersion).toBe(true)
|
|
1113
|
-
|
|
1114
|
-
} finally {
|
|
1115
|
-
await regular.end()
|
|
1116
|
-
// session.end() can hang because the replication handler keeps polling.
|
|
1117
|
-
// just force-close the underlying connection by destroying the socket.
|
|
1118
|
-
await session.end({ timeout: 2 }).catch(() => {})
|
|
1119
|
-
}
|
|
1120
|
-
})
|
|
1121
|
-
|
|
1122
|
-
it('postgres.js handles concurrent regular + replication connections', { timeout: 30000 }, async () => {
|
|
1123
|
-
const pg = (await import('postgres')).default
|
|
1124
|
-
|
|
1125
|
-
const regular = pg({
|
|
1126
|
-
host: '127.0.0.1', port,
|
|
1127
|
-
user: 'user', password: 'password', database: 'postgres',
|
|
1128
|
-
max: 1,
|
|
1129
|
-
})
|
|
1130
|
-
|
|
1131
|
-
const session = pg({
|
|
1132
|
-
host: '127.0.0.1', port,
|
|
1133
|
-
user: 'user', password: 'password', database: 'postgres',
|
|
1134
|
-
max: 1,
|
|
1135
|
-
fetch_types: false,
|
|
1136
|
-
idle_timeout: null,
|
|
1137
|
-
max_lifetime: null,
|
|
1138
|
-
connection: { replication: 'database' },
|
|
1139
|
-
})
|
|
1140
|
-
|
|
1141
|
-
try {
|
|
1142
|
-
await session.unsafe(
|
|
1143
|
-
`CREATE_REPLICATION_SLOT "conc_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT`
|
|
1144
|
-
).simple()
|
|
1145
|
-
|
|
1146
|
-
const stream = session.unsafe(
|
|
1147
|
-
`START_REPLICATION SLOT "conc_test" LOGICAL 0/0 (proto_version '1', publication_names 'zero_data', messages 'true')`
|
|
1148
|
-
).execute()
|
|
1149
|
-
|
|
1150
|
-
const [readable] = await Promise.all([stream.readable(), stream.writable()])
|
|
1151
|
-
|
|
1152
|
-
const received: Buffer[] = []
|
|
1153
|
-
readable.on('data', (chunk: Buffer) => received.push(chunk))
|
|
1154
|
-
|
|
1155
|
-
await new Promise((r) => setTimeout(r, 300))
|
|
1156
|
-
|
|
1157
|
-
// do 5 inserts via regular connection while replication is active
|
|
1158
|
-
for (let i = 0; i < 5; i++) {
|
|
1159
|
-
await regular.unsafe(`INSERT INTO public.items (id, val) VALUES ('c${i}', ${i})`)
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
await new Promise((r) => setTimeout(r, 2000))
|
|
1163
|
-
readable.destroy()
|
|
1164
|
-
|
|
1165
|
-
// should have received XLogData frames (0x77) from replication
|
|
1166
|
-
const xlogFrames = received.filter((b) => b[0] === 0x77)
|
|
1167
|
-
expect(xlogFrames.length).toBeGreaterThan(0)
|
|
1168
|
-
|
|
1169
|
-
} finally {
|
|
1170
|
-
await regular.end()
|
|
1171
|
-
await session.end({ timeout: 2 }).catch(() => {})
|
|
1172
|
-
}
|
|
1173
|
-
})
|
|
1174
|
-
})
|
package/src/shim/hooks.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const OrigDatabase = mod.Database;
|
|
|
34
34
|
const SqliteError = mod.SqliteError;
|
|
35
35
|
function Database(...args) {
|
|
36
36
|
const db = new OrigDatabase(...args);
|
|
37
|
-
try { db.pragma('busy_timeout = 30000'); db.pragma('synchronous = normal'); } catch(e) {}
|
|
37
|
+
try { db.pragma('journal_mode = delete'); db.pragma('busy_timeout = 30000'); db.pragma('synchronous = normal'); } catch(e) {}
|
|
38
38
|
return db;
|
|
39
39
|
}
|
|
40
40
|
Database.prototype = OrigDatabase.prototype;
|
package/src/vite-plugin.ts
CHANGED
|
@@ -7,21 +7,16 @@ import type { Plugin } from 'vite'
|
|
|
7
7
|
export interface OrezPluginOptions extends Partial<ZeroLiteConfig> {
|
|
8
8
|
s3?: boolean
|
|
9
9
|
s3Port?: number
|
|
10
|
-
admin?: boolean
|
|
11
|
-
adminPort?: number
|
|
12
|
-
adminLogs?: boolean
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
export default function orez(options?: OrezPluginOptions): Plugin {
|
|
16
13
|
let stop: (() => Promise<void>) | null = null
|
|
17
14
|
let s3Server: Server | null = null
|
|
18
|
-
let adminServer: Server | null = null
|
|
19
15
|
|
|
20
16
|
return {
|
|
21
17
|
name: 'orez',
|
|
22
18
|
|
|
23
19
|
async configureServer(server) {
|
|
24
|
-
const startTime = Date.now()
|
|
25
20
|
const result = await startZeroLite(options)
|
|
26
21
|
stop = result.stop
|
|
27
22
|
|
|
@@ -33,30 +28,7 @@ export default function orez(options?: OrezPluginOptions): Plugin {
|
|
|
33
28
|
})
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
if (options?.admin && result.logStore) {
|
|
37
|
-
const { findPort } = await import('./port.js')
|
|
38
|
-
const { log } = await import('./log.js')
|
|
39
|
-
const adminPort = options.adminPort || result.config.zeroPort + 2
|
|
40
|
-
const resolvedPort = await findPort(adminPort)
|
|
41
|
-
const { startAdminServer } = await import('./admin/server.js')
|
|
42
|
-
adminServer = await startAdminServer({
|
|
43
|
-
port: resolvedPort,
|
|
44
|
-
logStore: result.logStore,
|
|
45
|
-
config: result.config,
|
|
46
|
-
zeroEnv: result.zeroEnv,
|
|
47
|
-
actions: result.actions,
|
|
48
|
-
startTime,
|
|
49
|
-
httpLog: result.httpLogStore || undefined,
|
|
50
|
-
})
|
|
51
|
-
log.orez(`admin: http://127.0.0.1:${resolvedPort}`)
|
|
52
|
-
if (result.config.adminLogs) {
|
|
53
|
-
const { resolve } = await import('node:path')
|
|
54
|
-
log.orez(`logs: ${resolve(result.config.dataDir, 'logs', 'orez.log')}`)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
31
|
server.httpServer?.on('close', async () => {
|
|
59
|
-
adminServer?.close()
|
|
60
32
|
s3Server?.close()
|
|
61
33
|
if (stop) {
|
|
62
34
|
await stop()
|
package/src/wasm-sqlite.test.ts
CHANGED
|
@@ -19,8 +19,7 @@ import { resolve } from 'node:path'
|
|
|
19
19
|
|
|
20
20
|
// import bedrock-sqlite directly (our wasm build)
|
|
21
21
|
// @ts-expect-error - CJS module
|
|
22
|
-
import
|
|
23
|
-
const { Database } = bedrockSqlite
|
|
22
|
+
import { Database } from 'bedrock-sqlite'
|
|
24
23
|
import { describe, test, expect, beforeEach, afterEach } from 'vitest'
|
|
25
24
|
|
|
26
25
|
// helper: temp db file
|