@sanity/sdk 2.11.1 → 2.13.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 (60) hide show
  1. package/dist/_chunks-dts/utils.d.ts +175 -19
  2. package/dist/_chunks-es/_internal.js +41 -26
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +15 -4
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/telemetryManager.js +25 -19
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -1
  8. package/dist/_chunks-es/version.js +1 -1
  9. package/dist/_exports/_internal.d.ts +27 -11
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +465 -131
  12. package/dist/index.js.map +1 -1
  13. package/package.json +11 -11
  14. package/src/_exports/index.ts +23 -2
  15. package/src/config/sanityConfig.ts +12 -0
  16. package/src/document/actions.test.ts +112 -1
  17. package/src/document/actions.ts +148 -1
  18. package/src/document/applyDocumentActions.test.ts +24 -0
  19. package/src/document/applyDocumentActions.ts +17 -5
  20. package/src/document/documentConstants.ts +7 -0
  21. package/src/document/documentStore.test.ts +69 -0
  22. package/src/document/documentStore.ts +42 -10
  23. package/src/document/events.test.ts +57 -2
  24. package/src/document/events.ts +43 -24
  25. package/src/document/listen.ts +1 -1
  26. package/src/document/permissions.test.ts +79 -0
  27. package/src/document/permissions.ts +8 -7
  28. package/src/document/processActions/create.ts +7 -4
  29. package/src/document/processActions/delete.ts +4 -4
  30. package/src/document/processActions/discard.ts +2 -2
  31. package/src/document/processActions/edit.ts +13 -47
  32. package/src/document/processActions/processActions.ts +53 -3
  33. package/src/document/processActions/publish.ts +4 -4
  34. package/src/document/processActions/releaseArchive.ts +77 -0
  35. package/src/document/processActions/releaseCreate.ts +59 -0
  36. package/src/document/processActions/releaseDelete.ts +65 -0
  37. package/src/document/processActions/releaseEdit.ts +37 -0
  38. package/src/document/processActions/releasePublish.ts +45 -0
  39. package/src/document/processActions/releaseSchedule.ts +87 -0
  40. package/src/document/processActions/releaseUtil.ts +31 -0
  41. package/src/document/processActions/shared.ts +108 -4
  42. package/src/document/processActions/unpublish.ts +3 -3
  43. package/src/document/processActions.test.ts +423 -1
  44. package/src/document/reducers.ts +44 -8
  45. package/src/document/resourceRules.test.ts +178 -0
  46. package/src/document/resourceRules.ts +117 -0
  47. package/src/releases/getPerspectiveState.test.ts +1 -1
  48. package/src/releases/releasesStore.test.ts +50 -1
  49. package/src/releases/releasesStore.ts +41 -18
  50. package/src/releases/utils/sortReleases.test.ts +2 -2
  51. package/src/releases/utils/sortReleases.ts +1 -1
  52. package/src/telemetry/environment.test.ts +119 -0
  53. package/src/telemetry/environment.ts +92 -0
  54. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  55. package/src/telemetry/initTelemetry.test.ts +240 -16
  56. package/src/telemetry/initTelemetry.ts +39 -16
  57. package/src/telemetry/telemetryManager.test.ts +129 -65
  58. package/src/telemetry/telemetryManager.ts +41 -29
  59. package/src/telemetry/devMode.test.ts +0 -60
  60. package/src/telemetry/devMode.ts +0 -41
