@sanity/mutator
Version:
A set of models to make it easier to utilize the powerful real time collaborative features of Sanity
364 lines • 12.9 kB
TypeScript
import { CreateIfNotExistsMutation, CreateMutation, CreateOrReplaceMutation, DeleteMutation, PatchMutation, Path } from "@sanity/types";
/**
* Sanity document with a guaranteed `_id` and `_type`
*
* @internal
*/
interface Doc {
_id: string;
_type: string;
_rev?: string;
_updatedAt?: string;
_createdAt?: string;
[attribute: string]: unknown;
}
/**
* Internal mutation body representation - note that theoretically a
* mutation can only hold one of these operations each, but for sake
* of simpler code it is bundled together as one here
*
* @internal
*/
interface Mut {
create?: CreateMutation['create'];
createIfNotExists?: CreateIfNotExistsMutation['createIfNotExists'];
createOrReplace?: CreateOrReplaceMutation['createOrReplace'];
delete?: DeleteMutation['delete'];
patch?: PatchMutation['patch'];
}
/**
* Parameters attached to the mutation
*
* @internal
*/
interface MutationParams {
transactionId?: string;
transition?: string;
identity?: string;
previousRev?: string;
resultRev?: string;
mutations: Mut[];
timestamp?: string;
effects?: {
apply: unknown;
revert: unknown;
};
}
/**
* A mutation describing a number of operations on a single document.
* This should be considered an immutable structure. Mutations are compiled
* on first application, and any changes in properties will not effectively
* change its behavior after that.
*
* @internal
*/
declare class Mutation {
params: MutationParams;
compiled?: (doc: Doc | null) => Doc | null;
_appliesToMissingDocument: boolean | undefined;
constructor(options: MutationParams);
get transactionId(): string | undefined;
get transition(): string | undefined;
get identity(): string | undefined;
get previousRev(): string | undefined;
get resultRev(): string | undefined;
get mutations(): Mut[];
get timestamp(): Date | undefined;
get effects(): {
apply: unknown;
revert: unknown;
} | undefined;
assignRandomTransactionId(): void;
appliesToMissingDocument(): boolean;
compile(): void;
apply(document: Doc | null): Doc | null;
static applyAll(document: Doc | null, mutations: Mutation[]): Doc | null;
static squash(document: Doc | null, mutations: Mutation[]): Mutation;
}
/**
* @internal
*/
interface SubmissionResponder {
success: () => void;
failure: () => void;
}
/**
* Models a document as it is changed by our own local patches and remote patches coming in from
* the server. Consolidates incoming patches with our own submitted patches and maintains two
* versions of the document. EDGE is the optimistic document that the user sees that will always
* immediately reflect whatever she is doing to it, and HEAD which is the confirmed version of the
* document consistent with the mutations we have received from the server. As long as nothing out of
* the ordinary happens, we can track all changes by hooking into the onMutation callback, but we
* must also respect onRebase events that fire when we have to backtrack because one of our optimistically
* applied patches were rejected, or some bastard was able to slip a mutation in between ours own.
*
* @internal
*/
declare class Document {
/**
* Incoming patches from the server waiting to be applied to HEAD
*/
incoming: Mutation[];
/**
* Patches we know has been subitted to the server, but has not been seen yet in the return channel
* so we can't be sure about the ordering yet (someone else might have slipped something between them)
*/
submitted: Mutation[];
/**
* Pending mutations
*/
pending: Mutation[];
/**
* Our model of the document according to the incoming patches from the server
*/
HEAD: Doc | null;
/**
* Our optimistic model of what the document will probably look like as soon as all our patches
* have been processed. Updated every time we stage a new mutation, but also might revert back
* to previous states if our mutations fail, or could change if unexpected mutations arrive
* between our own. The `onRebase` callback will be called when EDGE changes in this manner.
*/
EDGE: Doc | null;
/**
* Called with the EDGE document when that document changes for a reason other than us staging
* a new patch or receiving a mutation from the server while our EDGE is in sync with HEAD:
* I.e. when EDGE changes because the order of mutations has changed in relation to our
* optimistic predictions.
*/
onRebase?: (edge: Doc | null, incomingMutations: Mutation[], pendingMutations: Mutation[]) => void;
/**
* Called when we receive a patch in the normal order of things, but the mutation is not ours
*/
onMutation?: (msg: {
mutation: Mutation;
document: Doc | null;
remote: boolean;
}) => void;
/**
* Called when consistency state changes with the boolean value of the current consistency state
*/
onConsistencyChanged?: (isConsistent: boolean) => void;
/**
* Called whenever a new incoming mutation comes in. These are always ordered correctly.
*/
onRemoteMutation?: (mut: Mutation) => void;
/**
* We are consistent when there are no unresolved mutations of our own, and no un-applicable
* incoming mutations. When this has been going on for too long, and there has been a while
* since we staged a new mutation, it is time to reset your state.
*/
inconsistentAt: Date | null;
/**
* The last time we staged a patch of our own. If we have been inconsistent for a while, but it
* hasn't been long since we staged a new mutation, the reason is probably just because the user
* is typing or something.
*
* Should be used as a guard against resetting state for inconsistency reasons.
*/
lastStagedAt: Date | null;
constructor(doc: Doc | null);
reset(doc: Doc | null): void;
arrive(mutation: Mutation): void;
stage(mutation: Mutation, silent?: boolean): SubmissionResponder;
isConsistent(): boolean;
considerIncoming(): void;
updateConsistencyFlag(): void;
applyIncoming(mut: Mutation | undefined): boolean;
/**
* Returns true if there are unresolved mutations between HEAD and EDGE, meaning we have
* mutations that are still waiting to be either submitted, or to be confirmed by the server.
*
* @returns true if there are unresolved mutations between HEAD and EDGE, false otherwise
*/
hasUnresolvedMutations(): boolean;
/**
* When an incoming mutation is applied to HEAD, this is called to remove the mutation from
* the unresolved state. If the newly applied patch is the next upcoming unresolved mutation,
* no rebase is needed, but we might have the wrong idea about the ordering of mutations, so in
* that case we are given the flag `needRebase` to tell us that this mutation arrived out of
* order in terms of our optimistic version, so a rebase is needed.
*
* @param txnId - Transaction ID of the remote mutation
* @returns true if rebase is needed, false otherwise
*/
consumeUnresolved(txnId: string): boolean;
pendingSuccessfullySubmitted(pendingTxnId: string): void;
pendingFailed(pendingTxnId: string): void;
rebase(incomingMutations: Mutation[]): void;
}
/**
* Implements a buffer for mutations that incrementally optimises the mutations by
* eliminating set-operations that overwrite earlier set-operations, and rewrite
* set-operations that change strings into other strings into diffMatchPatch operations.
*
* @internal
*/
declare class SquashingBuffer {
/**
* The document forming the basis of this squash
*/
BASIS: Doc | null;
/**
* The document after the out-Mutation has been applied, but before the staged
* operations are committed.
*/
PRESTAGE: Doc | null;
/**
* setOperations contain the latest set operation by path. If the set-operations are
* updating strings to new strings, they are rewritten as diffMatchPatch operations,
* any new set operations on the same paths overwrites any older set operations.
* Only set-operations assigning plain values to plain values gets optimized like this.
*/
setOperations: Record<string, Mut | undefined>;
/**
* `documentPresent` is true whenever we know that the document must be present due
* to preceeding mutations. `false` implies that it may or may not already exist.
*/
documentPresent: boolean;
/**
* The operations in the out-Mutation are not able to be optimized any further
*/
out: Mut[];
/**
* Staged mutation operations
*/
staged: Mut[];
constructor(doc: Doc | null);
add(mut: Mutation): void;
hasChanges(): boolean;
/**
* Extracts the mutations in this buffer.
* After this is done, the buffer lifecycle is over and the client should
* create an new one with the new, updated BASIS.
*
* @param txnId - Transaction ID
* @returns A `Mutation` instance if we had outgoing mutations pending, null otherwise
*/
purge(txnId?: string): Mutation | null;
addOperation(op: Mut): void;
/**
* Attempt to perform one single set operation in an optimised manner, return value
* reflects whether or not the operation could be performed.
* @param path - The JSONPath to the set operation in question
* @param nextValue - The value to be set
* @returns True of optimized, false otherwise
*/
optimiseSetOperation(path: string, nextValue: unknown): boolean;
stashStagedOperations(): void;
/**
* Rebases given the new base-document
*
* @param newBasis - New base document to rebase on
* @returns New "edge" document with buffered changes integrated
*/
rebase(newBasis: Doc | null): Doc | null;
}
/**
* @internal
*/
interface CommitHandlerMessage {
mutation: Mutation;
success: () => void;
failure: () => void;
cancel: (error: Error) => void;
}
/**
* @internal
*/
declare class BufferedDocument {
private mutations;
/**
* The Document we are wrapping
*/
document: Document;
/**
* The Document with local changes applied
*/
LOCAL: Doc | null;
/**
* Commits that are waiting to be delivered to the server
*/
private commits;
/**
* Local mutations that are not scheduled to be committed yet
*/
buffer: SquashingBuffer;
/**
* Assignable event handler for when the buffered document applies a mutation
*/
onMutation?: (message: {
mutation: Mutation;
document: Doc | null;
remote: boolean;
}) => void;
/**
* Assignable event handler for when a remote mutation happened
*/
onRemoteMutation?: Document['onRemoteMutation'];
/**
* Assignable event handler for when the buffered document rebased
*/
onRebase?: (localDoc: Doc | null, remoteMutations: Mut[], localMutations: Mut[]) => void;
/**
* Assignable event handler for when the document is deleted
*/
onDelete?: (doc: Doc | null) => void;
/**
* Assignable event handler for when the state of consistency changed
*/
onConsistencyChanged?: (isConsistent: boolean) => void;
/**
* Assignable event handler for when the buffered document should commit changes
*/
commitHandler?: (msg: CommitHandlerMessage) => void;
/**
* Whether or not we are currently commiting
*/
committerRunning: boolean;
constructor(doc: Doc | null);
reset(doc: Doc | null): void;
add(mutation: Mutation): void;
arrive(mutation: Mutation): void;
commit(): Promise<void>;
performCommits(): void;
_cycleCommitter(): void;
handleDocRebase(edge: Doc | null, remoteMutations: Mutation[], localMutations: Mutation[]): void;
handleDocumentDeleted(): void;
handleDocMutation(msg: {
mutation: Mutation;
document: Doc | null;
remote: boolean;
}): void;
rebase(remoteMutations: Mutation[], localMutations: Mutation[]): void;
handleDocConsistencyChanged(isConsistent: boolean): void;
}
/**
* Converts a path in array form to a JSONPath string
*
* @param pathArray - Array of path segments
* @returns String representation of the path
* @internal
*/
declare function arrayToJSONMatchPath(pathArray: Path): string;
/**
* Extracts values matching the given JsonPath
*
* @param path - Path to extract
* @param value - Value to extract from
* @returns An array of values matching the given path
* @public
*/
declare function extract(path: string, value: unknown): unknown[];
/**
* Extracts a value for the given JsonPath, and includes the specific path of where it was found
*
* @param path - Path to extract
* @param value - Value to extract from
* @returns An array of objects with `path` and `value` keys
* @internal
*/
declare function extractWithPath(path: string, value: unknown): {
path: (string | number)[];
value: unknown;
}[];
export { BufferedDocument, type CommitHandlerMessage, type Doc, type Document, type Mut, Mutation, type MutationParams, type SquashingBuffer, type SubmissionResponder, arrayToJSONMatchPath, extract, extractWithPath };