orez 0.2.24 → 0.2.26

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 (138) hide show
  1. package/dist/cf-do/test-protocol.d.ts +11 -0
  2. package/dist/cf-do/test-protocol.d.ts.map +1 -0
  3. package/dist/cf-do/test-protocol.js +137 -0
  4. package/dist/cf-do/test-protocol.js.map +1 -0
  5. package/dist/cf-do/watermark.d.ts +21 -0
  6. package/dist/cf-do/watermark.d.ts.map +1 -0
  7. package/dist/cf-do/watermark.js +93 -0
  8. package/dist/cf-do/watermark.js.map +1 -0
  9. package/dist/cf-do/worker.d.ts +91 -0
  10. package/dist/cf-do/worker.d.ts.map +1 -0
  11. package/dist/cf-do/worker.js +813 -0
  12. package/dist/cf-do/worker.js.map +1 -0
  13. package/dist/config.d.ts +4 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +1 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/do-sql-tracking.d.ts +6 -0
  18. package/dist/do-sql-tracking.d.ts.map +1 -0
  19. package/dist/do-sql-tracking.js +14 -0
  20. package/dist/do-sql-tracking.js.map +1 -0
  21. package/dist/index.d.ts +2 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +69 -23
  24. package/dist/index.js.map +1 -1
  25. package/dist/pg-proxy-browser.js +6 -6
  26. package/dist/pg-proxy-browser.js.map +1 -1
  27. package/dist/pg-proxy-do-backend.d.ts +128 -0
  28. package/dist/pg-proxy-do-backend.d.ts.map +1 -0
  29. package/dist/pg-proxy-do-backend.js +6292 -0
  30. package/dist/pg-proxy-do-backend.js.map +1 -0
  31. package/dist/pglite-ipc.d.ts +3 -0
  32. package/dist/pglite-ipc.d.ts.map +1 -1
  33. package/dist/pglite-ipc.js +34 -12
  34. package/dist/pglite-ipc.js.map +1 -1
  35. package/dist/pglite-web-proxy.d.ts +3 -0
  36. package/dist/pglite-web-proxy.d.ts.map +1 -1
  37. package/dist/pglite-web-proxy.js +50 -7
  38. package/dist/pglite-web-proxy.js.map +1 -1
  39. package/dist/query-rewrites.d.ts +2 -0
  40. package/dist/query-rewrites.d.ts.map +1 -0
  41. package/dist/query-rewrites.js +140 -0
  42. package/dist/query-rewrites.js.map +1 -0
  43. package/dist/replication/change-tracker.d.ts.map +1 -1
  44. package/dist/replication/change-tracker.js +18 -1
  45. package/dist/replication/change-tracker.js.map +1 -1
  46. package/dist/replication/handler.d.ts.map +1 -1
  47. package/dist/replication/handler.js +7 -2
  48. package/dist/replication/handler.js.map +1 -1
  49. package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
  50. package/dist/replication/pgoutput-encoder.js +72 -30
  51. package/dist/replication/pgoutput-encoder.js.map +1 -1
  52. package/dist/worker/browser-build-config.d.ts.map +1 -1
  53. package/dist/worker/browser-build-config.js +2 -1
  54. package/dist/worker/browser-build-config.js.map +1 -1
  55. package/dist/worker/cf-patches.d.ts +5 -2
  56. package/dist/worker/cf-patches.d.ts.map +1 -1
  57. package/dist/worker/cf-patches.js +238 -4
  58. package/dist/worker/cf-patches.js.map +1 -1
  59. package/dist/worker/shims/node-stub.d.ts +35 -0
  60. package/dist/worker/shims/node-stub.d.ts.map +1 -1
  61. package/dist/worker/shims/node-stub.js +53 -1
  62. package/dist/worker/shims/node-stub.js.map +1 -1
  63. package/dist/worker/shims/oxfmt.d.ts +4 -0
  64. package/dist/worker/shims/oxfmt.d.ts.map +1 -0
  65. package/dist/worker/shims/oxfmt.js +4 -0
  66. package/dist/worker/shims/oxfmt.js.map +1 -0
  67. package/dist/worker/shims/postgres-socket.js +1 -1
  68. package/dist/worker/shims/postgres-socket.js.map +1 -1
  69. package/dist/worker/shims/sqlite.d.ts +1 -0
  70. package/dist/worker/shims/sqlite.d.ts.map +1 -1
  71. package/dist/worker/shims/sqlite.js +229 -9
  72. package/dist/worker/shims/sqlite.js.map +1 -1
  73. package/dist/worker/shims/ws.d.ts.map +1 -1
  74. package/dist/worker/shims/ws.js +45 -0
  75. package/dist/worker/shims/ws.js.map +1 -1
  76. package/dist/worker/shims/zero-process-env.d.ts +2 -0
  77. package/dist/worker/shims/zero-process-env.d.ts.map +1 -0
  78. package/dist/worker/shims/zero-process-env.js +9 -0
  79. package/dist/worker/shims/zero-process-env.js.map +1 -0
  80. package/dist/worker/zero-cache-embed-cf.d.ts +29 -12
  81. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  82. package/dist/worker/zero-cache-embed-cf.js +83 -14
  83. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  84. package/package.json +6 -2
  85. package/src/cf-do/.wrangler/cache/cf.json +1 -0
  86. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  87. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  88. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  89. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  90. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  91. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  92. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  93. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  94. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  95. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  96. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  97. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  98. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  99. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  100. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  101. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  102. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  103. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  104. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  105. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  106. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  107. package/src/cf-do/ARCHITECTURE.md +83 -0
  108. package/src/cf-do/watermark.test.ts +103 -0
  109. package/src/cf-do/watermark.ts +118 -0
  110. package/src/cf-do/worker.ts +1033 -0
  111. package/src/cf-do/wrangler.toml +11 -0
  112. package/src/config.ts +5 -0
  113. package/src/do-sql-tracking.test.ts +19 -0
  114. package/src/do-sql-tracking.ts +19 -0
  115. package/src/index.ts +76 -28
  116. package/src/pg-proxy-browser.ts +6 -6
  117. package/src/pg-proxy-do-backend.test.ts +3890 -0
  118. package/src/pg-proxy-do-backend.ts +7157 -0
  119. package/src/pglite-ipc.test.ts +17 -0
  120. package/src/pglite-ipc.ts +31 -12
  121. package/src/pglite-web-proxy.test.ts +57 -0
  122. package/src/pglite-web-proxy.ts +48 -7
  123. package/src/replication/change-tracker.ts +16 -1
  124. package/src/replication/handler.test.ts +35 -0
  125. package/src/replication/handler.ts +7 -2
  126. package/src/replication/pgoutput-encoder.test.ts +71 -2
  127. package/src/replication/pgoutput-encoder.ts +65 -30
  128. package/src/worker/browser-build-config.test.ts +12 -0
  129. package/src/worker/browser-build-config.ts +2 -1
  130. package/src/worker/cf-patches.ts +274 -4
  131. package/src/worker/shims/node-stub.ts +53 -1
  132. package/src/worker/shims/oxfmt.ts +3 -0
  133. package/src/worker/shims/postgres-socket.ts +1 -1
  134. package/src/worker/shims/sqlite.test.ts +145 -0
  135. package/src/worker/shims/sqlite.ts +256 -9
  136. package/src/worker/shims/ws.ts +45 -0
  137. package/src/worker/shims/zero-process-env.ts +11 -0
  138. package/src/worker/zero-cache-embed-cf.ts +114 -18
