@@ -186,7 +186,7 @@ that will be made available to the Next.js Worker using a [`WorkerEntrypoint`](/
186186
187187
1881883 . 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