UNPKG

@sanity/mutate

Version:

Experimental toolkit for working with Sanity mutations in JavaScript & TypeScript

992 lines (991 loc) 34.3 kB
import { combineLatest, finalize, share, ReplaySubject, timer, switchMap, concat, of, throwError, concatMap, EMPTY, catchError, map, BehaviorSubject, Subject, filter, bufferWhen, mergeMap as mergeMap$1, takeUntil, Observable, defer, merge, takeWhile, scheduled, asyncScheduler, tap, from, withLatestFrom, startWith } from "rxjs"; import { TTLCache } from "@isaacs/ttlcache"; import { scan, mergeMap, map as map$1, filter as filter$1 } from "rxjs/operators"; import lodashPartition from "lodash/partition.js"; import keyBy from "lodash/keyBy.js"; import { decodeAll } from "./_chunks-es/decode.js"; import { applyAll, applyMutations, createTransactionId, getMutationDocumentId, rebase, toTransactions, squashDMPStrings, squashMutationGroups } from "./_chunks-es/toTransactions.js"; import { applyPatch } from "mendoza"; import sortedIndex from "lodash/sortedIndex.js"; import { encodeTransaction, encodeAll } from "./_chunks-es/encode.js"; function createReadOnlyStore(listenDocumentUpdates, options = {}) { const cache = /* @__PURE__ */ new Map(), { shutdownDelay } = options; function listenDocument(id) { if (cache.has(id)) return cache.get(id); const cached = listenDocumentUpdates(id).pipe( finalize(() => cache.delete(id)), share({ resetOnRefCountZero: typeof shutdownDelay == "number" ? () => timer(shutdownDelay) : !0, connector: () => new ReplaySubject(1) }) ); return cache.set(id, cached), cached; } return { listenDocument, listenDocuments(ids) { return combineLatest( ids.map((id) => listenDocument(id)) ); } }; } class FetchError extends Error { cause; constructor(message, extra) { super(message), this.cause = extra?.cause, this.name = "FetchError"; } } class PermissionDeniedError extends Error { cause; constructor(message, extra) { super(message), this.cause = extra?.cause, this.name = "PermissionDeniedError"; } } class ChannelError extends Error { constructor(message) { super(message), this.name = "ChannelError"; } } class DisconnectError extends Error { constructor(message) { super(message), this.name = "DisconnectError"; } } class OutOfSyncError extends Error { /** * Attach state to the error for debugging/reporting */ state; constructor(message, state) { super(message), this.name = "OutOfSyncError", this.state = state; } } class DeadlineExceededError extends OutOfSyncError { constructor(message, state) { super(message, state), this.name = "DeadlineExceededError"; } } class MaxBufferExceededError extends OutOfSyncError { constructor(message, state) { super(message, state), this.name = "MaxBufferExceededError"; } } function isClientError(e) { return typeof e != "object" || !e ? !1 : "statusCode" in e && "response" in e; } const DEFAULT_TTL = 12e4, DEFAULT_MAX_ENTRIES = 1e3; function dedupeListenerEvents({ ttl = DEFAULT_TTL, max = DEFAULT_MAX_ENTRIES } = {}) { return (input$) => input$.pipe( scan( ({ seen }, event) => { if (event.type !== "mutation") return { emit: [event], seen }; const key = `${event.transactionId}#${event.documentId}`; return seen.has(key) ? { seen, emit: [] } : (seen.set(key, null), { seen, emit: [event] }); }, { emit: [], seen: new TTLCache({ ttl, max }) } ), mergeMap( (state) => ( // note: if state.emit is an empty array, nothing will be emitted state.emit ) ) ); } function discardChainTo(chain, revision) { const revisionIndex = chain.findIndex((event) => event.resultRev === revision); return split(chain, revisionIndex + 1); } function split(array, index) { return index < 0 ? [[], array] : [array.slice(0, index), array.slice(index)]; } function toOrderedChains(events) { const parents = {}; return events.forEach((event) => { parents[event.resultRev || "undefined"] = events.find( (other) => other.resultRev === event.previousRev ); }), Object.entries(parents).filter(([, parent]) => !parent).map((orphan) => { const [headRev] = orphan; let current = events.find((event) => event.resultRev === headRev); const sortedList = []; for (; current; ) sortedList.push(current), current = events.find((event) => event.previousRev === current?.resultRev); return sortedList; }); } function partition(array, predicate) { return lodashPartition(array, predicate); } const DEFAULT_MAX_BUFFER_SIZE = 20, DEFAULT_DEADLINE_MS = 3e4, EMPTY_ARRAY$1 = []; function sequentializeListenerEvents(options) { const { resolveChainDeadline = DEFAULT_DEADLINE_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE, onDiscard, onBrokenChain } = options || {}; return (input$) => input$.pipe( scan( (state, event) => { if (event.type === "mutation" && !state.base) throw new Error( "Invalid state. Cannot create a sequence without a base" ); if (event.type === "sync") return { base: { revision: event.document?._rev }, buffer: EMPTY_ARRAY$1, emitEvents: [event] }; if (event.type === "mutation") { if (!event.resultRev && !event.previousRev) throw new Error( "Invalid mutation event: Events must have either resultRev or previousRev" ); const orderedChains = toOrderedChains( state.buffer.concat(event) ).map((chain) => { const [discarded, rest] = discardChainTo( chain, state.base.revision ); return onDiscard && discarded.length > 0 && onDiscard(discarded), rest; }), [applicableChains, _nextBuffer] = partition( orderedChains, (chain) => state.base.revision === chain[0]?.previousRev ), nextBuffer = _nextBuffer.flat(); if (applicableChains.length > 1) throw new Error("Expected at most one applicable chain"); if (applicableChains.length > 0 && applicableChains[0].length > 0) { const lastMutation = applicableChains[0].at(-1); return { base: { revision: ( // special case: if the mutation deletes the document it technically has no revision, despite // resultRev pointing at a transaction id. lastMutation.transition === "disappear" ? void 0 : lastMutation?.resultRev ) }, emitEvents: applicableChains[0], buffer: nextBuffer }; } if (nextBuffer.length >= maxBufferSize) throw new MaxBufferExceededError( `Too many unchainable mutation events: ${state.buffer.length}`, state ); return { ...state, buffer: nextBuffer, emitEvents: EMPTY_ARRAY$1 }; } return { ...state, emitEvents: [event] }; }, { emitEvents: EMPTY_ARRAY$1, base: void 0, buffer: EMPTY_ARRAY$1 } ), switchMap((state) => state.buffer.length > 0 ? (onBrokenChain?.(state.buffer), concat( of(state), timer(resolveChainDeadline).pipe( mergeMap( () => throwError(() => new DeadlineExceededError( `Did not resolve chain within a deadline of ${resolveChainDeadline}ms`, state )) ) ) )) : of(state)), mergeMap((state) => state.emitEvents) ); } function createDocumentEventListener(options) { const { listenerEvents, loadDocument } = options; return function(documentId) { return listenerEvents.pipe( concatMap((event) => event.type === "mutation" ? event.documentId === documentId ? of(event) : EMPTY : event.type === "reconnect" ? of(event) : event.type === "welcome" ? loadDocument(documentId).pipe( catchError((err) => { const error = toError(err); return isClientError(error) ? throwError(() => error) : throwError( () => new FetchError( `An unexpected error occurred while fetching document: ${error?.message}`, { cause: error } ) ); }), map((result) => { if (result.accessible) return result.document; if (result.reason === "permission") throw new PermissionDeniedError( `Permission denied. Make sure the current user (or token) has permission to read the document with ID="${documentId}".` ); }), map( (doc) => ({ type: "sync", document: doc }) ) ) : EMPTY), dedupeListenerEvents(), sequentializeListenerEvents({ maxBufferSize: 10, resolveChainDeadline: 1e4 }) ); }; } function toError(maybeErr) { return maybeErr instanceof Error ? maybeErr : typeof maybeErr == "object" && maybeErr ? Object.assign(new Error(), maybeErr) : new Error(String(maybeErr)); } const defaultDurationSelector = () => scheduled(of(0), asyncScheduler); function createDataLoader(options) { const durationSelector = options.durationSelector || defaultDurationSelector, requests$ = new BehaviorSubject(void 0), unsubscribes$ = new Subject(), batchResponses = requests$.pipe( filter((req) => !!req), bufferWhen(durationSelector), map((requests) => requests.filter((request) => !request.cancelled)), filter((requests) => requests.length > 0), mergeMap$1((requests) => { const keys = requests.map((request) => request.key), responses = options.onLoad(keys).pipe( takeUntil( unsubscribes$.pipe( filter(() => requests.every((request) => request.cancelled)) ) ), mergeMap$1((batchResult) => { if (batchResult.length !== requests.length) throw new Error( `The length of the returned batch must be equal to the number of batched requests. Requested a batch of length ${requests.length}, but received a batch of ${batchResult.length}.` ); return requests.map((request, i) => ({ type: "value", request, response: batchResult[i] })); }) ), responseEnds = requests.map((request) => ({ request, type: "complete" })); return concat(responses, responseEnds); }), share() ); return (key) => new Observable((subscriber) => { const mutableRequestState = { key, cancelled: !1 }, emit = defer(() => (requests$.next(mutableRequestState), EMPTY)), subscription = merge( batchResponses.pipe( filter((batchResult) => batchResult.request === mutableRequestState), takeWhile((batchResult) => batchResult.type !== "complete"), map((batchResult) => batchResult.response) ), emit ).subscribe(subscriber); return () => { mutableRequestState.cancelled = !0, unsubscribes$.next(), subscription.unsubscribe(); }; }); } function createDocumentLoader(fetchDocuments, options) { return createDataLoader({ onLoad: (ids) => fetchDedupedWith(fetchDocuments, ids), durationSelector: options?.durationSelector }); } function createDocumentLoaderFromClient(client, options) { return createDocumentLoader((ids) => { const requestOptions = { uri: client.getDataUrl("doc", ids.join(",")), json: !0, tag: options?.tag }; return client.observable.request(requestOptions); }, options); } function fetchDedupedWith(fetchDocuments, ids) { const unique = [...new Set(ids)]; return fetchDocuments(unique).pipe( map((results) => prepareResponse(ids, results)), map((results) => { const byId = keyBy(results, (result) => result.id); return ids.map((id) => byId[id]); }) ); } function prepareResponse(requestedIds, response) { const documents = keyBy(response.documents, (entry) => entry._id), omitted = keyBy(response.omitted, (entry) => entry.id); return requestedIds.map((id) => { if (documents[id]) return { id, accessible: !0, document: documents[id] }; const omittedEntry = omitted[id]; return omittedEntry ? omittedEntry.reason === "permission" ? { id, accessible: !1, reason: "permission" } : { id, accessible: !1, reason: "existence" } : { id, accessible: !1, reason: "existence" }; }); } function omitRev(document) { if (document === void 0) return; const { _rev, ...doc } = document; return doc; } function applyMendozaPatch(document, patch, patchBaseRev) { if (patchBaseRev !== document?._rev) throw new Error( "Invalid document revision. The provided patch is calculated from a different revision than the current document" ); const next = applyPatch(omitRev(document), patch); return next === null ? void 0 : next; } function applyMutationEventEffects(document, event) { if (!event.effects) throw new Error( "Mutation event is missing effects. Is the listener set up with effectFormat=mendoza?" ); const next = applyMendozaPatch( document, event.effects.apply, event.previousRev ); return next ? { ...next, _rev: event.resultRev } : void 0; } function hasProperty(value, property) { const val = value[property]; return typeof val < "u" && val !== null; } function createDocumentUpdateListener(options) { const { listenDocumentEvents } = options; return function(documentId) { return listenDocumentEvents(documentId).pipe( scan( (prev, event) => { if (event.type === "sync") return { event, documentId, snapshot: event.document }; if (event.type === "mutation") { if (prev?.event === void 0) throw new Error( "Received a mutation event before sync event. Something is wrong" ); if (hasProperty(event, "effects")) return { event, documentId, snapshot: applyMutationEventEffects( prev.snapshot, event ) }; if (hasProperty(event, "mutations")) return { event, documentId, snapshot: applyAll( prev.snapshot, decodeAll(event.mutations) ) }; throw new Error( "No effects found on listener event. The listener must be set up to use effectFormat=mendoza." ); } return { documentId, snapshot: prev?.snapshot, event }; }, void 0 ), // ignore seed value filter((update) => update !== void 0) ); }; } const INITIAL_STATE = { status: "connecting", event: { type: "connect" }, snapshot: [] }; function createIdSetListener(listen, fetch) { return function(queryFilter, params, options = {}) { const { tag } = options, query = `*[${queryFilter}]._id`; function fetchFilter() { return fetch(query, params, { tag: tag ? tag + ".fetch" : void 0 }).pipe( map$1((result) => { if (!Array.isArray(result)) throw new Error( `Expected query to return array of documents, but got ${typeof result}` ); return result; }) ); } return listen(query, params, { visibility: "transaction", events: ["welcome", "mutation", "reconnect"], includeResult: !1, includeMutations: !1, tag: tag ? tag + ".listen" : void 0 }).pipe( mergeMap((event) => event.type === "welcome" ? fetchFilter().pipe(map$1((result) => ({ type: "sync", result }))) : of(event)), map$1((event) => { if (event.type === "mutation") return event.transition === "update" ? void 0 : event.transition === "appear" ? { type: "op", op: "add", documentId: event.documentId } : event.transition === "disappear" ? { type: "op", op: "remove", documentId: event.documentId } : void 0; if (event.type === "sync") return { type: "sync", documentIds: event.result }; if (event.type === "reconnect") return { type: "reconnect" }; }), // ignore undefined filter$1((ev) => !!ev) ); }; } function createIdSetListenerFromClient(client) { } function toState(options = {}) { const { insert: insertOption = "sorted" } = options; return (input$) => input$.pipe( scan((state, event) => { if (event.type === "reconnect") return { ...state, event, status: "reconnecting" }; if (event.type === "sync") return { ...state, event, status: "connected" }; if (event.type === "op") { if (event.op === "add") return { event, status: "connected", snapshot: insert(state.snapshot, event.documentId, insertOption) }; if (event.op === "remove") return { event, status: "connected", snapshot: state.snapshot.filter((id) => id !== event.documentId) }; throw new Error(`Unexpected operation: ${event.op}`); } return state; }, INITIAL_STATE) ); } function insert(array, element, strategy) { let index; return strategy === "prepend" ? index = 0 : strategy === "append" ? index = array.length : index = sortedIndex(array, element), array.toSpliced(index, 0, element); } function shareReplayLatest(configOrPredicate, config) { return _shareReplayLatest( typeof configOrPredicate == "function" ? { predicate: configOrPredicate, ...config } : configOrPredicate ); } function _shareReplayLatest(config) { return (source) => { let latest, emitted = !1; const { predicate, ...shareConfig } = config, wrapped = source.pipe( tap((value) => { config.predicate(value) && (emitted = !0, latest = value); }), finalize(() => { emitted = !1, latest = void 0; }), share(shareConfig) ), emitLatest = new Observable((subscriber) => { emitted && subscriber.next(latest), subscriber.complete(); }); return merge(wrapped, emitLatest); }; } function withListenErrors() { return (input$) => input$.pipe( map((event) => { if (event.type === "mutation") return event; if (event.type === "disconnect") throw new DisconnectError(`DisconnectError: ${event.reason}`); if (event.type === "channelError") throw new ChannelError(`ChannelError: ${event.message}`); return event; }) ); } function createSharedListenerFromClient(client, options) { return createSharedListener((query, queryParams, request) => client.listen( query, queryParams, request ), options); } function createSharedListener(listen, options = {}) { const { filter: filter2, tag, shutdownDelay, includeSystemDocuments, includeMutations } = options, query = filter2 ? `*[${filter2}]` : includeSystemDocuments ? '*[!(_id in path("_.**"))]' : "*"; return listen( query, {}, { events: ["welcome", "mutation", "reconnect"], includeResult: !1, includePreviousRevision: !1, visibility: "transaction", effectFormat: "mendoza", ...includeMutations ? {} : { includeMutations: !1 }, tag } ).pipe( shareReplayLatest({ // note: resetOnError and resetOnComplete are both default true resetOnError: !0, resetOnComplete: !0, predicate: (event) => event.type === "welcome" || event.type === "reconnect", resetOnRefCountZero: typeof shutdownDelay == "number" ? () => timer(shutdownDelay) : !0 }), withListenErrors() ); } function createOptimisticStoreClientBackend(client) { return { listen: createDocumentEventListener({ loadDocument: createDocumentLoaderFromClient(client), listenerEvents: createSharedListenerFromClient(client) }), submit: (transaction) => from( client.dataRequest("mutate", encodeTransaction(transaction), { visibility: "async", returnDocuments: !1 }) ) }; } function createDocumentMap() { const documents = /* @__PURE__ */ new Map(); return { set: (id, doc) => { documents.set(id, doc); }, get: (id) => documents.get(id), delete: (id) => documents.delete(id) }; } function createWelcomeEvent() { return { type: "welcome", listenerName: "in-memory-" + Math.random().toString(32).substring(2) }; } function createInMemoryBackend() { const store = createDocumentMap(), listenerEvents = new Subject(); return { listen: (query) => concat( of(createWelcomeEvent()), listenerEvents.pipe(filter((m) => m.type === "mutation")) ), getDocuments(ids) { const docs = ids.map((id) => ({ id, document: store.get(id) })), [existing, omitted] = lodashPartition(docs, (entry) => entry.document); return of({ documents: existing.map((entry) => entry.document), omitted: omitted.map((entry) => ({ id: entry.id, reason: "existence" })) }); }, submit: (transaction) => (applyMutations( transaction.mutations, store, transaction.id ).forEach((res) => { listenerEvents.next({ type: "mutation", documentId: res.id, mutations: encodeAll(res.mutations), transactionId: transaction.id || createTransactionId(), previousRev: res.before?._rev, resultRev: res.after?._rev, transition: res.after === void 0 ? "disappear" : res.before === void 0 ? "appear" : "update" }); }), of({})) }; } function createOptimisticStoreInMemoryBackend() { const backend = createInMemoryBackend(), sharedListener = createSharedListener( (query, options) => backend.listen(query) ), loadDocument = createDocumentLoader((ids) => backend.getDocuments(ids)); return { listen: createDocumentEventListener({ loadDocument, listenerEvents: sharedListener }), submit: backend.submit }; } function filterDocumentTransactions(transactions, id) { return transactions.flatMap( (transaction) => transaction.mutations.flatMap( (mut) => getMutationDocumentId(mut) === id ? [mut] : [] ) ); } function createOptimisticStore(backend) { const localMutations$ = new Subject(), onSubmitLocal = new Subject(), store = createOptimisticStoreInternal({ localMutations: localMutations$, listen: backend.listen, onSubmitLocal, submitTransactions: backend.submit }); let activeListenSubscribers = 0; return { listenEvents(id) { return new Observable((subscriber) => { let state = { remote: void 0, local: void 0, stagedChanges: [], hasSynced: !1, pendingSyncEmit: !1, prevLocal: void 0, prevRemote: void 0 }, syncEmitScheduled = !1; const emitSyncEvent = (isFromMutation = !1) => { if (state.pendingSyncEmit && state.hasSynced) { const afterLocal = isFromMutation ? state.remote : state.local, syncEvent = { type: "sync", id, before: { local: state.prevLocal, remote: state.prevRemote }, after: { local: afterLocal, remote: state.remote }, rebasedStage: state.stagedChanges }; state = { ...state, pendingSyncEmit: !1 }, subscriber.next(syncEvent); } }, scheduleSyncEmit = () => { syncEmitScheduled || (syncEmitScheduled = !0, Promise.resolve().then(() => { syncEmitScheduled = !1, emitSyncEvent(); })); }, remoteEvents$ = backend.listen(id).pipe(share()), subscription = merge( remoteEvents$.pipe( map((event) => ({ source: "remote", event })) ), localMutations$.pipe( map((group) => ({ source: "local", group })) ) ).subscribe({ next: (action) => { if (action.source === "remote") { const event = action.event; if (event.type === "sync") { const newRemote = event.document; try { const [rebasedMutations] = rebase( id, state.remote, newRemote, state.stagedChanges ), newLocal = applyAll( newRemote, filterDocumentTransactions(rebasedMutations, id) ); state = { remote: newRemote, local: newLocal, stagedChanges: rebasedMutations, hasSynced: !0, pendingSyncEmit: !0, prevLocal: state.local, prevRemote: state.remote }, rebasedMutations.length > 0 ? emitSyncEvent(!1) : scheduleSyncEmit(); } catch (err) { subscriber.error(err); return; } } } else { const newStagedChanges = [...state.stagedChanges, action.group], newLocal = applyAll( state.remote, filterDocumentTransactions(newStagedChanges, id) ), wasPendingSync = state.pendingSyncEmit, beforeLocal = wasPendingSync ? state.remote : state.local; if (wasPendingSync) state = { ...state, stagedChanges: newStagedChanges, local: newLocal }, emitSyncEvent(!0); else if (state.hasSynced) { const syncEvent = { type: "sync", id, before: { local: state.local, remote: state.remote }, after: { local: newLocal, remote: state.remote }, rebasedStage: newStagedChanges }; subscriber.next(syncEvent); } const optimisticEvent = { type: "optimistic", id, before: beforeLocal, after: newLocal, mutations: [], stagedChanges: action.group.mutations }; state = { ...state, local: newLocal, stagedChanges: newStagedChanges, pendingSyncEmit: !1, prevLocal: beforeLocal, prevRemote: state.remote }, subscriber.next(optimisticEvent); } }, error: (err) => subscriber.error(err), complete: () => subscriber.complete() }); return () => subscription.unsubscribe(); }); }, submit: () => { if (activeListenSubscribers === 0) { console.warn( "[@sanity/mutate] submit() was called without an active listen() subscriber. Pending mutations will not be sent to the backend until at least one listen(id) subscription exists and submit() is called again. See the OptimisticStore docs for details." ); return; } onSubmitLocal.next(); }, listen(id) { return defer(() => (activeListenSubscribers++, store.listen(id))).pipe( finalize(() => { activeListenSubscribers--; }) ); }, mutate(mutations) { localMutations$.next({ transaction: !1, mutations }); }, transaction(mutationsOrTransaction) { const transaction = Array.isArray( mutationsOrTransaction ) ? { mutations: mutationsOrTransaction, transaction: !0 } : { ...mutationsOrTransaction, transaction: !0 }; localMutations$.next(transaction); } }; } const EMPTY_ARRAY = Object.freeze([]), SEED_STATE = Object.freeze({ inflight: EMPTY_ARRAY, local: EMPTY_ARRAY, base: void 0 }); function createOptimisticStoreInternal(config) { const edge = createDocumentMap(), { onSubmitLocal, localMutations, listen, submitTransactions } = config, rebasedMutations = new Subject(), clearPendingMutations = new Subject(); function listenDocumentUpdates(documentId) { return listen(documentId).pipe( scan( (prev, event) => { if (event.type === "sync") return { event, documentId, snapshot: event.document }; if (event.type === "mutation") { if (prev?.event === void 0) throw new Error( "Received a mutation event before sync event. Something is wrong" ); if (hasProperty(event, "effects")) return { event, documentId, snapshot: applyMutationEventEffects( prev.snapshot, event ) }; if (hasProperty(event, "mutations")) return { event, documentId, snapshot: applyAll( prev.snapshot, decodeAll(event.mutations) ) }; throw new Error( "No effects or mutations found on listener event. The listener must be set up to either use effectFormat=mendoza (recommended) or includeMutations=true." ); } return { documentId, snapshot: prev?.snapshot, event }; }, void 0 ), // ignore seed value filter((update) => update !== void 0) ); } const pendingMutations = merge( localMutations.pipe( map((local) => ({ type: "add", mutations: local })) ), rebasedMutations.pipe( // if pending mutations are rebased map((mutations) => ({ type: "rebase", mutations })) ), clearPendingMutations.pipe( // clear pending mutations after they've been captured for submit map(() => ({ type: "clear" })) ) ).pipe( scan((current, action) => action.type === "rebase" ? action.mutations : action.type === "add" ? current.concat(action.mutations) : action.type === "clear" ? [] : current, []) ), submitRequests = onSubmitLocal.pipe( withLatestFrom(pendingMutations), mergeMap$1(([, mutationGroups]) => { clearPendingMutations.next(); const transactions = toTransactions( squashDMPStrings(edge, squashMutationGroups(mutationGroups)) ); return concat( of({ type: "submit", transaction: transactions }) ); }), concatMap( (submitRequest) => merge( of(submitRequest), from(submitRequest.transaction).pipe( concatMap((transaction) => submitTransactions(transaction)), mergeMap$1(() => EMPTY) ) ) ), share() ); return { listen(id) { const remoteUpdates = listenDocumentUpdates(id).pipe(share()), remoteMutations = remoteUpdates.pipe( filter( (update) => update.event.type === "mutation" ), map((update) => ({ base: update.snapshot, type: "remoteMutation", transactionId: update.event.transactionId })) ), remoteSync = remoteUpdates.pipe( filter((update) => update.event.type === "sync"), map((update) => ({ type: "sync", snapshot: update.snapshot })) ); return merge( remoteSync, submitRequests, localMutations.pipe( map((m) => ({ type: "localMutation", mutations: m })) ), remoteMutations ).pipe( scan((state, ev) => { const { base, inflight, local } = state; if (ev.type === "sync") { const docRev = ev.snapshot?._rev, newInflight = docRev ? inflight.filter((tx) => tx.id !== docRev) : inflight; return { ...state, base: ev.snapshot, inflight: newInflight }; } if (ev.type === "localMutation") return { base, inflight, local: local.concat(ev.mutations) }; if (ev.type === "remoteMutation") { if (inflight[0]?.id === ev.transactionId) return { base: ev.base, inflight: inflight.slice(1), local }; const newEdge = applyAll( ev.base, filterDocumentTransactions(inflight, id) ), oldEdge = edge.get(id), [newLocalMutations] = rebase(id, oldEdge, newEdge, local); return rebasedMutations.next(newLocalMutations), { base: ev.base, inflight, local: newLocalMutations }; } return ev.type === "submit" ? { base, inflight: inflight.concat(ev.transaction), local: [] } : (console.warn('Unhandled "%s" event', ev.type), state); }, SEED_STATE), startWith({ inflight: [], local: [], base: void 0 }), map((state) => { const nextEdge = applyAll( state.base, filterDocumentTransactions(state.inflight, id) ); return edge.set(id, nextEdge), applyAll( nextEdge, filterDocumentTransactions(state.local, id) ); }) ); } }; } export { createDocumentEventListener, createDocumentLoader, createDocumentLoaderFromClient, createDocumentUpdateListener, createIdSetListener, createIdSetListenerFromClient, createOptimisticStore, createOptimisticStoreClientBackend, createOptimisticStoreInMemoryBackend, createOptimisticStoreInternal, createReadOnlyStore, createSharedListener, createSharedListenerFromClient, toState, toTransactions }; //# sourceMappingURL=_unstable_store.js.map