orez 0.2.27 → 0.2.29

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