UNPKG

@sanity/mutate

Version:

Experimental toolkit for working with Sanity mutations in JavaScript & TypeScript

1,145 lines (1,144 loc) 40.5 kB
import { combineLatest, finalize, share, ReplaySubject, timer, switchMap, concat, of, throwError, concatMap, EMPTY, catchError, map, BehaviorSubject, Subject, filter, bufferWhen, scheduled, mergeMap as mergeMap$1, takeUntil, Observable, defer, merge, takeWhile, asyncScheduler, tap, NEVER, from, lastValueFrom, toArray } from "rxjs"; import lodashPartition from "lodash/partition.js"; import { scan, mergeMap, map as map$1, filter as filter$1 } from "rxjs/operators"; import keyBy from "lodash/keyBy.js"; import { decodeAll, encodeAll, encodeTransaction } from "./_chunks-es/encode.js"; import { nanoid } from "nanoid"; import { assignId, hasId, applyPatchMutation, applyNodePatch, applyPatches } from "./_chunks-es/utils.js"; import { applyPatch } from "mendoza"; import sortedIndex from "lodash/sortedIndex.js"; import { uuid } from "@sanity/uuid"; import { stringifyPatches, makePatches } from "@sanity/diff-match-patch"; import { getAtPath } from "./_chunks-es/getAtPath.js"; import { startsWith, stringify } from "./_chunks-es/stringify.js"; import groupBy from "lodash/groupBy.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; } 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), 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) => 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 applyAll(current, mutation) { return mutation.reduce((doc, m) => { const res = applyDocumentMutation(doc, m); if (res.status === "error") throw new Error(res.message); return res.status === "noop" ? doc : res.after; }, current); } function applyDocumentMutation(document, mutation) { if (mutation.type === "create") return create(document, mutation); if (mutation.type === "createIfNotExists") return createIfNotExists(document, mutation); if (mutation.type === "delete") return del(document, mutation); if (mutation.type === "createOrReplace") return createOrReplace(document, mutation); if (mutation.type === "patch") return patch(document, mutation); throw new Error(`Invalid mutation type: ${mutation.type}`); } function create(document, mutation) { if (document) return { status: "error", message: "Document already exist" }; const result = assignId(mutation.document, nanoid); return { status: "created", id: result._id, after: result }; } function createIfNotExists(document, mutation) { return hasId(mutation.document) ? document ? { status: "noop" } : { status: "created", id: mutation.document._id, after: mutation.document } : { status: "error", message: "Cannot createIfNotExists on document without _id" }; } function createOrReplace(document, mutation) { return hasId(mutation.document) ? document ? { status: "updated", id: mutation.document._id, before: document, after: mutation.document } : { status: "created", id: mutation.document._id, after: mutation.document } : { status: "error", message: "Cannot createIfNotExists on document without _id" }; } function del(document, mutation) { return document ? mutation.id !== document._id ? { status: "error", message: "Delete mutation targeted wrong document" } : { status: "deleted", id: mutation.id, before: document, after: void 0 } : { status: "noop" }; } function patch(document, mutation) { if (!document) return { status: "error", message: "Cannot apply patch on nonexistent document" }; const next = applyPatchMutation(mutation, document); return document === next ? { status: "noop" } : { status: "updated", id: mutation.id, before: document, after: next }; } function omitRev(document) { if (document === void 0) return; const { ...doc } = document; return doc; } function applyMendozaPatch(document, patch2, 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), patch2); 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 { ...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 getMutationDocumentId(mutation) { if (mutation.type === "patch") return mutation.id; if (mutation.type === "create") return mutation.document._id; if (mutation.type === "delete") return mutation.id; if (mutation.type === "createIfNotExists" || mutation.type === "createOrReplace") return mutation.document._id; throw new Error("Invalid mutation type"); } function applyMutations(mutations, documentMap, transactionId) { const updatedDocs = /* @__PURE__ */ Object.create(null); for (const mutation of mutations) { const documentId = getMutationDocumentId(mutation); if (!documentId) throw new Error("Unable to get document id from mutation"); const before = updatedDocs[documentId]?.after || documentMap.get(documentId), res = applyDocumentMutation(before, mutation); if (res.status === "error") throw new Error(res.message); let entry = updatedDocs[documentId]; entry || (entry = { before, after: before, mutations: [] }, updatedDocs[documentId] = entry); const after = transactionId ? { ...res.status === "noop" ? before : res.after, _rev: transactionId } : res.status === "noop" ? before : res.after; documentMap.set(documentId, after), entry.after = after, entry.mutations.push(mutation); } return Object.entries(updatedDocs).map( ([id, { before, after, mutations: muts }]) => ({ id, status: after ? before ? "updated" : "created" : "deleted", mutations: muts, before, after }) ); } function createDocumentMap() { const documents = /* @__PURE__ */ new Map(); return { set: (id, doc) => void documents.set(id, doc), get: (id) => documents.get(id), delete: (id) => documents.delete(id) }; } function createTransactionId() { return uuid(); } function createWelcomeEvent() { return { type: "welcome", listenerName: "mock" + Math.random().toString(32).substring(2) }; } function createMockBackendAPI() { const documentMap = createDocumentMap(), listenerEvents = new Subject(); return { listen: (query) => concat( of(createWelcomeEvent()), merge(NEVER, listenerEvents).pipe( filter((m) => m.type === "mutation"), map$1((ev) => structuredClone(ev)) ) ), getDocuments(ids) { const docs = ids.map((id) => ({ id, document: documentMap.get(id) })), [existing, omitted] = lodashPartition(docs, (entry) => entry.document); return of( structuredClone({ documents: existing.map((entry) => entry.document), omitted: omitted.map((entry) => ({ id: entry.id, reason: "existence" })) }) ); }, submit: (_transaction) => { const transaction = structuredClone(_transaction); return applyMutations( transaction.mutations, documentMap, 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 createOptimisticStoreClientBackend(client) { return { listen: createDocumentEventListener({ loadDocument: createDocumentLoaderFromClient(client), listenerEvents: createSharedListenerFromClient(client) }), submit: (transaction) => from( client.dataRequest("mutate", encodeTransaction(transaction), { visibility: "async", returnDocuments: !1 }) ) }; } function createOptimisticStoreMockBackend(backendAPI) { const sharedListener = createSharedListener( (query, options) => backendAPI.listen(query) ), loadDocument = createDocumentLoader((ids) => backendAPI.getDocuments(ids)); return { listen: createDocumentEventListener({ loadDocument, listenerEvents: sharedListener }), submit: backendAPI.submit }; } function commit(results, documentMap) { results.forEach((result) => { (result.status === "created" || result.status === "updated") && documentMap.set(result.id, result.after), result.status === "deleted" && documentMap.delete(result.id); }); } function createReplayMemoizer(expiry) { const memo = /* @__PURE__ */ Object.create(null); return function(key, observable) { return key in memo || (memo[key] = observable.pipe( finalize(() => { delete memo[key]; }), share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(expiry) }) )), memo[key]; }; } function filterMutationGroupsById(mutationGroups, id) { return mutationGroups.flatMap( (mutationGroup) => mutationGroup.mutations.flatMap( (mut) => getMutationDocumentId(mut) === id ? [mut] : [] ) ); } function takeUntilRight(arr, predicate, opts) { const result = []; for (const item of arr.slice().reverse()) { if (predicate(item)) return result; result.push(item); } return result.reverse(); } function isEqualPath(p1, p2) { return stringify(p1) === stringify(p2); } function supersedes(later, earlier) { return (earlier.type === "set" || earlier.type === "unset") && (later.type === "set" || later.type === "unset"); } function squashNodePatches(patches) { return compactSetIfMissingPatches( compactSetPatches(compactUnsetPatches(patches)) ); } function compactUnsetPatches(patches) { return patches.reduce( (earlierPatches, laterPatch) => { if (laterPatch.op.type !== "unset") return earlierPatches.push(laterPatch), earlierPatches; const unaffected = earlierPatches.filter( (earlierPatch) => !startsWith(laterPatch.path, earlierPatch.path) ); return unaffected.push(laterPatch), unaffected; }, [] ); } function compactSetPatches(patches) { return patches.reduceRight( (laterPatches, earlierPatch) => (laterPatches.find( (later) => supersedes(later.op, earlierPatch.op) && isEqualPath(later.path, earlierPatch.path) ) || laterPatches.unshift(earlierPatch), laterPatches), [] ); } function compactSetIfMissingPatches(patches) { return patches.reduce( (previousPatches, laterPatch) => laterPatch.op.type !== "setIfMissing" ? (previousPatches.push(laterPatch), previousPatches) : (takeUntilRight( previousPatches, (patch2) => patch2.op.type === "unset" ).find( (precedingPatch) => precedingPatch.op.type === "setIfMissing" && isEqualPath(precedingPatch.path, laterPatch.path) ) || previousPatches.push(laterPatch), previousPatches), [] ); } function compactDMPSetPatches(base, patches) { let edge = base; return patches.reduce( (earlierPatches, laterPatch) => { const before = edge; if (edge = applyNodePatch(laterPatch, edge), laterPatch.op.type === "set" && typeof laterPatch.op.value == "string") { const current = getAtPath(laterPatch.path, before); if (typeof current == "string") { const replaced = { ...laterPatch, op: { type: "diffMatchPatch", value: stringifyPatches( makePatches(current, laterPatch.op.value) ) } }; return earlierPatches.flatMap((ep) => isEqualPath(ep.path, laterPatch.path) && ep.op.type === "diffMatchPatch" ? [] : ep).concat(replaced); } } return earlierPatches.push(laterPatch), earlierPatches; }, [] ); } function squashDMPStrings(base, mutationGroups) { return mutationGroups.map((mutationGroup) => ({ ...mutationGroup, mutations: dmpIfyMutations(base, mutationGroup.mutations) })); } function dmpIfyMutations(store, mutations) { return mutations.map((mutation, i) => { if (mutation.type !== "patch") return mutation; const base = store.get(mutation.id); return base ? dmpifyPatchMutation(base, mutation) : mutation; }); } function dmpifyPatchMutation(base, mutation) { return { ...mutation, patches: compactDMPSetPatches(base, mutation.patches) }; } function mergeMutationGroups(mutationGroups) { return chunkWhile(mutationGroups, (group) => !group.transaction).flatMap( (chunk) => ({ ...chunk[0], mutations: chunk.flatMap((c) => c.mutations) }) ); } function chunkWhile(arr, predicate) { const res = []; let currentChunk = []; return arr.forEach((item) => { predicate(item) ? currentChunk.push(item) : (currentChunk.length > 0 && res.push(currentChunk), currentChunk = [], res.push([item])); }), currentChunk.length > 0 && res.push(currentChunk), res; } function squashMutationGroups(staged) { return mergeMutationGroups(staged).map((transaction) => ({ ...transaction, mutations: squashMutations(transaction.mutations) })).map((transaction) => ({ ...transaction, mutations: transaction.mutations.map((mutation) => mutation.type !== "patch" ? mutation : { ...mutation, patches: squashNodePatches(mutation.patches) }) })); } function squashMutations(mutations) { const byDocument = groupBy(mutations, getMutationDocumentId); return Object.values(byDocument).flatMap((documentMutations) => squashCreateIfNotExists(squashDelete(documentMutations)).flat().reduce((acc, docMutation) => { const prev = acc[acc.length - 1]; return (!prev || prev.type === "patch") && docMutation.type === "patch" ? acc.slice(0, -1).concat({ ...docMutation, patches: (prev?.patches || []).concat(docMutation.patches) }) : acc.concat(docMutation); }, [])); } function squashCreateIfNotExists(mutations) { return mutations.length === 0 ? mutations : mutations.reduce((previousMuts, laterMut) => laterMut.type !== "createIfNotExists" ? (previousMuts.push(laterMut), previousMuts) : (takeUntilRight(previousMuts, (m) => m.type === "delete").find( (precedingPatch) => precedingPatch.type === "createIfNotExists" ) || previousMuts.push(laterMut), previousMuts), []); } function squashDelete(mutations) { return mutations.length === 0 ? mutations : mutations.reduce((previousMuts, laterMut) => laterMut.type === "delete" ? [laterMut] : (previousMuts.push(laterMut), previousMuts), []); } function rebase(documentId, oldBase, newBase, localMutations) { let edge = oldBase; const dmpified = localMutations.map((transaction) => { const mutations = transaction.mutations.flatMap((mut) => { if (getMutationDocumentId(mut) !== documentId) return []; const before = edge; return edge = applyAll(edge, [mut]), !before || mut.type !== "patch" ? mut : { type: "dmpified", mutation: { ...mut, // Todo: make compactDMPSetPatches return pairs of patches that was dmpified with their // original as dmpPatches and original is not 1:1 (e..g some of the original may not be dmpified) dmpPatches: compactDMPSetPatches(before, mut.patches), original: mut.patches } }; }); return { ...transaction, mutations }; }); let newBaseWithDMPForOldBaseApplied = newBase; return dmpified.map((transaction) => { const applied = []; return transaction.mutations.forEach((mut) => { if (mut.type === "dmpified") try { newBaseWithDMPForOldBaseApplied = applyPatches( mut.mutation.dmpPatches, newBaseWithDMPForOldBaseApplied ), applied.push(mut); } catch { console.warn("Failed to apply dmp patch, falling back to original"); try { newBaseWithDMPForOldBaseApplied = applyPatches( mut.mutation.original, newBaseWithDMPForOldBaseApplied ), applied.push(mut); } catch (second) { throw new Error( `Failed to apply patch for document "${documentId}": ${second.message}` ); } } else newBaseWithDMPForOldBaseApplied = applyAll( newBaseWithDMPForOldBaseApplied, [mut] ); }); }), [localMutations.map((transaction) => ({ ...transaction, mutations: transaction.mutations.map((mut) => mut.type !== "patch" || getMutationDocumentId(mut) !== documentId ? mut : { ...mut, patches: mut.patches.map((patch2) => patch2.op.type !== "set" ? patch2 : { ...patch2, op: { ...patch2.op, value: getAtPath(patch2.path, newBaseWithDMPForOldBaseApplied) } }) }) })), newBaseWithDMPForOldBaseApplied]; } let didEmitMutationsAccessWarning = !1; function warnNoMutationsReceived() { didEmitMutationsAccessWarning || (console.warn( new Error( "No mutation received from backend. The listener is likely set up with `excludeMutations: true`. If your app need to know about mutations, make sure the listener is set up to include mutations" ) ), didEmitMutationsAccessWarning = !0); } const EMPTY_ARRAY = []; function createOptimisticStore(backend) { const local = createDocumentMap(), remote = createDocumentMap(), memoize = createReplayMemoizer(1e3); let stagedChanges = []; const remoteEvents$ = new Subject(), localMutations$ = new Subject(), stage$ = new Subject(); function setStaged(nextPending) { stagedChanges = nextPending, stage$.next(); } function getLocalEvents(id) { return localMutations$.pipe(filter((event) => event.id === id)); } function getRemoteEvents(id) { return backend.listen(id).pipe( filter( (event) => event.type !== "reconnect" ), mergeMap$1((event) => { const oldLocal = local.get(id), oldRemote = remote.get(id); if (event.type === "sync") { const newRemote = event.document, [rebasedStage, newLocal] = rebase( id, oldRemote, newRemote, stagedChanges ); return of({ type: "sync", id, before: { remote: oldRemote, local: oldLocal }, after: { remote: newRemote, local: newLocal }, rebasedStage }); } else if (event.type === "mutation") { if (event.transactionId === oldRemote?._rev) return EMPTY; let newRemote; if (hasProperty(event, "effects")) newRemote = applyMutationEventEffects(oldRemote, event); else if (hasProperty(event, "mutations")) newRemote = applyAll(oldRemote, decodeAll(event.mutations)); else throw new Error( "Neither effects or mutations found on listener event" ); const [rebasedStage, newLocal] = rebase( id, oldRemote, newRemote, stagedChanges ); newLocal && (newLocal._rev = event.transactionId); const emittedEvent = { type: "mutation", id, rebasedStage, before: { remote: oldRemote, local: oldLocal }, after: { remote: newRemote, local: newLocal }, effects: event.effects, previousRev: event.previousRev, resultRev: event.resultRev, // overwritten below mutations: EMPTY_ARRAY }; return event.mutations ? emittedEvent.mutations = decodeAll( event.mutations ) : Object.defineProperty( emittedEvent, "mutations", warnNoMutationsReceived ), of(emittedEvent); } else throw new Error(`Unknown event type: ${event.type}`); }), tap((event) => { local.set(event.id, event.after.local), remote.set(event.id, event.after.remote), setStaged(event.rebasedStage); }), tap({ next: (event) => remoteEvents$.next(event), error: (err) => { } }) ); } function listenEvents(id) { return defer( () => memoize(id, merge(getLocalEvents(id), getRemoteEvents(id))) ); } return { meta: { events: merge(localMutations$, remoteEvents$), stage: stage$.pipe( map( () => ( // note: this should not be tampered with by consumers. We might want to do a deep-freeze during dev to avoid accidental mutations stagedChanges ) ) ), conflicts: EMPTY // does nothing for now }, mutate: (mutations) => { stagedChanges.push({ transaction: !1, mutations }); const results = applyMutations(mutations, local); return commit(results, local), results.forEach((result) => { localMutations$.next({ type: "optimistic", before: result.before, after: result.after, mutations: result.mutations, id: result.id, stagedChanges: filterMutationGroupsById(stagedChanges, result.id) }); }), results; }, transaction: (mutationsOrTransaction) => { const transaction = Array.isArray( mutationsOrTransaction ) ? { mutations: mutationsOrTransaction, transaction: !0 } : { ...mutationsOrTransaction, transaction: !0 }; stagedChanges.push(transaction); const results = applyMutations(transaction.mutations, local); return commit(results, local), results.forEach((result) => { localMutations$.next({ type: "optimistic", mutations: result.mutations, id: result.id, before: result.before, after: result.after, stagedChanges: filterMutationGroupsById(stagedChanges, result.id) }); }), results; }, listenEvents, listen: (id) => listenEvents(id).pipe( map( (event) => event.type === "optimistic" ? event.after : event.after.local ) ), optimize: () => { setStaged(squashMutationGroups(stagedChanges)); }, submit: () => { const pending = stagedChanges; return setStaged([]), lastValueFrom( from( toTransactions( // Squashing DMP strings is the last thing we do before submitting squashDMPStrings(remote, squashMutationGroups(pending)) ) ).pipe( concatMap((mut) => backend.submit(mut)), toArray() ) ); } }; } function toTransactions(groups) { return groups.map((group) => group.transaction && group.id !== void 0 ? { id: group.id, mutations: group.mutations } : { id: createTransactionId(), mutations: group.mutations }); } export { createDocumentEventListener, createDocumentLoader, createDocumentLoaderFromClient, createDocumentUpdateListener, createIdSetListener, createIdSetListenerFromClient, createMockBackendAPI, createOptimisticStore, createOptimisticStoreClientBackend, createOptimisticStoreMockBackend, createReadOnlyStore, createSharedListener, createSharedListenerFromClient, toState }; //# sourceMappingURL=_unstable_store.js.map