orez 0.2.27 → 0.2.30

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 (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,147 +0,0 @@
1
- import type { PGlite } from '@electric-sql/pglite'
2
-
3
- type DbLike = Pick<PGlite, 'query' | 'exec'>
4
-
5
- const ALLOW_ALL_CONDITION = { type: 'and', conditions: [] as unknown[] }
6
- const ALLOW_ALL_POLICY = [['allow', ALLOW_ALL_CONDITION]]
7
- const DEFAULT_APP_ID = process.env.ZERO_APP_ID?.trim() || 'zero'
8
-
9
- export async function installAllowAllPermissions(
10
- db: DbLike,
11
- tables: string[]
12
- ): Promise<void> {
13
- const schemas = await findPermissionsSchemas(db)
14
- if (schemas.length === 0) {
15
- schemas.push(DEFAULT_APP_ID)
16
- }
17
-
18
- for (const schema of schemas) {
19
- const quotedSchema = '"' + schema.replace(/"/g, '""') + '"'
20
-
21
- // Bootstrap the same global permissions table shape zero-cache expects.
22
- await db.exec(`
23
- CREATE SCHEMA IF NOT EXISTS ${quotedSchema};
24
-
25
- CREATE TABLE IF NOT EXISTS ${quotedSchema}.permissions (
26
- "permissions" JSONB,
27
- "hash" TEXT,
28
- "lock" BOOL PRIMARY KEY DEFAULT true CHECK (lock)
29
- );
30
-
31
- CREATE OR REPLACE FUNCTION ${quotedSchema}.set_permissions_hash()
32
- RETURNS TRIGGER AS $$
33
- BEGIN
34
- NEW.hash = md5(NEW.permissions::text);
35
- RETURN NEW;
36
- END;
37
- $$ LANGUAGE plpgsql;
38
-
39
- DROP TRIGGER IF EXISTS on_set_permissions ON ${quotedSchema}.permissions;
40
- CREATE TRIGGER on_set_permissions
41
- BEFORE INSERT OR UPDATE ON ${quotedSchema}.permissions
42
- FOR EACH ROW
43
- EXECUTE FUNCTION ${quotedSchema}.set_permissions_hash();
44
-
45
- INSERT INTO ${quotedSchema}.permissions ("permissions")
46
- VALUES (NULL)
47
- ON CONFLICT DO NOTHING;
48
- `)
49
-
50
- const existing = await db.query<{ permissions: unknown }>(
51
- `SELECT permissions FROM ${quotedSchema}.permissions WHERE lock = true LIMIT 1`
52
- )
53
- const existingPermissions = parsePermissions(existing.rows[0]?.permissions)
54
-
55
- const tablesToAdd = Object.fromEntries(
56
- tables.map((table) => [
57
- table,
58
- {
59
- row: {
60
- select: ALLOW_ALL_POLICY,
61
- insert: ALLOW_ALL_POLICY,
62
- update: {
63
- preMutation: ALLOW_ALL_POLICY,
64
- postMutation: ALLOW_ALL_POLICY,
65
- },
66
- delete: ALLOW_ALL_POLICY,
67
- },
68
- },
69
- ])
70
- )
71
-
72
- const permissions = {
73
- ...existingPermissions,
74
- tables: {
75
- ...(existingPermissions.tables || {}),
76
- ...tablesToAdd,
77
- },
78
- }
79
-
80
- await db.query(
81
- `UPDATE ${quotedSchema}.permissions SET permissions = $1 WHERE lock = true`,
82
- [JSON.stringify(permissions)]
83
- )
84
- }
85
- }
86
-
87
- export async function hasNonNullPermissions(db: DbLike): Promise<boolean> {
88
- const schemas = await findPermissionsSchemas(db)
89
- for (const schema of schemas) {
90
- const quotedSchema = '"' + schema.replace(/"/g, '""') + '"'
91
- const result = await db.query<{ has_permissions: boolean }>(
92
- `SELECT (permissions IS NOT NULL) AS has_permissions
93
- FROM ${quotedSchema}.permissions
94
- WHERE lock = true
95
- LIMIT 1`
96
- )
97
- if (result.rows[0]?.has_permissions) return true
98
- }
99
- return false
100
- }
101
-
102
- export async function ensureTablesInPublications(
103
- db: DbLike,
104
- tables: string[]
105
- ): Promise<void> {
106
- const pubs = await db.query<{ pubname: string }>(
107
- `SELECT pubname
108
- FROM pg_publication
109
- WHERE pubname NOT LIKE '%metadata%'
110
- ORDER BY pubname`
111
- )
112
- for (const { pubname } of pubs.rows) {
113
- const quotedPub = '"' + pubname.replace(/"/g, '""') + '"'
114
- for (const table of tables) {
115
- const quotedTable = '"' + table.replace(/"/g, '""') + '"'
116
- await db
117
- .exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public".${quotedTable}`)
118
- .catch(() => {})
119
- }
120
- }
121
- }
122
-
123
- function parsePermissions(value: unknown): { tables?: Record<string, unknown> } {
124
- if (!value) return {}
125
- if (typeof value === 'string') {
126
- try {
127
- return JSON.parse(value)
128
- } catch {
129
- return {}
130
- }
131
- }
132
- if (typeof value === 'object') return value as { tables?: Record<string, unknown> }
133
- return {}
134
- }
135
-
136
- async function findPermissionsSchemas(db: DbLike): Promise<string[]> {
137
- const result = await db.query<{ schemaname: string }>(
138
- `SELECT schemaname
139
- FROM pg_tables
140
- WHERE tablename = 'permissions'
141
- AND schemaname NOT IN ('pg_catalog', 'information_schema')
142
- AND schemaname NOT LIKE 'pg_%'
143
- ORDER BY CASE WHEN schemaname = $1 THEN 0 ELSE 1 END, schemaname`,
144
- [DEFAULT_APP_ID]
145
- )
146
- return result.rows.map((r) => r.schemaname)
147
- }
@@ -1,46 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { pathToFileURL } from 'node:url'
4
-
5
- import type { OrezConfig } from './config.js'
6
-
7
- const CONFIG_FILES = ['orez.config.ts', 'orez.config.js', 'orez.config.mjs']
8
-
9
- export async function loadConfigFile(cwd = process.cwd()): Promise<OrezConfig> {
10
- for (const name of CONFIG_FILES) {
11
- const filePath = resolve(cwd, name)
12
- if (!existsSync(filePath)) continue
13
-
14
- try {
15
- const mod = await import(pathToFileURL(filePath).href)
16
- const config: OrezConfig = mod.default ?? mod
17
- return config
18
- } catch (err) {
19
- throw new Error(
20
- `failed to load ${name}: ${err instanceof Error ? err.message : err}`
21
- )
22
- }
23
- }
24
-
25
- return {}
26
- }
27
-
28
- /**
29
- * resolve OrezConfig aliases and convert to the shape expected by
30
- * startZeroLite() + CLI extras (s3, s3Port, disableAdmin).
31
- *
32
- * CLI args are passed as overrides — they take precedence over config file values.
33
- */
34
- export function resolveOrezConfig(
35
- fileConfig: OrezConfig,
36
- cliOverrides: Partial<OrezConfig> = {}
37
- ): OrezConfig {
38
- // merge: file < cli (undefined cli values don't override)
39
- const merged: OrezConfig = { ...fileConfig }
40
- for (const [k, v] of Object.entries(cliOverrides)) {
41
- if (v !== undefined) {
42
- ;(merged as Record<string, unknown>)[k] = v
43
- }
44
- }
45
- return merged
46
- }
package/src/log.ts DELETED
@@ -1,96 +0,0 @@
1
- import type { LogStore } from './admin/log-store.js'
2
- import type { LogLevel } from './config.js'
3
-
4
- const RESET = '\x1b[0m'
5
- const BOLD = '\x1b[1m'
6
- const DIM = '\x1b[2m'
7
-
8
- const COLORS = {
9
- cyan: '\x1b[36m',
10
- green: '\x1b[32m',
11
- yellow: '\x1b[33m',
12
- magenta: '\x1b[35m',
13
- blue: '\x1b[34m',
14
- } as const
15
-
16
- const LEVEL_PRIORITY: Record<LogLevel, number> = {
17
- error: 0,
18
- warn: 1,
19
- info: 2,
20
- debug: 3,
21
- }
22
-
23
- let currentLevel: LogLevel = 'warn'
24
- let logStore: LogStore | undefined
25
-
26
- export function setLogLevel(level: LogLevel) {
27
- currentLevel = level
28
- }
29
-
30
- /** hook up logStore for admin dashboard observability */
31
- export function setLogStore(store: LogStore | undefined) {
32
- logStore = store
33
- }
34
-
35
- function prefix(label: string, color: string): string {
36
- return `${BOLD}${color}[${label}]${RESET}`
37
- }
38
-
39
- /** format a port number with matching dim color */
40
- export function port(n: number, color: keyof typeof COLORS): string {
41
- return `${DIM}${COLORS[color]}:${n}${RESET}`
42
- }
43
-
44
- /** format a url with green color */
45
- export function url(u: string): string {
46
- return `${COLORS.green}${u}${RESET}`
47
- }
48
-
49
- // map logger labels to logStore source names
50
- const LABEL_TO_SOURCE: Record<string, string> = {
51
- orez: 'orez',
52
- 'orez:pg': 'orez',
53
- pglite: 'pglite',
54
- 'pg-proxy': 'proxy',
55
- 'orez:zero': 'zero',
56
- 'orez:s3': 's3',
57
- 'orez:repl': 'proxy',
58
- }
59
-
60
- function makeLogger(label: string, color: string, level: LogLevel = 'info') {
61
- const p = prefix(label, color)
62
- const source = LABEL_TO_SOURCE[label] || 'orez'
63
- // zero logs are handled specially in startZeroCache with better level detection
64
- const skipLogStore = source === 'zero'
65
- return (...args: unknown[]) => {
66
- const shouldLog = LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[currentLevel]
67
- if (shouldLog) {
68
- console.info(p, ...args)
69
- }
70
- // push to logStore for non-debug messages, or debug only if console level allows it.
71
- // debug-level logs from the poll loop are very high volume and bloat the store.
72
- if (logStore && !skipLogStore && (level !== 'debug' || shouldLog)) {
73
- const msg = args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ')
74
- logStore.push(source, level, msg)
75
- }
76
- }
77
- }
78
-
79
- export const log = {
80
- orez: makeLogger('orez', COLORS.cyan, 'warn'),
81
- pg: makeLogger('orez:pg', COLORS.green, 'warn'),
82
- pglite: makeLogger('pglite', COLORS.green, 'warn'),
83
- proxy: makeLogger('pg-proxy', COLORS.yellow, 'warn'),
84
- zero: makeLogger('orez:zero', COLORS.magenta, 'warn'),
85
- s3: makeLogger('orez:s3', COLORS.blue, 'warn'),
86
- repl: makeLogger('orez:repl', COLORS.yellow, 'warn'),
87
- debug: {
88
- orez: makeLogger('orez', COLORS.cyan, 'debug'),
89
- pg: makeLogger('orez:pg', COLORS.green, 'debug'),
90
- pglite: makeLogger('pglite', COLORS.green, 'debug'),
91
- proxy: makeLogger('pg-proxy', COLORS.yellow, 'debug'),
92
- zero: makeLogger('orez:zero', COLORS.magenta, 'debug'),
93
- s3: makeLogger('orez:s3', COLORS.blue, 'debug'),
94
- repl: makeLogger('orez:repl', COLORS.yellow, 'debug'),
95
- },
96
- }
package/src/mutex.ts DELETED
@@ -1,47 +0,0 @@
1
- // simple mutex for serializing pglite access
2
- // uses head-index instead of Array.shift() for O(1) release
3
- export class Mutex {
4
- private locked = false
5
- private queue: Array<() => void> = []
6
- private head = 0
7
-
8
- /** check if the mutex is currently held (non-blocking, no side effects) */
9
- get isLocked(): boolean {
10
- return this.locked
11
- }
12
-
13
- async acquire(): Promise<void> {
14
- if (!this.locked) {
15
- this.locked = true
16
- return
17
- }
18
- return new Promise<void>((resolve) => {
19
- this.queue.push(resolve)
20
- })
21
- }
22
-
23
- // non-blocking acquire: returns true if lock was obtained, false otherwise
24
- tryAcquire(): boolean {
25
- if (!this.locked) {
26
- this.locked = true
27
- return true
28
- }
29
- return false
30
- }
31
-
32
- release(): void {
33
- if (this.head < this.queue.length) {
34
- const next = this.queue[this.head++]!
35
- // compact periodically to prevent unbounded array growth
36
- if (this.head > 64) {
37
- this.queue = this.queue.slice(this.head)
38
- this.head = 0
39
- }
40
- next()
41
- } else {
42
- this.queue = []
43
- this.head = 0
44
- this.locked = false
45
- }
46
- }
47
- }
@@ -1,233 +0,0 @@
1
- /**
2
- * regression test for the explicit singleDb option in createBrowserProxy.
3
- *
4
- * background: orez-web (in soot) wraps a single PGlite worker in three
5
- * distinct port-proxy façades (one per database role) and hands them to
6
- * createBrowserProxy. before this fix, mutex coalescing relied on
7
- * `instances.postgres === instances.cvr` reference equality — which fails
8
- * for distinct façades, leaving 3 separate mutexes guarding a single
9
- * underlying PGlite. that allows concurrent extended-protocol sequences on
10
- * one shared session, racing named-statement slots and replication state.
11
- *
12
- * the explicit `config.singleDb` option forces mutex coalescing regardless
13
- * of object identity. this test pins that contract: with singleDb=true and
14
- * three distinct façades over one PGlite, concurrent calls must serialize.
15
- */
16
-
17
- import { PGlite } from '@electric-sql/pglite'
18
- import postgres from 'postgres'
19
- import { afterAll, beforeAll, describe, expect, test } from 'vitest'
20
-
21
- import { createBrowserProxy } from './pg-proxy-browser.js'
22
- import { createSocketFactory } from './worker/shims/postgres-socket.js'
23
-
24
- import type { PGliteInstances } from './pglite-manager.js'
25
-
26
- /**
27
- * thin façade that re-exposes a PGlite's surface as a *distinct* object
28
- * reference. simulates what orez-web does (it wraps the same underlying
29
- * PGlite worker in three port-proxy objects, one per database role).
30
- */
31
- function makeFacade(real: PGlite, label: string) {
32
- const facade: any = {
33
- _label: label,
34
- closed: false,
35
- ready: true,
36
- get waitReady() {
37
- return real.waitReady
38
- },
39
- query: (sql: string, params?: any[]) => real.query(sql, params as any),
40
- exec: (sql: string) => real.exec(sql),
41
- execProtocolRaw: (data: Uint8Array, options?: any) =>
42
- real.execProtocolRaw(data, options),
43
- listen: () => Promise.resolve(async () => {}),
44
- close: () => Promise.resolve(),
45
- }
46
- return facade as PGlite
47
- }
48
-
49
- function createSql(
50
- proxy: ReturnType<typeof createBrowserProxy> extends Promise<infer T> ? T : never
51
- ) {
52
- return postgres({
53
- socket: createSocketFactory((port) => proxy.handleConnection(port)),
54
- database: 'postgres',
55
- username: 'u',
56
- password: '',
57
- host: '127.0.0.1',
58
- port: 0,
59
- ssl: false,
60
- max: 1,
61
- no_subscribe: true,
62
- } as any)
63
- }
64
-
65
- function deferred<T>() {
66
- let resolve!: (value: T | PromiseLike<T>) => void
67
- let reject!: (reason?: unknown) => void
68
- const promise = new Promise<T>((res, rej) => {
69
- resolve = res
70
- reject = rej
71
- })
72
- return { promise, resolve, reject }
73
- }
74
-
75
- describe('createBrowserProxy singleDb mutex coalescing', () => {
76
- let pg: PGlite
77
-
78
- beforeAll(async () => {
79
- pg = new PGlite()
80
- await pg.waitReady
81
- }, 30_000)
82
-
83
- afterAll(async () => {
84
- await pg.close().catch(() => {})
85
- })
86
-
87
- test('reference equality on the same PGlite still coalesces (legacy path)', async () => {
88
- const instances: PGliteInstances = {
89
- postgres: pg,
90
- cvr: pg,
91
- cdb: pg,
92
- postgresReplicas: [],
93
- }
94
- const proxy = await createBrowserProxy(instances, { pgPassword: '', pgUser: 'u' })
95
- // smoke: it constructs without error. proper concurrency proof requires
96
- // pg-wire client; covered by integration tests. this guards the legacy path.
97
- expect(proxy).toBeTruthy()
98
- proxy.close()
99
- })
100
-
101
- test('distinct façades + singleDb=true coalesces; without flag they would split', async () => {
102
- const facadePg = makeFacade(pg, 'postgres')
103
- const facadeCvr = makeFacade(pg, 'cvr')
104
- const facadeCdb = makeFacade(pg, 'cdb')
105
-
106
- // sanity: the three façades are distinct refs (would defeat reference equality)
107
- expect(facadePg).not.toBe(facadeCvr)
108
- expect(facadePg).not.toBe(facadeCdb)
109
- expect(facadeCvr).not.toBe(facadeCdb)
110
-
111
- const instances: PGliteInstances = {
112
- postgres: facadePg,
113
- cvr: facadeCvr,
114
- cdb: facadeCdb,
115
- postgresReplicas: [],
116
- }
117
-
118
- // explicit singleDb=true should still build a working proxy.
119
- const proxy = await createBrowserProxy(instances, {
120
- pgPassword: '',
121
- pgUser: 'u',
122
- singleDb: true,
123
- })
124
- expect(proxy).toBeTruthy()
125
- proxy.close()
126
- })
127
-
128
- test('coordinated query/exec runs through the same per-db mutex', async () => {
129
- // proxy.query / proxy.exec exist so out-of-band JSON callers (soot's
130
- // project-server / main-thread SAB JSON channels) go through the same
131
- // mutex + txState the wire-protocol path uses. this test pins that the
132
- // API works end-to-end against a shared-PGlite façade setup; the actual
133
- // 'E'-rescue behaviour is exercised by the soot integration suite where
134
- // a wire-protocol abort populates txState first.
135
- const facadePg = makeFacade(pg, 'postgres')
136
- const facadeCvr = makeFacade(pg, 'cvr')
137
- const facadeCdb = makeFacade(pg, 'cdb')
138
- const proxy = await createBrowserProxy(
139
- {
140
- postgres: facadePg,
141
- cvr: facadeCvr,
142
- cdb: facadeCdb,
143
- postgresReplicas: [],
144
- },
145
- { pgPassword: '', pgUser: 'u', singleDb: true }
146
- )
147
-
148
- const r1 = await proxy.query('postgres', 'SELECT 1 AS ok')
149
- expect(r1.rows).toEqual([{ ok: 1 }])
150
-
151
- const r2 = await proxy.exec(
152
- 'postgres',
153
- 'CREATE TABLE IF NOT EXISTS rescue_test (id int)'
154
- )
155
- expect(Array.isArray(r2)).toBe(true)
156
-
157
- proxy.close()
158
- })
159
-
160
- test('singleDb waits for the owning transaction before serving another client', async () => {
161
- await pg.exec(`
162
- DROP TABLE IF EXISTS singledb_tx_owner;
163
- CREATE TABLE singledb_tx_owner (id int);
164
- `)
165
- const facadePg = makeFacade(pg, 'postgres')
166
- const facadeCvr = makeFacade(pg, 'cvr')
167
- const facadeCdb = makeFacade(pg, 'cdb')
168
- const proxy = await createBrowserProxy(
169
- {
170
- postgres: facadePg,
171
- cvr: facadeCvr,
172
- cdb: facadeCdb,
173
- postgresReplicas: [],
174
- },
175
- { pgPassword: '', pgUser: 'u', singleDb: true }
176
- )
177
- const sql1 = createSql(proxy)
178
- const sql2 = createSql(proxy)
179
- const releaseTx = deferred<void>()
180
- const txStarted = deferred<void>()
181
-
182
- const tx = sql1.begin(async (sql) => {
183
- await sql`INSERT INTO singledb_tx_owner VALUES (1)`
184
- txStarted.resolve()
185
- await releaseTx.promise
186
- })
187
- await txStarted.promise
188
-
189
- let readCompleted = false
190
- const read = sql2`SELECT count(*)::int AS count FROM singledb_tx_owner`.then(
191
- (rows) => {
192
- readCompleted = true
193
- return rows[0]?.count
194
- }
195
- )
196
- await new Promise((resolve) => setTimeout(resolve, 25))
197
- expect(readCompleted).toBe(false)
198
-
199
- releaseTx.resolve()
200
- await tx
201
- await expect(read).resolves.toBe(1)
202
-
203
- await sql1.end({ timeout: 1 }).catch(() => {})
204
- await sql2.end({ timeout: 1 }).catch(() => {})
205
- proxy.close()
206
- }, 10_000)
207
-
208
- test('explicit singleDb=false on distinct façades preserves split mutexes', async () => {
209
- // negative case: when caller doesn't opt in and refs are distinct, the
210
- // legacy reference-equality heuristic gives separate mutexes (the bug we
211
- // shipped around). this test pins the contract that singleDb is opt-in,
212
- // so adding it later cannot quietly break consumers that rely on split
213
- // mutexes for their three real PGlite instances.
214
- const facadePg = makeFacade(pg, 'postgres')
215
- const facadeCvr = makeFacade(pg, 'cvr')
216
- const facadeCdb = makeFacade(pg, 'cdb')
217
-
218
- const instances: PGliteInstances = {
219
- postgres: facadePg,
220
- cvr: facadeCvr,
221
- cdb: facadeCdb,
222
- postgresReplicas: [],
223
- }
224
-
225
- const proxy = await createBrowserProxy(instances, {
226
- pgPassword: '',
227
- pgUser: 'u',
228
- // singleDb omitted — defaults to false
229
- })
230
- expect(proxy).toBeTruthy()
231
- proxy.close()
232
- })
233
- })