skip to the page

@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 | undefined

DEFAULT_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>