@sanity/mutate
Version:
Experimental toolkit for working with Sanity mutations in JavaScript & TypeScript
419 lines • 18.2 kB
TypeScript
import { d as SanityMutation } from "./index.js";
import { d as SanityDocumentBase, f as Transaction, o as Mutation } from "./types.js";
import { Observable } from "rxjs";
import { BaseMutationOptions, QueryParams, ReconnectEvent, RequestOptions, SanityClient, WelcomeEvent } from "@sanity/client";
declare type RawOperation = any;
declare type RawPatch = RawOperation[];
interface ListenerSyncEvent<Doc extends SanityDocumentBase = SanityDocumentBase> {
type: 'sync';
document: Doc | undefined;
}
interface ListenerMutationEvent {
type: 'mutation';
documentId: string;
transactionId: string;
resultRev?: string;
previousRev?: string;
effects?: {
apply: RawPatch;
};
mutations: SanityMutation[];
transition: 'update' | 'appear' | 'disappear';
}
interface ListenerReconnectEvent {
type: 'reconnect';
}
type ListenerChannelErrorEvent = {
type: 'channelError';
message: string;
};
type ListenerWelcomeEvent = {
type: 'welcome';
listenerName: string;
};
type ListenerDisconnectEvent = {
type: 'disconnect';
reason: string;
};
type ListenerEndpointEvent = ListenerWelcomeEvent | ListenerMutationEvent | ListenerReconnectEvent | ListenerChannelErrorEvent | ListenerDisconnectEvent;
type ListenerEvent<Doc extends SanityDocumentBase = SanityDocumentBase> = ListenerSyncEvent<Doc> | ListenerMutationEvent | ListenerReconnectEvent;
interface OptimisticDocumentEvent {
type: 'optimistic';
id: string;
before: SanityDocumentBase | undefined;
after: SanityDocumentBase | undefined;
mutations: Mutation[];
stagedChanges: Mutation[];
}
type QueryParams$1 = Record<string, string | number | boolean | (string | number | boolean)[]>;
interface RemoteSyncEvent {
type: 'sync';
id: string;
before: {
local: SanityDocumentBase | undefined;
remote: SanityDocumentBase | undefined;
};
after: {
local: SanityDocumentBase | undefined;
remote: SanityDocumentBase | undefined;
};
rebasedStage: MutationGroup[];
}
interface RemoteMutationEvent {
type: 'mutation';
id: string;
before: {
local: SanityDocumentBase | undefined;
remote: SanityDocumentBase | undefined;
};
after: {
local: SanityDocumentBase | undefined;
remote: SanityDocumentBase | undefined;
};
effects?: {
apply: RawPatch;
};
previousRev?: string;
resultRev?: string;
mutations: Mutation[];
rebasedStage: MutationGroup[];
}
type RemoteDocumentEvent = RemoteSyncEvent | RemoteMutationEvent;
type DocumentMap<Doc extends SanityDocumentBase> = {
get(id: string): Doc | undefined;
set(id: string, doc: Doc | undefined): void;
delete(id: string): void;
};
interface MutationResult {}
interface SubmitResult {}
interface NonTransactionalMutationGroup {
transaction: false;
mutations: Mutation[];
}
interface TransactionalMutationGroup {
transaction: true;
id?: string;
mutations: Mutation[];
}
/**
* A mutation group represents an incoming, locally added group of mutations
* They can either be transactional or non-transactional
* - Transactional means that they must be submitted as a separate transaction (with an optional id) and no other mutations can be mixed with it
* – Non-transactional means that they can be combined with other mutations
*/
type MutationGroup = NonTransactionalMutationGroup | TransactionalMutationGroup;
/**
* # Subscription requirement
*
* `mutate`, `transaction` and `submit` only do useful work while at least one
* subscriber to `listen(id)` is active. `listen(id)` is what wires the rebase
* pipeline (remote events ↔ inflight ↔ local mutations) for a document; with
* no subscriber, the pipeline is torn down and calls to `submit()` are
* silently dropped.
*
* In practice this means: keep a `listen(id)` subscription open for every
* document you intend to read or write. `listenEvents(id)` alone is not
* enough — it provides a richer event stream but does not activate the submit
* pipeline.
*
* In development builds the store will emit a `console.warn` if `submit()` is
* called without an active `listen()` subscriber, to make this contract
* observable.
*/
interface OptimisticStore {
/**
* Subscribe to a stream of rich events for a document (sync, optimistic,
* remote mutation). Useful for inspecting rebase behaviour and staged
* changes.
*
* Note: subscribing to `listenEvents` alone is not sufficient to activate
* the submit pipeline. Use `listen(id)` for that.
*/
listenEvents: (id: string) => Observable<RemoteDocumentEvent | OptimisticDocumentEvent>;
/**
* Stages mutations to be applied optimistically and later submitted to the
* backend. Mutations are not guaranteed to be submitted in the same
* transaction.
*
* Requires at least one active `listen(id)` subscriber covering the
* affected document(s) before `submit()` is called; otherwise the staged
* mutations are dropped when the pipeline tears down.
*/
mutate(mutation: Mutation[]): void;
/**
* Stages mutations to be applied optimistically and submitted as a single
* transaction.
*
* Requires at least one active `listen(id)` subscriber covering the
* affected document(s) before `submit()` is called; otherwise the staged
* mutations are dropped when the pipeline tears down.
*/
transaction(transaction: {
id?: string;
mutations: Mutation[];
} | Mutation[]): void;
/**
* Checkout a document for editing. This is required to be able to see
* optimistic changes and to flush mutations with `submit()`. Subscribing
* keeps the store's rebase pipeline alive for `id`; unsubscribing releases
* it.
*/
listen(id: string): Observable<SanityDocumentBase | undefined>;
/**
* Submit pending mutations to the backend.
*
* Only takes effect while at least one `listen(id)` subscriber is active
* for an affected document. If called with no active subscriber, the
* pending mutations remain staged (and a `console.warn` is emitted in
* development).
*/
submit(): void;
}
/**
* Converts a list of mutation groups into a list of transactions, assigning an ID to each.
* @param groups
*/
declare function toTransactions(groups: MutationGroup[]): Transaction[];
interface DocumentSyncUpdate<Doc extends SanityDocumentBase> {
documentId: string;
snapshot: Doc | undefined;
event: ListenerSyncEvent<Doc>;
}
interface DocumentMutationUpdate<Doc extends SanityDocumentBase> {
documentId: string;
snapshot: Doc | undefined;
event: ListenerMutationEvent;
}
interface DocumentReconnectUpdate<Doc extends SanityDocumentBase> {
documentId: string;
snapshot: Doc | undefined;
event: ListenerReconnectEvent;
}
type DocumentUpdate<Doc extends SanityDocumentBase> = DocumentSyncUpdate<Doc> | DocumentMutationUpdate<Doc> | DocumentReconnectUpdate<any>;
type DocumentUpdateListener<Doc extends SanityDocumentBase> = (id: string) => Observable<DocumentUpdate<Doc>>;
/**
* Creates a function that can be used to listen for document updates
* Emits the latest snapshot of the document along with the latest event
* @param options
*/
declare function createDocumentUpdateListener(options: {
listenDocumentEvents: (documentId: string) => Observable<ListenerEvent>;
}): <Doc extends SanityDocumentBase>(documentId: string) => Observable<DocumentUpdate<Doc>>;
type MapTuple<T, U> = { [K in keyof T]: U };
interface ReadOnlyDocumentStore {
listenDocument: <Doc extends SanityDocumentBase>(id: string) => Observable<DocumentUpdate<Doc>>;
listenDocuments: <Doc extends SanityDocumentBase, const IdTuple extends string[]>(id: IdTuple) => Observable<MapTuple<IdTuple, DocumentUpdate<Doc>>>;
}
/**
* @param listenDocumentUpdates – a function that takes a document id and returns an observable of document snapshots
* @param options
*/
declare function createReadOnlyStore(listenDocumentUpdates: DocumentUpdateListener<SanityDocumentBase>, options?: {
shutdownDelay?: number;
}): ReadOnlyDocumentStore;
interface AccessibleDocumentResult {
id: string;
document: SanityDocumentBase;
accessible: true;
}
type InaccessibleReason = 'existence' | 'permission';
interface InaccessibleDocumentResult {
accessible: false;
id: string;
reason: InaccessibleReason;
}
type DocumentResult = AccessibleDocumentResult | InaccessibleDocumentResult;
type DocumentLoader = (documentIds: string) => Observable<DocumentResult>;
/**
* Creates a function that can be used to listen for events that happens in a single document
* Features
* - builtin retrying and connection recovery (track disconnected state by listening for `reconnect` events)
* - builtin mutation event ordering (they might arrive out of order), lost events detection (/listen endpoint doesn't guarantee delivery) and recovery
* - discards already-applied mutation events received while fetching the initial document snapshot
* @param options
*/
declare function createDocumentEventListener(options: {
loadDocument: DocumentLoader;
listenerEvents: Observable<WelcomeEvent | ListenerMutationEvent | ReconnectEvent>;
}): <Doc extends SanityDocumentBase>(documentId: string) => Observable<ListenerEvent>;
interface OptimisticStoreBackend {
/**
* Sets up a subscription to a document
* The first event should either be a sync event or an error event.
* After that, it should emit mutation events, error events or sync events
* @param id
*/
listen: (id: string) => Observable<ListenerEvent>;
submit: (mutationGroups: Transaction) => Observable<SubmitResult>;
}
/**
* Local state for a document. Tracks inflight mutations and local mutations
* They change at the same time – local always comes after innflight in time
*/
type LocalState = {
readonly base: SanityDocumentBase | undefined;
readonly inflight: readonly Transaction[];
readonly local: readonly MutationGroup[];
};
/**
* 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.
*
* ## Terminology:
*
* ### Mutation buffers
* - *Local*: - an array of mutations only applied locally, waiting to be submitted to the server
* - *In-flight*: - an array of mutation on its way to the server, waiting to be received over the listener
*
* ### Snapshots:
* – *Base*: - a snapshot of the document consistent with the mutations we have received from the server.
* - *Edge*: - The base snapshot with in-flight mutations applied to it - presumably what will soon become the next base
* - *Local*: - the optimistic document that the user sees that will always immediately reflect whatever they are doing to it
*
*
* Creates a local dataset that allows subscribing to documents by id and submitting mutations to be optimistically applied
* @param backend
*/
declare function createOptimisticStore(backend: OptimisticStoreBackend): OptimisticStore;
type OptimisticStoreInternalConfig = {
/**
* Stream of local mutations that should be applied optimistically and be scheduled for later submission
*/
localMutations: Observable<MutationGroup>;
/**
* Stream of requests to submit local changes
*/
onSubmitLocal: Observable<void>;
/**
* A function that when called with an id must return a stream of listener events
* @param id
*/
listen: (id: string) => Observable<ListenerEvent>;
/**
* A function that, when called, must submit the given mutation groups to the backend
* @param mutationGroups
*/
submitTransactions: (mutationGroups: Transaction) => Observable<SubmitResult>;
};
declare function createOptimisticStoreInternal(config: OptimisticStoreInternalConfig): {
listen(id: string): Observable<SanityDocumentBase | undefined>;
};
interface SanityClientLike {
dataRequest(endpoint: string, body: unknown, options?: BaseMutationOptions): Promise<SubmitResult>;
getDataUrl(doc: string, s: string): string;
observable: {
request<T>(options: RequestOptions): Observable<T>;
};
listen(query: string, queryParams: QueryParams, request: RequestOptions): Observable<ListenerEndpointEvent>;
}
declare function createOptimisticStoreClientBackend(client: SanityClientLike): OptimisticStoreBackend;
type FetchDocuments = (ids: string[]) => Observable<DocEndpointResponse>;
interface OmittedDocument {
id: string;
reason: 'existence' | 'permission';
}
interface DocEndpointResponse {
documents: SanityDocumentBase[];
omitted: OmittedDocument[];
}
/**
* Creates a "dataloader" style document loader that fetches from the /doc endpoint
* @param {FetchDocuments} fetchDocuments - The client instance used for fetching documents.
* @param options
*/
declare function createDocumentLoader(fetchDocuments: FetchDocuments, options?: {
durationSelector?: () => Observable<unknown>;
tag?: string;
}): (key: string) => Observable<DocumentResult>;
declare function createDocumentLoaderFromClient(client: SanityClientLike, options?: {
durationSelector?: () => Observable<unknown>;
tag?: string;
}): (key: string) => Observable<DocumentResult>;
type DocumentIdSetState = {
status: 'connecting' | 'reconnecting' | 'connected';
event: DocumentIdSetEvent | InitialEvent;
snapshot: string[];
};
type InitialEvent = {
type: 'connect';
};
type InsertMethod = 'sorted' | 'prepend' | 'append';
type DocumentIdSetEvent = {
type: 'sync';
documentIds: string[];
} | {
type: 'reconnect';
} | {
type: 'op';
op: 'add' | 'remove';
documentId: string;
};
type FetchDocumentIdsFn = (query: string, params?: QueryParams$1, options?: {
tag?: string;
}) => Observable<string[]>;
type IdSetListenFn = (query: string, params?: QueryParams$1, options?: {
visibility: 'transaction';
events: ['welcome', 'mutation', 'reconnect'];
includeResult: false;
includeMutations: false;
tag?: string;
}) => Observable<ListenerEndpointEvent>;
declare function createIdSetListener(listen: IdSetListenFn, fetch: FetchDocumentIdsFn): (queryFilter: string, params: QueryParams$1, options?: {
tag?: string;
}) => Observable<DocumentIdSetEvent>;
declare function createIdSetListenerFromClient(client: SanityClient): void;
/** Converts a stream of id set listener events into a state containing the list of document ids */
declare function toState(options?: {
insert?: InsertMethod;
}): (input$: Observable<DocumentIdSetEvent>) => Observable<DocumentIdSetState>;
interface ListenerOptions {
/**
* Provide a custom filter to the listener. By default, this listener will include all events
* Note: make sure the filter includes events from documents you will subscribe to.
*/
filter?: string;
/**
* Whether to include system documents or not
* This will be ignored if a custom filter is provided
*/
includeSystemDocuments?: boolean;
/**
* How long after the last subscriber is unsubscribed to keep the connection open
*/
shutdownDelay?: number;
/**
* Include mutations in listener events
*/
includeMutations?: boolean;
/**
* Request tag
*/
tag?: string;
}
type SharedListenerListenFn = (query: string, queryParams: QueryParams$1, options: RequestOptions$1) => Observable<ListenerEndpointEvent>;
/**
* These are fixed, and it's up to the implementation of the listen function to turn them into request parameters
*/
interface RequestOptions$1 {
events: ['welcome', 'mutation', 'reconnect'];
includeResult: false;
includePreviousRevision: false;
visibility: 'transaction';
effectFormat: 'mendoza';
includeMutations?: boolean;
tag?: string;
}
/**
* Creates a (low level) shared listener that will emit 'welcome' for all new subscribers immediately, and thereafter emit every listener event, including welcome, mutation, and reconnects
* Requires a Sanity client instance
*/
declare function createSharedListenerFromClient(client: SanityClientLike, options?: ListenerOptions): Observable<WelcomeEvent | ListenerMutationEvent | ReconnectEvent>;
/**
* Creates a (low level) shared listener that will emit 'welcome' for all new subscribers immediately, and thereafter emit every listener event, including welcome, mutation, and reconnects
* Useful for cases where you need control of how the listen request is set up
*/
declare function createSharedListener(listen: SharedListenerListenFn, options?: ListenerOptions): Observable<WelcomeEvent | ListenerMutationEvent | ReconnectEvent>;
declare function createOptimisticStoreInMemoryBackend(): OptimisticStoreBackend;
export { MutationResult as $, DocumentResult as A, DocumentUpdateListener as B, LocalState as C, createDocumentEventListener as D, createOptimisticStoreInternal as E, createReadOnlyStore as F, ListenerDisconnectEvent as G, toTransactions as H, DocumentMutationUpdate as I, ListenerMutationEvent as J, ListenerEndpointEvent as K, DocumentReconnectUpdate as L, InaccessibleReason as M, MapTuple as N, AccessibleDocumentResult as O, ReadOnlyDocumentStore as P, MutationGroup as Q, DocumentSyncUpdate as R, createOptimisticStoreClientBackend as S, createOptimisticStore as T, DocumentMap as U, createDocumentUpdateListener as V, ListenerChannelErrorEvent as W, ListenerSyncEvent as X, ListenerReconnectEvent as Y, ListenerWelcomeEvent as Z, FetchDocuments as _, createSharedListener as a, RemoteMutationEvent as at, createDocumentLoaderFromClient as b, DocumentIdSetState as c, TransactionalMutationGroup as ct, InitialEvent as d, NonTransactionalMutationGroup as et, InsertMethod as f, DocEndpointResponse as g, toState as h, SharedListenerListenFn as i, RemoteDocumentEvent as it, InaccessibleDocumentResult as j, DocumentLoader as k, FetchDocumentIdsFn as l, RawPatch as lt, createIdSetListenerFromClient as m, ListenerOptions as n, OptimisticStore as nt, createSharedListenerFromClient as o, RemoteSyncEvent as ot, createIdSetListener as p, ListenerEvent as q, RequestOptions$1 as r, QueryParams$1 as rt, DocumentIdSetEvent as s, SubmitResult as st, createOptimisticStoreInMemoryBackend as t, OptimisticDocumentEvent as tt, IdSetListenFn as u, OmittedDocument as v, OptimisticStoreBackend as w, SanityClientLike as x, createDocumentLoader as y, DocumentUpdate as z };
//# sourceMappingURL=index3.d.ts.map