Repository: rocicorp/mono
Pull request #4068: feat(mutators): allow waiting for server results on the client
Author: login: tantaman
Created: 2025-03-21T15:41:06Z | Merged: 2025-03-21T17:00:08Z

Mutators now return a:

```ts
export type PromiseWithServerResult<T, S> = Promise<T> & {server: Promise<S>};
```

The `server` property allows waiting for authoritative results.


Usage looks like:

```ts
const close = z.mutate.issue.close(id);

// optionally await the local write
await close;


// optionally await/get/catch the server result
close.server.catch((e) => toast.showError(e));
```


Diff:
diff --git a/packages/zero-client/src/client/custom.test.ts b/packages/zero-client/src/client/custom.test.ts
index 8bcc378e59..2530c82992 100644
--- a/packages/zero-client/src/client/custom.test.ts
+++ b/packages/zero-client/src/client/custom.test.ts
@@ -4,6 +4,7 @@ import {
   TransactionImpl,
   type CustomMutatorDefs,
   type MakeCustomMutatorInterfaces,
+  type PromiseWithServerResult,
 } from './custom.ts';
 import {zeroForTest} from './test-utils.ts';
 import type {InsertValue, Transaction} from '../../../zql/src/mutate/custom.ts';
@@ -13,6 +14,7 @@ import {createSilentLogContext} from '../../../shared/src/logging-test-utils.ts'
 import type {WriteTransaction} from './replicache-types.ts';
 import {zeroData} from '../../../replicache/src/transactions.ts';
 import {must} from '../../../shared/src/must.ts';
+import type {MutationResult} from '../../../zero-protocol/src/push.ts';
 
 type Schema = typeof schema;
 
@@ -50,18 +52,25 @@ test('argument types are preserved on the generated mutator interface', () => {
   } satisfies CustomMutatorDefs<Schema>;
 
   type MutatorsInterface = MakeCustomMutatorInterfaces<Schema, typeof mutators>;
+
   expectTypeOf<MutatorsInterface>().toEqualTypeOf<{
     readonly issue: {
-      readonly setTitle: (args: {id: string; title: string}) => Promise<void>;
+      readonly setTitle: (args: {
+        id: string;
+        title: string;
+      }) => PromiseWithServerResult<void, MutationResult>;
       readonly setProps: (args: {
         id: string;
         title: string;
         status: 'closed' | 'open';
         assignee: string;
-      }) => Promise<void>;
+      }) => PromiseWithServerResult<void, MutationResult>;
     };
     readonly nonTableNamespace: {
-      readonly doThing: (args: {arg1: string; arg2: number}) => Promise<void>;
+      readonly doThing: (_a: {
+        arg1: string;
+        arg2: number;
+      }) => PromiseWithServerResult<void, MutationResult>;
     };
   }>();
 });
diff --git a/packages/zero-client/src/client/custom.ts b/packages/zero-client/src/client/custom.ts
index 96e8d4234f..237d9a36ca 100644
--- a/packages/zero-client/src/client/custom.ts
+++ b/packages/zero-client/src/client/custom.ts
@@ -25,6 +25,8 @@ import {ZeroContext} from './context.ts';
 import {deleteImpl, insertImpl, updateImpl, upsertImpl} from './crud.ts';
 import type {IVMSourceBranch} from './ivm-branch.ts';
 import type {WriteTransaction} from './replicache-types.ts';
