orez 0.2.25 → 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 (117) hide show
  1. package/dist/cf-do/watermark.d.ts +21 -0
  2. package/dist/cf-do/watermark.d.ts.map +1 -0
  3. package/dist/cf-do/watermark.js +93 -0
  4. package/dist/cf-do/watermark.js.map +1 -0
  5. package/dist/cf-do/worker.d.ts +48 -22
  6. package/dist/cf-do/worker.d.ts.map +1 -1
  7. package/dist/cf-do/worker.js +642 -269
  8. package/dist/cf-do/worker.js.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/do-sql-tracking.d.ts +6 -0
  12. package/dist/do-sql-tracking.d.ts.map +1 -0
  13. package/dist/do-sql-tracking.js +14 -0
  14. package/dist/do-sql-tracking.js.map +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +28 -14
  17. package/dist/index.js.map +1 -1
  18. package/dist/pg-proxy-browser.js +6 -6
  19. package/dist/pg-proxy-browser.js.map +1 -1
  20. package/dist/pg-proxy-do-backend.d.ts +96 -17
  21. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  22. package/dist/pg-proxy-do-backend.js +6033 -454
  23. package/dist/pg-proxy-do-backend.js.map +1 -1
  24. package/dist/replication/change-tracker.d.ts.map +1 -1
  25. package/dist/replication/change-tracker.js +18 -1
  26. package/dist/replication/change-tracker.js.map +1 -1
  27. package/dist/replication/handler.d.ts.map +1 -1
  28. package/dist/replication/handler.js +7 -2
  29. package/dist/replication/handler.js.map +1 -1
  30. package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
  31. package/dist/replication/pgoutput-encoder.js +72 -30
  32. package/dist/replication/pgoutput-encoder.js.map +1 -1
  33. package/dist/worker/browser-build-config.d.ts.map +1 -1
  34. package/dist/worker/browser-build-config.js +2 -1
  35. package/dist/worker/browser-build-config.js.map +1 -1
  36. package/dist/worker/cf-patches.d.ts +5 -2
  37. package/dist/worker/cf-patches.d.ts.map +1 -1
  38. package/dist/worker/cf-patches.js +238 -4
  39. package/dist/worker/cf-patches.js.map +1 -1
  40. package/dist/worker/shims/node-stub.d.ts +35 -0
  41. package/dist/worker/shims/node-stub.d.ts.map +1 -1
  42. package/dist/worker/shims/node-stub.js +53 -1
  43. package/dist/worker/shims/node-stub.js.map +1 -1
  44. package/dist/worker/shims/oxfmt.d.ts +4 -0
  45. package/dist/worker/shims/oxfmt.d.ts.map +1 -0
  46. package/dist/worker/shims/oxfmt.js +4 -0
  47. package/dist/worker/shims/oxfmt.js.map +1 -0
  48. package/dist/worker/shims/postgres-socket.js +1 -1
  49. package/dist/worker/shims/postgres-socket.js.map +1 -1
  50. package/dist/worker/shims/sqlite.d.ts +1 -0
  51. package/dist/worker/shims/sqlite.d.ts.map +1 -1
  52. package/dist/worker/shims/sqlite.js +229 -9
  53. package/dist/worker/shims/sqlite.js.map +1 -1
  54. package/dist/worker/shims/ws.d.ts.map +1 -1
  55. package/dist/worker/shims/ws.js +45 -0
  56. package/dist/worker/shims/ws.js.map +1 -1
  57. package/dist/worker/shims/zero-process-env.d.ts +2 -0
  58. package/dist/worker/shims/zero-process-env.d.ts.map +1 -0
  59. package/dist/worker/shims/zero-process-env.js +9 -0
  60. package/dist/worker/shims/zero-process-env.js.map +1 -0
  61. package/dist/worker/zero-cache-embed-cf.d.ts +29 -12
  62. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  63. package/dist/worker/zero-cache-embed-cf.js +83 -14
  64. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  65. package/package.json +6 -2
  66. package/src/cf-do/.wrangler/cache/cf.json +1 -0
  67. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  69. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  75. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  76. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  77. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  78. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  79. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  80. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  81. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  82. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  83. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  84. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  85. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  86. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  87. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  88. package/src/cf-do/ARCHITECTURE.md +83 -0
  89. package/src/cf-do/watermark.test.ts +103 -0
  90. package/src/cf-do/watermark.ts +118 -0
  91. package/src/cf-do/worker.ts +1033 -0
  92. package/src/cf-do/wrangler.toml +11 -0
  93. package/src/config.ts +1 -1
  94. package/src/do-sql-tracking.test.ts +19 -0
  95. package/src/do-sql-tracking.ts +19 -0
  96. package/src/index.ts +29 -14
  97. package/src/pg-proxy-browser.ts +6 -6
  98. package/src/pg-proxy-do-backend.test.ts +3890 -0
  99. package/src/pg-proxy-do-backend.ts +6799 -482
  100. package/src/replication/change-tracker.ts +16 -1
  101. package/src/replication/handler.test.ts +35 -0
  102. package/src/replication/handler.ts +7 -2
  103. package/src/replication/pgoutput-encoder.test.ts +71 -2
  104. package/src/replication/pgoutput-encoder.ts +65 -30
  105. package/src/worker/browser-build-config.test.ts +12 -0
  106. package/src/worker/browser-build-config.ts +2 -1
  107. package/src/worker/cf-patches.ts +274 -4
  108. package/src/worker/shims/node-stub.ts +53 -1
  109. package/src/worker/shims/oxfmt.ts +3 -0
  110. package/src/worker/shims/postgres-socket.ts +1 -1
  111. package/src/worker/shims/sqlite.test.ts +145 -0
  112. package/src/worker/shims/sqlite.ts +256 -9
  113. package/src/worker/shims/ws.ts +45 -0
  114. package/src/worker/shims/zero-process-env.ts +11 -0
  115. package/src/worker/zero-cache-embed-cf.ts +114 -18
  116. package/src/query-rewrites.test.ts +0 -30
  117. package/src/query-rewrites.ts +0 -152
