@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.cjs +2121 -505
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +546 -157
- package/dist/index.d.ts +546 -157
- package/dist/index.js +2010 -394
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
1582
|
-
|
|
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
|
-
|
|
1621
|
-
|
|
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
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1717
|
-
|
|
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
|
-
|
|
1744
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
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
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
1781
|
-
|
|
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
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
1816
|
-
|
|
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
|
-
|
|
1825
|
-
|
|
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
|
-
|
|
1834
|
-
|
|
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
|
-
|
|
1843
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1910
|
-
|
|
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
|
-
|
|
1928
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
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
|
-
|
|
1978
|
-
|
|
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
|
-
|
|
1990
|
-
|
|
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
|
-
|
|
2008
|
-
|
|
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
|
-
|
|
2129
|
-
|
|
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
|
-
|
|
2142
|
-
|
|
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
|
-
|
|
2155
|
-
|
|
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
|
-
|
|
2176
|
-
|
|
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
|
-
|
|
2191
|
-
|
|
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
|
-
|
|
2200
|
-
|
|
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
|
-
|
|
2209
|
-
|
|
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
|
-
|
|
2218
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
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
|
-
|
|
2254
|
-
|
|
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
|
-
|
|
2272
|
-
|
|
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
|
-
|
|
2292
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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/
|
|
5237
|
-
var Permission =
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
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
|
|
5310
|
-
|
|
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 (
|
|
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
|
-
|
|
5389
|
-
|
|
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(
|
|
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
|
-
|
|
5407
|
-
|
|
5626
|
+
const cachedToken2 = makeCachedToken(token, expiresAt);
|
|
5627
|
+
tokens.push(cachedToken2);
|
|
5408
5628
|
return { type: "secret", token };
|
|
5409
5629
|
} finally {
|
|
5410
|
-
if (
|
|
5411
|
-
requestPromises.delete(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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/
|
|
8430
|
-
function
|
|
8431
|
-
|
|
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 (
|
|
8452
|
-
return
|
|
8743
|
+
if (left === void 0 || right === void 0) {
|
|
8744
|
+
return false;
|
|
8453
8745
|
}
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
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
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
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
|
|
8483
|
-
return
|
|
8777
|
+
function dataToSegments(data) {
|
|
8778
|
+
return normalizeSegments(
|
|
8779
|
+
data.map(([text, attributes]) => ({
|
|
8780
|
+
text,
|
|
8781
|
+
attributes
|
|
8782
|
+
}))
|
|
8783
|
+
);
|
|
8484
8784
|
}
|
|
8485
|
-
function
|
|
8486
|
-
return
|
|
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
|
|
8489
|
-
return
|
|
8790
|
+
function textLength(segments) {
|
|
8791
|
+
return segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
8490
8792
|
}
|
|
8491
|
-
function
|
|
8492
|
-
|
|
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
|
|
8495
|
-
|
|
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
|
|
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
|
-
|
|
8547
|
-
if (!
|
|
9992
|
+
prev.forEach((_, id) => {
|
|
9993
|
+
if (!next.get(id)) {
|
|
8548
9994
|
ops.push({ type: OpCode.DELETE_CRDT, id });
|
|
8549
9995
|
}
|
|
8550
9996
|
});
|
|
8551
|
-
|
|
8552
|
-
const currentCrdt =
|
|
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
|
|
8556
|
-
ops.push({
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
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
|
-
|
|
10127
|
+
merged = mergeObjectStorageUpdates(first, second);
|
|
8649
10128
|
} else if (first.type === "LiveMap" && second.type === "LiveMap") {
|
|
8650
|
-
|
|
10129
|
+
merged = mergeMapStorageUpdates(first, second);
|
|
8651
10130
|
} else if (first.type === "LiveList" && second.type === "LiveList") {
|
|
8652
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
|
9702
|
-
return
|
|
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 =
|
|
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:
|
|
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 =
|
|
9797
|
-
const result = applyRemoteOps(
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
10233
|
-
//
|
|
10234
|
-
//
|
|
10235
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
|
10795
|
-
if (
|
|
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
|
|
10813
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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,
|