@sanity/sdk 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-es/version.js +1 -1
- package/dist/index.d.ts +69 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +282 -52
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/_exports/index.ts +2 -0
- package/src/document/actions.ts +36 -0
- package/src/document/documentStore.concurrency.test.ts +1015 -0
- package/src/document/documentStore.test.ts +143 -1
- package/src/document/documentStore.ts +8 -1
- package/src/document/events.ts +32 -0
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +73 -20
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/processActions/edit.ts +26 -14
- package/src/document/processActions/shared.ts +165 -8
- package/src/document/processActions.test.ts +330 -1
- package/src/document/reducers.test.ts +519 -0
- package/src/document/reducers.ts +99 -3
- package/src/document/sharedListener.test.ts +2 -1
- package/src/document/sharedListener.ts +12 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { switchMap, from, firstValueFrom, EMPTY, asapScheduler, distinctUntilChanged, map as map$1, combineLatest, of, concatMap, withLatestFrom, filter as filter$1, concat, timer, throwError, first as first$1, Subject, takeUntil, share, partition, merge, shareReplay, tap as tap$1, catchError as catchError$1, startWith as startWith$1, pairwise as pairwise$1, groupBy as groupBy$1, mergeMap as mergeMap$1, retry, throttle, race, skip,
|
|
1
|
+
import { switchMap, from, firstValueFrom, EMPTY, asapScheduler, distinctUntilChanged, map as map$1, combineLatest, of, concatMap, withLatestFrom, filter as filter$1, concat, timer, throwError, defer, Observable, first as first$1, Subject, takeUntil, share, partition, merge, shareReplay, tap as tap$1, catchError as catchError$1, startWith as startWith$1, pairwise as pairwise$1, groupBy as groupBy$1, mergeMap as mergeMap$1, retry, throttle, race, skip, NEVER, fromEvent, Subscription, debounceTime } from "rxjs";
|
|
2
2
|
import { createLogger, pickProperties, insecureRandomId, getClientState, bindActionGlobally, createStateSourceAction, setCleanupTimeout, omitProperty, defineStore, authStore, getAuthLogger, AuthStateType, getCleanedUrl, getTokenFromLocation, createLoggedInAuthState, getAuthCode, REQUEST_TAG_PREFIX, DEFAULT_API_VERSION, getDefaultLocation, isDeepEqual, configureLogging as configureLogging$1, isReleasePerspective, bindActionByResource, isDatasetResource, isMediaLibraryResource, isCanvasResource, getCurrentUserState, getUsersKey, addSubscription, USERS_STATE_CLEAR_DELAY, removeSubscription, parseUsersKey, getClient, PROJECT_API_VERSION, setUsersError, setUsersData, API_VERSION as API_VERSION$8, getDashboardOrganizationId as getDashboardOrganizationId$1, updateLastLoadMoreRequest, cancelRequest, initializeRequest, getTokenState, getQueryState, resolveQuery, bindActionByResourceAndPerspective, PREVIEW_PROJECTION, transformProjectionToPreview } from "./_chunks-es/createGroqSearchFilter.js";
|
|
3
3
|
import { createGroqSearchFilter, getActiveReleasesState, getAllReleasesState, getAuthState, getClientErrorApiBody, getClientErrorApiDescription, getClientErrorApiType, getIsInDashboardState, getLoginUrlState, getPerspectiveState, getQueryKey, isCanvasSource, isDatasetSource, isMediaLibrarySource, isProjectUserNotFoundClientError, isStudioConfig, parseQueryKey, setAuthToken } from "./_chunks-es/createGroqSearchFilter.js";
|
|
4
4
|
import { first, switchMap as switchMap$1, groupBy, mergeMap, startWith, pairwise, filter, map, delay, tap, catchError, scan, share as share$1 } from "rxjs/operators";
|
|
@@ -620,22 +620,24 @@ function convertSanityMutatePatch(sanityPatchMutation) {
|
|
|
620
620
|
return "id" in copy && delete copy.id, copy;
|
|
621
621
|
});
|
|
622
622
|
}
|
|
623
|
-
function editDocument(doc, patches) {
|
|
624
|
-
const effectiveDocumentId = getEffectiveDocumentId(doc);
|
|
623
|
+
function editDocument(doc, patches, options) {
|
|
624
|
+
const effectiveDocumentId = getEffectiveDocumentId(doc), preserveOperations = options?.preserveOperations && { preserveOperations: !0 };
|
|
625
625
|
if (isSanityMutatePatch(patches)) {
|
|
626
626
|
const converted = convertSanityMutatePatch(patches) ?? [];
|
|
627
627
|
return {
|
|
628
628
|
...doc,
|
|
629
629
|
type: "document.edit",
|
|
630
630
|
documentId: effectiveDocumentId,
|
|
631
|
-
patches: converted
|
|
631
|
+
patches: converted,
|
|
632
|
+
...preserveOperations
|
|
632
633
|
};
|
|
633
634
|
}
|
|
634
635
|
return {
|
|
635
636
|
...doc,
|
|
636
637
|
type: "document.edit",
|
|
637
638
|
documentId: effectiveDocumentId,
|
|
638
|
-
...patches && { patches: Array.isArray(patches) ? patches : [patches] }
|
|
639
|
+
...patches && { patches: Array.isArray(patches) ? patches : [patches] },
|
|
640
|
+
...preserveOperations
|
|
639
641
|
};
|
|
640
642
|
}
|
|
641
643
|
function publishDocument(doc) {
|
|
@@ -724,20 +726,40 @@ const ensureArrayKeysDeep = memoize((input) => {
|
|
|
724
726
|
const entries = Object.entries(input).map(([key, value]) => [key, ensureArrayKeysDeep(value)]);
|
|
725
727
|
return entries.every(([key, value]) => input[key] === value) ? input : Object.fromEntries(entries);
|
|
726
728
|
});
|
|
729
|
+
function resolvesArraySegments(input, path) {
|
|
730
|
+
let current = input;
|
|
731
|
+
for (const segment of path) {
|
|
732
|
+
if (typeof segment == "string") {
|
|
733
|
+
current = typeof current == "object" && current !== null && !Array.isArray(current) ? current[segment] : void 0;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (!Array.isArray(current)) return !1;
|
|
737
|
+
if (isKeySegment(segment)) {
|
|
738
|
+
const index = getIndexForKey(current, segment._key);
|
|
739
|
+
if (index === void 0) return !1;
|
|
740
|
+
current = current[index];
|
|
741
|
+
} else {
|
|
742
|
+
const index = segment < 0 ? current.length + segment : segment;
|
|
743
|
+
if (!(index in current)) return !1;
|
|
744
|
+
current = current[index];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return !0;
|
|
748
|
+
}
|
|
727
749
|
function set(input, pathExpressionValues) {
|
|
728
750
|
const result = Object.entries(pathExpressionValues).flatMap(
|
|
729
751
|
([pathExpression, replacementValue]) => Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
|
|
730
752
|
...matchEntry,
|
|
731
753
|
replacementValue
|
|
732
754
|
}))
|
|
733
|
-
).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
|
|
755
|
+
).filter(({ path }) => resolvesArraySegments(input, path)).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
|
|
734
756
|
return ensureArrayKeysDeep(result);
|
|
735
757
|
}
|
|
736
758
|
function setIfMissing(input, pathExpressionValues) {
|
|
737
759
|
const result = Object.entries(pathExpressionValues).flatMap(([pathExpression, replacementValue]) => Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
|
|
738
760
|
...matchEntry,
|
|
739
761
|
replacementValue
|
|
740
|
-
}))).filter((matchEntry) => matchEntry.value === null || matchEntry.value === void 0).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
|
|
762
|
+
}))).filter((matchEntry) => matchEntry.value === null || matchEntry.value === void 0).filter(({ path }) => resolvesArraySegments(input, path)).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
|
|
741
763
|
return ensureArrayKeysDeep(result);
|
|
742
764
|
}
|
|
743
765
|
function unset(input, pathExpressions) {
|
|
@@ -1038,6 +1060,24 @@ class MaxBufferExceededError extends OutOfSyncError {
|
|
|
1038
1060
|
super(message, state), this.name = "MaxBufferExceededError";
|
|
1039
1061
|
}
|
|
1040
1062
|
}
|
|
1063
|
+
function toOrderedChains(events) {
|
|
1064
|
+
return events.filter(
|
|
1065
|
+
(event) => !events.some((other) => other.resultRev === event.previousRev)
|
|
1066
|
+
).map((start) => {
|
|
1067
|
+
const chain = [];
|
|
1068
|
+
let current = start;
|
|
1069
|
+
for (; current; ) {
|
|
1070
|
+
chain.push(current);
|
|
1071
|
+
const currentResultRev = current.resultRev;
|
|
1072
|
+
current = events.find((event) => event.previousRev === currentResultRev);
|
|
1073
|
+
}
|
|
1074
|
+
return chain;
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function discardChainTo(chain, revision) {
|
|
1078
|
+
const revisionIndex = chain.findIndex((event) => event.resultRev === revision);
|
|
1079
|
+
return revisionIndex === -1 ? chain : chain.slice(revisionIndex + 1);
|
|
1080
|
+
}
|
|
1041
1081
|
function sortListenerEvents(options) {
|
|
1042
1082
|
const { resolveChainDeadline = DEFAULT_DEADLINE_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE } = {};
|
|
1043
1083
|
return (input$) => input$.pipe(
|
|
@@ -1056,25 +1096,25 @@ function sortListenerEvents(options) {
|
|
|
1056
1096
|
throw new Error(
|
|
1057
1097
|
"Invalid state. Cannot process mutation event without a base sync event"
|
|
1058
1098
|
);
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
emitEvents
|
|
1067
|
-
}
|
|
1099
|
+
const baseRevision = state.base.revision, chains = toOrderedChains(state.buffer.concat(event)).map((chain) => discardChainTo(chain, baseRevision)).filter((chain) => chain.length > 0), applicable = chains.find((chain) => chain[0].previousRev === baseRevision), buffer = chains.filter((chain) => chain !== applicable).flat();
|
|
1100
|
+
if (applicable) {
|
|
1101
|
+
const last = applicable[applicable.length - 1];
|
|
1102
|
+
return {
|
|
1103
|
+
// If the chain ends in a deletion, the new base revision is undefined.
|
|
1104
|
+
base: { revision: last.transition === "disappear" ? void 0 : last.resultRev },
|
|
1105
|
+
buffer,
|
|
1106
|
+
emitEvents: applicable
|
|
1107
|
+
};
|
|
1068
1108
|
}
|
|
1069
1109
|
if (buffer.length >= maxBufferSize)
|
|
1070
1110
|
throw new MaxBufferExceededError(
|
|
1071
1111
|
`Too many unchainable mutation events (${buffer.length}) waiting to resolve.`,
|
|
1072
|
-
{ base: { revision: baseRevision }, buffer, emitEvents }
|
|
1112
|
+
{ base: { revision: baseRevision }, buffer, emitEvents: [] }
|
|
1073
1113
|
);
|
|
1074
1114
|
return {
|
|
1075
1115
|
base: { revision: baseRevision },
|
|
1076
1116
|
buffer,
|
|
1077
|
-
emitEvents
|
|
1117
|
+
emitEvents: []
|
|
1078
1118
|
};
|
|
1079
1119
|
}
|
|
1080
1120
|
return { ...state, emitEvents: [event] };
|
|
@@ -1105,7 +1145,7 @@ function sortListenerEvents(options) {
|
|
|
1105
1145
|
const listen = ({ state }, documentId) => {
|
|
1106
1146
|
const { sharedListener, fetchDocument } = state.get();
|
|
1107
1147
|
return sharedListener.events.pipe(
|
|
1108
|
-
concatMap((e) => e.type === "welcome" ? fetchDocument(documentId).pipe(
|
|
1148
|
+
concatMap((e) => e.type === "welcome" || e.type === "reset" ? fetchDocument(documentId).pipe(
|
|
1109
1149
|
map$1((document) => ({ type: "sync", document }))
|
|
1110
1150
|
) : e.type === "mutation" && e.documentId === documentId ? of(e) : EMPTY),
|
|
1111
1151
|
sortListenerEvents(),
|
|
@@ -1139,6 +1179,7 @@ const listen = ({ state }, documentId) => {
|
|
|
1139
1179
|
document: document ?? null,
|
|
1140
1180
|
revision: transactionId,
|
|
1141
1181
|
timestamp,
|
|
1182
|
+
mutations: next.mutations,
|
|
1142
1183
|
...previousRev && { previousRev }
|
|
1143
1184
|
};
|
|
1144
1185
|
})
|
|
@@ -1219,6 +1260,67 @@ class ActionError extends Error {
|
|
|
1219
1260
|
}
|
|
1220
1261
|
class PermissionActionError extends ActionError {
|
|
1221
1262
|
}
|
|
1263
|
+
function createMutationApplier(options) {
|
|
1264
|
+
const { documentId, transactionId, timestamp, preserveOperations } = options;
|
|
1265
|
+
return (documents, mutations, documentSetName) => {
|
|
1266
|
+
try {
|
|
1267
|
+
return processMutations({ documents, transactionId, mutations, timestamp });
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
throw preserveOperations ? new ActionError({
|
|
1270
|
+
documentId,
|
|
1271
|
+
transactionId,
|
|
1272
|
+
message: `Failed to apply patches to the ${documentSetName} document: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1273
|
+
}) : error;
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function preserveNumericOperations(baseBefore, userPatches, diffedPatches) {
|
|
1278
|
+
if (!baseBefore || !userPatches?.length) return diffedPatches;
|
|
1279
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const patch of userPatches)
|
|
1281
|
+
for (const [op, sign] of [
|
|
1282
|
+
["inc", 1],
|
|
1283
|
+
["dec", -1]
|
|
1284
|
+
]) {
|
|
1285
|
+
const record = patch[op];
|
|
1286
|
+
if (record) {
|
|
1287
|
+
for (const [pathExpression, amount] of Object.entries(record))
|
|
1288
|
+
if (typeof amount == "number")
|
|
1289
|
+
for (const match of jsonMatch(baseBefore, pathExpression)) {
|
|
1290
|
+
const path = stringifyPath(match.path), existing = targets.get(path);
|
|
1291
|
+
existing ? (existing.expectedValue += sign * amount, existing.delta += sign * amount) : typeof match.value == "number" && targets.set(path, {
|
|
1292
|
+
baseValue: match.value,
|
|
1293
|
+
expectedValue: match.value + sign * amount,
|
|
1294
|
+
delta: sign * amount
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (!targets.size) return diffedPatches;
|
|
1300
|
+
const preserved = /* @__PURE__ */ new Map(), next = diffedPatches.flatMap((patch) => {
|
|
1301
|
+
if (!patch.set) return [patch];
|
|
1302
|
+
const keptSet = {};
|
|
1303
|
+
for (const [path, value] of Object.entries(patch.set)) {
|
|
1304
|
+
const target = targets.get(path);
|
|
1305
|
+
target && typeof value == "number" && value === target.expectedValue ? preserved.set(path, { delta: target.delta, baseValue: target.baseValue }) : keptSet[path] = value;
|
|
1306
|
+
}
|
|
1307
|
+
if (Object.keys(keptSet).length === Object.keys(patch.set).length) return [patch];
|
|
1308
|
+
const { set: _set, ...rest } = patch, withKept = Object.keys(keptSet).length ? { ...rest, set: keptSet } : rest;
|
|
1309
|
+
return Object.keys(withKept).length ? [withKept] : [];
|
|
1310
|
+
});
|
|
1311
|
+
if (!preserved.size) return next;
|
|
1312
|
+
const setIfMissing2 = {}, inc2 = {}, dec2 = {};
|
|
1313
|
+
for (const [path, { delta, baseValue }] of preserved)
|
|
1314
|
+
setIfMissing2[path] = baseValue, delta >= 0 ? inc2[path] = delta : dec2[path] = -delta;
|
|
1315
|
+
return [
|
|
1316
|
+
...next,
|
|
1317
|
+
{
|
|
1318
|
+
setIfMissing: setIfMissing2,
|
|
1319
|
+
...Object.keys(inc2).length && { inc: inc2 },
|
|
1320
|
+
...Object.keys(dec2).length && { dec: dec2 }
|
|
1321
|
+
}
|
|
1322
|
+
];
|
|
1323
|
+
}
|
|
1222
1324
|
function applySingleDocPatch({
|
|
1223
1325
|
base: initialBase,
|
|
1224
1326
|
working: initialWorking,
|
|
@@ -1229,7 +1331,8 @@ function applySingleDocPatch({
|
|
|
1229
1331
|
grants,
|
|
1230
1332
|
identity,
|
|
1231
1333
|
notFoundMessage = "Cannot edit document because it does not exist.",
|
|
1232
|
-
permissionMessage = `You do not have permission to edit document "${documentId}"
|
|
1334
|
+
permissionMessage = `You do not have permission to edit document "${documentId}".`,
|
|
1335
|
+
preserveOperations
|
|
1233
1336
|
}) {
|
|
1234
1337
|
let base = initialBase, working = initialWorking;
|
|
1235
1338
|
const userPatches = patches?.map((patch) => ({ patch: { id: documentId, ...patch } }));
|
|
@@ -1237,20 +1340,24 @@ function applySingleDocPatch({
|
|
|
1237
1340
|
return { base, working, diffedPatches: [], workingMutations: [] };
|
|
1238
1341
|
if (!working[documentId] || !base[documentId])
|
|
1239
1342
|
throw new ActionError({ documentId, transactionId, message: notFoundMessage });
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1343
|
+
const applyMutations = createMutationApplier({
|
|
1344
|
+
documentId,
|
|
1345
|
+
transactionId,
|
|
1346
|
+
timestamp,
|
|
1347
|
+
preserveOperations
|
|
1348
|
+
}), baseBefore = base[documentId];
|
|
1349
|
+
base = applyMutations(base, userPatches, "base");
|
|
1350
|
+
const baseAfter = base[documentId], diffedPatches = preserveOperations ? patches : preserveNumericOperations(
|
|
1351
|
+
baseBefore,
|
|
1352
|
+
patches,
|
|
1353
|
+
diffValue(baseBefore, baseAfter)
|
|
1354
|
+
), workingBefore = working[documentId];
|
|
1243
1355
|
if (!checkGrant(grants.update, workingBefore, identity))
|
|
1244
1356
|
throw new PermissionActionError({ documentId, transactionId, message: permissionMessage });
|
|
1245
1357
|
const workingMutations = diffedPatches.map((patch) => ({
|
|
1246
1358
|
patch: { id: documentId, ...patch }
|
|
1247
1359
|
}));
|
|
1248
|
-
return working =
|
|
1249
|
-
documents: working,
|
|
1250
|
-
transactionId,
|
|
1251
|
-
mutations: workingMutations,
|
|
1252
|
-
timestamp
|
|
1253
|
-
}), { base, working, diffedPatches, workingMutations };
|
|
1360
|
+
return working = applyMutations(working, workingMutations, "working"), { base, working, diffedPatches, workingMutations };
|
|
1254
1361
|
}
|
|
1255
1362
|
function createGrantsLookup(datasetAcl) {
|
|
1256
1363
|
const filtersByGrant = {
|
|
@@ -1550,7 +1657,8 @@ function handleEdit(action, ctx) {
|
|
|
1550
1657
|
transactionId,
|
|
1551
1658
|
timestamp,
|
|
1552
1659
|
grants,
|
|
1553
|
-
identity
|
|
1660
|
+
identity,
|
|
1661
|
+
preserveOperations: action.preserveOperations
|
|
1554
1662
|
});
|
|
1555
1663
|
return outgoingMutations.push(...result.workingMutations), { base: result.base, working: result.working };
|
|
1556
1664
|
}
|
|
@@ -1571,16 +1679,20 @@ function handleEdit(action, ctx) {
|
|
|
1571
1679
|
transactionId,
|
|
1572
1680
|
message: "Cannot edit document because it does not exist in draft or published form."
|
|
1573
1681
|
});
|
|
1574
|
-
const
|
|
1682
|
+
const applyMutations = createMutationApplier({
|
|
1683
|
+
documentId,
|
|
1684
|
+
transactionId,
|
|
1685
|
+
timestamp,
|
|
1686
|
+
preserveOperations: action.preserveOperations
|
|
1687
|
+
}), baseMutations = [];
|
|
1575
1688
|
!isReleasePerspective(action.perspective) && !base[draftId] && base[publishedId] && baseMutations.push({ create: { ...base[publishedId], _id: draftId } });
|
|
1576
1689
|
const baseBefore = base[patchDocumentId] ?? base[publishedId];
|
|
1577
|
-
userPatches && baseMutations.push(...userPatches), base =
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const baseAfter = base[patchDocumentId], patches = diffValue(baseBefore, baseAfter), workingMutations = [];
|
|
1690
|
+
userPatches && baseMutations.push(...userPatches), base = applyMutations(base, baseMutations, "base");
|
|
1691
|
+
const baseAfter = base[patchDocumentId], patches = action.preserveOperations ? action.patches ?? [] : preserveNumericOperations(
|
|
1692
|
+
baseBefore,
|
|
1693
|
+
action.patches,
|
|
1694
|
+
diffValue(baseBefore, baseAfter)
|
|
1695
|
+
), workingMutations = [];
|
|
1584
1696
|
if (!isReleasePerspective(action.perspective) && !working[draftId] && working[publishedId]) {
|
|
1585
1697
|
const newDraftFromPublished = { ...working[publishedId], _id: draftId };
|
|
1586
1698
|
if (!checkGrant(grants.create, newDraftFromPublished, identity))
|
|
@@ -1598,12 +1710,7 @@ function handleEdit(action, ctx) {
|
|
|
1598
1710
|
transactionId,
|
|
1599
1711
|
message: `You do not have permission to edit document "${documentId}".`
|
|
1600
1712
|
});
|
|
1601
|
-
return workingMutations.push(...patches.map((patch) => ({ patch: { id: patchDocumentId, ...patch } }))), working =
|
|
1602
|
-
documents: working,
|
|
1603
|
-
transactionId,
|
|
1604
|
-
mutations: workingMutations,
|
|
1605
|
-
timestamp
|
|
1606
|
-
}), outgoingMutations.push(...workingMutations), outgoingActions.push(
|
|
1713
|
+
return workingMutations.push(...patches.map((patch) => ({ patch: { id: patchDocumentId, ...patch } }))), working = applyMutations(working, workingMutations, "working"), outgoingMutations.push(...workingMutations), outgoingActions.push(
|
|
1607
1714
|
...patches.map((patch) => ({
|
|
1608
1715
|
actionType: "sanity.action.document.edit",
|
|
1609
1716
|
draftId: patchDocumentId,
|
|
@@ -2109,7 +2216,16 @@ function transitionAppliedTransactionsToOutgoing(prev) {
|
|
|
2109
2216
|
...documentState.unverifiedRevisions,
|
|
2110
2217
|
// add unverified revision
|
|
2111
2218
|
[transactionId]: { documentId, previousRev, transactionId, timestamp }
|
|
2112
|
-
}
|
|
2219
|
+
},
|
|
2220
|
+
// also track the transaction ID durably (unverified revisions can be
|
|
2221
|
+
// pruned by sync events before the listener echo arrives) so
|
|
2222
|
+
// `remote-patches` events label our own transactions correctly
|
|
2223
|
+
recentOwnTransactionIds: [
|
|
2224
|
+
...(documentState.recentOwnTransactionIds ?? []).slice(
|
|
2225
|
+
-49
|
|
2226
|
+
),
|
|
2227
|
+
transactionId
|
|
2228
|
+
]
|
|
2113
2229
|
}), acc;
|
|
2114
2230
|
},
|
|
2115
2231
|
{ ...prev.documentStates }
|
|
@@ -2159,15 +2275,38 @@ function revertOutgoingTransaction(prev) {
|
|
|
2159
2275
|
)
|
|
2160
2276
|
};
|
|
2161
2277
|
}
|
|
2162
|
-
function
|
|
2278
|
+
function extractPatchOperations(mutations, documentId) {
|
|
2279
|
+
return mutations ? mutations.flatMap((mutation) => {
|
|
2280
|
+
if (!("patch" in mutation) || !mutation.patch) return [];
|
|
2281
|
+
const { id, ...operations } = mutation.patch;
|
|
2282
|
+
return id !== documentId ? [] : [operations];
|
|
2283
|
+
}) : [];
|
|
2284
|
+
}
|
|
2285
|
+
function applyRemoteDocument(prev, { document, documentId, previousRev, revision, timestamp, type, mutations }, events) {
|
|
2163
2286
|
if (!prev.grants) return prev;
|
|
2164
2287
|
const prevDocState = prev.documentStates[documentId];
|
|
2165
2288
|
if (!prevDocState) return prev;
|
|
2166
2289
|
const prevUnverifiedRevisions = prevDocState.unverifiedRevisions, revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : void 0;
|
|
2167
2290
|
let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS;
|
|
2168
|
-
if (revision && revisionToVerify && (unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)), type === "
|
|
2291
|
+
if (revision && revisionToVerify && (unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)), type === "mutation" && revision) {
|
|
2292
|
+
const patches = extractPatchOperations(mutations, documentId);
|
|
2293
|
+
if (patches.length) {
|
|
2294
|
+
const isOwnTransaction = !!revisionToVerify || (prevDocState.recentOwnTransactionIds?.includes(revision) ?? !1);
|
|
2295
|
+
events.next({
|
|
2296
|
+
type: "remote-patches",
|
|
2297
|
+
documentId,
|
|
2298
|
+
transactionId: revision,
|
|
2299
|
+
previousRev,
|
|
2300
|
+
timestamp,
|
|
2301
|
+
patches,
|
|
2302
|
+
origin: isOwnTransaction ? "local" : "remote"
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (type === "sync" && (unverifiedRevisions = Object.fromEntries(
|
|
2169
2307
|
Object.entries(unverifiedRevisions).filter(([, unverifiedRevision]) => unverifiedRevision ? new Date(timestamp).getTime() <= new Date(unverifiedRevision.timestamp).getTime() : !1)
|
|
2170
|
-
)), revisionToVerify && revisionToVerify.previousRev === previousRev)
|
|
2308
|
+
)), revisionToVerify && revisionToVerify.previousRev === previousRev) {
|
|
2309
|
+
const modifiesDocument = (transaction) => transaction.working[documentId]?._rev !== transaction.previousRevs[documentId], local2 = !prev.applied.some(modifiesDocument) && (!prev.outgoing || prev.outgoing.transactionId === revision || !modifiesDocument(prev.outgoing)) && !isDeepEqual(prevDocState.local, document) ? document : prevDocState.local;
|
|
2171
2310
|
return {
|
|
2172
2311
|
...prev,
|
|
2173
2312
|
documentStates: {
|
|
@@ -2176,10 +2315,12 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
|
|
|
2176
2315
|
...prevDocState,
|
|
2177
2316
|
remote: document,
|
|
2178
2317
|
remoteRev: revision,
|
|
2318
|
+
local: local2,
|
|
2179
2319
|
unverifiedRevisions
|
|
2180
2320
|
}
|
|
2181
2321
|
}
|
|
2182
2322
|
};
|
|
2323
|
+
}
|
|
2183
2324
|
let working = { ...prev.applied.at(0)?.previous, [documentId]: document };
|
|
2184
2325
|
const nextApplied = [];
|
|
2185
2326
|
for (const curr of prev.applied)
|
|
@@ -2199,6 +2340,7 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
|
|
|
2199
2340
|
}
|
|
2200
2341
|
throw error;
|
|
2201
2342
|
}
|
|
2343
|
+
const nextLocal = working[documentId], local = isDeepEqual(prevDocState.local, nextLocal) ? prevDocState.local : nextLocal;
|
|
2202
2344
|
return {
|
|
2203
2345
|
...prev,
|
|
2204
2346
|
applied: nextApplied,
|
|
@@ -2208,7 +2350,7 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
|
|
|
2208
2350
|
...prevDocState,
|
|
2209
2351
|
remote: document,
|
|
2210
2352
|
remoteRev: revision,
|
|
2211
|
-
local
|
|
2353
|
+
local,
|
|
2212
2354
|
unverifiedRevisions
|
|
2213
2355
|
}
|
|
2214
2356
|
}
|
|
@@ -2300,6 +2442,85 @@ function getDocumentEvents(outgoing) {
|
|
|
2300
2442
|
)
|
|
2301
2443
|
);
|
|
2302
2444
|
}
|
|
2445
|
+
const DEDUPE_TTL = 12e4, DEDUPE_MAX_ENTRIES = 1e3, GROUP_FLUSH_DEADLINE = 3e4, GROUP_MAX_HELD_EVENTS = 100;
|
|
2446
|
+
function dedupeListenerEvents(options) {
|
|
2447
|
+
const { ttl = DEDUPE_TTL, maxEntries = DEDUPE_MAX_ENTRIES } = {};
|
|
2448
|
+
return (source) => defer(() => {
|
|
2449
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2450
|
+
return source.pipe(
|
|
2451
|
+
filter$1((event) => {
|
|
2452
|
+
if (event.type !== "mutation") return !0;
|
|
2453
|
+
const key = `${event.transactionId}#${event.documentId}`, now = Date.now(), expiry = seen.get(key);
|
|
2454
|
+
if (expiry !== void 0 && expiry > now) return !1;
|
|
2455
|
+
seen.delete(key), seen.set(key, now + ttl);
|
|
2456
|
+
for (const [existingKey, existingExpiry] of seen) {
|
|
2457
|
+
if (seen.size <= maxEntries && existingExpiry > now) break;
|
|
2458
|
+
seen.delete(existingKey);
|
|
2459
|
+
}
|
|
2460
|
+
return !0;
|
|
2461
|
+
})
|
|
2462
|
+
);
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
function groupTransactionEvents(options) {
|
|
2466
|
+
const { flushDeadline = GROUP_FLUSH_DEADLINE, maxHeldEvents = GROUP_MAX_HELD_EVENTS } = {};
|
|
2467
|
+
return (source) => new Observable((subscriber) => {
|
|
2468
|
+
let open = null, held = [];
|
|
2469
|
+
const closeOpen = () => {
|
|
2470
|
+
if (!open) return;
|
|
2471
|
+
clearTimeout(open.timer);
|
|
2472
|
+
const { events } = open;
|
|
2473
|
+
open = null;
|
|
2474
|
+
for (const event of events) subscriber.next(event);
|
|
2475
|
+
}, releaseHeld = () => {
|
|
2476
|
+
const queue = held;
|
|
2477
|
+
held = [];
|
|
2478
|
+
for (const event of queue) processEvent(event);
|
|
2479
|
+
}, releaseEverything = () => {
|
|
2480
|
+
closeOpen();
|
|
2481
|
+
const queue = held;
|
|
2482
|
+
held = [];
|
|
2483
|
+
for (const event of queue) subscriber.next(event);
|
|
2484
|
+
}, processEvent = (event) => {
|
|
2485
|
+
if (event.type !== "mutation") {
|
|
2486
|
+
releaseEverything(), subscriber.next(event);
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
const isMultiDocument = (event.transactionTotalEvents ?? 0) > 1;
|
|
2490
|
+
if (!open) {
|
|
2491
|
+
if (!isMultiDocument) {
|
|
2492
|
+
subscriber.next(event);
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
open = {
|
|
2496
|
+
transactionId: event.transactionId,
|
|
2497
|
+
events: [event],
|
|
2498
|
+
total: event.transactionTotalEvents,
|
|
2499
|
+
timer: setCleanupTimeout(() => {
|
|
2500
|
+
closeOpen(), releaseHeld();
|
|
2501
|
+
}, flushDeadline)
|
|
2502
|
+
};
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
if (event.transactionId === open.transactionId) {
|
|
2506
|
+
open.events.push(event), open.events.length >= open.total && (closeOpen(), releaseHeld());
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
held.push(event), held.length > maxHeldEvents && releaseEverything();
|
|
2510
|
+
}, subscription = source.subscribe({
|
|
2511
|
+
next: processEvent,
|
|
2512
|
+
error: (error) => {
|
|
2513
|
+
releaseEverything(), subscriber.error(error);
|
|
2514
|
+
},
|
|
2515
|
+
complete: () => {
|
|
2516
|
+
releaseEverything(), subscriber.complete();
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
return () => {
|
|
2520
|
+
open && clearTimeout(open.timer), open = null, held = [], subscription.unsubscribe();
|
|
2521
|
+
};
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2303
2524
|
const API_VERSION$3 = "v2025-05-06";
|
|
2304
2525
|
function createSharedListener(instance, resource) {
|
|
2305
2526
|
const dispose$ = new Subject(), events$ = getClientState(instance, {
|
|
@@ -2315,7 +2536,12 @@ function createSharedListener(instance, resource) {
|
|
|
2315
2536
|
"*",
|
|
2316
2537
|
{},
|
|
2317
2538
|
{
|
|
2318
|
-
|
|
2539
|
+
// with `enableResume`, a reconnect replays the events missed while
|
|
2540
|
+
// offline (emitting `welcomeback`) instead of forcing a refetch of
|
|
2541
|
+
// every subscribed document. when the backend cannot resume, it
|
|
2542
|
+
// emits `welcome` or `reset`, both of which trigger a refetch
|
|
2543
|
+
events: ["mutation", "welcome", "welcomeback", "reconnect", "reset"],
|
|
2544
|
+
enableResume: !0,
|
|
2319
2545
|
includeResult: !1,
|
|
2320
2546
|
// the document store handles version documents, so we need to include all versions
|
|
2321
2547
|
includeAllVersions: !0,
|
|
@@ -2328,6 +2554,11 @@ function createSharedListener(instance, resource) {
|
|
|
2328
2554
|
)
|
|
2329
2555
|
)
|
|
2330
2556
|
),
|
|
2557
|
+
// resumed events are delivered at-least-once, so drop replayed duplicates
|
|
2558
|
+
dedupeListenerEvents(),
|
|
2559
|
+
// hold multi-document transactions (e.g. publish) until complete so
|
|
2560
|
+
// per-document processing never observes half a transaction
|
|
2561
|
+
groupTransactionEvents(),
|
|
2331
2562
|
takeUntil(dispose$),
|
|
2332
2563
|
share()
|
|
2333
2564
|
), [welcome$, mutation$] = partition(events$, (e) => e.type === "welcome");
|
|
@@ -2607,8 +2838,7 @@ const _resolveDocument = bindActionByResource(
|
|
|
2607
2838
|
switchMap(
|
|
2608
2839
|
(client) => client.observable.request({
|
|
2609
2840
|
uri,
|
|
2610
|
-
tag: "acl.get"
|
|
2611
|
-
withCredentials: !0
|
|
2841
|
+
tag: "acl.get"
|
|
2612
2842
|
})
|
|
2613
2843
|
),
|
|
2614
2844
|
tap$1((datasetAcl) => state.set("setGrants", { grants: createGrantsLookup(datasetAcl) }))
|