@@ -20,6 +20,16 @@ export interface ChangeTrackingDb {
20
20
  query<T>(sql: string, params?: unknown[]): Promise<{ rows: T[] }>
21
21
  }
22
22
 
23
+ // PGlite returns JSONB columns as parsed objects; the DO backend returns them
24
+ // as JSON strings (it stores `row_data TEXT`). normalize once at the consumer
25
+ // boundary so callers always get an object.
26
+ function jsonRecord(value: unknown): Record<string, unknown> | null {
27
+ if (value === null || value === undefined) return null
28
+ if (typeof value === 'object') return value as Record<string, unknown>
29
+ if (typeof value !== 'string' || value === '') return null
30
+ return JSON.parse(value) as Record<string, unknown>
31
+ }
32
+
23
33
  export async function installChangeTracking(db: ChangeTrackingDb): Promise<void> {
24
34
  // use _orez schema for internal tables - survives pg_restore of public schema
25
35
  await db.exec(`CREATE SCHEMA IF NOT EXISTS _orez`)
@@ -241,7 +251,12 @@ export async function getChangesSince(
241
251
  'SELECT watermark, table_name, op, row_data, old_data FROM _orez._zero_changes WHERE watermark > $1 ORDER BY watermark LIMIT $2',
242
252
  [watermark, limit]
243
253
  )
244
- return result.rows
254
+ return result.rows.map((row) => ({
255
+ ...row,
256
+ watermark: Number(row.watermark),
257
+ row_data: jsonRecord(row.row_data),
258
+ old_data: jsonRecord(row.old_data),
259
+ }))
245
260
  }
246
261
 
