@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.
- 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 +492 -123
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/_exports/index.ts +7 -1
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +11 -0
- package/src/document/documentStore.ts +0 -1
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +65 -20
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/processActions/edit.ts +9 -2
- package/src/document/processActions/shared.ts +111 -1
- package/src/document/processActions.test.ts +185 -1
- package/src/document/reducers.test.ts +123 -0
- package/src/document/reducers.ts +37 -2
- package/src/document/sharedListener.test.ts +2 -1
- package/src/document/sharedListener.ts +12 -1
- 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
|
}
|
|
@@ -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
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
emitEvents
|
|
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 :
|
|
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 ?? [] :
|
|
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
|
|
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
|
-
|
|
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
|
-
},
|
|
3107
|
-
|
|
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
|
-
|
|
3122
|
-
|
|
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
|
-
}),
|
|
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
|
-
|
|
3150
|
-
incomingEvents$.subscribe(
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
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
|
-
|
|
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
|
-
),
|
|
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
|
-
}).
|
|
3172
|
-
|
|
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
|
|
3511
|
+
sendSafely(dispatch2, { type: "disconnect" }).subscribe(), subscription.unsubscribe();
|
|
3178
3512
|
};
|
|
3179
3513
|
}
|
|
3180
|
-
}), 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(
|
|
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
|
-
),
|
|
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,
|