@sanity/sdk 2.17.0 → 2.19.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.
Files changed (41) hide show
  1. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +8 -11
  2. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
  3. package/dist/_chunks-es/_internal.js +1 -0
  4. package/dist/_chunks-es/_internal.js.map +1 -1
  5. package/dist/_chunks-es/createGroqSearchFilter.js +8 -4
  6. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  7. package/dist/_chunks-es/telemetryManager.js +2 -2
  8. package/dist/_exports/_internal.d.ts.map +1 -1
  9. package/dist/index.d.ts +197 -13
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +492 -123
  12. package/dist/index.js.map +1 -1
  13. package/package.json +10 -10
  14. package/src/_exports/index.ts +7 -1
  15. package/src/document/documentStore.concurrency.test.ts +1306 -0
  16. package/src/document/documentStore.test.ts +11 -0
  17. package/src/document/documentStore.ts +0 -1
  18. package/src/document/listen.test.ts +56 -0
  19. package/src/document/listen.ts +65 -20
  20. package/src/document/listenerEventOperators.test.ts +311 -0
  21. package/src/document/listenerEventOperators.ts +217 -0
  22. package/src/document/patchOperations.test.ts +49 -2
  23. package/src/document/patchOperations.ts +50 -0
  24. package/src/document/processActions/edit.ts +9 -2
  25. package/src/document/processActions/shared.ts +111 -1
  26. package/src/document/processActions.test.ts +185 -1
  27. package/src/document/reducers.test.ts +123 -0
  28. package/src/document/reducers.ts +37 -2
  29. package/src/document/sharedListener.test.ts +2 -1
  30. package/src/document/sharedListener.ts +12 -1
  31. package/src/document/util.test.ts +46 -0
  32. package/src/document/util.ts +37 -1
  33. package/src/presence/bifurTransport.test.ts +155 -3
  34. package/src/presence/bifurTransport.ts +79 -12
  35. package/src/presence/paths.test.ts +70 -0
  36. package/src/presence/paths.ts +41 -0
  37. package/src/presence/presenceStore.test.ts +822 -2
  38. package/src/presence/presenceStore.ts +518 -102
  39. package/src/presence/types.ts +175 -3
  40. package/dist/_chunks-es/version.js +0 -7
  41. package/dist/_chunks-es/version.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,34 +1,36 @@
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 { CorsOriginError } from "@sanity/client";
2
+ 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, auditTime, debounceTime } from "rxjs";
2
3
  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
- 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
- import { first, switchMap as switchMap$1, groupBy, mergeMap, startWith, pairwise, filter, map, delay, tap, catchError, scan, share as share$1 } from "rxjs/operators";
4
+ import { CORE_SDK_VERSION, createGroqSearchFilter, getActiveReleasesState, getAllReleasesState, getAuthState, getClientErrorApiBody, getClientErrorApiDescription, getClientErrorApiType, getIsInDashboardState, getLoginUrlState, getPerspectiveState, getQueryKey, isCanvasSource, isDatasetSource, isMediaLibrarySource, isProjectUserNotFoundClientError, isStudioConfig, parseQueryKey, setAuthToken } from "./_chunks-es/createGroqSearchFilter.js";
5
+ import "zustand/middleware";
6
+ import "zustand/vanilla";
7
+ import { first, switchMap as switchMap$1, groupBy, mergeMap, startWith, pairwise, filter, map, delay, tap, catchError, scan, retry as retry$1, distinctUntilChanged as distinctUntilChanged$1, share as share$1 } from "rxjs/operators";
5
8
  import { createController, createNode } from "@sanity/comlink";
6
9
  import { createSelector } from "reselect";
7
10
  import { SanityEncoder } from "@sanity/mutate";
8
- import { getVersionId, DocumentId, getPublishedId, getDraftId, isDraftId, isVersionId, isPublishedId } from "@sanity/id-utils";
11
+ import { createDocumentLoaderFromClient } from "@sanity/mutate/_unstable_store";
12
+ import { isKeySegment, isKeyedObject } from "@sanity/types";
13
+ import "groq";
14
+ import { getPublishedId, DocumentId, getVersionId, getDraftId, isDraftId, isVersionId, isPublishedId } from "@sanity/id-utils";
9
15
  import { jsonMatch, stringifyPath, slicePath, getIndexForKey } from "@sanity/json-match";
10
16
  import { getIndexForKey as getIndexForKey2, getPathDepth, joinPaths, jsonMatch as jsonMatch2, slicePath as slicePath2, stringifyPath as stringifyPath2 } from "@sanity/json-match";
11
17
  import { evaluateSync, parse } from "groq-js";
12
18
  import { applyPatches, parsePatch } from "@sanity/diff-match-patch";
13
- import { isKeySegment, isKeyedObject } from "@sanity/types";
14
19
  import { diffValue } from "@sanity/diff-patch";
15
- import { createDocumentLoaderFromClient } from "@sanity/mutate/_unstable_store";
16
20
  import { SDK_CHANNEL_NAME, SDK_NODE_NAME } from "@sanity/message-protocol";
17
21
  import { fromUrl } from "@sanity/bifur-client";
