@sanity/sdk 2.19.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 (62) hide show
  1. package/dist/_exports/_internal.d.ts +2 -2
  2. package/dist/_exports/_internal.js +196 -22
  3. package/dist/_exports/_internal.js.map +1 -1
  4. package/dist/authStore-CWTAuieY.js +1072 -0
  5. package/dist/authStore-CWTAuieY.js.map +1 -0
  6. package/dist/clientStore-CMzJ7XhZ.js +168 -0
  7. package/dist/clientStore-CMzJ7XhZ.js.map +1 -0
  8. package/dist/createGroqSearchFilter-B0NdA-EP.js +1222 -0
  9. package/dist/createGroqSearchFilter-B0NdA-EP.js.map +1 -0
  10. package/dist/getEnv-76AjZd_f.js +178 -0
  11. package/dist/getEnv-76AjZd_f.js.map +1 -0
  12. package/dist/index.d.ts +2 -2105
  13. package/dist/index.js +4140 -3870
  14. package/dist/index.js.map +1 -1
  15. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  16. package/dist/telemetryManager-Bd3O8z2S.js +127 -0
  17. package/dist/telemetryManager-Bd3O8z2S.js.map +1 -0
  18. package/dist/utils-BMwA8Kg9.d.ts +3334 -0
  19. package/dist/utils-BMwA8Kg9.d.ts.map +1 -0
  20. package/dist/version-DL2l07HG.js +9 -0
  21. package/dist/version-DL2l07HG.js.map +1 -0
  22. package/package.json +12 -20
  23. package/src/_exports/_internal.ts +1 -0
  24. package/src/_exports/index.ts +30 -0
  25. package/src/comments/addonDatasetStore.test.ts +215 -0
  26. package/src/comments/addonDatasetStore.ts +249 -0
  27. package/src/comments/buildCommentThreads.test.ts +83 -0
  28. package/src/comments/buildCommentThreads.ts +54 -0
  29. package/src/comments/commentActions.test.ts +540 -0
  30. package/src/comments/commentActions.ts +555 -0
  31. package/src/comments/commentFieldPath.test.ts +40 -0
  32. package/src/comments/commentFieldPath.ts +44 -0
  33. package/src/comments/commentsConstants.ts +80 -0
  34. package/src/comments/commentsStore.test.ts +419 -0
  35. package/src/comments/commentsStore.ts +483 -0
  36. package/src/comments/normalizeComment.test.ts +116 -0
  37. package/src/comments/normalizeComment.ts +45 -0
  38. package/src/comments/observeComments.test.ts +193 -0
  39. package/src/comments/observeComments.ts +113 -0
  40. package/src/comments/reducers.test.ts +277 -0
  41. package/src/comments/reducers.ts +316 -0
  42. package/src/comments/types.ts +249 -0
  43. package/src/comments/weakenReferences.test.ts +47 -0
  44. package/src/comments/weakenReferences.ts +29 -0
  45. package/src/document/documentConstants.ts +8 -0
  46. package/src/document/documentStore.test.ts +98 -1
  47. package/src/document/documentStore.ts +36 -5
  48. package/src/document/patchOperations.test.ts +27 -0
  49. package/src/document/patchOperations.ts +10 -1
  50. package/src/document/permissions.test.ts +3 -1
  51. package/src/document/reducers.test.ts +133 -0
  52. package/src/document/reducers.ts +18 -3
  53. package/src/projection/projectionStore.test.ts +0 -8
  54. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -923
  55. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
  56. package/dist/_chunks-es/_internal.js +0 -134
  57. package/dist/_chunks-es/_internal.js.map +0 -1
  58. package/dist/_chunks-es/createGroqSearchFilter.js +0 -1590
  59. package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
  60. package/dist/_chunks-es/telemetryManager.js +0 -93
  61. package/dist/_chunks-es/telemetryManager.js.map +0 -1
  62. package/dist/index.d.ts.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type BaseActionOptions,
3
3
  type BaseMutationOptions,
4
+ ClientError,
4
5
  type FilteredResponseQueryOptions,
5
6
  type ListenEvent,