+import type {MutationResult} from '../../../zero-protocol/src/push.ts';
+import type {MutationTracker} from './mutation-tracker.ts';
 
 /**
  * The shape which a user's custom mutator definitions must conform to.
@@ -60,7 +62,7 @@ export type MakeCustomMutatorInterfaces<
     tx: Transaction<S>,
     ...args: infer Args
   ) => Promise<void>
-    ? (...args: Args) => Promise<void>
+    ? (...args: Args) => PromiseWithServerResult<void, MutationResult>
     : {
         readonly [P in keyof MD[NamespaceOrName]]: MakeCustomMutatorInterface<
           S,
@@ -74,7 +76,7 @@ export type MakeCustomMutatorInterface<
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   F,
 > = F extends (tx: ClientTransaction<S>, ...args: infer Args) => Promise<void>
-  ? (...args: Args) => Promise<void>
+  ? (...args: Args) => PromiseWithServerResult<void, MutationResult>
   : never;
 
 export class TransactionImpl<S extends Schema> implements ClientTransaction<S> {
@@ -116,17 +118,36 @@ export class TransactionImpl<S extends Schema> implements ClientTransaction<S> {
   readonly token: string | undefined;
 }
 
-// TODO: wire into `PushResponseTracker` so we can give out promises for each mutation
-// when it is `initial`
+type MaybeWithServerResult<T> = {
+  server?: Promise<T>;
+};
+
+export type PromiseMaybeWithServerResult<T, S> = Promise<T> &
+  MaybeWithServerResult<S>;
+export type PromiseWithServerResult<T, S> = Promise<T> & {server: Promise<S>};
+
 export function makeReplicacheMutator<S extends Schema>(
   lc: LogContext,
+  mutationTracker: MutationTracker,
   mutator: CustomMutatorImpl<S>,
   schema: S,
   slowMaterializeThreshold: number,
 ) {
-  return (repTx: WriteTransaction, args: ReadonlyJSONValue): Promise<void> => {
+  return (
+    repTx: WriteTransaction,
+    args: ReadonlyJSONValue,
+  ): PromiseMaybeWithServerResult<void, MutationResult> => {
     const tx = new TransactionImpl(lc, repTx, schema, slowMaterializeThreshold);
-    return mutator(tx, args);
+    const clientPromise = mutator(tx, args);
+
+    if (repTx.reason === 'initial') {
+      const serverPromise = mutationTracker.trackMutation(repTx.mutationID);
+      (
+        clientPromise as PromiseMaybeWithServerResult<void, MutationResult>
+      ).server = serverPromise;
+    }
+
+    return clientPromise;
   };
 }
 
diff --git a/packages/zero-client/src/client/mutation-tracker.test.ts b/packages/zero-client/src/client/mutation-tracker.test.ts
index b1779d6ff7..2b45dcd070 100644
--- a/packages/zero-client/src/client/mutation-tracker.test.ts
+++ b/packages/zero-client/src/client/mutation-tracker.test.ts
@@ -1,12 +1,18 @@
-import {describe, it, expect} from 'vitest';
+import {describe, test, expect} from 'vitest';
 import {MutationTracker} from './mutation-tracker.ts';
 import type {PushResponse} from '../../../zero-protocol/src/push.ts';
+import {makeReplicacheMutator} from './custom.ts';
+import {createSilentLogContext} from '../../../shared/src/logging-test-utils.ts';
+import {createSchema} from '../../../zero-schema/src/builder/schema-builder.ts';
+import type {WriteTransaction} from './replicache-types.ts';
+import {zeroData} from '../../../replicache/src/transactions.ts';
 
 describe('MutationTracker', () => {
   const CLIENT_ID = 'test-client-1';
 
-  it('tracks a mutation and resolves on success', async () => {
-    const tracker = new MutationTracker(CLIENT_ID);
+  test('tracks a mutation and resolves on success', async () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
     const mutationPromise = tracker.trackMutation(1);
 
     const response: PushResponse = {
@@ -23,8 +29,9 @@ describe('MutationTracker', () => {
     expect(result).toEqual({});
   });
 
-  it('tracks a mutation and rejects on error', async () => {
-    const tracker = new MutationTracker(CLIENT_ID);
+  test('tracks a mutation and rejects on error', async () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
     const mutationPromise = tracker.trackMutation(1);
 
     const response: PushResponse = {
@@ -46,8 +53,9 @@ describe('MutationTracker', () => {
     });
   });
 
-  it('handles push errors', async () => {
-    const tracker = new MutationTracker(CLIENT_ID);
+  test('handles push errors', async () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
     const mutationPromise = tracker.trackMutation(1);
 
     const response: PushResponse = {
@@ -62,8 +70,9 @@ describe('MutationTracker', () => {
     });
   });
 
-  it('rejects mutation when explicitly rejected', async () => {
-    const tracker = new MutationTracker(CLIENT_ID);
+  test('rejects mutation when explicitly rejected', async () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
     const mutationPromise = tracker.trackMutation(1);
 
     tracker.rejectMutation(1, new Error('Failed to send'));
@@ -71,8 +80,9 @@ describe('MutationTracker', () => {
     await expect(mutationPromise).rejects.toThrow('Failed to send');
   });
 
-  it('rejects mutations from other clients', () => {
-    const tracker = new MutationTracker(CLIENT_ID);
+  test('rejects mutations from other clients', () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
     void tracker.trackMutation(1);
 
     const response: PushResponse = {
@@ -96,8 +106,9 @@ describe('MutationTracker', () => {
     );
   });
 
-  it('handles multiple concurrent mutations', async () => {
-    const tracker = new MutationTracker(CLIENT_ID);
+  test('handles multiple concurrent mutations', async () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
     const mutation1 = tracker.trackMutation(1);
     const mutation2 = tracker.trackMutation(2);
 
@@ -122,4 +133,54 @@ describe('MutationTracker', () => {
     expect(result1).toBe(r1);
     expect(result2).toBe(r2);
   });
+
+  test('mutation tracker size goes down each time a mutation is resolved or rejected', () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
+    void tracker.trackMutation(1);
+    tracker.trackMutation(2).catch(() => {
+      // expected
+    });
+
+    const response: PushResponse = {
+      mutations: [
+        {
+          id: {clientID: CLIENT_ID, id: 1},
+          result: {},
+        },
+        {
+          id: {clientID: CLIENT_ID, id: 2},
+          result: {
+            error: 'app',
+          },
+        },
+      ],
+    };
+
+    tracker.processPushResponse(response);
+    expect(tracker.size).toBe(0);
+  });
+
+  test('mutations are not tracked on rebase', async () => {
+    const mt = new MutationTracker();
+    mt.clientID = CLIENT_ID;
+    const mutator = makeReplicacheMutator(
+      createSilentLogContext(),
+      mt,
+      async () => {},
+      createSchema({
+        tables: [],
+        relationships: [],
+      }),
+      0,
+    );
+
+    const tx = {
+      reason: 'rebase',
+      mutationID: 1,
+      [zeroData]: {},
+    };
+    await mutator(tx as unknown as WriteTransaction, {});
+    expect(mt.size).toBe(0);
+  });
 });
diff --git a/packages/zero-client/src/client/mutation-tracker.ts b/packages/zero-client/src/client/mutation-tracker.ts
index cde65ab8c1..ad30a2e911 100644
--- a/packages/zero-client/src/client/mutation-tracker.ts
+++ b/packages/zero-client/src/client/mutation-tracker.ts
@@ -16,10 +16,13 @@ type MutationResult = MutationOk | MutationError | PushError;
  */
 export class MutationTracker {
   readonly #outstandingMutations: Map<number, Resolver<MutationResult>>;
-  readonly #clientID: string;
+  #clientID: string | undefined;
 
-  constructor(clientID: string) {
+  constructor() {
     this.#outstandingMutations = new Map();
+  }
+
+  set clientID(clientID: string) {
     this.#clientID = clientID;
   }
 
@@ -50,6 +53,10 @@ export class MutationTracker {
     }
   }
 
+  get size() {
+    return this.#outstandingMutations.size;
+  }
+
   #processPushError(error: PushError): void {
     const mids = error.mutationIDs;
 
diff --git a/packages/zero-client/src/client/zero.ts b/packages/zero-client/src/client/zero.ts
index 767b980543..ca86887dcf 100644
--- a/packages/zero-client/src/client/zero.ts
+++ b/packages/zero-client/src/client/zero.ts
@@ -135,6 +135,7 @@ import {getServer} from './server-option.ts';
 import {version} from './version.ts';
 import {PokeHandler} from './zero-poke-handler.ts';
 import {ZeroRep} from './zero-rep.ts';
+import {MutationTracker} from './mutation-tracker.ts';
 
 type ConnectionState = Enum<typeof ConnectionState>;
 type PingResult = Enum<typeof PingResult>;
@@ -290,6 +291,7 @@ export class Zero<
   readonly #ivmMain: IVMSourceBranch;
   readonly #clientToServer: NameMapper;
   readonly #deleteClientsManager: DeleteClientsManager;
+  readonly #mutationTracker: MutationTracker;
 
   /**
    * The queries we sent when inside the sec-protocol header when establishing a connection.
@@ -465,6 +467,7 @@ export class Zero<
     }
 
     const lc = new LogContext(logOptions.logLevel, {}, logOptions.logSink);
+    this.#mutationTracker = new MutationTracker();
     if (options.mutators) {
       for (const [namespaceOrKey, mutatorOrMutators] of Object.entries(
         options.mutators,
@@ -474,6 +477,7 @@ export class Zero<
           assertUnique(key);
           replicacheMutators[key] = makeReplicacheMutator(
             lc,
+            this.#mutationTracker,
             mutatorOrMutators,
             schema,
             slowMaterializeThreshold,
@@ -489,6 +493,7 @@ export class Zero<
             assertUnique(key);
             replicacheMutators[key] = makeReplicacheMutator(
               lc,
+              this.#mutationTracker,
               mutator as CustomMutatorImpl<S>,
               schema,
               slowMaterializeThreshold,
@@ -555,6 +560,7 @@ export class Zero<
     this.#server = server;
     this.userID = userID;
     this.#lc = lc.withContext('clientID', rep.clientID);
+    this.#mutationTracker.clientID = rep.clientID;
 
     const onUpdateNeededCallback =
       onUpdateNeeded ??
@@ -903,9 +909,7 @@ export class Zero<
         );
 
       case 'push-response':
-        // Ignored for now. Will be sent to the `mutation-tracker`
-        // in order to resolve mutation promises.
-        break;
+        return this.#mutationTracker.processPushResponse(downMessage[1]);
 
       default:
         msgType satisfies never;
diff --git a/packages/zero-protocol/src/push.ts b/packages/zero-protocol/src/push.ts
index e2aef89f7f..5a625caf06 100644
--- a/packages/zero-protocol/src/push.ts
+++ b/packages/zero-protocol/src/push.ts
@@ -187,6 +187,7 @@ export type MutationError = v.Infer<typeof mutationErrorSchema>;
 export type PushError = v.Infer<typeof pushErrorSchema>;
 export type PushOk = v.Infer<typeof pushOkSchema>;
 export type MutationID = v.Infer<typeof mutationIDSchema>;
+export type MutationResult = v.Infer<typeof mutationResultSchema>;
 
 export function mapCRUD(
   arg: CRUDMutationArg,


Reviews:
user: login: tantaman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:42:35Z
user: login: tantaman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:43:00Z
user: login: arv
body: LGTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T12:57:38Z

Review comments:
diff_hunk: @@ -16,17 +16,21 @@ type MutationResult = MutationOk | MutationError | PushError;
  */
 export class MutationTracker {
   readonly #outstandingMutations: Map<number, Resolver<MutationResult>>;
-  readonly #clientID: string;
+  #clientID: string | undefined;
 
-  constructor(clientID: string) {
+  constructor() {
     this.#outstandingMutations = new Map();
+  }
+
+  set clientID(clientID: string) {
     this.#clientID = clientID;
   }
 
   trackMutation(id: number): Promise<MutationResult> {
path: packages/zero-client/src/client/mutation-tracker.ts
user: login: tantaman
body: note to self: add some tests that verify the map is not leaking.
created_at: 2025-03-21T15:42:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -116,17 +118,35 @@ export class TransactionImpl<S extends Schema> implements ClientTransaction<S> {
   readonly token: string | undefined;
 }
 
-// TODO: wire into `PushResponseTracker` so we can give out promises for each mutation
-// when it is `initial`
+type MaybeWithServerResult<T> = {
+  server?: Promise<T>;
+};
+
+type PromiseMaybeWithServerResult<T, S> = Promise<T> & MaybeWithServerResult<S>;
+export type PromiseWithServerResult<T, S> = Promise<T> & {server: Promise<S>};
+
 export function makeReplicacheMutator<S extends Schema>(
   lc: LogContext,
+  mutationTracker: MutationTracker,
   mutator: CustomMutatorImpl<S>,
   schema: S,
   slowMaterializeThreshold: number,
 ) {
-  return (repTx: WriteTransaction, args: ReadonlyJSONValue): Promise<void> => {
+  return (
+    repTx: WriteTransaction,
+    args: ReadonlyJSONValue,
+  ): PromiseMaybeWithServerResult<void, MutationResult> => {
     const tx = new TransactionImpl(lc, repTx, schema, slowMaterializeThreshold);
-    return mutator(tx, args);
+    const clientPromise = mutator(tx, args);
+
+    if (repTx.reason === 'initial') {
path: packages/zero-client/src/client/custom.ts
user: login: tantaman
body: note to self: add some test to verify we do not try to track mutations on a rebase.
created_at: 2025-03-21T15:43:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -465,6 +467,7 @@ export class Zero<
     }
 
     const lc = new LogContext(logOptions.logLevel, {}, logOptions.logSink);
+    this.#mutationTracker = new MutationTracker();
path: packages/zero-client/src/client/zero.ts
user: login: arv
body: move to line 294
created_at: 2025-03-24T12:49:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,10 +16,13 @@ type MutationResult = MutationOk | MutationError | PushError;
  */
 export class MutationTracker {
   readonly #outstandingMutations: Map<number, Resolver<MutationResult>>;
-  readonly #clientID: string;
+  #clientID: string | undefined;
 
-  constructor(clientID: string) {
+  constructor() {
     this.#outstandingMutations = new Map();
path: packages/zero-client/src/client/mutation-tracker.ts
user: login: arv
body: move to declaration and you can remove constructor
created_at: 2025-03-24T12:51:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,7 +76,7 @@ export type MakeCustomMutatorInterface<
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   F,
 > = F extends (tx: ClientTransaction<S>, ...args: infer Args) => Promise<void>
path: packages/zero-client/src/client/custom.ts
user: login: arv
body: TODO: Parameterize the return type too. Replicache allows return values from mutators.
created_at: 2025-03-24T12:52:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,4 +133,54 @@ describe('MutationTracker', () => {
     expect(result1).toBe(r1);
     expect(result2).toBe(r2);
   });
+
+  test('mutation tracker size goes down each time a mutation is resolved or rejected', () => {
+    const tracker = new MutationTracker();
+    tracker.clientID = CLIENT_ID;
+    void tracker.trackMutation(1);
+    tracker.trackMutation(2).catch(() => {
+      // expected
+    });
path: packages/zero-client/src/client/mutation-tracker.test.ts
user: login: arv
body: Can you also test the actual error we got?
created_at: 2025-03-24T12:57:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-21T15:41:11Z
body: [vc]: #FKRMkk3H7fdtffONb+m17TAAHutN/ByMyA2jj/iZGws=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyZXBsaWNhY2hlLWRvY3MiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm9jaWNvcnAvcmVwbGljYWNoZS1kb2NzL0Fxb2t2UndETkM2ejZ3YXZMTjN2anZSVzlLdlEiLCJwcmV2aWV3VXJsIjoicmVwbGljYWNoZS1kb2NzLWdpdC1tbGF3LXRyYWNrLW11dGF0aW9ucy1yb2NpY29ycC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InJlcGxpY2FjaGUtZG9jcy1naXQtbWxhdy10cmFjay1tdXRhdGlvbnMtcm9jaWNvcnAudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJ6YnVncyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3pidWdzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3JvY2ljb3JwL3pidWdzLzJqallzNjh1OE5IZUhhU214Tk55NENOenZZUm4iLCJwcmV2aWV3VXJsIjoiemJ1Z3MtZ2l0LW1sYXctdHJhY2stbXV0YXRpb25zLXJvY2ljb3JwLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiemJ1Z3MtZ2l0LW1sYXctdHJhY2stbXV0YXRpb25zLXJvY2ljb3JwLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **replicache-docs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/replicache-docs/AqokvRwDNC6z6wavLN3vjvRW9KvQ)) | [Visit Preview](https://replicache-docs-git-mlaw-track-mutations-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/replicache-docs-git-mlaw-track-mutations-rocicorp.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:09pm |
| **zbugs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/zbugs/2jjYs68u8NHeHaSmxNNy4CNzvYRn)) | [Visit Preview](https://zbugs-git-mlaw-track-mutations-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zbugs-git-mlaw-track-mutations-rocicorp.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:09pm |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-21T15:43:00Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/19ed5a0d-f8dd-40a3-a2b6-e746894e9a06?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/mlaw-track-mutations?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">mlaw/track-mutations</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/localhost?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">localhost</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/file-size?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">File Size</a></th><th>Benchmark Result<br />kilobytes (KB)<br />(Result Δ%)</th><th>Upper Boundary<br />kilobytes (KB)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-package-tgz?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero-package.tgz</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=8b487446-8c6e-4f63-8cd0-7caae3e211e4&heads=e69f4903-0a48-4b2d-a6c7-6d5cc103538d&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=2bcdd867-a170-440b-8c18-ee15b6476b90&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1739997811000&end_time=1742589811000&report=19ed5a0d-f8dd-40a3-a2b6-e746894e9a06&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/2cf493c8-a84a-4f6d-afb2-a1e7f51a3d74?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>1,078.24 KB<br /><details><summary>(+0.09%)</summary>Baseline: 1,077.29 KB</details></td><td>1,098.83 KB<br />(98.13%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=8b487446-8c6e-4f63-8cd0-7caae3e211e4&heads=e69f4903-0a48-4b2d-a6c7-6d5cc103538d&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=4f172dbf-1d31-4809-9c2d-12c79ed4c9d4&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1739997811000&end_time=1742589811000&report=19ed5a0d-f8dd-40a3-a2b6-e746894e9a06&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/2cf493c8-a84a-4f6d-afb2-a1e7f51a3d74?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>189.54 KB<br /><details><summary>(+0.26%)</summary>Baseline: 189.05 KB</details></td><td>192.83 KB<br />(98.29%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js-br?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js.br</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=8b487446-8c6e-4f63-8cd0-7caae3e211e4&heads=e69f4903-0a48-4b2d-a6c7-6d5cc103538d&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=f9d12b7c-f48c-41fc-a662-5019d902c9f9&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1739997811000&end_time=1742589811000&report=19ed5a0d-f8dd-40a3-a2b6-e746894e9a06&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/2cf493c8-a84a-4f6d-afb2-a1e7f51a3d74?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>53.19 KB<br /><details><summary>(+0.31%)</summary>Baseline: 53.02 KB</details></td><td>54.08 KB<br />(98.35%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/19ed5a0d-f8dd-40a3-a2b6-e746894e9a06?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/size"></div>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-21T15:43:22Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/854152f8-2547-491d-aae7-57f1d7e224c0?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/mlaw-track-mutations?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">mlaw/track-mutations</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/localhost?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">localhost</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/throughput?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Throughput</a></th><th>Benchmark Result<br />operations / second (ops/s)<br />(Result Δ%)</th><th>Lower Boundary<br />operations / second (ops/s)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-custom-bench-ts-big-schema?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/custom.bench.ts > big schema</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=8b487446-8c6e-4f63-8cd0-7caae3e211e4&heads=e4817c49-7df2-42d8-8cd9-cc0a3ba8a320&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=da25d28a-0b6b-4baa-8550-fb143072856d&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1739997887000&end_time=1742589887000&report=854152f8-2547-491d-aae7-57f1d7e224c0&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/8b2cdaa9-7b4e-4fc5-8030-e6ee6486137e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>0.00 ops/s<br /><details><summary>(-8.50%)</summary>Baseline: 0.00 ops/s</details></td><td>0.00 ops/s<br />(70.68%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-basics-all-1000-rows-x-10-colu-rypc9f4e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > basics > All 1000 rows x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=8b487446-8c6e-4f63-8cd0-7caae3e211e4&heads=e4817c49-7df2-42d8-8cd9-cc0a3ba8a320&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=8ff168e6-e598-4cc2-a9c7-e5d09c4aefec&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1739997887000&end_time=1742589887000&report=854152f8-2547-491d-aae7-57f1d7e224c0&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/8b2cdaa9-7b4e-4fc5-8030-e6ee6486137e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>41.26 ops/s<br /><details><summary>(-2.97%)</summary>Baseline: 42.53 ops/s</details></td><td>39.58 ops/s<br />(95.92%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-with-filter-lower-rows-500-x-1-d9xjs9n1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > with filter > Lower rows 500 x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=8b487446-8c6e-4f63-8cd0-7caae3e211e4&heads=e4817c49-7df2-42d8-8cd9-cc0a3ba8a320&testbeds=50a21deb-8296-4657-b7ad-5a4002b77d68&benchmarks=ce024352-2a47-400d-b489-b9951bf428ac&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1739997887000&end_time=1742589887000&report=854152f8-2547-491d-aae7-57f1d7e224c0&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/8b2cdaa9-7b4e-4fc5-8030-e6ee6486137e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>40.34 ops/s<br /><details><summary>(-14.43%)</summary>Baseline: 47.14 ops/s</details></td><td>36.85 ops/s<br />(91.36%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/854152f8-2547-491d-aae7-57f1d7e224c0?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/perf"></div>
author_association: NONE