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.
- package/dist/cf-do/watermark.d.ts +21 -0
- package/dist/cf-do/watermark.d.ts.map +1 -0
- package/dist/cf-do/watermark.js +93 -0
- package/dist/cf-do/watermark.js.map +1 -0
- package/dist/cf-do/worker.d.ts +48 -22
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +642 -269
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/do-sql-tracking.d.ts +6 -0
- package/dist/do-sql-tracking.d.ts.map +1 -0
- package/dist/do-sql-tracking.js +14 -0
- package/dist/do-sql-tracking.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -14
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy-browser.js +6 -6
- package/dist/pg-proxy-browser.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +96 -17
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +6033 -454
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +18 -1
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +7 -2
- package/dist/replication/handler.js.map +1 -1
- package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
- package/dist/replication/pgoutput-encoder.js +72 -30
- package/dist/replication/pgoutput-encoder.js.map +1 -1
- package/dist/worker/browser-build-config.d.ts.map +1 -1
- package/dist/worker/browser-build-config.js +2 -1
- package/dist/worker/browser-build-config.js.map +1 -1
- package/dist/worker/cf-patches.d.ts +5 -2
- package/dist/worker/cf-patches.d.ts.map +1 -1
- package/dist/worker/cf-patches.js +238 -4
- package/dist/worker/cf-patches.js.map +1 -1
- package/dist/worker/shims/node-stub.d.ts +35 -0
- package/dist/worker/shims/node-stub.d.ts.map +1 -1
- package/dist/worker/shims/node-stub.js +53 -1
- package/dist/worker/shims/node-stub.js.map +1 -1
- package/dist/worker/shims/oxfmt.d.ts +4 -0
- package/dist/worker/shims/oxfmt.d.ts.map +1 -0
- package/dist/worker/shims/oxfmt.js +4 -0
- package/dist/worker/shims/oxfmt.js.map +1 -0
- package/dist/worker/shims/postgres-socket.js +1 -1
- package/dist/worker/shims/postgres-socket.js.map +1 -1
- package/dist/worker/shims/sqlite.d.ts +1 -0
- package/dist/worker/shims/sqlite.d.ts.map +1 -1
- package/dist/worker/shims/sqlite.js +229 -9
- package/dist/worker/shims/sqlite.js.map +1 -1
- package/dist/worker/shims/ws.d.ts.map +1 -1
- package/dist/worker/shims/ws.js +45 -0
- package/dist/worker/shims/ws.js.map +1 -1
- package/dist/worker/shims/zero-process-env.d.ts +2 -0
- package/dist/worker/shims/zero-process-env.d.ts.map +1 -0
- package/dist/worker/shims/zero-process-env.js +9 -0
- package/dist/worker/shims/zero-process-env.js.map +1 -0
- package/dist/worker/zero-cache-embed-cf.d.ts +29 -12
- package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
- package/dist/worker/zero-cache-embed-cf.js +83 -14
- package/dist/worker/zero-cache-embed-cf.js.map +1 -1
- package/package.json +6 -2
- package/src/cf-do/.wrangler/cache/cf.json +1 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/ARCHITECTURE.md +83 -0
- package/src/cf-do/watermark.test.ts +103 -0
- package/src/cf-do/watermark.ts +118 -0
- package/src/cf-do/worker.ts +1033 -0
- package/src/cf-do/wrangler.toml +11 -0
- package/src/config.ts +1 -1
- package/src/do-sql-tracking.test.ts +19 -0
- package/src/do-sql-tracking.ts +19 -0
- package/src/index.ts +29 -14
- package/src/pg-proxy-browser.ts +6 -6
- package/src/pg-proxy-do-backend.test.ts +3890 -0
- package/src/pg-proxy-do-backend.ts +6799 -482
- package/src/replication/change-tracker.ts +16 -1
- package/src/replication/handler.test.ts +35 -0
- package/src/replication/handler.ts +7 -2
- package/src/replication/pgoutput-encoder.test.ts +71 -2
- package/src/replication/pgoutput-encoder.ts +65 -30
- package/src/worker/browser-build-config.test.ts +12 -0
- package/src/worker/browser-build-config.ts +2 -1
- package/src/worker/cf-patches.ts +274 -4
- package/src/worker/shims/node-stub.ts +53 -1
- package/src/worker/shims/oxfmt.ts +3 -0
- package/src/worker/shims/postgres-socket.ts +1 -1
- package/src/worker/shims/sqlite.test.ts +145 -0
- package/src/worker/shims/sqlite.ts +256 -9
- package/src/worker/shims/ws.ts +45 -0
- package/src/worker/shims/zero-process-env.ts +11 -0
- package/src/worker/zero-cache-embed-cf.ts +114 -18
- package/src/query-rewrites.test.ts +0 -30
- 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
|
-
//
|
|
1150
|
-
//
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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': '
|
|
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 --
|