Compare commits

...

5 Commits

Author SHA1 Message Date
Benjamin S. Leveritt
18a3650ab0 Add pnpm to lint workflow 2024-10-21 18:04:09 +01:00
Benjamin S. Leveritt
936db45abf Fix run cmd 2024-10-21 17:57:36 +01:00
Benjamin S. Leveritt
6a764b5248 Add prettier format to workspace linting rules 2024-10-21 17:55:59 +01:00
Benjamin S. Leveritt
84a1b5f893 Roll formatter over codebase 2024-10-21 17:45:51 +01:00
Benjamin S. Leveritt
4c3b0d3a27 Add root prettier config 2024-10-21 17:33:09 +01:00
49 changed files with 942 additions and 732 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# EditorConfig is awesome: https://editorconfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2

View File

@@ -13,9 +13,29 @@ jobs:
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run sherif
run: npx sherif@1.0.0
- name: Run formatter
run: pnpm format

1
.prettierrc Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,9 +0,0 @@
/** @type {import("prettier").Config} */
const config = {
trailingComma: "all",
tabWidth: 4,
semi: true,
singleQuote: false,
};
export default config;

View File

@@ -88,7 +88,7 @@ export class MusicaAccount extends Account {
profile = co.ref(Profile);
root = co.ref(MusicaAccountRoot);
/**
/**
* The account migration is run on account creation and on every log-in.
* You can use it to set up the account root and any other initial CoValues you need.
*/

View File

@@ -2,7 +2,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { createHashRouter, RouterProvider } from "react-router-dom";
import { Toaster } from "@/components/ui/toaster"
import { Toaster } from "@/components/ui/toaster";
import { useMediaPlayer } from "./5_useMediaPlayer";
import { HomePage } from "./3_HomePage";
import { InvitePage } from "./6_InvitePage";

View File

@@ -5,12 +5,16 @@ import { usePlayState } from "./lib/audio/usePlayState";
import { SidePanel } from "./components/SidePanel";
import { FileUploadButton } from "./components/FileUploadButton";
import { Button } from "./components/ui/button";
import { createNewPlaylist, updatePlaylistTitle, uploadMusicTracks } from "./4_actions";
import {
createNewPlaylist,
updatePlaylistTitle,
uploadMusicTracks,
} from "./4_actions";
import { useNavigate, useParams } from "react-router";
import { ID } from "jazz-tools";
import { Playlist } from "./1_schema";
import { createInviteLink } from "jazz-react";
import { useToast } from "@/hooks/use-toast"
import { useToast } from "@/hooks/use-toast";
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
/**
@@ -27,7 +31,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
const navigate = useNavigate();
const playState = usePlayState();
const isPlaying = playState.value === "play";
const { toast } = useToast()
const { toast } = useToast();
async function handleFileLoad(files: FileList) {
if (!me) return;
@@ -127,10 +131,15 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
}
isPlaying={
mediaPlayer.activeTrackId ===
track.id && isActivePlaylist && isPlaying
track.id &&
isActivePlaylist &&
isPlaying
}
onClick={() => {
mediaPlayer.setActiveTrack(track, playlist);
mediaPlayer.setActiveTrack(
track,
playlist,
);
}}
showAddToPlaylist={isRootPlaylist}
/>

View File

@@ -92,7 +92,8 @@ export async function addTrackToPlaylist(
) {
if (!account) return;
if (playlist.tracks?.some((t) => t?._refs.sourceTrack.id === track.id)) return;
if (playlist.tracks?.some((t) => t?._refs.sourceTrack.id === track.id))
return;
/**
* Since musicTracks are created as private values (see uploadMusicTracks)
@@ -137,10 +138,13 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
track.title = title;
}
export async function updateActivePlaylist(playlist: Playlist, me: MusicaAccount) {
export async function updateActivePlaylist(
playlist: Playlist,
me: MusicaAccount,
) {
me.root!.activePlaylist = playlist ?? me.root!.rootPlaylist;
}
export async function updateActiveTrack(track: MusicTrack, me: MusicaAccount) {
me.root!.activeTrack = track;
}
}

View File

@@ -15,7 +15,7 @@ export function useMediaPlayer() {
const [loading, setLoading] = useState<ID<MusicTrack> | null>(null);
const activeTrackId = me?.root?._refs.activeTrack?.id
const activeTrackId = me?.root?._refs.activeTrack?.id;
// Reference used to avoid out-of-order track loads
const lastLoadedTrackId = useRef<ID<MusicTrack> | null>(null);
@@ -71,10 +71,7 @@ export function useMediaPlayer() {
async function setActiveTrack(track: MusicTrack, playlist?: Playlist) {
if (!me?.root) return;
if (
activeTrackId === track.id &&
lastLoadedTrackId.current !== null
) {
if (activeTrackId === track.id && lastLoadedTrackId.current !== null) {
playState.toggle();
return;
}

View File

@@ -30,7 +30,7 @@ export function MusicTrackRow({
function handleTrackTitleChange(evt: ChangeEvent<HTMLInputElement>) {
if (!track) return;
updateMusicTrackTitle(track, evt.target.value);
}

View File

@@ -25,7 +25,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
});
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
waveform: {}
waveform: {},
});
if (!activeTrack) return null;
@@ -60,14 +60,16 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
</button>
</div>
</div>
<div className=" sm:hidden md:flex flex-col flex-shrink-1 items-center w-[75%]">
<div className=" sm:hidden md:flex flex-col flex-shrink-1 items-center w-[75%]">
<Waveform track={activeTrack} height={30} />
</div>
<div className="flex flex-col items-end gap-1 text-right min-w-fit w-[25%]">
<h4 className="font-medium text-blue-800">
{activeTrackTitle}
</h4>
<p className="text-sm text-gray-600">{activePlaylist?.title || "All tracks"}</p>
<p className="text-sm text-gray-600">
{activePlaylist?.title || "All tracks"}
</p>
</div>
</footer>
);

View File

@@ -10,9 +10,7 @@ export function SidePanel() {
},
});
function handleAllTracksClick(
evt: React.MouseEvent<HTMLAnchorElement>,
) {
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
navigate(`/`);
}

View File

@@ -1,57 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,198 +1,201 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
return (
<span
className={cn(
"ml-auto text-xs tracking-widest opacity-60",
className,
)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@@ -1,127 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
defaultVariants: {
variant: "default",
},
}
)
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@@ -1,33 +1,41 @@
import { useToast } from "@/hooks/use-toast"
import { useToast } from "@/hooks/use-toast";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast()
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
return (
<ToastProvider>
{toasts.map(function ({
id,
title,
description,
action,
...props
}) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>
{description}
</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@@ -1,191 +1,189 @@
import * as React from "react"
import * as React from "react";
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[]
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout)
}
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = []
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] }
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId()
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
return {
...state,
toast,
dismiss: (toastId?: string) =>
dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast }
export { useToast, toast };

View File

@@ -2,31 +2,34 @@ import { useLayoutEffect, useState } from "react";
import { useAudioManager } from "./AudioManager";
export function usePlayerCurrentTime() {
const audioManager = useAudioManager();
const [value, setValue] = useState<number>(0);
const audioManager = useAudioManager();
const [value, setValue] = useState<number>(0);
useLayoutEffect(() => {
setValue(audioManager.mediaElement.currentTime);
useLayoutEffect(() => {
setValue(audioManager.mediaElement.currentTime);
const onTimeUpdate = () => {
setValue(audioManager.mediaElement.currentTime);
};
const onTimeUpdate = () => {
setValue(audioManager.mediaElement.currentTime);
};
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
return () => {
audioManager.mediaElement.removeEventListener("timeupdate", onTimeUpdate);
};
}, [audioManager]);
return () => {
audioManager.mediaElement.removeEventListener(
"timeupdate",
onTimeUpdate,
);
};
}, [audioManager]);
function setCurrentTime(time: number) {
if (audioManager.mediaElement.paused) audioManager.play();
function setCurrentTime(time: number) {
if (audioManager.mediaElement.paused) audioManager.play();
audioManager.mediaElement.currentTime = time;
}
audioManager.mediaElement.currentTime = time;
}
return {
value,
setValue: setCurrentTime,
};
return {
value,
setValue: setCurrentTime,
};
}

View File

@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View File

@@ -1,6 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Link, RouterProvider, createHashRouter, useNavigate } from "react-router-dom";
import {
Link,
RouterProvider,
createHashRouter,
useNavigate,
} from "react-router-dom";
import "./index.css";
import { createJazzReactApp, DemoAuthBasicUI, useDemoAuth } from "jazz-react";
@@ -38,10 +43,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
return (
<>
<Jazz.Provider
auth={auth}
peer={peer}
>
<Jazz.Provider auth={auth} peer={peer}>
{children}
</Jazz.Provider>
<DemoAuthBasicUI appName={appName} state={authState} />
@@ -96,9 +98,7 @@ export default function App() {
<RouterProvider router={router} />
<Button
onClick={() =>
router.navigate("/").then(() => logOut())
}
onClick={() => router.navigate("/").then(() => logOut())}
variant="outline"
>
Log out
@@ -114,7 +114,7 @@ function AcceptInvite() {
onAccept: (petPostID) => navigate("/pet/" + petPostID),
});
return <p>Accepting invite...</p>
return <p>Accepting invite...</p>;
}
export function PostOverview() {

View File

@@ -6,7 +6,11 @@ import {
} from "react-router-dom";
import "./index.css";
import { createJazzReactApp, PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";
import {
createJazzReactApp,
PasskeyAuthBasicUI,
usePasskeyAuth,
} from "jazz-react";
import {
Button,
@@ -62,8 +66,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<App />
</JazzAndAuth>
</div>
</ThemeProvider>,
</React.StrictMode>
</ThemeProvider>
,
</React.StrictMode>,
);
/**
@@ -105,9 +110,7 @@ export default function App() {
<RouterProvider router={router} />
<Button
onClick={() =>
router.navigate("/").then(logOut)
}
onClick={() => router.navigate("/").then(logOut)}
variant="outline"
>
Log out

View File

@@ -76,7 +76,9 @@ function RenderPackageChild({
<div key={i} id={child.name} className="not-prose mt-4">
{
<Highlight hide={[0, 2]}>
{`function \n${printSimpleSignature(child, signature) + ":"}\n {}`}
{`function \n${
printSimpleSignature(child, signature) + ":"
}\n {}`}
</Highlight>
}{" "}
<span className="opacity-75 text-xs pl-1">
@@ -123,9 +125,9 @@ function RenderTypeAlias({
<Highlight>{`type ${child.name}`}</Highlight>
</h3>
<p className="not-prose text-sm ml-4">
<Highlight>{`type ${child.name}${typeParameters?.length && `<${typeParameters?.join(", ")}>`} = ${printType(
child.type,
)}`}</Highlight>
<Highlight>{`type ${child.name}${
typeParameters?.length && `<${typeParameters?.join(", ")}>`
} = ${printType(child.type)}`}</Highlight>
</p>
<div className="ml-4 mt-2 flex-[3]">
<DocComment>

View File

@@ -94,7 +94,12 @@ export class SQLiteStorage {
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
localNodeName,
"storage",
{ peer1role: "client", peer2role: "server", trace, crashOnClose: true },
{
peer1role: "client",
peer2role: "server",
trace,
crashOnClose: true,
},
);
await SQLiteStorage.open(
@@ -427,7 +432,9 @@ export class SQLiteStorage {
),
)
.filter(
(accountID): accountID is RawAccountID =>
(
accountID,
): accountID is RawAccountID =>
cojsonInternals.isAccountID(
accountID,
) && accountID !== theirKnown.id,

View File

@@ -4,10 +4,10 @@ import { addMessageToBacklog } from "./serialization.js";
export const MAX_OUTGOING_MESSAGES_CHUNK_BYTES = 25_000;
export class BatchedOutgoingMessages {
private backlog: string = '';
private backlog: string = "";
private timeout: ReturnType<typeof setTimeout> | null = null;
constructor(private send: (messages: string) => void) { }
constructor(private send: (messages: string) => void) {}
push(msg: SyncMessage) {
const payload = addMessageToBacklog(this.backlog, msg);
@@ -16,12 +16,13 @@ export class BatchedOutgoingMessages {
clearTimeout(this.timeout);
}
const maxChunkSizeReached = payload.length >= MAX_OUTGOING_MESSAGES_CHUNK_BYTES;
const maxChunkSizeReached =
payload.length >= MAX_OUTGOING_MESSAGES_CHUNK_BYTES;
const backlogExists = this.backlog.length > 0;
if (maxChunkSizeReached && backlogExists) {
this.sendMessagesInBulk();
this.backlog = addMessageToBacklog('', msg);
this.backlog = addMessageToBacklog("", msg);
this.timeout = setTimeout(() => {
this.sendMessagesInBulk();
}, 0);
@@ -38,7 +39,7 @@ export class BatchedOutgoingMessages {
sendMessagesInBulk() {
this.send(this.backlog);
this.backlog = '';
this.backlog = "";
}
close() {

View File

@@ -47,7 +47,11 @@ export function createWebSocketPeer({
const result = deserializeMessages(event.data as string);
if (!result.ok) {
console.error("Error while deserializing messages", event.data, result.error);
console.error(
"Error while deserializing messages",
event.data,
result.error,
);
return;
}
@@ -90,9 +94,7 @@ export function createWebSocketPeer({
const outgoingMessages = new BatchedOutgoingMessages((messages) => {
if (websocket.readyState === 1) {
websocket.send(
messages,
);
websocket.send(messages);
}
});

View File

@@ -8,11 +8,13 @@ export function addMessageToBacklog(backlog: string, message: SyncMessage) {
return `${backlog}\n${JSON.stringify(message)}`;
}
export function deserializeMessages(messages: string) {
export function deserializeMessages(messages: string) {
try {
return {
ok: true,
messages: messages.split("\n").map((msg) => JSON.parse(msg)) as SyncMessage[] | PingMsg[],
messages: messages.split("\n").map((msg) => JSON.parse(msg)) as
| SyncMessage[]
| PingMsg[],
} as const;
} catch (e) {
console.error("Error while deserializing messages", e);

View File

@@ -1,15 +1,18 @@
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
import { BatchedOutgoingMessages, MAX_OUTGOING_MESSAGES_CHUNK_BYTES } from "../BatchedOutgoingMessages.js";
import {
BatchedOutgoingMessages,
MAX_OUTGOING_MESSAGES_CHUNK_BYTES,
} from "../BatchedOutgoingMessages.js";
import { SyncMessage } from "cojson";
import { CoValueKnownState } from "cojson/src/sync.js";
beforeEach(() => {
vi.useFakeTimers();
})
});
afterEach(() => {
vi.useRealTimers();
})
});
describe("BatchedOutgoingMessages", () => {
function setup() {
@@ -20,8 +23,18 @@ describe("BatchedOutgoingMessages", () => {
test("should batch messages and send them after a timeout", () => {
const { sendMock, batchedMessages } = setup();
const message1: SyncMessage = { action: "known", id: "co_z1", header: false, sessions: {} };
const message2: SyncMessage = { action: "known", id: "co_z2", header: false, sessions: {} };
const message1: SyncMessage = {
action: "known",
id: "co_z1",
header: false,
sessions: {},
};
const message2: SyncMessage = {
action: "known",
id: "co_z2",
header: false,
sessions: {},
};
batchedMessages.push(message1);
batchedMessages.push(message2);
@@ -32,7 +45,7 @@ describe("BatchedOutgoingMessages", () => {
expect(sendMock).toHaveBeenCalledTimes(1);
expect(sendMock).toHaveBeenCalledWith(
`${JSON.stringify(message1)}\n${JSON.stringify(message2)}`
`${JSON.stringify(message1)}\n${JSON.stringify(message2)}`,
);
});
@@ -44,9 +57,8 @@ describe("BatchedOutgoingMessages", () => {
header: false,
sessions: {
// Add a large payload to exceed MAX_OUTGOING_MESSAGES_CHUNK_BYTES
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES)
} as CoValueKnownState['sessions'],
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES),
} as CoValueKnownState["sessions"],
};
batchedMessages.push(largeMessage);
@@ -57,15 +69,20 @@ describe("BatchedOutgoingMessages", () => {
test("should send accumulated messages before a large message", () => {
const { sendMock, batchedMessages } = setup();
const smallMessage: SyncMessage = { action: "known", id: "co_z_small", header: false, sessions: {} };
const smallMessage: SyncMessage = {
action: "known",
id: "co_z_small",
header: false,
sessions: {},
};
const largeMessage: SyncMessage = {
action: "known",
id: "co_z_large",
header: false,
sessions: {
// Add a large payload to exceed MAX_OUTGOING_MESSAGES_CHUNK_BYTES
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES)
} as CoValueKnownState['sessions'],
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES),
} as CoValueKnownState["sessions"],
};
batchedMessages.push(smallMessage);
@@ -74,13 +91,24 @@ describe("BatchedOutgoingMessages", () => {
vi.runAllTimers();
expect(sendMock).toHaveBeenCalledTimes(2);
expect(sendMock).toHaveBeenNthCalledWith(1, JSON.stringify(smallMessage));
expect(sendMock).toHaveBeenNthCalledWith(2, JSON.stringify(largeMessage));
expect(sendMock).toHaveBeenNthCalledWith(
1,
JSON.stringify(smallMessage),
);
expect(sendMock).toHaveBeenNthCalledWith(
2,
JSON.stringify(largeMessage),
);
});
test("should send remaining messages on close", () => {
const { sendMock, batchedMessages } = setup();
const message: SyncMessage = { action: "known", id: "co_z_test", header: false, sessions: {} };
const message: SyncMessage = {
action: "known",
id: "co_z_test",
header: false,
sessions: {},
};
batchedMessages.push(message);
expect(sendMock).not.toHaveBeenCalled();
@@ -93,13 +121,23 @@ describe("BatchedOutgoingMessages", () => {
test("should clear timeout when pushing new messages", () => {
const { sendMock, batchedMessages } = setup();
const message1: SyncMessage = { action: "known", id: "co_z1", header: false, sessions: {} };
const message2: SyncMessage = { action: "known", id: "co_z2", header: false, sessions: {} };
const message1: SyncMessage = {
action: "known",
id: "co_z1",
header: false,
sessions: {},
};
const message2: SyncMessage = {
action: "known",
id: "co_z2",
header: false,
sessions: {},
};
batchedMessages.push(message1);
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
batchedMessages.push(message2);
expect(clearTimeoutSpy).toHaveBeenCalled();
@@ -108,7 +146,7 @@ describe("BatchedOutgoingMessages", () => {
expect(sendMock).toHaveBeenCalledTimes(1);
expect(sendMock).toHaveBeenCalledWith(
`${JSON.stringify(message1)}\n${JSON.stringify(message2)}`
`${JSON.stringify(message1)}\n${JSON.stringify(message2)}`,
);
});
});
});

View File

@@ -132,12 +132,14 @@ describe("createWebSocketPeer", () => {
expect(mockWebSocket.send).toHaveBeenCalled();
});
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(message1));
expect(mockWebSocket.send).toHaveBeenCalledWith(
JSON.stringify(message1),
);
mockWebSocket.send.mockClear();
void peer.outgoing.push(message2);
await new Promise<void>((resolve) => setTimeout(resolve, 100))
await new Promise<void>((resolve) => setTimeout(resolve, 100));
expect(mockWebSocket.send).not.toHaveBeenCalled();
});
@@ -233,7 +235,10 @@ describe("createWebSocketPeer", () => {
const stream: SyncMessage[] = [];
while (serializeMessages(stream.concat(message1)).length < MAX_OUTGOING_MESSAGES_CHUNK_BYTES) {
while (
serializeMessages(stream.concat(message1)).length <
MAX_OUTGOING_MESSAGES_CHUNK_BYTES
) {
stream.push(message1);
void peer.outgoing.push(message1);
}
@@ -277,7 +282,10 @@ describe("createWebSocketPeer", () => {
const stream: SyncMessage[] = [];
while (serializeMessages(stream.concat(message1)).length < MAX_OUTGOING_MESSAGES_CHUNK_BYTES) {
while (
serializeMessages(stream.concat(message1)).length <
MAX_OUTGOING_MESSAGES_CHUNK_BYTES
) {
stream.push(message1);
void peer.outgoing.push(message1);
}
@@ -356,16 +364,12 @@ describe("createWebSocketPeer", () => {
messageHandler?.(
new MessageEvent("message", {
data: Array.from(
{ length: 5 },
() => message1,
)
data: Array.from({ length: 5 }, () => message1)
.map((msg) => JSON.stringify(msg))
.join("\n"),
}),
);
const message2: SyncMessage = {
action: "content",
id: "co_zlow",
@@ -407,7 +411,6 @@ describe("createWebSocketPeer", () => {
}),
);
const message2: SyncMessage = {
action: "content",
id: "co_zlow",

View File

@@ -1,7 +1,6 @@
export interface WebsocketEvents {
close: { code: number; reason: string; };
message: { data: unknown; };
close: { code: number; reason: string };
message: { data: unknown };
open: void;
}
@@ -15,11 +14,11 @@ export interface AnyWebSocket {
addEventListener<K extends keyof WebsocketEvents>(
type: K,
listener: (event: WebsocketEvents[K]) => void,
options?: { once: boolean; }
options?: { once: boolean },
): void;
removeEventListener<K extends keyof WebsocketEvents>(
type: K,
listener: (event: WebsocketEvents[K]) => void
listener: (event: WebsocketEvents[K]) => void,
): void;
close(): void;
send(data: string): void;

View File

@@ -44,7 +44,6 @@ export class PeerState {
this.processing = true;
let entry: QueueEntry | undefined;
while ((entry = this.queue.pull())) {
// Awaiting the push to send one message at a time

View File

@@ -27,28 +27,21 @@ export type QueueEntry = {
/**
* Since we have a fixed range of priority values (0-7) we can create a fixed array of queues.
*/
type Tuple<T, N extends number, A extends unknown[] = []> = A extends { length: N } ? A : Tuple<T, N, [...A, T]>;
type Tuple<T, N extends number, A extends unknown[] = []> = A extends {
length: N;
}
? A
: Tuple<T, N, [...A, T]>;
type QueueTuple = Tuple<QueueEntry[], 8>;
export class PriorityBasedMessageQueue {
private queues: QueueTuple = [
[],
[],
[],
[],
[],
[],
[],
[],
];
private queues: QueueTuple = [[], [], [], [], [], [], [], []];
private getQueue(priority: CoValuePriority) {
return this.queues[priority];
}
constructor(
private defaultPriority: CoValuePriority,
) {}
constructor(private defaultPriority: CoValuePriority) {}
public push(msg: SyncMessage) {
const { promise, resolve, reject } = promiseWithResolvers<void>();

View File

@@ -13,7 +13,10 @@ function createResolvablePromise<T>() {
class CoValueUnknownState {
type = "unknown" as const;
private peers: Map<PeerID, ReturnType<typeof createResolvablePromise<"available" | "unavailable">>>;
private peers: Map<
PeerID,
ReturnType<typeof createResolvablePromise<"available" | "unavailable">>
>;
private resolve: (value: "available" | "unavailable") => void;
ready: Promise<"available" | "unavailable">;
@@ -22,10 +25,15 @@ class CoValueUnknownState {
this.peers = new Map();
for (const peerId of peersIds) {
this.peers.set(peerId, createResolvablePromise<"available" | "unavailable">());
this.peers.set(
peerId,
createResolvablePromise<"available" | "unavailable">(),
);
}
const { resolve, promise } = createResolvablePromise<"available" | "unavailable">();
const { resolve, promise } = createResolvablePromise<
"available" | "unavailable"
>();
this.ready = promise;
this.resolve = resolve;
@@ -66,20 +74,22 @@ class CoValueUnknownState {
class CoValueAvailableState {
type = "available" as const;
constructor(public coValue: CoValueCore) { }
constructor(public coValue: CoValueCore) {}
}
type CoValueStateAction = {
type: "not-found";
peerId: PeerID;
} | {
type: "found";
peerId: PeerID;
coValue: CoValueCore;
};
type CoValueStateAction =
| {
type: "not-found";
peerId: PeerID;
}
| {
type: "found";
peerId: PeerID;
coValue: CoValueCore;
};
export class CoValueState {
constructor(public state: CoValueUnknownState | CoValueAvailableState) { }
constructor(public state: CoValueUnknownState | CoValueAvailableState) {}
static Unknown(peersToWaitFor: Set<PeerID>) {
return new CoValueState(new CoValueUnknownState(peersToWaitFor));

View File

@@ -5,7 +5,11 @@ import { JsonObject } from "../jsonValue.js";
import { RawBinaryCoStream, RawCoStream } from "./coStream.js";
import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
import { AgentID, isAgentID } from "../ids.js";
import { RawAccount, RawAccountID, ControlledAccountOrAgent } from "./account.js";
import {
RawAccount,
RawAccountID,
ControlledAccountOrAgent,
} from "./account.js";
import { Role } from "../permissions.js";
import { base58 } from "@scure/base";
import { CoValueUniqueness } from "../coValueCore.js";
@@ -19,7 +23,9 @@ export type GroupShape = {
[key: RawAccountID | AgentID]: Role;
[EVERYONE]?: Role;
readKey?: KeyID;
[revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`]: Sealed<KeySecret>;
[
revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`
]: Sealed<KeySecret>;
[revelationFor: `${KeyID}_for_${Everyone}`]: KeySecret;
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
KeySecret,
@@ -175,10 +181,12 @@ export class RawGroup<
const newReadKey = this.core.crypto.newRandomKeySecret();
for (const readerID of currentlyPermittedReaders) {
const reader = this.core.node.resolveAccountAgent(
readerID,
"Expected to know currently permitted reader",
)._unsafeUnwrap({ withStackTrace: true });
const reader = this.core.node
.resolveAccountAgent(
readerID,
"Expected to know currently permitted reader",
)
._unsafeUnwrap({ withStackTrace: true });
this.set(
`${newReadKey.id}_for_${readerID}`,
@@ -256,7 +264,7 @@ export class RawGroup<
init?: M["_shape"],
meta?: M["headerMeta"],
initPrivacy: "trusting" | "private" = "private",
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
): M {
const map = this.core.node
.createCoValue({
@@ -266,7 +274,7 @@ export class RawGroup<
group: this.id,
},
meta: meta || null,
...uniqueness
...uniqueness,
})
.getCurrentContent() as M;
@@ -289,7 +297,7 @@ export class RawGroup<
init?: L["_item"][],
meta?: L["headerMeta"],
initPrivacy: "trusting" | "private" = "private",
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
): L {
const list = this.core.node
.createCoValue({
@@ -299,7 +307,7 @@ export class RawGroup<
group: this.id,
},
meta: meta || null,
...uniqueness
...uniqueness,
})
.getCurrentContent() as L;
@@ -313,7 +321,10 @@ export class RawGroup<
}
/** @category 3. Value creation */
createStream<C extends RawCoStream>(meta?: C["headerMeta"], uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()): C {
createStream<C extends RawCoStream>(
meta?: C["headerMeta"],
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
): C {
return this.core.node
.createCoValue({
type: "costream",
@@ -322,7 +333,7 @@ export class RawGroup<
group: this.id,
},
meta: meta || null,
...uniqueness
...uniqueness,
})
.getCurrentContent() as C;
}
@@ -330,7 +341,7 @@ export class RawGroup<
/** @category 3. Value creation */
createBinaryStream<C extends RawBinaryCoStream>(
meta: C["headerMeta"] = { type: "binary" },
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
): C {
return this.core.node
.createCoValue({
@@ -340,7 +351,7 @@ export class RawGroup<
group: this.id,
},
meta: meta,
...uniqueness
...uniqueness,
})
.getCurrentContent() as C;
}

View File

@@ -5,27 +5,41 @@ export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
export type JsonArray = JsonValue[] | readonly JsonValue[];
export type JsonObject = { [key: string]: JsonValue | undefined };
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
type ExcludeEmpty<T> = T extends AtLeastOne<T> ? T : never;
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> &
U[keyof U];
type ExcludeEmpty<T> = T extends AtLeastOne<T> ? T : never;
export type CoJsonValue<T> = JsonValue | CoJsonObjectWithIndex<T> | CoJsonArray<T>;
export type CoJsonValue<T> =
| JsonValue
| CoJsonObjectWithIndex<T>
| CoJsonArray<T>;
export type CoJsonArray<T> = CoJsonValue<T>[] | readonly CoJsonValue<T>[];
/**
* Since we are forcing Typescript to elaborate the indexes from the given type passing
* non-object values to CoJsonObjectWithIndex will return an empty object
* E.g.
* E.g.
* CoJsonObjectWithIndex<() => void> --> {}
* CoJsonObjectWithIndex<RegExp> --> {}
*
*
* Applying the ExcludeEmpty type here to make sure we don't accept functions or non-serializable values
*/
export type CoJsonObjectWithIndex<T> = ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue1L<T[K]> | undefined }>;
export type CoJsonObjectWithIndex<T> = ExcludeEmpty<{
[K in keyof T & string]: CoJsonValue1L<T[K]> | undefined;
}>;
/**
* Manually handling the nested interface types to not get into infinite recursion issues.
*/
export type CoJsonValue1L<T> = ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue2L<T[K]> | undefined }> | JsonValue;
export type CoJsonValue2L<T> = ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue3L<T[K]> | undefined }> | JsonValue;
export type CoJsonValue3L<T> = ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue4L<T[K]> | undefined }> | JsonValue;
export type CoJsonValue4L<T> = ExcludeEmpty<{ [K in keyof T & string]: JsonValue | undefined }> | JsonValue;
export type CoJsonValue1L<T> =
| ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue2L<T[K]> | undefined }>
| JsonValue;
export type CoJsonValue2L<T> =
| ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue3L<T[K]> | undefined }>
| JsonValue;
export type CoJsonValue3L<T> =
| ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue4L<T[K]> | undefined }>
| JsonValue;
export type CoJsonValue4L<T> =
| ExcludeEmpty<{ [K in keyof T & string]: JsonValue | undefined }>
| JsonValue;

View File

@@ -3,10 +3,10 @@ import { type CoValueHeader } from "./coValueCore.js";
/**
* The priority of a `CoValue` determines how much priority is given
* to its content messages.
*
*
* The priority value is handled as weight in the weighed round robin algorithm
* used to determine the order in which messages are sent.
*
*
* Follows the HTTP urgency range and order:
* - https://www.rfc-editor.org/rfc/rfc9218.html#name-urgency
*/
@@ -18,7 +18,9 @@ export const CO_VALUE_PRIORITY = {
export type CoValuePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
export function getPriorityFromHeader(header: CoValueHeader | undefined | boolean): CoValuePriority {
export function getPriorityFromHeader(
header: CoValueHeader | undefined | boolean,
): CoValuePriority {
if (typeof header === "boolean" || !header) {
return CO_VALUE_PRIORITY.MEDIUM;
}

View File

@@ -33,7 +33,12 @@ describe("PeerState", () => {
test("should push outgoing message to peer", async () => {
const { mockPeer, peerState } = setup();
const message: SyncMessage = { action: "load", id: "co_ztest-id", header: false, sessions: {} };
const message: SyncMessage = {
action: "load",
id: "co_ztest-id",
header: false,
sessions: {},
};
await peerState.pushOutgoingMessage(message);
expect(mockPeer.outgoing.push).toHaveBeenCalledWith(message);
});
@@ -54,21 +59,46 @@ describe("PeerState", () => {
test("should perform graceful shutdown", () => {
const { mockPeer, peerState } = setup();
const consoleSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
const consoleSpy = vi
.spyOn(console, "debug")
.mockImplementation(() => {});
peerState.gracefulShutdown();
expect(mockPeer.outgoing.close).toHaveBeenCalled();
expect(peerState.closed).toBe(true);
expect(consoleSpy).toHaveBeenCalledWith("Gracefully closing", "test-peer");
expect(consoleSpy).toHaveBeenCalledWith(
"Gracefully closing",
"test-peer",
);
consoleSpy.mockRestore();
});
test("should schedule outgoing messages based on their priority", async () => {
const { peerState } = setup();
const loadMessage: SyncMessage = { action: "load", id: "co_zhigh", header: false, sessions: {} };
const contentMessageHigh: SyncMessage = { action: "content", id: "co_zhigh", new: {}, priority: CO_VALUE_PRIORITY.HIGH };
const contentMessageMid: SyncMessage = { action: "content", id: "co_zmid", new: {}, priority: CO_VALUE_PRIORITY.MEDIUM };
const contentMessageLow: SyncMessage = { action: "content", id: "co_zlow", new: {}, priority: CO_VALUE_PRIORITY.LOW };
const loadMessage: SyncMessage = {
action: "load",
id: "co_zhigh",
header: false,
sessions: {},
};
const contentMessageHigh: SyncMessage = {
action: "content",
id: "co_zhigh",
new: {},
priority: CO_VALUE_PRIORITY.HIGH,
};
const contentMessageMid: SyncMessage = {
action: "content",
id: "co_zmid",
new: {},
priority: CO_VALUE_PRIORITY.MEDIUM,
};
const contentMessageLow: SyncMessage = {
action: "content",
id: "co_zlow",
new: {},
priority: CO_VALUE_PRIORITY.LOW,
};
const promises = [
peerState.pushOutgoingMessage(contentMessageLow),
@@ -81,12 +111,24 @@ describe("PeerState", () => {
// The first message is pushed directly, the other three are queued because are waiting
// for the first push to be completed.
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(1, contentMessageLow);
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
1,
contentMessageLow,
);
// Load message are managed as high priority messages and having the same priority as the content message
// they follow the push order.
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(2, contentMessageHigh);
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(3, loadMessage);
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(4, contentMessageMid);
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
2,
contentMessageHigh,
);
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
3,
loadMessage,
);
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
4,
contentMessageMid,
);
});
});

View File

@@ -55,7 +55,7 @@ test("Can create account with one node, and then load it on another", async () =
trace: true,
peer1role: "server",
peer2role: "client",
})
});
console.log("After connected peers");

View File

@@ -726,7 +726,11 @@ test.skip("When replaying creation and transactions of a coValue as new content,
crashOnClose: true,
});
const node2 = new LocalNode(admin, Crypto.newRandomSessionID(admin.id), Crypto);
const node2 = new LocalNode(
admin,
Crypto.newRandomSessionID(admin.id),
Crypto,
);
const [inRx2, inTx2] = newQueuePair();
const [outRx2, outTx2] = newQueuePair();
@@ -878,7 +882,11 @@ test("Can sync a coValue through a server to another client", async () => {
client1.syncManager.addPeer(serverAsPeerForClient1);
server.syncManager.addPeer(client1AsPeer);
const client2 = new LocalNode(admin, Crypto.newRandomSessionID(admin.id), Crypto);
const client2 = new LocalNode(
admin,
Crypto.newRandomSessionID(admin.id),
Crypto,
);
const [serverAsPeerForClient2, client2AsPeer] = connectedPeers(
"serverFor2",
@@ -926,7 +934,11 @@ test("Can sync a coValue with private transactions through a server to another c
client1.syncManager.addPeer(serverAsPeer);
server.syncManager.addPeer(client1AsPeer);
const client2 = new LocalNode(admin, client1.crypto.newRandomSessionID(admin.id), Crypto);
const client2 = new LocalNode(
admin,
client1.crypto.newRandomSessionID(admin.id),
Crypto,
);
const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
"server",
@@ -1074,7 +1086,11 @@ test("If we start loading a coValue before connecting to a peer that has it, it
const map = group.createMap();
map.set("hello", "world", "trusting");
const node2 = new LocalNode(admin, Crypto.newRandomSessionID(admin.id), Crypto);
const node2 = new LocalNode(
admin,
Crypto.newRandomSessionID(admin.id),
Crypto,
);
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
peer1role: "server",

View File

@@ -2,24 +2,27 @@ import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
import { AgentSecret } from "cojson";
export type MinimalClerkClient = {
user: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unsafeMetadata: Record<string, any>;
fullName: string | null;
username: string | null;
id: string;
update: (args: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unsafeMetadata: Record<string, any>;
}) => Promise<unknown>;
} | null | undefined;
user:
| {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unsafeMetadata: Record<string, any>;
fullName: string | null;
username: string | null;
id: string;
update: (args: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unsafeMetadata: Record<string, any>;
}) => Promise<unknown>;
}
| null
| undefined;
signOut: () => Promise<void>;
}
};
export class BrowserClerkAuth implements AuthMethod {
constructor(
public driver: BrowserClerkAuth.Driver,
private readonly clerkClient: MinimalClerkClient
private readonly clerkClient: MinimalClerkClient,
) {}
async start(): Promise<AuthResult> {

View File

@@ -55,11 +55,11 @@ export class BrowserDemoAuth implements AuthMethod {
this.driver.onSignedIn({ logOut });
},
onError: (error: string | Error) => {
this.driver.onError(error)
this.driver.onError(error);
},
logOut: () => {
delete localStorage[localStorageKey];
}
},
} satisfies AuthResult;
} else {
return new Promise<AuthResult>((resolve) => {
@@ -96,11 +96,11 @@ export class BrowserDemoAuth implements AuthMethod {
this.driver.onSignedIn({ logOut });
},
onError: (error: string | Error) => {
this.driver.onError(error)
this.driver.onError(error);
},
logOut: () => {
delete localStorage[localStorageKey];
}
},
});
},
existingUsers:
@@ -126,11 +126,11 @@ export class BrowserDemoAuth implements AuthMethod {
this.driver.onSignedIn({ logOut });
},
onError: (error: string | Error) => {
this.driver.onError(error)
this.driver.onError(error);
},
logOut: () => {
delete localStorage[localStorageKey];
}
},
});
},
});

