@sanity/sdk 2.18.0 → 2.20.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.
Files changed (75) hide show
  1. package/dist/_exports/_internal.d.ts +2 -2
  2. package/dist/_exports/_internal.d.ts.map +1 -1
  3. package/dist/_exports/_internal.js +196 -22
  4. package/dist/_exports/_internal.js.map +1 -1
  5. package/dist/authStore-CWTAuieY.js +1072 -0
  6. package/dist/authStore-CWTAuieY.js.map +1 -0
  7. package/dist/clientStore-CMzJ7XhZ.js +168 -0
  8. package/dist/clientStore-CMzJ7XhZ.js.map +1 -0
  9. package/dist/createGroqSearchFilter-B0NdA-EP.js +1222 -0
  10. package/dist/createGroqSearchFilter-B0NdA-EP.js.map +1 -0
  11. package/dist/getEnv-76AjZd_f.js +178 -0
  12. package/dist/getEnv-76AjZd_f.js.map +1 -0
  13. package/dist/index.d.ts +2 -1921
  14. package/dist/index.js +4165 -3711
  15. package/dist/index.js.map +1 -1
  16. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  17. package/dist/telemetryManager-Bd3O8z2S.js +127 -0
  18. package/dist/telemetryManager-Bd3O8z2S.js.map +1 -0
  19. package/dist/utils-BMwA8Kg9.d.ts +3334 -0
  20. package/dist/utils-BMwA8Kg9.d.ts.map +1 -0
  21. package/dist/version-DL2l07HG.js +9 -0
  22. package/dist/version-DL2l07HG.js.map +1 -0
  23. package/package.json +13 -21
  24. package/src/_exports/_internal.ts +1 -0
  25. package/src/_exports/index.ts +37 -1
  26. package/src/comments/addonDatasetStore.test.ts +215 -0
  27. package/src/comments/addonDatasetStore.ts +249 -0
  28. package/src/comments/buildCommentThreads.test.ts +83 -0
  29. package/src/comments/buildCommentThreads.ts +54 -0
  30. package/src/comments/commentActions.test.ts +540 -0
  31. package/src/comments/commentActions.ts +555 -0
  32. package/src/comments/commentFieldPath.test.ts +40 -0
  33. package/src/comments/commentFieldPath.ts +44 -0
  34. package/src/comments/commentsConstants.ts +80 -0
  35. package/src/comments/commentsStore.test.ts +419 -0
  36. package/src/comments/commentsStore.ts +483 -0
  37. package/src/comments/normalizeComment.test.ts +116 -0
  38. package/src/comments/normalizeComment.ts +45 -0
  39. package/src/comments/observeComments.test.ts +193 -0
  40. package/src/comments/observeComments.ts +113 -0
  41. package/src/comments/reducers.test.ts +277 -0
  42. package/src/comments/reducers.ts +316 -0
  43. package/src/comments/types.ts +249 -0
  44. package/src/comments/weakenReferences.test.ts +47 -0
  45. package/src/comments/weakenReferences.ts +29 -0
  46. package/src/document/documentConstants.ts +8 -0
  47. package/src/document/documentStore.concurrency.test.ts +313 -22
  48. package/src/document/documentStore.test.ts +98 -1
  49. package/src/document/documentStore.ts +36 -5
  50. package/src/document/patchOperations.test.ts +27 -0
  51. package/src/document/patchOperations.ts +10 -1
  52. package/src/document/permissions.test.ts +3 -1
  53. package/src/document/reducers.test.ts +133 -0
  54. package/src/document/reducers.ts +18 -3
  55. package/src/document/util.test.ts +46 -0
  56. package/src/document/util.ts +37 -1
  57. package/src/presence/bifurTransport.test.ts +155 -3
  58. package/src/presence/bifurTransport.ts +79 -12
  59. package/src/presence/paths.test.ts +70 -0
  60. package/src/presence/paths.ts +41 -0
  61. package/src/presence/presenceStore.test.ts +822 -2
  62. package/src/presence/presenceStore.ts +518 -102
  63. package/src/presence/types.ts +175 -3
  64. package/src/projection/projectionStore.test.ts +0 -8
  65. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -926
  66. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
  67. package/dist/_chunks-es/_internal.js +0 -133
  68. package/dist/_chunks-es/_internal.js.map +0 -1
  69. package/dist/_chunks-es/createGroqSearchFilter.js +0 -1586
  70. package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
  71. package/dist/_chunks-es/telemetryManager.js +0 -93
  72. package/dist/_chunks-es/telemetryManager.js.map +0 -1
  73. package/dist/_chunks-es/version.js +0 -7
  74. package/dist/_chunks-es/version.js.map +0 -1
  75. package/dist/index.d.ts.map +0 -1
