@sanity/sdk 2.18.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.
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, 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";
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
  }
@@ -3242,7 +3247,7 @@ const organizations = createFetcherStore({
3242
3247
  signal,
3243
3248
  ...options
3244
3249
  }))?.data[0]
3245
- );
3250
+ ), CONNECT_RETRY_MAX_DELAY = 1e3 * 240;
3246
3251
  function getBifurClient(client, token$) {
3247
3252
  const bifurVersionedClient = client.withConfig({ apiVersion: "2022-06-30" }), {
3248
3253
  resource,
@@ -3288,8 +3293,37 @@ const handleIncomingMessage = (event) => {
3288
3293
  default:
3289
3294
  throw new Error(`Got unknown presence event: ${JSON.stringify(event)}`);
3290
3295
  }
3291
- }, createBifurTransport = (options) => {
3292
- 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) => {
3293
3327
  switch (message.type) {
3294
3328
  case "rollCall":
3295
3329
  return bifur.request("presence_rollcall", { session: sessionId });
@@ -3302,12 +3336,34 @@ const handleIncomingMessage = (event) => {
3302
3336
  default:
3303
3337
  return EMPTY;
3304
3338
  }
3305
- };
3306
- return typeof window < "u" && fromEvent(window, "beforeunload").pipe(switchMap$1(() => dispatchMessage({ type: "disconnect" }))).subscribe(), [incomingEvents$.pipe(share$1()), dispatchMessage];
3307
- }, 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 = () => ({
3308
3354
  locations: /* @__PURE__ */ new Map(),
3309
3355
  users: {}
3310
- }), 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({
3311
3367
  name: "presence",
3312
3368
  getInitialState,
3313
3369
  initialize: (context) => {
@@ -3326,97 +3382,223 @@ const handleIncomingMessage = (event) => {
3326
3382
  }) : getClient(instance, {
3327
3383
  apiVersion: PRESENCE_API_VERSION,
3328
3384
  resource
3329
- }), token$ = getTokenState(instance).observable.pipe(distinctUntilChanged()), [incomingEvents$, dispatch2] = createBifurTransport({
3385
+ }), token$ = getTokenState(instance).observable.pipe(distinctUntilChanged()), [incomingEvents$, dispatch2, connections$, unload$] = createBifurTransport({
3330
3386
  client,
3331
3387
  token$,
3332
3388
  sessionId
3333
3389
  }), subscription = new Subscription();
3334
- if (subscription.add(
3335
- incomingEvents$.subscribe((event) => {
3336
- "sessionId" in event && event.sessionId === sessionId || (event.type === "state" ? state.set("presence/state", (prevState) => {
3337
- const newLocations = new Map(prevState.locations);
3338
- return newLocations.set(event.sessionId, {
3339
- userId: event.userId,
3340
- locations: event.locations
3341
- }), {
3342
- ...prevState,
3343
- locations: newLocations
3344
- };
3345
- }) : 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;
3346
3427
  const newLocations = new Map(prevState.locations);
3347
- 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
+ }
3348
3492
  }));
3349
3493
  })
3350
- ), dispatch2({ type: "rollCall" }).subscribe(), isCanvasResource(resource)) {
3494
+ ), isCanvasResource(resource)) {
3351
3495
  const globalClient = getClient(instance, { apiVersion: PRESENCE_API_VERSION });
3352
3496
  subscription.add(
3353
3497
  globalClient.observable.request({
3354
3498
  uri: `/canvases/${resource.canvasId}`,
3355
3499
  tag: "canvases.get"
3356
- }).pipe(catchError$1(() => EMPTY)).subscribe(({ organizationId }) => {
3357
- 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
+ }
3358
3507
  })
3359
3508
  );
3360
3509
  }
3361
3510
  return () => {
3362
- dispatch2({ type: "disconnect" }).subscribe(), subscription.unsubscribe();
3511
+ sendSafely(dispatch2, { type: "disconnect" }).subscribe(), subscription.unsubscribe();
3363
3512
  };
3364
3513
  }
3365
- }), 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(
3366
3525
  selectLocations,
3367
3526
  selectUsers,
3368
3527
  (locations, users) => Array.from(locations.entries()).map(([sessionId, { userId, locations: locs }]) => ({
3369
- user: users[userId] || {
3370
- id: userId,
3371
- displayName: "Unknown user",
3372
- name: "Unknown user",
3373
- email: ""
3374
- },
3528
+ user: users[userId] || createUnresolvedUser(userId),
3375
3529
  sessionId,
3376
3530
  locations: locs
3377
3531
  }))
3378
- ), _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(
3379
3567
  presenceStore,
3380
3568
  createStateSourceAction({
3381
- selector: (context) => selectPresence(context.state),
3382
- onSubscribe: (context) => {
3383
- const resource = context.key.resource, userIds$ = context.state.observable.pipe(
3384
- map$1(
3385
- (state) => Array.from(state.locations.values()).map((l) => l.userId).filter((id) => !!id)
3386
- ),
3387
- distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i]))
3388
- ), organizationId$ = isCanvasResource(resource) ? context.state.observable.pipe(
3389
- map$1((s) => s.organizationId),
3390
- filter$1((id) => id !== void 0),
3391
- first$1()
3392
- ) : of(void 0), subscription = combineLatest([userIds$, organizationId$]).pipe(
3393
- switchMap(([userIds, organizationId]) => {
3394
- if (userIds.length === 0)
3395
- return of([]);
3396
- const userObservables = userIds.map(
3397
- (userId) => getUserState(context.instance, {
3398
- userId,
3399
- ...isDatasetResource(resource) ? { resourceType: "project", projectId: resource.projectId } : { resourceType: "organization", organizationId }
3400
- }).pipe(filter$1((v) => !!v))
3401
- );
3402
- return combineLatest(userObservables);
3403
- })
3404
- ).subscribe((users) => {
3405
- context.state.set("presence/users", (prevState) => ({
3406
- ...prevState,
3407
- users: {
3408
- ...prevState.users,
3409
- ...users.reduce((acc, user) => (user && (acc[user.profile.id] = user), acc), {})
3410
- }
3411
- }));
3412
- });
3413
- return () => subscription.unsubscribe();
3414
- }
3569
+ selector: (context) => selectPresence(context.state)
3415
3570
  })
3416
3571
  );
3417
3572
  function getPresence(instance, params) {
3418
3573
  return _getPresence(instance, params ?? {});
3419
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
+ }
3420
3602
  function hashString(str) {
3421
3603
  let hash = 0;
3422
3604
  for (let i = 0; i < str.length; i++)
@@ -3875,6 +4057,7 @@ export {
3875
4057
  getCurrentUserState,
3876
4058
  getDashboardOrganizationId$1 as getDashboardOrganizationId,
3877
4059
  getDatasetsState,
4060
+ getDocumentPresence,
3878
4061
  getDocumentState,
3879
4062
  getDocumentSyncStatus,
3880
4063
  getFavoritesState,
@@ -3923,6 +4106,7 @@ export {
3923
4106
  publishRelease,
3924
4107
  releaseChannel,
3925
4108
  releaseNode,
4109
+ reportPresence,
3926
4110
  resolveDatasets,
3927
4111
  resolveDocument,
3928
4112
  resolveFavoritesState,