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