canvu-react 0.3.9 → 0.3.11
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 +358 -141
- 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 +358 -142
- package/dist/react.js.map +1 -1
- package/dist/realtime.cjs +541 -38
- 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 +541 -39
- 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
|
@@ -102,7 +102,7 @@ function PresenceRemoteLayer({
|
|
|
102
102
|
const rootTransform = formatCameraTransform(camera);
|
|
103
103
|
const overlayStrokePx = 1.25;
|
|
104
104
|
const LUCIDE_POINTER_VIEWBOX = 24;
|
|
105
|
-
const remoteCursorScreenPx =
|
|
105
|
+
const remoteCursorScreenPx = 22;
|
|
106
106
|
const iconWorldScale = remoteCursorScreenPx / (LUCIDE_POINTER_VIEWBOX * z);
|
|
107
107
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
108
108
|
"svg",
|
|
@@ -166,9 +166,9 @@ function PresenceRemoteLayer({
|
|
|
166
166
|
let cursorNode = null;
|
|
167
167
|
if (cur) {
|
|
168
168
|
const displayName = peer.displayName;
|
|
169
|
-
const labelOffsetX =
|
|
170
|
-
const labelOffsetY =
|
|
171
|
-
const labelFont =
|
|
169
|
+
const labelOffsetX = 14 / z;
|
|
170
|
+
const labelOffsetY = 14 / z;
|
|
171
|
+
const labelFont = 12 / z;
|
|
172
172
|
cursorNode = /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
173
173
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
174
174
|
"g",
|
|
@@ -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,108 @@ 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
|
-
|
|
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
|
+
};
|
|
1990
2203
|
const didSend = sendRaw({
|
|
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
|
-
}, [roomId, sendRaw]);
|
|
2215
|
+
}, [clearDocumentFlushSchedule, roomId, sendRaw]);
|
|
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
|
+
scheduleDocumentFlush();
|
|
2239
|
+
},
|
|
2240
|
+
[roomId, scheduleDocumentFlush, scheduleDraftPersistence, setLocalDraft]
|
|
2241
|
+
);
|
|
2242
|
+
const applyDraftSnapshot = react.useCallback(
|
|
2243
|
+
(draft, options2) => {
|
|
2244
|
+
applyDocument(buildDraftSnapshot(draft, clientIdRef.current), options2);
|
|
2006
2245
|
},
|
|
2007
|
-
[
|
|
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
|
+
clearLocalDraft();
|
|
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;
|
|
2284
|
+
},
|
|
2285
|
+
[
|
|
2286
|
+
applyDocument,
|
|
2287
|
+
applyDraftSnapshot,
|
|
2288
|
+
clearLocalDraft,
|
|
2289
|
+
scheduleDocumentFlush,
|
|
2290
|
+
setConflictState
|
|
2291
|
+
]
|
|
2008
2292
|
);
|
|
2009
2293
|
const sendPresenceUpdate = react.useCallback(() => {
|
|
2010
2294
|
sendRaw({
|
|
@@ -2043,17 +2327,108 @@ function useRealtimeSession(options) {
|
|
|
2043
2327
|
reconnect,
|
|
2044
2328
|
updateConnection
|
|
2045
2329
|
]);
|
|
2330
|
+
const resolveConflict = react.useCallback(
|
|
2331
|
+
(action) => {
|
|
2332
|
+
const activeConflict = conflictRef.current;
|
|
2333
|
+
if (!activeConflict) return;
|
|
2334
|
+
if (action === "use-server") {
|
|
2335
|
+
clearLocalDraft();
|
|
2336
|
+
queuedItemsRef.current = null;
|
|
2337
|
+
outboundInFlightRef.current = null;
|
|
2338
|
+
setConflictState(null);
|
|
2339
|
+
applyDocument({
|
|
2340
|
+
revision: activeConflict.serverRevision,
|
|
2341
|
+
items: activeConflict.serverItems,
|
|
2342
|
+
updatedAt: nowMs()
|
|
2343
|
+
});
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const nextDraft = {
|
|
2347
|
+
roomId,
|
|
2348
|
+
baseRevision: activeConflict.serverRevision,
|
|
2349
|
+
items: activeConflict.localItems,
|
|
2350
|
+
updatedAt: nowMs()
|
|
2351
|
+
};
|
|
2352
|
+
setLocalDraft(nextDraft);
|
|
2353
|
+
scheduleDraftPersistence();
|
|
2354
|
+
setConflictState(null);
|
|
2355
|
+
applyDraftSnapshot(nextDraft, {
|
|
2356
|
+
suppressSubscriberNotify: sameSerializedItems(
|
|
2357
|
+
latestDocumentRef.current?.items,
|
|
2358
|
+
nextDraft.items
|
|
2359
|
+
)
|
|
2360
|
+
});
|
|
2361
|
+
currentRevisionRef.current = activeConflict.serverRevision;
|
|
2362
|
+
queuedItemsRef.current = nextDraft.items;
|
|
2363
|
+
outboundInFlightRef.current = null;
|
|
2364
|
+
setHasPendingDocumentSync(true);
|
|
2365
|
+
scheduleDocumentFlush();
|
|
2366
|
+
},
|
|
2367
|
+
[
|
|
2368
|
+
applyDocument,
|
|
2369
|
+
applyDraftSnapshot,
|
|
2370
|
+
clearLocalDraft,
|
|
2371
|
+
roomId,
|
|
2372
|
+
scheduleDocumentFlush,
|
|
2373
|
+
scheduleDraftPersistence,
|
|
2374
|
+
setConflictState,
|
|
2375
|
+
setLocalDraft
|
|
2376
|
+
]
|
|
2377
|
+
);
|
|
2378
|
+
react.useEffect(() => {
|
|
2379
|
+
if (!roomId) {
|
|
2380
|
+
clearDocumentFlushSchedule();
|
|
2381
|
+
clearDraftPersistSchedule();
|
|
2382
|
+
localDraftRef.current = null;
|
|
2383
|
+
setHasLocalOfflineDraft(false);
|
|
2384
|
+
setHasPendingDocumentSync(false);
|
|
2385
|
+
setConflictState(null);
|
|
2386
|
+
queuedItemsRef.current = null;
|
|
2387
|
+
outboundInFlightRef.current = null;
|
|
2388
|
+
latestDocumentRef.current = null;
|
|
2389
|
+
setDocument(null);
|
|
2390
|
+
currentRevisionRef.current = 0;
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const localDraft = readRealtimeOfflineDraft(roomId);
|
|
2394
|
+
setLocalDraft(localDraft);
|
|
2395
|
+
setHasPendingDocumentSync(localDraft != null);
|
|
2396
|
+
setConflictState(null);
|
|
2397
|
+
queuedItemsRef.current = localDraft?.items ?? null;
|
|
2398
|
+
outboundInFlightRef.current = null;
|
|
2399
|
+
if (localDraft) {
|
|
2400
|
+
applyDraftSnapshot(localDraft, {
|
|
2401
|
+
suppressSubscriberNotify: sameSerializedItems(
|
|
2402
|
+
latestDocumentRef.current?.items,
|
|
2403
|
+
localDraft.items
|
|
2404
|
+
)
|
|
2405
|
+
});
|
|
2406
|
+
} else {
|
|
2407
|
+
latestDocumentRef.current = null;
|
|
2408
|
+
setDocument(null);
|
|
2409
|
+
currentRevisionRef.current = 0;
|
|
2410
|
+
}
|
|
2411
|
+
}, [
|
|
2412
|
+
applyDraftSnapshot,
|
|
2413
|
+
clearDocumentFlushSchedule,
|
|
2414
|
+
clearDraftPersistSchedule,
|
|
2415
|
+
roomId,
|
|
2416
|
+
setConflictState,
|
|
2417
|
+
setLocalDraft
|
|
2418
|
+
]);
|
|
2046
2419
|
react.useEffect(() => {
|
|
2047
2420
|
if (!enabled) {
|
|
2048
2421
|
manualDisconnectRef.current = true;
|
|
2049
2422
|
clearReconnectTimer();
|
|
2050
2423
|
clearHeartbeatTimer();
|
|
2051
2424
|
clearConnectTimeout();
|
|
2425
|
+
clearDocumentFlushSchedule();
|
|
2052
2426
|
wsRef.current?.close();
|
|
2053
2427
|
wsRef.current = null;
|
|
2054
|
-
|
|
2055
|
-
queuedItemsRef.current = null;
|
|
2428
|
+
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2056
2429
|
outboundInFlightRef.current = null;
|
|
2430
|
+
setHasPendingDocumentSync(localDraftRef.current != null);
|
|
2431
|
+
collapsePeersToSelf("offline");
|
|
2057
2432
|
updateConnection((prev) => ({
|
|
2058
2433
|
...prev,
|
|
2059
2434
|
state: "offline",
|
|
@@ -2065,6 +2440,17 @@ function useRealtimeSession(options) {
|
|
|
2065
2440
|
}
|
|
2066
2441
|
manualDisconnectRef.current = false;
|
|
2067
2442
|
const socketUrl = normalizeSocketUrl(url);
|
|
2443
|
+
if (!socketUrl) {
|
|
2444
|
+
collapsePeersToSelf("offline");
|
|
2445
|
+
updateConnection((prev) => ({
|
|
2446
|
+
...prev,
|
|
2447
|
+
state: "offline",
|
|
2448
|
+
connected: false,
|
|
2449
|
+
roomId,
|
|
2450
|
+
lastError: null
|
|
2451
|
+
}));
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2068
2454
|
if (!isValidSocketUrl(socketUrl)) {
|
|
2069
2455
|
updateConnection((prev) => ({
|
|
2070
2456
|
...prev,
|
|
@@ -2134,8 +2520,6 @@ function useRealtimeSession(options) {
|
|
|
2134
2520
|
}));
|
|
2135
2521
|
if (parsed.type === "session:welcome") {
|
|
2136
2522
|
retryCountRef.current = 0;
|
|
2137
|
-
const queuedBeforeWelcome = queuedItemsRef.current;
|
|
2138
|
-
const shouldPromoteQueuedLocalDraft = queuedBeforeWelcome != null && parsed.document.revision === 0 && parsed.document.items.length === 0;
|
|
2139
2523
|
updateConnection((prev) => ({
|
|
2140
2524
|
...prev,
|
|
2141
2525
|
state: "connected",
|
|
@@ -2146,14 +2530,21 @@ function useRealtimeSession(options) {
|
|
|
2146
2530
|
lastError: null
|
|
2147
2531
|
}));
|
|
2148
2532
|
applyPeers(parsed.peers);
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2533
|
+
const handledByDraft = resolveAuthoritativeDocument(
|
|
2534
|
+
sanitizeRealtimeSnapshot(parsed.document),
|
|
2535
|
+
{
|
|
2536
|
+
suppressSubscriberNotify: localDraftRef.current != null && sameSerializedItems(
|
|
2537
|
+
latestDocumentRef.current?.items,
|
|
2538
|
+
localDraftRef.current.items
|
|
2539
|
+
)
|
|
2540
|
+
}
|
|
2541
|
+
);
|
|
2542
|
+
if (!handledByDraft) {
|
|
2153
2543
|
queuedItemsRef.current = null;
|
|
2154
2544
|
outboundInFlightRef.current = null;
|
|
2545
|
+
setHasPendingDocumentSync(false);
|
|
2155
2546
|
}
|
|
2156
|
-
|
|
2547
|
+
scheduleDocumentFlush();
|
|
2157
2548
|
return;
|
|
2158
2549
|
}
|
|
2159
2550
|
if (parsed.type === "presence:sync") {
|
|
@@ -2200,30 +2591,39 @@ function useRealtimeSession(options) {
|
|
|
2200
2591
|
const isSelfAck = parsed.document.updatedByClientId === selfClientId;
|
|
2201
2592
|
if (!isSelfAck) {
|
|
2202
2593
|
outboundInFlightRef.current = null;
|
|
2203
|
-
queuedItemsRef.current = null;
|
|
2204
|
-
|
|
2594
|
+
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2595
|
+
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2596
|
+
resolveAuthoritativeDocument(sanitizeRealtimeSnapshot(parsed.document));
|
|
2205
2597
|
return;
|
|
2206
2598
|
}
|
|
2207
2599
|
const shouldSuppress = inFlight != null && sameSerializedItems(inFlight.items, parsed.document.items);
|
|
2208
2600
|
outboundInFlightRef.current = null;
|
|
2209
|
-
applyDocument(parsed.document, {
|
|
2601
|
+
applyDocument(sanitizeRealtimeSnapshot(parsed.document), {
|
|
2602
|
+
suppressSubscriberNotify: shouldSuppress
|
|
2603
|
+
});
|
|
2210
2604
|
if (queuedItemsRef.current) {
|
|
2211
2605
|
if (sameSerializedItems(queuedItemsRef.current, parsed.document.items)) {
|
|
2212
2606
|
queuedItemsRef.current = null;
|
|
2213
2607
|
} else {
|
|
2214
|
-
|
|
2608
|
+
scheduleDocumentFlush();
|
|
2215
2609
|
}
|
|
2216
2610
|
}
|
|
2611
|
+
if (!queuedItemsRef.current) {
|
|
2612
|
+
clearLocalDraft();
|
|
2613
|
+
}
|
|
2614
|
+
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2615
|
+
setConflictState(null);
|
|
2217
2616
|
return;
|
|
2218
2617
|
}
|
|
2219
2618
|
if (parsed.type === "document:resync-required") {
|
|
2220
2619
|
outboundInFlightRef.current = null;
|
|
2221
|
-
queuedItemsRef.current = null;
|
|
2620
|
+
queuedItemsRef.current = localDraftRef.current?.items ?? null;
|
|
2621
|
+
setHasPendingDocumentSync(queuedItemsRef.current != null);
|
|
2222
2622
|
updateConnection((prev) => ({
|
|
2223
2623
|
...prev,
|
|
2224
2624
|
lastError: parsed.reason
|
|
2225
2625
|
}));
|
|
2226
|
-
|
|
2626
|
+
resolveAuthoritativeDocument(sanitizeRealtimeSnapshot(parsed.document));
|
|
2227
2627
|
}
|
|
2228
2628
|
});
|
|
2229
2629
|
socket.addEventListener("error", () => {
|
|
@@ -2239,6 +2639,9 @@ function useRealtimeSession(options) {
|
|
|
2239
2639
|
clearHeartbeatTimer();
|
|
2240
2640
|
clearConnectTimeout();
|
|
2241
2641
|
wsRef.current = null;
|
|
2642
|
+
collapsePeersToSelf(
|
|
2643
|
+
manualDisconnectRef.current || !enabled ? "offline" : "reconnecting"
|
|
2644
|
+
);
|
|
2242
2645
|
updateConnection((prev) => ({
|
|
2243
2646
|
...prev,
|
|
2244
2647
|
connected: false,
|
|
@@ -2254,6 +2657,7 @@ function useRealtimeSession(options) {
|
|
|
2254
2657
|
clearReconnectTimer();
|
|
2255
2658
|
clearHeartbeatTimer();
|
|
2256
2659
|
clearConnectTimeout();
|
|
2660
|
+
clearDocumentFlushSchedule();
|
|
2257
2661
|
socket.close();
|
|
2258
2662
|
};
|
|
2259
2663
|
}, [
|
|
@@ -2261,19 +2665,24 @@ function useRealtimeSession(options) {
|
|
|
2261
2665
|
applyPeers,
|
|
2262
2666
|
clearConnectTimeout,
|
|
2263
2667
|
clearHeartbeatTimer,
|
|
2668
|
+
clearLocalDraft,
|
|
2264
2669
|
clearReconnectTimer,
|
|
2670
|
+
collapsePeersToSelf,
|
|
2671
|
+
clearDocumentFlushSchedule,
|
|
2265
2672
|
connectSequence,
|
|
2266
2673
|
connectTimeoutMs,
|
|
2267
2674
|
enabled,
|
|
2268
|
-
flushQueuedDocument,
|
|
2269
2675
|
heartbeatMs,
|
|
2270
2676
|
peer.color,
|
|
2271
2677
|
peer.displayName,
|
|
2272
|
-
peer.image,
|
|
2273
2678
|
peer.id,
|
|
2679
|
+
peer.image,
|
|
2680
|
+
resolveAuthoritativeDocument,
|
|
2274
2681
|
roomId,
|
|
2682
|
+
scheduleDocumentFlush,
|
|
2275
2683
|
scheduleReconnect,
|
|
2276
2684
|
sendRaw,
|
|
2685
|
+
setConflictState,
|
|
2277
2686
|
updateConnection,
|
|
2278
2687
|
url
|
|
2279
2688
|
]);
|
|
@@ -2288,6 +2697,18 @@ function useRealtimeSession(options) {
|
|
|
2288
2697
|
)
|
|
2289
2698
|
);
|
|
2290
2699
|
}, [connection.state]);
|
|
2700
|
+
react.useEffect(
|
|
2701
|
+
() => () => {
|
|
2702
|
+
clearDocumentFlushSchedule();
|
|
2703
|
+
clearDraftPersistSchedule();
|
|
2704
|
+
},
|
|
2705
|
+
[clearDocumentFlushSchedule, clearDraftPersistSchedule]
|
|
2706
|
+
);
|
|
2707
|
+
const flushDocumentSync = react.useCallback(async () => {
|
|
2708
|
+
persistLocalDraft();
|
|
2709
|
+
if (!connection.connected) return;
|
|
2710
|
+
flushQueuedDocument();
|
|
2711
|
+
}, [connection.connected, flushQueuedDocument, persistLocalDraft]);
|
|
2291
2712
|
const remoteAdapter = react.useMemo(
|
|
2292
2713
|
() => ({
|
|
2293
2714
|
subscribe(onItems) {
|
|
@@ -2301,9 +2722,12 @@ function useRealtimeSession(options) {
|
|
|
2301
2722
|
},
|
|
2302
2723
|
send(items) {
|
|
2303
2724
|
queueDocumentSend(items);
|
|
2725
|
+
},
|
|
2726
|
+
flush() {
|
|
2727
|
+
return flushDocumentSync();
|
|
2304
2728
|
}
|
|
2305
2729
|
}),
|
|
2306
|
-
[queueDocumentSend]
|
|
2730
|
+
[flushDocumentSync, queueDocumentSend]
|
|
2307
2731
|
);
|
|
2308
2732
|
const disconnect = react.useCallback(() => {
|
|
2309
2733
|
manualDisconnectRef.current = true;
|
|
@@ -2317,6 +2741,7 @@ function useRealtimeSession(options) {
|
|
|
2317
2741
|
});
|
|
2318
2742
|
wsRef.current?.close();
|
|
2319
2743
|
wsRef.current = null;
|
|
2744
|
+
collapsePeersToSelf("offline");
|
|
2320
2745
|
updateConnection((prev) => ({
|
|
2321
2746
|
...prev,
|
|
2322
2747
|
state: "offline",
|
|
@@ -2326,6 +2751,7 @@ function useRealtimeSession(options) {
|
|
|
2326
2751
|
clearConnectTimeout,
|
|
2327
2752
|
clearHeartbeatTimer,
|
|
2328
2753
|
clearReconnectTimer,
|
|
2754
|
+
collapsePeersToSelf,
|
|
2329
2755
|
roomId,
|
|
2330
2756
|
sendRaw,
|
|
2331
2757
|
updateConnection
|
|
@@ -2391,8 +2817,15 @@ function useRealtimeSession(options) {
|
|
|
2391
2817
|
remotePresence,
|
|
2392
2818
|
remoteAdapter,
|
|
2393
2819
|
document: document2,
|
|
2820
|
+
hasLocalOfflineDraft,
|
|
2821
|
+
hasPendingDocumentSync,
|
|
2822
|
+
syncState,
|
|
2823
|
+
conflict,
|
|
2394
2824
|
bindViewportPresence,
|
|
2395
2825
|
syncViewportPresence,
|
|
2826
|
+
resolveConflict,
|
|
2827
|
+
clearLocalDraft,
|
|
2828
|
+
flushDocumentSync,
|
|
2396
2829
|
disconnect,
|
|
2397
2830
|
reconnectNow
|
|
2398
2831
|
};
|
|
@@ -2545,6 +2978,75 @@ function realtimeSessionPlugin(options) {
|
|
|
2545
2978
|
render: () => /* @__PURE__ */ jsxRuntime.jsx(RealtimeSessionPanel, { ...options })
|
|
2546
2979
|
};
|
|
2547
2980
|
}
|
|
2981
|
+
function useRealtimeCanvasDocument(options) {
|
|
2982
|
+
const {
|
|
2983
|
+
session,
|
|
2984
|
+
items,
|
|
2985
|
+
onItemsChange,
|
|
2986
|
+
normalizeItems,
|
|
2987
|
+
hydrateItems,
|
|
2988
|
+
enabled = true
|
|
2989
|
+
} = options;
|
|
2990
|
+
const [loading, setLoading] = react.useState(false);
|
|
2991
|
+
const lastAppliedRevisionRef = react.useRef(null);
|
|
2992
|
+
const realtimeEnabled = enabled && session != null;
|
|
2993
|
+
const applyIncomingItems = react.useCallback(
|
|
2994
|
+
async (nextItems) => {
|
|
2995
|
+
const normalizedItems = normalizeItems ? normalizeItems(nextItems) : [...nextItems];
|
|
2996
|
+
if (!hydrateItems) return normalizedItems;
|
|
2997
|
+
return await hydrateItems(normalizedItems);
|
|
2998
|
+
},
|
|
2999
|
+
[hydrateItems, normalizeItems]
|
|
3000
|
+
);
|
|
3001
|
+
const handleItemsChange = react.useCallback(
|
|
3002
|
+
(nextItems) => {
|
|
3003
|
+
if (!enabled) {
|
|
3004
|
+
onItemsChange?.(normalizeItems ? normalizeItems(nextItems) : nextItems);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
const normalizedItems = normalizeItems ? normalizeItems(nextItems) : nextItems;
|
|
3008
|
+
onItemsChange?.(normalizedItems);
|
|
3009
|
+
session?.remoteAdapter.send?.(normalizedItems);
|
|
3010
|
+
},
|
|
3011
|
+
[enabled, normalizeItems, onItemsChange, session]
|
|
3012
|
+
);
|
|
3013
|
+
react.useEffect(() => {
|
|
3014
|
+
if (!realtimeEnabled || !onItemsChange || !session?.document) return;
|
|
3015
|
+
if (session.document.updatedByClientId === session.connection.clientId) return;
|
|
3016
|
+
if (lastAppliedRevisionRef.current === session.document.revision) return;
|
|
3017
|
+
let cancelled = false;
|
|
3018
|
+
setLoading(true);
|
|
3019
|
+
void applyIncomingItems(session.document.items).then((resolvedItems) => {
|
|
3020
|
+
if (cancelled) return;
|
|
3021
|
+
lastAppliedRevisionRef.current = session.document?.revision ?? null;
|
|
3022
|
+
onItemsChange(resolvedItems);
|
|
3023
|
+
}).finally(() => {
|
|
3024
|
+
if (cancelled) return;
|
|
3025
|
+
setLoading(false);
|
|
3026
|
+
});
|
|
3027
|
+
return () => {
|
|
3028
|
+
cancelled = true;
|
|
3029
|
+
};
|
|
3030
|
+
}, [applyIncomingItems, realtimeEnabled, onItemsChange, session]);
|
|
3031
|
+
return react.useMemo(
|
|
3032
|
+
() => ({
|
|
3033
|
+
items,
|
|
3034
|
+
onItemsChange: onItemsChange ? handleItemsChange : void 0,
|
|
3035
|
+
loading,
|
|
3036
|
+
saving: session?.hasPendingDocumentSync ?? false,
|
|
3037
|
+
hasLocalOfflineDraft: session?.hasLocalOfflineDraft ?? false,
|
|
3038
|
+
syncState: session?.syncState ?? "offline",
|
|
3039
|
+
conflict: session?.conflict ?? null,
|
|
3040
|
+
resolveConflict: session?.resolveConflict ?? (() => {
|
|
3041
|
+
}),
|
|
3042
|
+
clearLocalDraft: session?.clearLocalDraft ?? (() => {
|
|
3043
|
+
}),
|
|
3044
|
+
flush: session?.flushDocumentSync ?? (async () => {
|
|
3045
|
+
})
|
|
3046
|
+
}),
|
|
3047
|
+
[handleItemsChange, items, loading, onItemsChange, session]
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
2548
3050
|
|
|
2549
3051
|
exports.PresenceRemoteLayer = PresenceRemoteLayer;
|
|
2550
3052
|
exports.REALTIME_COMMENT_TOOL = REALTIME_COMMENT_TOOL;
|
|
@@ -2563,6 +3065,7 @@ exports.realtimeCollaborationPlugin = realtimeCollaborationPlugin;
|
|
|
2563
3065
|
exports.realtimeCommentsPlugin = realtimeCommentsPlugin;
|
|
2564
3066
|
exports.realtimeSessionPlugin = realtimeSessionPlugin;
|
|
2565
3067
|
exports.remoteMarkupStrokeFromPlacementPreview = remoteMarkupStrokeFromPlacementPreview;
|
|
3068
|
+
exports.useRealtimeCanvasDocument = useRealtimeCanvasDocument;
|
|
2566
3069
|
exports.useRealtimeComments = useRealtimeComments;
|
|
2567
3070
|
exports.useRealtimePeerFollow = useRealtimePeerFollow;
|
|
2568
3071
|
exports.useRealtimeSession = useRealtimeSession;
|