@@ -0,0 +1,1222 @@
1
+ import { A as defineStore, D as bindActionByResource, E as createStateSourceAction, P as isDatasetResource, j as isReleasePerspective } from "./authStore-CWTAuieY.js";
2
+ import { a as isObject, n as getClient, o as omitProperty, r as getClientState } from "./clientStore-CMzJ7XhZ.js";
3
+ import { ConnectionFailedError, CorsOriginError, DisconnectError } from "@sanity/client";
4
+ import { BehaviorSubject, EMPTY, NEVER, Observable, catchError, combineLatest, defer, distinctUntilChanged, filter, first, firstValueFrom, groupBy, map, mergeMap, of, pairwise, race, retry, share, startWith, switchMap, take, tap } from "rxjs";
5
+ import "@sanity/types";
6
+ import { createSelector } from "reselect";
7
+ import { DocumentId, getPublishedId } from "@sanity/id-utils";
8
+ import { stringifyPath } from "@sanity/json-match";
9
+ import "groq";
10
+ import { createImageUrlBuilder } from "@sanity/image-url";
11
+ function insecureRandomId() {
12
+ return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
13
+ }
14
+ /**
15
+ * Like `setTimeout`, but calls `.unref()` on the timer when running in Node.js.
16
+ *
17
+ * In Node.js, active timers prevent the process from exiting. Cleanup timers
18
+ * (e.g. deferred subscription removal) should not keep the process alive -
19
+ * `.unref()` lets the process exit naturally while still firing the timer if
20
+ * the process happens to still be running.
21
+ *
22
+ * In browsers, `setTimeout` returns a number and has no `.unref()` method,
23
+ * so this is a no-op there.
24
+ */
25
+ function setCleanupTimeout(fn, delay) {
26
+ let timer = setTimeout(fn, delay), t = timer;
27
+ return typeof t == "object" && t && "unref" in t && typeof t.unref == "function" && t.unref(), timer;
28
+ }
29
+ /** Data API version used for reading and writing comments in the addon dataset. */
30
+ const COMMENTS_API_VERSION = "v2025-05-06", ADDON_DATASET_API_VERSION = "v2025-02-19", COMMENTS_STATE_CLEAR_DELAY = 5e3, LISTEN_OPTIONS = {
31
+ events: [
32
+ "welcome",
33
+ "mutation",
34
+ "reconnect"
35
+ ],
36
+ includeResult: !0,
37
+ includeAllVersions: !0,
38
+ visibility: "query",
39
+ tag: "comments.listen"
40
+ }, BASE_FILTERS = ["_type == \"comment\"", "target.document._ref == $documentId"];
41
+ /**
42
+ * Which comments belong to a document, matching the Studio's filter.
43
+ *
44
+ * A release's comments are kept apart from the default ones: asking for a
45
+ * release returns only that release's comments, and asking without one returns
46
+ * only comments with no version at all.
47
+ */
48
+ function buildCommentsFilter(documentVersionId) {
49
+ return [...BASE_FILTERS, documentVersionId ? "target.documentVersionId == $documentVersionId" : "!defined(target.documentVersionId)"].join(" && ");
50
+ }
51
+ /** The snapshot query. Newest comment first, as the Studio orders it. */
52
+ function buildCommentsQuery(documentVersionId) {
53
+ return `*[${buildCommentsFilter(documentVersionId)}] {
54
+ _createdAt,
55
+ _id,
56
+ _rev,
57
+ authorId,
58
+ contentSnapshot,
59
+ context,
60
+ lastEditedAt,
61
+ message,
62
+ parentCommentId,
63
+ reactions,
64
+ status,
65
+ target,
66
+ threadId
67
+ } | order(_createdAt desc)`;
68
+ }
69
+ /**
70
+ * The listener query. Unprojected on purpose: mutation events carry the whole
71
+ * comment, so there is nothing to trim.
72
+ */
73
+ function buildCommentsListenQuery(documentVersionId) {
74
+ return `*[${buildCommentsFilter(documentVersionId)}]`;
75
+ }
76
+ /**
77
+ * Comments need a project and a dataset. There is nowhere to put them for a
78
+ * media library or a canvas, and no Studio to interoperate with either.
79
+ *
80
+ * @internal
81
+ */
82
+ function assertDatasetResource(resource) {
83
+ if (!isDatasetResource(resource)) throw Error(`Comments are only supported for dataset resources, received: ${JSON.stringify(resource)}`);
84
+ return resource;
85
+ }
86
+ function isMissingOrForbidden(error) {
87
+ if (typeof error != "object" || !error || !("statusCode" in error)) return !1;
88
+ let { statusCode } = error;
89
+ return statusCode === 403 || statusCode === 404;
90
+ }
91
+ function requestAddonDataset(client, { projectId, dataset }) {
92
+ return client.observable.request({
93
+ uri: `/projects/${projectId}/datasets?datasetProfile=comments&addonFor=${dataset}`,
94
+ tag: "comments.addon-dataset.list"
95
+ }).pipe(map((datasets) => datasets?.[0]?.name));
96
+ }
97
+ function discover(instance, resource) {
98
+ return getClientState(instance, {
99
+ apiVersion: ADDON_DATASET_API_VERSION,
100
+ projectId: resource.projectId,
101
+ useProjectHostname: !0
102
+ }).observable.pipe(switchMap((client) => requestAddonDataset(client, resource).pipe(catchError((error) => isMissingOrForbidden(error) ? of(void 0) : EMPTY))));
103
+ }
104
+ async function discoverOnce(instance, resource) {
105
+ let client = await firstValueFrom(getClientState(instance, {
106
+ apiVersion: ADDON_DATASET_API_VERSION,
107
+ projectId: resource.projectId,
108
+ useProjectHostname: !0
109
+ }).observable.pipe(take(1)));
110
+ try {
111
+ return await firstValueFrom(requestAddonDataset(client, resource));
112
+ } catch (error) {
113
+ if (isMissingOrForbidden(error)) return;
114
+ throw error;
115
+ }
116
+ }
117
+ const addonDatasetStore = defineStore({
118
+ name: "CommentsAddonDataset",
119
+ getInitialState: () => ({ status: "unknown" }),
120
+ initialize: ({ instance, state, key }) => {
121
+ let subscription = discover(instance, assertDatasetResource(key.resource)).subscribe((datasetName) => state.set("setAddonDataset", datasetName ? {
122
+ status: "resolved",
123
+ datasetName
124
+ } : { status: "missing" }));
125
+ return () => subscription.unsubscribe();
126
+ }
127
+ }), getAddonDatasetState = bindActionByResource(addonDatasetStore, createStateSourceAction(({ state }) => {
128
+ if (state.status !== "unknown") return state.datasetName ?? null;
129
+ })), provisionAddonDataset = bindActionByResource(addonDatasetStore, ({ instance, state, key }) => {
130
+ let current = state.get();
131
+ if (current.datasetName) return Promise.resolve(current.datasetName);
132
+ if (current.provisioning) return current.provisioning;
133
+ let resource = assertDatasetResource(key.resource), provisioning = (async () => {
134
+ let existing = await discoverOnce(instance, resource);
135
+ if (existing) return existing;
136
+ let response = await getClient(instance, {
137
+ apiVersion: ADDON_DATASET_API_VERSION,
138
+ projectId: resource.projectId,
139
+ useProjectHostname: !0
140
+ }).request({
141
+ uri: `/comments/${resource.dataset}/setup`,
142
+ method: "POST",
143
+ tag: "comments.addon-dataset.setup"
144
+ });
145
+ if (!response?.datasetName) throw Error("Creating the comments addon dataset returned no dataset name.");
146
+ return response.datasetName;
147
+ })().then((datasetName) => (state.set("setAddonDataset", {
148
+ status: "resolved",
149
+ datasetName,
150
+ provisioning: void 0
151
+ }), datasetName)).catch((error) => {
152
+ throw state.set("clearProvisioning", { provisioning: void 0 }), error;
153
+ });
154
+ return state.set("startProvisioning", { provisioning }), provisioning;
155
+ }), observeAddonDatasetClient = bindActionByResource(addonDatasetStore, ({ instance, key }) => {
156
+ let { projectId } = assertDatasetResource(key.resource);
157
+ return getAddonDatasetState(instance, { resource: key.resource }).observable.pipe(distinctUntilChanged(), filter((datasetName) => datasetName !== void 0), switchMap((datasetName) => typeof datasetName == "string" ? getClientState(instance, {
158
+ apiVersion: COMMENTS_API_VERSION,
159
+ projectId,
160
+ dataset: datasetName
161
+ }).observable : of(null)));
162
+ }), RESERVED_SEGMENT = /\.(true|false|null)(?=$|[.[])/g, QUOTED_OR_PLAIN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^"']+/g;
163
+ function quoteReservedFieldSegments(path) {
164
+ return path.replace(QUOTED_OR_PLAIN, (chunk) => chunk.startsWith("\"") || chunk.startsWith("'") ? chunk : chunk.replace(RESERVED_SEGMENT, "[\"$1\"]"));
165
+ }
166
+ /**
167
+ * Normalises a field path to the string form comments are stored in.
168
+ *
169
+ * Comments keep `target.path.field` as a string rather than a path array,
170
+ * because that is what the Studio writes and reading it back has to match
171
+ * exactly. A string given here is parsed and re-stringified, so both a path
172
+ * array and a hand-written string come out in the same canonical form.
173
+ *
174
+ * `undefined` and `[]` both produce `''`. Callers writing a comment reject that
175
+ * rather than passing it on, since a comment has to point at a field.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * toCommentFieldPath(['body', {_key: 'intro'}, 'content'])
180
+ * // 'body[_key=="intro"].content'
181
+ * toCommentFieldPath(undefined)
182
+ * // ''
183
+ * ```
184
+ *
185
+ * @internal
186
+ */
187
+ function toCommentFieldPath(path) {
188
+ return quoteReservedFieldSegments(stringifyPath(path));
189
+ }
190
+ /**
191
+ * Groups a flat comment list into threads.
192
+ *
193
+ * A thread's parent is the comment with no `parentCommentId`; replies point at
194
+ * the parent's `id`. Replies whose parent is missing from the input are
195
+ * dropped, which is what happens when a parent is deleted but its replies have
196
+ * not disappeared yet.
197
+ *
198
+ * Threads come back in the order their parents appear in `comments`, so passing
199
+ * the store's newest-first list gives newest-thread-first. Replies within a
200
+ * thread are always oldest first, since that is reading order.
201
+ *
202
+ * @internal
203
+ */
204
+ function buildCommentThreads(comments) {
205
+ let repliesByParent = /* @__PURE__ */ new Map();
206
+ for (let comment of comments) {
207
+ if (!comment.parentCommentId) continue;
208
+ let existing = repliesByParent.get(comment.parentCommentId);
209
+ existing ? existing.push(comment) : repliesByParent.set(comment.parentCommentId, [comment]);
210
+ }
211
+ let threads = [];
212
+ for (let parentComment of comments) {
213
+ if (parentComment.parentCommentId) continue;
214
+ let replies = (repliesByParent.get(parentComment.id) ?? []).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), lastReply = replies[replies.length - 1];
215
+ threads.push({
216
+ threadId: parentComment.threadId,
217
+ fieldPath: parentComment.fieldPath,
218
+ parentComment,
219
+ replies,
220
+ commentsCount: replies.length + 1,
221
+ status: parentComment.status,
222
+ lastActivityAt: lastReply ? lastReply.createdAt : parentComment.createdAt
223
+ });
224
+ }
225
+ return threads;
226
+ }
227
+ /**
228
+ * Turns a stored comment into the shape consumers see.
229
+ *
230
+ * This is the whole seam between the storage format and the public API. When
231
+ * comments move to the organization-level store, the three fields that differ
232
+ * are handled here and nothing downstream changes:
233
+ *
234
+ * - the author moves from `authorId` to `_system.createdBy`
235
+ * - `target.document` becomes a global document reference, so `documentId` is
236
+ * parsed out of a `resourceType:resourceId:documentId` string rather than
237
+ * read off `_ref`
238
+ * - `_type` goes from `comment` to `sanity.comment`, and stops being exposed
239
+ *
240
+ * @internal
241
+ */
242
+ function normalizeComment(stored) {
243
+ let { target } = stored;
244
+ return {
245
+ id: stored._id,
246
+ createdAt: stored._createdAt,
247
+ ...stored.authorId ? { authorId: stored.authorId } : {},
248
+ message: stored.message,
249
+ threadId: stored.threadId,
250
+ ...stored.parentCommentId ? { parentCommentId: stored.parentCommentId } : {},
251
+ status: stored.status,
252
+ ...stored.lastEditedAt ? { lastEditedAt: stored.lastEditedAt } : {},
253
+ documentId: target.document._ref,
254
+ documentType: target.documentType,
255
+ fieldPath: target.path?.field ?? "",
256
+ ...target.path?.selection ? { selection: target.path.selection } : {},
257
+ ...stored.contentSnapshot === void 0 ? {} : { contentSnapshot: stored.contentSnapshot },
258
+ reactions: (stored.reactions ?? []).map(({ shortName, userId, addedAt }) => ({
259
+ shortName,
260
+ userId,
261
+ addedAt
262
+ })),
263
+ ...stored._state ? { state: stored._state } : {}
264
+ };
265
+ }
266
+ function toCommentsEvent(event) {
267
+ if (event.type === "mutation") {
268
+ if (event.transition === "disappear") return {
269
+ type: "disappear",
270
+ commentId: event.documentId
271
+ };
272
+ if (event.result) return event.transition === "appear" ? {
273
+ type: "appear",
274
+ comment: event.result
275
+ } : {
276
+ type: "update",
277
+ comment: event.result,
278
+ transactionId: event.transactionId
279
+ };
280
+ }
281
+ }
282
+ /**
283
+ * Watches one document's comments: a snapshot on connect, then live changes.
284
+ *
285
+ * The Studio awaits its snapshot inside the event handler, which loses any
286
+ * mutation that arrives while the fetch is in flight, because the older
287
+ * snapshot overwrites it. Here the mutations that arrive during a fetch are
288
+ * held and replayed straight after the snapshot, so nothing is dropped.
289
+ *
290
+ * A reconnect produces a fresh `welcome`, which cancels any fetch still running
291
+ * and starts a new one. Reconnects emit nothing themselves: dropping the list
292
+ * while reconnecting would only make readers suspend again for data we still
293
+ * have.
294
+ *
295
+ * @internal
296
+ */
297
+ function observeComments(options) {
298
+ let { client, documentId, documentVersionId } = options, params = {
299
+ documentId,
300
+ ...documentVersionId ? { documentVersionId } : {}
301
+ }, events$ = client.observable.listen(buildCommentsListenQuery(documentVersionId), params, LISTEN_OPTIONS).pipe(share()), mutations$ = events$.pipe(map(toCommentsEvent), filter((event) => event !== void 0));
302
+ return events$.pipe(filter((event) => event.type === "welcome"), switchMap(() => new Observable((observer) => {
303
+ let buffered = [], snapshotReceived = !1, mutationSubscription = mutations$.subscribe({
304
+ next: (event) => {
305
+ snapshotReceived ? observer.next(event) : buffered.push(event);
306
+ },
307
+ error: (error) => observer.error(error)
308
+ }), snapshotSubscription = client.observable.fetch(buildCommentsQuery(documentVersionId), params, { tag: "comments.list" }).pipe(take(1)).subscribe({
309
+ next: (comments) => {
310
+ observer.next({
311
+ type: "snapshot",
312
+ comments
313
+ }), snapshotReceived = !0;
314
+ for (let event of buffered) observer.next(event);
315
+ buffered.length = 0;
316
+ },
317
+ error: (error) => {
318
+ observer.next({
319
+ type: "error",
320
+ error
321
+ }), observer.complete();
322
+ }
323
+ });
324
+ return () => {
325
+ mutationSubscription.unsubscribe(), snapshotSubscription.unsubscribe(), buffered.length = 0;
326
+ };
327
+ })));
328
+ }
329
+ function getCommentsKey({ documentId, documentVersionId }) {
330
+ return JSON.stringify([documentId, documentVersionId ?? null]);
331
+ }
332
+ function parseCommentsKey(key) {
333
+ let [documentId, documentVersionId] = JSON.parse(key);
334
+ return {
335
+ documentId,
336
+ ...documentVersionId ? { documentVersionId } : {}
337
+ };
338
+ }
339
+ const addSubscriber$1 = (key, subscriptionId) => (prev) => {
340
+ let entry = prev.entries[key], subscribers = [...entry?.subscribers ?? [], subscriptionId];
341
+ return {
342
+ ...prev,
343
+ entries: {
344
+ ...prev.entries,
345
+ [key]: {
346
+ ...entry,
347
+ subscribers
348
+ }
349
+ }
350
+ };
351
+ }, removeSubscriber$1 = (key, subscriptionId) => (prev) => {
352
+ let entry = prev.entries[key];
353
+ if (!entry) return prev;
354
+ let subscribers = entry.subscribers.filter((id) => id !== subscriptionId);
355
+ if (!subscribers.length) {
356
+ let pendingCreates = { ...prev.pendingCreates }, pendingTransactions = { ...prev.pendingTransactions };
357
+ for (let commentId of Object.keys(entry.comments ?? {})) delete pendingCreates[commentId], delete pendingTransactions[commentId];
358
+ return {
359
+ ...prev,
360
+ entries: omitProperty(prev.entries, key),
361
+ pendingCreates,
362
+ pendingTransactions
363
+ };
364
+ }
365
+ return {
366
+ ...prev,
367
+ entries: {
368
+ ...prev.entries,
369
+ [key]: {
370
+ ...entry,
371
+ subscribers
372
+ }
373
+ }
374
+ };
375
+ }, setComments = (key, comments) => (prev) => {
376
+ let entry = prev.entries[key];
377
+ if (!entry) return prev;
378
+ let byId = Object.fromEntries(comments.map((comment) => [comment._id, comment])), pendingCreates = { ...prev.pendingCreates };
379
+ for (let [commentId, localComment] of Object.entries(entry.comments ?? {})) Object.hasOwn(byId, commentId) ? delete pendingCreates[commentId] : (Object.hasOwn(pendingCreates, commentId) || localComment._state) && (byId[commentId] = localComment);
380
+ return {
381
+ ...prev,
382
+ entries: {
383
+ ...prev.entries,
384
+ [key]: {
385
+ ...entry,
386
+ comments: byId,
387
+ error: void 0
388
+ }
389
+ },
390
+ pendingCreates
391
+ };
392
+ }, setCommentsError = (key, error) => (prev) => {
393
+ let entry = prev.entries[key];
394
+ return entry ? {
395
+ ...prev,
396
+ entries: {
397
+ ...prev.entries,
398
+ [key]: {
399
+ ...entry,
400
+ error
401
+ }
402
+ }
403
+ } : prev;
404
+ }, receiveComment = (key, comment) => (prev) => {
405
+ let entry = prev.entries[key], pendingCreates = omitProperty(prev.pendingCreates, comment._id);
406
+ return entry ? {
407
+ ...prev,
408
+ pendingCreates,
409
+ entries: {
410
+ ...prev.entries,
411
+ [key]: {
412
+ ...entry,
413
+ comments: {
414
+ ...entry.comments,
415
+ [comment._id]: comment
416
+ }
417
+ }
418
+ }
419
+ } : {
420
+ ...prev,
421
+ pendingCreates
422
+ };
423
+ };
424
+ /** A create that already failed once is being retried, not written fresh. */
425
+ function isRetryingCreate(existing) {
426
+ let state = existing?._state?.type;
427
+ return state === "createError" || state === "createRetrying";
428
+ }
429
+ function mergeOptimisticComment(existing, comment) {
430
+ return {
431
+ ...existing,
432
+ ...comment,
433
+ _createdAt: comment._createdAt ?? existing?._createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
434
+ _rev: comment._rev ?? existing?._rev ?? "",
435
+ ...isRetryingCreate(existing) ? { _state: { type: "createRetrying" } } : {}
436
+ };
437
+ }
438
+ const addComment = (key, comment) => (prev) => {
439
+ let entry = prev.entries[key];
440
+ if (!entry) return prev;
441
+ let existing = entry.comments && Object.hasOwn(entry.comments, comment._id) ? entry.comments[comment._id] : void 0;
442
+ return {
443
+ ...prev,
444
+ pendingCreates: {
445
+ ...prev.pendingCreates,
446
+ [comment._id]: !0
447
+ },
448
+ entries: {
449
+ ...prev.entries,
450
+ [key]: {
451
+ ...entry,
452
+ comments: {
453
+ ...entry.comments,
454
+ [comment._id]: mergeOptimisticComment(existing, comment)
455
+ }
456
+ }
457
+ }
458
+ };
459
+ }, applyCommentUpdate = (commentId, patch) => (prev) => {
460
+ let entries = { ...prev.entries }, changed = !1;
461
+ for (let [key, entry] of Object.entries(prev.entries)) {
462
+ let comment = entry?.comments?.[commentId];
463
+ !entry || !comment || (changed = !0, entries[key] = {
464
+ ...entry,
465
+ comments: {
466
+ ...entry.comments,
467
+ [commentId]: {
468
+ ...comment,
469
+ ...patch
470
+ }
471
+ }
472
+ });
473
+ }
474
+ return changed ? {
475
+ ...prev,
476
+ entries
477
+ } : prev;
478
+ }, removeCommentById = (commentId) => (prev) => {
479
+ let entries = { ...prev.entries }, removedIds = /* @__PURE__ */ new Set(), changed = !1;
480
+ for (let [key, entry] of Object.entries(prev.entries)) {
481
+ if (!entry?.comments) continue;
482
+ let remaining = Object.fromEntries(Object.entries(entry.comments).filter(([id, comment]) => {
483
+ let keep = id !== commentId && comment.parentCommentId !== commentId;
484
+ return keep || removedIds.add(id), keep;
485
+ }));
486
+ Object.keys(remaining).length !== Object.keys(entry.comments).length && (changed = !0, entries[key] = {
487
+ ...entry,
488
+ comments: remaining
489
+ });
490
+ }
491
+ if (!changed) return prev;
492
+ let pendingCreates = { ...prev.pendingCreates }, pendingTransactions = { ...prev.pendingTransactions };
493
+ for (let id of removedIds) delete pendingCreates[id], delete pendingTransactions[id];
494
+ return {
495
+ ...prev,
496
+ entries,
497
+ pendingCreates,
498
+ pendingTransactions
499
+ };
500
+ }, setPendingTransaction = (commentId, transactionId) => (prev) => ({
501
+ ...prev,
502
+ pendingTransactions: {
503
+ ...prev.pendingTransactions,
504
+ [commentId]: transactionId
505
+ }
506
+ }), clearPendingTransaction = (commentId) => (prev) => ({
507
+ ...prev,
508
+ pendingTransactions: omitProperty(prev.pendingTransactions, commentId)
509
+ }), setCommentCreateError = (commentId, error) => (prev) => {
510
+ if (!Object.hasOwn(prev.pendingCreates, commentId)) return prev;
511
+ let next = applyCommentUpdate(commentId, { _state: {
512
+ type: "createError",
513
+ error
514
+ } })(prev);
515
+ return {
516
+ ...next,
517
+ pendingCreates: omitProperty(next.pendingCreates, commentId)
518
+ };
519
+ }, rollbackCommentUpdate = (commentId, transactionId, previous) => (prev) => {
520
+ if (!Object.hasOwn(prev.pendingTransactions, commentId) || prev.pendingTransactions[commentId] !== transactionId) return prev;
521
+ let entries = Object.fromEntries(Object.entries(prev.entries).map(([key, entry]) => entry?.comments?.[commentId] ? [key, {
522
+ ...entry,
523
+ comments: {
524
+ ...entry.comments,
525
+ [commentId]: previous
526
+ }
527
+ }] : [key, entry]));
528
+ return {
529
+ ...prev,
530
+ entries,
531
+ pendingTransactions: omitProperty(prev.pendingTransactions, commentId)
532
+ };
533
+ }, restoreComments = (removed) => (prev) => {
534
+ let entries = { ...prev.entries }, changed = !1;
535
+ for (let { key, comments } of removed) {
536
+ let entry = entries[key];
537
+ if (!entry) continue;
538
+ let missing = comments.filter((comment) => !Object.hasOwn(entry.comments ?? {}, comment._id));
539
+ missing.length && (changed = !0, entries[key] = {
540
+ ...entry,
541
+ comments: Object.fromEntries([...Object.entries(entry.comments ?? {}), ...missing.map((comment) => [comment._id, comment])])
542
+ });
543
+ }
544
+ return changed ? {
545
+ ...prev,
546
+ entries
547
+ } : prev;
548
+ };
549
+ /**
550
+ * A stable string standing for one set of read options.
551
+ *
552
+ * Only React needs this: it holds one state source steady across renders and
553
+ * defers swapping to a new one while the previous list is still on screen.
554
+ * `fieldPath` is normalised on the way in, so a path array and the equivalent
555
+ * string address the same list.
556
+ *
557
+ * @internal
558
+ */
559
+ function getCommentsOptionsKey(options) {
560
+ return JSON.stringify({
561
+ documentId: options.documentId,
562
+ documentType: options.documentType,
563
+ projectId: options.projectId,
564
+ dataset: options.dataset,
565
+ resource: options.resource,
566
+ perspective: options.perspective,
567
+ fieldPath: options.fieldPath === void 0 ? void 0 : toCommentFieldPath(options.fieldPath),
568
+ status: options.status
569
+ });
570
+ }
571
+ /** @internal */
572
+ function parseCommentsOptionsKey(key) {
573
+ return JSON.parse(key);
574
+ }
575
+ /**
576
+ * Which comment list an option set addresses.
577
+ *
578
+ * Comments hang off the published id, so a draft and its published document
579
+ * share one list. A release keeps its own.
580
+ */
581
+ function toCommentsKeyParts(instance, options) {
582
+ let perspective = options.perspective ?? instance.config.perspective;
583
+ return {
584
+ documentId: getPublishedId(DocumentId(options.documentId)),
585
+ ...isReleasePerspective(perspective) ? { documentVersionId: perspective.releaseName } : {}
586
+ };
587
+ }
588
+ function applyEvent(state, key, event) {
589
+ switch (event.type) {
590
+ case "snapshot":
591
+ state.set("setComments", setComments(key, event.comments));
592
+ return;
593
+ case "appear":
594
+ state.set("receiveComment", receiveComment(key, event.comment));
595
+ return;
596
+ case "disappear":
597
+ state.set("removeComment", removeCommentById(event.commentId));
598
+ return;
599
+ case "error":
600
+ state.set("setCommentsError", setCommentsError(key, event.error));
601
+ return;
602
+ case "update": {
603
+ let pending = state.get().pendingTransactions[event.comment._id];
604
+ if (pending && pending !== event.transactionId) return;
605
+ state.set("receiveComment", receiveComment(key, event.comment)), pending && state.set("clearPendingTransaction", clearPendingTransaction(event.comment._id));
606
+ }
607
+ }
608
+ }
609
+ const watchSubscribedDocuments = ({ state, instance, key }) => {
610
+ let client$ = observeAddonDatasetClient(instance, { resource: key.resource });
611
+ return state.observable.pipe(map((current) => new Set(Object.keys(current.entries))), distinctUntilChanged((a, b) => a.size === b.size && Array.from(b).every((entry) => a.has(entry))), startWith(/* @__PURE__ */ new Set()), pairwise(), mergeMap(([previous, current]) => [...Array.from(current).filter((entry) => !previous.has(entry)).map((entry) => ({
612
+ key: entry,
613
+ added: !0
614
+ })), ...Array.from(previous).filter((entry) => !current.has(entry)).map((entry) => ({
615
+ key: entry,
616
+ added: !1
617
+ }))]), groupBy((event) => event.key), mergeMap((group$) => group$.pipe(switchMap((event) => {
618
+ if (!event.added) return EMPTY;
619
+ let { documentId, documentVersionId } = parseCommentsKey(group$.key);
620
+ return client$.pipe(switchMap((client) => client ? observeComments({
621
+ client,
622
+ documentId,
623
+ documentVersionId
624
+ }).pipe(tap((commentsEvent) => applyEvent(state, group$.key, commentsEvent)), catchError((error) => (state.set("setCommentsError", setCommentsError(group$.key, error)), EMPTY))) : (state.set("setComments", setComments(group$.key, [])), EMPTY)));
625
+ })))).subscribe({ error: (error) => state.set("setError", { error }) });
626
+ }, commentsStore = defineStore({
627
+ name: "Comments",
628
+ getInitialState: () => ({
629
+ entries: {},
630
+ pendingCreates: {},
631
+ pendingTransactions: {}
632
+ }),
633
+ initialize: (context) => {
634
+ let subscription = watchSubscribedDocuments(context);
635
+ return () => subscription.unsubscribe();
636
+ }
637
+ }), normalizedCache = /* @__PURE__ */ new WeakMap(), filteredCache = /* @__PURE__ */ new WeakMap(), threadCache = /* @__PURE__ */ new WeakMap();
638
+ /**
639
+ * The stored map turned into what consumers see, newest first.
640
+ *
641
+ * Cached alongside the filters below rather than done per read, so the
642
+ * normalised objects keep their identity for as long as the stored map does.
643
+ */
644
+ function normalizeAll(commentsById) {
645
+ let cached = normalizedCache.get(commentsById);
646
+ if (cached) return cached;
647
+ let normalized = Object.values(commentsById).map(normalizeComment).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
648
+ return normalizedCache.set(commentsById, normalized), normalized;
649
+ }
650
+ function filterComments(all, fieldPath, status) {
651
+ let byFilter = filteredCache.get(all);
652
+ byFilter || (byFilter = /* @__PURE__ */ new Map(), filteredCache.set(all, byFilter));
653
+ let cacheKey = `${fieldPath ?? "\0"}|${status ?? ""}`, cached = byFilter.get(cacheKey);
654
+ if (cached) return cached;
655
+ let filtered = all.filter((comment) => status && comment.status !== status ? !1 : fieldPath === void 0 || comment.fieldPath === fieldPath);
656
+ return byFilter.set(cacheKey, filtered), filtered;
657
+ }
658
+ function selectComments({ state, instance }, options) {
659
+ if (state.error) throw state.error;
660
+ let entry = state.entries[getCommentsKey(toCommentsKeyParts(instance, options))];
661
+ if (entry?.error) throw entry.error;
662
+ if (entry?.comments) return filterComments(normalizeAll(entry.comments), options.fieldPath === void 0 ? void 0 : toCommentFieldPath(options.fieldPath), options.status);
663
+ }
664
+ function selectCommentThreads(context, options) {
665
+ let comments = selectComments(context, {
666
+ ...options,
667
+ fieldPath: void 0,
668
+ status: void 0
669
+ });
670
+ if (comments === void 0) return;
671
+ let byFilter = threadCache.get(comments);
672
+ byFilter || (byFilter = /* @__PURE__ */ new Map(), threadCache.set(comments, byFilter));
673
+ let fieldPath = options.fieldPath === void 0 ? void 0 : toCommentFieldPath(options.fieldPath), cacheKey = `${fieldPath ?? "\0"}|${options.status ?? ""}`, cached = byFilter.get(cacheKey);
674
+ if (cached) return cached;
675
+ let threads = buildCommentThreads(comments).filter((thread) => options.status && thread.parentComment.status !== options.status ? !1 : fieldPath === void 0 || thread.fieldPath === fieldPath);
676
+ return byFilter.set(cacheKey, threads), threads;
677
+ }
678
+ /**
679
+ * Every comment on a document, newest first.
680
+ *
681
+ * `undefined` until the first snapshot arrives. Replies are included; use
682
+ * {@link getCommentThreadsState} to read them grouped.
683
+ *
684
+ * @beta
685
+ */
686
+ const getCommentsState = bindActionByResource(commentsStore, createStateSourceAction({
687
+ selector: selectComments,
688
+ onSubscribe: ({ state, instance }, options) => {
689
+ let key = getCommentsKey(toCommentsKeyParts(instance, options)), subscriptionId = insecureRandomId();
690
+ return state.set("addSubscriber", addSubscriber$1(key, subscriptionId)), () => {
691
+ setCleanupTimeout(() => state.set("removeSubscriber", removeSubscriber$1(key, subscriptionId)), COMMENTS_STATE_CLEAR_DELAY);
692
+ };
693
+ }
694
+ })), getCommentThreadsState = bindActionByResource(commentsStore, createStateSourceAction({
695
+ selector: selectCommentThreads,
696
+ onSubscribe: ({ state, instance }, options) => {
697
+ let key = getCommentsKey(toCommentsKeyParts(instance, options)), subscriptionId = insecureRandomId();
698
+ return state.set("addSubscriber", addSubscriber$1(key, subscriptionId)), () => {
699
+ setCleanupTimeout(() => state.set("removeSubscriber", removeSubscriber$1(key, subscriptionId)), COMMENTS_STATE_CLEAR_DELAY);
700
+ };
701
+ }
702
+ })), resolveComments = bindActionByResource(commentsStore, ({ state, instance }, { signal, ...options }) => resolveList(state, instance, options, signal, getCommentsState)), resolveCommentThreads = bindActionByResource(commentsStore, ({ state, instance }, { signal, ...options }) => resolveList(state, instance, options, signal, getCommentThreadsState));
703
+ function resolveList(state, instance, options, signal, getState) {
704
+ let key = getCommentsKey(toCommentsKeyParts(instance, options)), { getCurrent } = getState(instance, options), subscriptionId = insecureRandomId();
705
+ state.set("addSubscriber", addSubscriber$1(key, subscriptionId));
706
+ let release = () => state.set("removeSubscriber", removeSubscriber$1(key, subscriptionId)), aborted$ = signal ? new Observable((observer) => {
707
+ let listener = () => {
708
+ release(), observer.error(new DOMException("The operation was aborted.", "AbortError"));
709
+ };
710
+ return signal.addEventListener("abort", listener), () => signal.removeEventListener("abort", listener);
711
+ }) : NEVER, resolved$ = state.observable.pipe(map(() => getCurrent()), first((value) => value !== void 0)), promise = firstValueFrom(race([resolved$, aborted$])), releaseLater = () => setCleanupTimeout(release, COMMENTS_STATE_CLEAR_DELAY);
712
+ return promise.then(releaseLater, releaseLater), promise;
713
+ }
714
+ const PROJECT_API_VERSION = "2025-07-18", USERS_STATE_CLEAR_DELAY = 5e3, getUsersKey = (instance, { resourceType, organizationId, batchSize = 100, projectId = instance.config.projectId, userId } = {}) => JSON.stringify({
715
+ resourceType,
716
+ organizationId,
717
+ batchSize,
718
+ projectId,
719
+ userId
720
+ }), parseUsersKey = (key) => JSON.parse(key), addSubscription = (subscriptionId, key) => (prev) => {
721
+ let group = prev.users[key], subscriptions = [...group?.subscriptions ?? [], subscriptionId];
722
+ return {
723
+ ...prev,
724
+ users: {
725
+ ...prev.users,
726
+ [key]: {
727
+ ...group,
728
+ subscriptions
729
+ }
730
+ }
731
+ };
732
+ }, removeSubscription = (subscriptionId, key) => (prev) => {
733
+ let group = prev.users[key];
734
+ if (!group) return prev;
735
+ let subscriptions = group.subscriptions.filter((id) => id !== subscriptionId);
736
+ return subscriptions.length ? {
737
+ ...prev,
738
+ users: {
739
+ ...prev.users,
740
+ [key]: {
741
+ ...group,
742
+ subscriptions
743
+ }
744
+ }
745
+ } : {
746
+ ...prev,
747
+ users: omitProperty(prev.users, key)
748
+ };
749
+ }, setUsersData = (key, { data, nextCursor, totalCount }) => (prev) => {
750
+ let group = prev.users[key];
751
+ if (!group) return prev;
752
+ let users = [...group.users ?? [], ...data];
753
+ return {
754
+ ...prev,
755
+ users: {
756
+ ...prev.users,
757
+ [key]: {
758
+ ...group,
759
+ users,
760
+ totalCount,
761
+ nextCursor
762
+ }
763
+ }
764
+ };
765
+ }, updateLastLoadMoreRequest = (timestamp, key) => (prev) => {
766
+ let group = prev.users[key];
767
+ return group ? {
768
+ ...prev,
769
+ users: {
770
+ ...prev.users,
771
+ [key]: {
772
+ ...group,
773
+ lastLoadMoreRequest: timestamp
774
+ }
775
+ }
776
+ } : prev;
777
+ }, setUsersError = (key, error) => (prev) => {
778
+ let group = prev.users[key];
779
+ return group ? {
780
+ ...prev,
781
+ users: {
782
+ ...prev.users,
783
+ [key]: {
784
+ ...group,
785
+ error
786
+ }
787
+ }
788
+ } : prev;
789
+ }, cancelRequest = (key) => (prev) => {
790
+ let group = prev.users[key];
791
+ return !group || group.subscriptions.length ? prev : {
792
+ ...prev,
793
+ users: omitProperty(prev.users, key)
794
+ };
795
+ }, initializeRequest = (key) => (prev) => prev.users[key] ? prev : {
796
+ ...prev,
797
+ users: {
798
+ ...prev.users,
799
+ [key]: { subscriptions: [] }
800
+ }
801
+ };
802
+ /**
803
+ * Emits Live Content API messages for a dataset so consumers can invalidate
804
+ * fetched data via sync tags.
805
+ *
806
+ * All consumers observe the same underlying EventSource connection:
807
+ * `@sanity/client` caches the live-events stream per URL and connection
808
+ * options, and every subscription made through this function uses the same
809
+ * API version and request tag.
810
+ *
811
+ * @internal
812
+ */
813
+ function observeLiveEvents(instance, { resource, onCorsError }) {
814
+ return getClientState(instance, {
815
+ apiVersion: "v2025-05-06",
816
+ resource
817
+ }).observable.pipe(switchMap((client) => defer(() => client.live.events({
818
+ includeDrafts: !!client.config().token,
819
+ tag: "live-events"
820
+ })).pipe(catchError((error) => {
821
+ if (error instanceof CorsOriginError) return onCorsError(error), EMPTY;
822
+ if (error instanceof DisconnectError || error instanceof ConnectionFailedError && typeof error.status == "number" && error.status >= 400 && error.status < 500) return EMPTY;
823
+ throw error;
824
+ }), retry({ delay: 1e3 }))), filter((e) => e.type === "message"));
825
+ }
826
+ /**
827
+ * Emits every release document (including archived and published), refetching
828
+ * whenever a Live Content API event matches the query's sync tags.
829
+ *
830
+ * This fetches directly through the client store — deliberately not via the
831
+ * query store — because release data is itself an input to query perspective
832
+ * resolution (`getPerspectiveState`); fetching through the query store would
833
+ * make the two stores mutually dependent.
834
+ *
835
+ * @internal
836
+ */
837
+ function observeReleases(instance, { resource, onCorsError }) {
838
+ return getClientState(instance, {
839
+ apiVersion: "v2025-05-06",
840
+ resource
841
+ }).observable.pipe(switchMap((client) => {
842
+ let syncTags$ = new BehaviorSubject(void 0);
843
+ return combineLatest([observeLiveEvents(instance, {
844
+ resource,
845
+ onCorsError
846
+ }).pipe(startWith(void 0)), syncTags$]).pipe(filter(([message, syncTags]) => message === void 0 || message.tags.some((tag) => syncTags?.includes(tag))), map(([message]) => message?.id), distinctUntilChanged()).pipe(switchMap((lastLiveEventId) => client.observable.fetch("releases::all()", {}, {
847
+ perspective: "raw",
848
+ filterResponse: !1,
849
+ returnQuery: !1,
850
+ lastLiveEventId,
851
+ tag: "releases"
852
+ })), tap((response) => {
853
+ syncTags$.next(response.syncTags);
854
+ }), map((response) => response.result));
855
+ }), startWith(void 0));
856
+ }
857
+ function sortReleases(releases = []) {
858
+ return [...releases].sort((a, b) => {
859
+ let aType = a.metadata?.releaseType, bType = b.metadata?.releaseType;
860
+ if (aType === "undecided" && bType !== "undecided") return -1;
861
+ if (aType !== "undecided" && bType === "undecided") return 1;
862
+ if (aType === "undecided" && bType === "undecided") return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime();
863
+ if (aType === "scheduled" && bType === "scheduled") {
864
+ let aPublishAt = a.publishAt || a.metadata?.intendedPublishAt;
865
+ if (!aPublishAt) return 1;
866
+ let bPublishAt = b.publishAt || b.metadata?.intendedPublishAt;
867
+ return bPublishAt ? new Date(bPublishAt).getTime() - new Date(aPublishAt).getTime() : -1;
868
+ }
869
+ return aType === "asap" && bType !== "asap" ? 1 : aType !== "asap" && bType === "asap" ? -1 : aType === "asap" && bType === "asap" ? new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime() : 0;
870
+ });
871
+ }
872
+ const ARCHIVED_RELEASE_STATES = ["archived", "published"], STABLE_EMPTY_RELEASES = [], releasesStore = defineStore({
873
+ name: "Releases",
874
+ getInitialState: () => ({
875
+ activeReleases: void 0,
876
+ allReleases: void 0
877
+ }),
878
+ initialize: (context) => {
879
+ let subscription = subscribeToReleases(context);
880
+ return () => subscription.unsubscribe();
881
+ }
882
+ }), _getActiveReleasesState = bindActionByResource(releasesStore, createStateSourceAction({ selector: ({ state }) => {
883
+ if (state.error) throw state.error;
884
+ return state.activeReleases;
885
+ } })), getActiveReleasesState = (instance, options) => _getActiveReleasesState(instance, options ?? {}), _getAllReleasesState = bindActionByResource(releasesStore, createStateSourceAction({ selector: ({ state }) => {
886
+ if (state.error) throw state.error;
887
+ return state.allReleases;
888
+ } })), getAllReleasesState = (instance, options) => _getAllReleasesState(instance, options ?? {}), subscribeToReleases = ({ instance, state, key: { resource } }) => observeReleases(instance, {
889
+ resource,
890
+ onCorsError: (error) => state.set("setError", { error })
891
+ }).pipe(map((releases) => {
892
+ let sorted = sortReleases(releases ?? STABLE_EMPTY_RELEASES).reverse();
893
+ state.set("setReleases", {
894
+ allReleases: sorted,
895
+ activeReleases: sorted.filter((release) => !ARCHIVED_RELEASE_STATES.includes(release.state))
896
+ });
897
+ })).subscribe({ error: (error) => state.set("setError", { error }) }), optionsCache = /* @__PURE__ */ new Map(), selectInstancePerspective = (context, _) => context.instance.config.perspective, selectActiveReleases = (context) => context.state.activeReleases, selectOptions = (_context, options) => options, memoizedOptionsSelector = createSelector([selectActiveReleases, selectOptions], (activeReleases, options) => {
898
+ if (!options || !activeReleases) return options;
899
+ let releaseIds = activeReleases.map((release) => release._id).join(","), nestedCache = optionsCache.get(releaseIds);
900
+ nestedCache || (nestedCache = /* @__PURE__ */ new Map(), optionsCache.set(releaseIds, nestedCache));
901
+ let optionsKey = JSON.stringify(options), cachedOptions = nestedCache.get(optionsKey);
902
+ return cachedOptions || (cachedOptions = options, nestedCache.set(optionsKey, cachedOptions)), cachedOptions;
903
+ }), _getPerspectiveStateSelector = createStateSourceAction({ selector: createSelector([
904
+ selectInstancePerspective,
905
+ selectActiveReleases,
906
+ memoizedOptionsSelector
907
+ ], (instancePerspective, activeReleases, memoizedOptions) => {
908
+ let perspective = memoizedOptions?.perspective ?? instancePerspective ?? "drafts";
909
+ if (!isReleasePerspective(perspective)) return perspective;
910
+ if (!activeReleases || activeReleases.length === 0) return;
911
+ let releaseNames = sortReleases(activeReleases).map((release) => release.name), index = releaseNames.findIndex((name) => name === perspective.releaseName);
912
+ if (index < 0) throw Error(`Release "${perspective.releaseName}" not found in active releases`);
913
+ return ["drafts", ...releaseNames.slice(0, index + 1)].filter((name) => !perspective.excludedPerspectives?.includes(name)).reverse();
914
+ }) }), _boundGetPerspectiveState = bindActionByResource(releasesStore, _getPerspectiveStateSelector), getPerspectiveState = (instance, ...rest) => _boundGetPerspectiveState(instance, ...rest.length ? rest : [{}]), QUERY_STATE_CLEAR_DELAY = 1e3, setQueryError = (key, error) => (prev) => {
915
+ let prevQuery = prev.queries[key];
916
+ return prevQuery ? {
917
+ ...prev,
918
+ queries: {
919
+ ...prev.queries,
920
+ [key]: {
921
+ ...prevQuery,
922
+ error
923
+ }
924
+ }
925
+ } : prev;
926
+ }, setQueryData = (key, result, syncTags) => (prev) => {
927
+ let prevQuery = prev.queries[key];
928
+ return prevQuery ? {
929
+ ...prev,
930
+ queries: {
931
+ ...prev.queries,
932
+ [key]: {
933
+ ...prevQuery,
934
+ result: result ?? null,
935
+ syncTags
936
+ }
937
+ }
938
+ } : prev;
939
+ }, setLastLiveEventId = (key, lastLiveEventId) => (prev) => {
940
+ let prevQuery = prev.queries[key];
941
+ return prevQuery ? {
942
+ ...prev,
943
+ queries: {
944
+ ...prev.queries,
945
+ [key]: {
946
+ ...prevQuery,
947
+ lastLiveEventId
948
+ }
949
+ }
950
+ } : prev;
951
+ }, addSubscriber = (key, subscriptionId) => (prev) => {
952
+ let prevQuery = prev.queries[key], subscribers = [...prevQuery?.subscribers ?? [], subscriptionId];
953
+ return {
954
+ ...prev,
955
+ queries: {
956
+ ...prev.queries,
957
+ [key]: {
958
+ ...prevQuery,
959
+ subscribers
960
+ }
961
+ }
962
+ };
963
+ }, removeSubscriber = (key, subscriptionId) => (prev) => {
964
+ let prevQuery = prev.queries[key];
965
+ if (!prevQuery) return prev;
966
+ let subscribers = prevQuery.subscribers.filter((id) => id !== subscriptionId);
967
+ return subscribers.length ? {
968
+ ...prev,
969
+ queries: {
970
+ ...prev.queries,
971
+ [key]: {
972
+ ...prevQuery,
973
+ subscribers
974
+ }
975
+ }
976
+ } : {
977
+ ...prev,
978
+ queries: omitProperty(prev.queries, key)
979
+ };
980
+ }, EMPTY_ARRAY = [], getQueryKey = (options) => JSON.stringify(options), parseQueryKey = (key) => JSON.parse(key);
981
+ /**
982
+ * Ensures the query key includes an effective perspective so that
983
+ * implicit differences (e.g. different instance.config.perspective)
984
+ * don't collide in the dataset-scoped store.
985
+ *
986
+ * Since perspectives are unique, we can depend on the release stacks
987
+ * to be correct when we retrieve the results.
988
+ *
989
+ */
990
+ function normalizeOptionsWithPerspective(instance, options) {
991
+ if (options.perspective !== void 0) return options;
992
+ let instancePerspective = instance.config.perspective;
993
+ return {
994
+ ...options,
995
+ perspective: instancePerspective === void 0 ? "drafts" : instancePerspective
996
+ };
997
+ }
998
+ const queryStore = defineStore({
999
+ name: "QueryStore",
1000
+ getInitialState: () => ({ queries: {} }),
1001
+ initialize(context) {
1002
+ let subscriptions = [listenForNewSubscribersAndFetch(context), listenToLiveClientAndSetLastLiveEventIds(context)];
1003
+ return () => {
1004
+ for (let subscription of subscriptions) subscription.unsubscribe();
1005
+ };
1006
+ }
1007
+ }), errorHandler = (state) => (error) => state.set("setError", { error }), listenForNewSubscribersAndFetch = ({ state, instance }) => state.observable.pipe(map((s) => new Set(Object.keys(s.queries))), distinctUntilChanged((curr, next) => curr.size === next.size && Array.from(next).every((i) => curr.has(i))), startWith(/* @__PURE__ */ new Set()), pairwise(), mergeMap(([curr, next]) => {
1008
+ let added = Array.from(next).filter((i) => !curr.has(i)), removed = Array.from(curr).filter((i) => !next.has(i));
1009
+ return [...added.map((key) => ({
1010
+ key,
1011
+ added: !0
1012
+ })), ...removed.map((key) => ({
1013
+ key,
1014
+ added: !1
1015
+ }))];
1016
+ }), groupBy((i) => i.key), mergeMap((group$) => group$.pipe(switchMap((e) => {
1017
+ if (!e.added) return EMPTY;
1018
+ let lastLiveEventId$ = state.observable.pipe(map((s) => s.queries[group$.key]?.lastLiveEventId), distinctUntilChanged()), { query, params, projectId, dataset, tag, resource, perspective: perspectiveFromOptions, ...restOptions } = parseQueryKey(group$.key), perspective$ = isReleasePerspective(perspectiveFromOptions) ? getPerspectiveState(instance, {
1019
+ perspective: perspectiveFromOptions,
1020
+ resource
1021
+ }).observable.pipe(filter(Boolean)) : of(perspectiveFromOptions ?? "drafts"), client$ = getClientState(instance, {
1022
+ apiVersion: "v2025-05-06",
1023
+ projectId,
1024
+ dataset,
1025
+ resource
1026
+ }).observable;
1027
+ return combineLatest({
1028
+ lastLiveEventId: lastLiveEventId$,
1029
+ client: client$,
1030
+ perspective: perspective$
1031
+ }).pipe(switchMap(({ lastLiveEventId, client, perspective }) => client.observable.fetch(query, params, {
1032
+ ...restOptions,
1033
+ perspective,
1034
+ filterResponse: !1,
1035
+ returnQuery: !1,
1036
+ lastLiveEventId,
1037
+ tag
1038
+ })), tap(({ result, syncTags }) => {
1039
+ state.set("setQueryData", setQueryData(group$.key, result, syncTags));
1040
+ }), catchError((error) => (state.set("setQueryError", setQueryError(group$.key, error)), EMPTY)));
1041
+ })))).subscribe({ error: errorHandler(state) }), listenToLiveClientAndSetLastLiveEventIds = ({ state, instance, key: { resource } }) => {
1042
+ let liveMessages$ = observeLiveEvents(instance, {
1043
+ resource,
1044
+ onCorsError: (error) => state.set("setError", { error })
1045
+ }).pipe(share());
1046
+ return state.observable.pipe(mergeMap((s) => Object.entries(s.queries)), groupBy(([key]) => key), mergeMap((group$) => {
1047
+ let syncTags$ = group$.pipe(map(([, queryState]) => queryState), map((i) => i?.syncTags ?? EMPTY_ARRAY), distinctUntilChanged());
1048
+ return combineLatest([liveMessages$, syncTags$]).pipe(filter(([message, syncTags]) => message.tags.some((tag) => syncTags.includes(tag))), tap(([message]) => {
1049
+ state.set("setLastLiveEventId", setLastLiveEventId(group$.key, message.id));
1050
+ }));
1051
+ })).subscribe({ error: errorHandler(state) });
1052
+ };
1053
+ /** @beta */
1054
+ function getQueryState(...args) {
1055
+ return _getQueryState(...args);
1056
+ }
1057
+ const _getQueryState = bindActionByResource(queryStore, createStateSourceAction({
1058
+ selector: ({ state, instance }, options) => {
1059
+ if (state.error) throw state.error;
1060
+ let key = getQueryKey(normalizeOptionsWithPerspective(instance, options)), queryState = state.queries[key];
1061
+ if (queryState?.error) throw queryState.error;
1062
+ return queryState?.result;
1063
+ },
1064
+ onSubscribe: ({ state, instance }, options) => {
1065
+ let subscriptionId = insecureRandomId(), key = getQueryKey(normalizeOptionsWithPerspective(instance, options));
1066
+ return state.set("addSubscriber", addSubscriber(key, subscriptionId)), () => {
1067
+ setCleanupTimeout(() => state.set("removeSubscriber", removeSubscriber(key, subscriptionId)), QUERY_STATE_CLEAR_DELAY);
1068
+ };
1069
+ }
1070
+ }));
1071
+ /** @beta */
1072
+ function resolveQuery(...args) {
1073
+ return _resolveQuery(...args);
1074
+ }
1075
+ const _resolveQuery = bindActionByResource(queryStore, ({ state, instance }, { signal, ...options }) => {
1076
+ let normalized = normalizeOptionsWithPerspective(instance, options), { getCurrent } = getQueryState(instance, normalized), key = getQueryKey(normalized), subscriptionId = insecureRandomId();
1077
+ state.set("addSubscriber", addSubscriber(key, subscriptionId));
1078
+ let aborted$ = signal ? new Observable((observer) => {
1079
+ let cleanup = () => {
1080
+ signal.removeEventListener("abort", listener);
1081
+ }, listener = () => {
1082
+ observer.error(new DOMException("The operation was aborted.", "AbortError")), observer.complete(), cleanup();
1083
+ };
1084
+ return signal.addEventListener("abort", listener), cleanup;
1085
+ }).pipe(catchError((error) => {
1086
+ throw error instanceof Error && error.name === "AbortError" && state.set("removeSubscriber", removeSubscriber(key, subscriptionId)), error;
1087
+ })) : NEVER, resolved$ = state.observable.pipe(map(getCurrent), first((i) => i !== void 0)), promise = firstValueFrom(race([resolved$, aborted$])), releaseSubscriber = () => {
1088
+ setCleanupTimeout(() => {
1089
+ state.set("removeSubscriber", removeSubscriber(key, subscriptionId));
1090
+ }, QUERY_STATE_CLEAR_DELAY);
1091
+ };
1092
+ return promise.then(releaseSubscriber, releaseSubscriber), promise;
1093
+ }), TITLE_CANDIDATES = [
1094
+ "title",
1095
+ "name",
1096
+ "label",
1097
+ "heading",
1098
+ "header",
1099
+ "caption"
1100
+ ], SUBTITLE_CANDIDATES = [
1101
+ "description",
1102
+ "subtitle",
1103
+ ...TITLE_CANDIDATES
1104
+ ], PREVIEW_PROJECTION = `{
1105
+ // Get all potential title fields
1106
+ "titleCandidates": {
1107
+ ${TITLE_CANDIDATES.map((field) => `"${field}": ${field}`).join(",\n ")}
1108
+ },
1109
+ // Get all potential subtitle fields
1110
+ "subtitleCandidates": {
1111
+ ${SUBTITLE_CANDIDATES.map((field) => `"${field}": ${field}`).join(",\n ")}
1112
+ },
1113
+ "media": coalesce(
1114
+ select(
1115
+ defined(asset) => {"type": "image-asset", "_ref": asset._ref},
1116
+ defined(image.asset) => {"type": "image-asset", "_ref": image.asset._ref},
1117
+ defined(mainImage.asset) => {"type": "image-asset", "_ref": mainImage.asset._ref},
1118
+ null
1119
+ )
1120
+ ),
1121
+ _type,
1122
+ _id,
1123
+ _updatedAt
1124
+ }`;
1125
+ /**
1126
+ * Checks if the provided value has `_ref` property that is a string and starts with `image-`
1127
+ */
1128
+ function hasImageRef(value) {
1129
+ return isObject(value) && "_ref" in value && typeof value._ref == "string";
1130
+ }
1131
+ /**
1132
+ * Normalizes a media asset to a preview value.
1133
+ * Adds a url to a media asset reference using `@sanity/image-url`.
1134
+ *
1135
+ * @internal
1136
+ */
1137
+ function normalizeMedia(media, client) {
1138
+ if (!media || !hasImageRef(media)) return null;
1139
+ let url = createImageUrlBuilder(client).image({ _ref: media._ref }).url();
1140
+ return {
1141
+ type: "image-asset",
1142
+ _ref: media._ref,
1143
+ url
1144
+ };
1145
+ }
1146
+ /**
1147
+ * Finds a single field value from a set of candidates based on a priority list of field names.
1148
+ * Returns the first non-empty string value found from the candidates matching the priority list order.
1149
+ *
1150
+ * @internal
1151
+ */
1152
+ function findFirstDefined(fieldsToSearch, candidates, exclude) {
1153
+ if (candidates) for (let field of fieldsToSearch) {
1154
+ let value = candidates[field];
1155
+ if (typeof value == "string" && value.trim() !== "" && value !== exclude) return value;
1156
+ }
1157
+ }
1158
+ /**
1159
+ * Transforms a projection result (with titleCandidates, subtitleCandidates, media)
1160
+ * into a PreviewValue (with title, subtitle, media).
1161
+ *
1162
+ * @param projectionResult - The raw projection result from GROQ
1163
+ * @param instance - The Sanity instance to use for client configuration
1164
+ * @param resource - Data resource for the preview
1165
+ * @internal
1166
+ */
1167
+ function transformProjectionToPreview(instance, projectionResult, resource) {
1168
+ let title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates), subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title), client = getClient(instance, {
1169
+ apiVersion: "v2025-05-06",
1170
+ resource
1171
+ });
1172
+ return {
1173
+ title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
1174
+ subtitle: subtitle || void 0,
1175
+ media: normalizeMedia(projectionResult.media, client),
1176
+ ...projectionResult._status && { _status: projectionResult._status }
1177
+ };
1178
+ }
1179
+ const TOKEN_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
1180
+ /**
1181
+ * @internal
1182
+ * Checks if a token starts with the negation character.
1183
+ */
1184
+ function isNegationToken(token) {
1185
+ return token !== void 0 && token.trim().startsWith("-");
1186
+ }
1187
+ /**
1188
+ * @internal
1189
+ * Checks if a token ends with the wildcard character.
1190
+ */
1191
+ function isPrefixToken(token) {
1192
+ return token !== void 0 && token.trim().endsWith("*");
1193
+ }
1194
+ /**
1195
+ * @internal
1196
+ * Checks if a token is enclosed in double quotes.
1197
+ */
1198
+ function isExactMatchToken(token) {
1199
+ return !!token && token.length >= 2 && token.startsWith("\"") && token.endsWith("\"");
1200
+ }
1201
+ /**
1202
+ * Creates a GROQ search filter string (`[@] match text::query("...")`)
1203
+ * from a raw search query string.
1204
+ *
1205
+ * It applies wildcard ('*') logic to the last eligible token and escapes
1206
+ * double quotes within the search term.
1207
+ *
1208
+ * If the input query is empty or only whitespace, it returns an empty string.
1209
+ *
1210
+ * @param query - The raw input search string.
1211
+ * @returns The GROQ search filter string, or an empty string.
1212
+ * @internal
1213
+ */
1214
+ function createGroqSearchFilter(query) {
1215
+ let trimmedQuery = query.trim();
1216
+ if (!trimmedQuery) return "";
1217
+ let tokens = trimmedQuery.match(TOKEN_REGEX) ?? [], reversedIndex = [...tokens].reverse().findIndex((token) => !isNegationToken(token) && !isExactMatchToken(token)), finalIncrementalTokenIndex = reversedIndex === -1 ? -1 : tokens.length - 1 - reversedIndex, finalIncrementalToken = tokens[finalIncrementalTokenIndex], processedTokens = [...tokens];
1218
+ return finalIncrementalToken !== void 0 && !isPrefixToken(finalIncrementalToken) && processedTokens.splice(finalIncrementalTokenIndex, 1, `${finalIncrementalToken}*`), `[@] match text::query("${processedTokens.join(" ").replace(/"/g, "\\\"")}")`;
1219
+ }
1220
+ export { addComment as A, normalizeComment as B, getCommentThreadsState as C, resolveCommentThreads as D, parseCommentsOptionsKey as E, removeCommentById as F, COMMENTS_API_VERSION as G, assertDatasetResource as H, restoreComments as I, setCleanupTimeout as K, rollbackCommentUpdate as L, clearPendingTransaction as M, getCommentsKey as N, resolveComments as O, receiveComment as P, setCommentCreateError as R, commentsStore as S, getCommentsState as T, getAddonDatasetState as U, toCommentFieldPath as V, provisionAddonDataset as W, setUsersData as _, getQueryState as a, PROJECT_API_VERSION as b, getPerspectiveState as c, addSubscription as d, cancelRequest as f, removeSubscription as g, parseUsersKey as h, getQueryKey as i, applyCommentUpdate as j, toCommentsKeyParts as k, getActiveReleasesState as l, initializeRequest as m, transformProjectionToPreview as n, parseQueryKey as o, getUsersKey as p, insecureRandomId as q, PREVIEW_PROJECTION as r, resolveQuery as s, createGroqSearchFilter as t, getAllReleasesState as u, setUsersError as v, getCommentsOptionsKey as w, USERS_STATE_CLEAR_DELAY as x, updateLastLoadMoreRequest as y, setPendingTransaction as z };
1221
+
1222
+ //# sourceMappingURL=createGroqSearchFilter-B0NdA-EP.js.map