From 0a42e54702f9530e468fb67018aa33b83e46cd4d Mon Sep 17 00:00:00 2001 From: Noa Date: Thu, 29 Jan 2026 14:16:35 -0600 Subject: [PATCH 1/4] Don't generate cjs version of modules --- crates/bindings-typescript/tsup.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bindings-typescript/tsup.config.ts b/crates/bindings-typescript/tsup.config.ts index bbde6e1a263..962453c7f0b 100644 --- a/crates/bindings-typescript/tsup.config.ts +++ b/crates/bindings-typescript/tsup.config.ts @@ -172,11 +172,11 @@ export default defineConfig([ esbuildOptions: commonEsbuildTweaks(), }, - // Server subpath (SSR / node-friendly): dist/server/index.{mjs,cjs} + // Server subpath (SSR / node-friendly): dist/server/index.mjs { entry: { index: 'src/server/index.ts' }, - format: ['esm', 'cjs'], - target: 'es2022', + format: 'esm', + target: 'esnext', outDir: 'dist/server', dts: false, sourcemap: true, From c4647ab6127bb9c09aa09eeea13806fe1b25cc3f Mon Sep 17 00:00:00 2001 From: Noa Date: Tue, 6 Jan 2026 21:52:59 -0600 Subject: [PATCH 2/4] Reuse BinaryReader/Writer --- .../src/lib/binary_reader.ts | 5 ++ .../src/lib/binary_writer.ts | 56 +++++++---------- .../bindings-typescript/src/server/runtime.ts | 61 ++++++++++--------- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/crates/bindings-typescript/src/lib/binary_reader.ts b/crates/bindings-typescript/src/lib/binary_reader.ts index 83767755287..98850e79176 100644 --- a/crates/bindings-typescript/src/lib/binary_reader.ts +++ b/crates/bindings-typescript/src/lib/binary_reader.ts @@ -25,6 +25,11 @@ export default class BinaryReader { this.offset = 0; } + reset(view: DataView) { + this.view = view; + this.offset = 0; + } + get remaining(): number { return this.view.byteLength - this.offset; } diff --git a/crates/bindings-typescript/src/lib/binary_writer.ts b/crates/bindings-typescript/src/lib/binary_writer.ts index d88aa2faf59..144427284c5 100644 --- a/crates/bindings-typescript/src/lib/binary_writer.ts +++ b/crates/bindings-typescript/src/lib/binary_writer.ts @@ -15,15 +15,12 @@ const ArrayBufferPrototypeTransfer = }; export class ResizableBuffer { - #buffer: ArrayBuffer | null; + buffer: ArrayBuffer; + view: DataView; constructor(init: number | ArrayBuffer) { - this.#buffer = typeof init === 'number' ? new ArrayBuffer(init) : init; - } - - get buffer(): ArrayBuffer { - if (this.#buffer == null) throw new TypeError('Accessing detached buffer'); - return this.#buffer; + this.buffer = typeof init === 'number' ? new ArrayBuffer(init) : init; + this.view = new DataView(this.buffer); } get capacity(): number { @@ -31,42 +28,31 @@ export class ResizableBuffer { } grow(newSize: number) { - if (this.#buffer == null) - throw new TypeError('Cannot resize detached buffer'); - if (newSize <= this.#buffer.byteLength) return; - this.#buffer = ArrayBufferPrototypeTransfer.call(this.#buffer, newSize); - } - - get detached(): boolean { - return this.#buffer == null; - } - - detach(): ArrayBuffer { - if (this.#buffer == null) - throw new TypeError('Cannot detach detached buffer'); - const buf = this.#buffer!; - this.#buffer = null; - return buf; + if (newSize <= this.buffer.byteLength) return; + this.buffer = ArrayBufferPrototypeTransfer.call(this.buffer, newSize); + this.view = new DataView(this.buffer); } } export default class BinaryWriter { - #buffer: ResizableBuffer; - view: DataView; + buffer: ResizableBuffer; offset: number = 0; constructor(init: number | ResizableBuffer) { - this.#buffer = typeof init === 'number' ? new ResizableBuffer(init) : init; - this.view = new DataView(this.#buffer.buffer); + this.buffer = typeof init === 'number' ? new ResizableBuffer(init) : init; + } + + reset(buffer: ResizableBuffer) { + this.buffer = buffer; + this.offset = 0; } expandBuffer(additionalCapacity: number): void { const minCapacity = this.offset + additionalCapacity + 1; - if (minCapacity <= this.#buffer.capacity) return; - let newCapacity = this.#buffer.capacity * 2; + if (minCapacity <= this.buffer.capacity) return; + let newCapacity = this.buffer.capacity * 2; if (newCapacity < minCapacity) newCapacity = minCapacity; - this.#buffer.grow(newCapacity); - this.view = new DataView(this.#buffer.buffer); + this.buffer.grow(newCapacity); } toBase64(): string { @@ -74,7 +60,11 @@ export default class BinaryWriter { } getBuffer(): Uint8Array { - return new Uint8Array(this.#buffer.buffer, 0, this.offset); + return new Uint8Array(this.buffer.buffer, 0, this.offset); + } + + get view() { + return this.buffer.view; } writeUInt8Array(value: Uint8Array): void { @@ -83,7 +73,7 @@ export default class BinaryWriter { this.expandBuffer(4 + length); this.writeU32(length); - new Uint8Array(this.#buffer.buffer, this.offset).set(value); + new Uint8Array(this.buffer.buffer, this.offset).set(value); this.offset += length; } diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index b6d11cfc49d..4806cf1989b 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -273,7 +273,8 @@ export const hooks: ModuleHooks = { ); } const deserializeArgs = reducerArgsDeserializers[reducerId]; - const args = deserializeArgs(new BinaryReader(argsBuf)); + BINARY_READER.reset(argsBuf); + const args = deserializeArgs(BINARY_READER); const senderIdentity = new Identity(sender); const ctx: ReducerCtx = new ReducerCtxImpl( senderIdentity, @@ -369,6 +370,9 @@ function makeDbView(moduleDef: Infer): DbView { ); } +const BINARY_WRITER = new BinaryWriter(0); +const BINARY_READER = new BinaryReader(new Uint8Array()); + function makeTableView( typespace: Infer, table: Infer @@ -423,11 +427,11 @@ function makeTableView( tableIterator(sys.datastore_table_scan_bsatn(table_id), deserializeRow); const integrateGeneratedColumns = hasAutoIncrement - ? (row: RowType, ret_buf: Uint8Array) => { - const reader = new BinaryReader(ret_buf); + ? (row: RowType, ret_buf: DataView) => { + BINARY_READER.reset(ret_buf); for (const { colName, deserialize, sequenceTrigger } of sequences) { if (row[colName] === sequenceTrigger) { - row[colName] = deserialize(reader); + row[colName] = deserialize(BINARY_READER); } } } @@ -439,23 +443,23 @@ function makeTableView( [Symbol.iterator]: () => iter(), insert: row => { const buf = LEAF_BUF; - const writer = new BinaryWriter(buf); - serializeRow(writer, row); - sys.datastore_insert_bsatn(table_id, buf.buffer, writer.offset); + BINARY_WRITER.reset(buf); + serializeRow(BINARY_WRITER, row); + sys.datastore_insert_bsatn(table_id, buf.buffer, BINARY_WRITER.offset); const ret = { ...row }; - integrateGeneratedColumns?.(ret, new Uint8Array(buf.buffer)); + integrateGeneratedColumns?.(ret, buf.view); return ret; }, delete: (row: RowType): boolean => { const buf = LEAF_BUF; - const writer = new BinaryWriter(buf); - writer.writeU32(1); - serializeRow(writer, row); + BINARY_WRITER.reset(buf); + BINARY_WRITER.writeU32(1); + serializeRow(BINARY_WRITER, row); const count = sys.datastore_delete_all_by_eq_bsatn( table_id, buf.buffer, - writer.offset + BINARY_WRITER.offset ); return count > 0; }, @@ -495,11 +499,11 @@ function makeTableView( ); const serializePoint = (buffer: ResizableBuffer, colVal: any[]): number => { - const writer = new BinaryWriter(buffer); + BINARY_WRITER.reset(buffer); for (let i = 0; i < numColumns; i++) { - indexSerializers[i](writer, colVal[i]); + indexSerializers[i](BINARY_WRITER, colVal[i]); } - return writer.offset; + return BINARY_WRITER.offset; }; const serializeSingleElement = @@ -508,9 +512,9 @@ function makeTableView( const serializeSinglePoint = serializeSingleElement && ((buffer: ResizableBuffer, colVal: any): number => { - const writer = new BinaryWriter(buffer); - serializeSingleElement(writer, colVal); - return writer.offset; + BINARY_WRITER.reset(buffer); + serializeSingleElement(BINARY_WRITER, colVal); + return BINARY_WRITER.offset; }); type IndexScanArgs = [ @@ -553,15 +557,15 @@ function makeTableView( }, update: (row: RowType): RowType => { const buf = LEAF_BUF; - const writer = new BinaryWriter(buf); - serializeRow(writer, row); + BINARY_WRITER.reset(buf); + serializeRow(BINARY_WRITER, row); sys.datastore_update_bsatn( table_id, index_id, buf.buffer, - writer.offset + BINARY_WRITER.offset ); - integrateGeneratedColumns?.(row, new Uint8Array(buf.buffer)); + integrateGeneratedColumns?.(row, buf.view); return row; }, } as UniqueIndex; @@ -603,15 +607,15 @@ function makeTableView( }, update: (row: RowType): RowType => { const buf = LEAF_BUF; - const writer = new BinaryWriter(buf); - serializeRow(writer, row); + BINARY_WRITER.reset(buf); + serializeRow(BINARY_WRITER, row); sys.datastore_update_bsatn( table_id, index_id, buf.buffer, - writer.offset + BINARY_WRITER.offset ); - integrateGeneratedColumns?.(row, new Uint8Array(buf.buffer)); + integrateGeneratedColumns?.(row, buf.view); return row; }, } as UniqueIndex; @@ -646,7 +650,8 @@ function makeTableView( ): IndexScanArgs => { if (range.length > numColumns) throw new TypeError('too many elements'); - const writer = new BinaryWriter(buffer); + BINARY_WRITER.reset(buffer); + const writer = BINARY_WRITER; const prefix_elems = range.length - 1; for (let i = 0; i < prefix_elems; i++) { indexSerializers[i](writer, range[i]); @@ -737,7 +742,7 @@ function* tableIterator( try { let amt; while ((amt = advanceIter(iter, iterBuf))) { - const reader = new BinaryReader(new Uint8Array(iterBuf.buffer, amt)); + const reader = new BinaryReader(iterBuf.view); while (reader.offset < amt) { yield deserialize(reader); } From b34b5eaffb3d2cbab779b1c77fc7f4231a5e0c0d Mon Sep 17 00:00:00 2001 From: Noa Date: Wed, 7 Jan 2026 17:03:21 -0600 Subject: [PATCH 3/4] Don't construct unnecessary IteratorHandle --- .../bindings-typescript/src/server/runtime.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index 4806cf1989b..2c9e793dffa 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -536,14 +536,7 @@ function makeTableView( buf.buffer, point_len ); - const iter = tableIterator(iter_id, deserializeRow); - const { value, done } = iter.next(); - if (done) return null; - if (!iter.next().done) - throw new Error( - '`datastore_index_scan_range_bsatn` on unique field cannot return >1 rows' - ); - return value; + return tableIterateOne(iter_id, deserializeRow); }, delete: (colVal: IndexVal): boolean => { const buf = LEAF_BUF; @@ -583,14 +576,7 @@ function makeTableView( buf.buffer, point_len ); - const iter = tableIterator(iter_id, deserializeRow); - const { value, done } = iter.next(); - if (done) return null; - if (!iter.next().done) - throw new Error( - '`datastore_index_scan_range_bsatn` on unique field cannot return >1 rows' - ); - return value; + return tableIterateOne(iter_id, deserializeRow); }, delete: (colVal: IndexVal): boolean => { if (colVal.length !== numColumns) @@ -741,7 +727,7 @@ function* tableIterator( const iterBuf = takeBuf(); try { let amt; - while ((amt = advanceIter(iter, iterBuf))) { + while ((amt = iter.advance(iterBuf))) { const reader = new BinaryReader(iterBuf.view); while (reader.offset < amt) { yield deserialize(reader); @@ -752,10 +738,27 @@ function* tableIterator( } } -function advanceIter(iter: IteratorHandle, buf: ResizableBuffer): number { +function tableIterateOne(id: u32, deserialize: Deserializer): T | null { + const buf = LEAF_BUF; + // we only need to check for the `<= 0` case, since this function is only used + // with iterators that should only have zero or one element. + const ret = advanceIterRaw(id, buf); + if (ret !== 0) { + BINARY_READER.reset(buf.view); + return deserialize(BINARY_READER); + } + return null; +} + +/** + * `ret < 0` means the iterator yielded elements but is now exhausted and has been destroyed. + * `ret === 0` means the iterator was empty and has been destroyed. + * `ret > 0` means the iterator yielded elements and has more to give. + */ +function advanceIterRaw(id: u32, buf: ResizableBuffer): number { while (true) { try { - return iter.advance(buf.buffer); + return 0 | sys.row_iter_bsatn_advance(id, buf.buffer); } catch (e) { if (e && typeof e === 'object' && hasOwn(e, '__buffer_too_small__')) { buf.grow(e.__buffer_too_small__ as number); @@ -814,11 +817,9 @@ class IteratorHandle implements Disposable { } /** Call `row_iter_bsatn_advance`, returning 0 if this iterator has been exhausted. */ - advance(buf: ArrayBuffer): number { + advance(buf: ResizableBuffer): number { if (this.#id === -1) return 0; - // coerce to int32 - const ret = 0 | sys.row_iter_bsatn_advance(this.#id, buf); - // ret <= 0 means the iterator is exhausted + const ret = advanceIterRaw(this.#id, buf); if (ret <= 0) this.#detach(); return ret < 0 ? -ret : ret; } From 4ec52447a50d105821cb775c9dad6abf530126d2 Mon Sep 17 00:00:00 2001 From: Noa Date: Thu, 29 Jan 2026 15:19:46 -0600 Subject: [PATCH 4/4] Reuse ReducerCtx --- .../bindings-typescript/src/server/runtime.ts | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index 2c9e793dffa..be74607f336 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -27,7 +27,6 @@ import { type AuthCtx, type JsonObject, type JwtClaims, - type ReducerCtx, type ReducerCtx as IReducerCtx, } from '../lib/reducers'; import { type UntypedSchemaDef } from '../lib/schema'; @@ -178,6 +177,8 @@ class AuthCtxImpl implements AuthCtx { } } +let REDUCER_CTX: InstanceType | undefined; + // Using a class expression rather than declaration keeps the class out of the // type namespace, so that `ReducerCtx` still refers to the interface. export const ReducerCtxImpl = class ReducerCtx< @@ -205,6 +206,19 @@ export const ReducerCtxImpl = class ReducerCtx< this.db = getDbView(); } + static reset( + me: InstanceType, + sender: Identity, + timestamp: Timestamp, + connectionId: ConnectionId | null + ) { + me.sender = sender; + me.timestamp = timestamp; + me.connectionId = connectionId; + me.#uuidCounter = undefined; + me.#senderAuth = undefined; + } + get identity() { return (this.#identity ??= new Identity(sys.identity().__identity__)); } @@ -276,17 +290,24 @@ export const hooks: ModuleHooks = { BINARY_READER.reset(argsBuf); const args = deserializeArgs(BINARY_READER); const senderIdentity = new Identity(sender); - const ctx: ReducerCtx = new ReducerCtxImpl( - senderIdentity, - new Timestamp(timestamp), - ConnectionId.nullIfZero(new ConnectionId(connId)) - ); - try { - return ( - callUserFunction(moduleCtx.reducers[reducerId], ctx, args) ?? { - tag: 'ok', - } + let ctx; + if (REDUCER_CTX == null) { + ctx = REDUCER_CTX = new ReducerCtxImpl( + senderIdentity, + new Timestamp(timestamp), + ConnectionId.nullIfZero(new ConnectionId(connId)) ); + } else { + ctx = REDUCER_CTX; + ReducerCtxImpl.reset( + REDUCER_CTX, + senderIdentity, + new Timestamp(timestamp), + ConnectionId.nullIfZero(new ConnectionId(connId)) + ); + } + try { + callUserFunction(moduleCtx.reducers[reducerId], ctx, args); } catch (e) { if (e instanceof SenderError) { return { tag: 'err', value: e.message };