tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/examples/turbosync/components/ui/tooltip.tsx b/examples/turbosync/components/ui/tooltip.tsx
new file mode 100644
index 00000000..ec65c1e4
--- /dev/null
+++ b/examples/turbosync/components/ui/tooltip.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import * as React from "react"
+import { Tooltip as TooltipPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/examples/turbosync/components/video-player.tsx b/examples/turbosync/components/video-player.tsx
new file mode 100644
index 00000000..fbd96f46
--- /dev/null
+++ b/examples/turbosync/components/video-player.tsx
@@ -0,0 +1,877 @@
+"use client";
+
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import {
+ Maximize,
+ Minimize,
+ Pause,
+ Play,
+ Volume2,
+ VolumeOff,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export interface VideoPlayerHandle {
+ play: () => void;
+ pause: () => void;
+ seek: (time: number) => void;
+ setVolume: (volume: number) => void;
+ toggleMute: () => void;
+ toggleFullscreen: () => void;
+ toggleSubtitles: () => void;
+ getCurrentTime: () => number;
+ getDuration: () => number;
+ getVolume: () => number;
+ isMuted: () => boolean;
+ isPaused: () => boolean;
+ areSubtitlesVisible: () => boolean;
+ hasSubtitles: () => boolean;
+ getVideoWidth: () => number;
+ getVideoHeight: () => number;
+}
+
+export interface VideoPlayerClassNames {
+ root?: string;
+ video?: string;
+ overlay?: string;
+ progressTrack?: string;
+ progressFill?: string;
+ progressThumb?: string;
+ controlsBar?: string;
+ controlsLeft?: string;
+ controlsRight?: string;
+ button?: string;
+ volumeTrack?: string;
+ volumeFill?: string;
+ timeDisplay?: string;
+}
+
+export type VideoPlayerSize = "sm" | "md" | "lg";
+
+export interface VideoPlayerProps {
+ children?: React.ReactNode;
+ src: string;
+ poster?: string;
+ subTitlesFile?: string;
+ subtitlesLang?: string;
+ autoPlay?: boolean;
+ loop?: boolean;
+ muted?: boolean;
+ showControls?: boolean;
+ preload?: "auto" | "metadata" | "none";
+ size?: VideoPlayerSize;
+ accentColor?: string;
+ classNames?: VideoPlayerClassNames;
+ className?: string;
+ controlsTimeout?: number;
+ seekStep?: number;
+ volumeStep?: number;
+ disableKeyboardShortcuts?: boolean;
+ onPlay?: () => void;
+ onPause?: () => void;
+ onTimeUpdate?: (currentTime: number, duration: number) => void;
+ onLoadedMetadata?: (duration: number) => void;
+ onEnded?: () => void;
+ onVolumeChange?: (volume: number, muted: boolean) => void;
+ onFullscreenChange?: (isFullscreen: boolean) => void;
+ onSeek?: (time: number) => void;
+ renderPlayButton?: (
+ isPlaying: boolean,
+ toggle: () => void,
+ ) => React.ReactNode;
+ renderMuteButton?: (isMuted: boolean, toggle: () => void) => React.ReactNode;
+ renderFullscreenButton?: (
+ isFullscreen: boolean,
+ toggle: () => void,
+ ) => React.ReactNode;
+ renderTimeDisplay?: (
+ currentTime: number,
+ duration: number,
+ formatted: { current: string; total: string },
+ ) => React.ReactNode;
+ renderExtraControlsLeft?: () => React.ReactNode;
+ renderExtraControlsRight?: () => React.ReactNode;
+}
+
+/* ------------------------------------------------------------------ */
+/* Size presets */
+/* ------------------------------------------------------------------ */
+
+const SIZE_STYLES: Record<
+ VideoPlayerSize,
+ { controls: string; icon: string; text: string; padding: string }
+> = {
+ sm: {
+ controls: "gap-1.5",
+ icon: "size-3.5",
+ text: "text-[10px]",
+ padding: "px-2.5 py-1.5",
+ },
+ md: {
+ controls: "gap-2.5",
+ icon: "size-4",
+ text: "text-xs",
+ padding: "px-3.5 py-2",
+ },
+ lg: {
+ controls: "gap-3",
+ icon: "size-5",
+ text: "text-sm",
+ padding: "px-4 py-3",
+ },
+};
+
+/* ------------------------------------------------------------------ */
+/* Helpers */
+/* ------------------------------------------------------------------ */
+
+function formatTime(seconds: number): string {
+ if (!Number.isFinite(seconds)) return "0:00";
+ const hrs = Math.floor(seconds / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+ if (hrs > 0) {
+ return `${hrs}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
+ }
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+}
+
+function clamp(val: number, min: number, max: number) {
+ return Math.max(min, Math.min(max, val));
+}
+
+/* ------------------------------------------------------------------ */
+/* Component */
+/* ------------------------------------------------------------------ */
+
+export const VideoPlayer = React.forwardRef<
+ VideoPlayerHandle,
+ VideoPlayerProps
+>(
+ (
+ {
+ src,
+ poster,
+ subTitlesFile,
+ subtitlesLang = "en",
+ autoPlay = false,
+ loop = false,
+ muted = false,
+ showControls = true,
+ preload = "metadata",
+
+ // Customization
+ size = "md",
+ accentColor,
+ classNames = {},
+ className,
+ controlsTimeout = 3000,
+ seekStep = 5,
+ volumeStep = 0.1,
+ disableKeyboardShortcuts = false,
+
+ // Callbacks
+ onPlay,
+ onPause,
+ onTimeUpdate,
+ onLoadedMetadata,
+ onEnded,
+ onVolumeChange,
+ onFullscreenChange,
+ onSeek,
+
+ // Render Slots
+ renderPlayButton,
+ renderMuteButton,
+ renderFullscreenButton,
+ renderTimeDisplay,
+ renderExtraControlsLeft,
+ renderExtraControlsRight,
+ children,
+ },
+ ref,
+ ) => {
+ const videoRef = useRef(null);
+ const containerRef = useRef(null);
+ const progressTrackRef = useRef(null);
+ const hideControlsTimeoutRef = useRef | null>(
+ null,
+ );
+
+ const [isPlaying, setIsPlaying] = useState(autoPlay);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [isMuted, setIsMuted] = useState(muted);
+ const [volume, setVolume] = useState(muted ? 0 : 1);
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [showControlsUI, setShowControlsUI] = useState(true);
+ const [isDraggingProgress, setIsDraggingProgress] = useState(false);
+ const [hoverProgress, setHoverProgress] = useState(null);
+ const [hoverTime, setHoverTime] = useState(null);
+ const [isDraggingVolume, setIsDraggingVolume] = useState(false);
+ const [subtitlesVisible, setSubtitlesVisible] = useState(true);
+
+ const sizePreset = SIZE_STYLES[size];
+
+ // Accent color as CSS variable
+ const accentStyle = useMemo(
+ () =>
+ accentColor
+ ? ({ "--vp-accent": accentColor } as React.CSSProperties)
+ : undefined,
+ [accentColor],
+ );
+
+ const accentBg = accentColor ? "bg-[var(--vp-accent)]" : "bg-red-500";
+
+ /* ---- Imperative handle ---------------------------------------- */
+
+ useEffect(() => {
+ if (!ref) return;
+
+ const handle: VideoPlayerHandle = {
+ play: () => videoRef.current?.play().catch(() => {}),
+ pause: () => videoRef.current?.pause(),
+ seek: (time) => {
+ if (videoRef.current) {
+ videoRef.current.currentTime = clamp(
+ time,
+ 0,
+ videoRef.current.duration,
+ );
+ }
+ },
+ setVolume: (v) => {
+ if (videoRef.current) {
+ const clamped = clamp(v, 0, 1);
+ videoRef.current.volume = clamped;
+ setVolume(clamped);
+ }
+ },
+ toggleMute: () => handleMuteToggle(),
+ toggleFullscreen: () => handleFullscreen(),
+ toggleSubtitles: () => handleSubtitleToggle(),
+ getCurrentTime: () => videoRef.current?.currentTime ?? 0,
+ getDuration: () => videoRef.current?.duration ?? 0,
+ getVolume: () => videoRef.current?.volume ?? 0,
+ isMuted: () => videoRef.current?.muted ?? false,
+ isPaused: () => videoRef.current?.paused ?? true,
+ areSubtitlesVisible: () => subtitlesVisible,
+ hasSubtitles: () => !!subTitlesFile,
+ getVideoWidth: () => videoRef.current?.videoWidth ?? 0,
+ getVideoHeight: () => videoRef.current?.videoHeight ?? 0,
+ };
+
+ if (typeof ref === "function") {
+ ref(handle);
+ } else {
+ ref.current = handle;
+ }
+ });
+
+ /* ---- Event handlers ------------------------------------------- */
+
+ const handlePlayPause = useCallback(() => {
+ if (!videoRef.current) return;
+ if (videoRef.current.paused) {
+ videoRef.current.play().catch(() => {});
+ } else {
+ videoRef.current.pause();
+ }
+ }, []);
+
+ const handleSeek = useCallback(
+ (time: number) => {
+ if (!videoRef.current) return;
+ const clamped = clamp(time, 0, videoRef.current.duration || 0);
+ videoRef.current.currentTime = clamped;
+ onSeek?.(clamped);
+ },
+ [onSeek],
+ );
+
+ const handleVolumeChange = useCallback(
+ (newVolume: number) => {
+ if (!videoRef.current) return;
+ const vol = clamp(newVolume, 0, 1);
+ videoRef.current.volume = vol;
+ videoRef.current.muted = vol === 0;
+ setVolume(vol);
+ setIsMuted(vol === 0);
+ onVolumeChange?.(vol, vol === 0);
+ },
+ [onVolumeChange],
+ );
+
+ const handleMuteToggle = useCallback(() => {
+ if (!videoRef.current) return;
+ const newMuted = !videoRef.current.muted;
+ videoRef.current.muted = newMuted;
+ setIsMuted(newMuted);
+ onVolumeChange?.(volume, newMuted);
+ }, [onVolumeChange, volume]);
+
+ const handleFullscreen = useCallback(() => {
+ if (!containerRef.current) return;
+
+ if (!document.fullscreenElement) {
+ containerRef.current.requestFullscreen?.().catch(() => {});
+ } else {
+ document.exitFullscreen?.().catch(() => {});
+ }
+ }, []);
+
+ const handleSubtitleToggle = useCallback(() => {
+ if (!videoRef.current) return;
+ const track = videoRef.current.textTracks[0];
+ if (track) {
+ const newVisible = track.mode !== "showing";
+ track.mode = newVisible ? "showing" : "disabled";
+ setSubtitlesVisible(newVisible);
+ }
+ }, []);
+
+ // Sync fullscreen state with browser
+ useEffect(() => {
+ const onFsChange = () => {
+ const fs = !!document.fullscreenElement;
+ setIsFullscreen(fs);
+ onFullscreenChange?.(fs);
+ };
+ document.addEventListener("fullscreenchange", onFsChange);
+ return () => document.removeEventListener("fullscreenchange", onFsChange);
+ }, [onFullscreenChange]);
+
+ /* ---- Auto-hide controls --------------------------------------- */
+
+ const scheduleHideControls = useCallback(() => {
+ if (hideControlsTimeoutRef.current) {
+ clearTimeout(hideControlsTimeoutRef.current);
+ }
+ if (controlsTimeout > 0 && isPlaying && showControls) {
+ hideControlsTimeoutRef.current = setTimeout(() => {
+ setShowControlsUI(false);
+ }, controlsTimeout);
+ }
+ }, [isPlaying, showControls, controlsTimeout]);
+
+ const handleMouseActivity = useCallback(() => {
+ setShowControlsUI(true);
+ scheduleHideControls();
+ }, [scheduleHideControls]);
+
+ useEffect(() => {
+ return () => {
+ if (hideControlsTimeoutRef.current) {
+ clearTimeout(hideControlsTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ /* ---- Progress bar drag --------------------------------------- */
+
+ const getProgressFromEvent = useCallback(
+ (e: MouseEvent | React.MouseEvent) => {
+ if (!progressTrackRef.current || !duration) return 0;
+ const rect = progressTrackRef.current.getBoundingClientRect();
+ return clamp((e.clientX - rect.left) / rect.width, 0, 1) * duration;
+ },
+ [duration],
+ );
+
+ useEffect(() => {
+ if (!isDraggingProgress) return;
+
+ const onMove = (e: MouseEvent) => {
+ const time = getProgressFromEvent(e);
+ setCurrentTime(time);
+ };
+
+ const onUp = (e: MouseEvent) => {
+ const time = getProgressFromEvent(e);
+ handleSeek(time);
+ setIsDraggingProgress(false);
+ };
+
+ window.addEventListener("mousemove", onMove);
+ window.addEventListener("mouseup", onUp);
+ return () => {
+ window.removeEventListener("mousemove", onMove);
+ window.removeEventListener("mouseup", onUp);
+ };
+ }, [isDraggingProgress, getProgressFromEvent, handleSeek]);
+
+ /* ---- Keyboard shortcuts --------------------------------------- */
+
+ useEffect(() => {
+ if (disableKeyboardShortcuts) return;
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Only handle if the player or its children are focused
+ if (!containerRef.current?.contains(document.activeElement)) return;
+
+ switch (e.key) {
+ case " ":
+ case "k":
+ e.preventDefault();
+ handlePlayPause();
+ break;
+ case "ArrowLeft":
+ e.preventDefault();
+ handleSeek(
+ clamp(
+ (videoRef.current?.currentTime ?? 0) - seekStep,
+ 0,
+ duration,
+ ),
+ );
+ break;
+ case "ArrowRight":
+ e.preventDefault();
+ handleSeek(
+ clamp(
+ (videoRef.current?.currentTime ?? 0) + seekStep,
+ 0,
+ duration,
+ ),
+ );
+ break;
+ case "ArrowUp":
+ e.preventDefault();
+ handleVolumeChange(
+ clamp((videoRef.current?.volume ?? 1) + volumeStep, 0, 1),
+ );
+ break;
+ case "ArrowDown":
+ e.preventDefault();
+ handleVolumeChange(
+ clamp((videoRef.current?.volume ?? 1) - volumeStep, 0, 1),
+ );
+ break;
+ case "m":
+ e.preventDefault();
+ handleMuteToggle();
+ break;
+ case "f":
+ e.preventDefault();
+ handleFullscreen();
+ break;
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [
+ disableKeyboardShortcuts,
+ handlePlayPause,
+ handleSeek,
+ handleVolumeChange,
+ handleMuteToggle,
+ handleFullscreen,
+ seekStep,
+ volumeStep,
+ duration,
+ ]);
+
+ /* ---- Volume drag (window-level) -------------------------------- */
+
+ useEffect(() => {
+ if (!isDraggingVolume) return;
+
+ const onMove = (e: MouseEvent) => {
+ const volSlider =
+ containerRef.current?.querySelector(
+ "[data-vol-track]",
+ );
+ if (!volSlider) return;
+ const rect = volSlider.getBoundingClientRect();
+ const pct = clamp((e.clientX - rect.left) / rect.width, 0, 1);
+ handleVolumeChange(pct);
+ };
+
+ const onUp = () => setIsDraggingVolume(false);
+
+ window.addEventListener("mousemove", onMove);
+ window.addEventListener("mouseup", onUp);
+ return () => {
+ window.removeEventListener("mousemove", onMove);
+ window.removeEventListener("mouseup", onUp);
+ };
+ }, [isDraggingVolume, handleVolumeChange]);
+
+ /* ---- Computed values ------------------------------------------ */
+
+ const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
+ const volumePercent = volume * 100;
+ const formattedCurrent = formatTime(currentTime);
+ const formattedDuration = formatTime(duration);
+
+ /* ---- Render --------------------------------------------------- */
+
+ return (
+ {
+ if (hideControlsTimeoutRef.current) {
+ clearTimeout(hideControlsTimeoutRef.current);
+ }
+ if (isPlaying && showControls && controlsTimeout > 0) {
+ setShowControlsUI(false);
+ }
+ }}
+ >
+ {/* Video Element */}
+
{
+ if (!isDraggingProgress) {
+ const t = videoRef.current?.currentTime ?? 0;
+ setCurrentTime(t);
+ onTimeUpdate?.(t, duration);
+ }
+ }}
+ onLoadedMetadata={() => {
+ const d = videoRef.current?.duration ?? 0;
+ setDuration(d);
+ onLoadedMetadata?.(d);
+ }}
+ onPlay={() => {
+ setIsPlaying(true);
+ onPlay?.();
+ }}
+ onPause={() => {
+ setIsPlaying(false);
+ onPause?.();
+ }}
+ onEnded={() => onEnded?.()}
+ onVolumeChange={() => {
+ if (videoRef.current) {
+ setVolume(videoRef.current.volume);
+ setIsMuted(videoRef.current.muted);
+ }
+ }}
+ >
+ {children}
+
+ {subTitlesFile && (
+
+ )}
+ Your browser does not support the video tag.
+
+
+ {/* Controls Overlay */}
+ {showControls && (
+
+ {/* Progress Bar */}
+ {
+ if (isDraggingProgress) return;
+ const time = getProgressFromEvent(e as unknown as MouseEvent);
+ handleSeek(time);
+ }}
+ onMouseDown={(e) => {
+ e.preventDefault();
+ setIsDraggingProgress(true);
+ const time = getProgressFromEvent(e as unknown as MouseEvent);
+ setCurrentTime(time);
+ }}
+ onMouseMove={(e) => {
+ if (!progressTrackRef.current || !duration) return;
+ const rect = progressTrackRef.current.getBoundingClientRect();
+ const pct =
+ clamp((e.clientX - rect.left) / rect.width, 0, 1) * 100;
+ setHoverProgress(pct);
+ setHoverTime((pct / 100) * duration);
+ }}
+ onMouseLeave={() => {
+ setHoverProgress(null);
+ setHoverTime(null);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "ArrowLeft") {
+ e.preventDefault();
+ handleSeek(Math.max(0, currentTime - seekStep));
+ } else if (e.key === "ArrowRight") {
+ e.preventDefault();
+ handleSeek(Math.min(duration, currentTime + seekStep));
+ }
+ }}
+ >
+ {/* Track background */}
+
+
+ {/* Hover preview */}
+ {hoverProgress !== null && (
+
+ )}
+
+ {/* Hover timestamp tooltip */}
+ {hoverProgress !== null && hoverTime !== null && (
+
+
+ {formatTime(hoverTime)}
+
+
+ )}
+
+ {/* Filled progress */}
+
+
+ {/* Thumb */}
+
+
+
+ {/* Controls Bar */}
+
+ {/* Left Controls */}
+
+ {/* Play / Pause */}
+ {renderPlayButton ? (
+ renderPlayButton(isPlaying, handlePlayPause)
+ ) : (
+
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Mute / Unmute */}
+ {renderMuteButton ? (
+ renderMuteButton(isMuted, handleMuteToggle)
+ ) : (
+
+ {isMuted ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Volume Slider */}
+
{
+ e.preventDefault();
+ setIsDraggingVolume(true);
+ const rect = e.currentTarget.getBoundingClientRect();
+ const pct = clamp(
+ (e.clientX - rect.left) / rect.width,
+ 0,
+ 1,
+ );
+ handleVolumeChange(pct);
+ }}
+ >
+
+
+
+ {/* Time Display */}
+ {renderTimeDisplay ? (
+ renderTimeDisplay(currentTime, duration, {
+ current: formattedCurrent,
+ total: formattedDuration,
+ })
+ ) : (
+
+ {formattedCurrent} / {formattedDuration}
+
+ )}
+
+ {renderExtraControlsLeft?.()}
+
+
+ {/* Right Controls */}
+
+ {renderExtraControlsRight?.()}
+
+ {/* Fullscreen */}
+ {renderFullscreenButton ? (
+ renderFullscreenButton(isFullscreen, handleFullscreen)
+ ) : (
+
+ {isFullscreen ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ )}
+
+ );
+ },
+);
+
+VideoPlayer.displayName = "VideoPlayer";
+
+export default VideoPlayer;
diff --git a/examples/turbosync/lib/room-context.tsx b/examples/turbosync/lib/room-context.tsx
new file mode 100644
index 00000000..172b61ca
--- /dev/null
+++ b/examples/turbosync/lib/room-context.tsx
@@ -0,0 +1,448 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+ useCallback,
+ ReactNode,
+} from "react";
+import type {
+ User,
+ RoomState,
+ RoomPermissions,
+ WSClientMessage,
+ WSServerMessage,
+} from "@/types";
+import { toast } from "sonner";
+
+interface RoomContextType {
+ roomState: RoomState | null;
+ currentUser: User | null;
+ isConnected: boolean;
+ latency: number;
+ error: string | null;
+ connect: (
+ roomId: string,
+ user: Omit & { peerId?: string },
+ password?: string,
+ ) => void;
+ disconnect: (clearParams?: boolean) => void;
+ play: (currentTime: number) => void;
+ pause: (currentTime: number) => void;
+ seek: (currentTime: number) => void;
+ sendMessage: (message: string) => void;
+ setPlaybackRate: (rate: number) => void;
+ syncRequest: () => void;
+ kick: (userId: string) => void;
+ updatePermissions: (perms: Partial) => void;
+ reportTimeUpdate: (currentTime: number) => void;
+ latencyHistory: number[];
+}
+
+const RoomContext = createContext(undefined);
+
+export function RoomProvider({ children }: { children: ReactNode }) {
+ const [roomState, setRoomState] = useState(null);
+ const [currentUser, setCurrentUser] = useState(null);
+ const [isConnected, setIsConnected] = useState(false);
+ const [latency, setLatency] = useState(0);
+ const [latencyHistory, setLatencyHistory] = useState([]);
+ const [error, setError] = useState(null);
+
+ const wsRef = useRef(null);
+ const reconnectTimeoutRef = useRef(null);
+ const reconnectAttemptsRef = useRef(0);
+ const connectionParamsRef = useRef<{
+ roomId: string;
+ user: Omit & { peerId?: string };
+ password?: string;
+ } | null>(null);
+ const pingIntervalRef = useRef(null);
+ const pingStartRef = useRef(0);
+ const lastPongReceivedAtRef = useRef(0);
+
+ // Helper: Get or create persistent peerId
+ const getPeerId = useCallback(() => {
+ if (typeof window === "undefined") return "";
+ let pid = localStorage.getItem("turbosync_peerid");
+ if (!pid) {
+ pid = crypto.randomUUID();
+ localStorage.setItem("turbosync_peerid", pid);
+ }
+ return pid;
+ }, []);
+
+ const disconnect = useCallback((clearParams = true) => {
+ if (wsRef.current) {
+ // Remove handlers before closing to prevent unwanted reconnects during intentional disconnect
+ wsRef.current.onclose = null;
+ wsRef.current.onerror = null;
+ wsRef.current.close();
+ wsRef.current = null;
+ }
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+ if (pingIntervalRef.current) {
+ clearInterval(pingIntervalRef.current);
+ }
+ if (clearParams) {
+ connectionParamsRef.current = null;
+ reconnectAttemptsRef.current = 0;
+ setRoomState(null);
+ setCurrentUser(null);
+ setError(null);
+ }
+ setIsConnected(false);
+ }, []);
+
+ const connect = useCallback(
+ (
+ roomId: string,
+ user: Omit & { peerId?: string },
+ password?: string,
+ ) => {
+ // Save params for reconnection
+ connectionParamsRef.current = { roomId, user, password };
+
+ // Ensure peerId is present
+ const joinUser = {
+ ...user,
+ peerId: user.peerId || getPeerId(),
+ };
+
+ disconnect(false); // Close existing but keep params
+
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ const wsUrl = `${protocol}//${window.location.host}/ws/${roomId}`;
+
+ const ws = new WebSocket(wsUrl);
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ setIsConnected(true);
+ setError(null);
+ reconnectAttemptsRef.current = 0;
+
+ const joinMessage: WSClientMessage = {
+ type: "join",
+ user: joinUser,
+ password,
+ };
+ ws.send(JSON.stringify(joinMessage));
+
+ // Start pinging for latency measurement
+ lastPongReceivedAtRef.current = performance.now();
+ pingIntervalRef.current = setInterval(() => {
+ const now = performance.now();
+ // If we haven't received a pong in over 5s, assume connection is dead
+ if (now - lastPongReceivedAtRef.current > 5000) {
+ console.warn("Pong timeout, force closing for reconnection...");
+ if (wsRef.current) {
+ wsRef.current.close(); // Triggers onclose and auto-reconnect
+ }
+ return;
+ }
+ pingStartRef.current = now;
+ ws.send("ping");
+ }, 2000);
+ };
+
+ ws.onmessage = (event) => {
+ // Handle automatic pong
+ if (typeof event.data === "string" && event.data === "pong") {
+ const now = performance.now();
+ lastPongReceivedAtRef.current = now;
+ const currentLatency = Math.round(now - pingStartRef.current);
+ setLatency(currentLatency);
+ setLatencyHistory((prev) => {
+ const newHistory = [...prev, currentLatency];
+ return newHistory.length > 40
+ ? newHistory.slice(newHistory.length - 40)
+ : newHistory;
+ });
+ return;
+ }
+
+ try {
+ const msg = JSON.parse(event.data) as WSServerMessage;
+
+ switch (msg.type) {
+ case "room-state": {
+ setRoomState(msg.room);
+ // Server tells us our userId — use it to find ourselves in the user list
+ const myId = msg.yourUserId;
+ const me = msg.room.users.find((u) => u.id === myId);
+ if (me) {
+ setCurrentUser(me);
+ }
+ break;
+ }
+
+ case "sync":
+ setRoomState((prev) => {
+ if (!prev) return msg.room;
+ // Preserve 'away' users that aren't in the new snapshot
+ const onlineIds = new Set(msg.room.users.map((u) => u.id));
+ const awayUsers = prev.users.filter(
+ (u) => u.connectionStatus === "away" && !onlineIds.has(u.id),
+ );
+ return {
+ ...msg.room,
+ users: [...msg.room.users, ...awayUsers],
+ };
+ });
+ break;
+
+ case "user-joined":
+ toast.success(`${msg.user.displayName} joined the room`);
+ setRoomState((prev) =>
+ prev
+ ? {
+ ...prev,
+ users: [
+ ...prev.users.filter((u) => u.id !== msg.user.id),
+ msg.user,
+ ],
+ }
+ : null,
+ );
+ break;
+
+ case "user-left":
+ setRoomState((prev) => {
+ if (!prev) return null;
+ const user = prev.users.find((u) => u.id === msg.userId);
+ if (user && user.connectionStatus !== "away") {
+ toast.info(`${user.displayName} disconnected`);
+ }
+ return {
+ ...prev,
+ users: prev.users.map((u) =>
+ u.id === msg.userId
+ ? { ...u, connectionStatus: "away" as const }
+ : u,
+ ),
+ };
+ });
+ break;
+
+ case "play":
+ setRoomState((prev) =>
+ prev
+ ? { ...prev, paused: false, currentTime: msg.currentTime }
+ : null,
+ );
+ break;
+
+ case "pause":
+ setRoomState((prev) =>
+ prev
+ ? { ...prev, paused: true, currentTime: msg.currentTime }
+ : null,
+ );
+ break;
+
+ case "seek":
+ setRoomState((prev) =>
+ prev ? { ...prev, currentTime: msg.currentTime } : null,
+ );
+ break;
+
+ case "playback-rate":
+ setRoomState((prev) =>
+ prev ? { ...prev, playbackRate: msg.rate } : null,
+ );
+ break;
+
+ case "permissions-updated":
+ setRoomState((prev) =>
+ prev ? { ...prev, permissions: msg.permissions } : null,
+ );
+ break;
+
+ case "user-time-update":
+ setRoomState((prev) => {
+ if (!prev) return null;
+ return {
+ ...prev,
+ users: prev.users.map((u) =>
+ u.id === msg.userId
+ ? { ...u, videoTimestamp: msg.currentTime }
+ : u,
+ ),
+ };
+ });
+ break;
+
+ case "kicked":
+ setError(msg.reason);
+ disconnect(true); // Intentional kick = stop reconnecting
+ break;
+
+ case "chat":
+ console.log(`Chat from ${msg.displayName}: ${msg.message}`);
+ break;
+
+ case "error":
+ console.error("Room error:", msg.message);
+ // Only stop reconnecting if it's an auth/not-found error
+ if (msg.code === "UNAUTHORIZED" || msg.code === "NOT_FOUND") {
+ setError(msg.message);
+ disconnect(true);
+ } else {
+ setError(msg.message);
+ }
+ break;
+ }
+ } catch (err) {
+ console.error("Failed to parse websocket message", err);
+ }
+ };
+
+ ws.onclose = (event) => {
+ setIsConnected(false);
+ if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
+
+ // Attempt reconnection if not closed intentionally
+ if (event.code !== 1000 && connectionParamsRef.current) {
+ const delay = Math.min(
+ 1000 * Math.pow(2, reconnectAttemptsRef.current),
+ 10000,
+ );
+ reconnectAttemptsRef.current += 1;
+ setError(
+ `Connection lost. Retrying in ${Math.round(delay / 1000)}s...`,
+ );
+
+ reconnectTimeoutRef.current = setTimeout(() => {
+ const params = connectionParamsRef.current;
+ if (params) {
+ connect(params.roomId, params.user, params.password);
+ }
+ }, delay);
+ }
+ };
+
+ ws.onerror = () => {
+ setIsConnected(false);
+ // Error handler mostly triggers before close, let onclose handle the retry logic
+ };
+ },
+ [disconnect, getPeerId],
+ );
+
+ const sendWS = useCallback((msg: WSClientMessage) => {
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify(msg));
+ }
+ }, []);
+
+ const play = useCallback(
+ (currentTime: number) => {
+ sendWS({ type: "play", currentTime });
+ setRoomState((prev) =>
+ prev ? { ...prev, paused: false, currentTime } : null,
+ );
+ },
+ [sendWS],
+ );
+
+ const pause = useCallback(
+ (currentTime: number) => {
+ sendWS({ type: "pause", currentTime });
+ setRoomState((prev) =>
+ prev ? { ...prev, paused: true, currentTime } : null,
+ );
+ },
+ [sendWS],
+ );
+
+ const seek = useCallback(
+ (currentTime: number) => {
+ sendWS({ type: "seek", currentTime });
+ setRoomState((prev) => (prev ? { ...prev, currentTime } : null));
+ },
+ [sendWS],
+ );
+
+ const sendMessage = useCallback(
+ (message: string) => {
+ sendWS({ type: "chat", message });
+ },
+ [sendWS],
+ );
+
+ const setPlaybackRate = useCallback(
+ (rate: number) => {
+ sendWS({ type: "playback-rate", rate });
+ },
+ [sendWS],
+ );
+
+ const syncRequest = useCallback(() => {
+ sendWS({ type: "sync-request" });
+ }, [sendWS]);
+
+ const kick = useCallback(
+ (userId: string) => {
+ sendWS({ type: "kick", userId });
+ },
+ [sendWS],
+ );
+
+ const updatePermissions = useCallback(
+ (perms: Partial) => {
+ sendWS({ type: "update-permissions", permissions: perms });
+ },
+ [sendWS],
+ );
+
+ const reportTimeUpdate = useCallback(
+ (currentTime: number) => {
+ sendWS({ type: "time-update", currentTime });
+ },
+ [sendWS],
+ );
+
+ useEffect(() => {
+ return () => {
+ disconnect();
+ };
+ }, [disconnect]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useRoom() {
+ const context = useContext(RoomContext);
+ if (context === undefined) {
+ throw new Error("useRoom must be used within a RoomProvider");
+ }
+ return context;
+}
diff --git a/examples/turbosync/lib/room.ts b/examples/turbosync/lib/room.ts
new file mode 100644
index 00000000..f6207c32
--- /dev/null
+++ b/examples/turbosync/lib/room.ts
@@ -0,0 +1,559 @@
+import type {
+ User,
+ RoomState,
+ RoomPermissions,
+ WSClientMessage,
+ WSServerMessage,
+ CreateRoomResponse,
+ RoomStateResponse,
+} from "@/types";
+import { DurableObject } from "cloudflare:workers";
+
+// ─── Session attachment (serialized onto each WebSocket) ───────────
+interface SessionAttachment {
+ userId: string;
+ peerId: string;
+ displayName: string;
+ avatar?: string;
+ isHost: boolean;
+ /** User's last reported local video timestamp */
+ videoTimestamp?: number;
+}
+
+// ─── Default permissions ───────────────────────────────────────────
+const DEFAULT_PERMISSIONS: RoomPermissions = {
+ viewersCanControl: true,
+ viewersCanChat: true,
+};
+
+// ─── Room Durable Object ───────────────────────────────────────────
+export class Room extends DurableObject {
+ private sessions: Map;
+
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env);
+
+ // Rebuild sessions from any hibernated WebSockets
+ this.sessions = new Map();
+ for (const ws of this.ctx.getWebSockets()) {
+ const attachment = ws.deserializeAttachment() as SessionAttachment | null;
+ if (attachment) {
+ this.sessions.set(ws, { ...attachment });
+ }
+ }
+
+ // Automatic ping/pong that doesn't wake the DO from hibernation
+ this.ctx.setWebSocketAutoResponse(
+ new WebSocketRequestResponsePair("ping", "pong"),
+ );
+ }
+
+ // ─── RPC: Check if room exists ──────────────────────────────────
+ async exists(): Promise {
+ const name = await this.ctx.storage.get("name");
+ return name !== undefined;
+ }
+
+ // ─── RPC: Create / initialize room metadata ─────────────────────
+ async createRoom(
+ name: string,
+ password?: string,
+ hostPeerId?: string,
+ ): Promise {
+ const existingName = await this.ctx.storage.get("name");
+ const existingHostPeerId = await this.ctx.storage.get("hostPeerId");
+
+ // If the room is already claimed (has a stored hostPeerId), reject it
+ if (existingName && existingHostPeerId) {
+ return { conflict: true };
+ }
+
+ const initialData: Record = {
+ name,
+ paused: true,
+ currentTime: 0,
+ playbackRate: 1,
+ };
+
+ if (password) {
+ initialData.password = password;
+ } else {
+ await this.ctx.storage.delete("password");
+ }
+
+ // Set default permissions
+ initialData.permissions = DEFAULT_PERMISSIONS;
+
+ if (hostPeerId) {
+ initialData.hostPeerId = hostPeerId;
+ }
+
+ await this.ctx.storage.put(initialData);
+
+ return { roomId: this.ctx.id.toString(), name };
+ }
+
+ // ─── RPC: Get room state ────────────────────────────────────────
+ async getRoomState(): Promise {
+ const room = await this.buildRoomSnapshot();
+ return { room };
+ }
+
+ // ─── WebSocket upgrade via fetch() ──────────────────────────────
+ async fetch(request: Request): Promise {
+ const upgrade = request.headers.get("Upgrade");
+ if (!upgrade || upgrade !== "websocket") {
+ return new Response("Expected WebSocket upgrade", { status: 426 });
+ }
+
+ // Reject if room doesn't exist
+ const roomExists = await this.exists();
+ if (!roomExists) {
+ return new Response(JSON.stringify({ error: "Room not found" }), {
+ status: 404,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ const webSocketPair = new WebSocketPair();
+ const [client, server] = Object.values(webSocketPair);
+
+ this.ctx.acceptWebSocket(server);
+
+ const userId = crypto.randomUUID();
+ const attachment: SessionAttachment = {
+ userId,
+ peerId: "", // Will be set on Join
+ displayName: "Anonymous",
+ isHost: false, // Will be set on Join
+ };
+ server.serializeAttachment(attachment);
+ this.sessions.set(server, attachment);
+
+ return new Response(null, { status: 101, webSocket: client });
+ }
+
+ // ─── WebSocket message handler ──────────────────────────────────
+ async webSocketMessage(
+ ws: WebSocket,
+ message: string | ArrayBuffer,
+ ): Promise {
+ let parsed: WSClientMessage;
+
+ try {
+ const raw =
+ typeof message === "string"
+ ? message
+ : new TextDecoder().decode(message);
+ parsed = JSON.parse(raw) as WSClientMessage;
+ } catch {
+ this.send(ws, {
+ type: "error",
+ code: "INVALID_MESSAGE",
+ message: "Could not parse message as JSON",
+ });
+ return;
+ }
+
+ const session = this.sessions.get(ws);
+ if (!session) return;
+
+ switch (parsed.type) {
+ case "join":
+ await this.handleJoin(ws, session, parsed);
+ break;
+ case "leave":
+ await this.handleLeave(ws, session);
+ break;
+ case "play":
+ await this.handlePlaybackAction(
+ ws,
+ session,
+ "play",
+ parsed.currentTime,
+ );
+ break;
+ case "pause":
+ await this.handlePlaybackAction(
+ ws,
+ session,
+ "pause",
+ parsed.currentTime,
+ );
+ break;
+ case "seek":
+ await this.handlePlaybackAction(
+ ws,
+ session,
+ "seek",
+ parsed.currentTime,
+ );
+ break;
+ case "chat":
+ await this.handleChat(ws, session, parsed.message);
+ break;
+ case "sync-request": {
+ const roomState = await this.buildRoomSnapshot();
+ this.send(ws, { type: "sync", room: roomState });
+ break;
+ }
+ case "playback-rate":
+ await this.ctx.storage.put("playbackRate", parsed.rate);
+ this.broadcast({
+ type: "playback-rate",
+ rate: parsed.rate,
+ userId: session.userId,
+ });
+ break;
+ case "time-update":
+ this.handleTimeUpdate(ws, session, parsed.currentTime);
+ break;
+ case "kick":
+ await this.handleKick(ws, session, parsed.userId);
+ break;
+ case "update-permissions":
+ await this.handleUpdatePermissions(ws, session, parsed.permissions);
+ break;
+ default:
+ this.send(ws, {
+ type: "error",
+ code: "UNKNOWN_TYPE",
+ message: `Unknown message type: ${(parsed as { type: string }).type}`,
+ });
+ }
+ }
+
+ // ─── WebSocket close handler ────────────────────────────────────
+ async webSocketClose(
+ ws: WebSocket,
+ code: number,
+ reason: string,
+ _wasClean: boolean,
+ ): Promise {
+ const session = this.sessions.get(ws);
+ this.sessions.delete(ws);
+
+ try {
+ ws.close(code, reason);
+ } catch {
+ // Socket may already be closed
+ }
+
+ if (session) {
+ this.broadcast({ type: "user-left", userId: session.userId });
+ }
+ }
+
+ // ─── WebSocket error handler ────────────────────────────────────
+ async webSocketError(ws: WebSocket, _error: unknown): Promise {
+ const session = this.sessions.get(ws);
+ this.sessions.delete(ws);
+
+ if (session) {
+ this.broadcast({ type: "user-left", userId: session.userId });
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // Private helpers
+ // ═══════════════════════════════════════════════════════════════════
+
+ private async handleJoin(
+ ws: WebSocket,
+ session: SessionAttachment,
+ msg: Extract,
+ ): Promise {
+ const expectedPassword = await this.ctx.storage.get("password");
+ if (expectedPassword && msg.password !== expectedPassword) {
+ this.send(ws, {
+ type: "error",
+ code: "UNAUTHORIZED",
+ message: "Invalid room password",
+ });
+ ws.close(1008, "Invalid room password");
+ this.sessions.delete(ws);
+ return;
+ }
+
+ const hostPeerId = await this.ctx.storage.get("hostPeerId");
+ const hasActiveHost = Array.from(this.sessions.values()).some(
+ (s) => s.isHost && s.peerId !== msg.user.peerId,
+ );
+
+ let isHost = msg.user.isHost;
+
+ if (hostPeerId) {
+ // If a hostPeerId exists, only a matching peerId can be host
+ if (msg.user.peerId === hostPeerId) {
+ isHost = true;
+ } else {
+ isHost = false;
+ }
+ } else if (isHost) {
+ // If no hostPeerId exists yet, the first person who claims to be host sets it
+ await this.ctx.storage.put("hostPeerId", msg.user.peerId);
+ }
+
+ // Double check: if there's already an active host (different peer), demote this one
+ if (isHost && hasActiveHost) {
+ isHost = false;
+ }
+
+ // Generate userId from slugified displayName
+ const slugId = msg.user.displayName
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/(^-|-$)/g, "");
+ const newUserId = slugId || session.userId; // fallback to generated UUID if name is empty
+
+ // Deduplicate: If another session has the same userId, disconnect it
+ for (const [existingWs, existingSession] of this.sessions.entries()) {
+ if (existingSession.userId === newUserId && existingWs !== ws) {
+ this.sessions.delete(existingWs);
+ try {
+ existingWs.close(1008, "Joined from another device");
+ } catch {}
+ }
+ }
+
+ // Update session with real user info
+ session.userId = newUserId;
+ session.displayName = msg.user.displayName;
+ session.peerId = msg.user.peerId;
+ session.avatar = msg.user.avatar;
+ session.isHost = isHost;
+ ws.serializeAttachment(session);
+ this.sessions.set(ws, session);
+
+ const user = this.sessionToUser(session);
+
+ // Broadcast to everyone else
+ this.broadcast({ type: "user-joined", user }, ws);
+
+ // Send current room state to the joining user, including their server-assigned ID
+ const roomState = await this.buildRoomSnapshot();
+ this.send(ws, {
+ type: "room-state",
+ room: roomState,
+ yourUserId: session.userId,
+ });
+ }
+
+ private async handleLeave(
+ ws: WebSocket,
+ session: SessionAttachment,
+ ): Promise {
+ this.sessions.delete(ws);
+ this.broadcast({ type: "user-left", userId: session.userId });
+ ws.close(1000, "User left");
+ }
+
+ /** Unified playback action handler with permission enforcement */
+ private async handlePlaybackAction(
+ ws: WebSocket,
+ session: SessionAttachment,
+ action: "play" | "pause" | "seek",
+ currentTime: number,
+ ): Promise {
+ // Check permissions: only enforce if viewersCanControl is explicitly false
+ if (!session.isHost) {
+ const permissions = await this.getPermissions();
+ if (!permissions.viewersCanControl) {
+ this.send(ws, {
+ type: "error",
+ code: "PERMISSION_DENIED",
+ message: "You don't have permission to control playback",
+ });
+ return;
+ }
+ }
+
+ switch (action) {
+ case "play":
+ await this.ctx.storage.put({ paused: false, currentTime });
+ this.broadcast(
+ { type: "play", currentTime, userId: session.userId },
+ ws,
+ );
+ break;
+ case "pause":
+ await this.ctx.storage.put({ paused: true, currentTime });
+ this.broadcast(
+ { type: "pause", currentTime, userId: session.userId },
+ ws,
+ );
+ break;
+ case "seek":
+ await this.ctx.storage.put("currentTime", currentTime);
+ this.broadcast(
+ { type: "seek", currentTime, userId: session.userId },
+ ws,
+ );
+ break;
+ }
+ }
+
+ private async handleChat(
+ ws: WebSocket,
+ session: SessionAttachment,
+ message: string,
+ ): Promise {
+ if (!session.isHost) {
+ const permissions = await this.getPermissions();
+ if (!permissions.viewersCanChat) {
+ this.send(ws, {
+ type: "error",
+ code: "PERMISSION_DENIED",
+ message: "Chat is disabled by the host",
+ });
+ return;
+ }
+ }
+
+ this.broadcast({
+ type: "chat",
+ message,
+ userId: session.userId,
+ displayName: session.displayName,
+ });
+ }
+
+ private handleTimeUpdate(
+ ws: WebSocket,
+ session: SessionAttachment,
+ currentTime: number,
+ ): void {
+ // Update session's video timestamp
+ session.videoTimestamp = currentTime;
+ ws.serializeAttachment(session);
+ this.sessions.set(ws, session);
+
+ // Broadcast to all other clients
+ this.broadcast(
+ {
+ type: "user-time-update",
+ userId: session.userId,
+ currentTime,
+ },
+ ws,
+ );
+ }
+
+ private async handleKick(
+ ws: WebSocket,
+ session: SessionAttachment,
+ targetUserId: string,
+ ): Promise {
+ // Only host can kick
+ if (!session.isHost) {
+ this.send(ws, {
+ type: "error",
+ code: "PERMISSION_DENIED",
+ message: "Only the host can kick users",
+ });
+ return;
+ }
+
+ // Find and disconnect the target user
+ for (const [targetWs, targetSession] of this.sessions) {
+ if (targetSession.userId === targetUserId) {
+ this.send(targetWs, {
+ type: "kicked",
+ reason: "You have been kicked by the host",
+ });
+ targetWs.close(1000, "Kicked by host");
+ this.sessions.delete(targetWs);
+ this.broadcast({ type: "user-left", userId: targetUserId });
+ return;
+ }
+ }
+ }
+
+ private async handleUpdatePermissions(
+ ws: WebSocket,
+ session: SessionAttachment,
+ newPerms: Partial,
+ ): Promise {
+ if (!session.isHost) {
+ this.send(ws, {
+ type: "error",
+ code: "PERMISSION_DENIED",
+ message: "Only the host can update permissions",
+ });
+ return;
+ }
+
+ const current = await this.getPermissions();
+ const merged: RoomPermissions = { ...current, ...newPerms };
+ await this.ctx.storage.put("permissions", merged);
+
+ this.broadcast({ type: "permissions-updated", permissions: merged });
+ }
+
+ private async getPermissions(): Promise {
+ const stored = await this.ctx.storage.get("permissions");
+ return stored ?? DEFAULT_PERMISSIONS;
+ }
+
+ /** Send a typed message to a single WebSocket */
+ private send(ws: WebSocket, message: WSServerMessage): void {
+ try {
+ ws.send(JSON.stringify(message));
+ } catch {
+ // Socket may already be closed — ignore
+ }
+ }
+
+ /** Broadcast a typed message to all connected clients, optionally excluding one */
+ private broadcast(message: WSServerMessage, exclude?: WebSocket): void {
+ const payload = JSON.stringify(message);
+ for (const [ws] of this.sessions) {
+ if (ws === exclude) continue;
+ try {
+ ws.send(payload);
+ } catch {
+ // Dead socket — will be cleaned up on next close/error
+ }
+ }
+ }
+
+ /** Build a Room snapshot from storage + active sessions */
+ private async buildRoomSnapshot(): Promise {
+ const [name, paused, currentTime, playbackRate, permissions] =
+ await Promise.all([
+ this.ctx.storage.get("name"),
+ this.ctx.storage.get("paused"),
+ this.ctx.storage.get("currentTime"),
+ this.ctx.storage.get("playbackRate"),
+ this.getPermissions(),
+ ]);
+
+ const users: User[] = [];
+ for (const [, session] of this.sessions) {
+ users.push(this.sessionToUser(session));
+ }
+
+ return {
+ id: this.ctx.id.toString(),
+ name: name ?? "Unnamed Room",
+ users,
+ currentTime: currentTime ?? 0,
+ paused: paused ?? true,
+ playbackRate: playbackRate ?? 1,
+ permissions,
+ };
+ }
+
+ /** Convert a session attachment to a User */
+ private sessionToUser(session: SessionAttachment): User {
+ return {
+ id: session.userId,
+ peerId: session.peerId,
+ displayName: session.displayName,
+ avatar: session.avatar,
+ isHost: session.isHost,
+ connectionStatus: "online",
+ videoTimestamp: session.videoTimestamp,
+ };
+ }
+}
diff --git a/examples/turbosync/lib/utils.ts b/examples/turbosync/lib/utils.ts
new file mode 100644
index 00000000..bd0c391d
--- /dev/null
+++ b/examples/turbosync/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/examples/turbosync/next.config.ts b/examples/turbosync/next.config.ts
new file mode 100644
index 00000000..66e15661
--- /dev/null
+++ b/examples/turbosync/next.config.ts
@@ -0,0 +1,8 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+ reactCompiler: true,
+};
+
+export default nextConfig;
diff --git a/examples/turbosync/package.json b/examples/turbosync/package.json
new file mode 100644
index 00000000..cce4fc40
--- /dev/null
+++ b/examples/turbosync/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "turbosync",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vinext dev --port 3000 --host localhost",
+ "build": "vinext build",
+ "start": "wrangler dev",
+ "deploy": "vinext deploy",
+ "lint": "biome check",
+ "format": "biome format --write"
+ },
+ "dependencies": {
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.575.0",
+ "next": "^16.1.6",
+ "next-themes": "^0.4.6",
+ "radix-ui": "^1.4.3",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "recharts": "^3.7.0",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.5.0",
+ "vinext": "^0.0.24"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "2.2.0",
+ "@cloudflare/vite-plugin": "^1.26.1",
+ "@tailwindcss/postcss": "^4.2.1",
+ "@types/node": "^20.19.37",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-rsc": "^0.5.21",
+ "babel-plugin-react-compiler": "1.0.0",
+ "shadcn": "^3.8.5",
+ "tailwindcss": "^4.2.1",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^5.9.3",
+ "vite": "^7.3.1",
+ "wrangler": "^4.71.0"
+ },
+ "ignoreScripts": [
+ "sharp",
+ "unrs-resolver"
+ ],
+ "trustedDependencies": [
+ "sharp",
+ "unrs-resolver"
+ ]
+}
diff --git a/examples/turbosync/postcss.config.mjs b/examples/turbosync/postcss.config.mjs
new file mode 100644
index 00000000..61e36849
--- /dev/null
+++ b/examples/turbosync/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/examples/turbosync/public/file.svg b/examples/turbosync/public/file.svg
new file mode 100644
index 00000000..004145cd
--- /dev/null
+++ b/examples/turbosync/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/turbosync/public/globe.svg b/examples/turbosync/public/globe.svg
new file mode 100644
index 00000000..567f17b0
--- /dev/null
+++ b/examples/turbosync/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/turbosync/public/next.svg b/examples/turbosync/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/examples/turbosync/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/turbosync/public/vercel.svg b/examples/turbosync/public/vercel.svg
new file mode 100644
index 00000000..77053960
--- /dev/null
+++ b/examples/turbosync/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/turbosync/public/window.svg b/examples/turbosync/public/window.svg
new file mode 100644
index 00000000..b2b2a44f
--- /dev/null
+++ b/examples/turbosync/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/turbosync/skills-lock.json b/examples/turbosync/skills-lock.json
new file mode 100644
index 00000000..131d21ef
--- /dev/null
+++ b/examples/turbosync/skills-lock.json
@@ -0,0 +1,50 @@
+{
+ "version": 1,
+ "skills": {
+ "cloudflare": {
+ "source": "cloudflare/skills",
+ "sourceType": "github",
+ "computedHash": "02ef72b4906d295d03da15d52357babdef06a762f0374b85c89c73c9b22a7bad"
+ },
+ "durable-objects": {
+ "source": "cloudflare/skills",
+ "sourceType": "github",
+ "computedHash": "72efa2277fea08f43b393c74abaacc9fa575e20bc63bdc9d46829b8a39352852"
+ },
+ "find-skills": {
+ "source": "vercel-labs/skills",
+ "sourceType": "github",
+ "computedHash": "6412eb4eb3b91595ebab937f0c69501240e7ba761b3d82510b5cf506ec5c7adc"
+ },
+ "migrate-to-vinext": {
+ "source": "cloudflare/vinext",
+ "sourceType": "github",
+ "computedHash": "b14b593094e779e0e32d1ce0175be9576366476507f7dde0c30bfeeb31e0e74e"
+ },
+ "vercel-composition-patterns": {
+ "source": "vercel-labs/agent-skills",
+ "sourceType": "github",
+ "computedHash": "f98931159fa9c7fed043bcd18a891a46dcf89ababa38df13a4c5b7b30dc0ce07"
+ },
+ "vercel-react-best-practices": {
+ "source": "vercel-labs/agent-skills",
+ "sourceType": "github",
+ "computedHash": "3462ec83f862abb2d532953df33a4dbf87f4616849da5d4b5cc7c13601aaf997"
+ },
+ "web-design-guidelines": {
+ "source": "vercel-labs/agent-skills",
+ "sourceType": "github",
+ "computedHash": "a6a44d5498f7e8f68289902f3dedfc6f38ae0cee1e96527c80724cf27f727c2a"
+ },
+ "workers-best-practices": {
+ "source": "cloudflare/skills",
+ "sourceType": "github",
+ "computedHash": "420e1ad3eaeb8ed2016e6b34fff3757de6aa39899ec2bd6e9784a29c38e70f9a"
+ },
+ "wrangler": {
+ "source": "cloudflare/skills",
+ "sourceType": "github",
+ "computedHash": "cd0ac8f97a2f54d19166acfd380fca8236db5e9f37d3b2066f713967e2673d36"
+ }
+ }
+}
diff --git a/examples/turbosync/tsconfig.json b/examples/turbosync/tsconfig.json
new file mode 100644
index 00000000..f7b3ea78
--- /dev/null
+++ b/examples/turbosync/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts",
+ "worker-configuration.d.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/examples/turbosync/types/index.ts b/examples/turbosync/types/index.ts
new file mode 100644
index 00000000..bc43bd56
--- /dev/null
+++ b/examples/turbosync/types/index.ts
@@ -0,0 +1,113 @@
+// ─── Permissions ───────────────────────────────────────────────────
+export interface RoomPermissions {
+ /** Whether viewers can play/pause/seek (default: true) */
+ viewersCanControl: boolean;
+ /** Whether viewers can send chat messages (default: true) */
+ viewersCanChat: boolean;
+}
+
+// ─── User ──────────────────────────────────────────────────────────
+export interface User {
+ /** Unique session ID (generated on connect) */
+ id: string;
+ /** Display name shown in the room */
+ displayName: string;
+ /** Optional avatar URL or gravatar hash */
+ avatar?: string;
+ /** Whether this user created the room */
+ isHost: boolean;
+ /** Persistent device/browser ID (generated by client) */
+ peerId?: string;
+ /** Connection status */
+ connectionStatus?: "online" | "away";
+ /** User's current local video timestamp (seconds) */
+ videoTimestamp?: number;
+}
+
+// ─── Room ──────────────────────────────────────────────────────────
+export interface RoomState {
+ /** Deterministic room name (used as DO ID) */
+ id: string;
+ /** Human-readable room name */
+ name: string;
+ /** Connected users */
+ users: User[];
+ /** Current playback timestamp in seconds */
+ currentTime: number;
+ /** Whether playback is paused */
+ paused: boolean;
+ /** Playback rate (1 = normal) */
+ playbackRate: number;
+ /** Room permissions */
+ permissions: RoomPermissions;
+}
+
+// ─── WebSocket: Client → Server ────────────────────────────────────
+
+export type WSClientMessage =
+ | {
+ type: "join";
+ user: Omit & { peerId: string };
+ password?: string;
+ }
+ | { type: "leave" }
+ | { type: "play"; currentTime: number }
+ | { type: "pause"; currentTime: number }
+ | { type: "seek"; currentTime: number }
+ | { type: "chat"; message: string }
+ | { type: "sync-request" }
+ | { type: "playback-rate"; rate: number }
+ | { type: "time-update"; currentTime: number }
+ | { type: "kick"; userId: string }
+ | {
+ type: "update-permissions";
+ permissions: Partial;
+ };
+
+// ─── WebSocket: Server → Client ────────────────────────────────────
+
+export type WSServerMessage =
+ | { type: "user-joined"; user: User }
+ | { type: "user-left"; userId: string }
+ | { type: "play"; currentTime: number; userId: string }
+ | { type: "pause"; currentTime: number; userId: string }
+ | { type: "seek"; currentTime: number; userId: string }
+ | { type: "chat"; message: string; userId: string; displayName: string }
+ | { type: "sync"; room: RoomState }
+ | { type: "error"; code: string; message: string }
+ | { type: "room-state"; room: RoomState; yourUserId: string }
+ | { type: "playback-rate"; rate: number; userId: string }
+ | { type: "kicked"; reason: string }
+ | { type: "permissions-updated"; permissions: RoomPermissions }
+ | { type: "user-time-update"; userId: string; currentTime: number };
+
+// ─── REST API ──────────────────────────────────────────────────────
+
+export interface CreateRoomRequest {
+ name: string;
+ password?: string;
+ /** Initial host's client-generated persistent ID */
+ hostPeerId?: string;
+}
+
+export interface CreateRoomResponse {
+ roomId: string;
+ name: string;
+}
+
+export interface CreateRoomConflictResponse {
+ error: "ROOM_EXISTS";
+ message: string;
+}
+
+export interface RoomStateResponse {
+ room: RoomState;
+}
+
+export interface RoomExistsResponse {
+ exists: boolean;
+}
+
+export interface ApiErrorResponse {
+ error: string;
+}
diff --git a/examples/turbosync/vite.config.ts b/examples/turbosync/vite.config.ts
new file mode 100644
index 00000000..fb6512ee
--- /dev/null
+++ b/examples/turbosync/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "vite";
+import vinext from "vinext";
+import { cloudflare } from "@cloudflare/vite-plugin";
+
+export default defineConfig({
+ plugins: [
+ vinext(),
+ cloudflare({
+ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
+ }),
+ ],
+});
diff --git a/examples/turbosync/worker-configuration.d.ts b/examples/turbosync/worker-configuration.d.ts
new file mode 100644
index 00000000..f4623e06
--- /dev/null
+++ b/examples/turbosync/worker-configuration.d.ts
@@ -0,0 +1,11304 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types` (hash: 0a13fac9bcd7d70bb54693cb60aabb64)
+// Runtime types generated with workerd@1.20260301.1 2026-02-12 nodejs_compat
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./worker/index");
+ durableNamespaces: "Room";
+ }
+ interface Env {
+ IMAGES: ImagesBinding;
+ ROOM: DurableObjectNamespace;
+ }
+}
+interface Env extends Cloudflare.Env {}
+
+// Begin runtime types
+/*! *****************************************************************************
+Copyright (c) Cloudflare. All rights reserved.
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+/* eslint-disable */
+// noinspection JSUnusedGlobalSymbols
+declare var onmessage: never;
+/**
+ * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
+ */
+declare class DOMException extends Error {
+ constructor(message?: string, name?: string);
+ /**
+ * The **`message`** read-only property of the a message or description associated with the given error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)
+ */
+ readonly message: string;
+ /**
+ * The **`name`** read-only property of the one of the strings associated with an error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)
+ */
+ readonly name: string;
+ /**
+ * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
+ */
+ readonly code: number;
+ static readonly INDEX_SIZE_ERR: number;
+ static readonly DOMSTRING_SIZE_ERR: number;
+ static readonly HIERARCHY_REQUEST_ERR: number;
+ static readonly WRONG_DOCUMENT_ERR: number;
+ static readonly INVALID_CHARACTER_ERR: number;
+ static readonly NO_DATA_ALLOWED_ERR: number;
+ static readonly NO_MODIFICATION_ALLOWED_ERR: number;
+ static readonly NOT_FOUND_ERR: number;
+ static readonly NOT_SUPPORTED_ERR: number;
+ static readonly INUSE_ATTRIBUTE_ERR: number;
+ static readonly INVALID_STATE_ERR: number;
+ static readonly SYNTAX_ERR: number;
+ static readonly INVALID_MODIFICATION_ERR: number;
+ static readonly NAMESPACE_ERR: number;
+ static readonly INVALID_ACCESS_ERR: number;
+ static readonly VALIDATION_ERR: number;
+ static readonly TYPE_MISMATCH_ERR: number;
+ static readonly SECURITY_ERR: number;
+ static readonly NETWORK_ERR: number;
+ static readonly ABORT_ERR: number;
+ static readonly URL_MISMATCH_ERR: number;
+ static readonly QUOTA_EXCEEDED_ERR: number;
+ static readonly TIMEOUT_ERR: number;
+ static readonly INVALID_NODE_TYPE_ERR: number;
+ static readonly DATA_CLONE_ERR: number;
+ get stack(): any;
+ set stack(value: any);
+}
+type WorkerGlobalScopeEventMap = {
+ fetch: FetchEvent;
+ scheduled: ScheduledEvent;
+ queue: QueueEvent;
+ unhandledrejection: PromiseRejectionEvent;
+ rejectionhandled: PromiseRejectionEvent;
+};
+declare abstract class WorkerGlobalScope extends EventTarget {
+ EventTarget: typeof EventTarget;
+}
+/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *
+ * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)
+ */
+interface Console {
+ "assert"(condition?: boolean, ...data: any[]): void;
+ /**
+ * The **`console.clear()`** static method clears the console if possible.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)
+ */
+ clear(): void;
+ /**
+ * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)
+ */
+ count(label?: string): void;
+ /**
+ * The **`console.countReset()`** static method resets counter used with console/count_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)
+ */
+ countReset(label?: string): void;
+ /**
+ * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)
+ */
+ debug(...data: any[]): void;
+ /**
+ * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)
+ */
+ dir(item?: any, options?: any): void;
+ /**
+ * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)
+ */
+ dirxml(...data: any[]): void;
+ /**
+ * The **`console.error()`** static method outputs a message to the console at the 'error' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)
+ */
+ error(...data: any[]): void;
+ /**
+ * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)
+ */
+ group(...data: any[]): void;
+ /**
+ * The **`console.groupCollapsed()`** static method creates a new inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)
+ */
+ groupCollapsed(...data: any[]): void;
+ /**
+ * The **`console.groupEnd()`** static method exits the current inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)
+ */
+ groupEnd(): void;
+ /**
+ * The **`console.info()`** static method outputs a message to the console at the 'info' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)
+ */
+ info(...data: any[]): void;
+ /**
+ * The **`console.log()`** static method outputs a message to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
+ */
+ log(...data: any[]): void;
+ /**
+ * The **`console.table()`** static method displays tabular data as a table.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)
+ */
+ table(tabularData?: any, properties?: string[]): void;
+ /**
+ * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)
+ */
+ time(label?: string): void;
+ /**
+ * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)
+ */
+ timeEnd(label?: string): void;
+ /**
+ * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)
+ */
+ timeLog(label?: string, ...data: any[]): void;
+ timeStamp(label?: string): void;
+ /**
+ * The **`console.trace()`** static method outputs a stack trace to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)
+ */
+ trace(...data: any[]): void;
+ /**
+ * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)
+ */
+ warn(...data: any[]): void;
+}
+declare const console: Console;
+type BufferSource = ArrayBufferView | ArrayBuffer;
+type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
+declare namespace WebAssembly {
+ class CompileError extends Error {
+ constructor(message?: string);
+ }
+ class RuntimeError extends Error {
+ constructor(message?: string);
+ }
+ type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128";
+ interface GlobalDescriptor {
+ value: ValueType;
+ mutable?: boolean;
+ }
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+ value: any;
+ valueOf(): any;
+ }
+ type ImportValue = ExportValue | number;
+ type ModuleImports = Record;
+ type Imports = Record;
+ type ExportValue = Function | Global | Memory | Table;
+ type Exports = Record;
+ class Instance {
+ constructor(module: Module, imports?: Imports);
+ readonly exports: Exports;
+ }
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ shared?: boolean;
+ }
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+ readonly buffer: ArrayBuffer;
+ grow(delta: number): number;
+ }
+ type ImportExportKind = "function" | "global" | "memory" | "table";
+ interface ModuleExportDescriptor {
+ kind: ImportExportKind;
+ name: string;
+ }
+ interface ModuleImportDescriptor {
+ kind: ImportExportKind;
+ module: string;
+ name: string;
+ }
+ abstract class Module {
+ static customSections(module: Module, sectionName: string): ArrayBuffer[];
+ static exports(module: Module): ModuleExportDescriptor[];
+ static imports(module: Module): ModuleImportDescriptor[];
+ }
+ type TableKind = "anyfunc" | "externref";
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+ class Table {
+ constructor(descriptor: TableDescriptor, value?: any);
+ readonly length: number;
+ get(index: number): any;
+ grow(delta: number, value?: any): number;
+ set(index: number, value?: any): void;
+ }
+ function instantiate(module: Module, imports?: Imports): Promise;
+ function validate(bytes: BufferSource): boolean;
+}
+/**
+ * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
+ */
+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
+ DOMException: typeof DOMException;
+ WorkerGlobalScope: typeof WorkerGlobalScope;
+ btoa(data: string): string;
+ atob(data: string): string;
+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+ setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearTimeout(timeoutId: number | null): void;
+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+ setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearInterval(timeoutId: number | null): void;
+ queueMicrotask(task: Function): void;
+ structuredClone(value: T, options?: StructuredSerializeOptions): T;
+ reportError(error: any): void;
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+ self: ServiceWorkerGlobalScope;
+ crypto: Crypto;
+ caches: CacheStorage;
+ scheduler: Scheduler;
+ performance: Performance;
+ Cloudflare: Cloudflare;
+ readonly origin: string;
+ Event: typeof Event;
+ ExtendableEvent: typeof ExtendableEvent;
+ CustomEvent: typeof CustomEvent;
+ PromiseRejectionEvent: typeof PromiseRejectionEvent;
+ FetchEvent: typeof FetchEvent;
+ TailEvent: typeof TailEvent;
+ TraceEvent: typeof TailEvent;
+ ScheduledEvent: typeof ScheduledEvent;
+ MessageEvent: typeof MessageEvent;
+ CloseEvent: typeof CloseEvent;
+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;
+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;
+ ReadableStream: typeof ReadableStream;
+ WritableStream: typeof WritableStream;
+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;
+ TransformStream: typeof TransformStream;
+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
+ CountQueuingStrategy: typeof CountQueuingStrategy;
+ ErrorEvent: typeof ErrorEvent;
+ MessageChannel: typeof MessageChannel;
+ MessagePort: typeof MessagePort;
+ EventSource: typeof EventSource;
+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
+ ReadableByteStreamController: typeof ReadableByteStreamController;
+ WritableStreamDefaultController: typeof WritableStreamDefaultController;
+ TransformStreamDefaultController: typeof TransformStreamDefaultController;
+ CompressionStream: typeof CompressionStream;
+ DecompressionStream: typeof DecompressionStream;
+ TextEncoderStream: typeof TextEncoderStream;
+ TextDecoderStream: typeof TextDecoderStream;
+ Headers: typeof Headers;
+ Body: typeof Body;
+ Request: typeof Request;
+ Response: typeof Response;
+ WebSocket: typeof WebSocket;
+ WebSocketPair: typeof WebSocketPair;
+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;
+ AbortController: typeof AbortController;
+ AbortSignal: typeof AbortSignal;
+ TextDecoder: typeof TextDecoder;
+ TextEncoder: typeof TextEncoder;
+ navigator: Navigator;
+ Navigator: typeof Navigator;
+ URL: typeof URL;
+ URLSearchParams: typeof URLSearchParams;
+ URLPattern: typeof URLPattern;
+ Blob: typeof Blob;
+ File: typeof File;
+ FormData: typeof FormData;
+ Crypto: typeof Crypto;
+ SubtleCrypto: typeof SubtleCrypto;
+ CryptoKey: typeof CryptoKey;
+ CacheStorage: typeof CacheStorage;
+ Cache: typeof Cache;
+ FixedLengthStream: typeof FixedLengthStream;
+ IdentityTransformStream: typeof IdentityTransformStream;
+ HTMLRewriter: typeof HTMLRewriter;
+}
+declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void;
+declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void;
+/**
+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
+declare function btoa(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
+declare function atob(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */
+declare function clearTimeout(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */
+declare function clearInterval(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */
+declare function queueMicrotask(task: Function): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */
+declare function structuredClone(value: T, options?: StructuredSerializeOptions): T;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */
+declare function reportError(error: any): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */
+declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+declare const self: ServiceWorkerGlobalScope;
+/**
+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+* The Workers runtime implements the full surface of this API, but with some differences in
+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+* compared to those implemented in most browsers.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+*/
+declare const crypto: Crypto;
+/**
+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+*/
+declare const caches: CacheStorage;
+declare const scheduler: Scheduler;
+/**
+* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+* as well as timing of subrequests and other operations.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+*/
+declare const performance: Performance;
+declare const Cloudflare: Cloudflare;
+declare const origin: string;
+declare const navigator: Navigator;
+interface TestController {
+}
+interface ExecutionContext {
+ waitUntil(promise: Promise): void;
+ passThroughOnException(): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+}
+type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise;
+type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise;
+type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise;
+interface ExportedHandler {
+ fetch?: ExportedHandlerFetchHandler;
+ tail?: ExportedHandlerTailHandler;
+ trace?: ExportedHandlerTraceHandler;
+ tailStream?: ExportedHandlerTailStreamHandler;
+ scheduled?: ExportedHandlerScheduledHandler;
+ test?: ExportedHandlerTestHandler;
+ email?: EmailExportedHandler;
+ queue?: ExportedHandlerQueueHandler;
+}
+interface StructuredSerializeOptions {
+ transfer?: any[];
+}
+declare abstract class Navigator {
+ sendBeacon(url: string, body?: BodyInit): boolean;
+ readonly userAgent: string;
+ readonly hardwareConcurrency: number;
+ readonly language: string;
+ readonly languages: string[];
+}
+interface AlarmInvocationInfo {
+ readonly isRetry: boolean;
+ readonly retryCount: number;
+}
+interface Cloudflare {
+ readonly compatibilityFlags: Record;
+}
+interface DurableObject {
+ fetch(request: Request): Response | Promise;
+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise;
+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise;
+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise;
+ webSocketError?(ws: WebSocket, error: unknown): void | Promise;
+}
+type DurableObjectStub = Fetcher & {
+ readonly id: DurableObjectId;
+ readonly name?: string;
+};
+interface DurableObjectId {
+ toString(): string;
+ equals(other: DurableObjectId): boolean;
+ readonly name?: string;
+}
+declare abstract class DurableObjectNamespace {
+ newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId;
+ idFromName(name: string): DurableObjectId;
+ idFromString(id: string): DurableObjectId;
+ get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace;
+}
+type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high";
+interface DurableObjectNamespaceNewUniqueIdOptions {
+ jurisdiction?: DurableObjectJurisdiction;
+}
+type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me";
+type DurableObjectRoutingMode = "primary-only";
+interface DurableObjectNamespaceGetDurableObjectOptions {
+ locationHint?: DurableObjectLocationHint;
+ routingMode?: DurableObjectRoutingMode;
+}
+interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {
+}
+interface DurableObjectState {
+ waitUntil(promise: Promise): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+ readonly id: DurableObjectId;
+ readonly storage: DurableObjectStorage;
+ container?: Container;
+ blockConcurrencyWhile(callback: () => Promise): Promise;
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
+ getWebSockets(tag?: string): WebSocket[];
+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;
+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;
+ getHibernatableWebSocketEventTimeout(): number | null;
+ getTags(ws: WebSocket): string[];
+ abort(reason?: string): void;
+}
+interface DurableObjectTransaction {
+ get(key: string, options?: DurableObjectGetOptions): Promise;
+ get(keys: string[], options?: DurableObjectGetOptions): Promise>;
+ list(options?: DurableObjectListOptions): Promise>;
+ put(key: string, value: T, options?: DurableObjectPutOptions): Promise;
+ put(entries: Record, options?: DurableObjectPutOptions): Promise;
+ delete(key: string, options?: DurableObjectPutOptions): Promise;
+ delete(keys: string[], options?: DurableObjectPutOptions): Promise;
+ rollback(): void;
+ getAlarm(options?: DurableObjectGetAlarmOptions): Promise;
+ setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise;
+ deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise;
+}
+interface DurableObjectStorage {
+ get(key: string, options?: DurableObjectGetOptions): Promise;
+ get(keys: string[], options?: DurableObjectGetOptions): Promise>;
+ list(options?: DurableObjectListOptions): Promise>;
+ put(key: string, value: T, options?: DurableObjectPutOptions): Promise;
+ put(entries: Record, options?: DurableObjectPutOptions): Promise;
+ delete(key: string, options?: DurableObjectPutOptions): Promise;
+ delete(keys: string[], options?: DurableObjectPutOptions): Promise;
+ deleteAll(options?: DurableObjectPutOptions): Promise;
+ transaction(closure: (txn: DurableObjectTransaction) => Promise): Promise;
+ getAlarm(options?: DurableObjectGetAlarmOptions): Promise;
+ setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise;
+ deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise;
+ sync(): Promise;
+ sql: SqlStorage;
+ kv: SyncKvStorage;
+ transactionSync(closure: () => T): T;
+ getCurrentBookmark(): Promise;
+ getBookmarkForTime(timestamp: number | Date): Promise;
+ onNextSessionRestoreBookmark(bookmark: string): Promise;
+}
+interface DurableObjectListOptions {
+ start?: string;
+ startAfter?: string;
+ end?: string;
+ prefix?: string;
+ reverse?: boolean;
+ limit?: number;
+ allowConcurrency?: boolean;
+ noCache?: boolean;
+}
+interface DurableObjectGetOptions {
+ allowConcurrency?: boolean;
+ noCache?: boolean;
+}
+interface DurableObjectGetAlarmOptions {
+ allowConcurrency?: boolean;
+}
+interface DurableObjectPutOptions {
+ allowConcurrency?: boolean;
+ allowUnconfirmed?: boolean;
+ noCache?: boolean;
+}
+interface DurableObjectSetAlarmOptions {
+ allowConcurrency?: boolean;
+ allowUnconfirmed?: boolean;
+}
+declare class WebSocketRequestResponsePair {
+ constructor(request: string, response: string);
+ get request(): string;
+ get response(): string;
+}
+interface AnalyticsEngineDataset {
+ writeDataPoint(event?: AnalyticsEngineDataPoint): void;
+}
+interface AnalyticsEngineDataPoint {
+ indexes?: ((ArrayBuffer | string) | null)[];
+ doubles?: number[];
+ blobs?: ((ArrayBuffer | string) | null)[];
+}
+/**
+ * The **`Event`** interface represents an event which takes place on an `EventTarget`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event)
+ */
+declare class Event {
+ constructor(type: string, init?: EventInit);
+ /**
+ * The **`type`** read-only property of the Event interface returns a string containing the event's type.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type)
+ */
+ get type(): string;
+ /**
+ * The **`eventPhase`** read-only property of the being evaluated.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase)
+ */
+ get eventPhase(): number;
+ /**
+ * The read-only **`composed`** property of the or not the event will propagate across the shadow DOM boundary into the standard DOM.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed)
+ */
+ get composed(): boolean;
+ /**
+ * The **`bubbles`** read-only property of the Event interface indicates whether the event bubbles up through the DOM tree or not.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles)
+ */
+ get bubbles(): boolean;
+ /**
+ * The **`cancelable`** read-only property of the Event interface indicates whether the event can be canceled, and therefore prevented as if the event never happened.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable)
+ */
+ get cancelable(): boolean;
+ /**
+ * The **`defaultPrevented`** read-only property of the Event interface returns a boolean value indicating whether or not the call to Event.preventDefault() canceled the event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented)
+ */
+ get defaultPrevented(): boolean;
+ /**
+ * The Event property **`returnValue`** indicates whether the default action for this event has been prevented or not.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue)
+ */
+ get returnValue(): boolean;
+ /**
+ * The **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)
+ */
+ get currentTarget(): EventTarget | undefined;
+ /**
+ * The read-only **`target`** property of the dispatched.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target)
+ */
+ get target(): EventTarget | undefined;
+ /**
+ * The deprecated **`Event.srcElement`** is an alias for the Event.target property.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement)
+ */
+ get srcElement(): EventTarget | undefined;
+ /**
+ * The **`timeStamp`** read-only property of the Event interface returns the time (in milliseconds) at which the event was created.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp)
+ */
+ get timeStamp(): number;
+ /**
+ * The **`isTrusted`** read-only property of the when the event was generated by the user agent (including via user actions and programmatic methods such as HTMLElement.focus()), and `false` when the event was dispatched via The only exception is the `click` event, which initializes the `isTrusted` property to `false` in user agents.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted)
+ */
+ get isTrusted(): boolean;
+ /**
+ * The **`cancelBubble`** property of the Event interface is deprecated.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)
+ */
+ get cancelBubble(): boolean;
+ /**
+ * The **`cancelBubble`** property of the Event interface is deprecated.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)
+ */
+ set cancelBubble(value: boolean);
+ /**
+ * The **`stopImmediatePropagation()`** method of the If several listeners are attached to the same element for the same event type, they are called in the order in which they were added.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation)
+ */
+ stopImmediatePropagation(): void;
+ /**
+ * The **`preventDefault()`** method of the Event interface tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)
+ */
+ preventDefault(): void;
+ /**
+ * The **`stopPropagation()`** method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation)
+ */
+ stopPropagation(): void;
+ /**
+ * The **`composedPath()`** method of the Event interface returns the event's path which is an array of the objects on which listeners will be invoked.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath)
+ */
+ composedPath(): EventTarget[];
+ static readonly NONE: number;
+ static readonly CAPTURING_PHASE: number;
+ static readonly AT_TARGET: number;
+ static readonly BUBBLING_PHASE: number;
+}
+interface EventInit {
+ bubbles?: boolean;
+ cancelable?: boolean;
+ composed?: boolean;
+}
+type EventListener = (event: EventType) => void;
+interface EventListenerObject {
+ handleEvent(event: EventType): void;
+}
+type EventListenerOrEventListenerObject = EventListener | EventListenerObject;
+/**
+ * The **`EventTarget`** interface is implemented by objects that can receive events and may have listeners for them.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget)
+ */
+declare class EventTarget = Record> {
+ constructor();
+ /**
+ * The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
+ */
+ addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void;
+ /**
+ * The **`removeEventListener()`** method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)
+ */
+ removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void;
+ /**
+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+ dispatchEvent(event: EventMap[keyof EventMap]): boolean;
+}
+interface EventTargetEventListenerOptions {
+ capture?: boolean;
+}
+interface EventTargetAddEventListenerOptions {
+ capture?: boolean;
+ passive?: boolean;
+ once?: boolean;
+ signal?: AbortSignal;
+}
+interface EventTargetHandlerObject {
+ handleEvent: (event: Event) => any | undefined;
+}
+/**
+ * The **`AbortController`** interface represents a controller object that allows you to abort one or more Web requests as and when desired.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController)
+ */
+declare class AbortController {
+ constructor();
+ /**
+ * The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)
+ */
+ get signal(): AbortSignal;
+ /**
+ * The **`abort()`** method of the AbortController interface aborts an asynchronous operation before it has completed.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)
+ */
+ abort(reason?: any): void;
+}
+/**
+ * The **`AbortSignal`** interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal)
+ */
+declare abstract class AbortSignal extends EventTarget {
+ /**
+ * The **`AbortSignal.abort()`** static method returns an AbortSignal that is already set as aborted (and which does not trigger an AbortSignal/abort_event event).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static)
+ */
+ static abort(reason?: any): AbortSignal;
+ /**
+ * The **`AbortSignal.timeout()`** static method returns an AbortSignal that will automatically abort after a specified time.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static)
+ */
+ static timeout(delay: number): AbortSignal;
+ /**
+ * The **`AbortSignal.any()`** static method takes an iterable of abort signals and returns an AbortSignal.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static)
+ */
+ static any(signals: AbortSignal[]): AbortSignal;
+ /**
+ * The **`aborted`** read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (`true`) or not (`false`).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted)
+ */
+ get aborted(): boolean;
+ /**
+ * The **`reason`** read-only property returns a JavaScript value that indicates the abort reason.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason)
+ */
+ get reason(): any;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */
+ get onabort(): any | null;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */
+ set onabort(value: any | null);
+ /**
+ * The **`throwIfAborted()`** method throws the signal's abort AbortSignal.reason if the signal has been aborted; otherwise it does nothing.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted)
+ */
+ throwIfAborted(): void;
+}
+interface Scheduler {
+ wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise;
+}
+interface SchedulerWaitOptions {
+ signal?: AbortSignal;
+}
+/**
+ * The **`ExtendableEvent`** interface extends the lifetime of the `install` and `activate` events dispatched on the global scope as part of the service worker lifecycle.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent)
+ */
+declare abstract class ExtendableEvent extends Event {
+ /**
+ * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil)
+ */
+ waitUntil(promise: Promise): void;
+}
+/**
+ * The **`CustomEvent`** interface represents events initialized by an application for any purpose.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent)
+ */
+declare class CustomEvent extends Event {
+ constructor(type: string, init?: CustomEventCustomEventInit);
+ /**
+ * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail)
+ */
+ get detail(): T;
+}
+interface CustomEventCustomEventInit {
+ bubbles?: boolean;
+ cancelable?: boolean;
+ composed?: boolean;
+ detail?: any;
+}
+/**
+ * The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob)
+ */
+declare class Blob {
+ constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions);
+ /**
+ * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size)
+ */
+ get size(): number;
+ /**
+ * The **`type`** read-only property of the Blob interface returns the MIME type of the file.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type)
+ */
+ get type(): string;
+ /**
+ * The **`slice()`** method of the Blob interface creates and returns a new `Blob` object which contains data from a subset of the blob on which it's called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice)
+ */
+ slice(start?: number, end?: number, type?: string): Blob;
+ /**
+ * The **`arrayBuffer()`** method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer)
+ */
+ arrayBuffer(): Promise;
+ /**
+ * The **`bytes()`** method of the Blob interface returns a Promise that resolves with a Uint8Array containing the contents of the blob as an array of bytes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes)
+ */
+ bytes(): Promise;
+ /**
+ * The **`text()`** method of the string containing the contents of the blob, interpreted as UTF-8.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text)
+ */
+ text(): Promise;
+ /**
+ * The **`stream()`** method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the `Blob`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream)
+ */
+ stream(): ReadableStream;
+}
+interface BlobOptions {
+ type?: string;
+}
+/**
+ * The **`File`** interface provides information about files and allows JavaScript in a web page to access their content.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File)
+ */
+declare class File extends Blob {
+ constructor(bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, name: string, options?: FileOptions);
+ /**
+ * The **`name`** read-only property of the File interface returns the name of the file represented by a File object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name)
+ */
+ get name(): string;
+ /**
+ * The **`lastModified`** read-only property of the File interface provides the last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified)
+ */
+ get lastModified(): number;
+}
+interface FileOptions {
+ type?: string;
+ lastModified?: number;
+}
+/**
+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+*/
+declare abstract class CacheStorage {
+ /**
+ * The **`open()`** method of the the Cache object matching the `cacheName`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open)
+ */
+ open(cacheName: string): Promise;
+ readonly default: Cache;
+}
+/**
+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+*/
+declare abstract class Cache {
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */
+ delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise;
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */
+ match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise;
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */
+ put(request: RequestInfo | URL, response: Response): Promise;
+}
+interface CacheQueryOptions {
+ ignoreMethod?: boolean;
+}
+/**
+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+* The Workers runtime implements the full surface of this API, but with some differences in
+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+* compared to those implemented in most browsers.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+*/
+declare abstract class Crypto {
+ /**
+ * The **`Crypto.subtle`** read-only property returns a cryptographic operations.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle)
+ */
+ get subtle(): SubtleCrypto;
+ /**
+ * The **`Crypto.getRandomValues()`** method lets you get cryptographically strong random values.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)
+ */
+ getRandomValues(buffer: T): T;
+ /**
+ * The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)
+ */
+ randomUUID(): string;
+ DigestStream: typeof DigestStream;
+}
+/**
+ * The **`SubtleCrypto`** interface of the Web Crypto API provides a number of low-level cryptographic functions.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto)
+ */
+declare abstract class SubtleCrypto {
+ /**
+ * The **`encrypt()`** method of the SubtleCrypto interface encrypts data.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt)
+ */
+ encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, plainText: ArrayBuffer | ArrayBufferView): Promise;
+ /**
+ * The **`decrypt()`** method of the SubtleCrypto interface decrypts some encrypted data.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt)
+ */
+ decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, cipherText: ArrayBuffer | ArrayBufferView): Promise;
+ /**
+ * The **`sign()`** method of the SubtleCrypto interface generates a digital signature.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign)
+ */
+ sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView): Promise;
+ /**
+ * The **`verify()`** method of the SubtleCrypto interface verifies a digital signature.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify)
+ */
+ verify(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, signature: ArrayBuffer | ArrayBufferView, data: ArrayBuffer | ArrayBufferView): Promise;
+ /**
+ * The **`digest()`** method of the SubtleCrypto interface generates a _digest_ of the given data, using the specified hash function.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest)
+ */
+ digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView): Promise;
+ /**
+ * The **`generateKey()`** method of the SubtleCrypto interface is used to generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey)
+ */
+ generateKey(algorithm: string | SubtleCryptoGenerateKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise;
+ /**
+ * The **`deriveKey()`** method of the SubtleCrypto interface can be used to derive a secret key from a master key.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey)
+ */
+ deriveKey(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise;
+ /**
+ * The **`deriveBits()`** method of the key.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits)
+ */
+ deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, length?: number | null): Promise;
+ /**
+ * The **`importKey()`** method of the SubtleCrypto interface imports a key: that is, it takes as input a key in an external, portable format and gives you a CryptoKey object that you can use in the Web Crypto API.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey)
+ */
+ importKey(format: string, keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, algorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise;
+ /**
+ * The **`exportKey()`** method of the SubtleCrypto interface exports a key: that is, it takes as input a CryptoKey object and gives you the key in an external, portable format.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey)
+ */
+ exportKey(format: string, key: CryptoKey): Promise;
+ /**
+ * The **`wrapKey()`** method of the SubtleCrypto interface 'wraps' a key.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey)
+ */
+ wrapKey(format: string, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm): Promise;
+ /**
+ * The **`unwrapKey()`** method of the SubtleCrypto interface 'unwraps' a key.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey)
+ */
+ unwrapKey(format: string, wrappedKey: ArrayBuffer | ArrayBufferView, unwrappingKey: CryptoKey, unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise;
+ timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean;
+}
+/**
+ * The **`CryptoKey`** interface of the Web Crypto API represents a cryptographic key obtained from one of the SubtleCrypto methods SubtleCrypto.generateKey, SubtleCrypto.deriveKey, SubtleCrypto.importKey, or SubtleCrypto.unwrapKey.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey)
+ */
+declare abstract class CryptoKey {
+ /**
+ * The read-only **`type`** property of the CryptoKey interface indicates which kind of key is represented by the object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type)
+ */
+ readonly type: string;
+ /**
+ * The read-only **`extractable`** property of the CryptoKey interface indicates whether or not the key may be extracted using `SubtleCrypto.exportKey()` or `SubtleCrypto.wrapKey()`.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable)
+ */
+ readonly extractable: boolean;
+ /**
+ * The read-only **`algorithm`** property of the CryptoKey interface returns an object describing the algorithm for which this key can be used, and any associated extra parameters.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm)
+ */
+ readonly algorithm: CryptoKeyKeyAlgorithm | CryptoKeyAesKeyAlgorithm | CryptoKeyHmacKeyAlgorithm | CryptoKeyRsaKeyAlgorithm | CryptoKeyEllipticKeyAlgorithm | CryptoKeyArbitraryKeyAlgorithm;
+ /**
+ * The read-only **`usages`** property of the CryptoKey interface indicates what can be done with the key.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages)
+ */
+ readonly usages: string[];
+}
+interface CryptoKeyPair {
+ publicKey: CryptoKey;
+ privateKey: CryptoKey;
+}
+interface JsonWebKey {
+ kty: string;
+ use?: string;
+ key_ops?: string[];
+ alg?: string;
+ ext?: boolean;
+ crv?: string;
+ x?: string;
+ y?: string;
+ d?: string;
+ n?: string;
+ e?: string;
+ p?: string;
+ q?: string;
+ dp?: string;
+ dq?: string;
+ qi?: string;
+ oth?: RsaOtherPrimesInfo[];
+ k?: string;
+}
+interface RsaOtherPrimesInfo {
+ r?: string;
+ d?: string;
+ t?: string;
+}
+interface SubtleCryptoDeriveKeyAlgorithm {
+ name: string;
+ salt?: (ArrayBuffer | ArrayBufferView);
+ iterations?: number;
+ hash?: (string | SubtleCryptoHashAlgorithm);
+ $public?: CryptoKey;
+ info?: (ArrayBuffer | ArrayBufferView);
+}
+interface SubtleCryptoEncryptAlgorithm {
+ name: string;
+ iv?: (ArrayBuffer | ArrayBufferView);
+ additionalData?: (ArrayBuffer | ArrayBufferView);
+ tagLength?: number;
+ counter?: (ArrayBuffer | ArrayBufferView);
+ length?: number;
+ label?: (ArrayBuffer | ArrayBufferView);
+}
+interface SubtleCryptoGenerateKeyAlgorithm {
+ name: string;
+ hash?: (string | SubtleCryptoHashAlgorithm);
+ modulusLength?: number;
+ publicExponent?: (ArrayBuffer | ArrayBufferView);
+ length?: number;
+ namedCurve?: string;
+}
+interface SubtleCryptoHashAlgorithm {
+ name: string;
+}
+interface SubtleCryptoImportKeyAlgorithm {
+ name: string;
+ hash?: (string | SubtleCryptoHashAlgorithm);
+ length?: number;
+ namedCurve?: string;
+ compressed?: boolean;
+}
+interface SubtleCryptoSignAlgorithm {
+ name: string;
+ hash?: (string | SubtleCryptoHashAlgorithm);
+ dataLength?: number;
+ saltLength?: number;
+}
+interface CryptoKeyKeyAlgorithm {
+ name: string;
+}
+interface CryptoKeyAesKeyAlgorithm {
+ name: string;
+ length: number;
+}
+interface CryptoKeyHmacKeyAlgorithm {
+ name: string;
+ hash: CryptoKeyKeyAlgorithm;
+ length: number;
+}
+interface CryptoKeyRsaKeyAlgorithm {
+ name: string;
+ modulusLength: number;
+ publicExponent: ArrayBuffer | ArrayBufferView;
+ hash?: CryptoKeyKeyAlgorithm;
+}
+interface CryptoKeyEllipticKeyAlgorithm {
+ name: string;
+ namedCurve: string;
+}
+interface CryptoKeyArbitraryKeyAlgorithm {
+ name: string;
+ hash?: CryptoKeyKeyAlgorithm;
+ namedCurve?: string;
+ length?: number;
+}
+declare class DigestStream extends WritableStream {
+ constructor(algorithm: string | SubtleCryptoHashAlgorithm);
+ readonly digest: Promise;
+ get bytesWritten(): number | bigint;
+}
+/**
+ * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder)
+ */
+declare class TextDecoder {
+ constructor(label?: string, options?: TextDecoderConstructorOptions);
+ /**
+ * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode)
+ */
+ decode(input?: (ArrayBuffer | ArrayBufferView), options?: TextDecoderDecodeOptions): string;
+ get encoding(): string;
+ get fatal(): boolean;
+ get ignoreBOM(): boolean;
+}
+/**
+ * The **`TextEncoder`** interface takes a stream of code points as input and emits a stream of UTF-8 bytes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder)
+ */
+declare class TextEncoder {
+ constructor();
+ /**
+ * The **`TextEncoder.encode()`** method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode)
+ */
+ encode(input?: string): Uint8Array;
+ /**
+ * The **`TextEncoder.encodeInto()`** method takes a string to encode and a destination Uint8Array to put resulting UTF-8 encoded text into, and returns a dictionary object indicating the progress of the encoding.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto)
+ */
+ encodeInto(input: string, buffer: Uint8Array): TextEncoderEncodeIntoResult;
+ get encoding(): string;
+}
+interface TextDecoderConstructorOptions {
+ fatal: boolean;
+ ignoreBOM: boolean;
+}
+interface TextDecoderDecodeOptions {
+ stream: boolean;
+}
+interface TextEncoderEncodeIntoResult {
+ read: number;
+ written: number;
+}
+/**
+ * The **`ErrorEvent`** interface represents events providing information related to errors in scripts or in files.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent)
+ */
+declare class ErrorEvent extends Event {
+ constructor(type: string, init?: ErrorEventErrorEventInit);
+ /**
+ * The **`filename`** read-only property of the ErrorEvent interface returns a string containing the name of the script file in which the error occurred.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename)
+ */
+ get filename(): string;
+ /**
+ * The **`message`** read-only property of the ErrorEvent interface returns a string containing a human-readable error message describing the problem.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message)
+ */
+ get message(): string;
+ /**
+ * The **`lineno`** read-only property of the ErrorEvent interface returns an integer containing the line number of the script file on which the error occurred.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno)
+ */
+ get lineno(): number;
+ /**
+ * The **`colno`** read-only property of the ErrorEvent interface returns an integer containing the column number of the script file on which the error occurred.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno)
+ */
+ get colno(): number;
+ /**
+ * The **`error`** read-only property of the ErrorEvent interface returns a JavaScript value, such as an Error or DOMException, representing the error associated with this event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error)
+ */
+ get error(): any;
+}
+interface ErrorEventErrorEventInit {
+ message?: string;
+ filename?: string;
+ lineno?: number;
+ colno?: number;
+ error?: any;
+}
+/**
+ * The **`MessageEvent`** interface represents a message received by a target object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent)
+ */
+declare class MessageEvent extends Event {
+ constructor(type: string, initializer: MessageEventInit);
+ /**
+ * The **`data`** read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data)
+ */
+ readonly data: any;
+ /**
+ * The **`origin`** read-only property of the origin of the message emitter.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin)
+ */
+ readonly origin: string | null;
+ /**
+ * The **`lastEventId`** read-only property of the unique ID for the event.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId)
+ */
+ readonly lastEventId: string;
+ /**
+ * The **`source`** read-only property of the a WindowProxy, MessagePort, or a `MessageEventSource` (which can be a WindowProxy, message emitter.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source)
+ */
+ readonly source: MessagePort | null;
+ /**
+ * The **`ports`** read-only property of the containing all MessagePort objects sent with the message, in order.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports)
+ */
+ readonly ports: MessagePort[];
+}
+interface MessageEventInit {
+ data: ArrayBuffer | string;
+}
+/**
+ * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent)
+ */
+declare abstract class PromiseRejectionEvent extends Event {
+ /**
+ * The PromiseRejectionEvent interface's **`promise`** read-only property indicates the JavaScript rejected.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise)
+ */
+ readonly promise: Promise;
+ /**
+ * The PromiseRejectionEvent **`reason`** read-only property is any JavaScript value or Object which provides the reason passed into Promise.reject().
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason)
+ */
+ readonly reason: any;
+}
+/**
+ * The **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the Window/fetch, XMLHttpRequest.send() or navigator.sendBeacon() methods.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData)
+ */
+declare class FormData {
+ constructor();
+ /**
+ * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)
+ */
+ append(name: string, value: string | Blob): void;
+ /**
+ * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)
+ */
+ append(name: string, value: string): void;
+ /**
+ * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)
+ */
+ append(name: string, value: Blob, filename?: string): void;
+ /**
+ * The **`delete()`** method of the FormData interface deletes a key and its value(s) from a `FormData` object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete)
+ */
+ delete(name: string): void;
+ /**
+ * The **`get()`** method of the FormData interface returns the first value associated with a given key from within a `FormData` object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get)
+ */
+ get(name: string): (File | string) | null;
+ /**
+ * The **`getAll()`** method of the FormData interface returns all the values associated with a given key from within a `FormData` object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll)
+ */
+ getAll(name: string): (File | string)[];
+ /**
+ * The **`has()`** method of the FormData interface returns whether a `FormData` object contains a certain key.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has)
+ */
+ has(name: string): boolean;
+ /**
+ * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)
+ */
+ set(name: string, value: string | Blob): void;
+ /**
+ * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)
+ */
+ set(name: string, value: string): void;
+ /**
+ * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)
+ */
+ set(name: string, value: Blob, filename?: string): void;
+ /* Returns an array of key, value pairs for every entry in the list. */
+ entries(): IterableIterator<[
+ key: string,
+ value: File | string
+ ]>;
+ /* Returns a list of keys in the list. */
+ keys(): IterableIterator;
+ /* Returns a list of values in the list. */
+ values(): IterableIterator<(File | string)>;
+ forEach(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void;
+ [Symbol.iterator](): IterableIterator<[
+ key: string,
+ value: File | string
+ ]>;
+}
+interface ContentOptions {
+ html?: boolean;
+}
+declare class HTMLRewriter {
+ constructor();
+ on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter;
+ onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter;
+ transform(response: Response): Response;
+}
+interface HTMLRewriterElementContentHandlers {
+ element?(element: Element): void | Promise;
+ comments?(comment: Comment): void | Promise;
+ text?(element: Text): void | Promise;
+}
+interface HTMLRewriterDocumentContentHandlers {
+ doctype?(doctype: Doctype): void | Promise;
+ comments?(comment: Comment): void | Promise;
+ text?(text: Text): void | Promise