18
- import { CorsOriginError } from "@sanity/client";
19
- import { CORE_SDK_VERSION } from "./_chunks-es/version.js";
20
22
  function createSanityInstance(config = {}) {
21
23
  const instanceId = crypto.randomUUID(), disposeListeners = /* @__PURE__ */ new Map(), disposed = { current: !1 }, instanceContext = {
22
24
  instanceId,
23
25
  projectId: config.projectId,
24
26
  dataset: config.dataset
25
- }, logger = createLogger("sdk", { instanceContext });
26
- logger.info("Sanity instance created", {
27
+ }, logger2 = createLogger("sdk", { instanceContext });
28
+ logger2.info("Sanity instance created", {
27
29
  hasProjectId: !!config.projectId,
28
30
  hasDataset: !!config.dataset,
29
31
  hasAuth: !!config.auth,
30
32
  hasPerspective: !!config.perspective
31
- }), logger.debug("Instance configuration", {
33
+ }), logger2.debug("Instance configuration", {
32
34
  projectId: config.projectId,
33
35
  dataset: config.dataset,
34
36
  perspective: config.perspective,
@@ -44,13 +46,13 @@ function createSanityInstance(config = {}) {
44
46
  isDisposed: () => disposed.current,
45
47
  dispose: () => {
46
48
  if (disposed.current) {
47
- logger.trace("Dispose called on already disposed instance", { internal: !0 });
49
+ logger2.trace("Dispose called on already disposed instance", { internal: !0 });
48
50
  return;
49
51
  }
50
- logger.trace("Disposing instance", {
52
+ logger2.trace("Disposing instance", {
51
53
  internal: !0,
52
54
  listenerCount: disposeListeners.size
53
- }), disposed.current = !0, disposeListeners.forEach((listener) => listener()), disposeListeners.clear(), logger.info("Instance disposed");
55
+ }), disposed.current = !0, disposeListeners.forEach((listener) => listener()), disposeListeners.clear(), logger2.info("Instance disposed");
54
56
  },
55
57
  onDispose: (cb) => {
56
58
  const listenerId = insecureRandomId();
@@ -61,7 +63,7 @@ function createSanityInstance(config = {}) {
61
63
  getParent: () => {
62
64
  },
63
65
  createChild: (next) => {
64
- logger.debug("Creating child instance", {
66
+ logger2.debug("Creating child instance", {
65
67
  parentInstanceId: instanceId.slice(0, 8),
66
68
  overridingProjectId: !!next.projectId,
67
69
  overridingDataset: !!next.dataset,
@@ -75,7 +77,7 @@ function createSanityInstance(config = {}) {
75
77
  }),
76
78
  { getParent: () => instance }
77
79
  );
78
- return logger.trace("Child instance created", {
80
+ return logger2.trace("Child instance created", {
79
81
  internal: !0,
80
82
  childInstanceId: child.instanceId.slice(0, 8)
81
83
  }), child;
@@ -329,34 +331,34 @@ function observeOrganizationVerificationState(instance, projectIds) {
329
331
  const handleAuthCallback = bindActionGlobally(
330
332
  authStore,
331
333
  async ({ state, instance }, locationHref = getDefaultLocation()) => {
332
- const logger = getAuthLogger(instance), { providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey } = state.get().options;
334
+ const logger2 = getAuthLogger(instance), { providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey } = state.get().options;
333
335
  if (providedToken)
334
- return logger.debug("Skipping auth callback - token already provided"), !1;
336
+ return logger2.debug("Skipping auth callback - token already provided"), !1;
335
337
  const { authState } = state.get();
336
338
  if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken)
337
- return logger.debug("Skipping auth callback - token exchange already in progress"), !1;
339
+ return logger2.debug("Skipping auth callback - token exchange already in progress"), !1;
338
340
  const cleanedUrl = getCleanedUrl(locationHref), tokenFromUrl = getTokenFromLocation(locationHref);
339
341
  if (tokenFromUrl)
340
- return logger.info("Auth token found in URL, logging in"), state.set("setTokenFromUrl", {
342
+ return logger2.info("Auth token found in URL, logging in"), state.set("setTokenFromUrl", {
341
343
  authState: createLoggedInAuthState(tokenFromUrl, null)
342
344
  }), cleanedUrl;
343
345
  const authCode = getAuthCode(callbackUrl, locationHref);
344
346
  if (!authCode)
345
- return logger.debug("No auth code found in callback URL"), !1;
347
+ return logger2.debug("No auth code found in callback URL"), !1;
346
348
  const parsedUrl = new URL(locationHref);
347
349
  let dashboardContext = {};
348
350
  try {
349
351
  const contextParam = parsedUrl.searchParams.get("_context");
350
352
  if (contextParam) {
351
353
  const parsedContext = JSON.parse(contextParam);
352
- parsedContext && typeof parsedContext == "object" && (delete parsedContext.sid, dashboardContext = parsedContext, logger.debug("Dashboard context parsed from callback URL", {
354
+ parsedContext && typeof parsedContext == "object" && (delete parsedContext.sid, dashboardContext = parsedContext, logger2.debug("Dashboard context parsed from callback URL", {
353
355
  hasDashboardContext: !0
354
356
  }));
355
357
  }
356
358
  } catch (err) {
357
- logger.warn("Failed to parse dashboard context from callback URL", { error: err });
359
+ logger2.warn("Failed to parse dashboard context from callback URL", { error: err });
358
360
  }
359
- logger.info("Exchanging auth code for token"), state.set("exchangeSessionForToken", {
361
+ logger2.info("Exchanging auth code for token"), state.set("exchangeSessionForToken", {
360
362
  authState: { type: AuthStateType.LOGGING_IN, isExchangingToken: !0 },
361
363
  dashboardContext
362
364
  });
@@ -368,33 +370,33 @@ const handleAuthCallback = bindActionGlobally(
368
370
  useCdn: !1,
369
371
  ...apiHost && { apiHost }
370
372
  });
371
- logger.debug("Fetching token from auth endpoint");
373
+ logger2.debug("Fetching token from auth endpoint");
372
374
  const { token } = await client.request({
373
375
  method: "GET",
374
376
  uri: "/auth/fetch",
375
377
  query: { sid: authCode },
376
378
  tag: "fetch-token"
377
379
  });
378
- return logger.info("Auth token obtained successfully, user logged in"), storageArea?.setItem(storageKey, JSON.stringify({ token })), state.set("setToken", { authState: createLoggedInAuthState(token, null) }), cleanedUrl;
380
+ return logger2.info("Auth token obtained successfully, user logged in"), storageArea?.setItem(storageKey, JSON.stringify({ token })), state.set("setToken", { authState: createLoggedInAuthState(token, null) }), cleanedUrl;
379
381
  } catch (error) {
380
- return logger.error("Failed to exchange auth code for token", { error }), state.set("exchangeSessionForTokenError", { authState: { type: AuthStateType.ERROR, error } }), cleanedUrl;
382
+ return logger2.error("Failed to exchange auth code for token", { error }), state.set("exchangeSessionForTokenError", { authState: { type: AuthStateType.ERROR, error } }), cleanedUrl;
381
383
  }
382
384
  }
383
385
  ), logout = bindActionGlobally(authStore, async ({ state, instance }) => {
384
- const logger = getAuthLogger(instance), { clientFactory, apiHost, providedToken, storageArea, storageKey } = state.get().options;
386
+ const logger2 = getAuthLogger(instance), { clientFactory, apiHost, providedToken, storageArea, storageKey } = state.get().options;
385
387
  if (providedToken) {
386
- logger.debug("Skipping logout - token is statically provided");
388
+ logger2.debug("Skipping logout - token is statically provided");
387
389
  return;
388
390
  }
389
391
  const { authState } = state.get();
390
392
  if (authState.type === AuthStateType.LOGGED_OUT && authState.isDestroyingSession) {
391
- logger.debug("Skipping logout - already in progress");
393
+ logger2.debug("Skipping logout - already in progress");
392
394
  return;
393
395
  }
394
396
  const token = authState.type === AuthStateType.LOGGED_IN && authState.token;
395
397
  try {
396
398
  if (token) {
397
- logger.info("Logging out user"), state.set("loggingOut", {
399
+ logger2.info("Logging out user"), state.set("loggingOut", {
398
400
  authState: { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !0 }
399
401
  });
400
402
  const client = clientFactory({
@@ -405,13 +407,13 @@ const handleAuthCallback = bindActionGlobally(
405
407
  useProjectHostname: !1,
406
408
  useCdn: !1
407
409
  });
408
- logger.debug("Calling logout endpoint"), await client.request({ uri: "/auth/logout", method: "POST", tag: "logout" });
410
+ logger2.debug("Calling logout endpoint"), await client.request({ uri: "/auth/logout", method: "POST", tag: "logout" });
409
411
  } else
410
- logger.debug("No token to logout - already logged out");
412
+ logger2.debug("No token to logout - already logged out");
411
413
  } catch (error) {
412
- throw logger.error("Logout request failed", { error }), error;
414
+ throw logger2.error("Logout request failed", { error }), error;
413
415
  } finally {
414
- logger.info("User logged out, clearing stored tokens"), state.set("logoutSuccess", {
416
+ logger2.info("User logged out, clearing stored tokens"), state.set("logoutSuccess", {
415
417
  authState: { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 }
416
418
  }), storageArea?.removeItem(storageKey), storageArea?.removeItem(`${storageKey}_last_refresh`);
417
419
  }
@@ -593,6 +595,9 @@ const API_VERSION$5 = "v2025-02-19", datasets = createFetcherStore({
593
595
  useProjectHostname: !0
594
596
  }).observable.pipe(switchMap((client) => client.observable.datasets.list()))
595
597
  }), getDatasetsState = datasets.getState, resolveDatasets = datasets.resolveState;
598
+ function getEditingDocumentId(doc) {
599
+ return doc.liveEdit ? getPublishedId(DocumentId(doc.documentId)) : isReleasePerspective(doc.perspective) ? getVersionId(DocumentId(doc.documentId), doc.perspective.releaseName) : doc.perspective === "published" ? getPublishedId(DocumentId(doc.documentId)) : getDraftId(DocumentId(doc.documentId));
600
+ }
596
601
  function getEffectiveDocumentId(doc) {
597
602
  return doc.liveEdit ? doc.documentId : isReleasePerspective(doc.perspective) ? getVersionId(DocumentId(doc.documentId), doc.perspective.releaseName) : getPublishedId(DocumentId(doc.documentId));
598
603
  }
@@ -726,20 +731,40 @@ const ensureArrayKeysDeep = memoize((input) => {
726
731
  const entries = Object.entries(input).map(([key, value]) => [key, ensureArrayKeysDeep(value)]);
727
732
  return entries.every(([key, value]) => input[key] === value) ? input : Object.fromEntries(entries);
728
733
  });
734
+ function resolvesArraySegments(input, path) {
735
+ let current = input;
736
+ for (const segment of path) {
737
+ if (typeof segment == "string") {
738
+ current = typeof current == "object" && current !== null && !Array.isArray(current) ? current[segment] : void 0;
739
+ continue;
740
+ }
741
+ if (!Array.isArray(current)) return !1;
742
+ if (isKeySegment(segment)) {
743
+ const index = getIndexForKey(current, segment._key);
744
+ if (index === void 0) return !1;
745
+ current = current[index];
746
+ } else {
747
+ const index = segment < 0 ? current.length + segment : segment;
748
+ if (!(index in current)) return !1;
749
+ current = current[index];
750
+ }
751
+ }
752
+ return !0;
753
+ }
729
754
  function set(input, pathExpressionValues) {
730
755
  const result = Object.entries(pathExpressionValues).flatMap(
731
756
  ([pathExpression, replacementValue]) => Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
732
757
  ...matchEntry,
733
758
  replacementValue
734
759
  }))
735
- ).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
760
+ ).filter(({ path }) => resolvesArraySegments(input, path)).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
736
761
  return ensureArrayKeysDeep(result);
737
762
  }
738
763
  function setIfMissing(input, pathExpressionValues) {
739
764
  const result = Object.entries(pathExpressionValues).flatMap(([pathExpression, replacementValue]) => Array.from(jsonMatch(input, pathExpression)).map((matchEntry) => ({
740
765
  ...matchEntry,
741
766
  replacementValue
742
- }))).filter((matchEntry) => matchEntry.value === null || matchEntry.value === void 0).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
767
+ }))).filter((matchEntry) => matchEntry.value === null || matchEntry.value === void 0).filter(({ path }) => resolvesArraySegments(input, path)).reduce((acc, { path, replacementValue }) => setDeep(acc, path, replacementValue), input);
743
768
  return ensureArrayKeysDeep(result);
744
769
  }
745
770
  function unset(input, pathExpressions) {
@@ -1040,6 +1065,24 @@ class MaxBufferExceededError extends OutOfSyncError {
1040
1065
  super(message, state), this.name = "MaxBufferExceededError";
1041
1066
  }
1042
1067
  }
1068
+ function toOrderedChains(events) {
1069
+ return events.filter(
1070
+ (event) => !events.some((other) => other.resultRev === event.previousRev)
1071
+ ).map((start) => {
1072
+ const chain = [];
1073
+ let current = start;
1074
+ for (; current; ) {
1075
+ chain.push(current);
1076
+ const currentResultRev = current.resultRev;
1077
+ current = events.find((event) => event.previousRev === currentResultRev);
1078
+ }
1079
+ return chain;
1080
+ });
1081
+ }
1082
+ function discardChainTo(chain, revision) {
1083
+ const revisionIndex = chain.findIndex((event) => event.resultRev === revision);
1084
+ return revisionIndex === -1 ? chain : chain.slice(revisionIndex + 1);
1085
+ }
1043
1086
  function sortListenerEvents(options) {
1044
1087
  const { resolveChainDeadline = DEFAULT_DEADLINE_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE } = {};
1045
1088
  return (input$) => input$.pipe(
@@ -1058,25 +1101,25 @@ function sortListenerEvents(options) {
1058
1101
  throw new Error(
1059
1102
  "Invalid state. Cannot process mutation event without a base sync event"
1060
1103
  );
1061
- const buffer = state.buffer.concat(event), emitEvents = [];
1062
- let baseRevision = state.base.revision, progress = !0;
1063
- for (; progress; ) {
1064
- progress = !1;
1065
- const idx = buffer.findIndex((e) => e.previousRev === baseRevision);
1066
- if (idx !== -1) {
1067
- const [next] = buffer.splice(idx, 1);
1068
- emitEvents.push(next), baseRevision = next.transition === "disappear" ? void 0 : next.resultRev, progress = !0;
1069
- }
1104
+ 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();
1105
+ if (applicable) {
1106
+ const last = applicable[applicable.length - 1];
1107
+ return {
1108
+ // If the chain ends in a deletion, the new base revision is undefined.
1109
+ base: { revision: last.transition === "disappear" ? void 0 : last.resultRev },
1110
+ buffer,
1111
+ emitEvents: applicable
1112
+ };
1070
1113
  }
1071
1114
  if (buffer.length >= maxBufferSize)
1072
1115
  throw new MaxBufferExceededError(
1073
1116
  `Too many unchainable mutation events (${buffer.length}) waiting to resolve.`,
1074
- { base: { revision: baseRevision }, buffer, emitEvents }
1117
+ { base: { revision: baseRevision }, buffer, emitEvents: [] }
1075
1118
  );
1076
1119
  return {
1077
1120
  base: { revision: baseRevision },
1078
1121
  buffer,
1079
- emitEvents
1122
+ emitEvents: []
1080
1123
  };
1081
1124
  }
1082
1125
  return { ...state, emitEvents: [event] };
@@ -1107,7 +1150,7 @@ function sortListenerEvents(options) {
1107
1150
  const listen = ({ state }, documentId) => {
1108
1151
  const { sharedListener, fetchDocument } = state.get();
1109
1152
  return sharedListener.events.pipe(
1110
- concatMap((e) => e.type === "welcome" ? fetchDocument(documentId).pipe(
1153
+ concatMap((e) => e.type === "welcome" || e.type === "reset" ? fetchDocument(documentId).pipe(
1111
1154
  map$1((document) => ({ type: "sync", document }))
1112
1155
  ) : e.type === "mutation" && e.documentId === documentId ? of(e) : EMPTY),
1113
1156
  sortListenerEvents(),
@@ -1236,6 +1279,53 @@ function createMutationApplier(options) {
1236
1279
  }
1237
1280
  };
1238
1281
  }
1282
+ function preserveNumericOperations(baseBefore, userPatches, diffedPatches) {
1283
+ if (!baseBefore || !userPatches?.length) return diffedPatches;
1284
+ const targets = /* @__PURE__ */ new Map();
1285
+ for (const patch of userPatches)
1286
+ for (const [op, sign] of [
1287
+ ["inc", 1],
1288
+ ["dec", -1]
1289
+ ]) {
1290
+ const record = patch[op];
1291
+ if (record) {
1292
+ for (const [pathExpression, amount] of Object.entries(record))
1293
+ if (typeof amount == "number")
1294
+ for (const match of jsonMatch(baseBefore, pathExpression)) {
1295
+ const path = stringifyPath(match.path), existing = targets.get(path);
1296
+ existing ? (existing.expectedValue += sign * amount, existing.delta += sign * amount) : typeof match.value == "number" && targets.set(path, {
1297
+ baseValue: match.value,
1298
+ expectedValue: match.value + sign * amount,
1299
+ delta: sign * amount
1300
+ });
1301
+ }
1302
+ }
1303
+ }
1304
+ if (!targets.size) return diffedPatches;
1305
+ const preserved = /* @__PURE__ */ new Map(), next = diffedPatches.flatMap((patch) => {
1306
+ if (!patch.set) return [patch];
1307
+ const keptSet = {};
1308
+ for (const [path, value] of Object.entries(patch.set)) {
1309
+ const target = targets.get(path);
1310
+ target && typeof value == "number" && value === target.expectedValue ? preserved.set(path, { delta: target.delta, baseValue: target.baseValue }) : keptSet[path] = value;
1311
+ }
1312
+ if (Object.keys(keptSet).length === Object.keys(patch.set).length) return [patch];
1313
+ const { set: _set, ...rest } = patch, withKept = Object.keys(keptSet).length ? { ...rest, set: keptSet } : rest;
1314
+ return Object.keys(withKept).length ? [withKept] : [];
1315
+ });
1316
+ if (!preserved.size) return next;
1317
+ const setIfMissing2 = {}, inc2 = {}, dec2 = {};
1318
+ for (const [path, { delta, baseValue }] of preserved)
1319
+ setIfMissing2[path] = baseValue, delta >= 0 ? inc2[path] = delta : dec2[path] = -delta;
1320
+ return [
1321
+ ...next,
1322
+ {
1323
+ setIfMissing: setIfMissing2,
1324
+ ...Object.keys(inc2).length && { inc: inc2 },
1325
+ ...Object.keys(dec2).length && { dec: dec2 }
1326
+ }
1327
+ ];
1328
+ }
1239
1329
  function applySingleDocPatch({
1240
1330
  base: initialBase,
1241
1331
  working: initialWorking,
@@ -1262,7 +1352,11 @@ function applySingleDocPatch({
1262
1352
  preserveOperations
1263
1353
  }), baseBefore = base[documentId];
1264
1354
  base = applyMutations(base, userPatches, "base");
1265
- const baseAfter = base[documentId], diffedPatches = preserveOperations ? patches : diffValue(baseBefore, baseAfter), workingBefore = working[documentId];
1355
+ const baseAfter = base[documentId], diffedPatches = preserveOperations ? patches : preserveNumericOperations(
1356
+ baseBefore,
1357
+ patches,
1358
+ diffValue(baseBefore, baseAfter)
1359
+ ), workingBefore = working[documentId];
1266
1360
  if (!checkGrant(grants.update, workingBefore, identity))
1267
1361
  throw new PermissionActionError({ documentId, transactionId, message: permissionMessage });
1268
1362
  const workingMutations = diffedPatches.map((patch) => ({
@@ -1599,7 +1693,11 @@ function handleEdit(action, ctx) {
1599
1693
  !isReleasePerspective(action.perspective) && !base[draftId] && base[publishedId] && baseMutations.push({ create: { ...base[publishedId], _id: draftId } });
1600
1694
  const baseBefore = base[patchDocumentId] ?? base[publishedId];
1601
1695
  userPatches && baseMutations.push(...userPatches), base = applyMutations(base, baseMutations, "base");
1602
- const baseAfter = base[patchDocumentId], patches = action.preserveOperations ? action.patches ?? [] : diffValue(baseBefore, baseAfter), workingMutations = [];
1696
+ const baseAfter = base[patchDocumentId], patches = action.preserveOperations ? action.patches ?? [] : preserveNumericOperations(
1697
+ baseBefore,
1698
+ action.patches,
1699
+ diffValue(baseBefore, baseAfter)
1700
+ ), workingMutations = [];
1603
1701
  if (!isReleasePerspective(action.perspective) && !working[draftId] && working[publishedId]) {
1604
1702
  const newDraftFromPublished = { ...working[publishedId], _id: draftId };
1605
1703
  if (!checkGrant(grants.create, newDraftFromPublished, identity))
@@ -2212,7 +2310,8 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
2212
2310
  }
2213
2311
  if (type === "sync" && (unverifiedRevisions = Object.fromEntries(
2214
2312
  Object.entries(unverifiedRevisions).filter(([, unverifiedRevision]) => unverifiedRevision ? new Date(timestamp).getTime() <= new Date(unverifiedRevision.timestamp).getTime() : !1)
2215
- )), revisionToVerify && revisionToVerify.previousRev === previousRev)
2313
+ )), revisionToVerify && revisionToVerify.previousRev === previousRev) {
2314
+ 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;
2216
2315
  return {
2217
2316
  ...prev,
2218
2317
  documentStates: {
@@ -2221,10 +2320,12 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
2221
2320
  ...prevDocState,
2222
2321
  remote: document,
2223
2322
  remoteRev: revision,
2323
+ local: local2,
2224
2324
  unverifiedRevisions
2225
2325
  }
2226
2326
  }
2227
2327
  };
2328
+ }
2228
2329
  let working = { ...prev.applied.at(0)?.previous, [documentId]: document };
2229
2330
  const nextApplied = [];
2230
2331
  for (const curr of prev.applied)
@@ -2244,6 +2345,7 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
2244
2345
  }
2245
2346
  throw error;
2246
2347
  }
2348
+ const nextLocal = working[documentId], local = isDeepEqual(prevDocState.local, nextLocal) ? prevDocState.local : nextLocal;
2247
2349
  return {
2248
2350
  ...prev,
2249
2351
  applied: nextApplied,
@@ -2253,7 +2355,7 @@ function applyRemoteDocument(prev, { document, documentId, previousRev, revision
2253
2355
  ...prevDocState,
2254
2356
  remote: document,
2255
2357
  remoteRev: revision,
2256
- local: working[documentId],
2358
+ local,
2257
2359
  unverifiedRevisions
2258
2360
  }
2259
2361
  }
@@ -2345,6 +2447,85 @@ function getDocumentEvents(outgoing) {
2345
2447
  )
2346
2448
  );
2347
2449
  }
2450
+ const DEDUPE_TTL = 12e4, DEDUPE_MAX_ENTRIES = 1e3, GROUP_FLUSH_DEADLINE = 3e4, GROUP_MAX_HELD_EVENTS = 100;
2451
+ function dedupeListenerEvents(options) {
2452
+ const { ttl = DEDUPE_TTL, maxEntries = DEDUPE_MAX_ENTRIES } = {};
2453
+ return (source) => defer(() => {
2454
+ const seen = /* @__PURE__ */ new Map();
2455
+ return source.pipe(
2456
+ filter$1((event) => {
2457
+ if (event.type !== "mutation") return !0;
2458
+ const key = `${event.transactionId}#${event.documentId}`, now = Date.now(), expiry = seen.get(key);
2459
+ if (expiry !== void 0 && expiry > now) return !1;
2460
+ seen.delete(key), seen.set(key, now + ttl);
2461
+ for (const [existingKey, existingExpiry] of seen) {
2462
+ if (seen.size <= maxEntries && existingExpiry > now) break;
2463
+ seen.delete(existingKey);
2464
+ }
2465
+ return !0;
2466
+ })
2467
+ );
2468
+ });
2469
+ }
2470
+ function groupTransactionEvents(options) {
2471
+ const { flushDeadline = GROUP_FLUSH_DEADLINE, maxHeldEvents = GROUP_MAX_HELD_EVENTS } = {};
2472
+ return (source) => new Observable((subscriber) => {
2473
+ let open = null, held = [];
2474
+ const closeOpen = () => {
2475
+ if (!open) return;
2476
+ clearTimeout(open.timer);
2477
+ const { events } = open;
2478
+ open = null;
2479
+ for (const event of events) subscriber.next(event);
2480
+ }, releaseHeld = () => {
2481
+ const queue = held;
2482
+ held = [];
2483
+ for (const event of queue) processEvent(event);
2484
+ }, releaseEverything = () => {
2485
+ closeOpen();
2486
+ const queue = held;
2487
+ held = [];
2488
+ for (const event of queue) subscriber.next(event);
2489
+ }, processEvent = (event) => {
2490
+ if (event.type !== "mutation") {
2491
+ releaseEverything(), subscriber.next(event);
2492
+ return;
2493
+ }
2494
+ const isMultiDocument = (event.transactionTotalEvents ?? 0) > 1;
2495
+ if (!open) {
2496
+ if (!isMultiDocument) {
2497
+ subscriber.next(event);
2498
+ return;
2499
+ }
2500
+ open = {
2501
+ transactionId: event.transactionId,
2502
+ events: [event],
2503
+ total: event.transactionTotalEvents,
2504
+ timer: setCleanupTimeout(() => {
2505
+ closeOpen(), releaseHeld();
2506
+ }, flushDeadline)
2507
+ };
2508
+ return;
2509
+ }
2510
+ if (event.transactionId === open.transactionId) {
2511
+ open.events.push(event), open.events.length >= open.total && (closeOpen(), releaseHeld());
2512
+ return;
2513
+ }
2514
+ held.push(event), held.length > maxHeldEvents && releaseEverything();
2515
+ }, subscription = source.subscribe({
2516
+ next: processEvent,
2517
+ error: (error) => {
2518
+ releaseEverything(), subscriber.error(error);
2519
+ },
2520
+ complete: () => {
2521
+ releaseEverything(), subscriber.complete();
2522
+ }
2523
+ });
2524
+ return () => {
2525
+ open && clearTimeout(open.timer), open = null, held = [], subscription.unsubscribe();
2526
+ };
2527
+ });
2528
+ }
2348
2529
  const API_VERSION$3 = "v2025-05-06";
2349
2530
  function createSharedListener(instance, resource) {
2350
2531
  const dispose$ = new Subject(), events$ = getClientState(instance, {
@@ -2360,7 +2541,12 @@ function createSharedListener(instance, resource) {
2360
2541
  "*",
2361
2542
  {},
2362
2543
  {
2363
- events: ["mutation", "welcome", "reconnect"],
2544
+ // with `enableResume`, a reconnect replays the events missed while
2545
+ // offline (emitting `welcomeback`) instead of forcing a refetch of
2546
+ // every subscribed document. when the backend cannot resume, it
2547
+ // emits `welcome` or `reset`, both of which trigger a refetch
2548
+ events: ["mutation", "welcome", "welcomeback", "reconnect", "reset"],
2549
+ enableResume: !0,
2364
2550
  includeResult: !1,
2365
2551
  // the document store handles version documents, so we need to include all versions
2366
2552
  includeAllVersions: !0,
@@ -2373,6 +2559,11 @@ function createSharedListener(instance, resource) {
2373
2559
  )
2374
2560
  )
2375
2561
  ),
2562
+ // resumed events are delivered at-least-once, so drop replayed duplicates
2563
+ dedupeListenerEvents(),
2564
+ // hold multi-document transactions (e.g. publish) until complete so
2565
+ // per-document processing never observes half a transaction
2566
+ groupTransactionEvents(),
2376
2567
  takeUntil(dispose$),
2377
2568
  share()
2378
2569
  ), [welcome$, mutation$] = partition(events$, (e) => e.type === "welcome");
@@ -2652,8 +2843,7 @@ const _resolveDocument = bindActionByResource(
2652
2843
  switchMap(
2653
2844
  (client) => client.observable.request({
2654
2845
  uri,
2655
- tag: "acl.get",
2656
- withCredentials: !0
2846
+ tag: "acl.get"
2657
2847
  })
2658
2848
  ),
2659
2849
  tap$1((datasetAcl) => state.set("setGrants", { grants: createGrantsLookup(datasetAcl) }))
@@ -3057,7 +3247,7 @@ const organizations = createFetcherStore({
3057
3247
  signal,
3058
3248
  ...options
3059
3249
  }))?.data[0]
3060
- );
3250
+ ), CONNECT_RETRY_MAX_DELAY = 1e3 * 240;
3061
3251
  function getBifurClient(client, token$) {
3062
3252
  const bifurVersionedClient = client.withConfig({ apiVersion: "2022-06-30" }), {
3063
3253
  resource,
@@ -3103,8 +3293,37 @@ const handleIncomingMessage = (event) => {
3103
3293
  default:
3104
3294
  throw new Error(`Got unknown presence event: ${JSON.stringify(event)}`);
3105
3295
  }
3106
- }, createBifurTransport = (options) => {
3107
- const { client, token$, sessionId } = options, bifur = getBifurClient(client, token$), incomingEvents$ = bifur.listen("presence").pipe(map(handleIncomingMessage)), dispatchMessage = (message) => {
3296
+ }, createConnections = (bifur) => {
3297
+ let generation = 0;
3298
+ return defer(() => {
3299
+ const current = ++generation;
3300
+ return bifur.heartbeats.pipe(map(() => current));
3301
+ }).pipe(
3302
+ retry$1({
3303
+ delay: (_error, retryCount) => timer(Math.min(CONNECT_RETRY_MAX_DELAY, 2 ** retryCount * 100)),
3304
+ resetOnSuccess: !0
3305
+ }),
3306
+ // After `retry` so that one operator instance spans reconnects, making each
3307
+ // live socket yield exactly one emission.
3308
+ distinctUntilChanged$1(),
3309
+ share$1()
3310
+ );
3311
+ }, createIncomingEvents = (bifur, connections$) => connections$.pipe(
3312
+ switchMap$1(
3313
+ () => bifur.listen("presence").pipe(
3314
+ // Let a dead listener go quietly. The next connection replaces it, and
3315
+ // surfacing the error here would tear down the reconnect loop with it.
3316
+ catchError(() => EMPTY)
3317
+ )
3318
+ ),
3319
+ map(handleIncomingMessage),
3320
+ share$1()
3321
+ ), createUnload = () => typeof window > "u" ? EMPTY : merge(fromEvent(window, "beforeunload"), fromEvent(window, "pagehide")).pipe(
3322
+ map(() => {
3323
+ })
3324
+ ), createBifurTransport = (options) => {
3325
+ const { client, token$, sessionId } = options, bifur = getBifurClient(client, token$), connections$ = createConnections(bifur);
3326
+ return [createIncomingEvents(bifur, connections$), (message) => {
3108
3327
  switch (message.type) {
3109
3328
  case "rollCall":
3110
3329
  return bifur.request("presence_rollcall", { session: sessionId });
@@ -3117,12 +3336,34 @@ const handleIncomingMessage = (event) => {
3117
3336
  default:
3118
3337
  return EMPTY;
3119
3338
  }
3120
- };
3121
- return typeof window < "u" && fromEvent(window, "beforeunload").pipe(switchMap$1(() => dispatchMessage({ type: "disconnect" }))).subscribe(), [incomingEvents$.pipe(share$1()), dispatchMessage];
3122
- }, PRESENCE_API_VERSION = "2026-03-30", getInitialState = () => ({
3339
+ }, connections$, createUnload()];
3340
+ };
3341
+ function isKeyedSegment(segment) {
3342
+ return typeof segment == "object" && segment !== null && !Array.isArray(segment) && "_key" in segment;
3343
+ }
3344
+ function isEqualTuple(a, b) {
3345
+ return a.length === b.length && a.every((value, index) => value === b[index]);
3346
+ }
3347
+ function isEqualSegment(a, b) {
3348
+ return a === b ? !0 : isKeyedSegment(a) && isKeyedSegment(b) ? a._key === b._key : Array.isArray(a) && Array.isArray(b) ? isEqualTuple(a, b) : !1;
3349
+ }
3350
+ function startsWithPath(prefix, candidate) {
3351
+ return prefix.length > candidate.length ? !1 : prefix.every((segment, index) => isEqualSegment(segment, candidate[index]));
3352
+ }
3353
+ const logger = createLogger("presence"), PRESENCE_API_VERSION = "2026-03-30", REPORT_MIN_INTERVAL = 3e4, REPORT_AUDIT_TIME = 200, SESSION_TTL = 9e4, SWEEP_INTERVAL = 15e3, getInitialState = () => ({
3123
3354
  locations: /* @__PURE__ */ new Map(),
3124
3355
  users: {}
3125
- }), presenceStore = defineStore({
3356
+ }), sendSafely = (dispatch2, message) => dispatch2(message).pipe(
3357
+ catchError$1((error) => (logger.warn("Failed to send presence message", { messageType: message.type, error }), EMPTY))
3358
+ ), asTrigger = (source$, name) => source$.pipe(
3359
+ map$1(() => {
3360
+ }),
3361
+ catchError$1((error) => (logger.error("Presence announce trigger failed", { trigger: name, error }), EMPTY))
3362
+ ), locationKey = ({ documentId, path, selection }) => JSON.stringify([documentId, path, selection ?? null]);
3363
+ function isEqualLocations(a, b) {
3364
+ return a === b ? !0 : !a || !b || a.length !== b.length ? !1 : a.every((location, index) => locationKey(location) === locationKey(b[index]));
3365
+ }
3366
+ const presenceStore = defineStore({
3126
3367
  name: "presence",
3127
3368
  getInitialState,
3128
3369
  initialize: (context) => {
@@ -3141,97 +3382,223 @@ const handleIncomingMessage = (event) => {
3141
3382
  }) : getClient(instance, {
3142
3383
  apiVersion: PRESENCE_API_VERSION,
3143
3384
  resource
3144
- }), token$ = getTokenState(instance).observable.pipe(distinctUntilChanged()), [incomingEvents$, dispatch2] = createBifurTransport({
3385
+ }), token$ = getTokenState(instance).observable.pipe(distinctUntilChanged()), [incomingEvents$, dispatch2, connections$, unload$] = createBifurTransport({
3145
3386
  client,
3146
3387
  token$,
3147
3388
  sessionId
3148
3389
  }), subscription = new Subscription();
3149
- if (subscription.add(
3150
- incomingEvents$.subscribe((event) => {
3151
- "sessionId" in event && event.sessionId === sessionId || (event.type === "state" ? state.set("presence/state", (prevState) => {
3152
- const newLocations = new Map(prevState.locations);
3153
- return newLocations.set(event.sessionId, {
3154
- userId: event.userId,
3155
- locations: event.locations
3156
- }), {
3157
- ...prevState,
3158
- locations: newLocations
3159
- };
3160
- }) : event.type === "disconnect" && state.set("presence/disconnect", (prevState) => {
3390
+ subscription.add(
3391
+ incomingEvents$.subscribe({
3392
+ next: (event) => {
3393
+ "sessionId" in event && event.sessionId === sessionId || (event.type === "state" ? state.set("presence/state", (prevState) => {
3394
+ const newLocations = new Map(prevState.locations);
3395
+ return newLocations.set(event.sessionId, {
3396
+ userId: event.userId,
3397
+ locations: event.locations,
3398
+ lastSeenAt: Date.now()
3399
+ }), {
3400
+ ...prevState,
3401
+ locations: newLocations
3402
+ };
3403
+ }) : event.type === "disconnect" && state.set("presence/disconnect", (prevState) => {
3404
+ const newLocations = new Map(prevState.locations);
3405
+ return newLocations.delete(event.sessionId), { ...prevState, locations: newLocations };
3406
+ }));
3407
+ },
3408
+ // The transport re-establishes its listener on every live connection, so
3409
+ // this should not fire. Handled rather than left to become an unhandled
3410
+ // error, which is how presence used to die silently and for good.
3411
+ error: (error) => {
3412
+ logger.error("Presence event stream failed", { error });
3413
+ }
3414
+ })
3415
+ ), subscription.add(
3416
+ connections$.subscribe(() => {
3417
+ state.set("presence/reset", (prevState) => ({
3418
+ ...prevState,
3419
+ locations: /* @__PURE__ */ new Map()
3420
+ })), sendSafely(dispatch2, { type: "rollCall" }).subscribe();
3421
+ })
3422
+ ), subscription.add(
3423
+ connections$.pipe(switchMap(() => timer(SWEEP_INTERVAL, SWEEP_INTERVAL))).subscribe(() => {
3424
+ state.set("presence/expire", (prevState) => {
3425
+ const cutoff = Date.now() - SESSION_TTL, stale = [...prevState.locations].filter(([, s]) => s.lastSeenAt < cutoff);
3426
+ if (stale.length === 0) return prevState;
3161
3427
  const newLocations = new Map(prevState.locations);
3162
- return newLocations.delete(event.sessionId), { ...prevState, locations: newLocations };
3428
+ for (const [staleSessionId] of stale) newLocations.delete(staleSessionId);
3429
+ return { ...prevState, locations: newLocations };
3430
+ });
3431
+ })
3432
+ ), subscription.add(
3433
+ unload$.pipe(switchMap(() => sendSafely(dispatch2, { type: "disconnect" }))).subscribe()
3434
+ );
3435
+ const localLocations$ = state.observable.pipe(
3436
+ map$1((s) => s.localLocations),
3437
+ // Compared by value, not by reference. The Studio compares by reference and
3438
+ // so never dedupes, because its form rebuilds the array on every call.
3439
+ distinctUntilChanged(isEqualLocations)
3440
+ ), rollCallRequests$ = incomingEvents$.pipe(
3441
+ // Bifur echoes our own roll call back to us, since we are subscribed to the
3442
+ // same topic we published to. Answering it would be a pointless round trip.
3443
+ filter$1((event) => event.type === "rollCall" && event.sessionId !== sessionId)
3444
+ );
3445
+ subscription.add(
3446
+ merge(
3447
+ asTrigger(localLocations$, "locationChange"),
3448
+ asTrigger(rollCallRequests$, "rollCall"),
3449
+ asTrigger(connections$, "connection")
3450
+ ).pipe(
3451
+ switchMap(() => timer(0, REPORT_MIN_INTERVAL)),
3452
+ withLatestFrom(localLocations$),
3453
+ map$1(([, locations]) => locations),
3454
+ // Stay silent until the app has reported at least once.
3455
+ filter$1((locations) => locations !== void 0),
3456
+ auditTime(REPORT_AUDIT_TIME),
3457
+ switchMap((locations) => sendSafely(dispatch2, { type: "state", locations }))
3458
+ ).subscribe()
3459
+ );
3460
+ const userIds$ = state.observable.pipe(
3461
+ map$1(
3462
+ (s) => Array.from(s.locations.values()).map((l) => l.userId).filter((id) => !!id)
3463
+ ),
3464
+ distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i]))
3465
+ ), organizationId$ = isCanvasResource(resource) ? state.observable.pipe(
3466
+ filter$1((s) => s.organizationId !== void 0 || s.organizationIdError !== void 0),
3467
+ first$1(),
3468
+ map$1((s) => s.organizationId)
3469
+ ) : of(void 0);
3470
+ if (subscription.add(
3471
+ combineLatest([userIds$, organizationId$]).pipe(
3472
+ switchMap(([userIds, organizationId]) => {
3473
+ if (userIds.length === 0)
3474
+ return of([]);
3475
+ if (!isDatasetResource(resource) && !organizationId)
3476
+ return of([]);
3477
+ const userObservables = userIds.map(
3478
+ (userId) => getUserState(instance, {
3479
+ userId,
3480
+ ...isDatasetResource(resource) ? { resourceType: "project", projectId: resource.projectId } : { resourceType: "organization", organizationId }
3481
+ }).pipe(filter$1((v) => !!v))
3482
+ );
3483
+ return combineLatest(userObservables);
3484
+ })
3485
+ ).subscribe((users) => {
3486
+ state.set("presence/users", (prevState) => ({
3487
+ ...prevState,
3488
+ users: {
3489
+ ...prevState.users,
3490
+ ...users.reduce((acc, user) => (user && (acc[user.profile.id] = user), acc), {})
3491
+ }
3163
3492
  }));
3164
3493
  })
3165
- ), dispatch2({ type: "rollCall" }).subscribe(), isCanvasResource(resource)) {
3494
+ ), isCanvasResource(resource)) {
3166
3495
  const globalClient = getClient(instance, { apiVersion: PRESENCE_API_VERSION });
3167
3496
  subscription.add(
3168
3497
  globalClient.observable.request({
3169
3498
  uri: `/canvases/${resource.canvasId}`,
3170
3499
  tag: "canvases.get"
3171
- }).pipe(catchError$1(() => EMPTY)).subscribe(({ organizationId }) => {
3172
- state.set("presence/organizationId", (prev) => ({ ...prev, organizationId }));
3500
+ }).subscribe({
3501
+ next: ({ organizationId }) => {
3502
+ state.set("presence/organizationId", (prev) => ({ ...prev, organizationId }));
3503
+ },
3504
+ error: (organizationIdError) => {
3505
+ state.set("presence/organizationIdError", (prev) => ({ ...prev, organizationIdError }));
3506
+ }
3173
3507
  })
3174
3508
  );
3175
3509
  }
3176
3510
  return () => {
3177
- dispatch2({ type: "disconnect" }).subscribe(), subscription.unsubscribe();
3511
+ sendSafely(dispatch2, { type: "disconnect" }).subscribe(), subscription.unsubscribe();
3178
3512
  };
3179
3513
  }
3180
- }), selectLocations = (state) => state.locations, selectUsers = (state) => state.users, selectPresence = createSelector(
3514
+ }), selectLocations = (state) => state.locations, selectUsers = (state) => state.users, createUnresolvedUser = (userId) => ({
3515
+ sanityUserId: userId,
3516
+ profile: {
3517
+ id: userId,
3518
+ displayName: "Unknown user",
3519
+ email: "",
3520
+ provider: "",
3521
+ createdAt: ""
3522
+ },
3523
+ memberships: []
3524
+ }), selectPresence = createSelector(
3181
3525
  selectLocations,
3182
3526
  selectUsers,
3183
3527
  (locations, users) => Array.from(locations.entries()).map(([sessionId, { userId, locations: locs }]) => ({
3184
- user: users[userId] || {
3185
- id: userId,
3186
- displayName: "Unknown user",
3187
- name: "Unknown user",
3188
- email: ""
3189
- },
3528
+ user: users[userId] || createUnresolvedUser(userId),
3190
3529
  sessionId,
3191
3530
  locations: locs
3192
3531
  }))
3193
- ), _getPresence = bindActionByResource(
3532
+ ), documentPresenceCache = /* @__PURE__ */ new WeakMap();
3533
+ function selectDocumentPresence(state, options) {
3534
+ const cacheKey = JSON.stringify([
3535
+ getEditingDocumentId(options),
3536
+ options.path ?? null,
3537
+ options.excludeVersions ?? !1
3538
+ ]);
3539
+ let perState = documentPresenceCache.get(state);
3540
+ perState || (perState = /* @__PURE__ */ new Map(), documentPresenceCache.set(state, perState));
3541
+ const cached = perState.get(cacheKey);
3542
+ if (cached) return cached;
3543
+ const computed = computeDocumentPresence(state, options);
3544
+ return perState.set(cacheKey, computed), computed;
3545
+ }
3546
+ const scopeId = (documentId, excludeVersions) => excludeVersions ? documentId : getPublishedId(DocumentId(documentId)), pathOf = (location) => location.path;
3547
+ function matchesQuery(location, target, { path, excludeVersions }) {
3548
+ return scopeId(location.documentId, excludeVersions) !== target ? !1 : !path || startsWithPath(path, pathOf(location));
3549
+ }
3550
+ function toDocumentPresence(users, sessionId, userId, location) {
3551
+ return {
3552
+ user: users[userId] || createUnresolvedUser(userId),
3553
+ sessionId,
3554
+ documentId: location.documentId,
3555
+ path: pathOf(location),
3556
+ lastActiveAt: location.lastActiveAt,
3557
+ ...location.selection === void 0 ? {} : { selection: location.selection }
3558
+ };
3559
+ }
3560
+ function computeDocumentPresence(state, options) {
3561
+ const target = scopeId(getEditingDocumentId(options), options.excludeVersions);
3562
+ return Array.from(state.locations).flatMap(
3563
+ ([sessionId, session]) => session.locations.filter((location) => matchesQuery(location, target, options)).map((location) => toDocumentPresence(state.users, sessionId, session.userId, location))
3564
+ );
3565
+ }
3566
+ const _getPresence = bindActionByResource(
3194
3567
  presenceStore,
3195
3568
  createStateSourceAction({
3196
- selector: (context) => selectPresence(context.state),
3197
- onSubscribe: (context) => {
3198
- const resource = context.key.resource, userIds$ = context.state.observable.pipe(
3199
- map$1(
3200
- (state) => Array.from(state.locations.values()).map((l) => l.userId).filter((id) => !!id)
3201
- ),
3202
- distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i]))
3203
- ), organizationId$ = isCanvasResource(resource) ? context.state.observable.pipe(
3204
- map$1((s) => s.organizationId),
3205
- filter$1((id) => id !== void 0),
3206
- first$1()
3207
- ) : of(void 0), subscription = combineLatest([userIds$, organizationId$]).pipe(
3208
- switchMap(([userIds, organizationId]) => {
3209
- if (userIds.length === 0)
3210
- return of([]);
3211
- const userObservables = userIds.map(
3212
- (userId) => getUserState(context.instance, {
3213
- userId,
3214
- ...isDatasetResource(resource) ? { resourceType: "project", projectId: resource.projectId } : { resourceType: "organization", organizationId }
3215
- }).pipe(filter$1((v) => !!v))
3216
- );
3217
- return combineLatest(userObservables);
3218
- })
3219
- ).subscribe((users) => {
3220
- context.state.set("presence/users", (prevState) => ({
3221
- ...prevState,
3222
- users: {
3223
- ...prevState.users,
3224
- ...users.reduce((acc, user) => (user && (acc[user.profile.id] = user), acc), {})
3225
- }
3226
- }));
3227
- });
3228
- return () => subscription.unsubscribe();
3229
- }
3569
+ selector: (context) => selectPresence(context.state)
3230
3570
  })
3231
3571
  );
3232
3572
  function getPresence(instance, params) {
3233
3573
  return _getPresence(instance, params ?? {});
3234
3574
  }
3575
+ const _getDocumentPresence = bindActionByResource(
3576
+ presenceStore,
3577
+ createStateSourceAction({
3578
+ selector: ({ state }, options) => selectDocumentPresence(state, options)
3579
+ })
3580
+ );
3581
+ function getDocumentPresence(instance, params) {
3582
+ return _getDocumentPresence(instance, params);
3583
+ }
3584
+ const _reportPresence = bindActionByResource(
3585
+ presenceStore,
3586
+ ({ state }, { locations }) => {
3587
+ const lastActiveAt = (/* @__PURE__ */ new Date()).toISOString(), wireLocations = locations.map((location) => ({
3588
+ type: "document",
3589
+ // The specific document being edited, not the canonical id. Other clients,
3590
+ // the Studio included, compare exact ids for field-level presence.
3591
+ documentId: getEditingDocumentId(location),
3592
+ path: location.path ?? [],
3593
+ lastActiveAt,
3594
+ ...location.selection === void 0 ? {} : { selection: location.selection }
3595
+ }));
3596
+ state.set("presence/report", (prevState) => ({ ...prevState, localLocations: wireLocations }));
3597
+ }
3598
+ );
3599
+ function reportPresence(instance, params) {
3600
+ return _reportPresence(instance, params);
3601
+ }
3235
3602
  function hashString(str) {
3236
3603
  let hash = 0;
3237
3604
  for (let i = 0; i < str.length; i++)
@@ -3690,6 +4057,7 @@ export {
3690
4057
  getCurrentUserState,
3691
4058
  getDashboardOrganizationId$1 as getDashboardOrganizationId,
3692
4059
  getDatasetsState,
4060
+ getDocumentPresence,
3693
4061
  getDocumentState,
3694
4062
  getDocumentSyncStatus,
3695
4063
  getFavoritesState,
@@ -3738,6 +4106,7 @@ export {
3738
4106
  publishRelease,
3739
4107
  releaseChannel,
3740
4108
  releaseNode,
4109
+ reportPresence,
3741
4110
  resolveDatasets,
3742
4111
  resolveDocument,
3743
4112
  resolveFavoritesState,