diff --git a/packages/durable/docs/spec.md b/packages/durable/docs/spec.md index 725c81f98..635eddf75 100644 --- a/packages/durable/docs/spec.md +++ b/packages/durable/docs/spec.md @@ -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 diff --git a/packages/durable/src/types.ts b/packages/durable/src/types.ts index c54981ba3..804e49ef1 100644 --- a/packages/durable/src/types.ts +++ b/packages/durable/src/types.ts @@ -191,7 +191,7 @@ export interface TaskRuntime 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(change: (tx: Tx) => T | Promise, context: Context): Promise; /** Seal admission, settle admitted commits, then close storage. */ close(context: Context): Promise;