A folded mutation applies every call as a local prediction, but combines pending calls before sending them to the server. Use it for typing, sliders, or dragging when only the latest value matters.
This guide extends the manual synced-app quickstart. It uses that app’s issue schema, shared mutator registry, and browser client.
Define a mutation that sets the final value
Add this entry to mutators in shared/app-def.ts. The quickstart already defines
shared, imports z, and imports the MutationGen type:
renameIssue: shared(
z.object({ id: z.string(), title: z.string(), updatedAt: z.number() }),
function* (tx, a): MutationGen {
yield tx.update("issue", {
id: a.id,
title: a.title,
updatedAt: a.updatedAt,
});
},
),
This is an absorbing write: applying only the last arguments has the same effect as applying every call in order. It sets columns from its arguments and does not read state.
Do not fold increments, append operations, or writes whose result depends on
intermediate values. The folded path rejects reads through tx.row or tx.query.
Server authorization still applies to the mutation that the client sends.
Fold calls from the editor
// src/rename-issue.ts
import { app } from "./rindle-client.ts";
export function previewTitle(id: string, title: string) {
return app.mutate.renameIssue.folded(
{ key: id, debounceMs: 120, maxWaitMs: 1000 },
{ id, title, updatedAt: Date.now() },
);
}
const pending = previewTitle("issue-42", "Updated title");
// For example, force the last preview to send when the editor loses focus:
pending.flush();
const mid = await pending.mid;
console.log("Assigned mutation ID", mid);
The fold identity combines the mutator name with key. Use the row’s primary key
when each row has an independent edit stream.
Every call updates the local view immediately. A trailing debounce sends the latest
arguments after an idle gap. debounceMs defaults to 120 ms. maxWaitMs is optional;
without it, continuous input can keep delaying the flush. With the example’s
threshold, a call made at least one second into the window flushes it immediately.
This check runs on calls, so it is not a timer deadline: if input stops first,
the trailing debounce can flush later. A long edit can send multiple server writes.
flush() sends the pending window now. mid resolves when that window receives
its wire mutation ID. It does not indicate server acceptance or confirmation.
Use the client’s pending and rejection signals to show write status.
Finish a gesture
app.flushFolds() flushes all outstanding windows. The client also hooks page
lifecycle events, but a page closing is not a durable delivery guarantee.
A normal call to app.mutate.renameIssue(args) is useful when an explicit final
action should be a separate mutation. By default, overlapping ordinary writes
flush earlier folds so their order remains meaningful.
Keep undo history at the gesture level. Record the value before the edit and its final value, rather than every preview frame.