@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/_chunks-dts/createGroqSearchFilter.d.ts +8 -11
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -1
- package/dist/_chunks-es/_internal.js +1 -0
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +8 -4
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +2 -2
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/index.d.ts +197 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +286 -102
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/_exports/index.ts +7 -1
- package/src/document/documentStore.concurrency.test.ts +313 -22
- package/src/document/util.test.ts +46 -0
- package/src/document/util.ts +37 -1
- package/src/presence/bifurTransport.test.ts +155 -3
- package/src/presence/bifurTransport.ts +79 -12
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +822 -2
- package/src/presence/presenceStore.ts +518 -102
- package/src/presence/types.ts +175 -3
- package/dist/_chunks-es/version.js +0 -7
- package/dist/_chunks-es/version.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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 {
|
|
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
|
-
},
|
|
26
|
-
|
|
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
|
-
}),
|
|
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
|
-
|
|
49
|
+
logger2.trace("Dispose called on already disposed instance", { internal: !0 });
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
50
|
-
|
|
52
|
+
logger2.trace("Disposing instance", {
|
|
51
53
|
internal: !0,
|
|
52
54
|
listenerCount: disposeListeners.size
|
|
53
|
-
}), disposed.current = !0, disposeListeners.forEach((listener) => listener()), disposeListeners.clear(),
|
|
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
|
-
|
|
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
|
|
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
|
|
334
|
+
const logger2 = getAuthLogger(instance), { providedToken, callbackUrl, clientFactory, apiHost, storageArea, storageKey } = state.get().options;
|
|
333
335
|
if (providedToken)
|
|
334
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
359
|
+
logger2.warn("Failed to parse dashboard context from callback URL", { error: err });
|
|
358
360
|
}
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
386
|
+
const logger2 = getAuthLogger(instance), { clientFactory, apiHost, providedToken, storageArea, storageKey } = state.get().options;
|
|
385
387
|
if (providedToken) {
|
|
386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
+
logger2.debug("Calling logout endpoint"), await client.request({ uri: "/auth/logout", method: "POST", tag: "logout" });
|
|
409
411
|
} else
|
|
410
|
-
|
|
412
|
+
logger2.debug("No token to logout - already logged out");
|
|
411
413
|
} catch (error) {
|
|
412
|
-
throw
|
|
414
|
+
throw logger2.error("Logout request failed", { error }), error;
|
|
413
415
|
} finally {
|
|
414
|
-
|
|
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
|
-
},
|
|
3292
|
-
|
|
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
|
-
|
|
3307
|
-
|
|
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
|
-
}),
|
|
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
|
-
|
|
3335
|
-
incomingEvents$.subscribe(
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
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
|
-
|
|
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
|
-
),
|
|
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
|
-
}).
|
|
3357
|
-
|
|
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
|
|
3511
|
+
sendSafely(dispatch2, { type: "disconnect" }).subscribe(), subscription.unsubscribe();
|
|
3363
3512
|
};
|
|
3364
3513
|
}
|
|
3365
|
-
}), selectLocations = (state) => state.locations, selectUsers = (state) => state.users,
|
|
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
|
-
),
|
|
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,
|