pi-mono/packages/durable/src/errors.ts
Mario Zechner cc9c523107 feat(durable): admit submissions inside the caller's commit with submit(tx, draft)
Conversation.submit(tx, draft) and ConversationHandle.submit(tx, draft) run
the same admission inside the caller's commit and return the submission ID,
so a host's own checks and writes and the admission are stored together or
not at all. Admission must come before the commit's first table write, once
per commit, and throws ReadAfterWrite otherwise on every path. A first
argument that is not a draft is taken as a transaction; one of another
Session, or a wrapped one, is refused. The handle form also rejects once its
invocation is aborted. The pi.durable.submit span covers the admission only.

closes #11149
2026-10-28 13:09:00 +00:00

83 lines
2.8 KiB
TypeScript

import type { ConversationId } from "./types.ts";
/**
* The message of a thrown value, without throwing itself: `String()` throws for values such as `Object.create(null)`,
* and code that formats an error must not fail for the value it formats.
*/
export function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
try {
return String(error);
} catch {
return Object.prototype.toString.call(error);
}
}
/**
* A transaction read a table after its first table write, or admitted a submission (`submit(tx, draft)`) after it. Read
* every required row, and admit, before writing.
*/
export class ReadAfterWrite extends Error {
/** `operation` names the call, such as `Tx.task()`. */
constructor(operation: string) {
super(`${operation} cannot read tables after the first table write`);
this.name = "ReadAfterWrite";
}
}
/**
* A Storage read rejected an invalid request, such as an unknown conversation or a cursor from another scan, with no
* durable effect. Unlike any other error a Storage throws, it fails only that read, not the Session; from `commit()` it is
* fatal like any other.
*/
export class StorageRequestError extends Error {
constructor(message: string) {
super(message);
this.name = "StorageRequestError";
}
}
/**
* The Session failed, so nothing runs or commits any more. It fails when a Storage method throws, a commit cannot be
* adopted, or the Harness's own bookkeeping throws (a scheduler commit, an internal commit listener, a registry whose
* `snapshot()` throws). `cause` is that first error. Reopen the Session; reopening recovers from what was committed.
*/
export class SessionFailed extends Error {
constructor(cause: unknown) {
super("Session failed after a storage error; close and reopen it", { cause });
this.name = "SessionFailed";
}
}
/**
* The Harness was opened read-only (`HarnessOptions.readOnly`): a commit with writes, or a call that would run tasks, was
* refused. Nothing was written, and the Harness stays usable for reads.
*/
export class ReadOnlySession extends Error {
constructor(what: string) {
super(`Read-only Harness: ${what} refused`);
this.name = "ReadOnlySession";
}
}
/** A submission reached a busy conversation and was not admitted. */
export class ConversationBusy extends Error {
readonly conversationId: ConversationId;
constructor(conversationId: ConversationId) {
super(`Conversation ${conversationId} is busy`);
this.name = "ConversationBusy";
this.conversationId = conversationId;
}
}
/** An input with `whenIdle: "reject"` reached an idle conversation and was not admitted. */
export class ConversationIdle extends Error {
readonly conversationId: ConversationId;
constructor(conversationId: ConversationId) {
super(`Conversation ${conversationId} is idle`);
this.name = "ConversationIdle";
this.conversationId = conversationId;
}
}