6
7
  type MultipleActionResult,
@@ -17,7 +18,18 @@ import {diffValue} from '@sanity/diff-patch'
17
18
  import {DocumentId, getDraftId, getPublishedId} from '@sanity/id-utils'
18
19
  import {type Mutation, type SanityDocument} from '@sanity/types'
19
20
  import {evaluate, parse} from 'groq-js'
20
- import {delay, first, firstValueFrom, from, Observable, of, ReplaySubject, Subject} from 'rxjs'
21
+ import {
22
+ defer,
23
+ delay,
24
+ first,
25
+ firstValueFrom,
26
+ from,
27
+ Observable,
28
+ of,
29
+ ReplaySubject,
30
+ Subject,
31
+ throwError,
32
+ } from 'rxjs'
21
33
  import {afterEach, beforeEach, expect, it, vi} from 'vitest'
22
34
 
23
35
  import {getClientState} from '../client/clientStore'
@@ -917,6 +929,89 @@ it('does not send credentials with the dataset ACL request', async () => {
917
929
  expect(aclCall![0]).not.toHaveProperty('withCredentials')
918
930
  })
919
931
 
932
+ it('retries transient dataset ACL fetch failures instead of failing fatally', async () => {
933
+ const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
934
+
935
+ // the first attempt fails like a dropped connection; subsequent attempts succeed
936
+ let aclAttempts = 0
937
+ client.observable.request = vi.fn().mockReturnValue(
938
+ defer(() => {
939
+ aclAttempts++
940
+ if (aclAttempts === 1) return throwError(() => new Error('socket hang up'))
941
+ return of(datasetAcl)
942
+ }),
943
+ )
944
+
945
+ const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
946
+ const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
947
+
948
+ expect(result).toEqual({allowed: true})
949
+ expect(aclAttempts).toBe(2)
950
+
951
+ // the store never entered the fatal error state
952
+ const documentState = getDocumentState(instance, doc)
953
+ expect(() => documentState.getCurrent()).not.toThrow()
954
+ })
955
+
956
+ it('does not retry dataset ACL fetch failures caused by 4xx client errors', async () => {
957
+ const forbidden = new ClientError({
958
+ statusCode: 403,
959
+ headers: {},
960
+ body: {error: 'Forbidden', message: 'Session does not have access to this dataset'},
961
+ })
962
+
963
+ let aclAttempts = 0
964
+ client.observable.request = vi.fn().mockReturnValue(
965
+ defer(() => {
966
+ aclAttempts++
967
+ return throwError(() => forbidden)
968
+ }),
969
+ )
970
+
971
+ const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
972
+ const documentState = getDocumentState(instance, doc)
973
+ const unsubscribe = documentState.subscribe()
974
+
975
+ // the error is fatal: it surfaces through the store instead of being retried
976
+ await vi.waitFor(() => {
977
+ expect(() => documentState.getCurrent()).toThrow(
978
+ 'Forbidden - Session does not have access to this dataset',
979
+ )
980
+ })
981
+ expect(aclAttempts).toBe(1)
982
+
983
+ unsubscribe()
984
+ })
985
+
986
+ it('retries dataset ACL fetch failures caused by 429 rate-limit errors', async () => {
987
+ const rateLimited = new ClientError({
988
+ statusCode: 429,
989
+ headers: {},
990
+ body: {error: 'Too Many Requests', message: 'Rate limit exceeded'},
991
+ })
992
+ const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
993
+
994
+ // the first attempt is rate limited; subsequent attempts succeed
995
+ let aclAttempts = 0
996
+ client.observable.request = vi.fn().mockReturnValue(
997
+ defer(() => {
998
+ aclAttempts++
999
+ if (aclAttempts === 1) return throwError(() => rateLimited)
1000
+ return of(datasetAcl)
1001
+ }),
1002
+ )
1003
+
1004
+ const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
1005
+ const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
1006
+
1007
+ expect(result).toEqual({allowed: true})
1008
+ expect(aclAttempts).toBe(2)
1009
+
1010
+ // the store never entered the fatal error state
1011
+ const documentState = getDocumentState(instance, doc)
1012
+ expect(() => documentState.getCurrent()).not.toThrow()
1013
+ })
1014
+
920
1015
  it('fetches ACL for MediaLibraryResource', async () => {
921
1016
  const mediaLibraryInstance = createSanityInstance({
922
1017
  projectId: 'p',
@@ -1250,6 +1345,8 @@ vi.mock('./documentConstants.ts', async (importOriginal) => {
1250
1345
  DOCUMENT_STATE_CLEAR_DELAY: 25,
1251
1346
  OUT_OF_SYNC_RETRY_BASE_DELAY: 0,
1252
1347
  OUT_OF_SYNC_RETRY_MAX_DELAY: 0,
1348
+ ACL_RETRY_BASE_DELAY: 0,
1349
+ ACL_RETRY_MAX_DELAY: 0,
1253
1350
  }
1254
1351
  })
1255
1352
 
@@ -1,4 +1,4 @@
1
- import {type Action, type Mutation} from '@sanity/client'
1
+ import {type Action, ClientError, CorsOriginError, type Mutation} from '@sanity/client'
2
2
  import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
3
3
  import {jsonMatch} from '@sanity/json-match'
4
4
  import {type SanityDocument} from 'groq'
@@ -48,6 +48,8 @@ import {createStateSourceAction, type StateSource} from '../store/createStateSou
48
48
  import {defineStore, type StoreContext} from '../store/defineStore'
49
49
  import {type DocumentAction} from './actions'
50
50
  import {
51
+ ACL_RETRY_BASE_DELAY,
52
+ ACL_RETRY_MAX_DELAY,
51
53
  API_VERSION,
52
54
  INITIAL_OUTGOING_THROTTLE_TIME,
53
55
  OUT_OF_SYNC_RETRY_BASE_DELAY,
@@ -562,10 +564,39 @@ const subscribeToClientAndFetchDatasetAcl = ({
562
564
  return getClientState(instance, clientOptions)
563
565
  .observable.pipe(
564
566
  switchMap((client) =>
565
- client.observable.request<DatasetAcl>({
566
- uri,
567
- tag: 'acl.get',
568
- }),
567
+ client.observable
568
+ .request<DatasetAcl>({
569
+ uri,
570
+ tag: 'acl.get',
571
+ })
572
+ .pipe(
573
+ retry({
574
+ delay: (error, retryCount) => {
575
+ // 4xx responses and CORS misconfigurations are not transient —
576
+ // the server will keep rejecting the request, so rethrow to
577
+ // surface them as a fatal store error. 408 (request timeout)
578
+ // and 429 (rate limit) are the exceptions: they resolve on
579
+ // their own, so they are retried like network errors
580
+ const isTransientClientError =
581
+ error instanceof ClientError &&
582
+ (error.statusCode === 408 || error.statusCode === 429)
583
+ if (
584
+ (error instanceof ClientError && !isTransientClientError) ||
585
+ error instanceof CorsOriginError
586
+ ) {
587
+ return throwError(() => error)
588
+ }
589
+ // network errors (no status code), 408/429 responses, and 5xx
590
+ // responses are retried with exponential backoff so a transient
591
+ // failure during startup doesn't permanently brick the store
592
+ const backoff = Math.min(
593
+ ACL_RETRY_BASE_DELAY * 2 ** (retryCount - 1),
594
+ ACL_RETRY_MAX_DELAY,
595
+ )
596
+ return timer(backoff)
597
+ },
598
+ }),
599
+ ),
569
600
  ),
570
601
  tap((datasetAcl) => state.set('setGrants', {grants: createGrantsLookup(datasetAcl)})),
571
602
  )
@@ -565,6 +565,33 @@ describe('diffMatchPatch', () => {
565
565
  expect(output).toEqual(input)
566
566
  })
567
567
 
568
+ it('applies the patch when its offsets miss a multi-byte boundary', () => {
569
+ // `\u00f8` is two bytes, so this patch's offsets land inside it and the byte
570
+ // walk overshoots. The default is to throw, which escapes the rebase loop in
571
+ // `reducers.ts` and kills the whole outgoing-actions pipeline: the instance
572
+ // then silently stops writing every document. The patch itself is fine and
573
+ // still applies.
574
+ const input = {foo: 'abcdefg\u00f8'}
575
+ const patch = '@@ -9,1 +9,2 @@\n \u00f8\n+!\n'
576
+
577
+ expect(() => diffMatchPatch(input, {foo: patch})).not.toThrow()
578
+ expect(diffMatchPatch(input, {foo: patch})).toEqual({foo: 'abcdefg\u00f8!'})
579
+ })
580
+
581
+ it('still patches the other properties in the same mutation', () => {
582
+ const input = {multibyte: 'abcdefg\u00f8', ascii: 'the quick brown fox'}
583
+
584
+ const output = diffMatchPatch(input, {
585
+ multibyte: '@@ -9,1 +9,2 @@\n \u00f8\n+!\n',
586
+ ascii: '@@ -13,7 +13,7 @@\n own \n-fox\n+cat\n',
587
+ })
588
+
589
+ expect(output).toEqual({
590
+ multibyte: 'abcdefg\u00f8!',
591
+ ascii: 'the quick brown cat',
592
+ })
593
+ })
594
+
568
595
  it('applies a diff-match-patch that makes no changes', () => {
569
596
  const input = {foo: 'unchanged'}
570
597
  const patch = '@@ -1,9 +1,9 @@\n unchanged\n'
@@ -617,7 +617,16 @@ export function diffMatchPatch(
617
617
  )
618
618
  }
619
619
 
620
- const [nextValue] = applyPatches(parsePatch(dmp), value)
620
+ // Offsets arrive as UTF-8 byte positions and can overshoot when they
621
+ // land inside a multi-byte character, which happens once the string a
622
+ // patch was computed against has diverged. The default is to throw, and
623
+ // that throw escapes the rebase loop in `reducers.ts`, kills the
624
+ // outgoing-actions pipeline, and leaves the instance silently dropping
625
+ // every write. Clamping to the nearest index applies the patch instead,
626
+ // which is what the Studio and the Portable Text editor already do.
627
+ const [nextValue] = applyPatches(parsePatch(dmp), value, {
628
+ allowExceedingIndices: true,
629
+ })
621
630
  return {path, value: nextValue}
622
631
  })
623
632
  .reduce((acc, {path, value}) => setDeep(acc, path, value), input)
@@ -266,7 +266,9 @@ describe('calculatePermissions', () => {
266
266
  _createdAt: '2025-01-01T00:00:00.000Z',
267
267
  _updatedAt: '2025-01-01T00:00:00.000Z',
268
268
  _rev: 'rev-1',
269
- _system: {createdBy},
269
+ // Content Lake sets `_system.createdBy` on canvas documents; the public
270
+ // `DocumentSystem` type doesn't declare it, so the literal needs a cast.
271
+ _system: {createdBy} as SanityDocument['_system'],
270
272
  })
271
273
 
272
274
  const editAction: DocumentAction = {
@@ -632,6 +632,60 @@ describe('revertOutgoingTransaction', () => {
632
632
  const docState = newState.documentStates[draftId]
633
633
  expect(docState?.unverifiedRevisions && docState.unverifiedRevisions['txnOut']).toBeUndefined()
634
634
  })
635
+
636
+ it('unregisters the reverted batch subscription ids, making documents with no other subscribers evictable', () => {
637
+ const draftId = getDraftId(DocumentId('doc1'))
638
+ const pubId = getPublishedId(DocumentId('doc1'))
639
+ const state: SyncTransactionState = {
640
+ queued: [],
641
+ applied: [],
642
+ outgoing: {
643
+ transactionId: 'txnRevert',
644
+ actions: [
645
+ {
646
+ type: 'document.edit',
647
+ documentId: 'doc1',
648
+ documentType: 'book',
649
+ patches: [{set: {foo: 'changed'}}],
650
+ },
651
+ ],
652
+ disableBatching: false,
653
+ batchedTransactionIds: ['txnRevert'],
654
+ outgoingActions: [],
655
+ outgoingMutations: [],
656
+ base: {},
657
+ working: {},
658
+ previous: {},
659
+ previousRevs: {},
660
+ timestamp: '2025-02-06T00:04:30.000Z',
661
+ },
662
+ grants,
663
+ documentStates: {
664
+ // only subscriber is the reverted transaction itself
665
+ [draftId]: {
666
+ id: draftId,
667
+ subscriptions: ['txnRevert'],
668
+ local: {...exampleDoc, _id: draftId, foo: 'changed', _rev: 'rev2'},
669
+ remote: {...exampleDoc, _id: draftId, foo: 'old', _rev: 'rev1'},
670
+ },
671
+ // also has a UI subscriber, so it should survive with only the
672
+ // transaction subscription removed
673
+ [pubId]: {
674
+ id: pubId,
675
+ subscriptions: ['txnRevert', 'sub-ui'],
676
+ local: {...exampleDoc, _id: pubId, foo: 'pub', _rev: 'revPub'},
677
+ remote: {...exampleDoc, _id: pubId, foo: 'pub', _rev: 'revPub'},
678
+ },
679
+ },
680
+ }
681
+ const newState = revertOutgoingTransaction(state)
682
+ expect(newState.outgoing).toBeUndefined()
683
+ // no subscribers remain, so the document state is evicted entirely
684
+ expect(newState.documentStates[draftId]).toBeUndefined()
685
+ // the UI subscription keeps the other document alive, but the reverted
686
+ // transaction id is gone from its subscriptions
687
+ expect(newState.documentStates[pubId]?.subscriptions).toEqual(['sub-ui'])
688
+ })
635
689
  })
636
690
 
637
691
  describe('applyRemoteDocument', () => {
@@ -1124,6 +1178,85 @@ describe('applyRemoteDocument', () => {
1124
1178
  })
1125
1179
  })
1126
1180
 
1181
+ describe('rebase over multi-byte text', () => {
1182
+ it('does not kill the pipeline when a plain edit rebases onto multi-byte text', () => {
1183
+ // The reported production failure: a byte-offset throw from
1184
+ // `@sanity/diff-match-patch` is a plain `Error`, and `createMutationApplier`
1185
+ // only converts to `ActionError` when `preserveOperations` is set. So for an
1186
+ // ordinary `editDocument` it escapes this rebase loop, reaches the rxjs
1187
+ // subscription, and the outgoing-actions pipeline for the whole instance
1188
+ // dies. Every later write is then dropped silently, for every document.
1189
+ const docId = getDraftId(DocumentId('doc1'))
1190
+ const originalDoc: SanityDocument = {
1191
+ ...exampleDoc,
1192
+ _id: docId,
1193
+ _rev: 'rev1',
1194
+ title: 'abcdefg\u00f8',
1195
+ }
1196
+ const locallyEditedDoc: SanityDocument = {
1197
+ ...originalDoc,
1198
+ title: 'abcdefg\u00f8!',
1199
+ _rev: 'txnLocal',
1200
+ }
1201
+
1202
+ const appliedTransaction: AppliedTransaction = {
1203
+ transactionId: 'txnLocal',
1204
+ actions: [
1205
+ {
1206
+ type: 'document.edit',
1207
+ documentId: 'doc1',
1208
+ documentType: 'author',
1209
+ patches: [{diffMatchPatch: {title: '@@ -9,1 +9,2 @@\n \u00f8\n+!\n'}}],
1210
+ },
1211
+ ],
1212
+ previous: {[docId]: originalDoc},
1213
+ base: {[docId]: originalDoc},
1214
+ working: {[docId]: locallyEditedDoc},
1215
+ previousRevs: {[docId]: 'rev1'},
1216
+ timestamp: '2025-02-06T00:16:00.000Z',
1217
+ outgoingActions: [],
1218
+ outgoingMutations: [],
1219
+ }
1220
+
1221
+ const initialState: SyncTransactionState = {
1222
+ queued: [],
1223
+ applied: [appliedTransaction],
1224
+ outgoing: undefined,
1225
+ grants,
1226
+ documentStates: {
1227
+ [docId]: {
1228
+ id: docId,
1229
+ subscriptions: ['sub1'],
1230
+ local: locallyEditedDoc,
1231
+ remote: originalDoc,
1232
+ unverifiedRevisions: {},
1233
+ },
1234
+ },
1235
+ }
1236
+
1237
+ // someone else changed the same field, so the local patch is rebased onto
1238
+ // a base its byte offsets were not computed against
1239
+ const remoteDoc: SanityDocument = {
1240
+ ...originalDoc,
1241
+ title: '\u00f8\u00f8\u00f8\u00f8\u00f8\u00f8',
1242
+ _rev: 'txnForeign',
1243
+ }
1244
+ const remote: RemoteDocument = {
1245
+ type: 'mutation',
1246
+ documentId: docId,
1247
+ document: remoteDoc,
1248
+ revision: 'txnForeign',
1249
+ previousRev: 'rev1',
1250
+ timestamp: '2025-02-06T00:17:00.000Z',
1251
+ mutations: [{patch: {id: docId, set: {title: '\u00f8\u00f8\u00f8\u00f8\u00f8\u00f8'}}}],
1252
+ }
1253
+
1254
+ const events = new Subject<DocumentEvent>()
1255
+
1256
+ expect(() => applyRemoteDocument(initialState, remote, events)).not.toThrow()
1257
+ })
1258
+ })
1259
+
1127
1260
  describe('rebase with preserved operations', () => {
1128
1261
  it('skips a preserved-operations transaction that fails to re-apply and emits rebase-error', () => {
1129
1262
  const docId = getDraftId(DocumentId('doc1'))
@@ -406,8 +406,23 @@ export function cleanupOutgoingTransaction(prev: SyncTransactionState): SyncTran
406
406
 
407
407
  export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTransactionState {
408
408
  if (!prev.grants) return prev
409
+
410
+ // unregister the reverted batch's transaction-held subscriptions, symmetric
411
+ // with `cleanupOutgoingTransaction` on ack and `removeQueuedTransaction` on
412
+ // apply failure. without this, a reverted document's subscription IDs are
413
+ // never released and it can never be evicted
414
+ let base = prev
415
+ if (prev.outgoing) {
416
+ const ids = getDocumentIdsFromHandleLikes(prev.outgoing.actions)
417
+ for (const transactionId of prev.outgoing.batchedTransactionIds) {
418
+ for (const documentId of ids) {
419
+ base = removeSubscriptionIdFromDocument(base, documentId, transactionId)
420
+ }
421
+ }
422
+ }
423
+
409
424
  let working = Object.fromEntries(
410
- Object.entries(prev.documentStates).map(([documentId, documentState]) => [
425
+ Object.entries(base.documentStates).map(([documentId, documentState]) => [
411
426
  documentId,
412
427
  documentState?.remote,
413
428
  ]),
@@ -428,11 +443,11 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
428
443
  }
429
444
 
430
445
  return {
431
- ...prev,
446
+ ...base,
432
447
  applied: nextApplied,
433
448
  outgoing: undefined,
434
449
  documentStates: Object.fromEntries(
435
- Object.entries(prev.documentStates)
450
+ Object.entries(base.documentStates)
436
451
  .filter((e): e is [string, DocumentState] => !!e[1])
437
452
  .map(([documentId, {unverifiedRevisions = {}, local, ...documentState}]) => {
438
453
  const next: DocumentState = {
@@ -6,14 +6,6 @@ import {createStoreInstance} from '../store/createStoreInstance'
6
6
  import {projectionStore} from './projectionStore'
7
7
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
8
8
 
9
- // Mock the module with a factory function
10
- vi.mock('../common/createLiveEventSubscriber', () => {
11
- const mockLiveSubscriber = vi.fn()
12
- return {
13
- createLiveEventSubscriber: vi.fn(() => mockLiveSubscriber),
14
- }
15
- })
16
-
17
9
  vi.mock('./subscribeToStateAndFetchBatches')
18
10
 
19
11
  describe('projectionStore', () => {