View File

@@ -1,10 +1,5 @@
import { AgentSecret, cojsonInternals, CryptoProvider } from "cojson";
import {
Account,
AuthMethod,
AuthResult,
ID,
} from "jazz-tools";
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
import * as bip39 from "@scure/bip39";
type LocalStorageData = {

View File

@@ -1,9 +1,14 @@
import { BrowserClerkAuth, type MinimalClerkClient } from "jazz-browser-auth-clerk";
import {
BrowserClerkAuth,
type MinimalClerkClient,
} from "jazz-browser-auth-clerk";
import { useState, useMemo } from "react";
export function useJazzClerkAuth(clerk: MinimalClerkClient & {
signOut: () => Promise<unknown>;
}) {
export function useJazzClerkAuth(
clerk: MinimalClerkClient & {
signOut: () => Promise<unknown>;
},
) {
const [state, setState] = useState<{ errors: string[] }>({ errors: [] });
const authMethod = useMemo(() => {

View File

@@ -99,7 +99,7 @@ export const startSync = Command.make(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
websocket: ws as any, // TODO: fix types
expectPings: false,
batchingByDefault: false
batchingByDefault: false,
}),
);

View File

@@ -236,7 +236,7 @@ export function createCoValueObservable<V extends CoValue, Depth>() {
onUnavailable,
);
return unsubscribe
return unsubscribe;
}
const observable = {

View File

@@ -21,7 +21,11 @@ export { ImageDefinition } from "./internal.js";
export { CoValueBase, type CoValueClass } from "./internal.js";
export type { DepthsIn, DeeplyLoaded } from "./internal.js";
export { loadCoValue, subscribeToCoValue, createCoValueObservable } from "./internal.js";
export {
loadCoValue,
subscribeToCoValue,
createCoValueObservable,
} from "./internal.js";
export {
type AuthMethod,

View File

@@ -53,9 +53,7 @@ export class Ref<out V extends CoValue> {
"node" in this.controlledAccount
? this.controlledAccount.node
: this.controlledAccount._raw.core.node;
const raw = await node.load(
this.id as unknown as CoID<RawCoValue>,
);
const raw = await node.load(this.id as unknown as CoID<RawCoValue>);
if (raw === "unavailable") {
return "unavailable";
} else {

View File

@@ -173,7 +173,7 @@ export type SchemaFor<Field> = NonNullable<Field> extends CoValue
export type Encoder<V> = {
encode: (value: V) => JsonValue;
decode: (value: JsonValue) => V;
}
};
export type OptionalEncoder<V> =
| Encoder<V>
| {

View File

@@ -24,7 +24,7 @@ describe("co.json TypeScript validation", () => {
value: string;
};
};
}
};
class ValidNestedMap extends CoMap {
data = co.json<NestedType>();
@@ -41,7 +41,7 @@ describe("co.json TypeScript validation", () => {
type TypeWithOptional = {
value: string;
optional?: string | null;
}
};
class ValidMap extends CoMap {
data = co.json<TypeWithOptional>();

View File

@@ -170,18 +170,20 @@ describe("subscribeToCoValue", () => {
const { me, meOnSecondPeer } = await setupAccount();
const chatRoom = createChatRoom(me, "General");
const message = createMessage(me, "Hello Luigi, are you ready to save the princess?");
const message = createMessage(
me,
"Hello Luigi, are you ready to save the princess?",
);
chatRoom.messages?.push(message);
const updateFn = vi.fn()
const updateFn = vi.fn();
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [{
}],
messages: [{}],
},
updateFn,
);
@@ -200,30 +202,37 @@ describe("subscribeToCoValue", () => {
await waitFor(() => {
expect(updateFn).toHaveBeenCalled();
});
const lastValue = updateFn.mock.lastCall[0];
expect(lastValue?.messages?.[0]?.text).toBe("Nevermind, she was gone to the supermarket");
expect(lastValue?.messages?.[0]?.text).toBe(
"Nevermind, she was gone to the supermarket",
);
});
it("should handle the updates as immutable changes", async () => {
const { me, meOnSecondPeer } = await setupAccount();
const chatRoom = createChatRoom(me, "General");
const message = createMessage(me, "Hello Luigi, are you ready to save the princess?");
const message = createMessage(
me,
"Hello Luigi, are you ready to save the princess?",
);
const message2 = createMessage(me, "Let's go!");
chatRoom.messages?.push(message);
chatRoom.messages?.push(message2);
const updateFn = vi.fn()
const updateFn = vi.fn();
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [{
reactions: [],
}],
messages: [
{
reactions: [],
},
],
},
updateFn,
);
@@ -249,12 +258,14 @@ describe("subscribeToCoValue", () => {
await waitFor(() => {
expect(updateFn).toHaveBeenCalled();
});
const lastValue = updateFn.mock.lastCall[0];
expect(lastValue).not.toBe(initialValue);
expect(lastValue.messages).not.toBe(initialMessagesList);
expect(lastValue.messages[0]).not.toBe(initialMessage1);
expect(lastValue.messages[0].reactions).not.toBe(initialMessageReactions);
expect(lastValue.messages[0].reactions).not.toBe(
initialMessageReactions,
);
// This shouldn't change
expect(lastValue.messages[1]).toBe(initialMessage2);
@@ -270,21 +281,26 @@ describe("subscribeToCoValue", () => {
const { me, meOnSecondPeer } = await setupAccount();
const chatRoom = createChatRoom(me, "General");
const message = createMessage(me, "Hello Luigi, are you ready to save the princess?");
const message = createMessage(
me,
"Hello Luigi, are you ready to save the princess?",
);
const message2 = createMessage(me, "Let's go!");
chatRoom.messages?.push(message);
chatRoom.messages?.push(message2);
const updateFn = vi.fn()
const updateFn = vi.fn();
const unsubscribe = subscribeToCoValue(
ChatRoom,
chatRoom.id,
meOnSecondPeer,
{
messages: [{
reactions: [],
}],
messages: [
{
reactions: [],
},
],
},
updateFn,
);
@@ -306,19 +322,18 @@ describe("subscribeToCoValue", () => {
await waitFor(() => {
expect(updateFn).toHaveBeenCalled();
});
const lastValue = updateFn.mock.lastCall[0];
expect(lastValue).not.toBe(initialValue);
expect(lastValue.name).toBe("Me and Luigi");
expect(initialValue.name).toBe("General");
expect(lastValue.messages).toBe(initialValue.messages);
expect(lastValue.messages[0]).toBe(initialValue.messages[0]);
expect(lastValue.messages[1]).toBe(initialValue.messages[1]);
});
});
function waitFor(callback: () => boolean | void) {
return new Promise<void>((resolve, reject) => {
const checkPassed = () => {