@sanity/sdk 0.0.0-alpha.25 → 0.0.0-alpha.26
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.d.ts +5 -4
- package/dist/index.js +272 -111
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/_exports/index.ts +1 -1
- package/src/auth/authStore.test.ts +153 -26
- package/src/auth/authStore.ts +47 -5
- package/src/auth/handleAuthCallback.test.ts +2 -2
- package/src/auth/handleAuthCallback.ts +15 -11
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -0
- package/src/auth/refreshStampedToken.test.ts +220 -97
- package/src/auth/refreshStampedToken.ts +188 -34
- package/src/auth/utils.ts +34 -2
- package/src/projection/getProjectionState.test.ts +155 -59
- package/src/projection/getProjectionState.ts +58 -20
- package/src/projection/projectionQuery.test.ts +114 -12
- package/src/projection/projectionQuery.ts +75 -32
- package/src/projection/projectionStore.ts +1 -26
- package/src/projection/resolveProjection.test.ts +1 -1
- package/src/projection/resolveProjection.ts +2 -1
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +103 -31
- package/src/projection/subscribeToStateAndFetchBatches.ts +74 -32
- package/src/projection/types.ts +50 -0
- package/src/projection/util.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -20,8 +20,8 @@ import {PatchMutation} from '@sanity/mutate/_unstable_store'
|
|
|
20
20
|
import {PatchOperations} from '@sanity/types'
|
|
21
21
|
import {PathSegment} from '@sanity/types'
|
|
22
22
|
import {PreviewStoreState as PreviewStoreState_2} from './previewStore'
|
|
23
|
-
import {ProjectionStoreState} from './
|
|
24
|
-
import {ProjectionValuePending as ProjectionValuePending_2} from './
|
|
23
|
+
import {ProjectionStoreState} from './types'
|
|
24
|
+
import {ProjectionValuePending as ProjectionValuePending_2} from './types'
|
|
25
25
|
import {ResponseQueryOptions} from '@sanity/client'
|
|
26
26
|
import {Role} from '@sanity/types'
|
|
27
27
|
import {SanityClient} from '@sanity/client'
|
|
@@ -1649,7 +1649,8 @@ declare interface ProjectionNode extends BaseNode {
|
|
|
1649
1649
|
}
|
|
1650
1650
|
|
|
1651
1651
|
/**
|
|
1652
|
-
* @
|
|
1652
|
+
* @public
|
|
1653
|
+
* The result of a projection query
|
|
1653
1654
|
*/
|
|
1654
1655
|
export declare interface ProjectionValuePending<TValue extends object> {
|
|
1655
1656
|
data: TValue | null
|
|
@@ -2201,7 +2202,7 @@ declare interface UsersStoreState {
|
|
|
2201
2202
|
}
|
|
2202
2203
|
|
|
2203
2204
|
/**
|
|
2204
|
-
* @
|
|
2205
|
+
* @public
|
|
2205
2206
|
*/
|
|
2206
2207
|
export declare type ValidProjection = `{${string}}`
|
|
2207
2208
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createClient } from "@sanity/client";
|
|
2
|
-
import { Observable, share, map, distinctUntilChanged, skip, filter,
|
|
2
|
+
import { Observable, share, map, distinctUntilChanged, skip, filter, exhaustMap, timer, from, takeWhile, switchMap, firstValueFrom, fromEvent, EMPTY, defer, asapScheduler, concatMap, of, withLatestFrom, concat, throwError, first as first$1, Subject, takeUntil, 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, throttle, race, NEVER, combineLatest, debounceTime, Subscription } from "rxjs";
|
|
3
3
|
import { devtools } from "zustand/middleware";
|
|
4
4
|
import { createStore } from "zustand/vanilla";
|
|
5
5
|
import { pick, isEqual, omit, isObject } from "lodash-es";
|
|
@@ -161,42 +161,112 @@ function createStateSourceAction(options) {
|
|
|
161
161
|
}
|
|
162
162
|
return stateSourceAction;
|
|
163
163
|
}
|
|
164
|
-
const DEFAULT_BASE = "http://localhost", AUTH_CODE_PARAM = "sid", DEFAULT_API_VERSION$1 = "2021-06-07", REQUEST_TAG_PREFIX = "sanity.sdk.auth",
|
|
165
|
-
|
|
164
|
+
const DEFAULT_BASE = "http://localhost", AUTH_CODE_PARAM = "sid", DEFAULT_API_VERSION$1 = "2021-06-07", REQUEST_TAG_PREFIX = "sanity.sdk.auth", REFRESH_INTERVAL = 12 * 60 * 60 * 1e3, LOCK_NAME = "sanity-token-refresh-lock";
|
|
165
|
+
function getLastRefreshTime(storageArea, storageKey) {
|
|
166
|
+
try {
|
|
167
|
+
const data = storageArea?.getItem(`${storageKey}_last_refresh`);
|
|
168
|
+
return data ? parseInt(data, 10) : 0;
|
|
169
|
+
} catch {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function setLastRefreshTime(storageArea, storageKey) {
|
|
174
|
+
try {
|
|
175
|
+
storageArea?.setItem(`${storageKey}_last_refresh`, Date.now().toString());
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function getNextRefreshDelay(storageArea, storageKey) {
|
|
180
|
+
const lastRefresh = getLastRefreshTime(storageArea, storageKey);
|
|
181
|
+
if (!lastRefresh) return 0;
|
|
182
|
+
const now = Date.now(), nextRefreshTime = lastRefresh + REFRESH_INTERVAL;
|
|
183
|
+
return Math.max(0, nextRefreshTime - now);
|
|
184
|
+
}
|
|
185
|
+
function createTokenRefreshStream(token, clientFactory, apiHost) {
|
|
186
|
+
return new Observable((subscriber) => {
|
|
187
|
+
const subscription = clientFactory({
|
|
188
|
+
apiVersion: DEFAULT_API_VERSION$1,
|
|
189
|
+
requestTagPrefix: "sdk.token-refresh",
|
|
190
|
+
useProjectHostname: !1,
|
|
191
|
+
token,
|
|
192
|
+
ignoreBrowserTokenWarning: !0,
|
|
193
|
+
...apiHost && { apiHost }
|
|
194
|
+
}).observable.request({
|
|
195
|
+
uri: "auth/refresh-token",
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: {
|
|
198
|
+
token
|
|
199
|
+
}
|
|
200
|
+
}).subscribe(subscriber);
|
|
201
|
+
return () => subscription.unsubscribe();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async function acquireTokenRefreshLock(refreshFn, storageArea, storageKey) {
|
|
205
|
+
if (!navigator.locks)
|
|
206
|
+
return console.warn("Web Locks API not supported. Proceeding with uncoordinated refresh."), await refreshFn(), setLastRefreshTime(storageArea, storageKey), !0;
|
|
207
|
+
try {
|
|
208
|
+
return await navigator.locks.request(LOCK_NAME, { mode: "exclusive" }, async (lock) => {
|
|
209
|
+
if (!lock) return !1;
|
|
210
|
+
for (; ; ) {
|
|
211
|
+
const delay2 = getNextRefreshDelay(storageArea, storageKey);
|
|
212
|
+
delay2 > 0 && await new Promise((resolve) => setTimeout(resolve, delay2));
|
|
213
|
+
try {
|
|
214
|
+
await refreshFn(), setLastRefreshTime(storageArea, storageKey);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error("Token refresh failed within lock:", error);
|
|
217
|
+
}
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, REFRESH_INTERVAL));
|
|
219
|
+
}
|
|
220
|
+
}) === !0;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return console.error("Failed to request token refresh lock:", error), !1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const refreshStampedToken = ({ state }) => {
|
|
226
|
+
const { clientFactory, apiHost, storageArea, storageKey } = state.get().options;
|
|
166
227
|
return state.observable.pipe(
|
|
167
|
-
map((
|
|
228
|
+
map((storeState) => ({
|
|
229
|
+
authState: storeState.authState,
|
|
230
|
+
dashboardContext: storeState.dashboardContext
|
|
231
|
+
})),
|
|
168
232
|
filter(
|
|
169
|
-
(
|
|
233
|
+
(storeState) => storeState.authState.type === AuthStateType.LOGGED_IN
|
|
170
234
|
),
|
|
171
|
-
distinctUntilChanged(
|
|
172
|
-
|
|
235
|
+
distinctUntilChanged(
|
|
236
|
+
(prev, curr) => prev.authState.type === curr.authState.type && prev.authState.token === curr.authState.token && // Only care about token for distinctness here
|
|
237
|
+
prev.dashboardContext === curr.dashboardContext
|
|
238
|
+
),
|
|
239
|
+
// Make distinctness check explicit
|
|
240
|
+
filter((storeState) => storeState.authState.token.includes("-st")),
|
|
173
241
|
// Ensure we only try to refresh stamped tokens
|
|
174
|
-
|
|
175
|
-
(
|
|
242
|
+
exhaustMap((storeState) => {
|
|
243
|
+
const performRefresh = async () => {
|
|
244
|
+
const currentState = state.get();
|
|
245
|
+
if (currentState.authState.type !== AuthStateType.LOGGED_IN)
|
|
246
|
+
throw new Error("User logged out before refresh could complete");
|
|
247
|
+
const currentToken = currentState.authState.token, response = await firstValueFrom(
|
|
248
|
+
createTokenRefreshStream(currentToken, clientFactory, apiHost)
|
|
249
|
+
);
|
|
250
|
+
state.set("setRefreshStampedToken", (prev) => ({
|
|
251
|
+
authState: prev.authState.type === AuthStateType.LOGGED_IN ? { ...prev.authState, token: response.token } : prev.authState
|
|
252
|
+
})), storageArea?.setItem(storageKey, JSON.stringify({ token: response.token }));
|
|
253
|
+
};
|
|
254
|
+
return storeState.dashboardContext ? timer(REFRESH_INTERVAL, REFRESH_INTERVAL).pipe(
|
|
255
|
+
// Check if still logged in before each refresh attempt in the timer
|
|
176
256
|
takeWhile(() => state.get().authState.type === AuthStateType.LOGGED_IN),
|
|
177
|
-
|
|
178
|
-
distinctUntilChanged(),
|
|
179
|
-
map(
|
|
180
|
-
(token) => clientFactory({
|
|
181
|
-
apiVersion: DEFAULT_API_VERSION$1,
|
|
182
|
-
requestTagPrefix: "sdk.token-refresh",
|
|
183
|
-
useProjectHostname: !1,
|
|
184
|
-
token,
|
|
185
|
-
ignoreBrowserTokenWarning: !0,
|
|
186
|
-
...apiHost && { apiHost }
|
|
187
|
-
})
|
|
188
|
-
),
|
|
257
|
+
// Use switchMap here: if the timer ticks again, we *do* want the latest token request
|
|
189
258
|
switchMap(
|
|
190
|
-
(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
259
|
+
() => createTokenRefreshStream(storeState.authState.token, clientFactory, apiHost)
|
|
260
|
+
),
|
|
261
|
+
// Map the successful response for the outer subscribe block
|
|
262
|
+
map((response) => ({ token: response.token }))
|
|
263
|
+
) : from(acquireTokenRefreshLock(performRefresh, storageArea, storageKey)).pipe(
|
|
264
|
+
filter((hasLock) => hasLock),
|
|
265
|
+
// If acquireTokenRefreshLock *does* somehow resolve true (e.g., locks unsupported),
|
|
266
|
+
// emit the token that triggered this exhaustMap execution.
|
|
267
|
+
map(() => ({ token: storeState.authState.token }))
|
|
268
|
+
);
|
|
269
|
+
})
|
|
200
270
|
).subscribe({
|
|
201
271
|
next: (response) => {
|
|
202
272
|
state.set("setRefreshStampedToken", (prev) => ({
|
|
@@ -244,7 +314,17 @@ const DEFAULT_BASE = "http://localhost", AUTH_CODE_PARAM = "sid", DEFAULT_API_VE
|
|
|
244
314
|
});
|
|
245
315
|
};
|
|
246
316
|
function getAuthCode(callbackUrl, locationHref) {
|
|
247
|
-
const loc = new URL(locationHref, DEFAULT_BASE), callbackLocation = callbackUrl ? new URL(callbackUrl, DEFAULT_BASE) : void 0, callbackLocationMatches = callbackLocation ? loc.pathname.toLowerCase().startsWith(callbackLocation.pathname.toLowerCase()) : !0
|
|
317
|
+
const loc = new URL(locationHref, DEFAULT_BASE), callbackLocation = callbackUrl ? new URL(callbackUrl, DEFAULT_BASE) : void 0, callbackLocationMatches = callbackLocation ? loc.pathname.toLowerCase().startsWith(callbackLocation.pathname.toLowerCase()) : !0;
|
|
318
|
+
let authCode = new URLSearchParams(loc.hash.slice(1)).get(AUTH_CODE_PARAM) || new URLSearchParams(loc.search).get(AUTH_CODE_PARAM);
|
|
319
|
+
if (!authCode) {
|
|
320
|
+
const contextParam = new URLSearchParams(loc.search).get("_context");
|
|
321
|
+
if (contextParam)
|
|
322
|
+
try {
|
|
323
|
+
const parsedContext = JSON.parse(contextParam);
|
|
324
|
+
parsedContext && typeof parsedContext == "object" && typeof parsedContext.sid == "string" && parsedContext.sid && (authCode = parsedContext.sid);
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
248
328
|
return authCode && callbackLocationMatches ? authCode : null;
|
|
249
329
|
}
|
|
250
330
|
function getTokenFromStorage(storageArea, storageKey) {
|
|
@@ -277,6 +357,10 @@ function getDefaultLocation() {
|
|
|
277
357
|
return DEFAULT_BASE;
|
|
278
358
|
}
|
|
279
359
|
}
|
|
360
|
+
function getCleanedUrl(locationUrl) {
|
|
361
|
+
const loc = new URL(locationUrl);
|
|
362
|
+
return loc.hash = "", loc.searchParams.delete("sid"), loc.searchParams.delete("url"), loc.toString();
|
|
363
|
+
}
|
|
280
364
|
const subscribeToStorageEventsAndSetToken = ({
|
|
281
365
|
state
|
|
282
366
|
}) => {
|
|
@@ -303,20 +387,33 @@ const authStore = {
|
|
|
303
387
|
providers: customProviders,
|
|
304
388
|
token: providedToken,
|
|
305
389
|
clientFactory = createClient,
|
|
306
|
-
initialLocationHref = getDefaultLocation()
|
|
307
|
-
|
|
308
|
-
|
|
390
|
+
initialLocationHref = getDefaultLocation()
|
|
391
|
+
} = instance.config.auth ?? {};
|
|
392
|
+
let storageArea = instance.config.auth?.storageArea;
|
|
393
|
+
const storageKey = "__sanity_auth_token";
|
|
309
394
|
let loginDomain = "https://www.sanity.io";
|
|
310
395
|
try {
|
|
311
396
|
apiHost && new URL(apiHost).hostname.endsWith(".sanity.work") && (loginDomain = "https://www.sanity.work");
|
|
312
397
|
} catch {
|
|
313
398
|
}
|
|
314
399
|
const loginUrl = new URL("/login", loginDomain);
|
|
315
|
-
loginUrl.searchParams.set("origin", initialLocationHref), loginUrl.searchParams.set("type", "stampedToken"), loginUrl.searchParams.set("withSid", "true");
|
|
316
|
-
let
|
|
400
|
+
loginUrl.searchParams.set("origin", getCleanedUrl(initialLocationHref)), loginUrl.searchParams.set("type", "stampedToken"), loginUrl.searchParams.set("withSid", "true");
|
|
401
|
+
let dashboardContext = {}, isInDashboard = !1;
|
|
402
|
+
try {
|
|
403
|
+
const contextParam = new URL(initialLocationHref).searchParams.get("_context");
|
|
404
|
+
if (contextParam) {
|
|
405
|
+
const parsedContext = JSON.parse(contextParam);
|
|
406
|
+
parsedContext && typeof parsedContext == "object" && Object.keys(parsedContext).length > 0 && (delete parsedContext.sid, dashboardContext = parsedContext, isInDashboard = !0);
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error("Failed to parse dashboard context from initial location:", err);
|
|
410
|
+
}
|
|
411
|
+
isInDashboard || (storageArea = storageArea ?? getDefaultStorage());
|
|
317
412
|
const token = getTokenFromStorage(storageArea, storageKey);
|
|
318
|
-
|
|
413
|
+
let authState;
|
|
414
|
+
return providedToken ? authState = { type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null } : getAuthCode(callbackUrl, initialLocationHref) ? authState = { type: AuthStateType.LOGGING_IN, isExchangingToken: !1 } : token && !isInDashboard ? authState = { type: AuthStateType.LOGGED_IN, token, currentUser: null } : authState = { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 }, {
|
|
319
415
|
authState,
|
|
416
|
+
dashboardContext,
|
|
320
417
|
options: {
|
|
321
418
|
apiHost,
|
|
322
419
|
loginUrl: loginUrl.toString(),
|
|
@@ -365,18 +462,20 @@ const authStore = {
|
|
|
365
462
|
if (authState.type === AuthStateType.LOGGING_IN && authState.isExchangingToken) return !1;
|
|
366
463
|
const authCode = getAuthCode(callbackUrl, locationHref);
|
|
367
464
|
if (!authCode) return !1;
|
|
368
|
-
const parsedUrl = new URL(locationHref);
|
|
465
|
+
const cleanedUrl = getCleanedUrl(locationHref), parsedUrl = new URL(locationHref);
|
|
369
466
|
let dashboardContext = {};
|
|
370
467
|
try {
|
|
371
|
-
const contextParam = parsedUrl.searchParams.get("_context")
|
|
372
|
-
|
|
468
|
+
const contextParam = parsedUrl.searchParams.get("_context");
|
|
469
|
+
if (contextParam) {
|
|
470
|
+
const parsedContext = JSON.parse(contextParam);
|
|
471
|
+
parsedContext && typeof parsedContext == "object" && (delete parsedContext.sid, dashboardContext = parsedContext);
|
|
472
|
+
}
|
|
373
473
|
} catch (err) {
|
|
374
474
|
console.error("Failed to parse dashboard context:", err);
|
|
375
475
|
}
|
|
376
|
-
const { mode, env, orgId } = dashboardContext;
|
|
377
476
|
state.set("exchangeSessionForToken", {
|
|
378
477
|
authState: { type: AuthStateType.LOGGING_IN, isExchangingToken: !0 },
|
|
379
|
-
dashboardContext
|
|
478
|
+
dashboardContext
|
|
380
479
|
});
|
|
381
480
|
try {
|
|
382
481
|
const client = clientFactory({
|
|
@@ -390,11 +489,9 @@ const authStore = {
|
|
|
390
489
|
query: { sid: authCode },
|
|
391
490
|
tag: "fetch-token"
|
|
392
491
|
});
|
|
393
|
-
storageArea?.setItem(storageKey, JSON.stringify({ token })), state.set("setToken", { authState: { type: AuthStateType.LOGGED_IN, token, currentUser: null } });
|
|
394
|
-
const loc = new URL(locationHref);
|
|
395
|
-
return loc.hash = "", loc.searchParams.delete("sid"), loc.searchParams.delete("url"), loc.toString();
|
|
492
|
+
return storageArea?.setItem(storageKey, JSON.stringify({ token })), state.set("setToken", { authState: { type: AuthStateType.LOGGED_IN, token, currentUser: null } }), cleanedUrl;
|
|
396
493
|
} catch (error) {
|
|
397
|
-
return state.set("exchangeSessionForTokenError", { authState: { type: AuthStateType.ERROR, error } }),
|
|
494
|
+
return state.set("exchangeSessionForTokenError", { authState: { type: AuthStateType.ERROR, error } }), cleanedUrl;
|
|
398
495
|
}
|
|
399
496
|
}
|
|
400
497
|
), logout = bindActionGlobally(authStore, async ({ state }) => {
|
|
@@ -416,7 +513,7 @@ const authStore = {
|
|
|
416
513
|
} finally {
|
|
417
514
|
state.set("logoutSuccess", {
|
|
418
515
|
authState: { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 }
|
|
419
|
-
}), storageArea?.removeItem(storageKey);
|
|
516
|
+
}), storageArea?.removeItem(storageKey), storageArea?.removeItem(`${storageKey}_last_refresh`);
|
|
420
517
|
}
|
|
421
518
|
}), DEFAULT_API_VERSION = "2024-11-12", DEFAULT_REQUEST_TAG_PREFIX = "sanity.sdk", allowedKeys = Object.keys({
|
|
422
519
|
apiHost: null,
|
|
@@ -4593,25 +4690,19 @@ const _getPreviewState = bindActionByDataset(
|
|
|
4593
4690
|
)
|
|
4594
4691
|
);
|
|
4595
4692
|
}
|
|
4596
|
-
}), getProjectState = project.getState, resolveProject = project.resolveState
|
|
4597
|
-
data: null,
|
|
4598
|
-
isPending: !1
|
|
4599
|
-
};
|
|
4600
|
-
function validateProjection(projection) {
|
|
4601
|
-
if (!projection.startsWith("{") || !projection.endsWith("}"))
|
|
4602
|
-
throw new Error(
|
|
4603
|
-
`Invalid projection format: "${projection}". Projections must be enclosed in curly braces, e.g. "{title, 'author': author.name}"`
|
|
4604
|
-
);
|
|
4605
|
-
return projection;
|
|
4606
|
-
}
|
|
4693
|
+
}), getProjectState = project.getState, resolveProject = project.resolveState;
|
|
4607
4694
|
function createProjectionQuery(documentIds, documentProjections) {
|
|
4608
|
-
const projections = Array.from(documentIds).
|
|
4609
|
-
const
|
|
4610
|
-
return
|
|
4695
|
+
const projections = Array.from(documentIds).flatMap((id) => {
|
|
4696
|
+
const projectionsForDoc = documentProjections[id];
|
|
4697
|
+
return projectionsForDoc ? Object.entries(projectionsForDoc).map(([projectionHash, projection]) => ({
|
|
4698
|
+
documentId: id,
|
|
4699
|
+
projection,
|
|
4700
|
+
projectionHash
|
|
4701
|
+
})) : [];
|
|
4611
4702
|
}).reduce((acc, { documentId, projection, projectionHash }) => {
|
|
4612
4703
|
const obj = acc[projectionHash] ?? { documentIds: /* @__PURE__ */ new Set(), projection };
|
|
4613
4704
|
return obj.documentIds.add(documentId), acc[projectionHash] = obj, acc;
|
|
4614
|
-
}, {}), query = `[${Object.entries(projections).map(([projectionHash, { projection }]) => `...*[_id in $__ids_${projectionHash}]{_id,_type,_updatedAt,"result":{...${projection}}}`).join(",")}]`, params = Object.fromEntries(
|
|
4705
|
+
}, {}), query = `[${Object.entries(projections).map(([projectionHash, { projection }]) => `...*[_id in $__ids_${projectionHash}]{_id,_type,_updatedAt,"__projectionHash":"${projectionHash}","result":{...${projection}}}`).join(",")}]`, params = Object.fromEntries(
|
|
4615
4706
|
Object.entries(projections).map(([projectionHash, value]) => {
|
|
4616
4707
|
const idsInProjection = Array.from(value.documentIds).flatMap((id) => [
|
|
4617
4708
|
getPublishedId(id),
|
|
@@ -4623,50 +4714,83 @@ function createProjectionQuery(documentIds, documentProjections) {
|
|
|
4623
4714
|
return { query, params };
|
|
4624
4715
|
}
|
|
4625
4716
|
function processProjectionQuery({ ids, results }) {
|
|
4626
|
-
const
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4717
|
+
const groupedResults = {};
|
|
4718
|
+
for (const result of results) {
|
|
4719
|
+
const originalId = getPublishedId(result._id), hash = result.__projectionHash, isDraft = result._id.startsWith("drafts.");
|
|
4720
|
+
ids.has(originalId) && (groupedResults[originalId] || (groupedResults[originalId] = {}), groupedResults[originalId][hash] || (groupedResults[originalId][hash] = {}), isDraft ? groupedResults[originalId][hash].draft = result : groupedResults[originalId][hash].published = result);
|
|
4721
|
+
}
|
|
4722
|
+
const finalValues = {};
|
|
4723
|
+
for (const originalId of ids) {
|
|
4724
|
+
finalValues[originalId] = {};
|
|
4725
|
+
const projectionsForDoc = groupedResults[originalId];
|
|
4726
|
+
if (projectionsForDoc)
|
|
4727
|
+
for (const hash in projectionsForDoc) {
|
|
4728
|
+
const { draft, published } = projectionsForDoc[hash], projectionResultData = draft?.result ?? published?.result;
|
|
4729
|
+
if (!projectionResultData) {
|
|
4730
|
+
finalValues[originalId][hash] = { data: null, isPending: !1 };
|
|
4731
|
+
continue;
|
|
4732
|
+
}
|
|
4733
|
+
const status = {
|
|
4734
|
+
...draft?._updatedAt && { lastEditedDraftAt: draft._updatedAt },
|
|
4735
|
+
...published?._updatedAt && { lastEditedPublishedAt: published._updatedAt }
|
|
4736
|
+
};
|
|
4737
|
+
finalValues[originalId][hash] = {
|
|
4738
|
+
data: { ...projectionResultData, status },
|
|
4739
|
+
isPending: !1
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
return finalValues;
|
|
4744
|
+
}
|
|
4745
|
+
const PROJECTION_TAG = "sdk.projection", PROJECTION_PERSPECTIVE = "drafts", PROJECTION_STATE_CLEAR_DELAY = 1e3, STABLE_EMPTY_PROJECTION = {
|
|
4746
|
+
data: null,
|
|
4747
|
+
isPending: !1
|
|
4748
|
+
};
|
|
4749
|
+
function validateProjection(projection) {
|
|
4750
|
+
if (!projection.startsWith("{") || !projection.endsWith("}"))
|
|
4751
|
+
throw new Error(
|
|
4752
|
+
`Invalid projection format: "${projection}". Projections must be enclosed in curly braces, e.g. "{title, 'author': author.name}"`
|
|
4753
|
+
);
|
|
4754
|
+
return projection;
|
|
4641
4755
|
}
|
|
4642
4756
|
const BATCH_DEBOUNCE_TIME = 50, isSetEqual = (a2, b2) => a2.size === b2.size && Array.from(a2).every((i2) => b2.has(i2)), subscribeToStateAndFetchBatches = ({
|
|
4643
4757
|
state,
|
|
4644
4758
|
instance
|
|
4645
4759
|
}) => {
|
|
4646
4760
|
const documentProjections$ = state.observable.pipe(
|
|
4647
|
-
map((
|
|
4648
|
-
distinctUntilChanged()
|
|
4649
|
-
),
|
|
4761
|
+
map((s2) => s2.documentProjections),
|
|
4762
|
+
distinctUntilChanged(isEqual)
|
|
4763
|
+
), activeDocumentIds$ = state.observable.pipe(
|
|
4650
4764
|
map(({ subscriptions }) => new Set(Object.keys(subscriptions))),
|
|
4651
|
-
distinctUntilChanged(isSetEqual)
|
|
4765
|
+
distinctUntilChanged(isSetEqual)
|
|
4766
|
+
), pendingUpdateSubscription = activeDocumentIds$.pipe(
|
|
4652
4767
|
debounceTime(BATCH_DEBOUNCE_TIME),
|
|
4653
4768
|
startWith$1(/* @__PURE__ */ new Set()),
|
|
4654
4769
|
pairwise$1(),
|
|
4655
4770
|
tap$1(([prevIds, currIds]) => {
|
|
4656
|
-
const newIds = [...currIds].filter((
|
|
4657
|
-
state.set("updatingPending", (prev) => {
|
|
4658
|
-
const
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4771
|
+
const newIds = [...currIds].filter((id) => !prevIds.has(id));
|
|
4772
|
+
newIds.length !== 0 && state.set("updatingPending", (prev) => {
|
|
4773
|
+
const nextValues = { ...prev.values };
|
|
4774
|
+
for (const id of newIds) {
|
|
4775
|
+
const projectionsForDoc = prev.documentProjections[id];
|
|
4776
|
+
if (!projectionsForDoc) continue;
|
|
4777
|
+
const updatedValuesForDoc = { ...prev.values[id] ?? {} };
|
|
4778
|
+
for (const hash in projectionsForDoc) {
|
|
4779
|
+
const currentValue = updatedValuesForDoc[hash];
|
|
4780
|
+
updatedValuesForDoc[hash] = {
|
|
4781
|
+
data: currentValue?.data ?? null,
|
|
4782
|
+
isPending: !0
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
nextValues[id] = updatedValuesForDoc;
|
|
4786
|
+
}
|
|
4787
|
+
return { values: nextValues };
|
|
4663
4788
|
});
|
|
4664
|
-
})
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
distinctUntilChanged(isEqual),
|
|
4789
|
+
})
|
|
4790
|
+
).subscribe(), queryExecutionSubscription = combineLatest([activeDocumentIds$, documentProjections$]).pipe(
|
|
4791
|
+
debounceTime(BATCH_DEBOUNCE_TIME),
|
|
4792
|
+
distinctUntilChanged(isEqual)
|
|
4793
|
+
).pipe(
|
|
4670
4794
|
switchMap(([ids, documentProjections]) => {
|
|
4671
4795
|
if (!ids.size) return EMPTY;
|
|
4672
4796
|
const { query, params } = createProjectionQuery(ids, documentProjections), controller = new AbortController();
|
|
@@ -4688,19 +4812,31 @@ const BATCH_DEBOUNCE_TIME = 50, isSetEqual = (a2, b2) => a2.size === b2.size &&
|
|
|
4688
4812
|
};
|
|
4689
4813
|
}).pipe(map((data) => ({ data, ids })));
|
|
4690
4814
|
}),
|
|
4691
|
-
map(
|
|
4692
|
-
|
|
4815
|
+
map(
|
|
4816
|
+
({ ids, data }) => processProjectionQuery({
|
|
4693
4817
|
ids,
|
|
4694
4818
|
results: data
|
|
4695
4819
|
})
|
|
4696
|
-
|
|
4820
|
+
)
|
|
4697
4821
|
).subscribe({
|
|
4698
|
-
next: (
|
|
4699
|
-
state.set("updateResult", (prev) =>
|
|
4700
|
-
|
|
4701
|
-
|
|
4822
|
+
next: (processedValues) => {
|
|
4823
|
+
state.set("updateResult", (prev) => {
|
|
4824
|
+
const nextValues = { ...prev.values };
|
|
4825
|
+
for (const docId in processedValues)
|
|
4826
|
+
processedValues[docId] && (nextValues[docId] = {
|
|
4827
|
+
...prev.values[docId] ?? {},
|
|
4828
|
+
...processedValues[docId]
|
|
4829
|
+
});
|
|
4830
|
+
return { values: nextValues };
|
|
4831
|
+
});
|
|
4832
|
+
},
|
|
4833
|
+
error: (err) => {
|
|
4834
|
+
console.error("Error fetching projection batches:", err);
|
|
4702
4835
|
}
|
|
4703
4836
|
});
|
|
4837
|
+
return new Subscription(() => {
|
|
4838
|
+
pendingUpdateSubscription.unsubscribe(), queryExecutionSubscription.unsubscribe();
|
|
4839
|
+
});
|
|
4704
4840
|
}, projectionStore = {
|
|
4705
4841
|
name: "Projection",
|
|
4706
4842
|
getInitialState() {
|
|
@@ -4721,28 +4857,53 @@ function getProjectionState(...args) {
|
|
|
4721
4857
|
const _getProjectionState = bindActionByDataset(
|
|
4722
4858
|
projectionStore,
|
|
4723
4859
|
createStateSourceAction({
|
|
4724
|
-
selector: ({ state }, options) =>
|
|
4860
|
+
selector: ({ state }, options) => {
|
|
4861
|
+
const documentId = getPublishedId(options.documentId), projectionHash = hashString(options.projection);
|
|
4862
|
+
return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION;
|
|
4863
|
+
},
|
|
4725
4864
|
onSubscribe: ({ state }, { projection, ...docHandle }) => {
|
|
4726
|
-
const subscriptionId = insecureRandomId(), documentId = getPublishedId(docHandle.documentId);
|
|
4865
|
+
const subscriptionId = insecureRandomId(), documentId = getPublishedId(docHandle.documentId), validProjection = validateProjection(projection), projectionHash = hashString(validProjection);
|
|
4727
4866
|
return state.set("addSubscription", (prev) => ({
|
|
4728
4867
|
documentProjections: {
|
|
4729
4868
|
...prev.documentProjections,
|
|
4730
|
-
[documentId]:
|
|
4869
|
+
[documentId]: {
|
|
4870
|
+
...prev.documentProjections[documentId],
|
|
4871
|
+
[projectionHash]: validProjection
|
|
4872
|
+
}
|
|
4731
4873
|
},
|
|
4732
4874
|
subscriptions: {
|
|
4733
4875
|
...prev.subscriptions,
|
|
4734
4876
|
[documentId]: {
|
|
4735
4877
|
...prev.subscriptions[documentId],
|
|
4736
|
-
[
|
|
4878
|
+
[projectionHash]: {
|
|
4879
|
+
...prev.subscriptions[documentId]?.[projectionHash],
|
|
4880
|
+
[subscriptionId]: !0
|
|
4881
|
+
}
|
|
4737
4882
|
}
|
|
4738
4883
|
}
|
|
4739
4884
|
})), () => {
|
|
4740
4885
|
setTimeout(() => {
|
|
4741
4886
|
state.set("removeSubscription", (prev) => {
|
|
4742
|
-
const
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4887
|
+
const documentSubscriptionsForHash = omit(
|
|
4888
|
+
prev.subscriptions[documentId]?.[projectionHash],
|
|
4889
|
+
subscriptionId
|
|
4890
|
+
), hasSubscribersForProjection = !!Object.keys(documentSubscriptionsForHash).length, nextSubscriptions = { ...prev.subscriptions }, nextDocumentProjections = { ...prev.documentProjections }, nextValues = { ...prev.values };
|
|
4891
|
+
if (hasSubscribersForProjection)
|
|
4892
|
+
nextSubscriptions[documentId] && (nextSubscriptions[documentId][projectionHash] = documentSubscriptionsForHash);
|
|
4893
|
+
else {
|
|
4894
|
+
delete nextSubscriptions[documentId][projectionHash], delete nextDocumentProjections[documentId][projectionHash];
|
|
4895
|
+
const currentProjectionValue = prev.values[documentId]?.[projectionHash];
|
|
4896
|
+
currentProjectionValue && nextValues[documentId] && (nextValues[documentId][projectionHash] = {
|
|
4897
|
+
data: currentProjectionValue.data,
|
|
4898
|
+
isPending: !1
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
return Object.values(
|
|
4902
|
+
nextSubscriptions[documentId] ?? {}
|
|
4903
|
+
).some((subs) => Object.keys(subs).length > 0) || (delete nextSubscriptions[documentId], delete nextDocumentProjections[documentId]), {
|
|
4904
|
+
subscriptions: nextSubscriptions,
|
|
4905
|
+
documentProjections: nextDocumentProjections,
|
|
4906
|
+
values: nextValues
|
|
4746
4907
|
};
|
|
4747
4908
|
});
|
|
4748
4909
|
}, PROJECTION_STATE_CLEAR_DELAY);
|
|
@@ -4931,7 +5092,7 @@ function createGroqSearchFilter(query) {
|
|
|
4931
5092
|
`${finalIncrementalToken}${WILDCARD_TOKEN}`
|
|
4932
5093
|
), `[@] match text::query("${processedTokens.join(" ").replace(/"/g, '\\"')}")`;
|
|
4933
5094
|
}
|
|
4934
|
-
var version = "0.0.0-alpha.
|
|
5095
|
+
var version = "0.0.0-alpha.26";
|
|
4935
5096
|
const CORE_SDK_VERSION = getEnv("PKG_VERSION") || `${version}-development`;
|
|
4936
5097
|
export {
|
|
4937
5098
|
AuthStateType,
|