@sanity/mutate
Version:
Experimental toolkit for working with Sanity mutations in JavaScript & TypeScript
1,136 lines (1,135 loc) • 41.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: !0 });
var rxjs = require("rxjs"), lodashPartition = require("lodash/partition.js"), operators = require("rxjs/operators"), keyBy = require("lodash/keyBy.js"), encode = require("./_chunks-cjs/encode.cjs"), nanoid = require("nanoid"), utils = require("./_chunks-cjs/utils.cjs"), mendoza = require("mendoza"), sortedIndex = require("lodash/sortedIndex.js"), uuid = require("@sanity/uuid"), diffMatchPatch = require("@sanity/diff-match-patch"), getAtPath = require("./_chunks-cjs/getAtPath.cjs"), stringify = require("./_chunks-cjs/stringify.cjs"), groupBy = require("lodash/groupBy.js");
function _interopDefaultCompat(e) {
return e && typeof e == "object" && "default" in e ? e : { default: e };
}
var lodashPartition__default = /* @__PURE__ */ _interopDefaultCompat(lodashPartition), keyBy__default = /* @__PURE__ */ _interopDefaultCompat(keyBy), sortedIndex__default = /* @__PURE__ */ _interopDefaultCompat(sortedIndex), groupBy__default = /* @__PURE__ */ _interopDefaultCompat(groupBy);
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(
rxjs.finalize(() => cache.delete(id)),
rxjs.share({
resetOnRefCountZero: typeof shutdownDelay == "number" ? () => rxjs.timer(shutdownDelay) : !0,
connector: () => new rxjs.ReplaySubject(1)
})
);
return cache.set(id, cached), cached;
}
return {
listenDocument,
listenDocuments(ids) {
return rxjs.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__default.default(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(
operators.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
}
),
rxjs.switchMap((state) => state.buffer.length > 0 ? (onBrokenChain?.(state.buffer), rxjs.concat(
rxjs.of(state),
rxjs.timer(resolveChainDeadline).pipe(
operators.mergeMap(
() => rxjs.throwError(() => new DeadlineExceededError(
`Did not resolve chain within a deadline of ${resolveChainDeadline}ms`,
state
))
)
)
)) : rxjs.of(state)),
operators.mergeMap((state) => state.emitEvents)
);
}
function createDocumentEventListener(options) {
const { listenerEvents, loadDocument } = options;
return function(documentId) {
return listenerEvents.pipe(
rxjs.concatMap((event) => event.type === "mutation" ? event.documentId === documentId ? rxjs.of(event) : rxjs.EMPTY : event.type === "reconnect" ? rxjs.of(event) : event.type === "welcome" ? loadDocument(documentId).pipe(
rxjs.catchError((err) => {
const error = toError(err);
return isClientError(error) ? rxjs.throwError(() => error) : rxjs.throwError(
() => new FetchError(
`An unexpected error occurred while fetching document: ${error?.message}`,
{ cause: error }
)
);
}),
rxjs.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}".`
);
}),
rxjs.map(
(doc) => ({
type: "sync",
document: doc
})
)
) : rxjs.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 = () => rxjs.scheduled(rxjs.of(0), rxjs.asyncScheduler);
function createDataLoader(options) {
const durationSelector = options.durationSelector || defaultDurationSelector, requests$ = new rxjs.BehaviorSubject(void 0), unsubscribes$ = new rxjs.Subject(), batchResponses = requests$.pipe(
rxjs.filter((req) => !!req),
rxjs.bufferWhen(durationSelector),
rxjs.map((requests) => requests.filter((request) => !request.cancelled)),
rxjs.filter((requests) => requests.length > 0),
rxjs.mergeMap((requests) => {
const keys = requests.map((request) => request.key), responses = options.onLoad(keys).pipe(
rxjs.takeUntil(
unsubscribes$.pipe(
rxjs.filter(() => requests.every((request) => request.cancelled))
)
),
rxjs.mergeMap(
(batchResult) => requests.map((request, i) => ({
type: "value",
request,
response: batchResult[i]
}))
)
), responseEnds = requests.map((request) => ({
request,
type: "complete"
}));
return rxjs.concat(responses, responseEnds);
}),
rxjs.share()
);
return (key) => new rxjs.Observable((subscriber) => {
const mutableRequestState = { key, cancelled: !1 }, emit = rxjs.defer(() => (requests$.next(mutableRequestState), rxjs.EMPTY)), subscription = rxjs.merge(
batchResponses.pipe(
rxjs.filter((batchResult) => batchResult.request === mutableRequestState),
rxjs.takeWhile((batchResult) => batchResult.type !== "complete"),
rxjs.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(
rxjs.map((results) => prepareResponse(ids, results)),
rxjs.map((results) => {
const byId = keyBy__default.default(results, (result) => result.id);
return ids.map((id) => byId[id]);
})
);
}
function prepareResponse(requestedIds, response) {
const documents = keyBy__default.default(response.documents, (entry) => entry._id), omitted = keyBy__default.default(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 = utils.assignId(mutation.document, nanoid.nanoid);
return { status: "created", id: result._id, after: result };
}
function createIfNotExists(document, mutation) {
return utils.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 utils.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 = utils.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 = mendoza.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(
operators.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,
encode.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
rxjs.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(
operators.map((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(
operators.mergeMap((event) => event.type === "welcome" ? fetchFilter().pipe(operators.map((result) => ({ type: "sync", result }))) : rxjs.of(event)),
operators.map((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
operators.filter((ev) => !!ev)
);
};
}
function createIdSetListenerFromClient(client) {
}
function toState(options = {}) {
const { insert: insertOption = "sorted" } = options;
return (input$) => input$.pipe(
operators.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__default.default(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(
rxjs.tap((value) => {
config.predicate(value) && (emitted = !0, latest = value);
}),
rxjs.finalize(() => {
emitted = !1, latest = void 0;
}),
rxjs.share(shareConfig)
), emitLatest = new rxjs.Observable((subscriber) => {
emitted && subscriber.next(latest), subscriber.complete();
});
return rxjs.merge(wrapped, emitLatest);
};
}
function withListenErrors() {
return (input$) => input$.pipe(
rxjs.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, tag, shutdownDelay, includeSystemDocuments, includeMutations } = options, query = filter ? `*[${filter}]` : 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" ? () => rxjs.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.uuid();
}
function createWelcomeEvent() {
return {
type: "welcome",
listenerName: "mock" + Math.random().toString(32).substring(2)
};
}
function createMockBackendAPI() {
const documentMap = createDocumentMap(), listenerEvents = new rxjs.Subject();
return {
listen: (query) => rxjs.concat(
rxjs.of(createWelcomeEvent()),
rxjs.merge(rxjs.NEVER, listenerEvents).pipe(
rxjs.filter((m) => m.type === "mutation"),
operators.map((ev) => structuredClone(ev))
)
),
getDocuments(ids) {
const docs = ids.map((id) => ({ id, document: documentMap.get(id) })), [existing, omitted] = lodashPartition__default.default(docs, (entry) => entry.document);
return rxjs.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: encode.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"
});
}), rxjs.of({});
}
};
}
function createOptimisticStoreClientBackend(client) {
return {
listen: createDocumentEventListener({
loadDocument: createDocumentLoaderFromClient(client),
listenerEvents: createSharedListenerFromClient(client)
}),
submit: (transaction) => rxjs.from(
client.dataRequest("mutate", encode.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(
rxjs.finalize(() => {
delete memo[key];
}),
rxjs.share({
connector: () => new rxjs.ReplaySubject(1),
resetOnRefCountZero: () => rxjs.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.stringify(p1) === stringify.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) => !stringify.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 = utils.applyNodePatch(laterPatch, edge), laterPatch.op.type === "set" && typeof laterPatch.op.value == "string") {
const current = getAtPath.getAtPath(laterPatch.path, before);
if (typeof current == "string") {
const replaced = {
...laterPatch,
op: {
type: "diffMatchPatch",
value: diffMatchPatch.stringifyPatches(
diffMatchPatch.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__default.default(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 = utils.applyPatches(
mut.mutation.dmpPatches,
newBaseWithDMPForOldBaseApplied
), applied.push(mut);
} catch {
console.warn("Failed to apply dmp patch, falling back to original");
try {
newBaseWithDMPForOldBaseApplied = utils.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.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 rxjs.Subject(), localMutations$ = new rxjs.Subject(), stage$ = new rxjs.Subject();
function setStaged(nextPending) {
stagedChanges = nextPending, stage$.next();
}
function getLocalEvents(id) {
return localMutations$.pipe(rxjs.filter((event) => event.id === id));
}
function getRemoteEvents(id) {
return backend.listen(id).pipe(
rxjs.filter(
(event) => event.type !== "reconnect"
),
rxjs.mergeMap((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 rxjs.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 rxjs.EMPTY;
let newRemote;
if (hasProperty(event, "effects"))
newRemote = applyMutationEventEffects(oldRemote, event);
else if (hasProperty(event, "mutations"))
newRemote = applyAll(oldRemote, encode.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 = encode.decodeAll(
event.mutations
) : Object.defineProperty(
emittedEvent,
"mutations",
warnNoMutationsReceived
), rxjs.of(emittedEvent);
} else
throw new Error(`Unknown event type: ${event.type}`);
}),
rxjs.tap((event) => {
local.set(event.id, event.after.local), remote.set(event.id, event.after.remote), setStaged(event.rebasedStage);
}),
rxjs.tap({
next: (event) => remoteEvents$.next(event),
error: (err) => {
}
})
);
}
function listenEvents(id) {
return rxjs.defer(
() => memoize(id, rxjs.merge(getLocalEvents(id), getRemoteEvents(id)))
);
}
return {
meta: {
events: rxjs.merge(localMutations$, remoteEvents$),
stage: stage$.pipe(
rxjs.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: rxjs.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(
rxjs.map(
(event) => event.type === "optimistic" ? event.after : event.after.local
)
),
optimize: () => {
setStaged(squashMutationGroups(stagedChanges));
},
submit: () => {
const pending = stagedChanges;
return setStaged([]), rxjs.lastValueFrom(
rxjs.from(
toTransactions(
// Squashing DMP strings is the last thing we do before submitting
squashDMPStrings(remote, squashMutationGroups(pending))
)
).pipe(
rxjs.concatMap((mut) => backend.submit(mut)),
rxjs.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 });
}
exports.createDocumentEventListener = createDocumentEventListener;
exports.createDocumentLoader = createDocumentLoader;
exports.createDocumentLoaderFromClient = createDocumentLoaderFromClient;
exports.createDocumentUpdateListener = createDocumentUpdateListener;
exports.createIdSetListener = createIdSetListener;
exports.createIdSetListenerFromClient = createIdSetListenerFromClient;
exports.createMockBackendAPI = createMockBackendAPI;
exports.createOptimisticStore = createOptimisticStore;
exports.createOptimisticStoreClientBackend = createOptimisticStoreClientBackend;
exports.createOptimisticStoreMockBackend = createOptimisticStoreMockBackend;
exports.createReadOnlyStore = createReadOnlyStore;
exports.createSharedListener = createSharedListener;
exports.createSharedListenerFromClient = createSharedListenerFromClient;
exports.toState = toState;
//# sourceMappingURL=_unstable_store.cjs.map