@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/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, Observable, NEVER, fromEvent, Subscription, debounceTime, defer } from "rxjs";
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 buffer = state.buffer.concat(event), emitEvents = [];
1060
- let baseRevision = state.base.revision, progress = !0;
1061
- for (; progress; ) {
1062
- progress = !1;
1063
- const idx = buffer.findIndex((e) => e.previousRev === baseRevision);
1064
- if (idx !== -1) {
1065
- const [next] = buffer.splice(idx, 1);
1066
- emitEvents.push(next), baseRevision = next.transition === "disappear" ? void 0 : next.resultRev, progress = !0;
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 baseBefore = base[documentId];
1241
- base = processMutations({ documents: base, transactionId, mutations: userPatches, timestamp });
1242
- const baseAfter = base[documentId], diffedPatches = diffValue(baseBefore, baseAfter), workingBefore = working[documentId];
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 = processMutations({
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 baseMutations = [];
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 = processMutations({
1578
- documents: base,
1579
- transactionId,
1580
- mutations: baseMutations,
1581
- timestamp
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 = processMutations({
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 applyRemoteDocument(prev, { document, documentId, previousRev, revision, timestamp, type }, events) {
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 === "sync" && (unverifiedRevisions = Object.fromEntries(
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: working[documentId],
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
- events: ["mutation", "welcome", "reconnect"],
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) }))