247
262
  export async function purgeConsumedChanges(
@@ -174,6 +174,19 @@ describe('handleStartReplication', () => {
174
174
  return types
175
175
  }
176
176
 
177
+ function countCopyDataFrames(buf: Uint8Array): number {
178
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
179
+ let pos = 0
180
+ let count = 0
181
+ while (pos < buf.length) {
182
+ if (buf[pos] !== 0x64) return count
183
+ const len = dv.getInt32(pos + 1)
184
+ pos += 1 + len
185
+ count++
186
+ }
187
+ return count
188
+ }
189
+
177
190
  it('sends CopyBothResponse first', async () => {
178
191
  const { written, writer } = createWriter()
179
192
 
@@ -238,6 +251,28 @@ describe('handleStartReplication', () => {
238
251
  expect(insIdx).toBeLessThan(comIdx)
239
252
  })
240
253
 
254
+ it('writes one CopyData frame per socket chunk', async () => {
255
+ const { written, writer } = createWriter()
256
+
257
+ replicationPromise = handleStartReplication(
258
+ 'START_REPLICATION SLOT "s" LOGICAL 0/0',
259
+ writer,
260
+ db,
261
+ testMutex
262
+ )
263
+
264
+ await new Promise((r) => setTimeout(r, 100))
265
+ await db.exec(`INSERT INTO public.items (name, value) VALUES ('chunked', 123)`)
266
+ signalReplicationChange()
267
+ await new Promise((r) => setTimeout(r, 700))
268
+
269
+ const copyDataWrites = written.filter((msg) => msg[0] === 0x64)
270
+ expect(copyDataWrites.length).toBeGreaterThanOrEqual(4)
271
+ for (const msg of copyDataWrites) {
272
+ expect(countCopyDataFrames(msg)).toBe(1)
273
+ }
274
+ })
275
+
241
276
  it('streams UPDATE and DELETE operations', async () => {
242
277
  const { written, writer } = createWriter()
243
278
 
@@ -117,6 +117,10 @@ let _replicationWakeup: (() => void) | null = null
117
117
  * called by the proxy after executing writes on the postgres instance. */
118
118
  export function signalReplicationChange() {
119
119
  _replicationWakeup?.()
120
+ const globalWakeup = (globalThis as any).__orez_signal_replication
121
+ if (typeof globalWakeup === 'function' && globalWakeup !== _replicationWakeup) {
122
+ globalWakeup()
123
+ }
120
124
  }
121
125
 
122
126
  // cached setup results so reconnects skip the expensive mutex-holding setup phase.
@@ -1146,8 +1150,9 @@ async function streamChanges(
1146
1150
  const endLsn = nextLsn()
1147
1151
  messages.push(encodeWrappedChange(endLsn, endLsn, ts, encodeCommit(0, lsn, endLsn, ts)))
1148
1152
 
1149
- // write messages individually works for both TCP sockets and in-process
1150
- // pipes (browser pipe handler parses one message per write() call)
1153
+ // The MessagePort-backed socket delivers each write as one readable chunk.
1154
+ // zero-cache parses one replication payload per chunk, so each CopyData frame
1155
+ // must be written separately.
1151
1156
  let totalSize = 0
1152
1157
  for (const msg of messages) totalSize += msg.length
1153
1158
  log.debug.repl(
@@ -40,6 +40,24 @@ function rText(buf: Uint8Array, off: number): [string, number] {
40
40
  const str = new TextDecoder().decode(buf.subarray(off + 4, off + 4 + len))
41
41
  return [str, off + 4 + len]
42
42
  }
43
+ function rTupleTextValues(buf: Uint8Array, off: number): [Array<string | null>, number] {
44
+ const values: Array<string | null> = []
45
+ const count = r16(buf, off)
46
+ let pos = off + 2
47
+ for (let i = 0; i < count; i++) {
48
+ const kind = buf[pos++]
49
+ if (kind === 0x6e) {
50
+ values.push(null)
51
+ continue
52
+ }
53
+ expect(kind).toBe(0x74)
54
+ const len = r32(buf, pos)
55
+ pos += 4
56
+ values.push(new TextDecoder().decode(buf.subarray(pos, pos + len)))
57
+ pos += len
58
+ }
59
+ return [values, pos]
60
+ }
43
61
 
44
62
  describe('pgoutput-encoder', () => {
45
63
  describe('encodeBegin', () => {
@@ -276,6 +294,26 @@ describe('pgoutput-encoder', () => {
276
294
  }
277
295
  expect(buf[pos]).toBe(0x4e) // 'N' new tuple marker
278
296
  })
297
+
298
+ it('encodes sqlite integer booleans as postgres bool text', () => {
299
+ const boolCols: ColumnInfo[] = [
300
+ { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
301
+ { name: 'completed', typeOid: 16, typeMod: -1 },
302
+ ]
303
+ const buf = encodeUpdate(
304
+ 16384,
305
+ { id: '1', completed: 1 },
306
+ { id: '1', completed: 0 },
307
+ boolCols
308
+ )
309
+
310
+ expect(buf[5]).toBe(0x4f) // 'O' old tuple
311
+ const [oldValues, afterOld] = rTupleTextValues(buf, 6)
312
+ expect(oldValues).toEqual(['1', 'f'])
313
+ expect(buf[afterOld]).toBe(0x4e) // 'N' new tuple
314
+ const [newValues] = rTupleTextValues(buf, afterOld + 1)
315
+ expect(newValues).toEqual(['1', 't'])
316
+ })
279
317
  })
280
318
 
281
319
  describe('encodeDelete', () => {
@@ -364,6 +402,12 @@ describe('pgoutput-encoder', () => {
364
402
  const b = getTableOid('oid_test_y')
365
403
  expect(a).not.toBe(b)
366
404
  })
405
+
406
+ it('matches DoBackend catalog oids for flattened schema tables', () => {
407
+ expect(getTableOid('public.todo')).toBe(4392680)
408
+ expect(getTableOid('todo_0.clients')).toBe(9663976)
409
+ expect(getTableOid('todo_0.mutations')).toBe(8519194)
410
+ })
367
411
  })
368
412
 
369
413
  // roundtrip tests: encode with orez → parse with zero-cache's parser
@@ -384,8 +428,16 @@ describe('pgoutput-encoder', () => {
384
428
  PgoutputParser = (await import(parserPath)).PgoutputParser
385
429
  })
386
430
 
387
- function makeParser() {
388
- return new PgoutputParser(typeParsers)
431
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
432
+ function makeParser(parserOverrides: any = typeParsers) {
433
+ return new PgoutputParser(parserOverrides)
434
+ }
435
+
436
+ function makeBoolAwareParser() {
437
+ return makeParser({
438
+ getTypeParser: (oid: number) =>
439
+ oid === 16 ? (value: string) => value === 't' : String,
440
+ })
389
441
  }
390
442
 
391
443
  it('BEGIN roundtrip', () => {
@@ -490,6 +542,23 @@ describe('pgoutput-encoder', () => {
490
542
  expect(parsed.key).toBeNull()
491
543
  })
492
544
 
545
+ it('UPDATE roundtrip decodes sqlite boolean integers through zero-cache parser', () => {
546
+ const oid = getTableOid('rt.bool_update')
547
+ const cols: ColumnInfo[] = [
548
+ { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
549
+ { name: 'completed', typeOid: 16, typeMod: -1 },
550
+ ]
551
+ const parser = makeBoolAwareParser()
552
+ parser.parse(encodeRelation(oid, 'public', 'bool_update', 0x64, cols))
553
+
554
+ const parsed = parser.parse(
555
+ encodeUpdate(oid, { id: '1', completed: 1 }, { id: '1', completed: 0 }, cols)
556
+ )
557
+ expect(parsed.tag).toBe('update')
558
+ expect(parsed.old.completed).toBe(false)
559
+ expect(parsed.new.completed).toBe(true)
560
+ })
561
+
493
562
  it('DELETE roundtrip', () => {
494
563
  const oid = getTableOid('rt.del_test')
495
564
  const cols: ColumnInfo[] = [
@@ -9,21 +9,32 @@
9
9
 
10
10
  // postgres epoch: 2000-01-01 in microseconds from unix epoch
11
11
  const PG_EPOCH_MICROS = 946684800000000n
12
+ const PG_TYPE_BOOL = 16
13
+ const PG_TYPE_TIMESTAMP = 1114
14
+ const PG_TYPE_TIMESTAMPTZ = 1184
12
15
 
13
16
  // shared encoder instance - avoids per-call allocation
14
17
  const encoder = new TextEncoder()
15
18
 
16
- // table oid tracking
17
- const tableOids = new Map<string, number>()
18
- let nextOid = 16384
19
+ function flattenRelationName(tableName: string): string {
20
+ const dot = tableName.indexOf('.')
21
+ if (dot < 0) return tableName
22
+ const schema = tableName.slice(0, dot)
23
+ const name = tableName.slice(dot + 1)
24
+ if (schema === 'public') return name
25
+ if (schema === '_orez' && name === '_zero_changes') return '_zero_changes'
26
+ if (schema === '_orez' && name === '_zero_replication_slots')
27
+ return '_orez__zero_replication_slots'
28
+ if (schema === '_orez') return `_orez__${name}`
29
+ if (schema === '_zero') return `_zero_${name}`
30
+ return `${schema}_${name}`
31
+ }
19
32
 
20
33
  function getTableOid(tableName: string): number {
21
- let oid = tableOids.get(tableName)
22
- if (!oid) {
23
- oid = nextOid++
24
- tableOids.set(tableName, oid)
25
- }
26
- return oid
34
+ let hash = 0
35
+ const key = `table:${flattenRelationName(tableName)}`
36
+ for (let i = 0; i < key.length; i++) hash = (hash * 33 + key.charCodeAt(i)) >>> 0
37
+ return 50_000 + (hash % 10_000_000)
27
38
  }
28
39
 
29
40
  export interface ColumnInfo {
@@ -42,6 +53,50 @@ export function inferColumns(row: Record<string, unknown>): ColumnInfo[] {
42
53
  }))
43
54
  }
44
55
 
56
+ function postgresBooleanText(value: unknown): string | null {
57
+ if (typeof value === 'boolean') return value ? 't' : 'f'
58
+ if (typeof value === 'number') return value === 0 ? 'f' : 't'
59
+ if (typeof value === 'bigint') return value === 0n ? 'f' : 't'
60
+ if (typeof value !== 'string') return null
61
+ switch (value.trim().toLowerCase()) {
62
+ case 't':
63
+ case 'true':
64
+ case '1':
65
+ return 't'
66
+ case 'f':
67
+ case 'false':
68
+ case '0':
69
+ return 'f'
70
+ default:
71
+ return null
72
+ }
73
+ }
74
+
75
+ function postgresTupleTextValue(value: unknown, column: ColumnInfo): string {
76
+ if (column.typeOid === PG_TYPE_BOOL) {
77
+ const booleanText = postgresBooleanText(value)
78
+ if (booleanText !== null) return booleanText
79
+ }
80
+
81
+ if (typeof value === 'boolean') return value ? 't' : 'f'
82
+ if (typeof value === 'object') return JSON.stringify(value)
83
+
84
+ let strVal = String(value)
85
+ // normalize ISO timestamps to postgres text format.
86
+ // to_jsonb() produces "2026-03-19T07:20:11.643" but postgres
87
+ // pgoutput sends "2026-03-19 07:20:11.643" (space, no T).
88
+ // mismatch causes zero-cache to see different values during
89
+ // mutation reconciliation, triggering unnecessary rebases.
90
+ if (
91
+ (column.typeOid === PG_TYPE_TIMESTAMP || column.typeOid === PG_TYPE_TIMESTAMPTZ) &&
92
+ typeof value === 'string' &&
93
+ value.length >= 19
94
+ ) {
95
+ strVal = strVal.replace('T', ' ')
96
+ }
97
+ return strVal
98
+ }
99
+
45
100
  // reusable scratch buffer for building messages (64KB, grows if needed)
46
101
  let scratch = new Uint8Array(65536)
47
102
  let scratchView = new DataView(scratch.buffer)
@@ -175,27 +230,7 @@ function encodeTupleDataInto(
175
230
  ensureScratch(pos + 1)
176
231
  scratch[pos++] = 0x6e // 'n' for null
177
232
  } else {
178
- // convert to postgresql text format
179
- let strVal: string
180
- if (typeof val === 'boolean') {
181
- strVal = val ? 't' : 'f'
182
- } else if (typeof val === 'object') {
183
- strVal = JSON.stringify(val)
184
- } else {
185
- strVal = String(val)
186
- // normalize ISO timestamps to postgres text format.
187
- // to_jsonb() produces "2026-03-19T07:20:11.643" but postgres
188
- // pgoutput sends "2026-03-19 07:20:11.643" (space, no T).
189
- // mismatch causes zero-cache to see different values during
190
- // mutation reconciliation, triggering unnecessary rebases.
191
- if (
192
- (col.typeOid === 1114 || col.typeOid === 1184) &&
193
- typeof val === 'string' &&
194
- val.length >= 19
195
- ) {
196
- strVal = strVal.replace('T', ' ')
197
- }
198
- }
233
+ const strVal = postgresTupleTextValue(val, col)
199
234
  const bytes = encoder.encode(strVal)
200
235
  ensureScratch(pos + 1 + 4 + bytes.length)
201
236
  scratch[pos++] = 0x74 // 't' for text
@@ -5,6 +5,7 @@ import {
5
5
  getBrowserDefine,
6
6
  getBrowserBuildConfig,
7
7
  } from './browser-build-config.js'
8
+ import { getHeapStatistics } from './shims/node-stub.js'
8
9
 
9
10
  describe('browser build config', () => {
10
11
  describe('getBrowserAliases', () => {
@@ -19,6 +20,7 @@ describe('browser build config', () => {
19
20
  expect(aliases['@rocicorp/zero-sqlite3']).toBe('orez/worker/shims/sqlite')
20
21
  expect(aliases.fastify).toBe('orez/worker/shims/fastify')
21
22
  expect(aliases.ws).toBe('orez/worker/shims/ws')
23
+ expect(aliases.oxfmt).toBe('orez/worker/shims/oxfmt')
22
24
  })
23
25
 
24
26
  it('includes Node.js polyfills', () => {
@@ -26,6 +28,7 @@ describe('browser build config', () => {
26
28
  expect(aliases['node:events']).toBe('events')
27
29
  expect(aliases['node:stream']).toBe('orez/worker/shims/stream-browser')
28
30
  expect(aliases['node:path']).toBe('path-browserify')
31
+ expect(aliases['node:os']).toBe('orez/worker/shims/node-stub')
29
32
  })
30
33
 
31
34
  it('includes Node.js stubs', () => {
@@ -35,6 +38,7 @@ describe('browser build config', () => {
35
38
  expect(aliases['node:child_process']).toBe('orez/worker/shims/node-stub')
36
39
  expect(aliases['node:http']).toBe('orez/worker/shims/node-stub')
37
40
  expect(aliases['node:crypto']).toBe('orez/worker/shims/node-stub')
41
+ expect(aliases['node:v8']).toBe('orez/worker/shims/node-stub')
38
42
  })
39
43
  })
40
44
 
@@ -56,4 +60,12 @@ describe('browser build config', () => {
56
60
  expect(config.bundle).toBe(true)
57
61
  })
58
62
  })
63
+
64
+ describe('node:v8 shim', () => {
65
+ it('reports a positive worker heap budget', () => {
66
+ const stats = getHeapStatistics()
67
+ expect(stats.heap_size_limit).toBe(128 * 1024 * 1024)
68
+ expect(stats.heap_size_limit - stats.used_heap_size).toBeGreaterThan(0)
69
+ })
70
+ })
59
71
  })
@@ -49,6 +49,7 @@ export function getBrowserAliases(): Record<string, string> {
49
49
  '@rocicorp/zero-sqlite3': 'orez/worker/shims/sqlite',
50
50
  fastify: 'orez/worker/shims/fastify',
51
51
  ws: 'orez/worker/shims/ws',
52
+ oxfmt: 'orez/worker/shims/oxfmt',
52
53
 
53
54
  // -- Node.js built-in polyfills --
54
55
  // these are needed because zero-cache imports node: modules.
@@ -60,7 +61,7 @@ export function getBrowserAliases(): Record<string, string> {
60
61
  'crypto-browserify': 'orez/worker/shims/node-stub',
61
62
  'node:stream': 'orez/worker/shims/stream-browser',
62
63
  'node:path': 'path-browserify',
63
- 'node:os': 'os-browserify/browser',
64
+ 'node:os': 'orez/worker/shims/node-stub',
64
65
 
65
66
  // -- stubs for Node.js modules that zero-cache imports but doesn't --
66
67
  // -- use in SINGLE_PROCESS mode --