@@ -16,7 +16,7 @@ export function handleUnpublish(
16
16
  action: UnpublishDocumentAction,
17
17
  ctx: ActionHandlerContext,
18
18
  ): ActionHandlerResult {
19
- const {transactionId, timestamp, grants, outgoingActions, outgoingMutations} = ctx
19
+ const {transactionId, timestamp, grants, identity, outgoingActions, outgoingMutations} = ctx
20
20
  let {base, working} = ctx
21
21
 
22
22
  const documentId = getId(action.documentId)
@@ -48,7 +48,7 @@ export function handleUnpublish(
48
48
  {createIfNotExists: newDraftFromPublished},
49
49
  ]
50
50
 
51
- if (!checkGrant(grants.update, sourceDoc)) {
51
+ if (!checkGrant(grants.update, sourceDoc, identity)) {
52
52
  throw new PermissionActionError({
53
53
  documentId,
54
54
  transactionId,
@@ -56,7 +56,7 @@ export function handleUnpublish(
56
56
  })
57
57
  }
58
58
 
59
- if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished)) {
59
+ if (!working[draftId] && !checkGrant(grants.create, newDraftFromPublished, identity)) {
60
60
  throw new PermissionActionError({
61
61
  documentId,
62
62
  transactionId,
@@ -2,7 +2,7 @@ import {type CreateMutation, type Reference, type SanityDocument} from '@sanity/
2
2
  import {parse} from 'groq-js'
3
3
  import {describe, expect, it} from 'vitest'
4
4
 
5
- import {type DocumentAction} from './actions'
5
+ import {type Action, type DocumentAction} from './actions'
6
6
  import {ActionError, processActions} from './processActions/processActions'
7
7
  import {type DocumentSet} from './processMutations'
8
8
 
@@ -1420,4 +1420,426 @@ describe('processActions', () => {
1420
1420
  })
1421
1421
  })
1422
1422
  })
1423
+
1424
+ describe('release actions', () => {
1425
+ const releaseName = 'my-release'
1426
+ const releaseDocId = `_.releases.${releaseName}`
1427
+
1428
+ const createReleaseDoc = (overrides: Partial<SanityDocument> = {}): SanityDocument => ({
1429
+ _id: releaseDocId,
1430
+ _type: 'system.release',
1431
+ _createdAt: '2025-01-01T00:00:00.000Z',
1432
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1433
+ _rev: 'initial',
1434
+ name: releaseName,
1435
+ state: 'active',
1436
+ metadata: {releaseType: 'undecided'},
1437
+ ...overrides,
1438
+ })
1439
+
1440
+ describe('release.create', () => {
1441
+ it('inserts an optimistic release doc and emits a release.create action', () => {
1442
+ const base: DocumentSet = {}
1443
+ const working: DocumentSet = {}
1444
+ const actions: Action[] = [
1445
+ {
1446
+ type: 'release.create',
1447
+ releaseId: releaseName,
1448
+ metadata: {title: 'My release', releaseType: 'asap'},
1449
+ },
1450
+ ]
1451
+
1452
+ const result = processActions({
1453
+ actions,
1454
+ transactionId,
1455
+ base,
1456
+ working,
1457
+ timestamp,
1458
+ grants: defaultGrants,
1459
+ })
1460
+
1461
+ const doc = result.working[releaseDocId]
1462
+ expect(doc).toBeDefined()
1463
+ expect(doc?._type).toBe('system.release')
1464
+ expect(doc?.['name']).toBe(releaseName)
1465
+ expect(doc?.['state']).toBe('active')
1466
+ expect(doc?.['metadata']).toEqual({title: 'My release', releaseType: 'asap'})
1467
+
1468
+ expect(result.outgoingActions).toEqual([
1469
+ {
1470
+ actionType: 'sanity.action.release.create',
1471
+ releaseId: releaseName,
1472
+ metadata: {title: 'My release', releaseType: 'asap'},
1473
+ },
1474
+ ])
1475
+ })
1476
+
1477
+ it('throws if the release already exists in base or working', () => {
1478
+ const existing = createReleaseDoc()
1479
+ expect(() =>
1480
+ processActions({
1481
+ actions: [
1482
+ {
1483
+ type: 'release.create',
1484
+ releaseId: releaseName,
1485
+ metadata: {releaseType: 'undecided'},
1486
+ },
1487
+ ],
1488
+ transactionId,
1489
+ base: {[releaseDocId]: existing},
1490
+ working: {[releaseDocId]: existing},
1491
+ timestamp,
1492
+ grants: defaultGrants,
1493
+ }),
1494
+ ).toThrow(/already exists/)
1495
+ })
1496
+
1497
+ it('throws PermissionActionError when the create grant is denied', () => {
1498
+ expect(() =>
1499
+ processActions({
1500
+ actions: [
1501
+ {
1502
+ type: 'release.create',
1503
+ releaseId: releaseName,
1504
+ metadata: {releaseType: 'undecided'},
1505
+ },
1506
+ ],
1507
+ transactionId,
1508
+ base: {},
1509
+ working: {},
1510
+ timestamp,
1511
+ grants: {...defaultGrants, create: alwaysDeny},
1512
+ }),
1513
+ ).toThrow(/permission to create release/)
1514
+ })
1515
+ })
1516
+
1517
+ describe('release.edit', () => {
1518
+ it('applies the patch optimistically and forwards the original patch to the outgoing action', () => {
1519
+ const existing = createReleaseDoc({
1520
+ metadata: {releaseType: 'undecided'},
1521
+ } as Partial<SanityDocument>)
1522
+ const patch = {set: {'metadata.title': 'Updated title'}}
1523
+
1524
+ const result = processActions({
1525
+ actions: [{type: 'release.edit', releaseId: releaseName, patch}],
1526
+ transactionId,
1527
+ base: {[releaseDocId]: existing},
1528
+ working: {[releaseDocId]: existing},
1529
+ timestamp,
1530
+ grants: defaultGrants,
1531
+ })
1532
+
1533
+ const updated = result.working[releaseDocId] as SanityDocument & {
1534
+ metadata: {title?: string; releaseType: string}
1535
+ }
1536
+ expect(updated.metadata.title).toBe('Updated title')
1537
+ // releaseType wasn't unset
1538
+ expect(updated.metadata.releaseType).toBe('undecided')
1539
+
1540
+ expect(result.outgoingActions).toEqual([
1541
+ {
1542
+ actionType: 'sanity.action.release.edit',
1543
+ releaseId: releaseName,
1544
+ patch,
1545
+ },
1546
+ ])
1547
+ })
1548
+
1549
+ it('throws when editing a release that does not exist', () => {
1550
+ expect(() =>
1551
+ processActions({
1552
+ actions: [
1553
+ {
1554
+ type: 'release.edit',
1555
+ releaseId: releaseName,
1556
+ patch: {set: {'metadata.title': 'x'}},
1557
+ },
1558
+ ],
1559
+ transactionId,
1560
+ base: {},
1561
+ working: {},
1562
+ timestamp,
1563
+ grants: defaultGrants,
1564
+ }),
1565
+ ).toThrow(/does not exist/)
1566
+ })
1567
+
1568
+ it('throws PermissionActionError when the working release fails the update grant', () => {
1569
+ const existing = createReleaseDoc()
1570
+ expect(() =>
1571
+ processActions({
1572
+ actions: [
1573
+ {
1574
+ type: 'release.edit',
1575
+ releaseId: releaseName,
1576
+ patch: {set: {'metadata.title': 'x'}},
1577
+ },
1578
+ ],
1579
+ transactionId,
1580
+ base: {[releaseDocId]: existing},
1581
+ working: {[releaseDocId]: existing},
1582
+ timestamp,
1583
+ grants: {...defaultGrants, update: alwaysDeny},
1584
+ }),
1585
+ ).toThrow(/permission to edit release/)
1586
+ })
1587
+ })
1588
+
1589
+ describe('fire-and-forget actions', () => {
1590
+ const existing = createReleaseDoc()
1591
+ const cases: Array<{
1592
+ name: string
1593
+ action: Action
1594
+ expectedOutgoing: Record<string, unknown>
1595
+ }> = [
1596
+ {
1597
+ name: 'release.publish',
1598
+ action: {type: 'release.publish', releaseId: releaseName},
1599
+ expectedOutgoing: {actionType: 'sanity.action.release.publish', releaseId: releaseName},
1600
+ },
1601
+ {
1602
+ name: 'release.schedule',
1603
+ action: {
1604
+ type: 'release.schedule',
1605
+ releaseId: releaseName,
1606
+ publishAt: '2026-01-01T00:00:00.000Z',
1607
+ },
1608
+ expectedOutgoing: {
1609
+ actionType: 'sanity.action.release.schedule',
1610
+ releaseId: releaseName,
1611
+ publishAt: '2026-01-01T00:00:00.000Z',
1612
+ },
1613
+ },
1614
+ {
1615
+ name: 'release.unschedule',
1616
+ action: {type: 'release.unschedule', releaseId: releaseName},
1617
+ expectedOutgoing: {
1618
+ actionType: 'sanity.action.release.unschedule',
1619
+ releaseId: releaseName,
1620
+ },
1621
+ },
1622
+ {
1623
+ name: 'release.archive',
1624
+ action: {type: 'release.archive', releaseId: releaseName},
1625
+ expectedOutgoing: {actionType: 'sanity.action.release.archive', releaseId: releaseName},
1626
+ },
1627
+ {
1628
+ name: 'release.unarchive',
1629
+ action: {type: 'release.unarchive', releaseId: releaseName},
1630
+ expectedOutgoing: {actionType: 'sanity.action.release.unarchive', releaseId: releaseName},
1631
+ },
1632
+ ]
1633
+
1634
+ it.each(cases)(
1635
+ '$name does not mutate local state and emits the matching outgoing action',
1636
+ ({action, expectedOutgoing}) => {
1637
+ const base: DocumentSet = {[releaseDocId]: existing}
1638
+ const working: DocumentSet = {[releaseDocId]: existing}
1639
+
1640
+ const result = processActions({
1641
+ actions: [action],
1642
+ transactionId,
1643
+ base,
1644
+ working,
1645
+ timestamp,
1646
+ grants: defaultGrants,
1647
+ })
1648
+
1649
+ // working and base reference the same doc instance — no optimistic
1650
+ // mutation was applied
1651
+ expect(result.working[releaseDocId]).toBe(existing)
1652
+ expect(result.outgoingActions).toEqual([expectedOutgoing])
1653
+ expect(result.outgoingMutations).toEqual([])
1654
+ },
1655
+ )
1656
+
1657
+ it.each(cases)('$name throws if the release does not exist', ({action}) => {
1658
+ expect(() =>
1659
+ processActions({
1660
+ actions: [action],
1661
+ transactionId,
1662
+ base: {},
1663
+ working: {},
1664
+ timestamp,
1665
+ grants: defaultGrants,
1666
+ }),
1667
+ ).toThrow(ActionError)
1668
+ })
1669
+
1670
+ it.each(cases)(
1671
+ '$name throws PermissionActionError when update grant is denied',
1672
+ ({action}) => {
1673
+ const base: DocumentSet = {[releaseDocId]: existing}
1674
+ const working: DocumentSet = {[releaseDocId]: existing}
1675
+ expect(() =>
1676
+ processActions({
1677
+ actions: [action],
1678
+ transactionId,
1679
+ base,
1680
+ working,
1681
+ timestamp,
1682
+ grants: {...defaultGrants, update: alwaysDeny},
1683
+ }),
1684
+ ).toThrow(/permission to/)
1685
+ },
1686
+ )
1687
+ })
1688
+
1689
+ describe('release.schedule publishAt validation', () => {
1690
+ const existing = createReleaseDoc()
1691
+
1692
+ it.each(['not-a-date', '', '2026-13-40T99:99:99Z'])(
1693
+ 'throws ActionError when publishAt is not a valid timestamp (%s)',
1694
+ (publishAt) => {
1695
+ expect(() =>
1696
+ processActions({
1697
+ actions: [{type: 'release.schedule', releaseId: releaseName, publishAt}],
1698
+ transactionId,
1699
+ base: {[releaseDocId]: existing},
1700
+ working: {[releaseDocId]: existing},
1701
+ timestamp,
1702
+ grants: defaultGrants,
1703
+ }),
1704
+ ).toThrow(/must be a valid ISO 8601 timestamp/)
1705
+ },
1706
+ )
1707
+ })
1708
+
1709
+ describe('release.delete', () => {
1710
+ it.each(['archived', 'published'] as const)(
1711
+ 'optimistically removes the local release when state is %s',
1712
+ (state) => {
1713
+ const existing = createReleaseDoc({state} as Partial<SanityDocument>)
1714
+ const result = processActions({
1715
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1716
+ transactionId,
1717
+ base: {[releaseDocId]: existing},
1718
+ working: {[releaseDocId]: existing},
1719
+ timestamp,
1720
+ grants: defaultGrants,
1721
+ })
1722
+
1723
+ // processMutations sets deleted entries to null
1724
+ expect(result.working[releaseDocId]).toBeNull()
1725
+ expect(result.outgoingMutations).toEqual([{delete: {id: releaseDocId}}])
1726
+ expect(result.outgoingActions).toEqual([
1727
+ {actionType: 'sanity.action.release.delete', releaseId: releaseName},
1728
+ ])
1729
+ },
1730
+ )
1731
+
1732
+ it.each(['active', 'scheduled', 'archiving', 'publishing'] as const)(
1733
+ 'throws when state is %s (server only accepts archived/published)',
1734
+ (state) => {
1735
+ const existing = createReleaseDoc({state} as Partial<SanityDocument>)
1736
+ expect(() =>
1737
+ processActions({
1738
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1739
+ transactionId,
1740
+ base: {[releaseDocId]: existing},
1741
+ working: {[releaseDocId]: existing},
1742
+ timestamp,
1743
+ grants: defaultGrants,
1744
+ }),
1745
+ ).toThrow(/Archive it first/)
1746
+ },
1747
+ )
1748
+
1749
+ it('throws if the release does not exist', () => {
1750
+ expect(() =>
1751
+ processActions({
1752
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1753
+ transactionId,
1754
+ base: {},
1755
+ working: {},
1756
+ timestamp,
1757
+ grants: defaultGrants,
1758
+ }),
1759
+ ).toThrow(/does not exist/)
1760
+ })
1761
+
1762
+ it('throws PermissionActionError when the update grant is denied', () => {
1763
+ const existing = createReleaseDoc({state: 'archived'} as Partial<SanityDocument>)
1764
+ expect(() =>
1765
+ processActions({
1766
+ actions: [{type: 'release.delete', releaseId: releaseName}],
1767
+ transactionId,
1768
+ base: {[releaseDocId]: existing},
1769
+ working: {[releaseDocId]: existing},
1770
+ timestamp,
1771
+ grants: {...defaultGrants, update: alwaysDeny},
1772
+ }),
1773
+ ).toThrow(/permission to delete release/)
1774
+ })
1775
+ })
1776
+
1777
+ describe('mixed with liveEdit document actions', () => {
1778
+ const liveEditDocId = 'live-edit-doc'
1779
+ const liveEditDoc = {
1780
+ _id: liveEditDocId,
1781
+ _type: 'liveEditType',
1782
+ _createdAt: '2025-01-01T00:00:00.000Z',
1783
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1784
+ _rev: 'initial',
1785
+ }
1786
+ const liveEditAction = {
1787
+ type: 'document.edit',
1788
+ documentId: liveEditDocId,
1789
+ documentType: 'liveEditType',
1790
+ liveEdit: true,
1791
+ patches: [{set: {title: 'x'}}],
1792
+ } as const
1793
+
1794
+ it('throws ActionError when a transaction mixes a release action with a liveEdit document action', () => {
1795
+ expect(() =>
1796
+ processActions({
1797
+ actions: [
1798
+ {
1799
+ type: 'release.create',
1800
+ releaseId: releaseName,
1801
+ metadata: {releaseType: 'undecided'},
1802
+ },
1803
+ liveEditAction as unknown as Action,
1804
+ ],
1805
+ transactionId,
1806
+ base: {[liveEditDocId]: liveEditDoc},
1807
+ working: {[liveEditDocId]: liveEditDoc},
1808
+ timestamp,
1809
+ grants: defaultGrants,
1810
+ }),
1811
+ ).toThrow(/Cannot combine liveEdit document actions with other actions/)
1812
+ })
1813
+
1814
+ it('throws ActionError when a transaction mixes a non-liveEdit document action with a liveEdit document action', () => {
1815
+ const otherDocId = 'other-doc'
1816
+ const otherDoc = {
1817
+ _id: otherDocId,
1818
+ _type: 'someType',
1819
+ _createdAt: '2025-01-01T00:00:00.000Z',
1820
+ _updatedAt: '2025-01-01T00:00:00.000Z',
1821
+ _rev: 'initial',
1822
+ } as SanityDocument
1823
+
1824
+ expect(() =>
1825
+ processActions({
1826
+ actions: [
1827
+ {
1828
+ type: 'document.edit',
1829
+ documentId: otherDocId,
1830
+ documentType: 'someType',
1831
+ patches: [{set: {title: 'y'}}],
1832
+ },
1833
+ liveEditAction as unknown as Action,
1834
+ ],
1835
+ transactionId,
1836
+ base: {[liveEditDocId]: liveEditDoc, [otherDocId]: otherDoc},
1837
+ working: {[liveEditDocId]: liveEditDoc, [otherDocId]: otherDoc},
1838
+ timestamp,
1839
+ grants: defaultGrants,
1840
+ }),
1841
+ ).toThrow(/Cannot combine liveEdit document actions with other actions/)
1842
+ })
1843
+ })
1844
+ })
1423
1845
  })
