import React, { useMemo, Suspense, useCallback, useEffect, useRef } from 'react'; import { RecoilSync } from 'recoil-sync'; import WebSocket from 'reconnecting-websocket'; import Client, { Connection, Doc } from 'sharedb/lib/client'; import Deferred from 'deferred'; import * as json1 from 'ot-json1'; import { parseItemKey } from './key'; import { readDoc, writeOrCreate } from './sharedb-utils'; import config from './config'; Client.types.register(json1.type); export const MapPropsProvider = React.createContext({}); interface IRecoilSyncShareDBProps { wsUrl: string; children: JSX.Element; onError: (e: any) => void; }; export const Context = React.createContext({}); const RecoilSyncShareDB: React.FC = ({ children, wsUrl, onError, ...mapProps }) => { const connectionRef = useRef(null); const updateItemRef = useRef(null); const updateAllKnownItemsRef = useRef(null); const onErrorRef = useRef(null); const atomKeyMap = useMemo(() => { return new Map(); }, []); const registerMap = useMemo(() => { return new Map(); }, []); onErrorRef.current = onError; const listenNewDoc = useCallback((doc: Doc) => { if (registerMap.get(doc)) { return; } registerMap.set(doc, true); doc.on('op', (op) => { // 这里表示 sharedb 数据修改完成,此时需要将数据从sharedb同步到recoil中。 const k = atomKeyMap.get(`${doc.collection}.${doc.id}`); if (updateItemRef.current && k) { (updateItemRef.current as any)(k, doc.data); } }); doc.on('error', (e) => { doc.removeAllListeners('op'); doc.removeAllListeners('error'); registerMap.delete(doc); if (onErrorRef.current) { onErrorRef.current(e); } else { console.error(e); } }); }, []); const defCon = useMemo(() => { return new Deferred(); }, []); useEffect(() => { const socket = new WebSocket(wsUrl); socket.addEventListener('open', () => { if (connectionRef.current) { // 重新订阅 Object.keys((connectionRef.current as any).collections).forEach(key => { Object.keys((connectionRef.current as any).collections[key]).forEach(docId => { (connectionRef.current as any).collections[key][docId].subscribe(); }); }); } else { connectionRef.current = new Connection(socket); defCon.resolve(connectionRef.current); connectionRef.current.on('error', (error) => { console.error('error', error); }); connectionRef.current.on('connection error', (error) => { console.error('connection error', error); }); } }); return () => { socket.close(); if (connectionRef.current) { connectionRef.current.close(); connectionRef.current = null; } } }, []); const read = useCallback((itemKey: string) => { const { collection, key } = parseItemKey(itemKey, mapProps); atomKeyMap.set(`${collection}.${key}`, itemKey); const readWork = (con: Connection) => { const doc = con.get(collection, key); return readDoc(doc, listenNewDoc); } if (connectionRef.current) { return readWork(connectionRef.current); } return Promise.resolve(defCon.promise).then(readWork); }, [mapProps]); const write = useCallback(({ diff }: any) => { function doWork(con: Connection) { for (const [itemKey, value] of diff) { const { collection, key } = parseItemKey(itemKey, mapProps); const doc = con.get(collection, key); writeOrCreate(doc, value); } } if (connectionRef.current) { return doWork(connectionRef.current); } return defCon.promise.then((con: Connection) => { doWork(con); }); }, [mapProps]); const listen = useCallback(({ updateItem, updateAllKnownItems}: any) => { updateItemRef.current = updateItem; updateAllKnownItemsRef.current = updateAllKnownItems; }, []); return ( { children } ); }; export { RecoilSyncShareDB };