@jam-overture/ loom/ write
The write path: proposing, holding, confirming and applying a change.
Everything below is exported from that import. The names, the signatures and the sentences are read from the package itself rather than written here, so this page says what the copy of Loom in your node_modules says — and it changes in the same pull request the code does.
43 exports, in 4 modules. Generated from ./dist/write/index.d.ts, which is the declaration file this package publishes for @jam-overture/loom/write.
Nothing to install first. Everything this import loads arrives with @jam-overture/loom itself.
No import here has everything behind it. @jam-overture/loom/write publishes 43 of the 1,298 names this package publishes. The other 1,255 are behind one of the 16 other imports, and not one of those imports publishes a single name this one does. The imports do not nest: a name that is not on this page is not a name that does not exist, and the search at the top of the page says which import it comes from.
Commit
write/commit
The one write path.
WritePathtype
Shown in use on What your app has to do — The three things a change needs
type WritePath = {
readonly store: TreeStore;
readonly holds: HoldStore;
readonly runtime: CompositionRuntime;
};ProposalAnswertype
Answering a held proposal names the proposal and whoever answered it.
type ProposalAnswer = {
readonly proposalId: ProposalId;
readonly actor?: string;
};WriteOutcometype
type WriteOutcome = {
readonly kind: "committed";
readonly tree: LoomTree;
readonly proposal: ProposedChange;
readonly disposition: Disposition;
readonly inverse: TreeDelta;
} | {
readonly kind: "held";
readonly held: HeldProposal;
} | {
readonly kind: "refused";
readonly proposal: ProposedChange;
readonly disposition: Disposition;
/**
* Why no smaller change was offered instead, when a repairer was asked for
* one and declined. Carried straight through from the composition outcome,
* where the three endings a refusal can have are set out.
*/
readonly repairFailure?: InterpretationError;
} | {
readonly kind: "not-interpreted";
readonly error: InterpretationError;
} | {
readonly kind: "not-applicable";
readonly proposal: ProposedChange;
readonly error: TreeError;
}
/** Persistence refused, including the stale-intent case that never reached a model. */
| {
readonly kind: "not-written";
readonly error: StoreError;
}
/** The proposal being confirmed is not in custody — answered already, or never held. */
| {
readonly kind: "not-answerable";
readonly error: HoldError;
};WriteOutcomeKindtype
type WriteOutcomeKind = WriteOutcome["kind"];
WRITE_OUTCOME_KINDSvalue
Every way a write can end, in the order the runtime reaches them.
const WRITE_OUTCOME_KINDS: readonly WriteOutcomeKind[]
describeWriteOutcomefunction
const describeWriteOutcome: (outcome: WriteOutcome) => string
commitIntentfunction
Shown in use on Quickstart — What just happened, Your first change — The same thing, in your own app, What your app has to do — The three things a change needs and What every ask leaves behind — Turning it on is six lines and one decision
const commitIntent: (path: WritePath, intent: EditIntent) => Promise<WriteOutcome>
confirmHeldfunction
Answers a held proposal with yes.
Shown in use on Quickstart — Three asks, three answers, Your first change — The same thing, in your own app, Answering a held change — What saying yes does and What your app has to do — Answering a change that is waiting
const confirmHeld: (path: WritePath, answer: ProposalAnswer) => Promise<WriteOutcome>
discardHeldfunction
Answers a held proposal with no.
Shown in use on Answering a held change — What saying yes does and What your app has to do — Answering a change that is waiting
const discardHeld: (path: WritePath, answer: ProposalAnswer) => Promise<Result<HeldProposal, HoldError>>
Held
write/held
Custody of the changes the Gate held back.
HeldProposaltype
type HeldProposal = {
readonly proposalId: ProposalId;
readonly treeId: TreeId;
/**
* The revision the proposal was judged against. Held separately from the
* delta so a reader can tell a hold is stale without parsing the delta, and
* because it is the intent's promise, not the delta's.
*/
readonly baseRevision: number;
/** Kept whole so a confirmation can be re-judged, and so §6 can pair ask with answer. */
readonly intent: EditIntent;
readonly proposal: ProposedChange;
/** What the Gate said when it held it, so a reviewer sees the reason rather than a verdict. */
readonly disposition: Disposition;
readonly heldAt: string;
};heldProposalSchemaschema
const heldProposalSchema: z.ZodObject<…>
HoldErrortype
type HoldError =
/** Nothing under this id: never held, already answered, or expired. */
{
readonly code: "not-held";
readonly proposalId: ProposalId;
} | {
readonly code: "already-held";
readonly proposalId: ProposalId;
}
/**
* The store did not answer. Nothing is known about what it holds, and the
* same call a moment later may well succeed.
*/
| {
readonly code: "unavailable";
readonly detail: string;
}
/**
* The store answered, and something it returned is not a hold this build can
* read.
*
* Separate from `unavailable` because the two want opposite next moves from
* whoever is reading the screen: a store that did not answer is waited out,
* and a row that did not parse is gone and looked at. A reader told only
* "unavailable" either waits out a problem that does not resolve or
* investigates a blip, and the store is the only party that knows which.
*/
| {
readonly code: "unreadable";
readonly detail: string;
};describeHoldErrorfunction
const describeHoldError: (error: HoldError) => string
parseHeldProposalfunction
Reads a hold back out of storage, or says why it could not.
const parseHeldProposal: (row: unknown) => Result<HeldProposal, HoldError>
HoldPositiontype
Where a page of holds resumes: an instant and an id, never an instant alone.
type HoldPosition = {
readonly heldAt: string;
readonly proposalId: ProposalId;
};compareHoldsfunction
The order both listings are in: oldest first, then by id.
const compareHolds: (left: HoldPosition, right: HoldPosition) => number
holdCursorfunction
const holdCursor: (position: HoldPosition) => string
holdCursorPositionfunction
Reads a cursor back into the position it names, or says it cannot.
const holdCursorPosition: (cursor: string | undefined) => HoldPosition | undefined
holdPositionfunction
The two columns a page is ordered by, checked, or nothing.
const holdPosition: (row: {
readonly heldAt: string;
readonly proposalId: string;
}) => HoldPosition | undefinedDEFAULT_HOLD_LIMITvalue
Smaller than a tree listing's page, because a held proposal is the heaviest row this store keeps: a whole delta, the intent that asked for it, and the judgment that held it back. A reviewer reads them one at a time, so a page is sized for a screen rather than for a fold.
const DEFAULT_HOLD_LIMIT = 25
MAX_HOLD_LIMITvalue
const MAX_HOLD_LIMIT = 100
clampHoldLimitfunction
const clampHoldLimit: (limit: number | undefined) => number
HoldListRequesttype
type HoldListRequest = {
/**
* Opaque to the caller: the previous page's `cursor`, passed back unread. A
* cursor naming a hold that has since been answered is not an error — the
* page resumes from the next position after it, which is what makes a queue
* safe to page through while people are emptying it.
*/
readonly cursor?: string;
/** Clamped by the implementation — see `clampHoldLimit`. */
readonly limit?: number;
};UnreadableHoldtype
A row the listing could place and could not read.
type UnreadableHold = HoldPosition & {
readonly detail: string;
};HoldListingtype
What a listing found, and what it could not read while finding it.
type HoldListing = {
readonly held: readonly HeldProposal[];
readonly unreadable: readonly UnreadableHold[];
};HoldPagetype
type HoldPage = HoldListing & {
/** `null` when this was the last page. */
readonly cursor: string | null;
};HoldStoreinterface
release is a take, not a read: it removes and returns in one step, which is what makes answering a proposal exactly once a property of the store rather than a rule every caller has to remember. Confirming and discarding both go through it, so two confirmations racing cannot both apply the same delta.
interface HoldStore {
readonly hold: (held: HeldProposal) => Promise<Result<HeldProposal, HoldError>>;
readonly get: (proposalId: ProposalId) => Promise<Result<HeldProposal, HoldError>>;
readonly forTree: (treeId: TreeId) => Promise<Result<HoldListing, HoldError>>;
readonly waiting: (request?: HoldListRequest) => Promise<Result<HoldPage, HoldError>>;
readonly release: (proposalId: ProposalId) => Promise<Result<HeldProposal, HoldError>>;
}memoryHoldStorefunction
The reference implementation. Both listings are in compareHolds order, so a queue reads oldest-first: a change that has been waiting longest is the one most likely to be about to go stale.
Shown in use on Going to production — Three places state lives, and only one of them is your pages
const memoryHoldStore: () => HoldStore
Liveness
write/liveness
Which of the changes waiting for an answer can still happen.
HoldLivenesstype
What can be said about a hold, given what is known about its tree.
type HoldLiveness = "live" | "dead" | "unknown";
HOLD_LIVENESSvalue
The three answers, as a list a host can walk.
const HOLD_LIVENESS: readonly HoldLiveness[]
holdLivenessfunction
The one comparison, in the direction confirmHeld makes it.
const holdLiveness: (hold: HeldProposal, headRevision: number | undefined) => HoldLiveness
MarkedHoldtype
A hold and what is known about whether answering it could work.
type MarkedHold = {
readonly held: HeldProposal;
readonly liveness: HoldLiveness;
/** Absent exactly when `liveness` is `unknown`. */
readonly headRevision?: number;
};markHoldsfunction
Marks a page of holds against the revisions the caller already knows.
const markHolds: (holds: readonly HeldProposal[], heads: ReadonlyMap<TreeId, number>) => readonly MarkedHold[]
treesAwaitingAnswerfunction
The trees a page of holds is waiting on, each named once, in first-seen order.
const treesAwaitingAnswer: (holds: readonly HeldProposal[]) => readonly TreeId[]
MarkedHoldstype
The same page of holds, marked against heads read from the store.
type MarkedHolds = {
readonly marked: readonly MarkedHold[];
/**
* One per tree whose head could not be read. Those holds are `unknown`, and
* the errors are handed back rather than swallowed so a host can log them or
* say so on the page.
*/
readonly unreadable: readonly StoreError[];
};markHoldsFromStorefunction
Reads what it needs and never fails.
const markHoldsFromStore: (reader: TreeReader, holds: readonly HeldProposal[]) => Promise<MarkedHolds>
Revert
write/revert
Undo, as a change like any other.
REVERT_INTERPRETERvalue
What produced the delta, for Provenance.interpreter. Not a model.
const REVERT_INTERPRETER = "loom/revert"
RevertablePlantype
type RevertablePlan = Extract<RevertPlan, {
readonly outcome: "revertable";
}>;revertInterpreterfunction
The same interpreter every undo uses, with the log's half filled in.
const revertInterpreter: (plan: RevertablePlan, idFactory: IdFactory, clock: Clock) => ChangeInterpreter
RevertRequesttype
type RevertRequest = {
readonly treeId: TreeId;
/** The revision to undo. */
readonly revision: number;
/** Replayed from, to recover the tree the target's delta observed. */
readonly seed: LoomTree;
readonly origin: IntentOrigin;
readonly actor?: string;
};RevertOutcometype
type RevertOutcome = WriteOutcome
/**
* Nothing was proposed, because no undo could be computed: the revision is
* outside the span the seed reaches, the log does not replay, or the delta does
* not invert. Work the undo would write over is not one of these — that is a
* proposal the Gate holds, not a plan that failed.
*/
| {
readonly kind: "not-revertable";
readonly plan: UnrevertablePlan;
};describeRevertOutcomefunction
const describeRevertOutcome: (outcome: RevertOutcome) => string
revertRevisionfunction
Undoes a revision, if the log allows it and the Gate agrees.
Shown in use on What your app has to do — What you do not have to write
const revertRevision: (path: WritePath, request: RevertRequest) => Promise<RevertOutcome>