@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 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 './projectionStore'
24
- import {ProjectionValuePending as ProjectionValuePending_2} from './projectionStore'
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
- * @beta
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
- * @beta
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, switchMap, interval, takeWhile, fromEvent, EMPTY, defer, firstValueFrom, from, asapScheduler, concatMap, of, withLatestFrom, concat, timer, 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 } from "rxjs";
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", refreshStampedToken = ({ state }) => {
165
- const { clientFactory, apiHost, storageArea, storageKey } = state.get().options, refreshInterval = 12 * 60 * 60 * 1e3;
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(({ authState }) => authState),
228
+ map((storeState) => ({
229
+ authState: storeState.authState,
230
+ dashboardContext: storeState.dashboardContext
231
+ })),
168
232
  filter(
169
- (authState) => authState.type === AuthStateType.LOGGED_IN
233
+ (storeState) => storeState.authState.type === AuthStateType.LOGGED_IN
170
234
  ),
171
- distinctUntilChanged(),
172
- filter((authState) => authState.token.includes("-st")),
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
- switchMap(
175
- (authState) => interval(refreshInterval).pipe(
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
- map(() => authState.token),
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
- (client) => client.observable.request({
191
- uri: "auth/refresh-token",
192
- method: "POST",
193
- body: {
194
- token: authState.token
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, authCode = new URLSearchParams(loc.hash.slice(1)).get(AUTH_CODE_PARAM) || new URLSearchParams(loc.search).get(AUTH_CODE_PARAM);
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
- storageArea = getDefaultStorage()
308
- } = instance.config.auth ?? {}, storageKey = "__sanity_auth_token";
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 authState;
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
- return providedToken ? authState = { type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null } : getAuthCode(callbackUrl, initialLocationHref) ? authState = { type: AuthStateType.LOGGING_IN, isExchangingToken: !1 } : token ? authState = { type: AuthStateType.LOGGED_IN, token, currentUser: null } : authState = { type: AuthStateType.LOGGED_OUT, isDestroyingSession: !1 }, {
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
- dashboardContext = JSON.parse(contextParam);
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: { mode, env, orgId }
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 } }), !1;
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, PROJECTION_TAG = "sdk.projection", PROJECTION_PERSPECTIVE = "drafts", PROJECTION_STATE_CLEAR_DELAY = 1e3, STABLE_EMPTY_PROJECTION = {
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).filter((id) => documentProjections[id]).map((id) => {
4609
- const projection = validateProjection(documentProjections[id]), projectionHash = hashString(projection);
4610
- return { documentId: id, projection, projectionHash };
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 resultMap = results.reduce(
4627
- (acc, next) => (acc[next._id] = next, acc),
4628
- {}
4629
- );
4630
- return Object.fromEntries(
4631
- Array.from(ids).map((id) => {
4632
- const publishedId = getPublishedId(id), draftId = getDraftId(id), draftResult = resultMap[draftId], publishedResult = resultMap[publishedId], projectionResult = draftResult?.result ?? publishedResult?.result;
4633
- if (!projectionResult) return [id, { data: null, isPending: !1 }];
4634
- const status = {
4635
- ...draftResult?._updatedAt && { lastEditedDraftAt: draftResult._updatedAt },
4636
- ...publishedResult?._updatedAt && { lastEditedPublishedAt: publishedResult._updatedAt }
4637
- };
4638
- return [id, { data: { ...projectionResult, status }, isPending: !1 }];
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((i2) => i2.documentProjections),
4648
- distinctUntilChanged()
4649
- ), newSubscriberIds$ = state.observable.pipe(
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((element) => !prevIds.has(element));
4657
- state.set("updatingPending", (prev) => {
4658
- const pendingValues = newIds.reduce((acc, id) => {
4659
- const prevValue = prev.values[id], value = prevValue?.data ? prevValue.data : null;
4660
- return acc[id] = { data: value, isPending: !0 }, acc;
4661
- }, {});
4662
- return { values: { ...prev.values, ...pendingValues } };
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
- map(([, ids]) => ids),
4666
- distinctUntilChanged(isSetEqual)
4667
- );
4668
- return combineLatest([newSubscriberIds$, documentProjections$]).pipe(
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(({ ids, data }) => ({
4692
- values: processProjectionQuery({
4815
+ map(
4816
+ ({ ids, data }) => processProjectionQuery({
4693
4817
  ids,
4694
4818
  results: data
4695
4819
  })
4696
- }))
4820
+ )
4697
4821
  ).subscribe({
4698
- next: ({ values }) => {
4699
- state.set("updateResult", (prev) => ({
4700
- values: { ...prev.values, ...values }
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) => state.values[options.documentId] ?? STABLE_EMPTY_PROJECTION,
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]: validateProjection(projection)
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
- [subscriptionId]: !0
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 documentSubscriptions = omit(prev.subscriptions[documentId], subscriptionId), hasSubscribers = !!Object.keys(documentSubscriptions).length, prevValue = prev.values[documentId], projectionValue = prevValue?.data ? prevValue.data : null;
4743
- return {
4744
- subscriptions: hasSubscribers ? { ...prev.subscriptions, [documentId]: documentSubscriptions } : omit(prev.subscriptions, documentId),
4745
- values: hasSubscribers ? prev.values : { ...prev.values, [documentId]: { data: projectionValue, isPending: !1 } }
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.25";
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,