@@ -39,6 +39,9 @@ export interface SqlStorageLike {
39
39
  transactionSync?<T>(fn: () => T): T
40
40
  }
41
41
 
42
+ type SqliteConnectionRole = 'default' | 'replica-writer'
43
+ const activeSnapshotPrefixes = new Set<string>()
44
+
42
45
  // -- SqliteError --
43
46
 
44
47
  export class SqliteError extends Error {
@@ -109,6 +112,178 @@ function serializeSqliteParams(params: unknown[]): SqlStorageValue[] {
109
112
  return params.map(serializeValue)
110
113
  }
111
114
 
115
+ function quoteIdentifier(value: string): string {
116
+ return `"${value.replace(/"/g, '""')}"`
117
+ }
118
+
119
+ function snapshotTableName(prefix: string, table: string): string {
120
+ return `${prefix}_${table.replace(/[^A-Za-z0-9_]/g, '_')}`
121
+ }
122
+
123
+ function isSnapshotInternalTable(name: string): boolean {
124
+ return name.startsWith('_orez_snapshot_')
125
+ }
126
+
127
+ function createSnapshotPrefix(): string {
128
+ const uuid =
129
+ typeof globalThis.crypto?.randomUUID === 'function'
130
+ ? globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 16)
131
+ : Math.random().toString(36).slice(2, 18)
132
+ return `_orez_snapshot_${Date.now().toString(36)}_${uuid}`
133
+ }
134
+
135
+ function isActiveSnapshotTable(name: string): boolean {
136
+ for (const prefix of activeSnapshotPrefixes) {
137
+ if (name.startsWith(`${prefix}_`)) return true
138
+ }
139
+ return false
140
+ }
141
+
142
+ function cleanupInactiveSnapshotTables(sql: SqlStorageLike): void {
143
+ try {
144
+ const rows = sql
145
+ .exec(
146
+ `SELECT name FROM sqlite_master
147
+ WHERE type = 'table'
148
+ AND name LIKE '_orez_snapshot_%'`
149
+ )
150
+ .toArray()
151
+ for (const row of rows) {
152
+ const name = String(row.name ?? '')
153
+ if (!isSnapshotInternalTable(name) || isActiveSnapshotTable(name)) continue
154
+ try {
155
+ sql.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(name)}`)
156
+ } catch {}
157
+ }
158
+ } catch {}
159
+ }
160
+
161
+ function shouldSnapshotTable(name: string): boolean {
162
+ return (
163
+ name !== '__miniflare_do_name' &&
164
+ name !== 'storage' &&
165
+ name !== 'sqlite_stat1' &&
166
+ !isSnapshotInternalTable(name)
167
+ )
168
+ }
169
+
170
+ function currentConnectionRole(): SqliteConnectionRole {
171
+ return (globalThis as any).__orez_zero_sqlite_role === 'replica-writer'
172
+ ? 'replica-writer'
173
+ : 'default'
174
+ }
175
+
176
+ function isSqliteCatalogQuery(sql: string): boolean {
177
+ return /\bsqlite_(?:master|schema)\b/i.test(sql)
178
+ }
179
+
180
+ function hasSnapshotCatalogName(row: Record<string, unknown>): boolean {
181
+ for (const key of ['name', 'tbl_name', 'table', 'tableName']) {
182
+ const value = row[key]
183
+ if (typeof value === 'string' && isSnapshotInternalTable(value)) return true
184
+ }
185
+ return false
186
+ }
187
+
188
+ function filterSnapshotCatalogRows<T>(sql: string, rows: T[]): T[] {
189
+ if (!isSqliteCatalogQuery(sql)) return rows
190
+ return rows.filter((row) => !hasSnapshotCatalogName(row as Record<string, unknown>))
191
+ }
192
+
193
+ function replaceIdentifierOutsideLiterals(
194
+ sql: string,
195
+ identifier: string,
196
+ replacement: string
197
+ ): string {
198
+ let out = ''
199
+ let i = 0
200
+
201
+ while (i < sql.length) {
202
+ const ch = sql[i]
203
+ const next = sql[i + 1]
204
+
205
+ if (ch === "'") {
206
+ const start = i
207
+ i++
208
+ while (i < sql.length) {
209
+ if (sql[i] === "'" && sql[i + 1] === "'") {
210
+ i += 2
211
+ continue
212
+ }
213
+ if (sql[i] === "'") {
214
+ i++
215
+ break
216
+ }
217
+ i++
218
+ }
219
+ out += sql.slice(start, i)
220
+ continue
221
+ }
222
+
223
+ if (ch === '"') {
224
+ const start = i
225
+ let value = ''
226
+ i++
227
+ while (i < sql.length) {
228
+ if (sql[i] === '"' && sql[i + 1] === '"') {
229
+ value += '"'
230
+ i += 2
231
+ continue
232
+ }
233
+ if (sql[i] === '"') {
234
+ i++
235
+ break
236
+ }
237
+ value += sql[i]
238
+ i++
239
+ }
240
+ out += value === identifier ? quoteIdentifier(replacement) : sql.slice(start, i)
241
+ continue
242
+ }
243
+
244
+ if (ch === '-' && next === '-') {
245
+ const start = i
246
+ i += 2
247
+ while (i < sql.length && sql[i] !== '\n') i++
248
+ out += sql.slice(start, i)
249
+ continue
250
+ }
251
+
252
+ if (ch === '/' && next === '*') {
253
+ const start = i
254
+ i += 2
255
+ while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) i++
256
+ i = Math.min(sql.length, i + 2)
257
+ out += sql.slice(start, i)
258
+ continue
259
+ }
260
+
261
+ if (/[A-Za-z_]/.test(ch)) {
262
+ const start = i
263
+ i++
264
+ while (i < sql.length && /[A-Za-z0-9_]/.test(sql[i])) i++
265
+ const word = sql.slice(start, i)
266
+ out += word === identifier ? replacement : word
267
+ continue
268
+ }
269
+
270
+ out += ch
271
+ i++
272
+ }
273
+
274
+ return out
275
+ }
276
+
277
+ function rewriteSQLTables(sql: string, tables: Map<string, string>): string {
278
+ let rewritten = sql
279
+ const names = [...tables.keys()].sort((a, b) => b.length - a.length)
280
+ for (const name of names) {
281
+ const snapshot = tables.get(name)!
282
+ rewritten = replaceIdentifierOutsideLiterals(rewritten, name, snapshot)
283
+ }
284
+ return rewritten
285
+ }
286
+
112
287
  // -- Statement --
113
288
 
114
289
  export class Statement<T = Record<string, SqlStorageValue>> {
@@ -193,7 +368,9 @@ export class Statement<T = Record<string, SqlStorageValue>> {
193
368
  upper.startsWith('ROLLBACK') ||
194
369
  upper === 'END' ||
195
370
  upper.startsWith('END ')
196
- const { sql, values } = this.#resolveParams(params)
371
+ const resolved = this.#resolveParams(params)
372
+ const sql = this.#db._rewriteForSnapshot(resolved.sql)
373
+ const values = resolved.values
197
374
  const cursor =
198
375
  isTxCmd && values.length === 0
199
376
  ? this.#db._execTransactionAware(sql, this.#sql)
@@ -213,9 +390,11 @@ export class Statement<T = Record<string, SqlStorageValue>> {
213
390
  return pragma.result.toArray()[0] as T
214
391
  }
215
392
 
216
- const { sql, values } = this.#resolveParams(params)
393
+ const resolved = this.#resolveParams(params)
394
+ const sql = this.#db._rewriteForSnapshot(resolved.sql)
395
+ const values = resolved.values
217
396
  const cursor = this.#sql.exec(sql, ...values)
218
- const rows = cursor.toArray()
397
+ const rows = filterSnapshotCatalogRows(sql, cursor.toArray())
219
398
  return (rows[0] as T) ?? undefined
220
399
  }
221
400
 
@@ -228,9 +407,11 @@ export class Statement<T = Record<string, SqlStorageValue>> {
228
407
  return pragma.result.toArray() as T[]
229
408
  }
230
409
 
231
- const { sql, values } = this.#resolveParams(params)
410
+ const resolved = this.#resolveParams(params)
411
+ const sql = this.#db._rewriteForSnapshot(resolved.sql)
412
+ const values = resolved.values
232
413
  const cursor = this.#sql.exec(sql, ...values)
233
- return cursor.toArray() as T[]
414
+ return filterSnapshotCatalogRows(sql, cursor.toArray()) as T[]
234
415
  }
235
416
 
236
417
  iterate(...params: unknown[]): IterableIterator<T> {
@@ -271,7 +452,7 @@ export class Statement<T = Record<string, SqlStorageValue>> {
271
452
  /** columns() returns column name metadata */
272
453
  columns(): Array<{ name: string; column: string | null; table: string | null }> {
273
454
  // execute a dummy query to get column names
274
- const cursor = this.#sql.exec(this.source)
455
+ const cursor = this.#sql.exec(this.#db._rewriteForSnapshot(this.source))
275
456
  return cursor.columnNames.map((name) => ({
276
457
  name,
277
458
  column: null,
@@ -295,6 +476,10 @@ export class Database {
295
476
  #sql: SqlStorageLike
296
477
  #open: boolean
297
478
  #inTransaction: boolean
479
+ #snapshotTables: Map<string, string> | null
480
+ #snapshotPrefix: string
481
+ #snapshotCounter: number
482
+ #connectionRole: SqliteConnectionRole
298
483
 
299
484
  constructor(sqlOrFilename: SqlStorageLike | string, _options?: { readonly?: boolean }) {
300
485
  if (typeof sqlOrFilename === 'string') {
@@ -316,6 +501,12 @@ export class Database {
316
501
  }
317
502
  this.#open = true
318
503
  this.#inTransaction = false
504
+ this.#snapshotTables = null
505
+ this.#snapshotPrefix = createSnapshotPrefix()
506
+ this.#snapshotCounter = 0
507
+ this.#connectionRole = currentConnectionRole()
508
+ activeSnapshotPrefixes.add(this.#snapshotPrefix)
509
+ cleanupInactiveSnapshotTables(this.#sql)
319
510
 
320
511
  // expose storage for StatementRunner to access transactionSync
321
512
  ;(this as any).__orez_sql = this.#sql
@@ -348,6 +539,15 @@ export class Database {
348
539
 
349
540
  // CF DO: all transaction control is no-op (DO coalesces writes automatically)
350
541
  if (sqlStorage.transactionSync) {
542
+ if (upper.startsWith('BEGIN CONCURRENT')) {
543
+ if (this.#connectionRole === 'replica-writer') {
544
+ shared.__txDepth = (shared.__txDepth || 0) + 1
545
+ } else {
546
+ this.#beginSnapshot()
547
+ }
548
+ this.#inTransaction = true
549
+ return noopCursor
550
+ }
351
551
  if (upper.startsWith('BEGIN')) {
352
552
  shared.__txDepth = (shared.__txDepth || 0) + 1
353
553
  this.#inTransaction = true
@@ -359,6 +559,7 @@ export class Database {
359
559
  upper.startsWith('END ') ||
360
560
  upper.startsWith('ROLLBACK')
361
561
  ) {
562
+ this.#dropSnapshot()
362
563
  shared.__txDepth = Math.max(0, (shared.__txDepth || 0) - 1)
363
564
  if (shared.__txDepth === 0) this.#inTransaction = false
364
565
  return noopCursor
@@ -404,6 +605,47 @@ export class Database {
404
605
  return sqlStorage.exec(sql)
405
606
  }
406
607
 
608
+ #beginSnapshot(): void {
609
+ this.#dropSnapshot()
610
+ const prefix = `${this.#snapshotPrefix}_${++this.#snapshotCounter}`
611
+ const tables = new Map<string, string>()
612
+ const rows = this.#sql
613
+ .exec(
614
+ `SELECT name FROM sqlite_master
615
+ WHERE type = 'table'
616
+ ORDER BY name`
617
+ )
618
+ .toArray()
619
+
620
+ for (const row of rows) {
621
+ const name = String(row.name ?? '')
622
+ if (!shouldSnapshotTable(name)) continue
623
+ const snapshot = snapshotTableName(prefix, name)
624
+ this.#sql.exec(
625
+ `CREATE TABLE ${quoteIdentifier(snapshot)} AS SELECT * FROM ${quoteIdentifier(name)}`
626
+ )
627
+ tables.set(name, snapshot)
628
+ }
629
+
630
+ this.#snapshotTables = tables
631
+ }
632
+
633
+ #dropSnapshot(): void {
634
+ const tables = this.#snapshotTables
635
+ if (!tables) return
636
+ this.#snapshotTables = null
637
+ for (const snapshot of tables.values()) {
638
+ try {
639
+ this.#sql.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(snapshot)}`)
640
+ } catch {}
641
+ }
642
+ }
643
+
644
+ _rewriteForSnapshot(sql: string): string {
645
+ if (!this.#snapshotTables) return sql
646
+ return rewriteSQLTables(sql, this.#snapshotTables)
647
+ }
648
+
407
649
  /** prepare a statement */
408
650
  prepare<T = Record<string, SqlStorageValue>>(sql: string): Statement<T> {
409
651
  if (!this.#open) {
@@ -535,7 +777,7 @@ export class Database {
535
777
  if (isTxCmd) {
536
778
  this._execTransactionAware(stmt, this.#sql)
537
779
  } else {
538
- this.#sql.exec(stmt)
780
+ this.#sql.exec(this._rewriteForSnapshot(stmt))
539
781
  }
540
782
  }
541
783
 
@@ -603,6 +845,8 @@ export class Database {
603
845
 
604
846
  /** close the database connection */
605
847
  close(): this {
848
+ this.#dropSnapshot()
849
+ activeSnapshotPrefixes.delete(this.#snapshotPrefix)
606
850
  this.#open = false
607
851
  return this
608
852
  }
@@ -697,6 +941,9 @@ export class StatementRunner {
697
941
  }
698
942
 
699
943
  beginConcurrent(): RunResult {
944
+ if (this.#storage?.transactionSync) {
945
+ return this.run('BEGIN CONCURRENT')
946
+ }
700
947
  return this.begin()
701
948
  }
702
949
 
@@ -709,14 +956,14 @@ export class StatementRunner {
709
956
 
710
957
  commit(): RunResult {
711
958
  if (this.#storage?.transactionSync) {
712
- return this.#txnResult
959
+ return this.run('COMMIT')
713
960
  }
714
961
  return this.run('COMMIT')
715
962
  }
716
963
 
717
964
  rollback(): RunResult {
718
965
  if (this.#storage?.transactionSync) {
719
- return this.#txnResult
966
+ return this.run('ROLLBACK')
720
967
  }
721
968
  return this.run('ROLLBACK')
722
969
  }
@@ -31,6 +31,35 @@ interface CFWebSocket {
31
31
  accept?(): void
32
32
  }
33
33
 
34
+ function debugWS(event: Record<string, unknown>): void {
35
+ try {
36
+ const log = (globalThis as any).__orez_ws_debug_log
37
+ if (typeof log === 'function') log(event)
38
+ } catch {
39
+ // debug logging must never affect websocket behavior
40
+ }
41
+ }
42
+
43
+ function summarizeWSData(data: unknown): Record<string, unknown> {
44
+ if (typeof data === 'string') {
45
+ return {
46
+ dataType: 'string',
47
+ bytes: data.length,
48
+ text: data.length > 300 ? `${data.slice(0, 300)}...` : data,
49
+ }
50
+ }
51
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
52
+ return { dataType: 'buffer', bytes: data.byteLength }
53
+ }
54
+ if (data instanceof ArrayBuffer) {
55
+ return { dataType: 'arraybuffer', bytes: data.byteLength }
56
+ }
57
+ if (ArrayBuffer.isView(data)) {
58
+ return { dataType: data.constructor.name, bytes: data.byteLength }
59
+ }
60
+ return { dataType: typeof data }
61
+ }
62
+
34
63
  // -- WebSocket shim --
35
64
  // wraps a CF WebSocket to match the ws package WebSocket API
36
65
 
@@ -198,6 +227,7 @@ class WebSocket extends EventEmitter {
198
227
  } else {
199
228
  this.#ws = urlOrSocket
200
229
  this.#url = ''
230
+ debugWS({ event: 'wrap-cf-socket', readyState: this.#ws.readyState })
201
231
  this.#setupListeners()
202
232
  }
203
233
  }
@@ -215,6 +245,7 @@ class WebSocket extends EventEmitter {
215
245
  cb?: (err?: Error) => void
216
246
  ): void {
217
247
  try {
248
+ debugWS({ event: 'send', url: this.#url, ...summarizeWSData(data) })
218
249
  if (typeof data === 'string') {
219
250
  this.#ws.send(data)
220
251
  } else if (Buffer.isBuffer(data)) {
@@ -299,15 +330,28 @@ class WebSocket extends EventEmitter {
299
330
  // error: (err: Error)
300
331
  const onMessage = (event: any) => {
301
332
  const data = event.data
333
+ debugWS({ event: 'message', url: this.#url, ...summarizeWSData(data) })
302
334
  this.emit('message', data, typeof data !== 'string')
303
335
  }
304
336
  const onClose = (event: any) => {
337
+ debugWS({
338
+ event: 'close',
339
+ url: this.#url,
340
+ code: event.code ?? 1000,
341
+ reason: event.reason ?? '',
342
+ })
305
343
  this.emit('close', event.code ?? 1000, event.reason ?? '')
306
344
  }
307
345
  const onError = (event: any) => {
346
+ debugWS({
347
+ event: 'error',
348
+ url: this.#url,
349
+ message: event?.message ?? event?.error?.message ?? 'WebSocket error',
350
+ })
308
351
  this.emit('error', event.error ?? new Error(event.message ?? 'WebSocket error'))
309
352
  }
310
353
  const onOpen = () => {
354
+ debugWS({ event: 'open', url: this.#url })
311
355
  this.emit('open')
312
356
  }
313
357
 
@@ -352,6 +396,7 @@ class WebSocketServer extends EventEmitter {
352
396
  callback: (ws: WebSocket) => void
353
397
  ): void {
354
398
  // wrap the CF WebSocket in our shim
399
+ debugWS({ event: 'handle-upgrade' })
355
400
  const ws = new WebSocket(socket as CFWebSocket)
356
401
  callback(ws)
357
402
  }
@@ -0,0 +1,11 @@
1
+ const globalProcess = ((globalThis as any).process ??= {})
2
+
3
+ globalProcess.env ??= {}
4
+ globalProcess.pid ??= 1
5
+ globalProcess.argv ??= []
6
+ globalProcess.kill ??= () => true
7
+
8
+ globalProcess.env.SINGLE_PROCESS = '1'
9
+ globalProcess.env.NODE_ENV ??= 'development'
10
+
11
+ export {}