docs(durable): read only through tx inside a commit callback

Session, Harness, and runtime reads such as snapshot(), documentState(), watchDoc(), and getTask() may queue on the mutation line a commit callback holds; on a cache miss they never resolve. Documented on Session.commit, TaskRuntime.commit, and in the spec's pitfalls.
This commit is contained in:
Mario Zechner 2026-10-10 17:36:18 +00:00
parent 4130bd5b32
commit b9f2b3e827
2 changed files with 9 additions and 2 deletions

View file

@ -5004,6 +5004,11 @@ These are contracts, not invitations to add defensive machinery:
- **Long transactions:** an async commit callback holds the Session mutation
line. Never await models, tools, processes, network calls, humans, a nested
Session commit, or a Session waiter inside it. Use methods on the current `Tx`.
- **Reads inside a commit:** `snapshot()`, `documentState()`, `watchDoc()`,
`getTask()`, and the other Session, Harness, and runtime reads may queue on the
mutation line, for example to load a document not in memory. Inside a commit
callback, which holds that line, such a read never resolves, and the line stays
held. Only a cache hit returns, so the hang is intermittent. Read through `tx`.
- **Explicit creation:** only typed `tx.doc()` creates an absent document. Snapshot,
state, and watch lookup return `undefined` instead.
- **Family initialization:** the first acquisition of an absent family address

View file

@ -191,7 +191,7 @@ export interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver
* Commit on the Session line after rereading the task. Rejects when the task is terminal, the invocation ended, the
* Harness is closing, or, in a run invocation, the task carries an abort mark. A returned state replaces the task's
* state in the same commit; returning nothing leaves it unchanged. `tx.createTask()` defaults to the task's
* conversation.
* conversation. Inside the callback read only through `tx`: `snapshot()`, `documentState()`, `watchDoc()`, `getTask()`, and the other Session and runtime reads may queue on the line the callback holds, and then never resolve.
*/
commit(
change: (
@ -948,7 +948,9 @@ export type SessionEnd = { readonly reason: "closed" } | { readonly reason: "fai
/** Owner of one mutation line, its records, and its tracked documents. */
export interface Session extends DocumentObserver {
/** Run one atomic transaction on the Session mutation line. */
/**
* Run one atomic transaction on the Session mutation line. Inside the callback read only through `tx`: `snapshot()`, `documentState()`, `watchDoc()`, `getTask()`, and the other Session and runtime reads may queue on the line the callback holds, and then never resolve.
*/
commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
/** Seal admission, settle admitted commits, then close storage. */
close(context: Context): Promise<void>;