canvu-react 0.3.10 → 0.3.12
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/asset-hydration-3Iv5xHxM.d.cts +27 -0
- package/dist/asset-hydration-BEG21hMp.d.ts +27 -0
- package/dist/chatbot.d.cts +2 -2
- package/dist/chatbot.d.ts +2 -2
- package/dist/index.cjs +469 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +468 -9
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +645 -704
- package/dist/native.cjs.map +1 -1
- package/dist/native.js +645 -704
- package/dist/native.js.map +1 -1
- package/dist/react.cjs +354 -137
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +9 -16
- package/dist/react.d.ts +9 -16
- package/dist/react.js +354 -138
- package/dist/react.js.map +1 -1
- package/dist/realtime.cjs +566 -55
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.d.cts +40 -5
- package/dist/realtime.d.ts +40 -5
- package/dist/realtime.js +566 -56
- package/dist/realtime.js.map +1 -1
- package/dist/{shape-builders-DxPoOecg.d.cts → shape-builders-DFudWDFI.d.cts} +111 -1
- package/dist/{shape-builders-DTYvub8W.d.ts → shape-builders-ENwnK-zT.d.ts} +111 -1
- package/dist/{types-DgEArHkA.d.ts → types-BtAJFS_-.d.ts} +1 -0
- package/dist/{types-BtLGGw0r.d.cts → types-CTyASYIm.d.cts} +2 -105
- package/dist/{types-B58i5k-u.d.cts → types-DNwjgs5U.d.cts} +1 -0
- package/dist/{types-ChnTSRSe.d.ts → types-UvUy2Eed.d.ts} +2 -105
- package/package.json +1 -1
package/dist/realtime.js
CHANGED
|
@@ -1742,7 +1742,7 @@ function useRealtimePeerFollow(options) {
|
|
|
1742
1742
|
const { viewportRef, sessionPeers, followedPeerId, onFollowEnd } = options;
|
|
1743
1743
|
const endedPeerIdRef = useRef(null);
|
|
1744
1744
|
const lastAppliedCameraKeyRef = useRef(null);
|
|
1745
|
-
const [
|
|
1745
|
+
const [, setViewportSizeVersion] = useState(0);
|
|
1746
1746
|
useEffect(() => {
|
|
1747
1747
|
if (!followedPeerId) return;
|
|
1748
1748
|
let animationFrameId = 0;
|
|
@@ -1794,16 +1794,27 @@ function useRealtimePeerFollow(options) {
|
|
|
1794
1794
|
return;
|
|
1795
1795
|
}
|
|
1796
1796
|
lastAppliedCameraKeyRef.current = nextCameraKey;
|
|
1797
|
-
}, [
|
|
1798
|
-
followedPeerId,
|
|
1799
|
-
onFollowEnd,
|
|
1800
|
-
sessionPeers,
|
|
1801
|
-
viewportRef,
|
|
1802
|
-
viewportSizeVersion
|
|
1803
|
-
]);
|
|
1797
|
+
}, [followedPeerId, onFollowEnd, sessionPeers, viewportRef]);
|
|
1804
1798
|
}
|
|
1805
1799
|
|
|
1806
1800
|
// src/react/plugins/realtime/use-realtime-session.ts
|
|
1801
|
+
var DRAFT_STORAGE_PREFIX = "canvu-realtime-draft:";
|
|
1802
|
+
var DOCUMENT_FLUSH_DEBOUNCE_MS = 120;
|
|
1803
|
+
var DRAFT_PERSIST_DEBOUNCE_MS = 420;
|
|
1804
|
+
function requestWindowIdleCallback(callback, timeout) {
|
|
1805
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
1806
|
+
return window.requestIdleCallback(callback, { timeout });
|
|
1807
|
+
}
|
|
1808
|
+
return window.setTimeout(callback, timeout);
|
|
1809
|
+
}
|
|
1810
|
+
function cancelWindowIdleCallback(handle) {
|
|
1811
|
+
if (handle == null) return;
|
|
1812
|
+
if (typeof window.cancelIdleCallback === "function") {
|
|
1813
|
+
window.cancelIdleCallback(handle);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
window.clearTimeout(handle);
|
|
1817
|
+
}
|
|
1807
1818
|
function createClientId() {
|
|
1808
1819
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1809
1820
|
return crypto.randomUUID();
|
|
@@ -1811,6 +1822,7 @@ function createClientId() {
|
|
|
1811
1822
|
return `client-${Math.random().toString(36).slice(2, 10)}`;
|
|
1812
1823
|
}
|
|
1813
1824
|
function normalizeSocketUrl(input) {
|
|
1825
|
+
if (!input) return "";
|
|
1814
1826
|
if (input.startsWith("ws://") || input.startsWith("wss://")) return input;
|
|
1815
1827
|
if (input.startsWith("http://")) return `ws://${input.slice("http://".length)}`;
|
|
1816
1828
|
if (input.startsWith("https://")) return `wss://${input.slice("https://".length)}`;
|
|
@@ -1829,9 +1841,29 @@ function isValidSocketUrl(input) {
|
|
|
1829
1841
|
return false;
|
|
1830
1842
|
}
|
|
1831
1843
|
}
|
|
1844
|
+
function sanitizeRealtimeItem(item) {
|
|
1845
|
+
if (item.toolKind !== "image") return item;
|
|
1846
|
+
return {
|
|
1847
|
+
...item,
|
|
1848
|
+
imageBlobId: void 0,
|
|
1849
|
+
imageThumbnailBlobId: void 0,
|
|
1850
|
+
imageRasterHref: void 0,
|
|
1851
|
+
imageThumbnailHref: void 0,
|
|
1852
|
+
childrenSvg: ""
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
function sanitizeRealtimeItems(items) {
|
|
1856
|
+
return items.map(sanitizeRealtimeItem);
|
|
1857
|
+
}
|
|
1858
|
+
function sanitizeRealtimeSnapshot(snapshot) {
|
|
1859
|
+
return {
|
|
1860
|
+
...snapshot,
|
|
1861
|
+
items: sanitizeRealtimeItems(snapshot.items)
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1832
1864
|
function serializeItems(items) {
|
|
1833
1865
|
try {
|
|
1834
|
-
return JSON.stringify(items);
|
|
1866
|
+
return JSON.stringify(sanitizeRealtimeItems(items));
|
|
1835
1867
|
} catch {
|
|
1836
1868
|
return null;
|
|
1837
1869
|
}
|
|
@@ -1846,6 +1878,58 @@ function sameSerializedItems(left, right) {
|
|
|
1846
1878
|
function nowMs() {
|
|
1847
1879
|
return Date.now();
|
|
1848
1880
|
}
|
|
1881
|
+
function draftStorageKey(roomId) {
|
|
1882
|
+
return `${DRAFT_STORAGE_PREFIX}${roomId}`;
|
|
1883
|
+
}
|
|
1884
|
+
function isRealtimeOfflineDraft(value) {
|
|
1885
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1886
|
+
const record = value;
|
|
1887
|
+
return typeof record.roomId === "string" && typeof record.baseRevision === "number" && typeof record.updatedAt === "number" && Array.isArray(record.items);
|
|
1888
|
+
}
|
|
1889
|
+
function readRealtimeOfflineDraft(roomId) {
|
|
1890
|
+
if (typeof window === "undefined" || !roomId) return null;
|
|
1891
|
+
try {
|
|
1892
|
+
const raw = window.localStorage.getItem(draftStorageKey(roomId));
|
|
1893
|
+
if (!raw) return null;
|
|
1894
|
+
const parsed = JSON.parse(raw);
|
|
1895
|
+
if (!isRealtimeOfflineDraft(parsed)) return null;
|
|
1896
|
+
return {
|
|
1897
|
+
...parsed,
|
|
1898
|
+
items: sanitizeRealtimeItems(parsed.items)
|
|
1899
|
+
};
|
|
1900
|
+
} catch {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function writeRealtimeOfflineDraft(draft) {
|
|
1905
|
+
if (typeof window === "undefined") return;
|
|
1906
|
+
if (!draft) return;
|
|
1907
|
+
try {
|
|
1908
|
+
window.localStorage.setItem(
|
|
1909
|
+
draftStorageKey(draft.roomId),
|
|
1910
|
+
JSON.stringify({
|
|
1911
|
+
...draft,
|
|
1912
|
+
items: sanitizeRealtimeItems(draft.items)
|
|
1913
|
+
})
|
|
1914
|
+
);
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
function removeRealtimeOfflineDraft(roomId) {
|
|
1919
|
+
if (typeof window === "undefined" || !roomId) return;
|
|
1920
|
+
try {
|
|
1921
|
+
window.localStorage.removeItem(draftStorageKey(roomId));
|
|
1922
|
+
} catch {
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function buildDraftSnapshot(draft, clientId) {
|
|
1926
|
+
return {
|
|
1927
|
+
revision: draft.baseRevision,
|
|
1928
|
+
items: draft.items,
|
|
1929
|
+
updatedAt: draft.updatedAt,
|
|
1930
|
+
updatedByClientId: clientId
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1849
1933
|
function getViewportCameraSnapshot(viewport) {
|
|
1850
1934
|
if (!viewport) return null;
|
|
1851
1935
|
const camera = viewport.getCamera();
|
|
@@ -1859,6 +1943,17 @@ function getViewportCameraSnapshot(viewport) {
|
|
|
1859
1943
|
viewportHeight: viewportSize.height
|
|
1860
1944
|
};
|
|
1861
1945
|
}
|
|
1946
|
+
function prepareRealtimeItems(items) {
|
|
1947
|
+
try {
|
|
1948
|
+
const sanitizedItems = sanitizeRealtimeItems(items);
|
|
1949
|
+
return {
|
|
1950
|
+
items: sanitizedItems,
|
|
1951
|
+
serialized: JSON.stringify(sanitizedItems)
|
|
1952
|
+
};
|
|
1953
|
+
} catch {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1862
1957
|
function useRealtimeSession(options) {
|
|
1863
1958
|
const {
|
|
1864
1959
|
url,
|
|
@@ -1877,6 +1972,10 @@ function useRealtimeSession(options) {
|
|
|
1877
1972
|
const reconnectTimerRef = useRef(null);
|
|
1878
1973
|
const heartbeatTimerRef = useRef(null);
|
|
1879
1974
|
const connectTimeoutRef = useRef(null);
|
|
1975
|
+
const documentFlushTimerRef = useRef(null);
|
|
1976
|
+
const documentFlushIdleRef = useRef(null);
|
|
1977
|
+
const draftPersistTimerRef = useRef(null);
|
|
1978
|
+
const draftPersistIdleRef = useRef(null);
|
|
1880
1979
|
const manualDisconnectRef = useRef(false);
|
|
1881
1980
|
const retryCountRef = useRef(0);
|
|
1882
1981
|
const currentRevisionRef = useRef(0);
|
|
@@ -1893,6 +1992,8 @@ function useRealtimeSession(options) {
|
|
|
1893
1992
|
const connectionStateRef = useRef(
|
|
1894
1993
|
enabled ? "connecting" : "offline"
|
|
1895
1994
|
);
|
|
1995
|
+
const localDraftRef = useRef(null);
|
|
1996
|
+
const conflictRef = useRef(null);
|
|
1896
1997
|
const onErrorRef = useRef(onError);
|
|
1897
1998
|
onErrorRef.current = onError;
|
|
1898
1999
|
const [connectSequence, setConnectSequence] = useState(0);
|
|
@@ -1909,7 +2010,18 @@ function useRealtimeSession(options) {
|
|
|
1909
2010
|
});
|
|
1910
2011
|
const [sessionPeers, setSessionPeers] = useState([]);
|
|
1911
2012
|
const [document2, setDocument] = useState(null);
|
|
2013
|
+
const [hasLocalOfflineDraft, setHasLocalOfflineDraft] = useState(false);
|
|
2014
|
+
const [hasPendingDocumentSync, setHasPendingDocumentSync] = useState(false);
|
|
2015
|
+
const [conflict, setConflict] = useState(null);
|
|
1912
2016
|
connectionStateRef.current = connection.state;
|
|
2017
|
+
const syncState = useMemo(() => {
|
|
2018
|
+
if (conflict != null) return "conflicted";
|
|
2019
|
+
if (connection.connected) return "connected";
|
|
2020
|
+
if (connection.state === "reconnecting" || connection.state === "connecting") {
|
|
2021
|
+
return "reconnecting";
|
|
2022
|
+
}
|
|
2023
|
+
return "offline";
|
|
2024
|
+
}, [conflict, connection.connected, connection.state]);
|
|
1913
2025
|
const clearReconnectTimer = useCallback(() => {
|
|
1914
2026
|
if (reconnectTimerRef.current != null) {
|
|
1915
2027
|
window.clearTimeout(reconnectTimerRef.current);
|
|
@@ -1928,6 +2040,22 @@ function useRealtimeSession(options) {
|
|
|
1928
2040
|
connectTimeoutRef.current = null;
|
|
1929
2041
|
}
|
|
1930
2042
|
}, []);
|
|
2043
|
+
const clearDocumentFlushSchedule = useCallback(() => {
|
|
2044
|
+
if (documentFlushTimerRef.current != null) {
|
|
2045
|
+
window.clearTimeout(documentFlushTimerRef.current);
|
|
2046
|
+
documentFlushTimerRef.current = null;
|
|
2047
|
+
}
|
|
2048
|
+
cancelWindowIdleCallback(documentFlushIdleRef.current);
|
|
2049
|
+
documentFlushIdleRef.current = null;
|
|
2050
|
+
}, []);
|
|
2051
|
+
const clearDraftPersistSchedule = useCallback(() => {
|
|
2052
|
+
if (draftPersistTimerRef.current != null) {
|
|
2053
|
+
window.clearTimeout(draftPersistTimerRef.current);
|
|
2054
|
+
draftPersistTimerRef.current = null;
|
|
2055
|
+
}
|
|
2056
|
+
cancelWindowIdleCallback(draftPersistIdleRef.current);
|
|
2057
|
+
draftPersistIdleRef.current = null;
|
|
2058
|
+
}, []);
|
|
1931
2059
|
const updateConnection = useCallback(
|
|
1932
2060
|
(patch) => {
|
|
1933
2061
|
setConnection((prev) => {
|
|
@@ -1953,6 +2081,84 @@ function useRealtimeSession(options) {
|
|
|
1953
2081
|
},
|
|
1954
2082
|
[notifySubscribers]
|
|
1955
2083
|
);
|
|
2084
|
+
const setConflictState = useCallback(
|
|
2085
|
+
(nextConflict) => {
|
|
2086
|
+
conflictRef.current = nextConflict;
|
|
2087
|
+
setConflict(nextConflict);
|
|
2088
|
+
},
|
|
2089
|
+
[]
|
|
2090
|
+
);
|
|
2091
|
+
const setLocalDraft = useCallback(
|
|
2092
|
+
(nextDraft) => {
|
|
2093
|
+
localDraftRef.current = nextDraft;
|
|
2094
|
+
setHasLocalOfflineDraft(nextDraft != null);
|
|
2095
|
+
if (nextDraft) return;
|
|
2096
|
+
removeRealtimeOfflineDraft(roomId);
|
|
2097
|
+
},
|
|
2098
|
+
[roomId]
|
|
2099
|
+
);
|
|
2100
|
+
const persistLocalDraft = useCallback(() => {
|
|
2101
|
+
clearDraftPersistSchedule();
|
|
2102
|
+
if (!localDraftRef.current) {
|
|
2103
|
+
removeRealtimeOfflineDraft(roomId);
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
writeRealtimeOfflineDraft(localDraftRef.current);
|
|
2107
|
+
}, [clearDraftPersistSchedule, roomId]);
|
|
2108
|
+
const scheduleDraftPersistence = useCallback(() => {
|
|
2109
|
+
clearDraftPersistSchedule();
|
|
2110
|
+
draftPersistTimerRef.current = window.setTimeout(() => {
|
|
2111
|
+
draftPersistTimerRef.current = null;
|
|
2112
|
+
draftPersistIdleRef.current = requestWindowIdleCallback(() => {
|
|
2113
|
+
draftPersistIdleRef.current = null;
|
|
2114
|
+
persistLocalDraft();
|
|
2115
|
+
}, DRAFT_PERSIST_DEBOUNCE_MS);
|
|
2116
|
+
}, DRAFT_PERSIST_DEBOUNCE_MS);
|
|
2117
|
+
}, [clearDraftPersistSchedule, persistLocalDraft]);
|
|
2118
|
+
const clearLocalDraft = useCallback(() => {
|
|
2119
|
+
setLocalDraft(null);
|
|
2120
|
+
clearDraftPersistSchedule();
|
|
2121
|
+
}, [clearDraftPersistSchedule, setLocalDraft]);
|
|
2122
|
+
const createSelfPeer = useCallback(
|
|
2123
|
+
(state) => ({
|
|
2124
|
+
id: peer.id,
|
|
2125
|
+
clientId: clientIdRef.current,
|
|
2126
|
+
peerId: peer.id,
|
|
2127
|
+
roomId,
|
|
2128
|
+
joinedAt: connection.lastConnectedAt ?? nowMs(),
|
|
2129
|
+
lastSeenAt: nowMs(),
|
|
2130
|
+
cursor: lastCursorRef.current,
|
|
2131
|
+
isSelf: true,
|
|
2132
|
+
connectionState: state,
|
|
2133
|
+
...peer.displayName ? { displayName: peer.displayName } : {},
|
|
2134
|
+
...peer.color ? { color: peer.color } : {},
|
|
2135
|
+
...peer.image ? { image: peer.image } : {},
|
|
2136
|
+
...lastMarkupStrokeRef.current !== void 0 ? { markupStroke: lastMarkupStrokeRef.current ?? null } : {},
|
|
2137
|
+
...lastCameraRef.current !== void 0 ? { camera: lastCameraRef.current ?? null } : {},
|
|
2138
|
+
...lastActiveToolRef.current ? { activeTool: lastActiveToolRef.current } : {}
|
|
2139
|
+
}),
|
|
2140
|
+
[
|
|
2141
|
+
connection.lastConnectedAt,
|
|
2142
|
+
peer.color,
|
|
2143
|
+
peer.displayName,
|
|
2144
|
+
peer.id,
|
|
2145
|
+
peer.image,
|
|
2146
|
+
roomId
|
|
2147
|
+
]
|
|
2148
|
+
);
|
|
2149
|
+
const collapsePeersToSelf = useCallback(
|
|
2150
|
+
(state) => {
|
|
2151
|
+
setSessionPeers((prev) => {
|
|
2152
|
+
const selfPeer = prev.find(
|
|
2153
|
+
(peerState) => peerState.clientId === clientIdRef.current
|
|
2154
|
+
);
|
|
2155
|
+
return [
|
|
2156
|
+
selfPeer ? { ...selfPeer, isSelf: true, connectionState: state } : createSelfPeer(state)
|
|
2157
|
+
];
|
|
2158
|
+
});
|
|
2159
|
+
},
|
|
2160
|
+
[createSelfPeer]
|
|
2161
|
+
);
|
|
1956
2162
|
const applyPeers = useCallback((peers) => {
|
|
1957
2163
|
const selfClientId = clientIdRef.current;
|
|
1958
2164
|
setSessionPeers(
|
|
@@ -1979,30 +2185,102 @@ function useRealtimeSession(options) {
|
|
|
1979
2185
|
[buildClientMessage]
|
|
1980
2186
|
);
|
|
1981
2187
|
const flushQueuedDocument = useCallback(() => {
|
|
2188
|
+
clearDocumentFlushSchedule();
|
|
2189
|
+
if (conflictRef.current) return;
|
|
1982
2190
|
const next = queuedItemsRef.current;
|
|
1983
2191
|
if (!next || outboundInFlightRef.current) return;
|
|
1984
|
-
queuedItemsRef.current = null;
|
|
1985
2192
|
const baseRevision = currentRevisionRef.current;
|
|
1986
|
-
const
|
|
1987
|
-
|
|
1988
|
-
|
|
2193
|
+
const preparedItems = prepareRealtimeItems(next);
|
|
2194
|
+
if (!preparedItems) return;
|
|
2195
|
+
queuedItemsRef.current = null;
|
|
2196
|
+
outboundInFlightRef.current = {
|
|
2197
|
+
baseRevision,
|
|
2198
|
+
items: preparedItems.items,
|
|
2199
|
+
serialized: preparedItems.serialized
|
|
2200
|
+
};
|
|
2201
|
+
const didSend = sendRawRef.current({
|
|
1989
2202
|
type: "document:update",
|
|
1990
2203
|
roomId,
|
|
1991
2204
|
clientId: clientIdRef.current,
|
|
1992
2205
|
baseRevision,
|
|
1993
|
-
items:
|
|
2206
|
+
items: preparedItems.items
|
|
1994
2207
|
});
|
|
1995
2208
|
if (!didSend) {
|
|
1996
2209
|
queuedItemsRef.current = next;
|
|
1997
2210
|
outboundInFlightRef.current = null;
|
|
2211
|
+
setHasPendingDocumentSync(true);
|
|
1998
2212
|
}
|
|
1999
|
-
}, [
|
|
2213
|
+
}, [clearDocumentFlushSchedule, roomId]);
|
|
2214
|
+
const scheduleDocumentFlush = useCallback(() => {
|
|
2215
|
+
clearDocumentFlushSchedule();
|
|
2216
|
+
documentFlushTimerRef.current = window.setTimeout(() => {
|
|
2217
|
+
documentFlushTimerRef.current = null;
|
|
2218
|
+
documentFlushIdleRef.current = requestWindowIdleCallback(() => {
|
|
2219
|
+
documentFlushIdleRef.current = null;
|
|
2220
|
+
flushQueuedDocument();
|
|
2221
|
+
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2222
|
+
}, DOCUMENT_FLUSH_DEBOUNCE_MS);
|
|
2223
|
+
}, [clearDocumentFlushSchedule, flushQueuedDocument]);
|
|
2000
2224
|
const queueDocumentSend = useCallback(
|
|
2001
2225
|
(items) => {
|
|
2226
|
+
setLocalDraft({
|
|
2227
|
+
roomId,
|
|
2228
|
+
baseRevision: currentRevisionRef.current,
|
|
2229
|
+
items,
|
|
2230
|
+
updatedAt: nowMs()
|
|
2231
|
+
});
|
|
2232
|
+
scheduleDraftPersistence();
|
|
2002
2233
|
queuedItemsRef.current = items;
|
|
2003
|
-
|
|
2234
|
+
setHasPendingDocumentSync(true);
|
|
2235
|
+
if (conflictRef.current) return;
|
|
2236
|
+
scheduleDocumentFlushRef.current();
|
|
2237
|
+
},
|
|
2238
|
+
[roomId, scheduleDraftPersistence, setLocalDraft]
|
|
2239
|
+
);
|
|
2240
|
+
const applyDraftSnapshot = useCallback(
|
|
2241
|
+
(draft, options2) => {
|
|
2242
|
+
applyDocument(buildDraftSnapshot(draft, clientIdRef.current), options2);
|
|
2243
|
+
},
|
|
2244
|
+
[applyDocument]
|
|
2245
|
+
);
|
|
2246
|
+
const resolveAuthoritativeDocument = useCallback(
|
|
2247
|
+
(serverDocument, options2) => {
|
|
2248
|
+
const localDraft = localDraftRef.current;
|
|
2249
|
+
if (!localDraft) {
|
|
2250
|
+
setConflictState(null);
|
|
2251
|
+
setHasPendingDocumentSync(false);
|
|
2252
|
+
applyDocument(serverDocument, options2);
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
if (sameSerializedItems(localDraft.items, serverDocument.items)) {
|
|
2256
|
+
clearLocalDraftRef.current();
|
|
2257
|
+
setHasPendingDocumentSync(false);
|
|
2258
|
+
setConflictState(null);
|
|
2259
|
+
applyDocument(serverDocument, options2);
|
|
2260
|
+
return true;
|
|
2261
|
+
}
|
|
2262
|
+
if (serverDocument.revision === localDraft.baseRevision) {
|
|
2263
|
+
setConflictState(null);
|
|
2264
|
+
applyDraftSnapshot(localDraft, {
|
|
2265
|
+
suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
|
|
2266
|
+
});
|
|
2267
|
+
outboundInFlightRef.current = null;
|
|
2268
|
+
queuedItemsRef.current = localDraft.items;
|
|
2269
|
+
setHasPendingDocumentSync(true);
|
|
2270
|
+
scheduleDocumentFlush();
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
setConflictState({
|
|
2274
|
+
serverRevision: serverDocument.revision,
|
|
2275
|
+
serverItems: serverDocument.items,
|
|
2276
|
+
localItems: localDraft.items
|
|
2277
|
+
});
|
|
2278
|
+
applyDraftSnapshot(localDraft, {
|
|
2279
|
+
suppressSubscriberNotify: options2?.suppressSubscriberNotify ?? sameSerializedItems(latestDocumentRef.current?.items, localDraft.items)
|
|
2280
|
+
});
|
|
2281
|
+
return true;
|
|
2004
2282
|
},
|
|
2005
|
-
[
|
|
2283
|
+
[applyDocument, applyDraftSnapshot, scheduleDocumentFlush, setConflictState]
|
|
2006
2284
|
);
|
|
2007
2285
|
const sendPresenceUpdate = useCallback(() => {
|
|
2008
2286
|
sendRaw({
|
|
@@ -2041,18 +2319,129 @@ function useRealtimeSession(options) {
|
|
|
2041
2319
|
reconnect,
|
|
2042
2320
|
updateConnection
|
|
2043
2321
|
]);
|
|
2322
|
+
const resolveConflict = useCallback(
|
|
2323
|
+
(action) => {
|
|
2324
|
+
const activeConflict = conflictRef.current;
|
|
2325
|
+
if (!activeConflict) return;
|
|
2326
|
+
if (action === "use-server") {
|
|
2327
|
+
clearLocalDraft();
|
|
2328
|
+
queuedItemsRef.current = null;
|
|
2329
|
+
outboundInFlightRef.current = null;
|
|
2330
|
+
setConflictState(null);
|
|
2331
|
+
applyDocument({
|
|
2332
|
+
revision: activeConflict.serverRevision,
|
|
2333
|
+
items: activeConflict.serverItems,
|
|
2334
|
+
updatedAt: nowMs()
|
|
2335
|
+
});
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
const nextDraft = {
|
|
2339
|
+
roomId,
|
|
2340
|
+
baseRevision: activeConflict.serverRevision,
|
|
2341
|
+
items: activeConflict.localItems,
|
|
2342
|
+
updatedAt: nowMs()
|
|
2343
|
+
};
|
|
2344
|
+
setLocalDraft(nextDraft);
|
|
2345
|
+
scheduleDraftPersistence();
|
|
2346
|
+
setConflictState(null);
|
|
2347
|
+
applyDraftSnapshot(nextDraft, {
|
|
2348
|
+
suppressSubscriberNotify: sameSerializedItems(
|
|
2349
|
+
latestDocumentRef.current?.items,
|
|
2350
|
+
nextDraft.items
|
|
2351
|
+
)
|
|
2352
|
+
});
|
|
2353
|
+
currentRevisionRef.current = activeConflict.serverRevision;
|
|
2354
|
+
queuedItemsRef.current = nextDraft.items;
|
|
2355
|
+
outboundInFlightRef.current = null;
|
|
2356
|
+
setHasPendingDocumentSync(true);
|
|
2357
|
+
scheduleDocumentFlush();
|
|
2358
|
+
},
|
|
2359
|
+
[
|
|
2360
|
+
applyDocument,
|
|
2361
|
+
applyDraftSnapshot,
|
|
2362
|
+
clearLocalDraft,
|
|
2363
|
+
roomId,
|
|
2364
|
+
scheduleDocumentFlush,
|
|
2365
|
+
scheduleDraftPersistence,
|
|
2366
|
+
setConflictState,
|
|
2367
|
+
setLocalDraft
|
|
2368
|
+
]
|
|
2369
|
+
);
|
|
2370
|
+
const setConflictStateRef = useRef(setConflictState);
|
|
2371
|
+
setConflictStateRef.current = setConflictState;
|
|
2372
|
+
const updateConnectionRef = useRef(updateConnection);
|
|
2373
|
+
updateConnectionRef.current = updateConnection;
|
|
2374
|
+
const applyDocumentRef = useRef(applyDocument);
|
|
2375
|
+
applyDocumentRef.current = applyDocument;
|
|
2376
|
+
const clearLocalDraftRef = useRef(clearLocalDraft);
|
|
2377
|
+
clearLocalDraftRef.current = clearLocalDraft;
|
|
2378
|
+
const collapsePeersToSelfRef = useRef(collapsePeersToSelf);
|
|
2379
|
+
collapsePeersToSelfRef.current = collapsePeersToSelf;
|
|
2380
|
+
const applyPeersRef = useRef(applyPeers);
|
|
2381
|
+
applyPeersRef.current = applyPeers;
|
|
2382
|
+
const resolveAuthoritativeDocumentRef = useRef(resolveAuthoritativeDocument);
|
|
2383
|
+
resolveAuthoritativeDocumentRef.current = resolveAuthoritativeDocument;
|
|
2384
|
+
const scheduleDocumentFlushRef = useRef(scheduleDocumentFlush);
|
|
2385
|
+
scheduleDocumentFlushRef.current = scheduleDocumentFlush;
|
|
2386
|
+
const scheduleReconnectRef = useRef(scheduleReconnect);
|
|
2387
|
+
scheduleReconnectRef.current = scheduleReconnect;
|
|
2388
|
+
const sendRawRef = useRef(sendRaw);
|
|
2389
|
+
sendRawRef.current = sendRaw;
|
|
2390
|
+
useEffect(() => {
|
|
2391
|
+
if (!roomId) {
|
|
2392
|
+
clearDocumentFlushSchedule();
|
|
2393
|
+
clearDraftPersistSchedule();
|
|
2394
|
+
localDraftRef.current = null;
|
|
2395
|
+
setHasLocalOfflineDraft(false);
|
|
2396
|
+
setHasPendingDocumentSync(false);
|
|
2397
|
+
setConflictState(null);
|
|
2398
|
+
queuedItemsRef.current = null;
|
|
2399
|
+
outboundInFlightRef.current = null;
|
|
2400
|
+
latestDocumentRef.current = null;
|
|
2401
|
+
setDocument(null);
|
|
2402
|
+
currentRevisionRef.current = 0;
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const localDraft = readRealtimeOfflineDraft(roomId);
|
|
2406
|
+
setLocalDraft(localDraft);
|
|
2407
|
+
setHasPendingDocumentSync(localDraft != null);
|
|
2408
|
+
setConflictState(null);
|
|
2409
|
+
queuedItemsRef.current = localDraft?.items ?? null;
|
|
2410
|
+
outboundInFlightRef.current = null;
|
|
2411
|
+
if (localDraft) {
|
|
2412
|
+
applyDraftSnapshot(localDraft, {
|
|
2413
|
+
suppressSubscriberNotify: sameSerializedItems(
|
|
2414
|
+
latestDocumentRef.current?.items,
|
|
2415
|
+
localDraft.items
|
|
2416
|
+
)
|
|
2417
|
+
});
|
|
2418
|
+
} else {
|
|
2419
|
+
latestDocumentRef.current = null;
|
|
2420
|
+
setDocument(null);
|
|
2421
|
+
currentRevisionRef.current = 0;
|
|
2422
|
+
}
|
|
2423
|
+
}, [
|
|
2424
|
+
applyDraftSnapshot,
|
|
2425
|
+
clearDocumentFlushSchedule,
|
|
2426
|
+
clearDraftPersistSchedule,
|
|
2427
|
+
roomId,
|
|
2428
|
+
setConflictState,
|
|
2429
|
+
setLocalDraft
|
|
2430
|
+
]);
|
|
2044
2431
|
useEffect(() => {
|
|
2045
2432
|
if (!enabled) {
|
|
2046
2433
|
manualDisconnectRef.current = true;
|
|
2047
2434
|
clearReconnectTimer();
|
|
2048
2435
|
clearHeartbeatTimer();
|
|
2049
2436
|
clearConnectTimeout();
|
|
2437
|
+
clearDocumentFlushSchedule();
|
|
2050
2438
|
wsRef.current?.close();
|
|
2051
2439
|
wsRef.current = null;
|
|
2052
|
-
|
|
2053
|
-
queuedItemsRef.current = null;
|
|
2440
|
+
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2054
2441
|
outboundInFlightRef.current = null;
|
|
2055
|
-
|
|
2442
|
+
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2443
|
+
collapsePeersToSelfRef.current("offline");
|
|
2444
|
+
updateConnectionRef.current((prev) => ({
|
|
2056
2445
|
...prev,
|
|
2057
2446
|
state: "offline",
|
|
2058
2447
|
connected: false,
|
|
@@ -2063,8 +2452,19 @@ function useRealtimeSession(options) {
|
|
|
2063
2452
|
}
|
|
2064
2453
|
manualDisconnectRef.current = false;
|
|
2065
2454
|
const socketUrl = normalizeSocketUrl(url);
|
|
2455
|
+
if (!socketUrl) {
|
|
2456
|
+
collapsePeersToSelfRef.current("offline");
|
|
2457
|
+
updateConnectionRef.current((prev) => ({
|
|
2458
|
+
...prev,
|
|
2459
|
+
state: "offline",
|
|
2460
|
+
connected: false,
|
|
2461
|
+
roomId,
|
|
2462
|
+
lastError: null
|
|
2463
|
+
}));
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2066
2466
|
if (!isValidSocketUrl(socketUrl)) {
|
|
2067
|
-
|
|
2467
|
+
updateConnectionRef.current((prev) => ({
|
|
2068
2468
|
...prev,
|
|
2069
2469
|
state: "error",
|
|
2070
2470
|
connected: false,
|
|
@@ -2075,7 +2475,7 @@ function useRealtimeSession(options) {
|
|
|
2075
2475
|
return;
|
|
2076
2476
|
}
|
|
2077
2477
|
let disposed = false;
|
|
2078
|
-
|
|
2478
|
+
updateConnectionRef.current((prev) => ({
|
|
2079
2479
|
...prev,
|
|
2080
2480
|
state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
|
|
2081
2481
|
connected: false,
|
|
@@ -2093,7 +2493,7 @@ function useRealtimeSession(options) {
|
|
|
2093
2493
|
socket.addEventListener("open", () => {
|
|
2094
2494
|
if (disposed) return;
|
|
2095
2495
|
clearConnectTimeout();
|
|
2096
|
-
|
|
2496
|
+
sendRawRef.current({
|
|
2097
2497
|
type: "session:join",
|
|
2098
2498
|
roomId,
|
|
2099
2499
|
peer: {
|
|
@@ -2106,7 +2506,7 @@ function useRealtimeSession(options) {
|
|
|
2106
2506
|
});
|
|
2107
2507
|
clearHeartbeatTimer();
|
|
2108
2508
|
heartbeatTimerRef.current = window.setInterval(() => {
|
|
2109
|
-
|
|
2509
|
+
sendRawRef.current({
|
|
2110
2510
|
type: "session:ping",
|
|
2111
2511
|
roomId,
|
|
2112
2512
|
clientId: clientIdRef.current,
|
|
@@ -2126,15 +2526,13 @@ function useRealtimeSession(options) {
|
|
|
2126
2526
|
}
|
|
2127
2527
|
const parsed = parseRealtimeServerMessage(payload);
|
|
2128
2528
|
if (!parsed) return;
|
|
2129
|
-
|
|
2529
|
+
updateConnectionRef.current((prev) => ({
|
|
2130
2530
|
...prev,
|
|
2131
2531
|
lastMessageAt: nowMs()
|
|
2132
2532
|
}));
|
|
2133
2533
|
if (parsed.type === "session:welcome") {
|
|
2134
2534
|
retryCountRef.current = 0;
|
|
2135
|
-
|
|
2136
|
-
const shouldPromoteQueuedLocalDraft = queuedBeforeWelcome != null && parsed.document.revision === 0 && parsed.document.items.length === 0;
|
|
2137
|
-
updateConnection((prev) => ({
|
|
2535
|
+
updateConnectionRef.current((prev) => ({
|
|
2138
2536
|
...prev,
|
|
2139
2537
|
state: "connected",
|
|
2140
2538
|
connected: true,
|
|
@@ -2143,19 +2541,26 @@ function useRealtimeSession(options) {
|
|
|
2143
2541
|
lastConnectedAt: nowMs(),
|
|
2144
2542
|
lastError: null
|
|
2145
2543
|
}));
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2544
|
+
applyPeersRef.current(parsed.peers);
|
|
2545
|
+
const handledByDraft = resolveAuthoritativeDocumentRef.current(
|
|
2546
|
+
sanitizeRealtimeSnapshot(parsed.document),
|
|
2547
|
+
{
|
|
2548
|
+
suppressSubscriberNotify: localDraftRef.current != null && sameSerializedItems(
|
|
2549
|
+
latestDocumentRef.current?.items,
|
|
2550
|
+
localDraftRef.current.items
|
|
2551
|
+
)
|
|
2552
|
+
}
|
|
2553
|
+
);
|
|
2554
|
+
if (!handledByDraft) {
|
|
2151
2555
|
queuedItemsRef.current = null;
|
|
2152
2556
|
outboundInFlightRef.current = null;
|
|
2557
|
+
setHasPendingDocumentSync(false);
|
|
2153
2558
|
}
|
|
2154
|
-
|
|
2559
|
+
scheduleDocumentFlushRef.current();
|
|
2155
2560
|
return;
|
|
2156
2561
|
}
|
|
2157
2562
|
if (parsed.type === "presence:sync") {
|
|
2158
|
-
|
|
2563
|
+
applyPeersRef.current(parsed.peers);
|
|
2159
2564
|
return;
|
|
2160
2565
|
}
|
|
2161
2566
|
if (parsed.type === "session:peer-joined") {
|
|
@@ -2177,14 +2582,14 @@ function useRealtimeSession(options) {
|
|
|
2177
2582
|
return;
|
|
2178
2583
|
}
|
|
2179
2584
|
if (parsed.type === "session:pong") {
|
|
2180
|
-
|
|
2585
|
+
updateConnectionRef.current((prev) => ({
|
|
2181
2586
|
...prev,
|
|
2182
2587
|
lastPongAt: parsed.serverTime
|
|
2183
2588
|
}));
|
|
2184
2589
|
return;
|
|
2185
2590
|
}
|
|
2186
2591
|
if (parsed.type === "session:error") {
|
|
2187
|
-
|
|
2592
|
+
updateConnectionRef.current((prev) => ({
|
|
2188
2593
|
...prev,
|
|
2189
2594
|
state: prev.connected ? prev.state : "error",
|
|
2190
2595
|
lastError: parsed.message
|
|
@@ -2198,35 +2603,48 @@ function useRealtimeSession(options) {
|
|
|
2198
2603
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2199
2604
|
if (!isSelfAck) {
|
|
2200
2605
|
outboundInFlightRef.current = null;
|
|
2201
|
-
queuedItemsRef.current = null;
|
|
2202
|
-
|
|
2606
|
+
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2607
|
+
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2608
|
+
resolveAuthoritativeDocumentRef.current(
|
|
2609
|
+
sanitizeRealtimeSnapshot(parsed.document)
|
|
2610
|
+
);
|
|
2203
2611
|
return;
|
|
2204
2612
|
}
|
|
2205
2613
|
const shouldSuppress = inFlight != null && sameSerializedItems(inFlight.items, parsed.document.items);
|
|
2206
2614
|
outboundInFlightRef.current = null;
|
|
2207
|
-
|
|
2615
|
+
applyDocumentRef.current(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2616
|
+
suppressSubscriberNotify: shouldSuppress
|
|
2617
|
+
});
|
|
2208
2618
|
if (queuedItemsRef.current) {
|
|
2209
2619
|
if (sameSerializedItems(queuedItemsRef.current, parsed.document.items)) {
|
|
2210
2620
|
queuedItemsRef.current = null;
|
|
2211
2621
|
} else {
|
|
2212
|
-
|
|
2622
|
+
scheduleDocumentFlushRef.current();
|
|
2213
2623
|
}
|
|
2214
2624
|
}
|
|
2625
|
+
if (!queuedItemsRef.current) {
|
|
2626
|
+
clearLocalDraftRef.current();
|
|
2627
|
+
}
|
|
2628
|
+
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2629
|
+
setConflictStateRef.current(null);
|
|
2215
2630
|
return;
|
|
2216
2631
|
}
|
|
2217
2632
|
if (parsed.type === "document:resync-required") {
|
|
2218
2633
|
outboundInFlightRef.current = null;
|
|
2219
|
-
queuedItemsRef.current = null;
|
|
2220
|
-
|
|
2634
|
+
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2635
|
+
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2636
|
+
updateConnectionRef.current((prev) => ({
|
|
2221
2637
|
...prev,
|
|
2222
2638
|
lastError: parsed.reason
|
|
2223
2639
|
}));
|
|
2224
|
-
|
|
2640
|
+
resolveAuthoritativeDocumentRef.current(
|
|
2641
|
+
sanitizeRealtimeSnapshot(parsed.document)
|
|
2642
|
+
);
|
|
2225
2643
|
}
|
|
2226
2644
|
});
|
|
2227
2645
|
socket.addEventListener("error", () => {
|
|
2228
2646
|
if (disposed) return;
|
|
2229
|
-
|
|
2647
|
+
updateConnectionRef.current((prev) => ({
|
|
2230
2648
|
...prev,
|
|
2231
2649
|
state: prev.connected ? prev.state : "error",
|
|
2232
2650
|
lastError: "Falha de conex\xE3o websocket."
|
|
@@ -2237,14 +2655,17 @@ function useRealtimeSession(options) {
|
|
|
2237
2655
|
clearHeartbeatTimer();
|
|
2238
2656
|
clearConnectTimeout();
|
|
2239
2657
|
wsRef.current = null;
|
|
2240
|
-
|
|
2658
|
+
collapsePeersToSelfRef.current(
|
|
2659
|
+
manualDisconnectRef.current || !enabled ? "offline" : "reconnecting"
|
|
2660
|
+
);
|
|
2661
|
+
updateConnectionRef.current((prev) => ({
|
|
2241
2662
|
...prev,
|
|
2242
2663
|
connected: false,
|
|
2243
2664
|
clientId: prev.clientId,
|
|
2244
2665
|
state: manualDisconnectRef.current || !enabled ? "offline" : prev.state
|
|
2245
2666
|
}));
|
|
2246
2667
|
if (!manualDisconnectRef.current && enabled) {
|
|
2247
|
-
|
|
2668
|
+
scheduleReconnectRef.current();
|
|
2248
2669
|
}
|
|
2249
2670
|
});
|
|
2250
2671
|
return () => {
|
|
@@ -2252,27 +2673,23 @@ function useRealtimeSession(options) {
|
|
|
2252
2673
|
clearReconnectTimer();
|
|
2253
2674
|
clearHeartbeatTimer();
|
|
2254
2675
|
clearConnectTimeout();
|
|
2676
|
+
clearDocumentFlushSchedule();
|
|
2255
2677
|
socket.close();
|
|
2256
2678
|
};
|
|
2257
2679
|
}, [
|
|
2258
|
-
applyDocument,
|
|
2259
|
-
applyPeers,
|
|
2260
2680
|
clearConnectTimeout,
|
|
2681
|
+
clearDocumentFlushSchedule,
|
|
2261
2682
|
clearHeartbeatTimer,
|
|
2262
2683
|
clearReconnectTimer,
|
|
2263
2684
|
connectSequence,
|
|
2264
2685
|
connectTimeoutMs,
|
|
2265
2686
|
enabled,
|
|
2266
|
-
flushQueuedDocument,
|
|
2267
2687
|
heartbeatMs,
|
|
2268
2688
|
peer.color,
|
|
2269
2689
|
peer.displayName,
|
|
2270
|
-
peer.image,
|
|
2271
2690
|
peer.id,
|
|
2691
|
+
peer.image,
|
|
2272
2692
|
roomId,
|
|
2273
|
-
scheduleReconnect,
|
|
2274
|
-
sendRaw,
|
|
2275
|
-
updateConnection,
|
|
2276
2693
|
url
|
|
2277
2694
|
]);
|
|
2278
2695
|
useEffect(() => {
|
|
@@ -2286,6 +2703,18 @@ function useRealtimeSession(options) {
|
|
|
2286
2703
|
)
|
|
2287
2704
|
);
|
|
2288
2705
|
}, [connection.state]);
|
|
2706
|
+
useEffect(
|
|
2707
|
+
() => () => {
|
|
2708
|
+
clearDocumentFlushSchedule();
|
|
2709
|
+
clearDraftPersistSchedule();
|
|
2710
|
+
},
|
|
2711
|
+
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2712
|
+
);
|
|
2713
|
+
const flushDocumentSync = useCallback(async () => {
|
|
2714
|
+
persistLocalDraft();
|
|
2715
|
+
if (!connection.connected) return;
|
|
2716
|
+
flushQueuedDocument();
|
|
2717
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
2289
2718
|
const remoteAdapter = useMemo(
|
|
2290
2719
|
() => ({
|
|
2291
2720
|
subscribe(onItems) {
|
|
@@ -2299,9 +2728,12 @@ function useRealtimeSession(options) {
|
|
|
2299
2728
|
},
|
|
2300
2729
|
send(items) {
|
|
2301
2730
|
queueDocumentSend(items);
|
|
2731
|
+
},
|
|
2732
|
+
flush() {
|
|
2733
|
+
return flushDocumentSync();
|
|
2302
2734
|
}
|
|
2303
2735
|
}),
|
|
2304
|
-
[queueDocumentSend]
|
|
2736
|
+
[flushDocumentSync, queueDocumentSend]
|
|
2305
2737
|
);
|
|
2306
2738
|
const disconnect = useCallback(() => {
|
|
2307
2739
|
manualDisconnectRef.current = true;
|
|
@@ -2315,6 +2747,7 @@ function useRealtimeSession(options) {
|
|
|
2315
2747
|
});
|
|
2316
2748
|
wsRef.current?.close();
|
|
2317
2749
|
wsRef.current = null;
|
|
2750
|
+
collapsePeersToSelf("offline");
|
|
2318
2751
|
updateConnection((prev) => ({
|
|
2319
2752
|
...prev,
|
|
2320
2753
|
state: "offline",
|
|
@@ -2324,6 +2757,7 @@ function useRealtimeSession(options) {
|
|
|
2324
2757
|
clearConnectTimeout,
|
|
2325
2758
|
clearHeartbeatTimer,
|
|
2326
2759
|
clearReconnectTimer,
|
|
2760
|
+
collapsePeersToSelf,
|
|
2327
2761
|
roomId,
|
|
2328
2762
|
sendRaw,
|
|
2329
2763
|
updateConnection
|
|
@@ -2389,8 +2823,15 @@ function useRealtimeSession(options) {
|
|
|
2389
2823
|
remotePresence,
|
|
2390
2824
|
remoteAdapter,
|
|
2391
2825
|
document: document2,
|
|
2826
|
+
hasLocalOfflineDraft,
|
|
2827
|
+
hasPendingDocumentSync,
|
|
2828
|
+
syncState,
|
|
2829
|
+
conflict,
|
|
2392
2830
|
bindViewportPresence,
|
|
2393
2831
|
syncViewportPresence,
|
|
2832
|
+
resolveConflict,
|
|
2833
|
+
clearLocalDraft,
|
|
2834
|
+
flushDocumentSync,
|
|
2394
2835
|
disconnect,
|
|
2395
2836
|
reconnectNow
|
|
2396
2837
|
};
|
|
@@ -2543,7 +2984,76 @@ function realtimeSessionPlugin(options) {
|
|
|
2543
2984
|
render: () => /* @__PURE__ */ jsx(RealtimeSessionPanel, { ...options })
|
|
2544
2985
|
};
|
|
2545
2986
|
}
|
|
2987
|
+
function useRealtimeCanvasDocument(options) {
|
|
2988
|
+
const {
|
|
2989
|
+
session,
|
|
2990
|
+
items,
|
|
2991
|
+
onItemsChange,
|
|
2992
|
+
normalizeItems,
|
|
2993
|
+
hydrateItems,
|
|
2994
|
+
enabled = true
|
|
2995
|
+
} = options;
|
|
2996
|
+
const [loading, setLoading] = useState(false);
|
|
2997
|
+
const lastAppliedRevisionRef = useRef(null);
|
|
2998
|
+
const realtimeEnabled = enabled && session != null;
|
|
2999
|
+
const applyIncomingItems = useCallback(
|
|
3000
|
+
async (nextItems) => {
|
|
3001
|
+
const normalizedItems = normalizeItems ? normalizeItems(nextItems) : [...nextItems];
|
|
3002
|
+
if (!hydrateItems) return normalizedItems;
|
|
3003
|
+
return await hydrateItems(normalizedItems);
|
|
3004
|
+
},
|
|
3005
|
+
[hydrateItems, normalizeItems]
|
|
3006
|
+
);
|
|
3007
|
+
const handleItemsChange = useCallback(
|
|
3008
|
+
(nextItems) => {
|
|
3009
|
+
if (!enabled) {
|
|
3010
|
+
onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
|
|
3014
|
+
onItemsChange?.(normalizedItems);
|
|
3015
|
+
session?.remoteAdapter.send?.(normalizedItems);
|
|
3016
|
+
},
|
|
3017
|
+
[enabled, normalizeItems, onItemsChange, session]
|
|
3018
|
+
);
|
|
3019
|
+
useEffect(() => {
|
|
3020
|
+
if (!realtimeEnabled || !onItemsChange || !session?.document) return;
|
|
3021
|
+
if (session.document.updatedByClientId === session.connection.clientId) return;
|
|
3022
|
+
if (lastAppliedRevisionRef.current === session.document.revision) return;
|
|
3023
|
+
let cancelled = false;
|
|
3024
|
+
setLoading(true);
|
|
3025
|
+
void applyIncomingItems(session.document.items).then((resolvedItems) => {
|
|
3026
|
+
if (cancelled) return;
|
|
3027
|
+
lastAppliedRevisionRef.current = session.document?.revision ?? null;
|
|
3028
|
+
onItemsChange(resolvedItems);
|
|
3029
|
+
}).finally(() => {
|
|
3030
|
+
if (cancelled) return;
|
|
3031
|
+
setLoading(false);
|
|
3032
|
+
});
|
|
3033
|
+
return () => {
|
|
3034
|
+
cancelled = true;
|
|
3035
|
+
};
|
|
3036
|
+
}, [applyIncomingItems, realtimeEnabled, onItemsChange, session]);
|
|
3037
|
+
return useMemo(
|
|
3038
|
+
() => ({
|
|
3039
|
+
items,
|
|
3040
|
+
onItemsChange: onItemsChange ? handleItemsChange : void 0,
|
|
3041
|
+
loading,
|
|
3042
|
+
saving: session?.hasPendingDocumentSync ?? false,
|
|
3043
|
+
hasLocalOfflineDraft: session?.hasLocalOfflineDraft ?? false,
|
|
3044
|
+
syncState: session?.syncState ?? "offline",
|
|
3045
|
+
conflict: session?.conflict ?? null,
|
|
3046
|
+
resolveConflict: session?.resolveConflict ?? (() => {
|
|
3047
|
+
}),
|
|
3048
|
+
clearLocalDraft: session?.clearLocalDraft ?? (() => {
|
|
3049
|
+
}),
|
|
3050
|
+
flush: session?.flushDocumentSync ?? (async () => {
|
|
3051
|
+
})
|
|
3052
|
+
}),
|
|
3053
|
+
[handleItemsChange, items, loading, onItemsChange, session]
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
2546
3056
|
|
|
2547
|
-
export { PresenceRemoteLayer, REALTIME_COMMENT_TOOL, RealtimeCommentsOverlay, RealtimeSessionPanel, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimePeerFollow, useRealtimeSession, withRealtimeCommentTool };
|
|
3057
|
+
export { PresenceRemoteLayer, REALTIME_COMMENT_TOOL, RealtimeCommentsOverlay, RealtimeSessionPanel, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeCanvasDocument, useRealtimeComments, useRealtimePeerFollow, useRealtimeSession, withRealtimeCommentTool };
|
|
2548
3058
|
//# sourceMappingURL=realtime.js.map
|
|
2549
3059
|
//# sourceMappingURL=realtime.js.map
|