Skip to content

Commit e91bd9f

Browse files
committed
fix: durable object bug and simplify client-side code
- Ensured WebSocket ID remains accessible by closing connections after broadcasting. - Cleaned up `cursor.tsx` by moving the main functions to the top. - Simplified component logic for easier understanding and maintenance.
1 parent 7f975ef commit e91bd9f

File tree

1 file changed

+75
-75
lines changed
  • src/content/docs/workers/tutorials/live-cursors-with-nextjs-rpc-do

1 file changed

+75
-75
lines changed

src/content/docs/workers/tutorials/live-cursors-with-nextjs-rpc-do/index.mdx

Lines changed: 75 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
186186

187187

188188
3. Update the Durable Object to manage WebSockets:
189-
```ts title="worker/src/index.ts" {28-33,35-42,57,78,89-99}
189+
```ts title="worker/src/index.ts" {29-34,36-43,56,79,89-100}
190190
// Rest of the code
191191

192192
export class CursorSessions extends DurableObject<Env> {
@@ -242,9 +242,9 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
242242

243243
async webSocketClose(ws: WebSocket, code: number) {
244244
const id = this.sessions.get(ws)?.id;
245-
ws.close();
245+
id && this.broadcast({ type: 'quit', id });
246246
this.sessions.delete(ws);
247-
id && this.broadcast({ type: "quit", id });
247+
ws.close();
248248
}
249249

250250
closeSessions() {
@@ -432,71 +432,18 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
432432
import type { Session, WsMessage } from "../../worker/src/index";
433433
import { PerfectCursor } from "perfect-cursors";
434434

435-
export function usePerfectCursor(
436-
cb: (point: number[]) => void,
437-
point?: number[],
438-
) {
439-
const [pc] = useState(() => new PerfectCursor(cb));
440-
441-
useLayoutEffect(() => {
442-
if (point) pc.addPoint(point);
443-
return () => pc.dispose();
444-
// eslint-disable-next-line react-hooks/exhaustive-deps
445-
}, [pc]);
446-
447-
const onPointChange = useCallback(
448-
(point: number[]) => pc.addPoint(point),
449-
[pc],
450-
);
451-
452-
return onPointChange;
453-
}
454-
455-
type MessageState = { in: string; out: string };
456-
type MessageAction = { type: "in" | "out"; message: string };
457-
function messageReducer(state: MessageState, action: MessageAction) {
458-
switch (action.type) {
459-
case "in":
460-
return { ...state, in: action.message };
461-
case "out":
462-
return { ...state, out: action.message };
463-
default:
464-
return state;
465-
}
466-
}
467-
468-
function useHighlight(duration = 250) {
469-
const timestampRef = useRef(0);
470-
const [highlighted, setHighlighted] = useState(false);
471-
function highlight() {
472-
timestampRef.current = Date.now();
473-
setHighlighted(true);
474-
setTimeout(() => {
475-
const now = Date.now();
476-
if (now - timestampRef.current >= duration) {
477-
setHighlighted(false);
478-
}
479-
}, duration);
480-
}
481-
return [highlighted, highlight] as const;
482-
}
435+
const INTERVAL = 55;
483436

484437
export function Cursors(props: { id: string }) {
485-
const [mounted, setMounted] = useState(false);
486438
const wsRef = useRef<WebSocket | null>(null);
439+
const [cursors, setCursors] = useState<Map<string, Session>>(new Map());
440+
const lastSentTimestamp = useRef(0);
487441
const [messageState, dispatchMessage] = useReducer(messageReducer, {
488442
in: "",
489443
out: "",
490444
});
491-
const [cursors, setCursors] = useState<Map<string, Session>>(new Map());
492445
const [highlightedIn, highlightIn] = useHighlight();
493446
const [highlightedOut, highlightOut] = useHighlight();
494-
const lastSentTimestamp = useRef(0);
495-
const sendInterval = 40;
496-
497-
useEffect(() => {
498-
setMounted(true);
499-
}, []);
500447

501448
function startWebSocket() {
502449
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
@@ -567,7 +514,7 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
567514
y = ev.pageY / window.innerHeight;
568515
const now = Date.now();
569516
if (
570-
now - lastSentTimestamp.current > sendInterval &&
517+
now - lastSentTimestamp.current > INTERVAL &&
571518
wsRef.current?.readyState === WebSocket.OPEN
572519
) {
573520
const message: WsMessage = { type: "move", id: props.id, x, y };
@@ -599,6 +546,10 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
599546
);
600547
}
601548

549+
const otherCursors = Array.from(cursors.values()).filter(
550+
({ id, x, y }) => id !== props.id && x !== -1 && y !== -1,
551+
);
552+
602553
return (
603554
<>
604555
<div className="flex border">
@@ -651,22 +602,22 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
651602
</button>
652603
</div>
653604
<div>
654-
{mounted &&
655-
Array.from(cursors.values()).map(
656-
(session) =>
657-
props.id !== session.id && (
658-
<SvgCursor key={session.id} x={session.x} y={session.y} />
659-
),
660-
)}
605+
{otherCursors.map((session) => (
606+
<SvgCursor
607+
key={session.id}
608+
point={[
609+
session.x * window.innerWidth,
610+
session.y * window.innerHeight,
611+
]}
612+
/>
613+
))}
661614
</div>
662615
</>
663616
);
664617
}
665618

666-
function SvgCursor(props: { x: number; y: number }) {
619+
function SvgCursor({ point }: { point: number[] }) {
667620
const refSvg = useRef<SVGSVGElement>(null);
668-
// eslint-disable-next-line react-hooks/exhaustive-deps
669-
const point = [window.innerWidth * props.x, window.innerHeight * props.y];
670621
const animateCursor = useCallback((point: number[]) => {
671622
refSvg.current?.style.setProperty(
672623
"transform",
@@ -687,7 +638,7 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
687638
width="32"
688639
viewBox="0 0 32 32"
689640
xmlns="http://www.w3.org/2000/svg"
690-
className={`absolute -top-[12px] -left-[12px] pointer-events-none ${props.x === -1 || props.y === -1 ? "hidden" : ""}`}
641+
className={"absolute -top-[12px] -left-[12px] pointer-events-none"}
691642
>
692643
<defs>
693644
<filter id="shadow" x="-40%" y="-40%" width="180%" height="180%">
@@ -715,6 +666,56 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
715666
</svg>
716667
);
717668
}
669+
670+
function usePerfectCursor(cb: (point: number[]) => void, point?: number[]) {
671+
const [pc] = useState(() => new PerfectCursor(cb));
672+
673+
useLayoutEffect(() => {
674+
if (point) pc.addPoint(point);
675+
return () => pc.dispose();
676+
// eslint-disable-next-line react-hooks/exhaustive-deps
677+
}, [pc]);
678+
679+
useLayoutEffect(() => {
680+
PerfectCursor.MAX_INTERVAL = 58;
681+
}, []);
682+
683+
const onPointChange = useCallback(
684+
(point: number[]) => pc.addPoint(point),
685+
[pc],
686+
);
687+
688+
return onPointChange;
689+
}
690+
691+
type MessageState = { in: string; out: string };
692+
type MessageAction = { type: "in" | "out"; message: string };
693+
function messageReducer(state: MessageState, action: MessageAction) {
694+
switch (action.type) {
695+
case "in":
696+
return { ...state, in: action.message };
697+
case "out":
698+
return { ...state, out: action.message };
699+
default:
700+
return state;
701+
}
702+
}
703+
704+
function useHighlight(duration = 250) {
705+
const timestampRef = useRef(0);
706+
const [highlighted, setHighlighted] = useState(false);
707+
function highlight() {
708+
timestampRef.current = Date.now();
709+
setHighlighted(true);
710+
setTimeout(() => {
711+
const now = Date.now();
712+
if (now - timestampRef.current >= duration) {
713+
setHighlighted(false);
714+
}
715+
}, duration);
716+
}
717+
return [highlighted, highlight] as const;
718+
}
718719
```
719720
</Details>
720721
The generated ID is used here and passed as a parameter to the WebSocket server:
@@ -743,8 +744,8 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
743744
y = ev.pageY / window.innerHeight;
744745
const now = Date.now();
745746
if (
746-
now - lastSentTimestamp.current > sendInterval &&
747-
wsRef.current?.readyState === WebSocket.OPEN
747+
now - lastSentTimestamp.current > INTERVAL &&
748+
wsRef.current?.readyState === WebSocket.OPEN
748749
) {
749750
const message: WsMessage = { type: "move", id: props.id, x, y };
750751
wsRef.current.send(JSON.stringify(message));
@@ -756,10 +757,9 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
756757
```
757758
758759
Each animated cursor is controlled by a `PerfectCursor` instance, which animates its position along a spline curve defined by the cursor's latest positions:
759-
```ts {10-12}
760+
```ts {9-11}
760761
// SvgCursor react component
761762
const refSvg = useRef<SVGSVGElement>(null);
762-
const point = [window.innerWidth * props.x, window.innerHeight * props.y];
763763
const animateCursor = useCallback((point: number[]) => {
764764
refSvg.current?.style.setProperty(
765765
"transform",

0 commit comments

Comments
 (0)