@@ -7,18 +7,19 @@ import {type StoreContext} from '../store/defineStore'
7
7
  import {insecureRandomId} from '../utils/ids'
8
8
  import {omitProperty} from '../utils/object'
9
9
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
10
- import {type DocumentAction} from './actions'
10
+ import {type Action} from './actions'
11
11
  import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
12
12
  import {type DocumentState, type DocumentStoreState} from './documentStore'
13
13
  import {type RemoteDocument} from './listen'
14
14
  import {ActionError, processActions} from './processActions/processActions'
15
+ import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
15
16
  import {type DocumentSet} from './processMutations'
16
17
 
17
18
  const EMPTY_REVISIONS: NonNullable<Required<DocumentState['unverifiedRevisions']>> = {}
18
19
 
19
20
  export type SyncTransactionState = Pick<
20
21
  DocumentStoreState,
21
- 'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants'
22
+ 'queued' | 'applied' | 'documentStates' | 'outgoing' | 'grants' | 'identity'
22
23
  >
23
24
 
24
25
  type DocumentHandleLike = Pick<DocumentHandle, 'perspective'> & {
@@ -33,6 +34,14 @@ type ActionMap = {
33
34
  delete: 'sanity.action.document.delete'
34
35
  edit: 'sanity.action.document.edit'
35
36
  publish: 'sanity.action.document.publish'
37
+ releaseCreate: 'sanity.action.release.create'
38
+ releaseEdit: 'sanity.action.release.edit'
39
+ releasePublish: 'sanity.action.release.publish'
40
+ releaseSchedule: 'sanity.action.release.schedule'
41
+ releaseUnschedule: 'sanity.action.release.unschedule'
42
+ releaseArchive: 'sanity.action.release.archive'
43
+ releaseUnarchive: 'sanity.action.release.unarchive'
44
+ releaseDelete: 'sanity.action.release.delete'
36
45
  }
37
46
 
38
47
  type OptimisticLock = {
@@ -40,6 +49,14 @@ type OptimisticLock = {
40
49
  ifPublishedRevisionId?: string
41
50
  }
42
51
 
52
+ interface ReleaseMetadataPayload {
53
+ title?: string
54
+ description?: string
55
+ intendedPublishAt?: string
56
+ releaseType?: 'asap' | 'scheduled' | 'undecided'
57
+ cardinality?: 'one' | 'many'
58
+ }
59
+
43
60
  export type HttpAction =
44
61
  | {actionType: ActionMap['create']; publishedId: string; attributes: SanityDocumentLike}
45
62
  | {actionType: ActionMap['discard']; versionId: string; purge?: boolean}
@@ -47,6 +64,18 @@ export type HttpAction =
47
64
  | {actionType: ActionMap['delete']; publishedId: string; includeDrafts?: string[]}
48
65
  | {actionType: ActionMap['edit']; draftId: string; publishedId: string; patch: PatchOperations}
49
66
  | ({actionType: ActionMap['publish']; draftId: string; publishedId: string} & OptimisticLock)
67
+ | {
68
+ actionType: ActionMap['releaseCreate']
69
+ releaseId: string
70
+ metadata?: ReleaseMetadataPayload
71
+ }
72
+ | {actionType: ActionMap['releaseEdit']; releaseId: string; patch: PatchOperations}
73
+ | {actionType: ActionMap['releasePublish']; releaseId: string}
74
+ | {actionType: ActionMap['releaseSchedule']; releaseId: string; publishAt: string}
75
+ | {actionType: ActionMap['releaseUnschedule']; releaseId: string}
76
+ | {actionType: ActionMap['releaseArchive']; releaseId: string}
77
+ | {actionType: ActionMap['releaseUnarchive']; releaseId: string}
78
+ | {actionType: ActionMap['releaseDelete']; releaseId: string}
50
79
 
51
80
  /**
52
81
  * Represents a transaction that is queued to be applied but has not yet been
@@ -63,7 +92,7 @@ export interface QueuedTransaction {
63
92
  * actions don't mention draft IDs and is meant to abstract away the draft
64
93
  * model from users.
65
94
  */
66
- actions: DocumentAction[]
95
+ actions: Action[]
67
96
  /**
68
97
  * An optional flag set to disable this transaction from being batched with
69
98
  * other transactions.
@@ -203,6 +232,7 @@ export function applyFirstQueuedTransaction(prev: SyncTransactionState): SyncTra
203
232
  base: working,
204
233
  timestamp,
205
234
  grants: prev.grants,
235
+ identity: prev.identity,
206
236
  })
207
237
  const applied: AppliedTransaction = {
208
238
  ...queued,
@@ -272,7 +302,9 @@ export function batchAppliedTransactions([curr, ...rest]: AppliedTransaction[]):
272
302
  if (next.disableBatching) return editAction
273
303
 
274
304
  // Don't batch a liveEdit edit with a non-liveEdit edit — they route to different APIs
275
- if (!!action.liveEdit !== !!next.actions[0]?.liveEdit) return editAction
305
+ const nextFirst = next.actions[0]
306
+ const nextLiveEdit = nextFirst && 'liveEdit' in nextFirst ? nextFirst.liveEdit : false
307
+ if (!!action.liveEdit !== !!nextLiveEdit) return editAction
276
308
 
277
309
  return {
278
310
  disableBatching: false,
@@ -368,7 +400,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
368
400
 
369
401
  for (const t of prev.applied) {
370
402
  try {
371
- const next = processActions({...t, working, grants: prev.grants})
403
+ const next = processActions({...t, working, grants: prev.grants, identity: prev.identity})
372
404
  working = next.working
373
405
  nextApplied.push({...t, ...next})
374
406
  } catch (error) {
@@ -475,7 +507,7 @@ export function applyRemoteDocument(
475
507
  // transaction again through the listener and this same flow will run then
476
508
  for (const curr of prev.applied) {
477
509
  try {
478
- const next = processActions({...curr, working, grants: prev.grants})
510
+ const next = processActions({...curr, working, grants: prev.grants, identity: prev.identity})
479
511
  working = next.working
480
512
  // next includes an updated `previous` set and `working` set and updates
481
513
  // the `outgoingAction` and `outgoingMutations`. the `base` set from the
@@ -586,9 +618,13 @@ export function manageSubscriberIds(
586
618
 
587
619
  // document handles are passed in via the public facing API, but we also need to
588
620
  // pull the correct document ids from action bodies, which have similar but not
589
- // identical shapes to the document handles.
590
- function getDocumentIdsFromHandleLikes(handles: DocumentHandleLike[]): string[] {
621
+ // identical shapes to the document handles. release actions also flow through
622
+ // here, and resolve to the underlying release document id.
623
+ function getDocumentIdsFromHandleLikes(handles: (DocumentHandleLike | Action)[]): string[] {
591
624
  return handles.flatMap((handle) => {
625
+ if ('type' in handle && isReleaseAction(handle)) {
626
+ return [getReleaseDocumentId(handle.releaseId)]
627
+ }
592
628
  const idsForDocument = []
593
629
  if (!handle.documentId) return []
594
630
  if (handle.liveEdit) {