UNPKG

@sanity/mutator

Version:

A set of models to make it easier to utilize the powerful real time collaborative features of Sanity

1,417 lines • 68 kB
import isEqual from "lodash/isEqual.js"; import debugIt from "debug"; import flatten from "lodash/flatten.js"; import { parsePatch as parsePatch$1, applyPatches, stringifyPatches, makePatches } from "@sanity/diff-match-patch"; import max from "lodash/max.js"; import min from "lodash/min.js"; import { uuid } from "@sanity/uuid"; import compact from "lodash/compact.js"; const debug = debugIt("mutator-document"); class ImmutableAccessor { _value; path; constructor(value, path) { this._value = value, this.path = path || []; } containerType() { return Array.isArray(this._value) ? "array" : this._value !== null && typeof this._value == "object" ? "object" : "primitive"; } // Common reader, supported by all containers get() { return this._value; } // Array reader length() { if (!Array.isArray(this._value)) throw new Error("Won't return length of non-indexable _value"); return this._value.length; } getIndex(i) { return Array.isArray(this._value) ? i >= this.length() ? null : new ImmutableAccessor(this._value[i], this.path.concat(i)) : !1; } // Object reader hasAttribute(key) { return isRecord$1(this._value) ? this._value.hasOwnProperty(key) : !1; } attributeKeys() { return isRecord$1(this._value) ? Object.keys(this._value) : []; } getAttribute(key) { if (!isRecord$1(this._value)) throw new Error("getAttribute only applies to plain objects"); return this.hasAttribute(key) ? new ImmutableAccessor(this._value[key], this.path.concat(key)) : null; } // Common writer, supported by all containers set(value) { return value === this._value ? this : new ImmutableAccessor(value, this.path); } // array writer interface setIndex(i, value) { if (!Array.isArray(this._value)) throw new Error("setIndex only applies to arrays"); if (Object.is(value, this._value[i])) return this; const nextValue = this._value.slice(); return nextValue[i] = value, new ImmutableAccessor(nextValue, this.path); } setIndexAccessor(i, accessor) { return this.setIndex(i, accessor.get()); } unsetIndices(indices) { if (!Array.isArray(this._value)) throw new Error("unsetIndices only applies to arrays"); const length = this._value.length, nextValue = []; for (let i = 0; i < length; i++) indices.indexOf(i) === -1 && nextValue.push(this._value[i]); return new ImmutableAccessor(nextValue, this.path); } insertItemsAt(pos, items) { if (!Array.isArray(this._value)) throw new Error("insertItemsAt only applies to arrays"); let nextValue; return this._value.length === 0 && pos === 0 ? nextValue = items : nextValue = this._value.slice(0, pos).concat(items).concat(this._value.slice(pos)), new ImmutableAccessor(nextValue, this.path); } // Object writer interface setAttribute(key, value) { if (!isRecord$1(this._value)) throw new Error("Unable to set attribute of non-object container"); if (Object.is(value, this._value[key])) return this; const nextValue = Object.assign({}, this._value, { [key]: value }); return new ImmutableAccessor(nextValue, this.path); } setAttributeAccessor(key, accessor) { return this.setAttribute(key, accessor.get()); } unsetAttribute(key) { if (!isRecord$1(this._value)) throw new Error("Unable to unset attribute of non-object container"); const nextValue = Object.assign({}, this._value); return delete nextValue[key], new ImmutableAccessor(nextValue, this.path); } } function isRecord$1(value) { return value !== null && typeof value == "object"; } function isRecord(value) { return value !== null && typeof value == "object"; } const IS_DOTTABLE = /^[a-z_$]+/; function arrayToJSONMatchPath(pathArray) { let path = ""; return pathArray.forEach((segment, index) => { path += stringifySegment(segment, index === 0); }), path; } function stringifySegment(segment, hasLeading) { if (typeof segment == "number") return `[${segment}]`; if (isRecord(segment)) { const seg = segment; return Object.keys(segment).map((key) => isPrimitiveValue(seg[key]) ? `[${key}=="${seg[key]}"]` : "").join(""); } return typeof segment == "string" && IS_DOTTABLE.test(segment) ? hasLeading ? segment : `.${segment}` : `['${segment}']`; } function isPrimitiveValue(val) { switch (typeof val) { case "number": case "string": case "boolean": return !0; default: return !1; } } function descend$1(tail) { const [head, newTail] = splitIfPath(tail); if (!head) throw new Error("Head cannot be null"); return spreadIfUnionHead(head, newTail); } function splitIfPath(tail) { if (tail.type !== "path") return [tail, null]; const nodes = tail.nodes; return nodes.length === 0 ? [null, null] : nodes.length === 1 ? [nodes[0], null] : [nodes[0], { type: "path", nodes: nodes.slice(1) }]; } function concatPaths(path1, path2) { if (!path1 && !path2) return null; const nodes1 = path1 ? path1.nodes : [], nodes2 = path2 ? path2.nodes : []; return { type: "path", nodes: nodes1.concat(nodes2) }; } function spreadIfUnionHead(head, tail) { return head.type !== "union" ? [[head, tail]] : head.nodes.map((node) => { if (node.type === "path") { const [subHead, subTail] = splitIfPath(node); return [subHead, concatPaths(subTail, tail)]; } return [node, tail]; }); } const digitChar = /[0-9]/, attributeCharMatcher = /^[a-zA-Z0-9_]$/, attributeFirstCharMatcher = /^[a-zA-Z_]$/, symbols = { // NOTE: These are compared against in order of definition, // thus '==' must come before '=', '>=' before '>', etc. operator: ["..", ".", ",", ":", "?"], comparator: [">=", "<=", "<", ">", "==", "!="], keyword: ["$", "@"], boolean: ["true", "false"], paren: ["[", "]"] }, symbolClasses = Object.keys(symbols); class Tokenizer { source; i; length; tokenizers; constructor(path) { this.source = path, this.length = path.length, this.i = 0, this.tokenizers = [ this.tokenizeSymbol, this.tokenizeIdentifier, this.tokenizeNumber, this.tokenizeQuoted ].map((fn) => fn.bind(this)); } tokenize() { const result = []; for (; !this.EOF(); ) { this.chompWhitespace(); let token = null; if (!this.tokenizers.some((tokenizer) => (token = tokenizer(), !!token)) || !token) throw new Error(`Invalid tokens in jsonpath '${this.source}' @ ${this.i}`); result.push(token); } return result; } takeWhile(fn) { const start = this.i; let result = ""; for (; !this.EOF(); ) { const nextChar = fn(this.source[this.i]); if (nextChar === null) break; result += nextChar, this.i++; } return this.i === start ? null : result; } EOF() { return this.i >= this.length; } peek() { return this.EOF() ? null : this.source[this.i]; } consume(str) { if (this.i + str.length > this.length) throw new Error(`Expected ${str} at end of jsonpath`); if (str === this.source.slice(this.i, this.i + str.length)) this.i += str.length; else throw new Error(`Expected "${str}", but source contained "${this.source.slice()}`); } // Tries to match the upcoming bit of string with the provided string. If it matches, returns // the string, then advances the read pointer to the next bit. If not, returns null and nothing // happens. tryConsume(str) { if (this.i + str.length > this.length) return null; if (str === this.source.slice(this.i, this.i + str.length)) { if (str[0].match(attributeCharMatcher) && this.length > this.i + str.length) { const nextChar = this.source[this.i + str.length]; if (nextChar && nextChar.match(attributeCharMatcher)) return null; } return this.i += str.length, str; } return null; } chompWhitespace() { this.takeWhile((char) => char === " " ? "" : null); } tokenizeQuoted() { const quote = this.peek(); if (quote === "'" || quote === '"') { this.consume(quote); let escape = !1; const inner = this.takeWhile((char) => escape ? (escape = !1, char) : char === "\\" ? (escape = !0, "") : char != quote ? char : null); return this.consume(quote), { type: "quoted", value: inner, quote: quote === '"' ? "double" : "single" }; } return null; } tokenizeIdentifier() { let first = !0; const identifier = this.takeWhile((char) => first ? (first = !1, char.match(attributeFirstCharMatcher) ? char : null) : char.match(attributeCharMatcher) ? char : null); return identifier !== null ? { type: "identifier", name: identifier } : null; } tokenizeNumber() { const start = this.i; let dotSeen = !1, digitSeen = !1, negative = !1; this.peek() === "-" && (negative = !0, this.consume("-")); const number = this.takeWhile((char) => char === "." && !dotSeen && digitSeen ? (dotSeen = !0, char) : (digitSeen = !0, char.match(digitChar) ? char : null)); return number !== null ? { type: "number", value: negative ? -number : +number, raw: negative ? `-${number}` : number } : (this.i = start, null); } tokenizeSymbol() { for (const symbolClass of symbolClasses) { const symbol = symbols[symbolClass].find((pattern) => this.tryConsume(pattern)); if (symbol) return { type: symbolClass, symbol }; } return null; } } function tokenize(jsonpath) { return new Tokenizer(jsonpath).tokenize(); } class Parser { tokens; length; i; constructor(path) { this.tokens = tokenize(path), this.length = this.tokens.length, this.i = 0; } parse() { return this.parsePath(); } EOF() { return this.i >= this.length; } // Look at upcoming token peek() { return this.EOF() ? null : this.tokens[this.i]; } consume() { const result = this.peek(); return this.i += 1, result; } // Return next token if it matches the pattern probe(pattern) { const token = this.peek(); if (!token) return null; const record = token; return Object.keys(pattern).every((key) => key in token && pattern[key] === record[key]) ? token : null; } // Return and consume next token if it matches the pattern match(pattern) { return this.probe(pattern) ? this.consume() : null; } parseAttribute() { const token = this.match({ type: "identifier" }); if (token && token.type === "identifier") return { type: "attribute", name: token.name }; const quoted = this.match({ type: "quoted", quote: "single" }); return quoted && quoted.type === "quoted" ? { type: "attribute", name: quoted.value || "" } : null; } parseAlias() { return this.match({ type: "keyword", symbol: "@" }) || this.match({ type: "keyword", symbol: "$" }) ? { type: "alias", target: "self" } : null; } parseNumber() { const token = this.match({ type: "number" }); return token && token.type === "number" ? { type: "number", value: token.value } : null; } parseNumberValue() { const expr = this.parseNumber(); return expr ? expr.value : null; } parseSliceSelector() { const start = this.i, rangeStart = this.parseNumberValue(); if (!this.match({ type: "operator", symbol: ":" })) return rangeStart === null ? (this.i = start, null) : { type: "index", value: rangeStart }; const result = { type: "range", start: rangeStart, end: this.parseNumberValue() }; return this.match({ type: "operator", symbol: ":" }) && (result.step = this.parseNumberValue()), result.start === null && result.end === null ? (this.i = start, null) : result; } parseValueReference() { return this.parseAttribute() || this.parseSliceSelector(); } parseLiteralValue() { const literalString = this.match({ type: "quoted", quote: "double" }); if (literalString && literalString.type === "quoted") return { type: "string", value: literalString.value || "" }; const literalBoolean = this.match({ type: "boolean" }); return literalBoolean && literalBoolean.type === "boolean" ? { type: "boolean", value: literalBoolean.symbol === "true" } : this.parseNumber(); } // TODO: Reorder constraints so that literal value is always on rhs, and variable is always // on lhs. parseFilterExpression() { const start = this.i, expr = this.parseAttribute() || this.parseAlias(); if (!expr) return null; if (this.match({ type: "operator", symbol: "?" })) return { type: "constraint", operator: "?", lhs: expr }; const binOp = this.match({ type: "comparator" }); if (!binOp || binOp.type !== "comparator") return this.i = start, null; const lhs = expr, rhs = this.parseLiteralValue(); if (!rhs) throw new Error(`Operator ${binOp.symbol} needs a literal value at the right hand side`); return { type: "constraint", operator: binOp.symbol, lhs, rhs }; } parseExpression() { return this.parseFilterExpression() || this.parseValueReference(); } parseUnion() { if (!this.match({ type: "paren", symbol: "[" })) return null; const terms = []; let expr = this.parseFilterExpression() || this.parsePath() || this.parseValueReference(); for (; expr && (terms.push(expr), !this.match({ type: "paren", symbol: "]" })); ) { if (!this.match({ type: "operator", symbol: "," })) throw new Error("Expected ]"); if (expr = this.parseFilterExpression() || this.parsePath() || this.parseValueReference(), !expr) throw new Error("Expected expression following ','"); } return { type: "union", nodes: terms }; } parseRecursive() { if (!this.match({ type: "operator", symbol: ".." })) return null; const subpath = this.parsePath(); if (!subpath) throw new Error("Expected path following '..' operator"); return { type: "recursive", term: subpath }; } parsePath() { const nodes = [], expr = this.parseAttribute() || this.parseUnion() || this.parseRecursive(); if (!expr) return null; for (nodes.push(expr); !this.EOF(); ) if (this.match({ type: "operator", symbol: "." })) { const attr = this.parseAttribute(); if (!attr) throw new Error("Expected attribute name following '."); nodes.push(attr); continue; } else if (this.probe({ type: "paren", symbol: "[" })) { const union = this.parseUnion(); if (!union) throw new Error("Expected union following '['"); nodes.push(union); } else { const recursive = this.parseRecursive(); recursive && nodes.push(recursive); break; } return nodes.length === 1 ? nodes[0] : { type: "path", nodes }; } } function parseJsonPath(path) { const parsed = new Parser(path).parse(); if (!parsed) throw new Error(`Failed to parse JSON path "${path}"`); return parsed; } function toPath(expr) { return toPathInner(expr, !1); } function toPathInner(expr, inUnion) { switch (expr.type) { case "attribute": return expr.name; case "alias": return expr.target === "self" ? "@" : "$"; case "number": return `${expr.value}`; case "range": { const result = []; return inUnion || result.push("["), expr.start && result.push(`${expr.start}`), result.push(":"), expr.end && result.push(`${expr.end}`), expr.step && result.push(`:${expr.step}`), inUnion || result.push("]"), result.join(""); } case "index": return inUnion ? `${expr.value}` : `[${expr.value}]`; case "constraint": { const rhs = expr.rhs ? ` ${toPathInner(expr.rhs, !1)}` : "", inner = `${toPathInner(expr.lhs, !1)} ${expr.operator}${rhs}`; return inUnion ? inner : `[${inner}]`; } case "string": return JSON.stringify(expr.value); case "path": { const result = [], nodes = expr.nodes.slice(); for (; nodes.length > 0; ) { const node = nodes.shift(); node && result.push(toPath(node)); const upcoming = nodes[0]; upcoming && toPathInner(upcoming, !1)[0] !== "[" && result.push("."); } return result.join(""); } case "union": return `[${expr.nodes.map((e) => toPathInner(e, !0)).join(",")}]`; default: throw new Error(`Unknown node type ${expr.type}`); case "recursive": return `..${toPathInner(expr.term, !1)}`; } } class Expression { expr; constructor(expr) { if (!expr) throw new Error("Attempted to create Expression from null-value"); if ("expr" in expr ? this.expr = expr.expr : this.expr = expr, !("type" in this.expr)) throw new Error("Attempt to create Expression for expression with no type"); } isPath() { return this.expr.type === "path"; } isUnion() { return this.expr.type === "union"; } isCollection() { return this.isPath() || this.isUnion(); } isConstraint() { return this.expr.type === "constraint"; } isRecursive() { return this.expr.type === "recursive"; } isExistenceConstraint() { return this.expr.type === "constraint" && this.expr.operator === "?"; } isIndex() { return this.expr.type === "index"; } isRange() { return this.expr.type === "range"; } expandRange(probe) { const probeLength = () => { if (!probe) throw new Error("expandRange() required a probe that was not passed"); return probe.length(); }; let start = "start" in this.expr && this.expr.start || 0; start = interpretNegativeIndex(start, probe); let end = "end" in this.expr && this.expr.end || probeLength(); end = interpretNegativeIndex(end, probe); const step = "step" in this.expr && this.expr.step || 1; return { start, end, step }; } isAttributeReference() { return this.expr.type === "attribute"; } // Is a range or index -> something referencing indexes isIndexReference() { return this.isIndex() || this.isRange(); } name() { return "name" in this.expr ? this.expr.name : ""; } isSelfReference() { return this.expr.type === "alias" && this.expr.target === "self"; } constraintTargetIsSelf() { return this.expr.type === "constraint" && this.expr.lhs.type === "alias" && this.expr.lhs.target === "self"; } constraintTargetIsAttribute() { return this.expr.type === "constraint" && this.expr.lhs.type === "attribute"; } testConstraint(probe) { const expr = this.expr; if (expr.type === "constraint" && expr.lhs.type === "alias" && expr.lhs.target === "self") { if (probe.containerType() !== "primitive") return !1; if (expr.type === "constraint" && expr.operator === "?") return !0; const lhs2 = probe.get(), rhs2 = expr.rhs && "value" in expr.rhs ? expr.rhs.value : void 0; return testBinaryOperator(lhs2, expr.operator, rhs2); } if (expr.type !== "constraint") return !1; const lhs = expr.lhs; if (!lhs) throw new Error("No LHS of expression"); if (lhs.type !== "attribute") throw new Error(`Constraint target ${lhs.type} not supported`); if (probe.containerType() !== "object") return !1; const lhsValue = probe.getAttribute(lhs.name); if (lhsValue == null || lhsValue.containerType() !== "primitive") return !1; if (this.isExistenceConstraint()) return !0; const rhs = expr.rhs && "value" in expr.rhs ? expr.rhs.value : void 0; return testBinaryOperator(lhsValue.get(), expr.operator, rhs); } pathNodes() { return this.expr.type === "path" ? this.expr.nodes : [this.expr]; } prepend(node) { return node ? new Expression({ type: "path", nodes: node.pathNodes().concat(this.pathNodes()) }) : this; } concat(other) { return other ? other.prepend(this) : this; } descend() { return descend$1(this.expr).map((headTail) => { const [head, tail] = headTail; return { head: head ? new Expression(head) : null, tail: tail ? new Expression(tail) : null }; }); } unwrapRecursive() { if (this.expr.type !== "recursive") throw new Error(`Attempt to unwrap recursive on type ${this.expr.type}`); return new Expression(this.expr.term); } toIndicies(probe) { if (this.expr.type !== "index" && this.expr.type !== "range") throw new Error("Node cannot be converted to indexes"); if (this.expr.type === "index") return [interpretNegativeIndex(this.expr.value, probe)]; const result = [], range = this.expandRange(probe); let { start, end } = range; range.step < 0 && ([start, end] = [end, start]); for (let i = start; i < end; i++) result.push(i); return result; } toFieldReferences() { if (this.isIndexReference()) return this.toIndicies(); if (this.expr.type === "attribute") return [this.expr.name]; throw new Error(`Can't convert ${this.expr.type} to field references`); } toString() { return toPath(this.expr); } static fromPath(path) { const parsed = parseJsonPath(path); if (!parsed) throw new Error(`Failed to parse path "${path}"`); return new Expression(parsed); } static attributeReference(name) { return new Expression({ type: "attribute", name }); } static indexReference(i) { return new Expression({ type: "index", value: i }); } } function testBinaryOperator(lhsValue, operator, rhsValue) { switch (operator) { case ">": return lhsValue > rhsValue; case ">=": return lhsValue >= rhsValue; case "<": return lhsValue < rhsValue; case "<=": return lhsValue <= rhsValue; case "==": return lhsValue === rhsValue; case "!=": return lhsValue !== rhsValue; default: throw new Error(`Unsupported binary operator ${operator}`); } } function interpretNegativeIndex(index, probe) { if (index >= 0) return index; if (!probe) throw new Error("interpretNegativeIndex() must have a probe when < 0"); return index + probe.length(); } class Descender { head; tail; constructor(head, tail) { this.head = head, this.tail = tail; } // Iterate this descender once processing any constraints that are // resolvable on the current value. Returns an array of new descenders // that are guaranteed to be without constraints in the head iterate(probe) { let result = [this]; if (this.head && this.head.isConstraint()) { let anyConstraints = !0; for (; anyConstraints; ) result = flatten( result.map((descender) => descender.iterateConstraints(probe)) ), anyConstraints = result.some((descender) => descender.head && descender.head.isConstraint()); } return result; } isRecursive() { return !!(this.head && this.head.isRecursive()); } hasArrived() { return this.head === null && this.tail === null; } extractRecursives() { if (this.head && this.head.isRecursive()) { const term = this.head.unwrapRecursive(); return new Descender(null, term.concat(this.tail)).descend(); } return []; } iterateConstraints(probe) { const head = this.head; if (head === null || !head.isConstraint()) return [this]; const result = []; if (probe.containerType() === "primitive" && head.constraintTargetIsSelf()) return head.testConstraint(probe) && result.push(...this.descend()), result; if (probe.containerType() === "array") { const length = probe.length(); for (let i = 0; i < length; i++) { const constraint = probe.getIndex(i); constraint && head.testConstraint(constraint) && result.push(new Descender(new Expression({ type: "index", value: i }), this.tail)); } return result; } return probe.containerType() === "object" ? head.constraintTargetIsSelf() ? [] : head.testConstraint(probe) ? this.descend() : result : result; } descend() { return this.tail ? this.tail.descend().map((ht) => new Descender(ht.head, ht.tail)) : [new Descender(null, null)]; } toString() { const result = ["<"]; return this.head && result.push(this.head.toString()), result.push("|"), this.tail && result.push(this.tail.toString()), result.push(">"), result.join(""); } } class Matcher { active; recursives; payload; constructor(active, parent) { this.active = active || [], parent ? (this.recursives = parent.recursives, this.payload = parent.payload) : this.recursives = [], this.extractRecursives(); } setPayload(payload) { return this.payload = payload, this; } // Moves any recursive descenders onto the recursive track, removing them from // the active set extractRecursives() { this.active = this.active.filter((descender) => descender.isRecursive() ? (this.recursives.push(...descender.extractRecursives()), !1) : !0); } // Find recursives that are relevant now and should be considered part of the active set activeRecursives(probe) { return this.recursives.filter((descender) => { const head = descender.head; return head ? head.isConstraint() || probe.containerType() === "array" && head.isIndexReference() ? !0 : probe.containerType() === "object" ? head.isAttributeReference() && probe.hasAttribute(head.name()) : !1 : !1; }); } match(probe) { return this.iterate(probe).extractMatches(probe); } iterate(probe) { const newActiveSet = []; return this.active.concat(this.activeRecursives(probe)).forEach((descender) => { newActiveSet.push(...descender.iterate(probe)); }), new Matcher(newActiveSet, this); } // Returns true if any of the descenders in the active or recursive set // consider the current state a final destination isDestination() { return this.active.some((descender) => descender.hasArrived()); } hasRecursives() { return this.recursives.length > 0; } // Returns any payload delivieries and leads that needs to be followed to complete // the process. extractMatches(probe) { const leads = [], targets = []; if (this.active.forEach((descender) => { if (descender.hasArrived()) { targets.push( new Expression({ type: "alias", target: "self" }) ); return; } const descenderHead = descender.head; if (descenderHead && !(probe.containerType() === "array" && !descenderHead.isIndexReference()) && !(probe.containerType() === "object" && !descenderHead.isAttributeReference())) if (descender.tail) { const matcher = new Matcher(descender.descend(), this); descenderHead.toFieldReferences().forEach(() => { leads.push({ target: descenderHead, matcher }); }); } else targets.push(descenderHead); }), this.hasRecursives()) { const recursivesMatcher = new Matcher([], this); if (probe.containerType() === "array") { const length = probe.length(); for (let i = 0; i < length; i++) leads.push({ target: Expression.indexReference(i), matcher: recursivesMatcher }); } else probe.containerType() === "object" && probe.attributeKeys().forEach((name) => { leads.push({ target: Expression.attributeReference(name), matcher: recursivesMatcher }); }); } return targets.length > 0 ? { leads, delivery: { targets, payload: this.payload } } : { leads }; } static fromPath(jsonpath) { const path = parseJsonPath(jsonpath); if (!path) throw new Error(`Failed to parse path from "${jsonpath}"`); const descender = new Descender(null, new Expression(path)); return new Matcher(descender.descend()); } } class PlainProbe { _value; path; constructor(value, path) { this._value = value, this.path = path || []; } containerType() { return Array.isArray(this._value) ? "array" : this._value !== null && typeof this._value == "object" ? "object" : "primitive"; } length() { if (!Array.isArray(this._value)) throw new Error("Won't return length of non-indexable _value"); return this._value.length; } getIndex(i) { return Array.isArray(this._value) ? i >= this.length() ? null : new PlainProbe(this._value[i], this.path.concat(i)) : !1; } hasAttribute(key) { return isRecord(this._value) ? this._value.hasOwnProperty(key) : !1; } attributeKeys() { return isRecord(this._value) ? Object.keys(this._value) : []; } getAttribute(key) { if (!isRecord(this._value)) throw new Error("getAttribute only applies to plain objects"); return this.hasAttribute(key) ? new PlainProbe(this._value[key], this.path.concat(key)) : null; } get() { return this._value; } } function extractAccessors(path, value) { const result = [], matcher = Matcher.fromPath(path).setPayload(function(values) { result.push(...values); }), accessor = new PlainProbe(value); return descend(matcher, accessor), result; } function descend(matcher, accessor) { const { leads, delivery } = matcher.match(accessor); leads.forEach((lead) => { accessorsFromTarget(lead.target, accessor).forEach((childAccessor) => { descend(lead.matcher, childAccessor); }); }), delivery && delivery.targets.forEach((target) => { typeof delivery.payload == "function" && delivery.payload(accessorsFromTarget(target, accessor)); }); } function accessorsFromTarget(target, accessor) { const result = []; if (target.isIndexReference()) target.toIndicies(accessor).forEach((i) => { result.push(accessor.getIndex(i)); }); else if (target.isAttributeReference()) result.push(accessor.getAttribute(target.name())); else if (target.isSelfReference()) result.push(accessor); else throw new Error(`Unable to derive accessor for target ${target.toString()}`); return compact(result); } function extract(path, value) { return extractAccessors(path, value).map((acc) => acc.get()); } function extractWithPath(path, value) { return extractAccessors(path, value).map((acc) => ({ path: acc.path, value: acc.get() })); } function applyPatch(patch, oldValue) { if (typeof oldValue != "string") return oldValue; const [result] = applyPatches(patch, oldValue, { allowExceedingIndices: !0 }); return result; } class DiffMatchPatch { path; dmpPatch; id; constructor(id, path, dmpPatchSrc) { this.id = id, this.path = path, this.dmpPatch = parsePatch$1(dmpPatchSrc); } apply(targets, accessor) { let result = accessor; if (result.containerType() === "primitive") return result; for (const target of targets) { if (target.isIndexReference()) { for (const index of target.toIndicies(accessor)) { const item = result.getIndex(index); if (!item) continue; const oldValue = item.get(), nextValue = applyPatch(this.dmpPatch, oldValue); result = result.setIndex(index, nextValue); } continue; } if (target.isAttributeReference() && result.hasAttribute(target.name())) { const attribute = result.getAttribute(target.name()); if (!attribute) continue; const oldValue = attribute.get(), nextValue = applyPatch(this.dmpPatch, oldValue); result = result.setAttribute(target.name(), nextValue); continue; } throw new Error(`Unable to apply diffMatchPatch to target ${target.toString()}`); } return result; } } function performIncrement(previousValue, delta) { return typeof previousValue != "number" || !Number.isFinite(previousValue) ? previousValue : previousValue + delta; } class IncPatch { path; value; id; constructor(id, path, value) { this.path = path, this.value = value, this.id = id; } apply(targets, accessor) { let result = accessor; if (result.containerType() === "primitive") return result; for (const target of targets) { if (target.isIndexReference()) { for (const index of target.toIndicies(accessor)) { const item = result.getIndex(index); if (!item) continue; const previousValue = item.get(); result = result.setIndex(index, performIncrement(previousValue, this.value)); } continue; } if (target.isAttributeReference()) { const attribute = result.getAttribute(target.name()); if (!attribute) continue; const previousValue = attribute.get(); result = result.setAttribute(target.name(), performIncrement(previousValue, this.value)); continue; } throw new Error(`Unable to apply to target ${target.toString()}`); } return result; } } function targetsToIndicies(targets, accessor) { const result = []; return targets.forEach((target) => { target.isIndexReference() && result.push(...target.toIndicies(accessor)); }), result.sort(); } class InsertPatch { location; path; items; id; constructor(id, location, path, items) { this.id = id, this.location = location, this.path = path, this.items = items; } apply(targets, accessor) { let result = accessor; if (accessor.containerType() !== "array") throw new Error("Attempt to apply insert patch to non-array value"); switch (this.location) { case "before": { const pos = minIndex(targets, accessor); result = result.insertItemsAt(pos, this.items); break; } case "after": { const pos = maxIndex(targets, accessor); result = result.insertItemsAt(pos + 1, this.items); break; } case "replace": { const indicies = targetsToIndicies(targets, accessor); result = result.unsetIndices(indicies), result = result.insertItemsAt(indicies[0], this.items); break; } default: throw new Error(`Unsupported location atm: ${this.location}`); } return result; } } function minIndex(targets, accessor) { let result = min(targetsToIndicies(targets, accessor)) || 0; return targets.forEach((target) => { if (target.isRange()) { const { start } = target.expandRange(); start < result && (result = start); } }), result; } function maxIndex(targets, accessor) { let result = max(targetsToIndicies(targets, accessor)) || 0; return targets.forEach((target) => { if (target.isRange()) { const { end } = target.expandRange(); end > result && (result = end); } }), result; } class SetIfMissingPatch { id; path; value; constructor(id, path, value) { this.id = id, this.path = path, this.value = value; } apply(targets, accessor) { let result = accessor; return targets.forEach((target) => { if (!target.isIndexReference()) if (target.isAttributeReference()) result.containerType() === "primitive" ? result = result.set({ [target.name()]: this.value }) : result.hasAttribute(target.name()) || (result = accessor.setAttribute(target.name(), this.value)); else throw new Error(`Unable to apply to target ${target.toString()}`); }), result; } } class SetPatch { id; path; value; constructor(id, path, value) { this.id = id, this.path = path, this.value = value; } apply(targets, accessor) { let result = accessor; return targets.forEach((target) => { if (target.isSelfReference()) result = result.set(this.value); else if (target.isIndexReference()) target.toIndicies(accessor).forEach((i) => { result = result.setIndex(i, this.value); }); else if (target.isAttributeReference()) result.containerType() === "primitive" ? result = result.set({ [target.name()]: this.value }) : result = result.setAttribute(target.name(), this.value); else throw new Error(`Unable to apply to target ${target.toString()}`); }), result; } } class UnsetPatch { id; path; value; constructor(id, path) { this.id = id, this.path = path; } // eslint-disable-next-line class-methods-use-this apply(targets, accessor) { let result = accessor; switch (accessor.containerType()) { case "array": result = result.unsetIndices(targetsToIndicies(targets, accessor)); break; case "object": targets.forEach((target) => { result = result.unsetAttribute(target.name()); }); break; default: throw new Error( "Target value is neither indexable or an object. This error should potentially just be silently ignored?" ); } return result; } } function parsePatch(patch) { const result = []; if (Array.isArray(patch)) return patch.reduce((r, p) => r.concat(parsePatch(p)), result); const { set, setIfMissing, unset, diffMatchPatch, inc, dec, insert } = patch; if (setIfMissing && Object.keys(setIfMissing).forEach((path) => { result.push(new SetIfMissingPatch(patch.id, path, setIfMissing[path])); }), set && Object.keys(set).forEach((path) => { result.push(new SetPatch(patch.id, path, set[path])); }), unset && unset.forEach((path) => { result.push(new UnsetPatch(patch.id, path)); }), diffMatchPatch && Object.keys(diffMatchPatch).forEach((path) => { result.push(new DiffMatchPatch(patch.id, path, diffMatchPatch[path])); }), inc && Object.keys(inc).forEach((path) => { result.push(new IncPatch(patch.id, path, inc[path])); }), dec && Object.keys(dec).forEach((path) => { result.push(new IncPatch(patch.id, path, -dec[path])); }), insert) { let location, path; const spec = insert; if ("before" in spec) location = "before", path = spec.before; else if ("after" in spec) location = "after", path = spec.after; else if ("replace" in spec) location = "replace", path = spec.replace; else throw new Error("Invalid insert patch"); result.push(new InsertPatch(patch.id, location, path, spec.items)); } return result; } class Patcher { patches; constructor(patch) { this.patches = parsePatch(patch); } apply(value) { const accessor = new ImmutableAccessor(value); return this.applyViaAccessor(accessor).get(); } // If you want to use your own accessor implementation, you can use this method // to invoke the patcher. Since all subsequent accessors for children of this accessor // are obtained through the methods in the accessors, you retain full control of the // implementation throguhgout the application. Have a look in ImmutableAccessor // to see an example of how accessors are implemented. applyViaAccessor(accessor) { let result = accessor; const idAccessor = accessor.getAttribute("_id"); if (!idAccessor) throw new Error("Cannot apply patch to document with no _id"); const id = idAccessor.get(); for (const patch of this.patches) { if (patch.id !== id) continue; const matcher = Matcher.fromPath(patch.path).setPayload(patch); result = process(matcher, result); } return result; } } function process(matcher, accessor) { const isSetPatch = matcher.payload instanceof SetPatch || matcher.payload instanceof SetIfMissingPatch; let result = accessor; const { leads, delivery } = matcher.match(accessor); return leads.forEach((lead) => { if (lead.target.isIndexReference()) lead.target.toIndicies().forEach((i) => { const item = result.getIndex(i); if (!item) throw new Error("Index out of bounds"); result = result.setIndexAccessor(i, process(lead.matcher, item)); }); else if (lead.target.isAttributeReference()) { isSetPatch && result.containerType() === "primitive" && (result = result.set({})); let oldValueAccessor = result.getAttribute(lead.target.name()); if (!oldValueAccessor && isSetPatch && (result = result.setAttribute(lead.target.name(), {}), oldValueAccessor = result.getAttribute(lead.target.name())), !oldValueAccessor) return; const newValueAccessor = process(lead.matcher, oldValueAccessor); oldValueAccessor !== newValueAccessor && (result = result.setAttributeAccessor(lead.target.name(), newValueAccessor)); } else throw new Error(`Unable to handle target ${lead.target.toString()}`); }), delivery && isPatcher(delivery.payload) && (result = delivery.payload.apply(delivery.targets, result)), result; } function isPatcher(payload) { return !!(payload && typeof payload == "object" && payload !== null && "apply" in payload && typeof payload.apply == "function"); } const luid = uuid; class Mutation { params; compiled; _appliesToMissingDocument; constructor(options) { this.params = options; } get transactionId() { return this.params.transactionId; } get transition() { return this.params.transition; } get identity() { return this.params.identity; } get previousRev() { return this.params.previousRev; } get resultRev() { return this.params.resultRev; } get mutations() { return this.params.mutations; } get timestamp() { if (typeof this.params.timestamp == "string") return new Date(this.params.timestamp); } get effects() { return this.params.effects; } assignRandomTransactionId() { this.params.transactionId = luid(), this.params.resultRev = this.params.transactionId; } appliesToMissingDocument() { if (typeof this._appliesToMissingDocument < "u") return this._appliesToMissingDocument; const firstMut = this.mutations[0]; return firstMut ? this._appliesToMissingDocument = !!(firstMut.create || firstMut.createIfNotExists || firstMut.createOrReplace) : this._appliesToMissingDocument = !0, this._appliesToMissingDocument; } // Compiles all mutations into a handy function compile() { const operations = [], getGuaranteedCreatedAt = (doc) => doc?._createdAt || this.params.timestamp || (/* @__PURE__ */ new Date()).toISOString(); this.mutations.forEach((mutation) => { if (mutation.create) { const create = mutation.create || {}; operations.push((doc) => doc || Object.assign(create, { _createdAt: getGuaranteedCreatedAt(create) })); return; } if (mutation.createIfNotExists) { const createIfNotExists = mutation.createIfNotExists || {}; operations.push( (doc) => doc === null ? Object.assign(createIfNotExists, { _createdAt: getGuaranteedCreatedAt(createIfNotExists) }) : doc ); return; } if (mutation.createOrReplace) { const createOrReplace = mutation.createOrReplace || {}; operations.push( () => Object.assign(createOrReplace, { _createdAt: getGuaranteedCreatedAt(createOrReplace) }) ); return; } if (mutation.delete) { operations.push(() => null); return; } if (mutation.patch) { if ("query" in mutation.patch) return; const patch = new Patcher(mutation.patch); operations.push((doc) => patch.apply(doc)); return; } throw new Error(`Unsupported mutation ${JSON.stringify(mutation, null, 2)}`); }), typeof this.params.timestamp == "string" && operations.push((doc) => doc ? Object.assign(doc, { _updatedAt: this.params.timestamp }) : null); const prevRev = this.previousRev, rev = this.resultRev || this.transactionId; this.compiled = (doc) => { if (prevRev && doc && prevRev !== doc._rev) throw new Error( `Previous revision for this mutation was ${prevRev}, but the document revision is ${doc._rev}` ); let result = doc; for (const operation of operations) result = operation(result); return result && rev && (result === doc && (result = Object.assign({}, doc)), result._rev = rev), result; }; } apply(document) { debug("Applying mutation %O to document %O", this.mutations, document), this.compiled || this.compile(); const result = this.compiled(document); return debug(" => %O", result), result; } static applyAll(document, mutations) { return mutations.reduce((doc, mutation) => mutation.apply(doc), document); } // Given a number of yet-to-be-committed mutation objects, collects them into one big mutation // any metadata like transactionId is ignored and must be submitted by the client. It is assumed // that all mutations are on the same document. // TOOO: Optimize mutations, eliminating mutations that overwrite themselves! static squash(document, mutations) { const squashed = mutations.reduce( (result, mutation) => result.concat(...mutation.mutations), [] ); return new Mutation({ mutations: squashed }); } } class Document { /** * Incoming patches from the server waiting to be applied to HEAD */ incoming = []; /** * Patches we know has been subitted to the server, but has not been seen yet in the return channel * so we can't be sure about the ordering yet (someone else might have slipped something between them) */ submitted = []; /** * Pending mutations */ pending = []; /** * Our model of the document according to the incoming patches from the server */ HEAD; /** * Our optimistic model of what the document will probably look like as soon as all our patches * have been processed. Updated every time we stage a new mutation, but also might revert back * to previous states if our mutations fail, or could change if unexpected mutations arrive * between our own. The `onRebase` callback will be called when EDGE changes in this manner. */ EDGE; /** * Called with the EDGE document when that document changes for a reason other than us staging * a new patch or receiving a mutation from the server while our EDGE is in sync with HEAD: * I.e. when EDGE changes because the order of mutations has changed in relation to our * optimistic predictions. */ onRebase; /** * Called when we receive a patch in the normal order of things, but the mutation is not ours */ onMutation; /** * Called when consistency state changes with the boolean value of the current consistency state */ onConsistencyChanged; /** * Called whenever a new incoming mutation comes in. These are always ordered correctly. */ onRemoteMutation; /** * We are consistent when there are no unresolved mutations of our own, and no un-applicable * incoming mutations. When this has been going on for too long, and there has been a while * since we staged a new mutation, it is time to reset your state. */ inconsistentAt = null; /** * The last time we staged a patch of our own. If we have been inconsistent for a while, but it * hasn't been long since we staged a new mutation, the reason is probably just because the user * is typing or something. * * Should be used as a guard against resetting state for inconsistency reasons. */ lastStagedAt = null; constructor(doc) { this.reset(doc), this.HEAD = doc, this.EDGE = doc; } // Reset the state of the Document, used to recover from unsavory states by reloading the document reset(doc) { this.incoming = [], this.submitted = [], this.pending = [], this.inconsistentAt = null, this.HEAD = doc, this.EDGE = doc, this.considerIncoming(), this.updateConsistencyFlag(); } // Call when a mutation arrives from Sanity arrive(mutation) { this.incoming.push(mutation), this.considerIncoming(), this.updateConsistencyFlag(); } // Call to signal that we are submitting a mutation. Returns a callback object with a // success and failure handler that must be called according to the outcome of our // submission. stage(mutation, silent) { if (!mutation.transactionId) throw new Error("Mutations _must_ have transactionId when submitted"); this.lastStagedAt = /* @__PURE__ */ new Date(), debug("Staging mutation %s (pushed to pending)", mutation.transactionId), this.pending.push(mutation), this.EDGE = mutation.apply(this.EDGE), this.onMutation && !silent && this.onMutation({ mutation, document: this.EDGE, remote: !1 }); const txnId = mutation.transactionId; return this.updateConsistencyFlag(), { success: () => { this.pendingSuccessfullySubmitted(txnId), this.updateConsistencyFlag(); }, failure: () => { this.pendingFailed(txnId), this.updateConsistencyFlag(); } }; } // Call to check if everything is nice and quiet and there are no unresolved mutations. // Means this model thinks both HEAD and EDGE is up to date with what the server sees. isConsistent() { return !this.inconsistentAt; } // Private // Attempts to apply any resolvable incoming patches to HEAD. Will keep patching as long as there // are applicable patches to be applied considerIncoming() { let mustRebase = !1, nextMut; const rebaseMutations = []; if (this.HEAD && this.HEAD._updatedAt) { const updatedAt = new Date(this.HEAD._updatedAt); this.incoming.find((mut) => mut.timestamp && mut.timestamp < updatedAt) && (this.incoming = this.incoming.filter((mut) => mut.timestamp && mut.timestamp < updatedAt)); } let protect = 0; do { if (this.HEAD) { const HEAD = this.HEAD; nextMut = HEAD._rev ? this.incoming.find((mut) => mut.previousRev === HEAD._rev) : void 0; } else nextMut = this.incoming.find((mut)