@liveblocks/core 3.20.0-rc1 → 3.21.0-exp1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/version.ts
8
8
  var PKG_NAME = "@liveblocks/core";
9
- var PKG_VERSION = "3.20.0-rc1";
9
+ var PKG_VERSION = "3.21.0-exp1";
10
10
  var PKG_FORMAT = "esm";
11
11
 
12
12
  // src/dupe-detection.ts
@@ -1570,7 +1570,6 @@ function isUrl(string) {
1570
1570
  function createApiClient({
1571
1571
  baseUrl,
1572
1572
  authManager,
1573
- currentUserId,
1574
1573
  fetchPolyfill
1575
1574
  }) {
1576
1575
  const httpClient = new HttpClient(baseUrl, fetchPolyfill);
@@ -1578,8 +1577,9 @@ function createApiClient({
1578
1577
  const result = await httpClient.get(
1579
1578
  url`/v2/c/rooms/${options.roomId}/threads/delta`,
1580
1579
  await authManager.getAuthValue({
1581
- requestedScope: "comments:read",
1582
- roomId: options.roomId
1580
+ roomId: options.roomId,
1581
+ resource: "comments",
1582
+ access: "read"
1583
1583
  }),
1584
1584
  {
1585
1585
  since: options.since.toISOString()
@@ -1617,8 +1617,9 @@ function createApiClient({
1617
1617
  const result = await httpClient.get(
1618
1618
  url`/v2/c/rooms/${options.roomId}/threads`,
1619
1619
  await authManager.getAuthValue({
1620
- requestedScope: "comments:read",
1621
- roomId: options.roomId
1620
+ roomId: options.roomId,
1621
+ resource: "comments",
1622
+ access: "read"
1622
1623
  }),
1623
1624
  {
1624
1625
  cursor: options.cursor,
@@ -1662,8 +1663,9 @@ function createApiClient({
1662
1663
  const result = await httpClient.get(
1663
1664
  url`/v2/c/rooms/${options.roomId}/threads/comments/search`,
1664
1665
  await authManager.getAuthValue({
1665
- requestedScope: "comments:read",
1666
- roomId: options.roomId
1666
+ roomId: options.roomId,
1667
+ resource: "comments",
1668
+ access: "read"
1667
1669
  }),
1668
1670
  {
1669
1671
  text: options.query.text,
@@ -1684,8 +1686,9 @@ function createApiClient({
1684
1686
  const thread = await httpClient.post(
1685
1687
  url`/v2/c/rooms/${options.roomId}/threads`,
1686
1688
  await authManager.getAuthValue({
1687
- requestedScope: "comments:read",
1688
- roomId: options.roomId
1689
+ roomId: options.roomId,
1690
+ resource: "comments",
1691
+ access: "write"
1689
1692
  }),
1690
1693
  {
1691
1694
  id: threadId,
@@ -1704,8 +1707,9 @@ function createApiClient({
1704
1707
  await httpClient.delete(
1705
1708
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}`,
1706
1709
  await authManager.getAuthValue({
1707
- requestedScope: "comments:read",
1708
- roomId: options.roomId
1710
+ roomId: options.roomId,
1711
+ resource: "comments",
1712
+ access: "write"
1709
1713
  })
1710
1714
  );
1711
1715
  }
@@ -1713,8 +1717,9 @@ function createApiClient({
1713
1717
  const response = await httpClient.rawGet(
1714
1718
  url`/v2/c/rooms/${options.roomId}/thread-with-notification/${options.threadId}`,
1715
1719
  await authManager.getAuthValue({
1716
- requestedScope: "comments:read",
1717
- roomId: options.roomId
1720
+ roomId: options.roomId,
1721
+ resource: "comments",
1722
+ access: "read"
1718
1723
  })
1719
1724
  );
1720
1725
  if (response.ok) {
@@ -1740,8 +1745,9 @@ function createApiClient({
1740
1745
  return await httpClient.post(
1741
1746
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/metadata`,
1742
1747
  await authManager.getAuthValue({
1743
- requestedScope: "comments:read",
1744
- roomId: options.roomId
1748
+ roomId: options.roomId,
1749
+ resource: "comments",
1750
+ access: "write"
1745
1751
  }),
1746
1752
  options.metadata
1747
1753
  );
@@ -1750,8 +1756,9 @@ function createApiClient({
1750
1756
  return await httpClient.post(
1751
1757
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/metadata`,
1752
1758
  await authManager.getAuthValue({
1753
- requestedScope: "comments:read",
1754
- roomId: options.roomId
1759
+ roomId: options.roomId,
1760
+ resource: "comments",
1761
+ access: "write"
1755
1762
  }),
1756
1763
  options.metadata
1757
1764
  );
@@ -1761,8 +1768,9 @@ function createApiClient({
1761
1768
  const comment = await httpClient.post(
1762
1769
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments`,
1763
1770
  await authManager.getAuthValue({
1764
- requestedScope: "comments:read",
1765
- roomId: options.roomId
1771
+ roomId: options.roomId,
1772
+ resource: "comments",
1773
+ access: "write"
1766
1774
  }),
1767
1775
  {
1768
1776
  id: commentId,
@@ -1777,8 +1785,9 @@ function createApiClient({
1777
1785
  const comment = await httpClient.post(
1778
1786
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}`,
1779
1787
  await authManager.getAuthValue({
1780
- requestedScope: "comments:read",
1781
- roomId: options.roomId
1788
+ roomId: options.roomId,
1789
+ resource: "comments",
1790
+ access: "write"
1782
1791
  }),
1783
1792
  {
1784
1793
  body: options.body,
@@ -1792,8 +1801,9 @@ function createApiClient({
1792
1801
  await httpClient.delete(
1793
1802
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}`,
1794
1803
  await authManager.getAuthValue({
1795
- requestedScope: "comments:read",
1796
- roomId: options.roomId
1804
+ roomId: options.roomId,
1805
+ resource: "comments",
1806
+ access: "write"
1797
1807
  })
1798
1808
  );
1799
1809
  }
@@ -1801,8 +1811,9 @@ function createApiClient({
1801
1811
  const reaction = await httpClient.post(
1802
1812
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/reactions`,
1803
1813
  await authManager.getAuthValue({
1804
- requestedScope: "comments:read",
1805
- roomId: options.roomId
1814
+ roomId: options.roomId,
1815
+ resource: "comments",
1816
+ access: "write"
1806
1817
  }),
1807
1818
  { emoji: options.emoji }
1808
1819
  );
@@ -1812,8 +1823,9 @@ function createApiClient({
1812
1823
  await httpClient.delete(
1813
1824
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/reactions/${options.emoji}`,
1814
1825
  await authManager.getAuthValue({
1815
- requestedScope: "comments:read",
1816
- roomId: options.roomId
1826
+ roomId: options.roomId,
1827
+ resource: "comments",
1828
+ access: "write"
1817
1829
  })
1818
1830
  );
1819
1831
  }
@@ -1821,8 +1833,9 @@ function createApiClient({
1821
1833
  await httpClient.post(
1822
1834
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/mark-as-resolved`,
1823
1835
  await authManager.getAuthValue({
1824
- requestedScope: "comments:read",
1825
- roomId: options.roomId
1836
+ roomId: options.roomId,
1837
+ resource: "comments",
1838
+ access: "write"
1826
1839
  })
1827
1840
  );
1828
1841
  }
@@ -1830,8 +1843,9 @@ function createApiClient({
1830
1843
  await httpClient.post(
1831
1844
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/mark-as-unresolved`,
1832
1845
  await authManager.getAuthValue({
1833
- requestedScope: "comments:read",
1834
- roomId: options.roomId
1846
+ roomId: options.roomId,
1847
+ resource: "comments",
1848
+ access: "write"
1835
1849
  })
1836
1850
  );
1837
1851
  }
@@ -1839,8 +1853,9 @@ function createApiClient({
1839
1853
  const subscription = await httpClient.post(
1840
1854
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/subscribe`,
1841
1855
  await authManager.getAuthValue({
1842
- requestedScope: "comments:read",
1843
- roomId: options.roomId
1856
+ roomId: options.roomId,
1857
+ resource: "comments",
1858
+ access: "read"
1844
1859
  })
1845
1860
  );
1846
1861
  return convertToSubscriptionData(subscription);
@@ -1849,8 +1864,9 @@ function createApiClient({
1849
1864
  await httpClient.post(
1850
1865
  url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/unsubscribe`,
1851
1866
  await authManager.getAuthValue({
1852
- requestedScope: "comments:read",
1853
- roomId: options.roomId
1867
+ roomId: options.roomId,
1868
+ resource: "comments",
1869
+ access: "read"
1854
1870
  })
1855
1871
  );
1856
1872
  }
@@ -1906,8 +1922,9 @@ function createApiClient({
1906
1922
  async () => httpClient.putBlob(
1907
1923
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/upload/${encodeURIComponent(attachment.name)}`,
1908
1924
  await authManager.getAuthValue({
1909
- requestedScope: "comments:read",
1910
- roomId
1925
+ roomId,
1926
+ resource: "comments",
1927
+ access: "write"
1911
1928
  }),
1912
1929
  attachment.file,
1913
1930
  { fileSize: attachment.size },
@@ -1924,8 +1941,9 @@ function createApiClient({
1924
1941
  async () => httpClient.post(
1925
1942
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${encodeURIComponent(attachment.name)}`,
1926
1943
  await authManager.getAuthValue({
1927
- requestedScope: "comments:read",
1928
- roomId
1944
+ roomId,
1945
+ resource: "comments",
1946
+ access: "write"
1929
1947
  }),
1930
1948
  void 0,
1931
1949
  { signal: abortSignal },
@@ -1950,8 +1968,9 @@ function createApiClient({
1950
1968
  async () => httpClient.putBlob(
1951
1969
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${createMultiPartUpload.uploadId}/${String(partNumber)}`,
1952
1970
  await authManager.getAuthValue({
1953
- requestedScope: "comments:read",
1954
- roomId
1971
+ roomId,
1972
+ resource: "comments",
1973
+ access: "write"
1955
1974
  }),
1956
1975
  part,
1957
1976
  void 0,
@@ -1974,8 +1993,9 @@ function createApiClient({
1974
1993
  return httpClient.post(
1975
1994
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${uploadId}/complete`,
1976
1995
  await authManager.getAuthValue({
1977
- requestedScope: "comments:read",
1978
- roomId
1996
+ roomId,
1997
+ resource: "comments",
1998
+ access: "write"
1979
1999
  }),
1980
2000
  { parts: sortedUploadedParts },
1981
2001
  { signal: abortSignal }
@@ -1986,8 +2006,9 @@ function createApiClient({
1986
2006
  await httpClient.rawDelete(
1987
2007
  url`/v2/c/rooms/${roomId}/attachments/${attachment.id}/multipart/${uploadId}`,
1988
2008
  await authManager.getAuthValue({
1989
- requestedScope: "comments:read",
1990
- roomId
2009
+ roomId,
2010
+ resource: "comments",
2011
+ access: "write"
1991
2012
  })
1992
2013
  );
1993
2014
  } catch {
@@ -2004,8 +2025,9 @@ function createApiClient({
2004
2025
  const { urls } = await httpClient.post(
2005
2026
  url`/v2/c/rooms/${roomId}/attachments/presigned-urls`,
2006
2027
  await authManager.getAuthValue({
2007
- requestedScope: "comments:read",
2008
- roomId
2028
+ roomId,
2029
+ resource: "comments",
2030
+ access: "read"
2009
2031
  }),
2010
2032
  { attachmentIds }
2011
2033
  );
@@ -2024,109 +2046,13 @@ function createApiClient({
2024
2046
  const batch2 = getOrCreateAttachmentUrlsStore(options.roomId).batch;
2025
2047
  return batch2.get(options.attachmentId);
2026
2048
  }
2027
- async function uploadChatAttachment(options) {
2028
- const { chatId, attachment, signal } = options;
2029
- const userId = currentUserId.get();
2030
- if (userId === void 0) {
2031
- throw new Error("Attachment upload requires an authenticated user.");
2032
- }
2033
- const ATTACHMENT_PART_SIZE = 5 * 1024 * 1024;
2034
- if (options.attachment.file.size <= ATTACHMENT_PART_SIZE) {
2035
- await httpClient.putBlob(
2036
- url`/v2/c/chats/${chatId}/attachments/${attachment.id}/upload/${encodeURIComponent(attachment.file.name)}`,
2037
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2038
- attachment.file,
2039
- { fileSize: attachment.file.size },
2040
- { signal }
2041
- );
2042
- } else {
2043
- const multipartUpload = await httpClient.post(
2044
- url`/v2/c/chats/${chatId}/attachments/${attachment.id}/multipart/${encodeURIComponent(attachment.file.name)}`,
2045
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2046
- void 0,
2047
- { signal },
2048
- { fileSize: attachment.file.size }
2049
- );
2050
- try {
2051
- const uploadedParts = [];
2052
- const parts = [];
2053
- let start = 0;
2054
- while (start < attachment.file.size) {
2055
- const end = Math.min(
2056
- start + ATTACHMENT_PART_SIZE,
2057
- attachment.file.size
2058
- );
2059
- parts.push({
2060
- number: parts.length + 1,
2061
- part: attachment.file.slice(start, end)
2062
- });
2063
- start = end;
2064
- }
2065
- uploadedParts.push(
2066
- ...await Promise.all(
2067
- parts.map(async ({ number, part }) => {
2068
- return await httpClient.putBlob(
2069
- url`/v2/c/chats/${chatId}/attachments/${attachment.id}/multipart/${multipartUpload.uploadId}/${String(number)}`,
2070
- await authManager.getAuthValue({
2071
- requestedScope: "comments:read"
2072
- }),
2073
- part,
2074
- void 0,
2075
- { signal }
2076
- );
2077
- })
2078
- )
2079
- );
2080
- await httpClient.post(
2081
- url`/v2/c/chats/${chatId}/attachments/${attachment.id}/multipart/${multipartUpload.uploadId}/complete`,
2082
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2083
- { parts: uploadedParts.sort((a, b) => a.number - b.number) },
2084
- { signal }
2085
- );
2086
- } catch (err) {
2087
- try {
2088
- await httpClient.delete(
2089
- url`/v2/c/chats/${chatId}/attachments/${attachment.id}/multipart/${multipartUpload.uploadId}`,
2090
- await authManager.getAuthValue({ requestedScope: "comments:read" })
2091
- );
2092
- } catch {
2093
- }
2094
- throw err;
2095
- }
2096
- }
2097
- }
2098
- const attachmentUrlsBatchStoresByChat = new DefaultMap((chatId) => {
2099
- const batch2 = new Batch(
2100
- async (batchedAttachmentIds) => {
2101
- const attachmentIds = batchedAttachmentIds.flat();
2102
- const { urls } = await httpClient.post(
2103
- url`/v2/c/chats/${chatId}/attachments/presigned-urls`,
2104
- await authManager.getAuthValue({
2105
- requestedScope: "comments:read"
2106
- }),
2107
- { attachmentIds }
2108
- );
2109
- return urls.map(
2110
- (url2) => url2 ?? new Error("There was an error while getting this attachment's URL")
2111
- );
2112
- },
2113
- { delay: 50 }
2114
- );
2115
- return createBatchStore(batch2);
2116
- });
2117
- function getOrCreateChatAttachmentUrlsStore(chatId) {
2118
- return attachmentUrlsBatchStoresByChat.getOrCreate(chatId);
2119
- }
2120
- function getChatAttachmentUrl(options) {
2121
- const batch2 = getOrCreateChatAttachmentUrlsStore(options.chatId).batch;
2122
- return batch2.get(options.attachmentId);
2123
- }
2124
2049
  async function getSubscriptionSettings(options) {
2125
2050
  return httpClient.get(
2126
2051
  url`/v2/c/rooms/${options.roomId}/subscription-settings`,
2127
2052
  await authManager.getAuthValue({
2128
- requestedScope: "comments:read",
2129
- roomId: options.roomId
2053
+ roomId: options.roomId,
2054
+ resource: "comments",
2055
+ access: "read"
2130
2056
  }),
2131
2057
  void 0,
2132
2058
  {
@@ -2138,8 +2064,9 @@ function createApiClient({
2138
2064
  return httpClient.post(
2139
2065
  url`/v2/c/rooms/${options.roomId}/subscription-settings`,
2140
2066
  await authManager.getAuthValue({
2141
- requestedScope: "comments:read",
2142
- roomId: options.roomId
2067
+ roomId: options.roomId,
2068
+ resource: "comments",
2069
+ access: "read"
2143
2070
  }),
2144
2071
  options.settings
2145
2072
  );
@@ -2151,8 +2078,9 @@ function createApiClient({
2151
2078
  await httpClient.post(
2152
2079
  url`/v2/c/rooms/${roomId}/inbox-notifications/read`,
2153
2080
  await authManager.getAuthValue({
2154
- requestedScope: "comments:read",
2155
- roomId
2081
+ roomId,
2082
+ resource: "comments",
2083
+ access: "read"
2156
2084
  }),
2157
2085
  { inboxNotificationIds }
2158
2086
  );
@@ -2172,8 +2100,9 @@ function createApiClient({
2172
2100
  await httpClient.rawPost(
2173
2101
  url`/v2/c/rooms/${options.roomId}/text-mentions`,
2174
2102
  await authManager.getAuthValue({
2175
- requestedScope: "comments:read",
2176
- roomId: options.roomId
2103
+ roomId: options.roomId,
2104
+ resource: "storage",
2105
+ access: "write"
2177
2106
  }),
2178
2107
  {
2179
2108
  userId: options.mention.kind === "user" ? options.mention.id : void 0,
@@ -2187,8 +2116,9 @@ function createApiClient({
2187
2116
  await httpClient.rawDelete(
2188
2117
  url`/v2/c/rooms/${options.roomId}/text-mentions/${options.mentionId}`,
2189
2118
  await authManager.getAuthValue({
2190
- requestedScope: "comments:read",
2191
- roomId: options.roomId
2119
+ roomId: options.roomId,
2120
+ resource: "storage",
2121
+ access: "write"
2192
2122
  })
2193
2123
  );
2194
2124
  }
@@ -2196,8 +2126,9 @@ function createApiClient({
2196
2126
  return httpClient.rawGet(
2197
2127
  url`/v2/c/rooms/${options.roomId}/y-version/${options.versionId}`,
2198
2128
  await authManager.getAuthValue({
2199
- requestedScope: "comments:read",
2200
- roomId: options.roomId
2129
+ roomId: options.roomId,
2130
+ resource: "storage",
2131
+ access: "read"
2201
2132
  })
2202
2133
  );
2203
2134
  }
@@ -2205,8 +2136,9 @@ function createApiClient({
2205
2136
  await httpClient.rawPost(
2206
2137
  url`/v2/c/rooms/${options.roomId}/version`,
2207
2138
  await authManager.getAuthValue({
2208
- requestedScope: "comments:read",
2209
- roomId: options.roomId
2139
+ roomId: options.roomId,
2140
+ resource: "storage",
2141
+ access: "write"
2210
2142
  })
2211
2143
  );
2212
2144
  }
@@ -2214,8 +2146,9 @@ function createApiClient({
2214
2146
  await httpClient.rawPost(
2215
2147
  url`/v2/c/rooms/${options.roomId}/text-metadata`,
2216
2148
  await authManager.getAuthValue({
2217
- requestedScope: "comments:read",
2218
- roomId: options.roomId
2149
+ roomId: options.roomId,
2150
+ resource: "storage",
2151
+ access: "read"
2219
2152
  }),
2220
2153
  {
2221
2154
  type: options.type,
@@ -2227,8 +2160,9 @@ function createApiClient({
2227
2160
  const result = await httpClient.post(
2228
2161
  url`/v2/c/rooms/${options.roomId}/ai/contextual-prompt`,
2229
2162
  await authManager.getAuthValue({
2230
- requestedScope: "room:read",
2231
- roomId: options.roomId
2163
+ roomId: options.roomId,
2164
+ resource: "storage",
2165
+ access: "read"
2232
2166
  }),
2233
2167
  {
2234
2168
  prompt: options.prompt,
@@ -2250,8 +2184,9 @@ function createApiClient({
2250
2184
  const result = await httpClient.get(
2251
2185
  url`/v2/c/rooms/${options.roomId}/versions`,
2252
2186
  await authManager.getAuthValue({
2253
- requestedScope: "comments:read",
2254
- roomId: options.roomId
2187
+ roomId: options.roomId,
2188
+ resource: "storage",
2189
+ access: "read"
2255
2190
  })
2256
2191
  );
2257
2192
  return {
@@ -2268,8 +2203,9 @@ function createApiClient({
2268
2203
  const result = await httpClient.get(
2269
2204
  url`/v2/c/rooms/${options.roomId}/versions/delta`,
2270
2205
  await authManager.getAuthValue({
2271
- requestedScope: "comments:read",
2272
- roomId: options.roomId
2206
+ roomId: options.roomId,
2207
+ resource: "storage",
2208
+ access: "read"
2273
2209
  }),
2274
2210
  { since: options.since.toISOString() },
2275
2211
  { signal: options.signal }
@@ -2288,8 +2224,9 @@ function createApiClient({
2288
2224
  const result = await httpClient.rawGet(
2289
2225
  url`/v2/c/rooms/${options.roomId}/storage`,
2290
2226
  await authManager.getAuthValue({
2291
- requestedScope: "room:read",
2292
- roomId: options.roomId
2227
+ roomId: options.roomId,
2228
+ resource: "storage",
2229
+ access: "read"
2293
2230
  })
2294
2231
  );
2295
2232
  return await result.json();
@@ -2302,7 +2239,7 @@ function createApiClient({
2302
2239
  }
2303
2240
  const json = await httpClient.get(
2304
2241
  url`/v2/c/inbox-notifications`,
2305
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2242
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2306
2243
  {
2307
2244
  cursor: options?.cursor,
2308
2245
  limit: PAGE_SIZE,
@@ -2328,7 +2265,7 @@ function createApiClient({
2328
2265
  }
2329
2266
  const json = await httpClient.get(
2330
2267
  url`/v2/c/inbox-notifications/delta`,
2331
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2268
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2332
2269
  { since: options.since.toISOString(), query },
2333
2270
  { signal: options.signal }
2334
2271
  );
@@ -2357,7 +2294,7 @@ function createApiClient({
2357
2294
  }
2358
2295
  const { count } = await httpClient.get(
2359
2296
  url`/v2/c/inbox-notifications/count`,
2360
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2297
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2361
2298
  { query },
2362
2299
  { signal: options?.signal }
2363
2300
  );
@@ -2366,7 +2303,7 @@ function createApiClient({
2366
2303
  async function markAllInboxNotificationsAsRead() {
2367
2304
  await httpClient.post(
2368
2305
  url`/v2/c/inbox-notifications/read`,
2369
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2306
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2370
2307
  {
2371
2308
  inboxNotificationIds: "all"
2372
2309
  }
@@ -2375,7 +2312,7 @@ function createApiClient({
2375
2312
  async function markInboxNotificationsAsRead(inboxNotificationIds) {
2376
2313
  await httpClient.post(
2377
2314
  url`/v2/c/inbox-notifications/read`,
2378
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2315
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2379
2316
  {
2380
2317
  inboxNotificationIds
2381
2318
  }
@@ -2395,19 +2332,19 @@ function createApiClient({
2395
2332
  async function deleteAllInboxNotifications() {
2396
2333
  await httpClient.delete(
2397
2334
  url`/v2/c/inbox-notifications`,
2398
- await authManager.getAuthValue({ requestedScope: "comments:read" })
2335
+ await authManager.getAuthValue({ resource: "personal", access: "write" })
2399
2336
  );
2400
2337
  }
2401
2338
  async function deleteInboxNotification(inboxNotificationId) {
2402
2339
  await httpClient.delete(
2403
2340
  url`/v2/c/inbox-notifications/${inboxNotificationId}`,
2404
- await authManager.getAuthValue({ requestedScope: "comments:read" })
2341
+ await authManager.getAuthValue({ resource: "personal", access: "write" })
2405
2342
  );
2406
2343
  }
2407
2344
  async function getNotificationSettings(options) {
2408
2345
  return httpClient.get(
2409
2346
  url`/v2/c/notification-settings`,
2410
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2347
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2411
2348
  void 0,
2412
2349
  { signal: options?.signal }
2413
2350
  );
@@ -2415,7 +2352,7 @@ function createApiClient({
2415
2352
  async function updateNotificationSettings(settings) {
2416
2353
  return httpClient.post(
2417
2354
  url`/v2/c/notification-settings`,
2418
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2355
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2419
2356
  settings
2420
2357
  );
2421
2358
  }
@@ -2427,7 +2364,7 @@ function createApiClient({
2427
2364
  const PAGE_SIZE = 50;
2428
2365
  const json = await httpClient.get(
2429
2366
  url`/v2/c/threads`,
2430
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2367
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2431
2368
  {
2432
2369
  cursor: options?.cursor,
2433
2370
  query,
@@ -2448,7 +2385,7 @@ function createApiClient({
2448
2385
  async function getUserThreadsSince_experimental(options) {
2449
2386
  const json = await httpClient.get(
2450
2387
  url`/v2/c/threads/delta`,
2451
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2388
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2452
2389
  { since: options.since.toISOString() },
2453
2390
  { signal: options.signal }
2454
2391
  );
@@ -2477,7 +2414,8 @@ function createApiClient({
2477
2414
  const { groups: plainGroups } = await httpClient.post(
2478
2415
  url`/v2/c/groups/find`,
2479
2416
  await authManager.getAuthValue({
2480
- requestedScope: "comments:read"
2417
+ resource: "personal",
2418
+ access: "write"
2481
2419
  }),
2482
2420
  { groupIds }
2483
2421
  );
@@ -2496,7 +2434,7 @@ function createApiClient({
2496
2434
  async function getUrlMetadata(_url) {
2497
2435
  const { metadata } = await httpClient.get(
2498
2436
  url`/v2/c/urls/metadata`,
2499
- await authManager.getAuthValue({ requestedScope: "comments:read" }),
2437
+ await authManager.getAuthValue({ resource: "personal", access: "write" }),
2500
2438
  { url: _url }
2501
2439
  );
2502
2440
  return metadata;
@@ -2536,10 +2474,6 @@ function createApiClient({
2536
2474
  getAttachmentUrl,
2537
2475
  uploadAttachment,
2538
2476
  getOrCreateAttachmentUrlsStore,
2539
- // User attachments
2540
- uploadChatAttachment,
2541
- getOrCreateChatAttachmentUrlsStore,
2542
- getChatAttachmentUrl,
2543
2477
  // Room storage
2544
2478
  streamStorage,
2545
2479
  // Notifications
@@ -3913,6 +3847,7 @@ var ManagedSocket = class {
3913
3847
 
3914
3848
  // src/internal.ts
3915
3849
  var kInternal = /* @__PURE__ */ Symbol();
3850
+ var kStorageUpdateSource = /* @__PURE__ */ Symbol();
3916
3851
 
3917
3852
  // src/lib/IncrementalJsonParser.ts
3918
3853
  var EMPTY_OBJECT = Object.freeze({});
@@ -5233,22 +5168,327 @@ function createReceivingToolInvocation(invocationId, name, partialArgsText = "")
5233
5168
  };
5234
5169
  }
5235
5170
 
5236
- // src/protocol/AuthToken.ts
5237
- var Permission = /* @__PURE__ */ ((Permission2) => {
5238
- Permission2["Read"] = "room:read";
5239
- Permission2["Write"] = "room:write";
5240
- Permission2["PresenceWrite"] = "room:presence:write";
5241
- Permission2["CommentsWrite"] = "comments:write";
5242
- Permission2["CommentsRead"] = "comments:read";
5243
- Permission2["FeedsWrite"] = "feeds:write";
5244
- return Permission2;
5245
- })(Permission || {});
5246
- function canWriteStorage(scopes) {
5247
- return scopes.includes("room:write" /* Write */);
5248
- }
5249
- function canComment(scopes) {
5250
- return scopes.includes("comments:write" /* CommentsWrite */) || scopes.includes("room:write" /* Write */);
5171
+ // src/permissions.ts
5172
+ var Permission = {
5173
+ /**
5174
+ * Default permission for a room.
5175
+ */
5176
+ Read: "*:read",
5177
+ Write: "*:write",
5178
+ /**
5179
+ * Legacy aliases for default room permissions.
5180
+ */
5181
+ RoomWrite: "room:write",
5182
+ RoomRead: "room:read",
5183
+ /**
5184
+ * Storage
5185
+ */
5186
+ StorageRead: "storage:read",
5187
+ StorageWrite: "storage:write",
5188
+ StorageNone: "storage:none",
5189
+ /**
5190
+ * Comments
5191
+ */
5192
+ CommentsWrite: "comments:write",
5193
+ CommentsRead: "comments:read",
5194
+ CommentsNone: "comments:none",
5195
+ /**
5196
+ * Feeds
5197
+ */
5198
+ FeedsRead: "feeds:read",
5199
+ FeedsWrite: "feeds:write",
5200
+ FeedsNone: "feeds:none",
5201
+ /**
5202
+ * Legacy
5203
+ */
5204
+ LegacyRoomPresenceWrite: "room:presence:write"
5205
+ };
5206
+ var ACCESS_LEVELS = ["none", "read", "write"];
5207
+ var basePermissionScopes = /* @__PURE__ */ new Set([
5208
+ Permission.Read,
5209
+ Permission.Write,
5210
+ Permission.RoomRead,
5211
+ Permission.RoomWrite
5212
+ ]);
5213
+ var ACCESS_LEVEL_RANKS = {
5214
+ none: 0,
5215
+ read: 1,
5216
+ write: 2
5217
+ };
5218
+ var PERMISSIONS_BY_RESOURCE = {
5219
+ room: {
5220
+ read: [Permission.Read, Permission.RoomRead],
5221
+ write: [Permission.Write, Permission.RoomWrite]
5222
+ },
5223
+ personal: {
5224
+ write: []
5225
+ },
5226
+ storage: {
5227
+ write: [Permission.StorageWrite],
5228
+ read: [Permission.StorageRead],
5229
+ none: [Permission.StorageNone]
5230
+ },
5231
+ comments: {
5232
+ write: [Permission.CommentsWrite],
5233
+ read: [Permission.CommentsRead],
5234
+ none: [Permission.CommentsNone]
5235
+ },
5236
+ feeds: {
5237
+ write: [Permission.FeedsWrite],
5238
+ read: [Permission.FeedsRead],
5239
+ none: [Permission.FeedsNone]
5240
+ }
5241
+ };
5242
+ var NO_PERMISSION_MATRIX = {
5243
+ room: "none",
5244
+ storage: "none",
5245
+ comments: "none",
5246
+ feeds: "none",
5247
+ personal: "none"
5248
+ };
5249
+ var BASE_PERMISSION_RESOURCE = "room";
5250
+ var ROOM_PERMISSION_RESOURCES = [
5251
+ "storage",
5252
+ "comments",
5253
+ "feeds"
5254
+ ];
5255
+ var VALID_PERMISSIONS = new Set(Object.values(Permission));
5256
+ function isPermission(permission) {
5257
+ return VALID_PERMISSIONS.has(permission);
5258
+ }
5259
+ function resolveResourceAccess(scopes, resource) {
5260
+ const permissions = PERMISSIONS_BY_RESOURCE[resource];
5261
+ let resourceAccess;
5262
+ for (const access of ACCESS_LEVELS) {
5263
+ const scopedPermissions = permissions[access];
5264
+ if (scopedPermissions !== void 0 && scopedPermissions.some((permission) => scopes.includes(permission))) {
5265
+ resourceAccess = access;
5266
+ }
5267
+ }
5268
+ return resourceAccess;
5269
+ }
5270
+ function permissionMatrixFromResolvedScopes(resolved) {
5271
+ if (!resolved.hasDefaultPermission) {
5272
+ return { ...NO_PERMISSION_MATRIX };
5273
+ }
5274
+ const matrix = {
5275
+ ...NO_PERMISSION_MATRIX,
5276
+ [BASE_PERMISSION_RESOURCE]: resolved.baseAccess,
5277
+ personal: "write"
5278
+ };
5279
+ for (const resource of ROOM_PERMISSION_RESOURCES) {
5280
+ matrix[resource] = resolved.matrix[resource] ?? resolved.baseAccess;
5281
+ }
5282
+ return matrix;
5283
+ }
5284
+ function permissionMatrixFromScopes(scopes) {
5285
+ return permissionMatrixFromResolvedScopes(resolvePermissionScopes(scopes));
5286
+ }
5287
+ function resolvePermissionScopes(scopes) {
5288
+ const hasDefaultPermission = scopes.includes(Permission.Write) || scopes.includes(Permission.Read) || scopes.includes(Permission.RoomWrite) || scopes.includes(Permission.RoomRead);
5289
+ const baseAccess = scopes.includes(Permission.Write) || scopes.includes(Permission.RoomWrite) ? "write" : scopes.includes(Permission.Read) || scopes.includes(Permission.RoomRead) ? "read" : "none";
5290
+ const matrix = {};
5291
+ for (const resource of ROOM_PERMISSION_RESOURCES) {
5292
+ const access = resolveResourceAccess(scopes, resource);
5293
+ if (access !== void 0) {
5294
+ matrix[resource] = access;
5295
+ }
5296
+ }
5297
+ return { hasDefaultPermission, baseAccess, matrix };
5298
+ }
5299
+ function hasPermissionAccess(matrix, resource, requiredAccess) {
5300
+ const access = matrix[resource] ?? "none";
5301
+ return ACCESS_LEVEL_RANKS[access] >= ACCESS_LEVEL_RANKS[requiredAccess];
5302
+ }
5303
+ function resolveRoomPermissionMatrix(permissions, roomId) {
5304
+ const matchedPermissions = permissions.filter(
5305
+ (entry) => roomPatternMatches(entry.pattern, roomId)
5306
+ );
5307
+ if (matchedPermissions.length === 0) {
5308
+ return void 0;
5309
+ }
5310
+ let hasDefaultPermission = false;
5311
+ let baseAccess = "none";
5312
+ const explicitMatrix = {};
5313
+ const explicitSpecificity = {};
5314
+ for (const entry of matchedPermissions) {
5315
+ const resolved = resolvePermissionScopes(entry.scopes);
5316
+ const specificity = roomPatternSpecificity(entry.pattern);
5317
+ if (resolved.hasDefaultPermission) {
5318
+ hasDefaultPermission = true;
5319
+ baseAccess = strongestAccess(baseAccess, resolved.baseAccess);
5320
+ }
5321
+ for (const resource of ROOM_PERMISSION_RESOURCES) {
5322
+ const access = resolved.matrix[resource];
5323
+ if (access !== void 0) {
5324
+ const currentSpecificity = explicitSpecificity[resource] ?? -1;
5325
+ if (specificity > currentSpecificity) {
5326
+ explicitMatrix[resource] = access;
5327
+ explicitSpecificity[resource] = specificity;
5328
+ } else if (specificity === currentSpecificity) {
5329
+ explicitMatrix[resource] = strongestAccess(
5330
+ explicitMatrix[resource] ?? "none",
5331
+ access
5332
+ );
5333
+ }
5334
+ }
5335
+ }
5336
+ }
5337
+ return permissionMatrixFromResolvedScopes({
5338
+ hasDefaultPermission,
5339
+ baseAccess,
5340
+ matrix: explicitMatrix
5341
+ });
5342
+ }
5343
+ function normalizeRoomPermissions(permissions) {
5344
+ if (!Array.isArray(permissions)) {
5345
+ throw new Error("Permission list must be an array");
5346
+ }
5347
+ const result = [];
5348
+ for (const permission of permissions) {
5349
+ if (!isPermission(permission)) {
5350
+ throw new Error(`Not a valid permission: ${permission}`);
5351
+ }
5352
+ result.push(permission);
5353
+ }
5354
+ return result;
5355
+ }
5356
+ function normalizeRoomAccesses(accesses) {
5357
+ if (accesses === void 0) {
5358
+ return void 0;
5359
+ }
5360
+ return Object.fromEntries(
5361
+ Object.entries(accesses).map(([id, permissions]) => [
5362
+ id,
5363
+ normalizeRoomPermissions(permissions)
5364
+ ])
5365
+ );
5366
+ }
5367
+ function normalizeUpdateRoomAccesses(accesses) {
5368
+ if (accesses === void 0) {
5369
+ return void 0;
5370
+ }
5371
+ return Object.fromEntries(
5372
+ Object.entries(accesses).map(([id, permissions]) => [
5373
+ id,
5374
+ permissions === null ? null : normalizeRoomPermissions(permissions)
5375
+ ])
5376
+ );
5377
+ }
5378
+ function permissionMatrixToScopes(matrix) {
5379
+ const scopes = [];
5380
+ const baseAccess = matrix.room;
5381
+ if (baseAccess !== "none") {
5382
+ scopes.push(permissionForAccessLevel(BASE_PERMISSION_RESOURCE, baseAccess));
5383
+ }
5384
+ for (const resource of ROOM_PERMISSION_RESOURCES) {
5385
+ const access = matrix[resource];
5386
+ if (access !== baseAccess) {
5387
+ scopes.push(permissionForAccessLevel(resource, access));
5388
+ }
5389
+ }
5390
+ return scopes;
5391
+ }
5392
+ function mergeRoomPermissionScopes({
5393
+ defaultAccesses,
5394
+ groupsAccesses,
5395
+ userAccesses
5396
+ }) {
5397
+ const sources = [
5398
+ resolvePermissionScopes(defaultAccesses),
5399
+ mergeResolvedScopesByHighestAccess(
5400
+ groupsAccesses.map(resolvePermissionScopes)
5401
+ ),
5402
+ resolvePermissionScopes(userAccesses)
5403
+ ];
5404
+ const merged = {
5405
+ hasDefaultPermission: false,
5406
+ baseAccess: "none",
5407
+ matrix: {}
5408
+ };
5409
+ for (const source of sources) {
5410
+ if (source.hasDefaultPermission) {
5411
+ merged.hasDefaultPermission = true;
5412
+ merged.baseAccess = source.baseAccess;
5413
+ }
5414
+ for (const resource of ROOM_PERMISSION_RESOURCES) {
5415
+ const access = source.matrix[resource];
5416
+ if (access !== void 0) {
5417
+ merged.matrix[resource] = access;
5418
+ }
5419
+ }
5420
+ }
5421
+ return permissionMatrixToScopes(permissionMatrixFromResolvedScopes(merged));
5422
+ }
5423
+ function mergeResolvedScopesByHighestAccess(sources) {
5424
+ const merged = {
5425
+ hasDefaultPermission: false,
5426
+ baseAccess: "none",
5427
+ matrix: {}
5428
+ };
5429
+ for (const source of sources) {
5430
+ if (source.hasDefaultPermission) {
5431
+ merged.hasDefaultPermission = true;
5432
+ merged.baseAccess = strongestAccess(merged.baseAccess, source.baseAccess);
5433
+ }
5434
+ for (const resource of ROOM_PERMISSION_RESOURCES) {
5435
+ const access = source.matrix[resource];
5436
+ if (access !== void 0) {
5437
+ merged.matrix[resource] = strongestAccess(
5438
+ merged.matrix[resource] ?? "none",
5439
+ access
5440
+ );
5441
+ }
5442
+ }
5443
+ }
5444
+ return merged;
5445
+ }
5446
+ function permissionForAccessLevel(resource, access, field = resource) {
5447
+ const levels = PERMISSIONS_BY_RESOURCE[resource];
5448
+ const permissions = levels[access];
5449
+ if (permissions === void 0 || permissions.length === 0) {
5450
+ throw new Error(
5451
+ `Invalid permission level for ${field}: ${JSON.stringify(access) ?? String(access)}`
5452
+ );
5453
+ }
5454
+ return permissions[0];
5455
+ }
5456
+ function strongestAccess(left, right) {
5457
+ return ACCESS_LEVEL_RANKS[right] > ACCESS_LEVEL_RANKS[left] ? right : left;
5251
5458
  }
5459
+ function roomPatternMatches(pattern, roomId) {
5460
+ if (pattern.includes("*")) {
5461
+ return roomId.startsWith(pattern.replace("*", ""));
5462
+ }
5463
+ return pattern === roomId;
5464
+ }
5465
+ function roomPatternSpecificity(pattern) {
5466
+ return pattern.replace("*", "").length;
5467
+ }
5468
+ function validatePermissionsSet(scopes) {
5469
+ const unknownScopes = scopes.filter((scope) => !VALID_PERMISSIONS.has(scope));
5470
+ if (unknownScopes.length > 0) {
5471
+ return `Unknown permission scope(s): ${unknownScopes.join(", ")}`;
5472
+ }
5473
+ const baseScopes = scopes.filter((scope) => basePermissionScopes.has(scope));
5474
+ if (baseScopes.length !== 1) {
5475
+ return `Permissions must include exactly one of ${Permission.Read}, ${Permission.Write} (or the legacy aliases ${Permission.RoomRead}, ${Permission.RoomWrite}), got ${baseScopes.length === 0 ? "none" : baseScopes.join(", ")}`;
5476
+ }
5477
+ const seenFeatures = /* @__PURE__ */ new Set();
5478
+ for (const scope of scopes) {
5479
+ if (basePermissionScopes.has(scope) || scope === Permission.LegacyRoomPresenceWrite) {
5480
+ continue;
5481
+ }
5482
+ const feature = scope.slice(0, scope.indexOf(":"));
5483
+ if (seenFeatures.has(feature)) {
5484
+ return `Permissions can include at most one scope per feature, got multiple "${feature}" scopes`;
5485
+ }
5486
+ seenFeatures.add(feature);
5487
+ }
5488
+ return true;
5489
+ }
5490
+
5491
+ // src/protocol/AuthToken.ts
5252
5492
  function isValidAuthTokenPayload(data) {
5253
5493
  return isPlainObject(data) && (data.k === "acc" /* ACCESS_TOKEN */ || data.k === "id" /* ID_TOKEN */);
5254
5494
  }
@@ -5287,47 +5527,22 @@ function createAuthManager(authOptions, onAuthenticate) {
5287
5527
  const authentication = prepareAuthentication(authOptions);
5288
5528
  const seenTokens = /* @__PURE__ */ new Set();
5289
5529
  const tokens = [];
5290
- const expiryTimes = [];
5291
5530
  const requestPromises = /* @__PURE__ */ new Map();
5292
5531
  function reset() {
5293
5532
  seenTokens.clear();
5294
5533
  tokens.length = 0;
5295
- expiryTimes.length = 0;
5296
5534
  requestPromises.clear();
5297
5535
  }
5298
- function hasCorrespondingScopes(requestedScope, scopes) {
5299
- if (requestedScope === "comments:read") {
5300
- return scopes.includes("comments:read" /* CommentsRead */) || scopes.includes("comments:write" /* CommentsWrite */) || scopes.includes("room:read" /* Read */) || scopes.includes("room:write" /* Write */);
5301
- } else if (requestedScope === "room:read") {
5302
- return scopes.includes("room:read" /* Read */) || scopes.includes("room:write" /* Write */);
5303
- }
5304
- return false;
5305
- }
5306
5536
  function getCachedToken(requestOptions) {
5307
5537
  const now2 = Math.ceil(Date.now() / 1e3);
5308
5538
  for (let i = tokens.length - 1; i >= 0; i--) {
5309
- const token = tokens[i];
5310
- const expiresAt = expiryTimes[i];
5311
- if (expiresAt <= now2) {
5539
+ const cachedToken = tokens[i];
5540
+ if (cachedToken.expiresAt <= now2) {
5312
5541
  tokens.splice(i, 1);
5313
- expiryTimes.splice(i, 1);
5314
5542
  continue;
5315
5543
  }
5316
- if (token.parsed.k === "id" /* ID_TOKEN */) {
5317
- return token;
5318
- } else if (token.parsed.k === "acc" /* ACCESS_TOKEN */) {
5319
- if (!requestOptions.roomId && Object.entries(token.parsed.perms).length === 0) {
5320
- return token;
5321
- }
5322
- for (const [resource, scopes] of Object.entries(token.parsed.perms)) {
5323
- if (!requestOptions.roomId) {
5324
- if (resource.includes("*") && hasCorrespondingScopes(requestOptions.requestedScope, scopes)) {
5325
- return token;
5326
- }
5327
- } else if (resource.includes("*") && requestOptions.roomId.startsWith(resource.replace("*", "")) || requestOptions.roomId === resource && hasCorrespondingScopes(requestOptions.requestedScope, scopes)) {
5328
- return token;
5329
- }
5330
- }
5544
+ if (cachedTokenSatisfiesRequest(cachedToken, requestOptions)) {
5545
+ return cachedToken.token;
5331
5546
  }
5332
5547
  }
5333
5548
  return void 0;
@@ -5345,6 +5560,10 @@ function createAuthManager(authOptions, onAuthenticate) {
5345
5560
  });
5346
5561
  const parsed = parseAuthToken(response.token);
5347
5562
  if (seenTokens.has(parsed.raw)) {
5563
+ const cachedToken = getCachedToken(options);
5564
+ if (cachedToken?.raw === parsed.raw) {
5565
+ return cachedToken;
5566
+ }
5348
5567
  throw new StopRetrying(
5349
5568
  "The same Liveblocks auth token was issued from the backend before. Caching Liveblocks tokens is not supported."
5350
5569
  );
@@ -5385,11 +5604,12 @@ function createAuthManager(authOptions, onAuthenticate) {
5385
5604
  return { type: "secret", token: cachedToken };
5386
5605
  }
5387
5606
  let currentPromise;
5388
- if (requestOptions.roomId) {
5389
- currentPromise = requestPromises.get(requestOptions.roomId);
5607
+ const requestKey = getAuthRequestKey(requestOptions);
5608
+ if (requestKey !== void 0) {
5609
+ currentPromise = requestPromises.get(requestKey);
5390
5610
  if (currentPromise === void 0) {
5391
5611
  currentPromise = makeAuthRequest(requestOptions);
5392
- requestPromises.set(requestOptions.roomId, currentPromise);
5612
+ requestPromises.set(requestKey, currentPromise);
5393
5613
  }
5394
5614
  } else {
5395
5615
  currentPromise = requestPromises.get("liveblocks-user-token");
@@ -5403,12 +5623,12 @@ function createAuthManager(authOptions, onAuthenticate) {
5403
5623
  const BUFFER = 30;
5404
5624
  const expiresAt = Math.floor(Date.now() / 1e3) + (token.parsed.exp - token.parsed.iat) - BUFFER;
5405
5625
  seenTokens.add(token.raw);
5406
- tokens.push(token);
5407
- expiryTimes.push(expiresAt);
5626
+ const cachedToken2 = makeCachedToken(token, expiresAt);
5627
+ tokens.push(cachedToken2);
5408
5628
  return { type: "secret", token };
5409
5629
  } finally {
5410
- if (requestOptions.roomId) {
5411
- requestPromises.delete(requestOptions.roomId);
5630
+ if (requestKey !== void 0) {
5631
+ requestPromises.delete(requestKey);
5412
5632
  } else {
5413
5633
  requestPromises.delete("liveblocks-user-token");
5414
5634
  }
@@ -5419,6 +5639,43 @@ function createAuthManager(authOptions, onAuthenticate) {
5419
5639
  getAuthValue
5420
5640
  };
5421
5641
  }
5642
+ function getAuthRequestKey(request) {
5643
+ if (request.roomId === void 0) {
5644
+ return void 0;
5645
+ }
5646
+ return `${request.roomId}:${request.resource}:${request.access}`;
5647
+ }
5648
+ function makeCachedToken(token, expiresAt) {
5649
+ if (token.parsed.k === "acc" /* ACCESS_TOKEN */) {
5650
+ return {
5651
+ token,
5652
+ expiresAt,
5653
+ permissions: Object.entries(token.parsed.perms).map(
5654
+ ([pattern, scopes]) => ({
5655
+ pattern,
5656
+ scopes: normalizeRoomPermissions(scopes)
5657
+ })
5658
+ )
5659
+ };
5660
+ }
5661
+ return { token, expiresAt };
5662
+ }
5663
+ function cachedTokenSatisfiesRequest(cachedToken, request) {
5664
+ if (cachedToken.token.parsed.k === "id" /* ID_TOKEN */) {
5665
+ return true;
5666
+ }
5667
+ if (request.resource === "personal") {
5668
+ return true;
5669
+ }
5670
+ if (request.roomId === void 0) {
5671
+ return false;
5672
+ }
5673
+ const matrix = resolveRoomPermissionMatrix(
5674
+ cachedToken.permissions ?? [],
5675
+ request.roomId
5676
+ );
5677
+ return matrix !== void 0 && hasPermissionAccess(matrix, request.resource, request.access);
5678
+ }
5422
5679
  function prepareAuthentication(authOptions) {
5423
5680
  const { publicApiKey, authEndpoint } = authOptions;
5424
5681
  if (authEndpoint !== void 0 && publicApiKey !== void 0) {
@@ -5511,13 +5768,15 @@ var OpCode = Object.freeze({
5511
5768
  DELETE_CRDT: 5,
5512
5769
  DELETE_OBJECT_KEY: 6,
5513
5770
  CREATE_MAP: 7,
5514
- CREATE_REGISTER: 8
5771
+ CREATE_REGISTER: 8,
5772
+ CREATE_TEXT: 9,
5773
+ UPDATE_TEXT: 10
5515
5774
  });
5516
5775
  function isIgnoredOp(op) {
5517
5776
  return op.type === OpCode.DELETE_CRDT && op.id === "ACK";
5518
5777
  }
5519
5778
  function isCreateOp(op) {
5520
- return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST;
5779
+ return op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_REGISTER || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_TEXT;
5521
5780
  }
5522
5781
 
5523
5782
  // src/protocol/StorageNode.ts
@@ -5525,7 +5784,8 @@ var CrdtType = Object.freeze({
5525
5784
  OBJECT: 0,
5526
5785
  LIST: 1,
5527
5786
  MAP: 2,
5528
- REGISTER: 3
5787
+ REGISTER: 3,
5788
+ TEXT: 4
5529
5789
  });
5530
5790
  function isRootStorageNode(node) {
5531
5791
  return node[0] === "root";
@@ -5542,6 +5802,9 @@ function isMapStorageNode(node) {
5542
5802
  function isRegisterStorageNode(node) {
5543
5803
  return node[1].type === CrdtType.REGISTER;
5544
5804
  }
5805
+ function isTextStorageNode(node) {
5806
+ return node[1].type === CrdtType.TEXT;
5807
+ }
5545
5808
  function isCompactRootNode(node) {
5546
5809
  return node[0] === "root";
5547
5810
  }
@@ -5564,6 +5827,9 @@ function* compactNodesToNodeStream(compactNodes) {
5564
5827
  case CrdtType.REGISTER:
5565
5828
  yield [cnode[0], { type: CrdtType.REGISTER, parentId: cnode[2], parentKey: cnode[3], data: cnode[4] }];
5566
5829
  break;
5830
+ case CrdtType.TEXT:
5831
+ yield [cnode[0], { type: CrdtType.TEXT, parentId: cnode[2], parentKey: cnode[3], data: cnode[4], version: cnode[5] }];
5832
+ break;
5567
5833
  default:
5568
5834
  }
5569
5835
  }
@@ -5592,6 +5858,17 @@ function* nodeStreamToCompactNodes(nodes) {
5592
5858
  const id = node[0];
5593
5859
  const crdt = node[1];
5594
5860
  yield [id, CrdtType.REGISTER, crdt.parentId, crdt.parentKey, crdt.data];
5861
+ } else if (isTextStorageNode(node)) {
5862
+ const id = node[0];
5863
+ const crdt = node[1];
5864
+ yield [
5865
+ id,
5866
+ CrdtType.TEXT,
5867
+ crdt.parentId,
5868
+ crdt.parentKey,
5869
+ crdt.data,
5870
+ crdt.version
5871
+ ];
5595
5872
  } else {
5596
5873
  }
5597
5874
  }
@@ -5784,6 +6061,9 @@ var UnacknowledgedOps = class {
5784
6061
  #createOpsByPosition = /* @__PURE__ */ new Map();
5785
6062
  // parentId -> (opId -> Create op)
5786
6063
  #createOpsByParent = /* @__PURE__ */ new Map();
6064
+ // opIds of pending ops that were in flight when a connection died, so the
6065
+ // server may already have processed them. See isPossiblyStored().
6066
+ #possiblyStoredOpIds = /* @__PURE__ */ new Set();
5787
6067
  #posKey(parentId, parentKey) {
5788
6068
  return `${parentId}
5789
6069
  ${parentKey}`;
@@ -5791,6 +6071,10 @@ ${parentKey}`;
5791
6071
  get size() {
5792
6072
  return this.#byOpId.size;
5793
6073
  }
6074
+ /** The still-unacknowledged op with the given opId, if any. */
6075
+ get(opId) {
6076
+ return this.#byOpId.get(opId);
6077
+ }
5794
6078
  /**
5795
6079
  * Mark the given Op as still unacknowledged.
5796
6080
  */
@@ -5823,6 +6107,7 @@ ${parentKey}`;
5823
6107
  return;
5824
6108
  }
5825
6109
  this.#byOpId.delete(opId);
6110
+ this.#possiblyStoredOpIds.delete(opId);
5826
6111
  if (isCreateOp(op)) {
5827
6112
  const posKey = this.#posKey(op.parentId, op.parentKey);
5828
6113
  const atPosition = this.#createOpsByPosition.get(posKey);
@@ -5856,6 +6141,19 @@ ${parentKey}`;
5856
6141
  values() {
5857
6142
  return this.#byOpId.values();
5858
6143
  }
6144
+ isPossiblyStored(opId) {
6145
+ return this.#possiblyStoredOpIds.has(opId);
6146
+ }
6147
+ /**
6148
+ * Mark every currently pending op as possibly stored on the server. Called
6149
+ * when the connection dies: all of these ops were in flight, and their
6150
+ * (possibly lost) acks would have been the only way to know their fate.
6151
+ */
6152
+ markAllAsPossiblyStored() {
6153
+ for (const opId of this.#byOpId.keys()) {
6154
+ this.#possiblyStoredOpIds.add(opId);
6155
+ }
6156
+ }
5859
6157
  };
5860
6158
 
5861
6159
  // src/crdts/AbstractCrdt.ts
@@ -5877,8 +6175,8 @@ function createManagedPool(roomId, options) {
5877
6175
  deleteNode: (id) => void nodes.delete(id),
5878
6176
  generateId: () => `${getCurrentConnectionId()}:${clock++}`,
5879
6177
  generateOpId: () => `${getCurrentConnectionId()}:${opClock++}`,
5880
- dispatch(ops, reverse, storageUpdates) {
5881
- onDispatch?.(ops, reverse, storageUpdates);
6178
+ dispatch(ops, reverse, storageUpdates, options2) {
6179
+ onDispatch?.(ops, reverse, storageUpdates, options2);
5882
6180
  },
5883
6181
  assertStorageIsWritable: () => {
5884
6182
  if (!isStorageWritable()) {
@@ -6452,7 +6750,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6452
6750
  }
6453
6751
  return result.modified.updates[0];
6454
6752
  }
6455
- #applyRemoteInsert(op, fromSnapshot) {
6753
+ #applyRemoteInsert(op) {
6456
6754
  if (this._pool === void 0) {
6457
6755
  throw new Error("Can't attach child if managed pool is not present");
6458
6756
  }
@@ -6462,7 +6760,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6462
6760
  this.#shiftItemPosition(existingItemIndex, key);
6463
6761
  }
6464
6762
  const { newItem, newIndex } = this.#createAttachItemAndSort(op, key);
6465
- const bumpDeltas = fromSnapshot ? [] : this.#bumpUnackedPushesAbove(key);
6763
+ const bumpDeltas = this.#bumpUnackedPushesAbove(key);
6466
6764
  return {
6467
6765
  modified: makeUpdate(this, [
6468
6766
  insertDelta(newIndex, newItem),
@@ -6477,6 +6775,13 @@ var LiveList = class _LiveList extends AbstractCrdt {
6477
6775
  * the single source of truth, so an item drops out the instant its op is
6478
6776
  * acked, with no per-instance membership to leak. Yielded in push order.
6479
6777
  *
6778
+ * Excludes ops that may already be stored on the server (they were in
6779
+ * flight when a connection died, so their fate is unknown): the bump
6780
+ * prediction assumes the server has not processed the op yet, which is only
6781
+ * guaranteed for ops sent on the current connection. For these excluded
6782
+ * ops, the server's (re-)ack states the authoritative position; predicting
6783
+ * locally could produce a wrong position that no ack would correct.
6784
+ *
6480
6785
  * Restricted to items currently in `#items`: a pushed node whose op is still
6481
6786
  * pending may have been pulled out of the list (e.g. implicitly deleted by a
6482
6787
  * remote set, or removed by an undo) while still living in the pool, and such
@@ -6490,6 +6795,9 @@ var LiveList = class _LiveList extends AbstractCrdt {
6490
6795
  if (op.intent !== "push") {
6491
6796
  continue;
6492
6797
  }
6798
+ if (this._pool.unacknowledgedOps.isPossiblyStored(op.opId)) {
6799
+ continue;
6800
+ }
6493
6801
  const node = this._pool.getNode(op.id);
6494
6802
  if (node !== void 0 && this.#items.includes(node)) {
6495
6803
  yield node;
@@ -6654,7 +6962,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6654
6962
  }
6655
6963
  }
6656
6964
  /** @internal */
6657
- _attachChild(op, source, fromSnapshot = false) {
6965
+ _attachChild(op, source) {
6658
6966
  if (this._pool === void 0) {
6659
6967
  throw new Error("Can't attach child if managed pool is not present");
6660
6968
  }
@@ -6669,7 +6977,7 @@ var LiveList = class _LiveList extends AbstractCrdt {
6669
6977
  }
6670
6978
  } else {
6671
6979
  if (source === 1 /* THEIRS */) {
6672
- result = this.#applyRemoteInsert(op, fromSnapshot);
6980
+ result = this.#applyRemoteInsert(op);
6673
6981
  } else if (source === 2 /* OURS */) {
6674
6982
  result = this.#applyInsertAck(op);
6675
6983
  } else {
@@ -7928,6 +8236,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
7928
8236
  const id = nn(this._id);
7929
8237
  const parentKey = nn(child._parentKey);
7930
8238
  const reverse = child._toOps(id, parentKey);
8239
+ const deletedItem = liveNodeToLson(child);
7931
8240
  for (const [key, value] of this.#synced) {
7932
8241
  if (value === child) {
7933
8242
  this.#synced.delete(key);
@@ -7939,7 +8248,7 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
7939
8248
  node: this,
7940
8249
  type: "LiveObject",
7941
8250
  updates: {
7942
- [parentKey]: { type: "delete" }
8251
+ [parentKey]: { type: "delete", deletedItem }
7943
8252
  }
7944
8253
  };
7945
8254
  return { modified: storageUpdate, reverse };
@@ -8426,73 +8735,1181 @@ var LiveObject = class _LiveObject extends AbstractCrdt {
8426
8735
  }
8427
8736
  };
8428
8737
 
8429
- // src/crdts/liveblocks-helpers.ts
8430
- function creationOpToLiveNode(op) {
8431
- return lsonToLiveNode(creationOpToLson(op));
8432
- }
8433
- function creationOpToLson(op) {
8434
- switch (op.type) {
8435
- case OpCode.CREATE_REGISTER:
8436
- return op.data;
8437
- case OpCode.CREATE_OBJECT:
8438
- return new LiveObject(op.data);
8439
- case OpCode.CREATE_MAP:
8440
- return new LiveMap();
8441
- case OpCode.CREATE_LIST:
8442
- return new LiveList([]);
8443
- default:
8444
- return assertNever(op, "Unknown creation Op");
8445
- }
8446
- }
8447
- function isSameNodeOrChildOf(node, parent) {
8448
- if (node === parent) {
8738
+ // src/crdts/liveTextOps.ts
8739
+ function attributesEqual(left, right) {
8740
+ if (left === right) {
8449
8741
  return true;
8450
8742
  }
8451
- if (node.parent.type === "HasParent") {
8452
- return isSameNodeOrChildOf(node.parent.node, parent);
8743
+ if (left === void 0 || right === void 0) {
8744
+ return false;
8453
8745
  }
8454
- return false;
8455
- }
8456
- function deserialize(node, parentToChildren, pool) {
8457
- if (isObjectStorageNode(node)) {
8458
- return LiveObject._deserialize(node, parentToChildren, pool);
8459
- } else if (isListStorageNode(node)) {
8460
- return LiveList._deserialize(node, parentToChildren, pool);
8461
- } else if (isMapStorageNode(node)) {
8462
- return LiveMap._deserialize(node, parentToChildren, pool);
8463
- } else if (isRegisterStorageNode(node)) {
8464
- return LiveRegister._deserialize(node, parentToChildren, pool);
8465
- } else {
8466
- throw new Error("Unexpected CRDT type");
8746
+ const leftKeys = Object.keys(left);
8747
+ const rightKeys = Object.keys(right);
8748
+ if (leftKeys.length !== rightKeys.length) {
8749
+ return false;
8750
+ }
8751
+ for (const key of leftKeys) {
8752
+ if (left[key] !== right[key]) {
8753
+ return false;
8754
+ }
8467
8755
  }
8756
+ return true;
8468
8757
  }
8469
- function deserializeToLson(node, parentToChildren, pool) {
8470
- if (isObjectStorageNode(node)) {
8471
- return LiveObject._deserialize(node, parentToChildren, pool);
8472
- } else if (isListStorageNode(node)) {
8473
- return LiveList._deserialize(node, parentToChildren, pool);
8474
- } else if (isMapStorageNode(node)) {
8475
- return LiveMap._deserialize(node, parentToChildren, pool);
8476
- } else if (isRegisterStorageNode(node)) {
8477
- return node[1].data;
8478
- } else {
8479
- throw new Error("Unexpected CRDT type");
8758
+ function cloneAttributes(attributes) {
8759
+ return attributes === void 0 ? void 0 : freeze({ ...attributes });
8760
+ }
8761
+ function normalizeSegments(segments) {
8762
+ const normalized = [];
8763
+ for (const segment of segments) {
8764
+ if (segment.text.length === 0) {
8765
+ continue;
8766
+ }
8767
+ const last = normalized.at(-1);
8768
+ const attributes = cloneAttributes(segment.attributes);
8769
+ if (last !== void 0 && attributesEqual(last.attributes, attributes)) {
8770
+ last.text += segment.text;
8771
+ } else {
8772
+ normalized.push({ text: segment.text, attributes });
8773
+ }
8480
8774
  }
8775
+ return normalized;
8481
8776
  }
8482
- function isLiveStructure(value) {
8483
- return isLiveList(value) || isLiveMap(value) || isLiveObject(value);
8777
+ function dataToSegments(data) {
8778
+ return normalizeSegments(
8779
+ data.map(([text, attributes]) => ({
8780
+ text,
8781
+ attributes
8782
+ }))
8783
+ );
8484
8784
  }
8485
- function isLiveNode(value) {
8486
- return isLiveStructure(value) || isLiveRegister(value);
8785
+ function segmentsToData(segments) {
8786
+ return segments.map(
8787
+ (segment) => segment.attributes === void 0 ? [segment.text] : [segment.text, { ...segment.attributes }]
8788
+ );
8487
8789
  }
8488
- function isLiveList(value) {
8489
- return value instanceof LiveList;
8790
+ function textLength(segments) {
8791
+ return segments.reduce((sum, segment) => sum + segment.text.length, 0);
8490
8792
  }
8491
- function isLiveMap(value) {
8492
- return value instanceof LiveMap;
8793
+ function splitSegmentsAt(segments, index) {
8794
+ const result = [];
8795
+ let offset = 0;
8796
+ for (const segment of segments) {
8797
+ const end = offset + segment.text.length;
8798
+ if (index > offset && index < end) {
8799
+ const before2 = segment.text.slice(0, index - offset);
8800
+ const after2 = segment.text.slice(index - offset);
8801
+ result.push({ text: before2, attributes: segment.attributes });
8802
+ result.push({ text: after2, attributes: segment.attributes });
8803
+ } else {
8804
+ result.push({ text: segment.text, attributes: segment.attributes });
8805
+ }
8806
+ offset = end;
8807
+ }
8808
+ return result;
8493
8809
  }
8494
- function isLiveObject(value) {
8495
- return value instanceof LiveObject;
8810
+ function clipRange(index, length, contentLength) {
8811
+ const clippedIndex = Math.max(0, Math.min(index, contentLength));
8812
+ const clippedEnd = Math.max(
8813
+ clippedIndex,
8814
+ Math.min(index + length, contentLength)
8815
+ );
8816
+ return { index: clippedIndex, length: clippedEnd - clippedIndex };
8817
+ }
8818
+ function applyInsert(segments, index, text, attributes) {
8819
+ if (text.length === 0) {
8820
+ return normalizeSegments(segments);
8821
+ }
8822
+ const split = splitSegmentsAt(segments, index);
8823
+ const result = [];
8824
+ let offset = 0;
8825
+ let inserted = false;
8826
+ for (const segment of split) {
8827
+ if (!inserted && offset === index) {
8828
+ result.push({ text, attributes });
8829
+ inserted = true;
8830
+ }
8831
+ result.push(segment);
8832
+ offset += segment.text.length;
8833
+ }
8834
+ if (!inserted) {
8835
+ result.push({ text, attributes });
8836
+ }
8837
+ return normalizeSegments(result);
8838
+ }
8839
+ function extractDeletedSegments(segments, index, length) {
8840
+ const split = splitSegmentsAt(
8841
+ splitSegmentsAt(segments, index),
8842
+ index + length
8843
+ );
8844
+ const deleted = [];
8845
+ let offset = 0;
8846
+ for (const segment of split) {
8847
+ const end = offset + segment.text.length;
8848
+ if (offset >= index && end <= index + length) {
8849
+ deleted.push({
8850
+ text: segment.text,
8851
+ attributes: segment.attributes
8852
+ });
8853
+ }
8854
+ offset = end;
8855
+ }
8856
+ return normalizeSegments(deleted);
8857
+ }
8858
+ function applyDelete(segments, index, length) {
8859
+ const deletedSegments = extractDeletedSegments(segments, index, length);
8860
+ const split = splitSegmentsAt(
8861
+ splitSegmentsAt(segments, index),
8862
+ index + length
8863
+ );
8864
+ const result = [];
8865
+ let offset = 0;
8866
+ let deletedText = "";
8867
+ for (const segment of split) {
8868
+ const end = offset + segment.text.length;
8869
+ if (offset >= index && end <= index + length) {
8870
+ deletedText += segment.text;
8871
+ } else {
8872
+ result.push(segment);
8873
+ }
8874
+ offset = end;
8875
+ }
8876
+ return {
8877
+ segments: normalizeSegments(result),
8878
+ deletedText,
8879
+ deletedSegments
8880
+ };
8881
+ }
8882
+ function applyFormat(segments, index, length, attributes) {
8883
+ const split = splitSegmentsAt(
8884
+ splitSegmentsAt(segments, index),
8885
+ index + length
8886
+ );
8887
+ const result = [];
8888
+ let offset = 0;
8889
+ for (const segment of split) {
8890
+ const end = offset + segment.text.length;
8891
+ if (offset >= index && end <= index + length) {
8892
+ const nextAttributes = {
8893
+ ...segment.attributes ?? {}
8894
+ };
8895
+ for (const [key, value] of Object.entries(attributes)) {
8896
+ if (value === null) {
8897
+ delete nextAttributes[key];
8898
+ } else {
8899
+ nextAttributes[key] = value;
8900
+ }
8901
+ }
8902
+ result.push({
8903
+ text: segment.text,
8904
+ attributes: Object.keys(nextAttributes).length === 0 ? void 0 : freeze(nextAttributes)
8905
+ });
8906
+ } else {
8907
+ result.push(segment);
8908
+ }
8909
+ offset = end;
8910
+ }
8911
+ return normalizeSegments(result);
8912
+ }
8913
+ function formatReverseOperations(segments, index, length, patch) {
8914
+ const split = splitSegmentsAt(
8915
+ splitSegmentsAt(segments, index),
8916
+ index + length
8917
+ );
8918
+ const result = [];
8919
+ let offset = 0;
8920
+ for (const segment of split) {
8921
+ const end = offset + segment.text.length;
8922
+ if (offset >= index && end <= index + length) {
8923
+ const attributes = {};
8924
+ for (const key of Object.keys(patch)) {
8925
+ attributes[key] = segment.attributes?.[key] ?? null;
8926
+ }
8927
+ result.push({
8928
+ type: "format",
8929
+ index: offset,
8930
+ length: segment.text.length,
8931
+ attributes
8932
+ });
8933
+ }
8934
+ offset = end;
8935
+ }
8936
+ return result;
8937
+ }
8938
+ function mapIndexThroughOperation(index, op) {
8939
+ if (op.type === "insert") {
8940
+ return op.index <= index ? index + op.text.length : index;
8941
+ } else if (op.type === "delete") {
8942
+ if (op.index >= index) {
8943
+ return index;
8944
+ }
8945
+ return Math.max(op.index, index - op.length);
8946
+ } else {
8947
+ return index;
8948
+ }
8949
+ }
8950
+ function mapTextIndexThroughOperations(index, ops) {
8951
+ let mapped = index;
8952
+ for (const op of ops) {
8953
+ mapped = mapIndexThroughOperation(mapped, op);
8954
+ }
8955
+ return mapped;
8956
+ }
8957
+ function inverseMapIndexThroughOperation(index, op) {
8958
+ if (op.type === "insert") {
8959
+ if (index <= op.index) {
8960
+ return index;
8961
+ }
8962
+ return Math.max(op.index, index - op.text.length);
8963
+ } else if (op.type === "delete") {
8964
+ return op.index <= index ? index + op.length : index;
8965
+ } else {
8966
+ return index;
8967
+ }
8968
+ }
8969
+ function inverseMapTextIndexThroughOperations(index, ops) {
8970
+ let mapped = index;
8971
+ for (let i = ops.length - 1; i >= 0; i--) {
8972
+ mapped = inverseMapIndexThroughOperation(mapped, ops[i]);
8973
+ }
8974
+ return mapped;
8975
+ }
8976
+ function oppositeOrder(order) {
8977
+ return order === "before" ? "after" : "before";
8978
+ }
8979
+ function mapIndexOverDelete(index, deleteIndex, deleteLength) {
8980
+ if (deleteIndex >= index) {
8981
+ return index;
8982
+ }
8983
+ return Math.max(deleteIndex, index - deleteLength);
8984
+ }
8985
+ function transformInsert(op, over, order) {
8986
+ if (over.type === "insert") {
8987
+ const shifts = over.index < op.index || over.index === op.index && order === "after";
8988
+ return [shifts ? { ...op, index: op.index + over.text.length } : { ...op }];
8989
+ } else if (over.type === "delete") {
8990
+ return [
8991
+ { ...op, index: mapIndexOverDelete(op.index, over.index, over.length) }
8992
+ ];
8993
+ } else {
8994
+ return [{ ...op }];
8995
+ }
8996
+ }
8997
+ function transformDelete(op, over) {
8998
+ const start = op.index;
8999
+ const end = op.index + op.length;
9000
+ if (over.type === "insert") {
9001
+ const at = over.index;
9002
+ const len = over.text.length;
9003
+ if (at <= start) {
9004
+ return [{ ...op, index: start + len }];
9005
+ }
9006
+ if (at >= end) {
9007
+ return [{ ...op }];
9008
+ }
9009
+ return [
9010
+ { type: "delete", index: start, length: at - start },
9011
+ { type: "delete", index: start + len, length: end - at }
9012
+ ];
9013
+ } else if (over.type === "delete") {
9014
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
9015
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
9016
+ return newEnd - newStart > 0 ? [{ type: "delete", index: newStart, length: newEnd - newStart }] : [];
9017
+ } else {
9018
+ return [{ ...op }];
9019
+ }
9020
+ }
9021
+ function transformFormat(op, over, order) {
9022
+ const start = op.index;
9023
+ const end = op.index + op.length;
9024
+ if (over.type === "insert") {
9025
+ const at = over.index;
9026
+ const len = over.text.length;
9027
+ if (at <= start) {
9028
+ return [{ ...op, index: start + len }];
9029
+ }
9030
+ if (at >= end) {
9031
+ return [{ ...op }];
9032
+ }
9033
+ return [
9034
+ {
9035
+ type: "format",
9036
+ index: start,
9037
+ length: at - start,
9038
+ attributes: op.attributes
9039
+ },
9040
+ {
9041
+ type: "format",
9042
+ index: at + len,
9043
+ length: end - at,
9044
+ attributes: op.attributes
9045
+ }
9046
+ ];
9047
+ } else if (over.type === "delete") {
9048
+ const newStart = mapIndexOverDelete(start, over.index, over.length);
9049
+ const newEnd = mapIndexOverDelete(end, over.index, over.length);
9050
+ return newEnd - newStart > 0 ? [
9051
+ {
9052
+ type: "format",
9053
+ index: newStart,
9054
+ length: newEnd - newStart,
9055
+ attributes: op.attributes
9056
+ }
9057
+ ] : [];
9058
+ } else {
9059
+ if (order === "after") {
9060
+ return [{ ...op }];
9061
+ }
9062
+ const overlapStart = Math.max(start, over.index);
9063
+ const overlapEnd = Math.min(end, over.index + over.length);
9064
+ if (overlapStart >= overlapEnd) {
9065
+ return [{ ...op }];
9066
+ }
9067
+ const hasConflict = Object.keys(op.attributes).some(
9068
+ (key) => key in over.attributes
9069
+ );
9070
+ if (!hasConflict) {
9071
+ return [{ ...op }];
9072
+ }
9073
+ const reduced = {};
9074
+ for (const [key, value] of Object.entries(op.attributes)) {
9075
+ if (!(key in over.attributes)) {
9076
+ reduced[key] = value;
9077
+ }
9078
+ }
9079
+ const pieces = [];
9080
+ if (start < overlapStart) {
9081
+ pieces.push({
9082
+ type: "format",
9083
+ index: start,
9084
+ length: overlapStart - start,
9085
+ attributes: op.attributes
9086
+ });
9087
+ }
9088
+ if (Object.keys(reduced).length > 0) {
9089
+ pieces.push({
9090
+ type: "format",
9091
+ index: overlapStart,
9092
+ length: overlapEnd - overlapStart,
9093
+ attributes: reduced
9094
+ });
9095
+ }
9096
+ if (overlapEnd < end) {
9097
+ pieces.push({
9098
+ type: "format",
9099
+ index: overlapEnd,
9100
+ length: end - overlapEnd,
9101
+ attributes: op.attributes
9102
+ });
9103
+ }
9104
+ return pieces;
9105
+ }
9106
+ }
9107
+ function transformSingle(op, over, order) {
9108
+ switch (op.type) {
9109
+ case "insert":
9110
+ return transformInsert(op, over, order);
9111
+ case "delete":
9112
+ return transformDelete(op, over);
9113
+ case "format":
9114
+ return transformFormat(op, over, order);
9115
+ }
9116
+ }
9117
+ function transformTextOperationsX(a, b, order) {
9118
+ if (a.length === 0 || b.length === 0) {
9119
+ return [[...a], [...b]];
9120
+ }
9121
+ if (a.length === 1 && b.length === 1) {
9122
+ return [
9123
+ transformSingle(a[0], b[0], order),
9124
+ transformSingle(b[0], a[0], oppositeOrder(order))
9125
+ ];
9126
+ }
9127
+ if (a.length > 1) {
9128
+ const [headA1, b1] = transformTextOperationsX([a[0]], b, order);
9129
+ const [restA1, b2] = transformTextOperationsX(a.slice(1), b1, order);
9130
+ return [[...headA1, ...restA1], b2];
9131
+ }
9132
+ const [a1, headB1] = transformTextOperationsX(a, [b[0]], order);
9133
+ const [a2, restB1] = transformTextOperationsX(a1, b.slice(1), order);
9134
+ return [a2, [...headB1, ...restB1]];
9135
+ }
9136
+ function transformTextOperations(ops, over, order) {
9137
+ return transformTextOperationsX(ops, over, order)[0];
9138
+ }
9139
+ function textOperationsEqual(a, b) {
9140
+ return a === b || stableStringify(a) === stableStringify(b);
9141
+ }
9142
+ function applyTextOperationsToSegments(segments, ops) {
9143
+ let next = [...segments];
9144
+ for (const op of ops) {
9145
+ if (op.type === "insert") {
9146
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9147
+ next = applyInsert(next, index, op.text, op.attributes);
9148
+ } else if (op.type === "delete") {
9149
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9150
+ const clipped = clipRange(index, op.length, textLength(next));
9151
+ next = applyDelete(next, clipped.index, clipped.length).segments;
9152
+ } else {
9153
+ const index = Math.max(0, Math.min(op.index, textLength(next)));
9154
+ const clipped = clipRange(index, op.length, textLength(next));
9155
+ next = applyFormat(next, clipped.index, clipped.length, op.attributes);
9156
+ }
9157
+ }
9158
+ return next;
9159
+ }
9160
+ function applyLiveTextOperations(data, ops) {
9161
+ return segmentsToData(
9162
+ applyTextOperationsToSegments(dataToSegments(data), ops)
9163
+ );
9164
+ }
9165
+ function invertTextOperations(segments, ops) {
9166
+ let shadow = [...segments];
9167
+ const reverse = [];
9168
+ for (const op of ops) {
9169
+ if (op.type === "insert") {
9170
+ shadow = applyInsert(shadow, op.index, op.text, op.attributes);
9171
+ reverse.unshift({
9172
+ type: "delete",
9173
+ index: op.index,
9174
+ length: op.text.length
9175
+ });
9176
+ } else if (op.type === "delete") {
9177
+ const deletedSegments = extractDeletedSegments(
9178
+ shadow,
9179
+ op.index,
9180
+ op.length
9181
+ );
9182
+ shadow = applyDelete(shadow, op.index, op.length).segments;
9183
+ const inserts = [];
9184
+ let insertIndex = op.index;
9185
+ for (const segment of deletedSegments) {
9186
+ inserts.push({
9187
+ type: "insert",
9188
+ index: insertIndex,
9189
+ text: segment.text,
9190
+ attributes: segment.attributes
9191
+ });
9192
+ insertIndex += segment.text.length;
9193
+ }
9194
+ for (let index = inserts.length - 1; index >= 0; index--) {
9195
+ reverse.unshift(inserts[index]);
9196
+ }
9197
+ } else {
9198
+ const inverse = formatReverseOperations(
9199
+ shadow,
9200
+ op.index,
9201
+ op.length,
9202
+ op.attributes
9203
+ );
9204
+ shadow = applyFormat(shadow, op.index, op.length, op.attributes);
9205
+ reverse.unshift(...inverse.reverse());
9206
+ }
9207
+ }
9208
+ return reverse;
9209
+ }
9210
+
9211
+ // src/crdts/LiveText.ts
9212
+ var ACCEPTED_OPS_HISTORY_LIMIT = 1e3;
9213
+ var LiveText = class _LiveText extends AbstractCrdt {
9214
+ /** The local document: #confirmed ⊕ #inFlightOps ⊕ #queuedOps. */
9215
+ #segments;
9216
+ /** The server-confirmed document (only authoritative ops applied). */
9217
+ #confirmed;
9218
+ #version;
9219
+ /** The op currently awaiting server acknowledgement (at most one). */
9220
+ #inFlightOpId;
9221
+ /** Its ops, continuously re-expressed against current server state. */
9222
+ #inFlightOps = [];
9223
+ /** Local edits made while an op is in flight; sent after the ack. */
9224
+ #queuedOps = [];
9225
+ #acceptedOps = [];
9226
+ /**
9227
+ * Creates a new LiveText document.
9228
+ *
9229
+ * @param textOrData Initial plain text, or an array of `[text]` /
9230
+ * `[text, attributes]` segments. Defaults to an empty document.
9231
+ *
9232
+ * @example
9233
+ * new LiveText();
9234
+ * new LiveText("Hello world");
9235
+ * new LiveText([["Hello ", { bold: true }], ["world"]]);
9236
+ */
9237
+ constructor(textOrData = "", version = 0) {
9238
+ super();
9239
+ this.#segments = typeof textOrData === "string" ? textOrData.length === 0 ? [] : [{ text: textOrData }] : dataToSegments(textOrData);
9240
+ this.#confirmed = [...this.#segments];
9241
+ this.#version = version;
9242
+ Object.defineProperty(this, kInternal, {
9243
+ value: {
9244
+ encodeIndex: (localIndex) => this.#encodeIndex(localIndex),
9245
+ decodeIndex: (index, fromVersion) => this.#decodeIndex(index, fromVersion)
9246
+ },
9247
+ enumerable: false
9248
+ });
9249
+ }
9250
+ get version() {
9251
+ return this.#version;
9252
+ }
9253
+ get length() {
9254
+ return textLength(this.#segments);
9255
+ }
9256
+ /** @internal */
9257
+ static _deserialize([id, item], _parentToChildren, pool) {
9258
+ const text = new _LiveText(item.data, item.version);
9259
+ text._attach(id, pool);
9260
+ return text;
9261
+ }
9262
+ /** @internal */
9263
+ _toOps(parentId, parentKey) {
9264
+ if (this._id === void 0) {
9265
+ throw new Error("Cannot serialize LiveText if it is not attached");
9266
+ }
9267
+ return [
9268
+ {
9269
+ type: OpCode.CREATE_TEXT,
9270
+ id: this._id,
9271
+ parentId,
9272
+ parentKey,
9273
+ data: this.toJSON(),
9274
+ version: this.#version
9275
+ }
9276
+ ];
9277
+ }
9278
+ /** @internal */
9279
+ _serialize() {
9280
+ if (this.parent.type !== "HasParent") {
9281
+ throw new Error("Cannot serialize LiveText if parent is missing");
9282
+ }
9283
+ return {
9284
+ type: CrdtType.TEXT,
9285
+ parentId: nn(this.parent.node._id, "Parent node expected to have ID"),
9286
+ parentKey: this.parent.key,
9287
+ data: this.toJSON(),
9288
+ version: this.#version
9289
+ };
9290
+ }
9291
+ /** @internal */
9292
+ _attachChild(_op) {
9293
+ throw new Error("LiveText cannot contain child nodes");
9294
+ }
9295
+ /** @internal */
9296
+ _detachChild(_crdt) {
9297
+ throw new Error("LiveText cannot contain child nodes");
9298
+ }
9299
+ /** @internal */
9300
+ _apply(op, isLocal) {
9301
+ if (op.type !== OpCode.UPDATE_TEXT) {
9302
+ return super._apply(op, isLocal);
9303
+ }
9304
+ if (isLocal) {
9305
+ return this.#applyLocal(op);
9306
+ }
9307
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
9308
+ return this.#applyAck(op);
9309
+ }
9310
+ if (op.opId !== void 0 && this.#acceptedOps.some((entry) => entry.opId === op.opId)) {
9311
+ this.#version = Math.max(this.#version, op.version ?? op.baseVersion + 1);
9312
+ return { modified: false };
9313
+ }
9314
+ return this.#applyRemote(op);
9315
+ }
9316
+ /**
9317
+ * Inserts text at the given index.
9318
+ *
9319
+ * @param index Character index at which to insert. Values outside the
9320
+ * document range are clipped.
9321
+ * @param text Text to insert.
9322
+ * @param attributes Optional inline attributes for the inserted text.
9323
+ *
9324
+ * @example
9325
+ * const text = new LiveText("Hello");
9326
+ * text.insert(5, " world");
9327
+ * text.insert(0, "Say: ", { italic: true });
9328
+ */
9329
+ insert(index, text, attributes) {
9330
+ const clippedIndex = Math.max(0, Math.min(index, this.length));
9331
+ this.#dispatch([{ type: "insert", index: clippedIndex, text, attributes }]);
9332
+ }
9333
+ /**
9334
+ * Deletes `length` characters starting at `index`.
9335
+ *
9336
+ * @example
9337
+ * const text = new LiveText("Hello world");
9338
+ * text.delete(5, 6); // "Hello"
9339
+ */
9340
+ delete(index, length) {
9341
+ const clipped = clipRange(index, length, this.length);
9342
+ if (clipped.length === 0) {
9343
+ return;
9344
+ }
9345
+ this.#dispatch([
9346
+ { type: "delete", index: clipped.index, length: clipped.length }
9347
+ ]);
9348
+ }
9349
+ /**
9350
+ * Replaces a range of text with new text.
9351
+ *
9352
+ * @example
9353
+ * const text = new LiveText("Hello world");
9354
+ * text.replace(0, 5, "Hi"); // "Hi world"
9355
+ */
9356
+ replace(index, length, text, attributes) {
9357
+ const clipped = clipRange(index, length, this.length);
9358
+ const ops = [];
9359
+ if (clipped.length > 0) {
9360
+ ops.push({
9361
+ type: "delete",
9362
+ index: clipped.index,
9363
+ length: clipped.length
9364
+ });
9365
+ }
9366
+ if (text.length > 0) {
9367
+ ops.push({ type: "insert", index: clipped.index, text, attributes });
9368
+ }
9369
+ this.#dispatch(ops);
9370
+ }
9371
+ /**
9372
+ * Encode a local-document index (an offset into this LiveText's current
9373
+ * #segments, which CodeMirror or any consumer mirrors as its document)
9374
+ * into server-confirmed coordinates suitable for broadcasting to peers via
9375
+ * presence or any other side channel.
9376
+ *
9377
+ * The returned index is in this LiveText's current #confirmed coordinates
9378
+ * — that is, with this client's local pending ops inverse-mapped out.
9379
+ * Pair it with the current {@link LiveText.version} when sending so the
9380
+ * receiver can call {@link PrivateLiveTextApi.decodeIndex} to land the
9381
+ * position in their own local document coordinates regardless of their
9382
+ * private pending ops.
9383
+ *
9384
+ * Index ambiguity at boundaries is resolved by an inverse-of-forward
9385
+ * convention: a position at or before a local insertion is reported as
9386
+ * the position right before the insertion in #confirmed; a position past
9387
+ * the insertion shifts left by the insertion's length. Positions inside
9388
+ * an own-pending insertion collapse to the insertion point.
9389
+ */
9390
+ #encodeIndex(localIndex) {
9391
+ let mapped = Math.max(0, Math.min(localIndex, this.length));
9392
+ mapped = inverseMapTextIndexThroughOperations(mapped, this.#queuedOps);
9393
+ mapped = inverseMapTextIndexThroughOperations(mapped, this.#inFlightOps);
9394
+ return mapped;
9395
+ }
9396
+ /**
9397
+ * Decode an `(index, fromVersion)` pair produced by
9398
+ * {@link PrivateLiveTextApi.encodeIndex} — typically on a peer — into an
9399
+ * offset in this LiveText's current local document (an index suitable for
9400
+ * placing a CodeMirror marker, an annotation anchor, or anything else that
9401
+ * lives over #segments).
9402
+ *
9403
+ * Composes the accepted ops applied since `fromVersion` (drawn from
9404
+ * #acceptedOps in locally-applied form) with this client's own local
9405
+ * pending ops, in that order. The result is in current #segments
9406
+ * coordinates.
9407
+ *
9408
+ * Returns `null` when the position cannot be decoded against the current
9409
+ * state:
9410
+ * - `fromVersion` is greater than this LiveText's current version: the
9411
+ * peer is ahead of us. The caller should park the message and retry
9412
+ * after more accepted ops arrive.
9413
+ * - `fromVersion` falls outside the retained accepted-ops history. This
9414
+ * only happens after very long-lived disconnections; the caller can
9415
+ * fall back to using the raw index and letting subsequent local
9416
+ * transactions map it (with bounded drift).
9417
+ */
9418
+ #decodeIndex(index, fromVersion) {
9419
+ if (fromVersion > this.#version) {
9420
+ return null;
9421
+ }
9422
+ if (fromVersion < this.#version) {
9423
+ const oldest = this.#acceptedOps[0]?.version;
9424
+ if (oldest === void 0 || oldest > fromVersion + 1) {
9425
+ return null;
9426
+ }
9427
+ }
9428
+ let mapped = index;
9429
+ for (const entry of this.#acceptedOps) {
9430
+ if (entry.version <= fromVersion) continue;
9431
+ if (entry.version > this.#version) break;
9432
+ if (entry.ops.length === 0) continue;
9433
+ mapped = mapTextIndexThroughOperations(mapped, entry.ops);
9434
+ }
9435
+ mapped = mapTextIndexThroughOperations(mapped, this.#inFlightOps);
9436
+ mapped = mapTextIndexThroughOperations(mapped, this.#queuedOps);
9437
+ return Math.max(0, Math.min(mapped, this.length));
9438
+ }
9439
+ /**
9440
+ * Applies or removes inline attributes on a range of text.
9441
+ *
9442
+ * Set an attribute to `null` to remove it from the range.
9443
+ *
9444
+ * @example
9445
+ * const text = new LiveText("Hello world");
9446
+ * text.format(0, 5, { bold: true });
9447
+ * text.format(0, 5, { bold: null });
9448
+ */
9449
+ format(index, length, attributes) {
9450
+ const clipped = clipRange(index, length, this.length);
9451
+ if (clipped.length === 0) {
9452
+ return;
9453
+ }
9454
+ this.#dispatch([
9455
+ {
9456
+ type: "format",
9457
+ index: clipped.index,
9458
+ length: clipped.length,
9459
+ attributes
9460
+ }
9461
+ ]);
9462
+ }
9463
+ /** Local edits made through the public API. */
9464
+ #dispatch(ops) {
9465
+ if (ops.length === 0) {
9466
+ return;
9467
+ }
9468
+ this._pool?.assertStorageIsWritable();
9469
+ const attached = this._pool !== void 0 && this._id !== void 0;
9470
+ const reverse = attached ? this.#invertOperations(ops) : [];
9471
+ const changes = this.#applyOperationsLocally(ops);
9472
+ if (!attached) {
9473
+ return;
9474
+ }
9475
+ const pool = nn(this._pool);
9476
+ const id = nn(this._id);
9477
+ const updates = /* @__PURE__ */ new Map([
9478
+ [
9479
+ id,
9480
+ {
9481
+ type: "LiveText",
9482
+ node: this,
9483
+ version: this.#version,
9484
+ updates: changes
9485
+ }
9486
+ ]
9487
+ ]);
9488
+ if (this.#inFlightOpId === void 0) {
9489
+ const opId = pool.generateOpId();
9490
+ this.#inFlightOpId = opId;
9491
+ this.#inFlightOps = [...ops];
9492
+ pool.dispatch(
9493
+ [
9494
+ {
9495
+ type: OpCode.UPDATE_TEXT,
9496
+ id,
9497
+ opId,
9498
+ baseVersion: this.#version,
9499
+ ops: [...ops]
9500
+ }
9501
+ ],
9502
+ reverse,
9503
+ updates
9504
+ );
9505
+ } else {
9506
+ this.#queuedOps.push(...ops);
9507
+ pool.dispatch([], reverse, updates, { clearRedoStack: true });
9508
+ }
9509
+ }
9510
+ /**
9511
+ * A local replay of an existing wire op: an undo/redo frame, or an
9512
+ * unacknowledged op re-sent after a reconnect.
9513
+ */
9514
+ #applyLocal(op) {
9515
+ const mutableOp = op;
9516
+ if (op.opId !== void 0 && op.opId === this.#inFlightOpId) {
9517
+ this.#inFlightOps = [...this.#inFlightOps, ...this.#queuedOps];
9518
+ this.#queuedOps = [];
9519
+ mutableOp.baseVersion = this.#version;
9520
+ mutableOp.ops = [...this.#inFlightOps];
9521
+ return { modified: false };
9522
+ }
9523
+ let ops = op.ops;
9524
+ for (const entry of this.#acceptedOps) {
9525
+ if (entry.version > op.baseVersion && entry.ops.length > 0) {
9526
+ ops = transformTextOperations(ops, entry.ops, "after");
9527
+ }
9528
+ }
9529
+ const reverse = this.#invertOperations(ops);
9530
+ const changes = this.#applyOperationsLocally(ops);
9531
+ if (this.#inFlightOpId === void 0 && ops.length > 0) {
9532
+ this.#inFlightOpId = nn(op.opId, "Local ops must have an opId");
9533
+ this.#inFlightOps = [...ops];
9534
+ mutableOp.baseVersion = this.#version;
9535
+ mutableOp.ops = [...ops];
9536
+ } else {
9537
+ this.#queuedOps.push(...ops);
9538
+ mutableOp.baseVersion = this.#version;
9539
+ mutableOp.ops = [];
9540
+ }
9541
+ if (changes.length === 0) {
9542
+ return { modified: false };
9543
+ }
9544
+ return {
9545
+ reverse,
9546
+ modified: {
9547
+ type: "LiveText",
9548
+ node: this,
9549
+ version: this.#version,
9550
+ updates: changes
9551
+ }
9552
+ };
9553
+ }
9554
+ /** Server acknowledgement of our in-flight op. */
9555
+ #applyAck(op) {
9556
+ const ackedVersion = op.version ?? Math.max(this.#version, op.baseVersion + 1);
9557
+ const predicted = this.#inFlightOps;
9558
+ const opId = this.#inFlightOpId;
9559
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
9560
+ this.#inFlightOpId = void 0;
9561
+ this.#inFlightOps = [];
9562
+ let appliedOps = [];
9563
+ let result = { modified: false };
9564
+ if (!textOperationsEqual(op.ops, predicted)) {
9565
+ error2(
9566
+ "LiveText: acknowledgement did not match the local prediction; resynchronizing"
9567
+ );
9568
+ const rebuilt = this.#rebuildLocalFromConfirmed();
9569
+ appliedOps = rebuilt.appliedOps;
9570
+ if (rebuilt.changes.length > 0) {
9571
+ result = {
9572
+ reverse: [],
9573
+ modified: {
9574
+ type: "LiveText",
9575
+ node: this,
9576
+ version: ackedVersion,
9577
+ updates: rebuilt.changes
9578
+ }
9579
+ };
9580
+ }
9581
+ }
9582
+ this.#version = Math.max(this.#version, ackedVersion);
9583
+ this.#recordAccepted(ackedVersion, appliedOps, opId);
9584
+ this.#flushQueued();
9585
+ return result;
9586
+ }
9587
+ /** An accepted op from another client (or a server-fabricated fix op). */
9588
+ #applyRemote(op) {
9589
+ const version = op.version ?? this.#version + 1;
9590
+ this.#confirmed = applyTextOperationsToSegments(this.#confirmed, op.ops);
9591
+ const [overInFlight, inFlight] = transformTextOperationsX(
9592
+ op.ops,
9593
+ this.#inFlightOps,
9594
+ "before"
9595
+ );
9596
+ const [applied, queued] = transformTextOperationsX(
9597
+ overInFlight,
9598
+ this.#queuedOps,
9599
+ "before"
9600
+ );
9601
+ this.#inFlightOps = inFlight;
9602
+ this.#queuedOps = queued;
9603
+ this.#recordAccepted(version, applied, op.opId);
9604
+ if (applied.length === 0) {
9605
+ this.#version = Math.max(this.#version, version);
9606
+ return { modified: false };
9607
+ }
9608
+ const reverse = this.#invertOperations(applied);
9609
+ const changes = this.#applyOperationsLocally(applied);
9610
+ this.#version = Math.max(this.#version, version);
9611
+ return {
9612
+ reverse,
9613
+ modified: {
9614
+ type: "LiveText",
9615
+ node: this,
9616
+ version: this.#version,
9617
+ updates: changes
9618
+ }
9619
+ };
9620
+ }
9621
+ /** Send the queued ops as the next in-flight op (after an ack). */
9622
+ #flushQueued() {
9623
+ if (this.#queuedOps.length === 0 || this._pool === void 0 || this._id === void 0) {
9624
+ return;
9625
+ }
9626
+ const opId = this._pool.generateOpId();
9627
+ this.#inFlightOpId = opId;
9628
+ this.#inFlightOps = this.#queuedOps;
9629
+ this.#queuedOps = [];
9630
+ this._pool.dispatch(
9631
+ [
9632
+ {
9633
+ type: OpCode.UPDATE_TEXT,
9634
+ id: this._id,
9635
+ opId,
9636
+ baseVersion: this.#version,
9637
+ ops: [...this.#inFlightOps]
9638
+ }
9639
+ ],
9640
+ [],
9641
+ /* @__PURE__ */ new Map(),
9642
+ // The local content was already applied (and made undoable) when the
9643
+ // edits happened; this is purely an outbound flush.
9644
+ { clearRedoStack: false }
9645
+ );
9646
+ }
9647
+ /**
9648
+ * Rebuild the local document as confirmed ⊕ queued ops, returning the
9649
+ * coarse delta that was applied. Only used by defensive recovery paths.
9650
+ */
9651
+ #rebuildLocalFromConfirmed() {
9652
+ const before2 = this.#segments;
9653
+ const after2 = applyTextOperationsToSegments(this.#confirmed, [
9654
+ ...this.#inFlightOps,
9655
+ ...this.#queuedOps
9656
+ ]);
9657
+ if (stableStringify(segmentsToData(before2)) === stableStringify(segmentsToData(after2))) {
9658
+ this.#segments = after2;
9659
+ return { appliedOps: [], changes: [] };
9660
+ }
9661
+ const beforeText = before2.map((segment) => segment.text).join("");
9662
+ this.#segments = after2;
9663
+ this.invalidate();
9664
+ const appliedOps = [];
9665
+ const changes = [];
9666
+ if (beforeText.length > 0) {
9667
+ appliedOps.push({ type: "delete", index: 0, length: beforeText.length });
9668
+ changes.push({
9669
+ type: "delete",
9670
+ index: 0,
9671
+ length: beforeText.length,
9672
+ deletedText: beforeText
9673
+ });
9674
+ }
9675
+ let index = 0;
9676
+ for (const segment of after2) {
9677
+ appliedOps.push({
9678
+ type: "insert",
9679
+ index,
9680
+ text: segment.text,
9681
+ attributes: segment.attributes
9682
+ });
9683
+ changes.push({
9684
+ type: "insert",
9685
+ index,
9686
+ text: segment.text,
9687
+ attributes: segment.attributes
9688
+ });
9689
+ index += segment.text.length;
9690
+ }
9691
+ return { appliedOps, changes };
9692
+ }
9693
+ /**
9694
+ * Reconcile this node against an authoritative storage snapshot (e.g.
9695
+ * after a reconnect). The confirmed state and version are replaced by the
9696
+ * snapshot's; pending (in-flight + queued) ops are preserved on top and
9697
+ * will be re-sent by the offline-ops replay.
9698
+ *
9699
+ * @internal
9700
+ */
9701
+ _resyncText(data, version) {
9702
+ this.#confirmed = dataToSegments(data);
9703
+ this.#version = version;
9704
+ this.#acceptedOps = [];
9705
+ const rebuilt = this.#rebuildLocalFromConfirmed();
9706
+ if (rebuilt.changes.length === 0) {
9707
+ return void 0;
9708
+ }
9709
+ return {
9710
+ type: "LiveText",
9711
+ node: this,
9712
+ version: this.#version,
9713
+ updates: rebuilt.changes
9714
+ };
9715
+ }
9716
+ /**
9717
+ * Called when the server rejected one of our ops. Drops all pending state
9718
+ * for this node (edits queued behind a rejected op cannot be trusted
9719
+ * either); the room follows up with a storage resync.
9720
+ *
9721
+ * @internal
9722
+ */
9723
+ _rejectPendingOp(opId) {
9724
+ if (opId !== this.#inFlightOpId) {
9725
+ return;
9726
+ }
9727
+ this.#inFlightOpId = void 0;
9728
+ this.#inFlightOps = [];
9729
+ this.#queuedOps = [];
9730
+ }
9731
+ #recordAccepted(version, ops, opId) {
9732
+ if (this.#acceptedOps.some((entry) => entry.version === version)) {
9733
+ return;
9734
+ }
9735
+ this.#acceptedOps.push({ version, opId, ops: [...ops] });
9736
+ this.#acceptedOps.sort((left, right) => left.version - right.version);
9737
+ if (this.#acceptedOps.length > ACCEPTED_OPS_HISTORY_LIMIT) {
9738
+ this.#acceptedOps.splice(
9739
+ 0,
9740
+ this.#acceptedOps.length - ACCEPTED_OPS_HISTORY_LIMIT
9741
+ );
9742
+ }
9743
+ }
9744
+ #applyOperationsLocally(ops) {
9745
+ const changes = [];
9746
+ for (const op of ops) {
9747
+ if (op.type === "insert") {
9748
+ this.#segments = applyInsert(
9749
+ this.#segments,
9750
+ op.index,
9751
+ op.text,
9752
+ op.attributes
9753
+ );
9754
+ changes.push({
9755
+ type: "insert",
9756
+ index: op.index,
9757
+ text: op.text,
9758
+ attributes: op.attributes
9759
+ });
9760
+ } else if (op.type === "delete") {
9761
+ const result = applyDelete(this.#segments, op.index, op.length);
9762
+ this.#segments = result.segments;
9763
+ changes.push({
9764
+ type: "delete",
9765
+ index: op.index,
9766
+ length: op.length,
9767
+ deletedText: result.deletedText
9768
+ });
9769
+ } else {
9770
+ this.#segments = applyFormat(
9771
+ this.#segments,
9772
+ op.index,
9773
+ op.length,
9774
+ op.attributes
9775
+ );
9776
+ changes.push({
9777
+ type: "format",
9778
+ index: op.index,
9779
+ length: op.length,
9780
+ attributes: op.attributes
9781
+ });
9782
+ }
9783
+ }
9784
+ this.invalidate();
9785
+ return changes;
9786
+ }
9787
+ #invertOperations(ops) {
9788
+ return [
9789
+ {
9790
+ type: OpCode.UPDATE_TEXT,
9791
+ id: nn(this._id),
9792
+ baseVersion: this.#version,
9793
+ ops: invertTextOperations(this.#segments, ops)
9794
+ }
9795
+ ];
9796
+ }
9797
+ /** Returns the plain text content without attributes. Equivalent to joining the text from each segment in {@link LiveText.toJSON}. */
9798
+ toString() {
9799
+ return this.#segments.map((segment) => segment.text).join("");
9800
+ }
9801
+ /**
9802
+ * Returns a JSON-compatible snapshot of the document as a {@link LiveTextData}
9803
+ * array.
9804
+ *
9805
+ * @example
9806
+ * new LiveText([["Hello ", { bold: true }], ["world"]]).toJSON();
9807
+ * // [["Hello ", { bold: true }], ["world"]]
9808
+ */
9809
+ toJSON() {
9810
+ return super.toJSON();
9811
+ }
9812
+ /** @internal */
9813
+ _toJSON() {
9814
+ return segmentsToData(this.#segments);
9815
+ }
9816
+ /** @internal */
9817
+ _toTreeNode(key) {
9818
+ return {
9819
+ type: "LiveText",
9820
+ id: this._id ?? nanoid(),
9821
+ key,
9822
+ payload: [
9823
+ {
9824
+ type: "Json",
9825
+ id: `${this._id ?? nanoid()}:text`,
9826
+ key: "text",
9827
+ payload: this.toString()
9828
+ }
9829
+ ]
9830
+ };
9831
+ }
9832
+ clone() {
9833
+ return new _LiveText(this.toJSON(), this.#version);
9834
+ }
9835
+ };
9836
+
9837
+ // src/crdts/liveblocks-helpers.ts
9838
+ function creationOpToLiveNode(op) {
9839
+ return lsonToLiveNode(creationOpToLson(op));
9840
+ }
9841
+ function creationOpToLson(op) {
9842
+ switch (op.type) {
9843
+ case OpCode.CREATE_REGISTER:
9844
+ return op.data;
9845
+ case OpCode.CREATE_OBJECT:
9846
+ return new LiveObject(op.data);
9847
+ case OpCode.CREATE_MAP:
9848
+ return new LiveMap();
9849
+ case OpCode.CREATE_LIST:
9850
+ return new LiveList([]);
9851
+ case OpCode.CREATE_TEXT:
9852
+ return new LiveText(op.data, op.version);
9853
+ default:
9854
+ return assertNever(op, "Unknown creation Op");
9855
+ }
9856
+ }
9857
+ function isSameNodeOrChildOf(node, parent) {
9858
+ if (node === parent) {
9859
+ return true;
9860
+ }
9861
+ if (node.parent.type === "HasParent") {
9862
+ return isSameNodeOrChildOf(node.parent.node, parent);
9863
+ }
9864
+ return false;
9865
+ }
9866
+ function deserialize(node, parentToChildren, pool) {
9867
+ if (isObjectStorageNode(node)) {
9868
+ return LiveObject._deserialize(node, parentToChildren, pool);
9869
+ } else if (isListStorageNode(node)) {
9870
+ return LiveList._deserialize(node, parentToChildren, pool);
9871
+ } else if (isMapStorageNode(node)) {
9872
+ return LiveMap._deserialize(node, parentToChildren, pool);
9873
+ } else if (isRegisterStorageNode(node)) {
9874
+ return LiveRegister._deserialize(node, parentToChildren, pool);
9875
+ } else if (isTextStorageNode(node)) {
9876
+ return LiveText._deserialize(node, parentToChildren, pool);
9877
+ } else {
9878
+ throw new Error("Unexpected CRDT type");
9879
+ }
9880
+ }
9881
+ function deserializeToLson(node, parentToChildren, pool) {
9882
+ if (isObjectStorageNode(node)) {
9883
+ return LiveObject._deserialize(node, parentToChildren, pool);
9884
+ } else if (isListStorageNode(node)) {
9885
+ return LiveList._deserialize(node, parentToChildren, pool);
9886
+ } else if (isMapStorageNode(node)) {
9887
+ return LiveMap._deserialize(node, parentToChildren, pool);
9888
+ } else if (isRegisterStorageNode(node)) {
9889
+ return node[1].data;
9890
+ } else if (isTextStorageNode(node)) {
9891
+ return LiveText._deserialize(node, parentToChildren, pool);
9892
+ } else {
9893
+ throw new Error("Unexpected CRDT type");
9894
+ }
9895
+ }
9896
+ function isLiveStructure(value) {
9897
+ return isLiveList(value) || isLiveMap(value) || isLiveObject(value) || isLiveText(value);
9898
+ }
9899
+ function isLiveNode(value) {
9900
+ return isLiveStructure(value) || isLiveRegister(value);
9901
+ }
9902
+ function isLiveList(value) {
9903
+ return value instanceof LiveList;
9904
+ }
9905
+ function isLiveMap(value) {
9906
+ return value instanceof LiveMap;
9907
+ }
9908
+ function isLiveObject(value) {
9909
+ return value instanceof LiveObject;
9910
+ }
9911
+ function isLiveText(value) {
9912
+ return value instanceof LiveText;
8496
9913
  }
8497
9914
  function isLiveRegister(value) {
8498
9915
  return value instanceof LiveRegister;
@@ -8503,14 +9920,14 @@ function cloneLson(value) {
8503
9920
  function liveNodeToLson(obj) {
8504
9921
  if (obj instanceof LiveRegister) {
8505
9922
  return obj.data;
8506
- } else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject) {
9923
+ } else if (obj instanceof LiveList || obj instanceof LiveMap || obj instanceof LiveObject || obj instanceof LiveText) {
8507
9924
  return obj;
8508
9925
  } else {
8509
9926
  return assertNever(obj, "Unknown AbstractCrdt");
8510
9927
  }
8511
9928
  }
8512
9929
  function lsonToLiveNode(value) {
8513
- if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList) {
9930
+ if (value instanceof LiveObject || value instanceof LiveMap || value instanceof LiveList || value instanceof LiveText) {
8514
9931
  return value;
8515
9932
  } else {
8516
9933
  return new LiveRegister(value);
@@ -8541,23 +9958,68 @@ function dumpPool(pool) {
8541
9958
  (r) => ` ${r.id} parent=${r.parentId} key=${r.key || "\u2014"} ${r.value}`
8542
9959
  ).join("\n");
8543
9960
  }
8544
- function getTreesDiffOperations(currentItems, newItems) {
9961
+ function isJsonEq(a, b) {
9962
+ if (a === b) {
9963
+ return true;
9964
+ }
9965
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
9966
+ return false;
9967
+ }
9968
+ if (Array.isArray(a) || Array.isArray(b)) {
9969
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
9970
+ return false;
9971
+ }
9972
+ for (let i = 0; i < a.length; i++) {
9973
+ if (!isJsonEq(a[i], b[i])) {
9974
+ return false;
9975
+ }
9976
+ }
9977
+ return true;
9978
+ }
9979
+ const aKeys = Object.keys(a);
9980
+ if (aKeys.length !== Object.keys(b).length) {
9981
+ return false;
9982
+ }
9983
+ for (const key of aKeys) {
9984
+ if (!isJsonEq(a[key], b[key])) {
9985
+ return false;
9986
+ }
9987
+ }
9988
+ return true;
9989
+ }
9990
+ function diffNodeMap(prev, next) {
8545
9991
  const ops = [];
8546
- currentItems.forEach((_, id) => {
8547
- if (!newItems.get(id)) {
9992
+ prev.forEach((_, id) => {
9993
+ if (!next.get(id)) {
8548
9994
  ops.push({ type: OpCode.DELETE_CRDT, id });
8549
9995
  }
8550
9996
  });
8551
- newItems.forEach((crdt, id) => {
8552
- const currentCrdt = currentItems.get(id);
9997
+ next.forEach((crdt, id) => {
9998
+ const currentCrdt = prev.get(id);
8553
9999
  if (currentCrdt) {
8554
10000
  if (crdt.type === CrdtType.OBJECT) {
8555
- if (currentCrdt.type !== CrdtType.OBJECT || stringifyOrLog(crdt.data) !== stringifyOrLog(currentCrdt.data)) {
8556
- ops.push({
8557
- type: OpCode.UPDATE_OBJECT,
8558
- id,
8559
- data: crdt.data
8560
- });
10001
+ if (currentCrdt.type !== CrdtType.OBJECT) {
10002
+ ops.push({ type: OpCode.UPDATE_OBJECT, id, data: crdt.data });
10003
+ } else {
10004
+ const changed = /* @__PURE__ */ new Map();
10005
+ for (const key of Object.keys(crdt.data)) {
10006
+ const value = crdt.data[key];
10007
+ if (value !== void 0 && !isJsonEq(value, currentCrdt.data[key])) {
10008
+ changed.set(key, value);
10009
+ }
10010
+ }
10011
+ if (changed.size > 0) {
10012
+ ops.push({
10013
+ type: OpCode.UPDATE_OBJECT,
10014
+ id,
10015
+ data: Object.fromEntries(changed)
10016
+ });
10017
+ }
10018
+ for (const key of Object.keys(currentCrdt.data)) {
10019
+ if (!(key in crdt.data)) {
10020
+ ops.push({ type: OpCode.DELETE_OBJECT_KEY, id, key });
10021
+ }
10022
+ }
8561
10023
  }
8562
10024
  }
8563
10025
  if (crdt.parentKey !== currentCrdt.parentKey) {
@@ -8608,6 +10070,16 @@ function getTreesDiffOperations(currentItems, newItems) {
8608
10070
  parentKey: crdt.parentKey
8609
10071
  });
8610
10072
  break;
10073
+ case CrdtType.TEXT:
10074
+ ops.push({
10075
+ type: OpCode.CREATE_TEXT,
10076
+ id,
10077
+ parentId: crdt.parentId,
10078
+ parentKey: crdt.parentKey,
10079
+ data: crdt.data,
10080
+ version: crdt.version
10081
+ });
10082
+ break;
8611
10083
  }
8612
10084
  }
8613
10085
  });
@@ -8640,19 +10112,43 @@ function mergeListStorageUpdates(first, second) {
8640
10112
  updates: updates.concat(second.updates)
8641
10113
  };
8642
10114
  }
10115
+ function mergeTextStorageUpdates(first, second) {
10116
+ return {
10117
+ ...second,
10118
+ updates: first.updates.concat(second.updates)
10119
+ };
10120
+ }
8643
10121
  function mergeStorageUpdates(first, second) {
8644
10122
  if (first === void 0) {
8645
10123
  return second;
8646
10124
  }
10125
+ let merged;
8647
10126
  if (first.type === "LiveObject" && second.type === "LiveObject") {
8648
- return mergeObjectStorageUpdates(first, second);
10127
+ merged = mergeObjectStorageUpdates(first, second);
8649
10128
  } else if (first.type === "LiveMap" && second.type === "LiveMap") {
8650
- return mergeMapStorageUpdates(first, second);
10129
+ merged = mergeMapStorageUpdates(first, second);
8651
10130
  } else if (first.type === "LiveList" && second.type === "LiveList") {
8652
- return mergeListStorageUpdates(first, second);
10131
+ merged = mergeListStorageUpdates(first, second);
10132
+ } else if (first.type === "LiveText" && second.type === "LiveText") {
10133
+ merged = mergeTextStorageUpdates(first, second);
8653
10134
  } else {
10135
+ merged = second;
10136
+ }
10137
+ const sa = first[kStorageUpdateSource];
10138
+ const sb = second[kStorageUpdateSource];
10139
+ if (sa !== void 0 || sb !== void 0) {
10140
+ if (sa?.origin === "remote" || sb?.origin === "remote") {
10141
+ merged[kStorageUpdateSource] = { origin: "remote" };
10142
+ } else if (sa?.via === "history" || sb?.via === "history") {
10143
+ const historySource = sb?.via === "history" ? sb : sa?.via === "history" ? sa : void 0;
10144
+ if (historySource?.via === "history") {
10145
+ merged[kStorageUpdateSource] = historySource;
10146
+ }
10147
+ } else {
10148
+ merged[kStorageUpdateSource] = { origin: "local", via: "mutation" };
10149
+ }
8654
10150
  }
8655
- return second;
10151
+ return merged;
8656
10152
  }
8657
10153
 
8658
10154
  // src/devtools/bridge.ts
@@ -8780,7 +10276,7 @@ function partialSyncConnection(room) {
8780
10276
  });
8781
10277
  }
8782
10278
  function partialSyncStorage(room) {
8783
- const root = room.getStorageSnapshot();
10279
+ const root = room.getStorageOrNull();
8784
10280
  if (root) {
8785
10281
  sendToPanel({
8786
10282
  msg: "room::sync::partial",
@@ -8810,7 +10306,7 @@ function partialSyncOthers(room) {
8810
10306
  }
8811
10307
  }
8812
10308
  function fullSync(room) {
8813
- const root = room.getStorageSnapshot();
10309
+ const root = room.getStorageOrNull();
8814
10310
  const me = room[kInternal].getSelf_forDevTools();
8815
10311
  const others = room[kInternal].getOthers_forDevTools();
8816
10312
  room.fetchYDoc("");
@@ -9235,15 +10731,15 @@ var ClientMsgCode = Object.freeze({
9235
10731
 
9236
10732
  // src/refs/ManagedOthers.ts
9237
10733
  function makeUser(conn, presence) {
9238
- const { connectionId, id, info } = conn;
9239
- const canWrite = canWriteStorage(conn.scopes);
10734
+ const { connectionId, id, info, access } = conn;
10735
+ const { canWrite, canComment } = access;
9240
10736
  return freeze(
9241
10737
  compactObject({
9242
10738
  connectionId,
9243
10739
  id,
9244
10740
  info,
9245
10741
  canWrite,
9246
- canComment: canComment(conn.scopes),
10742
+ canComment,
9247
10743
  isReadOnly: !canWrite,
9248
10744
  // Deprecated, kept for backward-compatibility
9249
10745
  presence
@@ -9314,7 +10810,7 @@ var ManagedOthers = class {
9314
10810
  * Records a known connection. This records the connection ID and the
9315
10811
  * associated metadata.
9316
10812
  */
9317
- setConnection(connectionId, metaUserId, metaUserInfo, scopes) {
10813
+ setConnection(connectionId, metaUserId, metaUserInfo, access) {
9318
10814
  this.#internal.mutate((state) => {
9319
10815
  state.connections.set(
9320
10816
  connectionId,
@@ -9322,7 +10818,7 @@ var ManagedOthers = class {
9322
10818
  connectionId,
9323
10819
  id: metaUserId,
9324
10820
  info: metaUserInfo,
9325
- scopes
10821
+ access
9326
10822
  })
9327
10823
  );
9328
10824
  if (!state.presences.has(connectionId)) {
@@ -9475,6 +10971,14 @@ function defaultMessageFromContext(context) {
9475
10971
 
9476
10972
  // src/room.ts
9477
10973
  var FEEDS_TIMEOUT = 5e3;
10974
+ function connectionAccessFromScopes(scopes) {
10975
+ const roomPermissions = normalizeRoomPermissions(scopes);
10976
+ const matrix = permissionMatrixFromScopes(roomPermissions);
10977
+ return {
10978
+ canWrite: hasPermissionAccess(matrix, "storage", "write"),
10979
+ canComment: hasPermissionAccess(matrix, "comments", "write")
10980
+ };
10981
+ }
9478
10982
  function makeIdFactory(connectionId) {
9479
10983
  let count = 0;
9480
10984
  return () => `${connectionId}:${count++}`;
@@ -9588,6 +11092,8 @@ function createRoom(options, config) {
9588
11092
  activeBatch: null,
9589
11093
  unacknowledgedOps
9590
11094
  };
11095
+ let nextHistoryItemId = 0;
11096
+ let historyDisabled = 0;
9591
11097
  const nodeMapBuffer = makeNodeMapBuffer();
9592
11098
  const stopwatch = config.enableDebugLogging ? makeStopWatch() : void 0;
9593
11099
  let lastTokenKey;
@@ -9647,12 +11153,13 @@ function createRoom(options, config) {
9647
11153
  )
9648
11154
  };
9649
11155
  if (_getStorage$ !== null) {
9650
- refreshStorage({ flush: false });
11156
+ refreshStorage();
9651
11157
  }
9652
11158
  flushNowOrSoon();
9653
11159
  }
9654
11160
  function onDidDisconnect() {
9655
11161
  clearTimeout(context.buffer.flushTimerID);
11162
+ context.unacknowledgedOps.markAllAsPossiblyStored();
9656
11163
  }
9657
11164
  managedSocket.events.onMessage.subscribe(handleServerMessage);
9658
11165
  managedSocket.events.statusDidChange.subscribe(onStatusDidChange);
@@ -9671,7 +11178,10 @@ function createRoom(options, config) {
9671
11178
  }
9672
11179
  }
9673
11180
  });
9674
- function onDispatch(ops, reverse, storageUpdates) {
11181
+ function onDispatch(ops, reverse, storageUpdates, options2) {
11182
+ for (const value of storageUpdates.values()) {
11183
+ value[kStorageUpdateSource] = { origin: "local", via: "mutation" };
11184
+ }
9675
11185
  if (context.activeBatch) {
9676
11186
  for (const op of ops) {
9677
11187
  context.activeBatch.ops.push(op);
@@ -9690,16 +11200,18 @@ function createRoom(options, config) {
9690
11200
  if (reverse.length > 0) {
9691
11201
  addToUndoStack(reverse);
9692
11202
  }
11203
+ if (options2?.clearRedoStack ?? ops.length > 0) {
11204
+ clearRedoStack();
11205
+ }
9693
11206
  if (ops.length > 0) {
9694
- context.redoStack.length = 0;
9695
11207
  dispatchOps(ops);
9696
11208
  }
9697
11209
  notify({ storageUpdates });
9698
11210
  }
9699
11211
  }
9700
11212
  function isStorageWritable() {
9701
- const scopes = context.dynamicSessionInfoSig.get()?.scopes;
9702
- return scopes !== void 0 ? canWriteStorage(scopes) : true;
11213
+ const permissionMatrix = context.dynamicSessionInfoSig.get()?.permissionMatrix;
11214
+ return permissionMatrix !== void 0 ? hasPermissionAccess(permissionMatrix, "storage", "write") : true;
9703
11215
  }
9704
11216
  const eventHub = {
9705
11217
  status: makeEventSource(),
@@ -9711,6 +11223,7 @@ function createRoom(options, config) {
9711
11223
  others: makeEventSource(),
9712
11224
  storageBatch: makeEventSource(),
9713
11225
  history: makeEventSource(),
11226
+ privateHistory: makeEventSource(),
9714
11227
  storageDidLoad: makeEventSource(),
9715
11228
  storageStatus: makeEventSource(),
9716
11229
  ydoc: makeEventSource(),
@@ -9760,14 +11273,22 @@ function createRoom(options, config) {
9760
11273
  if (staticSession === null || dynamicSession === null) {
9761
11274
  return null;
9762
11275
  } else {
9763
- const canWrite = canWriteStorage(dynamicSession.scopes);
11276
+ const canWrite = hasPermissionAccess(
11277
+ dynamicSession.permissionMatrix,
11278
+ "storage",
11279
+ "write"
11280
+ );
9764
11281
  return {
9765
11282
  connectionId: dynamicSession.actor,
9766
11283
  id: staticSession.userId,
9767
11284
  info: staticSession.userInfo,
9768
11285
  presence: myPresence,
9769
11286
  canWrite,
9770
- canComment: canComment(dynamicSession.scopes)
11287
+ canComment: hasPermissionAccess(
11288
+ dynamicSession.permissionMatrix,
11289
+ "comments",
11290
+ "write"
11291
+ )
9771
11292
  };
9772
11293
  }
9773
11294
  }
@@ -9793,12 +11314,25 @@ function createRoom(options, config) {
9793
11314
  for (const [id, crdt] of context.pool.nodes) {
9794
11315
  currentItems.set(id, crdt._serialize());
9795
11316
  }
9796
- const ops = getTreesDiffOperations(currentItems, nodes);
9797
- const result = applyRemoteOps(
9798
- ops,
9799
- /* fromSnapshot */
9800
- true
9801
- );
11317
+ const ops = diffNodeMap(currentItems, nodes);
11318
+ const result = applyRemoteOps(ops);
11319
+ for (const [id, crdt] of nodes) {
11320
+ if (crdt.type === CrdtType.TEXT) {
11321
+ const node = context.pool.nodes.get(id);
11322
+ if (node !== void 0 && isLiveText(node)) {
11323
+ const update = node._resyncText(crdt.data, crdt.version);
11324
+ if (update !== void 0) {
11325
+ result.updates.storageUpdates.set(
11326
+ id,
11327
+ mergeStorageUpdates(
11328
+ result.updates.storageUpdates.get(id),
11329
+ update
11330
+ )
11331
+ );
11332
+ }
11333
+ }
11334
+ }
11335
+ }
9802
11336
  notify(result.updates);
9803
11337
  } else {
9804
11338
  context.root = LiveObject._fromItems(
@@ -9822,11 +11356,26 @@ function createRoom(options, config) {
9822
11356
  }
9823
11357
  });
9824
11358
  }
11359
+ function notifyPrivateHistory(event) {
11360
+ if (historyDisabled > 0) return;
11361
+ eventHub.privateHistory.notify(event);
11362
+ }
11363
+ function clearRedoStack() {
11364
+ if (context.redoStack.length === 0) return;
11365
+ const ids = context.redoStack.map((item) => item.id);
11366
+ context.redoStack.length = 0;
11367
+ notifyPrivateHistory({ action: "discard", ids });
11368
+ }
9825
11369
  function _addToRealUndoStack(frames) {
9826
11370
  if (context.undoStack.length >= 50) {
9827
- context.undoStack.shift();
11371
+ const evicted = context.undoStack.shift();
11372
+ if (evicted !== void 0) {
11373
+ notifyPrivateHistory({ action: "discard", ids: [evicted.id] });
11374
+ }
9828
11375
  }
9829
- context.undoStack.push(frames);
11376
+ const id = nextHistoryItemId++;
11377
+ context.undoStack.push({ id, frames });
11378
+ notifyPrivateHistory({ action: "push", id });
9830
11379
  onHistoryChange();
9831
11380
  }
9832
11381
  function addToUndoStack(frames) {
@@ -9864,7 +11413,7 @@ function createRoom(options, config) {
9864
11413
  "Internal. Tried to get connection id but connection was never open"
9865
11414
  );
9866
11415
  }
9867
- function applyLocalOps(frames) {
11416
+ function applyLocalOps(frames, localStorageUpdateSource = { origin: "local", via: "mutation" }) {
9868
11417
  const [pframes, ops] = partition(
9869
11418
  frames,
9870
11419
  (f) => f.type === "presence"
@@ -9876,20 +11425,20 @@ function createRoom(options, config) {
9876
11425
  pframes,
9877
11426
  opsWithOpIds,
9878
11427
  /* isLocal */
9879
- true
11428
+ true,
11429
+ localStorageUpdateSource
9880
11430
  );
9881
11431
  return { opsToEmit: opsWithOpIds, reverse, updates };
9882
11432
  }
9883
- function applyRemoteOps(ops, fromSnapshot = false) {
11433
+ function applyRemoteOps(ops) {
9884
11434
  return applyOps(
9885
11435
  [],
9886
11436
  ops,
9887
11437
  /* isLocal */
9888
- false,
9889
- fromSnapshot
11438
+ false
9890
11439
  );
9891
11440
  }
9892
- function applyOps(pframes, ops, isLocal, fromSnapshot = false) {
11441
+ function applyOps(pframes, ops, isLocal, localStorageUpdateSource = { origin: "local", via: "mutation" }) {
9893
11442
  const output = {
9894
11443
  reverse: new Deque(),
9895
11444
  storageUpdates: /* @__PURE__ */ new Map(),
@@ -9925,8 +11474,9 @@ function createRoom(options, config) {
9925
11474
  } else {
9926
11475
  source = 1 /* THEIRS */;
9927
11476
  }
9928
- const applyOpResult = applyOp(op, source, fromSnapshot);
11477
+ const applyOpResult = applyOp(op, source);
9929
11478
  if (applyOpResult.modified) {
11479
+ applyOpResult.modified[kStorageUpdateSource] = source === 1 /* THEIRS */ ? { origin: "remote" } : localStorageUpdateSource;
9930
11480
  const nodeId = applyOpResult.modified.node._id;
9931
11481
  if (!(nodeId && createdNodeIds.has(nodeId))) {
9932
11482
  output.storageUpdates.set(
@@ -9938,7 +11488,7 @@ function createRoom(options, config) {
9938
11488
  );
9939
11489
  output.reverse.pushLeft(applyOpResult.reverse);
9940
11490
  }
9941
- if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT) {
11491
+ if (op.type === OpCode.CREATE_LIST || op.type === OpCode.CREATE_MAP || op.type === OpCode.CREATE_OBJECT || op.type === OpCode.CREATE_TEXT) {
9942
11492
  createdNodeIds.add(op.id);
9943
11493
  }
9944
11494
  }
@@ -9951,13 +11501,14 @@ function createRoom(options, config) {
9951
11501
  }
9952
11502
  };
9953
11503
  }
9954
- function applyOp(op, source, fromSnapshot = false) {
11504
+ function applyOp(op, source) {
9955
11505
  if (isIgnoredOp(op)) {
9956
11506
  return { modified: false };
9957
11507
  }
9958
11508
  switch (op.type) {
9959
11509
  case OpCode.DELETE_OBJECT_KEY:
9960
11510
  case OpCode.UPDATE_OBJECT:
11511
+ case OpCode.UPDATE_TEXT:
9961
11512
  case OpCode.DELETE_CRDT: {
9962
11513
  const node = context.pool.nodes.get(op.id);
9963
11514
  if (node === void 0) {
@@ -9982,6 +11533,7 @@ function createRoom(options, config) {
9982
11533
  case OpCode.CREATE_OBJECT:
9983
11534
  case OpCode.CREATE_LIST:
9984
11535
  case OpCode.CREATE_MAP:
11536
+ case OpCode.CREATE_TEXT:
9985
11537
  case OpCode.CREATE_REGISTER: {
9986
11538
  if (op.parentId === void 0) {
9987
11539
  return { modified: false };
@@ -9990,7 +11542,7 @@ function createRoom(options, config) {
9990
11542
  if (parentNode === void 0) {
9991
11543
  return { modified: false };
9992
11544
  }
9993
- return parentNode._attachChild(op, source, fromSnapshot);
11545
+ return parentNode._attachChild(op, source);
9994
11546
  }
9995
11547
  }
9996
11548
  }
@@ -10061,7 +11613,9 @@ function createRoom(options, config) {
10061
11613
  context.dynamicSessionInfoSig.set({
10062
11614
  actor: message.actor,
10063
11615
  nonce: message.nonce,
10064
- scopes: message.scopes,
11616
+ permissionMatrix: permissionMatrixFromScopes(
11617
+ normalizeRoomPermissions(message.scopes)
11618
+ ),
10065
11619
  meta: message.meta
10066
11620
  });
10067
11621
  context.idFactory = makeIdFactory(message.actor);
@@ -10082,7 +11636,7 @@ function createRoom(options, config) {
10082
11636
  connectionId,
10083
11637
  user.id,
10084
11638
  user.info,
10085
- user.scopes
11639
+ connectionAccessFromScopes(user.scopes)
10086
11640
  );
10087
11641
  }
10088
11642
  return { type: "reset" };
@@ -10102,7 +11656,7 @@ function createRoom(options, config) {
10102
11656
  message.actor,
10103
11657
  message.id,
10104
11658
  message.info,
10105
- message.scopes
11659
+ connectionAccessFromScopes(message.scopes)
10106
11660
  );
10107
11661
  context.buffer.messages.push({
10108
11662
  type: ClientMsgCode.UPDATE_PRESENCE,
@@ -10229,16 +11783,37 @@ function createRoom(options, config) {
10229
11783
  }
10230
11784
  break;
10231
11785
  }
10232
- // Receiving a RejectedOps message in the client means that the server is no
10233
- // longer in sync with the client. Trying to synchronize the client again by
10234
- // rolling back particular Ops may be hard/impossible. It's fine to not try and
10235
- // accept the out-of-sync reality and throw an error.
11786
+ // Receiving a RejectedOps message means the server refused some of
11787
+ // our ops, so our optimistic local state is out of sync with the
11788
+ // server. For LiveText ops this is a normal (if rare) situation
11789
+ // e.g. a client that was offline long enough to fall outside the
11790
+ // server's retained history window — and we can recover: drop the
11791
+ // rejected pending state and re-fetch the authoritative storage
11792
+ // snapshot. For other ops (e.g. permission rejections), rolling back
11793
+ // particular Ops is hard/impossible, so we keep the old behavior of
11794
+ // accepting the out-of-sync reality and surfacing an error.
10236
11795
  case ServerMsgCode.REJECT_STORAGE_OP: {
10237
11796
  errorWithTitle(
10238
11797
  "Storage mutation rejection error",
10239
11798
  message.reason
10240
11799
  );
10241
- if (process.env.NODE_ENV !== "production") {
11800
+ let needsStorageResync = false;
11801
+ for (const opId of message.opIds) {
11802
+ const rejectedOp = context.unacknowledgedOps.get(opId);
11803
+ context.unacknowledgedOps.delete(opId);
11804
+ context.buffer.storageOperations = context.buffer.storageOperations.filter((op) => op.opId !== opId);
11805
+ if (rejectedOp !== void 0 && rejectedOp.type === OpCode.UPDATE_TEXT) {
11806
+ const node = context.pool.nodes.get(rejectedOp.id);
11807
+ if (node !== void 0 && isLiveText(node)) {
11808
+ node._rejectPendingOp(opId);
11809
+ needsStorageResync = true;
11810
+ }
11811
+ }
11812
+ }
11813
+ if (needsStorageResync) {
11814
+ refreshStorage();
11815
+ flushNowOrSoon();
11816
+ } else if (process.env.NODE_ENV !== "production") {
10242
11817
  throw new Error(
10243
11818
  `Storage mutations rejected by server: ${message.reason}`
10244
11819
  );
@@ -10592,29 +12167,18 @@ function createRoom(options, config) {
10592
12167
  notifyStorageStatus();
10593
12168
  eventHub.storageDidLoad.notify();
10594
12169
  }
10595
- async function streamStorage() {
10596
- if (!managedSocket.authValue) return;
10597
- const nodes = new Map(
10598
- await httpClient.streamStorage({ roomId })
10599
- );
10600
- processInitialStorage(nodes);
10601
- }
10602
- function refreshStorage(options2) {
12170
+ function refreshStorage() {
10603
12171
  const messages = context.buffer.messages;
10604
- if (config.unstable_streamData) {
10605
- void streamStorage();
10606
- } else if (!messages.some((msg) => msg.type === ClientMsgCode.FETCH_STORAGE)) {
12172
+ if (!messages.some((msg) => msg.type === ClientMsgCode.FETCH_STORAGE)) {
10607
12173
  messages.push({ type: ClientMsgCode.FETCH_STORAGE });
10608
12174
  nodeMapBuffer.take();
10609
12175
  stopwatch?.start();
10610
12176
  }
10611
- if (options2.flush) {
10612
- flushNowOrSoon();
10613
- }
10614
12177
  }
10615
12178
  function startLoadingStorage() {
10616
12179
  if (_getStorage$ === null) {
10617
- refreshStorage({ flush: true });
12180
+ refreshStorage();
12181
+ flushNowOrSoon();
10618
12182
  _getStorage$ = new Promise((resolve) => {
10619
12183
  _resolveStoragePromise = resolve;
10620
12184
  });
@@ -10622,7 +12186,7 @@ function createRoom(options, config) {
10622
12186
  }
10623
12187
  return _getStorage$;
10624
12188
  }
10625
- function getStorageSnapshot() {
12189
+ function getStorageOrNull() {
10626
12190
  const root = context.root;
10627
12191
  if (root !== void 0) {
10628
12192
  return root;
@@ -10791,14 +12355,19 @@ function createRoom(options, config) {
10791
12355
  if (context.activeBatch) {
10792
12356
  throw new Error("undo is not allowed during a batch");
10793
12357
  }
10794
- const frames = context.undoStack.pop();
10795
- if (frames === void 0) {
12358
+ const item = context.undoStack.pop();
12359
+ if (item === void 0) {
10796
12360
  return;
10797
12361
  }
10798
12362
  context.pausedHistory = null;
10799
- const result = applyLocalOps(frames);
12363
+ const result = applyLocalOps(item.frames, {
12364
+ origin: "local",
12365
+ via: "history",
12366
+ action: "undo"
12367
+ });
12368
+ context.redoStack.push({ id: item.id, frames: result.reverse });
12369
+ notifyPrivateHistory({ action: "undo", id: item.id });
10800
12370
  notify(result.updates);
10801
- context.redoStack.push(result.reverse);
10802
12371
  onHistoryChange();
10803
12372
  for (const op of result.opsToEmit) {
10804
12373
  context.buffer.storageOperations.push(op);
@@ -10809,14 +12378,19 @@ function createRoom(options, config) {
10809
12378
  if (context.activeBatch) {
10810
12379
  throw new Error("redo is not allowed during a batch");
10811
12380
  }
10812
- const frames = context.redoStack.pop();
10813
- if (frames === void 0) {
12381
+ const item = context.redoStack.pop();
12382
+ if (item === void 0) {
10814
12383
  return;
10815
12384
  }
10816
12385
  context.pausedHistory = null;
10817
- const result = applyLocalOps(frames);
12386
+ const result = applyLocalOps(item.frames, {
12387
+ origin: "local",
12388
+ via: "history",
12389
+ action: "redo"
12390
+ });
12391
+ context.undoStack.push({ id: item.id, frames: result.reverse });
12392
+ notifyPrivateHistory({ action: "redo", id: item.id });
10818
12393
  notify(result.updates);
10819
- context.undoStack.push(result.reverse);
10820
12394
  onHistoryChange();
10821
12395
  for (const op of result.opsToEmit) {
10822
12396
  context.buffer.storageOperations.push(op);
@@ -10826,6 +12400,8 @@ function createRoom(options, config) {
10826
12400
  function clear() {
10827
12401
  context.undoStack.length = 0;
10828
12402
  context.redoStack.length = 0;
12403
+ notifyPrivateHistory({ action: "clear" });
12404
+ onHistoryChange();
10829
12405
  }
10830
12406
  function batch2(callback) {
10831
12407
  if (context.activeBatch) {
@@ -10854,7 +12430,7 @@ function createRoom(options, config) {
10854
12430
  commitPausedHistoryToUndoStack();
10855
12431
  }
10856
12432
  if (currentBatch.ops.length > 0) {
10857
- context.redoStack.length = 0;
12433
+ clearRedoStack();
10858
12434
  }
10859
12435
  if (currentBatch.ops.length > 0) {
10860
12436
  dispatchOps(currentBatch.ops);
@@ -10883,7 +12459,6 @@ function createRoom(options, config) {
10883
12459
  }
10884
12460
  commitPausedHistoryToUndoStack();
10885
12461
  }
10886
- let historyDisabled = 0;
10887
12462
  function disableHistory(fn) {
10888
12463
  const origUndo = context.undoStack;
10889
12464
  const origRedo = context.redoStack;
@@ -10936,7 +12511,7 @@ function createRoom(options, config) {
10936
12511
  }
10937
12512
  }
10938
12513
  function isStorageReady() {
10939
- return getStorageSnapshot() !== null;
12514
+ return getStorageOrNull() !== null;
10940
12515
  }
10941
12516
  async function waitUntilStorageReady() {
10942
12517
  while (!isStorageReady()) {
@@ -11122,13 +12697,28 @@ function createRoom(options, config) {
11122
12697
  },
11123
12698
  // prettier-ignore
11124
12699
  get undoStack() {
11125
- return deepClone(context.undoStack);
12700
+ return structuredClone(
12701
+ context.undoStack.map((item) => ({
12702
+ id: item.id,
12703
+ frames: item.frames
12704
+ }))
12705
+ );
12706
+ },
12707
+ // prettier-ignore
12708
+ get redoStack() {
12709
+ return structuredClone(
12710
+ context.redoStack.map((item) => ({
12711
+ id: item.id,
12712
+ frames: item.frames
12713
+ }))
12714
+ );
11126
12715
  },
11127
12716
  // prettier-ignore
11128
12717
  get nodeCount() {
11129
12718
  return context.pool.nodes.size;
11130
12719
  },
11131
12720
  // prettier-ignore
12721
+ history: eventHub.privateHistory.observable,
11132
12722
  getYjsProvider() {
11133
12723
  return context.yjsProvider;
11134
12724
  },
@@ -11179,7 +12769,9 @@ function createRoom(options, config) {
11179
12769
  _dump: () => {
11180
12770
  const n = context.pool.nodes.size;
11181
12771
  return `Room "${roomId}" (${n} node${n === 1 ? "" : "s"}):
11182
- ${dumpPool(context.pool)}`;
12772
+ ${dumpPool(
12773
+ context.pool
12774
+ )}`;
11183
12775
  },
11184
12776
  destroy: () => {
11185
12777
  pendingFeedsRequests.forEach(
@@ -11226,7 +12818,9 @@ ${dumpPool(context.pool)}`;
11226
12818
  updateFeedMessage,
11227
12819
  deleteFeedMessage,
11228
12820
  getStorage,
11229
- getStorageSnapshot,
12821
+ getStorageOrNull,
12822
+ getStorageSnapshot: getStorageOrNull,
12823
+ // Deprecated alias, will be removed in the future
11230
12824
  getStorageStatus,
11231
12825
  isPresenceReady,
11232
12826
  isStorageReady,
@@ -11372,7 +12966,11 @@ function isRoomEventName(value) {
11372
12966
  }
11373
12967
  function makeAuthDelegateForRoom(roomId, authManager) {
11374
12968
  return async () => {
11375
- return authManager.getAuthValue({ requestedScope: "room:read", roomId });
12969
+ return authManager.getAuthValue({
12970
+ roomId,
12971
+ resource: "room",
12972
+ access: "read"
12973
+ });
11376
12974
  };
11377
12975
  }
11378
12976
  function makeCreateSocketDelegateForRoom(roomId, baseUrl, WebSocketPolyfill) {
@@ -11444,7 +13042,6 @@ function createClient(options) {
11444
13042
  const httpClient = createApiClient({
11445
13043
  baseUrl,
11446
13044
  fetchPolyfill,
11447
- currentUserId,
11448
13045
  authManager
11449
13046
  });
11450
13047
  const roomsById = /* @__PURE__ */ new Map();
@@ -11462,7 +13059,8 @@ function createClient(options) {
11462
13059
  ),
11463
13060
  authenticate: async () => {
11464
13061
  const resp = await authManager.getAuthValue({
11465
- requestedScope: "room:read"
13062
+ resource: "personal",
13063
+ access: "write"
11466
13064
  });
11467
13065
  if (resp.type === "public") {
11468
13066
  throw new StopRetrying(
@@ -11535,7 +13133,6 @@ function createClient(options) {
11535
13133
  enableDebugLogging: clientOptions.enableDebugLogging,
11536
13134
  baseUrl,
11537
13135
  errorEventSource: liveblocksErrorSource,
11538
- unstable_streamData: !!clientOptions.unstable_streamData,
11539
13136
  roomHttpClient: httpClient,
11540
13137
  createSyncSource,
11541
13138
  badgeLocation: clientOptions.badgeLocation ?? "bottom-right"
@@ -12141,6 +13738,12 @@ function toPlainLson(lson) {
12141
13738
  liveblocksType: "LiveList",
12142
13739
  data: [...lson].map((item) => toPlainLson(item))
12143
13740
  };
13741
+ } else if (lson instanceof LiveText) {
13742
+ return {
13743
+ liveblocksType: "LiveText",
13744
+ data: lson.toJSON(),
13745
+ version: lson.version
13746
+ };
12144
13747
  } else {
12145
13748
  return lson;
12146
13749
  }
@@ -12322,6 +13925,7 @@ export {
12322
13925
  LiveList,
12323
13926
  LiveMap,
12324
13927
  LiveObject,
13928
+ LiveText,
12325
13929
  LiveblocksError,
12326
13930
  MENTION_CHARACTER,
12327
13931
  MutableSignal,
@@ -12333,6 +13937,7 @@ export {
12333
13937
  SortedList,
12334
13938
  TextEditorType,
12335
13939
  WebsocketCloseCodes,
13940
+ applyLiveTextOperations,
12336
13941
  asPos,
12337
13942
  assert,
12338
13943
  assertNever,
@@ -12359,6 +13964,7 @@ export {
12359
13964
  createManagedPool,
12360
13965
  createNotificationSettings,
12361
13966
  createThreadId,
13967
+ deepLiveify,
12362
13968
  defineAiTool,
12363
13969
  deprecate,
12364
13970
  deprecateIf,
@@ -12370,6 +13976,7 @@ export {
12370
13976
  generateUrl,
12371
13977
  getMentionsFromCommentBody,
12372
13978
  getSubscriptionKey,
13979
+ hasPermissionAccess,
12373
13980
  html,
12374
13981
  htmlSafe,
12375
13982
  isCommentBodyLink,
@@ -12388,8 +13995,10 @@ export {
12388
13995
  isRegisterStorageNode,
12389
13996
  isRootStorageNode,
12390
13997
  isStartsWithOperator,
13998
+ isTextStorageNode,
12391
13999
  isUrl,
12392
14000
  kInternal,
14001
+ kStorageUpdateSource,
12393
14002
  keys,
12394
14003
  makeAbortController,
12395
14004
  makeEventSource,
@@ -12397,11 +14006,16 @@ export {
12397
14006
  makePosition,
12398
14007
  mapValues,
12399
14008
  memoizeOnSuccess,
14009
+ mergeRoomPermissionScopes,
12400
14010
  nanoid,
12401
14011
  nn,
12402
14012
  nodeStreamToCompactNodes,
14013
+ normalizeRoomAccesses,
14014
+ normalizeRoomPermissions,
14015
+ normalizeUpdateRoomAccesses,
12403
14016
  objectToQuery,
12404
14017
  patchNotificationSettings,
14018
+ permissionMatrixFromScopes,
12405
14019
  raise,
12406
14020
  resolveMentionsInCommentBody,
12407
14021
  sanitizeUrl,
@@ -12411,9 +14025,11 @@ export {
12411
14025
  stringifyCommentBody,
12412
14026
  throwUsageError,
12413
14027
  toPlainLson,
14028
+ transformTextOperations,
12414
14029
  tryParseJson,
12415
14030
  url,
12416
14031
  urljoin,
14032
+ validatePermissionsSet,
12417
14033
  wait,
12418
14034
  warnOnce,
12419
14035
  warnOnceIf,