orez 0.0.48 → 0.0.50

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 (54) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +6 -56
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +0 -5
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +0 -5
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.d.ts +0 -9
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +91 -280
  11. package/dist/index.js.map +1 -1
  12. package/dist/log.d.ts +0 -9
  13. package/dist/log.d.ts.map +1 -1
  14. package/dist/log.js +1 -24
  15. package/dist/log.js.map +1 -1
  16. package/dist/mutex.d.ts.map +1 -1
  17. package/dist/mutex.js +2 -13
  18. package/dist/mutex.js.map +1 -1
  19. package/dist/pg-proxy.d.ts +2 -3
  20. package/dist/pg-proxy.d.ts.map +1 -1
  21. package/dist/pg-proxy.js +167 -377
  22. package/dist/pg-proxy.js.map +1 -1
  23. package/dist/pglite-manager.d.ts +0 -1
  24. package/dist/pglite-manager.d.ts.map +1 -1
  25. package/dist/pglite-manager.js +2 -8
  26. package/dist/pglite-manager.js.map +1 -1
  27. package/dist/replication/change-tracker.d.ts +0 -5
  28. package/dist/replication/change-tracker.d.ts.map +1 -1
  29. package/dist/replication/change-tracker.js +1 -43
  30. package/dist/replication/change-tracker.js.map +1 -1
  31. package/dist/replication/handler.d.ts.map +1 -1
  32. package/dist/replication/handler.js +16 -20
  33. package/dist/replication/handler.js.map +1 -1
  34. package/dist/vite-plugin.d.ts +0 -3
  35. package/dist/vite-plugin.d.ts.map +1 -1
  36. package/dist/vite-plugin.js +0 -24
  37. package/dist/vite-plugin.js.map +1 -1
  38. package/package.json +5 -4
  39. package/src/cli.ts +18 -70
  40. package/src/config.ts +0 -10
  41. package/src/index.ts +92 -309
  42. package/src/integration/integration.test.ts +264 -133
  43. package/src/log.ts +1 -25
  44. package/src/mutex.ts +2 -12
  45. package/src/pg-proxy.ts +187 -451
  46. package/src/pglite-manager.ts +2 -9
  47. package/src/replication/change-tracker.test.ts +114 -0
  48. package/src/replication/change-tracker.ts +4 -54
  49. package/src/replication/handler.ts +16 -33
  50. package/src/replication/pgoutput-encoder.test.ts +16 -7
  51. package/src/replication/zero-compat.test.ts +84 -211
  52. package/src/shim/hooks.mjs +1 -1
  53. package/src/vite-plugin.ts +0 -28
  54. 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
- const inserts = all.filter((x) => x.tag === 'insert')
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')
@@ -945,230 +940,108 @@ describe('zero-cache pgoutput compatibility', { timeout: 30000 }, () => {
945
940
 
946
941
  s.close()
947
942
  })
948
- })
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
943
 
963
- beforeEach(async () => {
964
- db = new PGlite()
965
- await db.waitReady
944
+ it('shard replicas/mutations changes NOT streamed (only clients)', async () => {
945
+ // zero-cache creates shard schemas (chat_0) with clients, replicas, mutations.
946
+ // if we stream replicas/mutations changes, zero-cache crashes with
947
+ // "Unknown table chat_0.replicas". only clients changes should be streamed.
966
948
  await db.exec(`
967
- CREATE TABLE public.items (
949
+ CREATE SCHEMA chat_0;
950
+ CREATE TABLE chat_0.clients (
951
+ "clientGroupID" TEXT NOT NULL,
952
+ "clientID" TEXT NOT NULL,
953
+ "lastMutationID" BIGINT,
954
+ PRIMARY KEY ("clientGroupID", "clientID")
955
+ );
956
+ CREATE TABLE chat_0.replicas (
968
957
  id TEXT PRIMARY KEY,
969
- val INTEGER,
970
- note TEXT
971
- )
958
+ version TEXT
959
+ );
960
+ CREATE TABLE chat_0.mutations (
961
+ id TEXT PRIMARY KEY,
962
+ "clientID" TEXT,
963
+ name TEXT
964
+ );
972
965
  `)
973
- await db.exec(`CREATE PUBLICATION zero_data FOR ALL TABLES`)
974
966
  await installChangeTracking(db)
975
967
 
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
- }
968
+ const s = await stream()
969
+ const q = s.messages
1048
970
 
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
- })
971
+ // insert into all three shard tables + a public table
972
+ await db.exec(
973
+ `INSERT INTO chat_0.clients ("clientGroupID", "clientID", "lastMutationID") VALUES ('cg1', 'c1', 1)`
974
+ )
975
+ await db.exec(`INSERT INTO chat_0.replicas (id, version) VALUES ('r1', 'v1')`)
976
+ await db.exec(
977
+ `INSERT INTO chat_0.mutations (id, "clientID", name) VALUES ('m1', 'c1', 'send')`
978
+ )
979
+ await db.exec(`INSERT INTO public.foo (id) VALUES ('normal')`)
1061
980
 
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(() => {})
981
+ // collect all inserts for a few seconds
982
+ const inserts: ZcInsert[] = []
983
+ const deadline = Date.now() + 4000
984
+ while (Date.now() < deadline) {
985
+ const m = await q.dequeue(1500).catch(() => null)
986
+ if (!m) break
987
+ if (m.tag === 'insert') inserts.push(m as ZcInsert)
1119
988
  }
1120
- })
1121
-
1122
- it('postgres.js handles concurrent regular + replication connections', { timeout: 30000 }, async () => {
1123
- const pg = (await import('postgres')).default
1124
989
 
1125
- const regular = pg({
1126
- host: '127.0.0.1', port,
1127
- user: 'user', password: 'password', database: 'postgres',
1128
- max: 1,
1129
- })
990
+ // should see clients + foo inserts, but NOT replicas or mutations
991
+ const streamedTables = inserts.map((i) => `${i.relation.schema}.${i.relation.name}`)
992
+ expect(streamedTables).toContain('public.foo')
993
+ expect(streamedTables).toContain('chat_0.clients')
994
+ expect(streamedTables).not.toContain('chat_0.replicas')
995
+ expect(streamedTables).not.toContain('chat_0.mutations')
1130
996
 
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
- })
997
+ s.close()
998
+ })
1140
999
 
1141
- try {
1142
- await session.unsafe(
1143
- `CREATE_REPLICATION_SLOT "conc_test" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT`
1144
- ).simple()
1000
+ it('shard clients table created AFTER replication starts still gets tracked', async () => {
1001
+ // zero-cache creates shard schemas after the replication connection is live.
1002
+ // the poll loop must detect new shard tables and install triggers dynamically.
1003
+ const s = await stream()
1004
+ const q = s.messages
1145
1005
 
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()
1006
+ // give replication time to start polling
1007
+ await new Promise((r) => setTimeout(r, 300))
1149
1008
 
1150
- const [readable] = await Promise.all([stream.readable(), stream.writable()])
1009
+ // now create shard schema (simulating zero-cache's DDL during initial sync)
1010
+ await db.exec(`
1011
+ CREATE SCHEMA chat_0;
1012
+ CREATE TABLE chat_0.clients (
1013
+ "clientGroupID" TEXT NOT NULL,
1014
+ "clientID" TEXT NOT NULL,
1015
+ "lastMutationID" BIGINT,
1016
+ PRIMARY KEY ("clientGroupID", "clientID")
1017
+ );
1018
+ `)
1151
1019
 
1152
- const received: Buffer[] = []
1153
- readable.on('data', (chunk: Buffer) => received.push(chunk))
1020
+ // wait for poll loop to detect new table (rescan every ~10s)
1021
+ await new Promise((r) => setTimeout(r, 12000))
1154
1022
 
1155
- await new Promise((r) => setTimeout(r, 300))
1023
+ // insert data that should be captured
1024
+ await db.exec(
1025
+ `INSERT INTO chat_0.clients ("clientGroupID", "clientID", "lastMutationID") VALUES ('cg1', 'c1', 42)`
1026
+ )
1156
1027
 
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})`)
1028
+ // the insert should appear in the replication stream
1029
+ let found = false
1030
+ const deadline = Date.now() + 5000
1031
+ while (Date.now() < deadline) {
1032
+ const m = await q.dequeue(2000).catch(() => null)
1033
+ if (!m) break
1034
+ if (m.tag === 'insert') {
1035
+ const ins = m as ZcInsert
1036
+ if (ins.relation.schema === 'chat_0' && ins.relation.name === 'clients') {
1037
+ found = true
1038
+ break
1039
+ }
1160
1040
  }
1041
+ }
1161
1042
 
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)
1043
+ expect(found).toBe(true)
1168
1044
 
1169
- } finally {
1170
- await regular.end()
1171
- await session.end({ timeout: 2 }).catch(() => {})
1172
- }
1045
+ s.close()
1173
1046
  })
1174
1047
  })
@@ -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;
@@ -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()
@@ -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 bedrockSqlite from 'bedrock-sqlite'
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