Compare commits
5 Commits
authv2
...
add-code-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a3650ab0 | ||
|
|
936db45abf | ||
|
|
6a764b5248 | ||
|
|
84a1b5f893 | ||
|
|
4c3b0d3a27 |
11
.editorconfig
Normal file
11
.editorconfig
Normal 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
|
||||||
22
.github/workflows/monorepo-linting.yml
vendored
22
.github/workflows/monorepo-linting.yml
vendored
@@ -13,9 +13,29 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
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
|
- name: Run sherif
|
||||||
run: npx sherif@1.0.0
|
run: npx sherif@1.0.0
|
||||||
|
|
||||||
|
- name: Run formatter
|
||||||
|
run: pnpm format
|
||||||
|
|||||||
1
.prettierrc
Normal file
1
.prettierrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/** @type {import("prettier").Config} */
|
|
||||||
const config = {
|
|
||||||
trailingComma: "all",
|
|
||||||
tabWidth: 4,
|
|
||||||
semi: true,
|
|
||||||
singleQuote: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -88,7 +88,7 @@ export class MusicaAccount extends Account {
|
|||||||
profile = co.ref(Profile);
|
profile = co.ref(Profile);
|
||||||
root = co.ref(MusicaAccountRoot);
|
root = co.ref(MusicaAccountRoot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account migration is run on account creation and on every log-in.
|
* 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.
|
* You can use it to set up the account root and any other initial CoValues you need.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { createHashRouter, RouterProvider } from "react-router-dom";
|
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 { useMediaPlayer } from "./5_useMediaPlayer";
|
||||||
import { HomePage } from "./3_HomePage";
|
import { HomePage } from "./3_HomePage";
|
||||||
import { InvitePage } from "./6_InvitePage";
|
import { InvitePage } from "./6_InvitePage";
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import { usePlayState } from "./lib/audio/usePlayState";
|
|||||||
import { SidePanel } from "./components/SidePanel";
|
import { SidePanel } from "./components/SidePanel";
|
||||||
import { FileUploadButton } from "./components/FileUploadButton";
|
import { FileUploadButton } from "./components/FileUploadButton";
|
||||||
import { Button } from "./components/ui/button";
|
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 { useNavigate, useParams } from "react-router";
|
||||||
import { ID } from "jazz-tools";
|
import { ID } from "jazz-tools";
|
||||||
import { Playlist } from "./1_schema";
|
import { Playlist } from "./1_schema";
|
||||||
import { createInviteLink } from "jazz-react";
|
import { createInviteLink } from "jazz-react";
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +31,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const playState = usePlayState();
|
const playState = usePlayState();
|
||||||
const isPlaying = playState.value === "play";
|
const isPlaying = playState.value === "play";
|
||||||
const { toast } = useToast()
|
const { toast } = useToast();
|
||||||
|
|
||||||
async function handleFileLoad(files: FileList) {
|
async function handleFileLoad(files: FileList) {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
@@ -127,10 +131,15 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
}
|
}
|
||||||
isPlaying={
|
isPlaying={
|
||||||
mediaPlayer.activeTrackId ===
|
mediaPlayer.activeTrackId ===
|
||||||
track.id && isActivePlaylist && isPlaying
|
track.id &&
|
||||||
|
isActivePlaylist &&
|
||||||
|
isPlaying
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mediaPlayer.setActiveTrack(track, playlist);
|
mediaPlayer.setActiveTrack(
|
||||||
|
track,
|
||||||
|
playlist,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
showAddToPlaylist={isRootPlaylist}
|
showAddToPlaylist={isRootPlaylist}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ export async function addTrackToPlaylist(
|
|||||||
) {
|
) {
|
||||||
if (!account) return;
|
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)
|
* Since musicTracks are created as private values (see uploadMusicTracks)
|
||||||
@@ -137,10 +138,13 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
|
|||||||
track.title = title;
|
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;
|
me.root!.activePlaylist = playlist ?? me.root!.rootPlaylist;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateActiveTrack(track: MusicTrack, me: MusicaAccount) {
|
export async function updateActiveTrack(track: MusicTrack, me: MusicaAccount) {
|
||||||
me.root!.activeTrack = track;
|
me.root!.activeTrack = track;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function useMediaPlayer() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState<ID<MusicTrack> | null>(null);
|
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
|
// Reference used to avoid out-of-order track loads
|
||||||
const lastLoadedTrackId = useRef<ID<MusicTrack> | null>(null);
|
const lastLoadedTrackId = useRef<ID<MusicTrack> | null>(null);
|
||||||
@@ -71,10 +71,7 @@ export function useMediaPlayer() {
|
|||||||
async function setActiveTrack(track: MusicTrack, playlist?: Playlist) {
|
async function setActiveTrack(track: MusicTrack, playlist?: Playlist) {
|
||||||
if (!me?.root) return;
|
if (!me?.root) return;
|
||||||
|
|
||||||
if (
|
if (activeTrackId === track.id && lastLoadedTrackId.current !== null) {
|
||||||
activeTrackId === track.id &&
|
|
||||||
lastLoadedTrackId.current !== null
|
|
||||||
) {
|
|
||||||
playState.toggle();
|
playState.toggle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function MusicTrackRow({
|
|||||||
|
|
||||||
function handleTrackTitleChange(evt: ChangeEvent<HTMLInputElement>) {
|
function handleTrackTitleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
|
|
||||||
updateMusicTrackTitle(track, evt.target.value);
|
updateMusicTrackTitle(track, evt.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
||||||
waveform: {}
|
waveform: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!activeTrack) return null;
|
if (!activeTrack) return null;
|
||||||
@@ -60,14 +60,16 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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} />
|
<Waveform track={activeTrack} height={30} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-1 text-right min-w-fit w-[25%]">
|
<div className="flex flex-col items-end gap-1 text-right min-w-fit w-[25%]">
|
||||||
<h4 className="font-medium text-blue-800">
|
<h4 className="font-medium text-blue-800">
|
||||||
{activeTrackTitle}
|
{activeTrackTitle}
|
||||||
</h4>
|
</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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ export function SidePanel() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleAllTracksClick(
|
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {
|
||||||
evt: React.MouseEvent<HTMLAnchorElement>,
|
|
||||||
) {
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
navigate(`/`);
|
navigate(`/`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,58 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default:
|
||||||
destructive:
|
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
destructive:
|
||||||
outline:
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
outline:
|
||||||
secondary:
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
secondary:
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
},
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
size: {
|
},
|
||||||
default: "h-10 px-4 py-2",
|
size: {
|
||||||
sm: "h-9 rounded-md px-3",
|
default: "h-10 px-4 py-2",
|
||||||
lg: "h-11 rounded-md px-8",
|
sm: "h-9 rounded-md px-3",
|
||||||
icon: "h-10 w-10",
|
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
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button";
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -1,198 +1,201 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
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<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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",
|
inset && "pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
))
|
));
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName =
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSubContent.displayName =
|
DropdownMenuSubContent.displayName =
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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",
|
inset && "pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
))
|
));
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName =
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
))
|
));
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
const DropdownMenuShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
className={cn(
|
||||||
{...props}
|
"ml-auto text-xs tracking-widest opacity-60",
|
||||||
/>
|
className,
|
||||||
)
|
)}
|
||||||
}
|
{...props}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,127 +1,127 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { X } from "lucide-react"
|
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<
|
const ToastViewport = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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]",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
const toastVariants = cva(
|
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",
|
"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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border bg-background text-foreground",
|
default: "border bg-background text-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
);
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const Toast = React.forwardRef<
|
const Toast = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
VariantProps<typeof toastVariants>
|
VariantProps<typeof toastVariants>
|
||||||
>(({ className, variant, ...props }, ref) => {
|
>(({ className, variant, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<ToastPrimitives.Root
|
<ToastPrimitives.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(toastVariants({ variant }), className)}
|
className={cn(toastVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
Toast.displayName = ToastPrimitives.Root.displayName
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
const ToastAction = React.forwardRef<
|
const ToastAction = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Action
|
<ToastPrimitives.Action
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
const ToastClose = React.forwardRef<
|
const ToastClose = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Close
|
<ToastPrimitives.Close
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
toast-close=""
|
toast-close=""
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</ToastPrimitives.Close>
|
</ToastPrimitives.Close>
|
||||||
))
|
));
|
||||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
const ToastTitle = React.forwardRef<
|
const ToastTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Title
|
<ToastPrimitives.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm font-semibold", className)}
|
className={cn("text-sm font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
const ToastDescription = React.forwardRef<
|
const ToastDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Description
|
<ToastPrimitives.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm opacity-90", className)}
|
className={cn("text-sm opacity-90", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
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 {
|
export {
|
||||||
type ToastProps,
|
type ToastProps,
|
||||||
type ToastActionElement,
|
type ToastActionElement,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
Toast,
|
Toast,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastAction,
|
ToastAction,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,33 +1,41 @@
|
|||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Toast,
|
Toast,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
ToastDescription,
|
ToastDescription,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
} from "@/components/ui/toast"
|
} from "@/components/ui/toast";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast()
|
const { toasts } = useToast();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
{toasts.map(function ({
|
||||||
return (
|
id,
|
||||||
<Toast key={id} {...props}>
|
title,
|
||||||
<div className="grid gap-1">
|
description,
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
action,
|
||||||
{description && (
|
...props
|
||||||
<ToastDescription>{description}</ToastDescription>
|
}) {
|
||||||
)}
|
return (
|
||||||
</div>
|
<Toast key={id} {...props}>
|
||||||
{action}
|
<div className="grid gap-1">
|
||||||
<ToastClose />
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
</Toast>
|
{description && (
|
||||||
)
|
<ToastDescription>
|
||||||
})}
|
{description}
|
||||||
<ToastViewport />
|
</ToastDescription>
|
||||||
</ToastProvider>
|
)}
|
||||||
)
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,191 +1,189 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import type {
|
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||||
ToastActionElement,
|
|
||||||
ToastProps,
|
|
||||||
} from "@/components/ui/toast"
|
|
||||||
|
|
||||||
const TOAST_LIMIT = 1
|
const TOAST_LIMIT = 1;
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
const TOAST_REMOVE_DELAY = 1000000;
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
type ToasterToast = ToastProps & {
|
||||||
id: string
|
id: string;
|
||||||
title?: React.ReactNode
|
title?: React.ReactNode;
|
||||||
description?: React.ReactNode
|
description?: React.ReactNode;
|
||||||
action?: ToastActionElement
|
action?: ToastActionElement;
|
||||||
}
|
};
|
||||||
|
|
||||||
const actionTypes = {
|
const actionTypes = {
|
||||||
ADD_TOAST: "ADD_TOAST",
|
ADD_TOAST: "ADD_TOAST",
|
||||||
UPDATE_TOAST: "UPDATE_TOAST",
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
DISMISS_TOAST: "DISMISS_TOAST",
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
REMOVE_TOAST: "REMOVE_TOAST",
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
let count = 0
|
let count = 0;
|
||||||
|
|
||||||
function genId() {
|
function genId() {
|
||||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||||
return count.toString()
|
return count.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionType = typeof actionTypes
|
type ActionType = typeof actionTypes;
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| {
|
| {
|
||||||
type: ActionType["ADD_TOAST"]
|
type: ActionType["ADD_TOAST"];
|
||||||
toast: ToasterToast
|
toast: ToasterToast;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["UPDATE_TOAST"]
|
type: ActionType["UPDATE_TOAST"];
|
||||||
toast: Partial<ToasterToast>
|
toast: Partial<ToasterToast>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["DISMISS_TOAST"]
|
type: ActionType["DISMISS_TOAST"];
|
||||||
toastId?: ToasterToast["id"]
|
toastId?: ToasterToast["id"];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ActionType["REMOVE_TOAST"]
|
type: ActionType["REMOVE_TOAST"];
|
||||||
toastId?: ToasterToast["id"]
|
toastId?: ToasterToast["id"];
|
||||||
}
|
};
|
||||||
|
|
||||||
interface State {
|
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) => {
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
if (toastTimeouts.has(toastId)) {
|
if (toastTimeouts.has(toastId)) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
toastTimeouts.delete(toastId)
|
toastTimeouts.delete(toastId);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "REMOVE_TOAST",
|
type: "REMOVE_TOAST",
|
||||||
toastId: toastId,
|
toastId: toastId,
|
||||||
})
|
});
|
||||||
}, TOAST_REMOVE_DELAY)
|
}, TOAST_REMOVE_DELAY);
|
||||||
|
|
||||||
toastTimeouts.set(toastId, timeout)
|
toastTimeouts.set(toastId, timeout);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
export const reducer = (state: State, action: Action): State => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "ADD_TOAST":
|
case "ADD_TOAST":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
}
|
};
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
case "UPDATE_TOAST":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||||
),
|
),
|
||||||
}
|
};
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
case "DISMISS_TOAST": {
|
||||||
const { toastId } = action
|
const { toastId } = action;
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
// but I'll keep it here for simplicity
|
// but I'll keep it here for simplicity
|
||||||
if (toastId) {
|
if (toastId) {
|
||||||
addToRemoveQueue(toastId)
|
addToRemoveQueue(toastId);
|
||||||
} else {
|
} else {
|
||||||
state.toasts.forEach((toast) => {
|
state.toasts.forEach((toast) => {
|
||||||
addToRemoveQueue(toast.id)
|
addToRemoveQueue(toast.id);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toasts: state.toasts.map((t) =>
|
toasts: state.toasts.map((t) =>
|
||||||
t.id === toastId || toastId === undefined
|
t.id === toastId || toastId === undefined
|
||||||
? {
|
? {
|
||||||
...t,
|
...t,
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
: t
|
: t,
|
||||||
),
|
),
|
||||||
}
|
};
|
||||||
}
|
|
||||||
case "REMOVE_TOAST":
|
|
||||||
if (action.toastId === undefined) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [],
|
|
||||||
}
|
}
|
||||||
}
|
case "REMOVE_TOAST":
|
||||||
return {
|
if (action.toastId === undefined) {
|
||||||
...state,
|
return {
|
||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
...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) {
|
function dispatch(action: Action) {
|
||||||
memoryState = reducer(memoryState, action)
|
memoryState = reducer(memoryState, action);
|
||||||
listeners.forEach((listener) => {
|
listeners.forEach((listener) => {
|
||||||
listener(memoryState)
|
listener(memoryState);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">
|
type Toast = Omit<ToasterToast, "id">;
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
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({
|
dispatch({
|
||||||
type: "UPDATE_TOAST",
|
type: "ADD_TOAST",
|
||||||
toast: { ...props, id },
|
toast: {
|
||||||
})
|
...props,
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
dispatch({
|
return {
|
||||||
type: "ADD_TOAST",
|
id: id,
|
||||||
toast: {
|
dismiss,
|
||||||
...props,
|
update,
|
||||||
id,
|
};
|
||||||
open: true,
|
|
||||||
onOpenChange: (open) => {
|
|
||||||
if (!open) dismiss()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
dismiss,
|
|
||||||
update,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useToast() {
|
function useToast() {
|
||||||
const [state, setState] = React.useState<State>(memoryState)
|
const [state, setState] = React.useState<State>(memoryState);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
listeners.push(setState)
|
listeners.push(setState);
|
||||||
return () => {
|
return () => {
|
||||||
const index = listeners.indexOf(setState)
|
const index = listeners.indexOf(setState);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
listeners.splice(index, 1)
|
listeners.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [state])
|
}, [state]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
toast,
|
toast,
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
dismiss: (toastId?: string) =>
|
||||||
}
|
dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useToast, toast }
|
export { useToast, toast };
|
||||||
|
|||||||
@@ -2,31 +2,34 @@ import { useLayoutEffect, useState } from "react";
|
|||||||
import { useAudioManager } from "./AudioManager";
|
import { useAudioManager } from "./AudioManager";
|
||||||
|
|
||||||
export function usePlayerCurrentTime() {
|
export function usePlayerCurrentTime() {
|
||||||
const audioManager = useAudioManager();
|
const audioManager = useAudioManager();
|
||||||
const [value, setValue] = useState<number>(0);
|
const [value, setValue] = useState<number>(0);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setValue(audioManager.mediaElement.currentTime);
|
setValue(audioManager.mediaElement.currentTime);
|
||||||
|
|
||||||
const onTimeUpdate = () => {
|
const onTimeUpdate = () => {
|
||||||
setValue(audioManager.mediaElement.currentTime);
|
setValue(audioManager.mediaElement.currentTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
|
audioManager.mediaElement.addEventListener("timeupdate", onTimeUpdate);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
audioManager.mediaElement.removeEventListener("timeupdate", onTimeUpdate);
|
audioManager.mediaElement.removeEventListener(
|
||||||
};
|
"timeupdate",
|
||||||
}, [audioManager]);
|
onTimeUpdate,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [audioManager]);
|
||||||
|
|
||||||
function setCurrentTime(time: number) {
|
function setCurrentTime(time: number) {
|
||||||
if (audioManager.mediaElement.paused) audioManager.play();
|
if (audioManager.mediaElement.paused) audioManager.play();
|
||||||
|
|
||||||
audioManager.mediaElement.currentTime = time;
|
audioManager.mediaElement.currentTime = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
setValue: setCurrentTime,
|
setValue: setCurrentTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
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 "./index.css";
|
||||||
|
|
||||||
import { createJazzReactApp, DemoAuthBasicUI, useDemoAuth } from "jazz-react";
|
import { createJazzReactApp, DemoAuthBasicUI, useDemoAuth } from "jazz-react";
|
||||||
@@ -38,10 +43,7 @@ function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Jazz.Provider
|
<Jazz.Provider auth={auth} peer={peer}>
|
||||||
auth={auth}
|
|
||||||
peer={peer}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Jazz.Provider>
|
</Jazz.Provider>
|
||||||
<DemoAuthBasicUI appName={appName} state={authState} />
|
<DemoAuthBasicUI appName={appName} state={authState} />
|
||||||
@@ -96,9 +98,7 @@ export default function App() {
|
|||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() => router.navigate("/").then(() => logOut())}
|
||||||
router.navigate("/").then(() => logOut())
|
|
||||||
}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
@@ -114,7 +114,7 @@ function AcceptInvite() {
|
|||||||
onAccept: (petPostID) => navigate("/pet/" + petPostID),
|
onAccept: (petPostID) => navigate("/pet/" + petPostID),
|
||||||
});
|
});
|
||||||
|
|
||||||
return <p>Accepting invite...</p>
|
return <p>Accepting invite...</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostOverview() {
|
export function PostOverview() {
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import {
|
|||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
import { createJazzReactApp, PasskeyAuthBasicUI, usePasskeyAuth } from "jazz-react";
|
import {
|
||||||
|
createJazzReactApp,
|
||||||
|
PasskeyAuthBasicUI,
|
||||||
|
usePasskeyAuth,
|
||||||
|
} from "jazz-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -62,8 +66,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<App />
|
<App />
|
||||||
</JazzAndAuth>
|
</JazzAndAuth>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
,
|
||||||
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,9 +110,7 @@ export default function App() {
|
|||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() => router.navigate("/").then(logOut)}
|
||||||
router.navigate("/").then(logOut)
|
|
||||||
}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Log out
|
Log out
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ function RenderPackageChild({
|
|||||||
<div key={i} id={child.name} className="not-prose mt-4">
|
<div key={i} id={child.name} className="not-prose mt-4">
|
||||||
{
|
{
|
||||||
<Highlight hide={[0, 2]}>
|
<Highlight hide={[0, 2]}>
|
||||||
{`function \n${printSimpleSignature(child, signature) + ":"}\n {}`}
|
{`function \n${
|
||||||
|
printSimpleSignature(child, signature) + ":"
|
||||||
|
}\n {}`}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
}{" "}
|
}{" "}
|
||||||
<span className="opacity-75 text-xs pl-1">
|
<span className="opacity-75 text-xs pl-1">
|
||||||
@@ -123,9 +125,9 @@ function RenderTypeAlias({
|
|||||||
<Highlight>{`type ${child.name}`}</Highlight>
|
<Highlight>{`type ${child.name}`}</Highlight>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="not-prose text-sm ml-4">
|
<p className="not-prose text-sm ml-4">
|
||||||
<Highlight>{`type ${child.name}${typeParameters?.length && `<${typeParameters?.join(", ")}>`} = ${printType(
|
<Highlight>{`type ${child.name}${
|
||||||
child.type,
|
typeParameters?.length && `<${typeParameters?.join(", ")}>`
|
||||||
)}`}</Highlight>
|
} = ${printType(child.type)}`}</Highlight>
|
||||||
</p>
|
</p>
|
||||||
<div className="ml-4 mt-2 flex-[3]">
|
<div className="ml-4 mt-2 flex-[3]">
|
||||||
<DocComment>
|
<DocComment>
|
||||||
|
|||||||
@@ -94,7 +94,12 @@ export class SQLiteStorage {
|
|||||||
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
const [localNodeAsPeer, storageAsPeer] = cojsonInternals.connectedPeers(
|
||||||
localNodeName,
|
localNodeName,
|
||||||
"storage",
|
"storage",
|
||||||
{ peer1role: "client", peer2role: "server", trace, crashOnClose: true },
|
{
|
||||||
|
peer1role: "client",
|
||||||
|
peer2role: "server",
|
||||||
|
trace,
|
||||||
|
crashOnClose: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await SQLiteStorage.open(
|
await SQLiteStorage.open(
|
||||||
@@ -427,7 +432,9 @@ export class SQLiteStorage {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
(accountID): accountID is RawAccountID =>
|
(
|
||||||
|
accountID,
|
||||||
|
): accountID is RawAccountID =>
|
||||||
cojsonInternals.isAccountID(
|
cojsonInternals.isAccountID(
|
||||||
accountID,
|
accountID,
|
||||||
) && accountID !== theirKnown.id,
|
) && accountID !== theirKnown.id,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { addMessageToBacklog } from "./serialization.js";
|
|||||||
export const MAX_OUTGOING_MESSAGES_CHUNK_BYTES = 25_000;
|
export const MAX_OUTGOING_MESSAGES_CHUNK_BYTES = 25_000;
|
||||||
|
|
||||||
export class BatchedOutgoingMessages {
|
export class BatchedOutgoingMessages {
|
||||||
private backlog: string = '';
|
private backlog: string = "";
|
||||||
private timeout: ReturnType<typeof setTimeout> | null = null;
|
private timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
constructor(private send: (messages: string) => void) { }
|
constructor(private send: (messages: string) => void) {}
|
||||||
|
|
||||||
push(msg: SyncMessage) {
|
push(msg: SyncMessage) {
|
||||||
const payload = addMessageToBacklog(this.backlog, msg);
|
const payload = addMessageToBacklog(this.backlog, msg);
|
||||||
@@ -16,12 +16,13 @@ export class BatchedOutgoingMessages {
|
|||||||
clearTimeout(this.timeout);
|
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;
|
const backlogExists = this.backlog.length > 0;
|
||||||
|
|
||||||
if (maxChunkSizeReached && backlogExists) {
|
if (maxChunkSizeReached && backlogExists) {
|
||||||
this.sendMessagesInBulk();
|
this.sendMessagesInBulk();
|
||||||
this.backlog = addMessageToBacklog('', msg);
|
this.backlog = addMessageToBacklog("", msg);
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
this.sendMessagesInBulk();
|
this.sendMessagesInBulk();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -38,7 +39,7 @@ export class BatchedOutgoingMessages {
|
|||||||
|
|
||||||
sendMessagesInBulk() {
|
sendMessagesInBulk() {
|
||||||
this.send(this.backlog);
|
this.send(this.backlog);
|
||||||
this.backlog = '';
|
this.backlog = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
|||||||
@@ -47,7 +47,11 @@ export function createWebSocketPeer({
|
|||||||
const result = deserializeMessages(event.data as string);
|
const result = deserializeMessages(event.data as string);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
console.error("Error while deserializing messages", event.data, result.error);
|
console.error(
|
||||||
|
"Error while deserializing messages",
|
||||||
|
event.data,
|
||||||
|
result.error,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +94,7 @@ export function createWebSocketPeer({
|
|||||||
|
|
||||||
const outgoingMessages = new BatchedOutgoingMessages((messages) => {
|
const outgoingMessages = new BatchedOutgoingMessages((messages) => {
|
||||||
if (websocket.readyState === 1) {
|
if (websocket.readyState === 1) {
|
||||||
websocket.send(
|
websocket.send(messages);
|
||||||
messages,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ export function addMessageToBacklog(backlog: string, message: SyncMessage) {
|
|||||||
return `${backlog}\n${JSON.stringify(message)}`;
|
return `${backlog}\n${JSON.stringify(message)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeMessages(messages: string) {
|
export function deserializeMessages(messages: string) {
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
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;
|
} as const;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error while deserializing messages", e);
|
console.error("Error while deserializing messages", e);
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
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 { SyncMessage } from "cojson";
|
||||||
import { CoValueKnownState } from "cojson/src/sync.js";
|
import { CoValueKnownState } from "cojson/src/sync.js";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
})
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
})
|
});
|
||||||
|
|
||||||
describe("BatchedOutgoingMessages", () => {
|
describe("BatchedOutgoingMessages", () => {
|
||||||
function setup() {
|
function setup() {
|
||||||
@@ -20,8 +23,18 @@ describe("BatchedOutgoingMessages", () => {
|
|||||||
|
|
||||||
test("should batch messages and send them after a timeout", () => {
|
test("should batch messages and send them after a timeout", () => {
|
||||||
const { sendMock, batchedMessages } = setup();
|
const { sendMock, batchedMessages } = setup();
|
||||||
const message1: SyncMessage = { action: "known", id: "co_z1", header: false, sessions: {} };
|
const message1: SyncMessage = {
|
||||||
const message2: SyncMessage = { action: "known", id: "co_z2", header: false, sessions: {} };
|
action: "known",
|
||||||
|
id: "co_z1",
|
||||||
|
header: false,
|
||||||
|
sessions: {},
|
||||||
|
};
|
||||||
|
const message2: SyncMessage = {
|
||||||
|
action: "known",
|
||||||
|
id: "co_z2",
|
||||||
|
header: false,
|
||||||
|
sessions: {},
|
||||||
|
};
|
||||||
|
|
||||||
batchedMessages.push(message1);
|
batchedMessages.push(message1);
|
||||||
batchedMessages.push(message2);
|
batchedMessages.push(message2);
|
||||||
@@ -32,7 +45,7 @@ describe("BatchedOutgoingMessages", () => {
|
|||||||
|
|
||||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||||
expect(sendMock).toHaveBeenCalledWith(
|
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,
|
header: false,
|
||||||
sessions: {
|
sessions: {
|
||||||
// Add a large payload to exceed MAX_OUTGOING_MESSAGES_CHUNK_BYTES
|
// Add a large payload to exceed MAX_OUTGOING_MESSAGES_CHUNK_BYTES
|
||||||
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES)
|
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES),
|
||||||
} as CoValueKnownState['sessions'],
|
} as CoValueKnownState["sessions"],
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
batchedMessages.push(largeMessage);
|
batchedMessages.push(largeMessage);
|
||||||
@@ -57,15 +69,20 @@ describe("BatchedOutgoingMessages", () => {
|
|||||||
|
|
||||||
test("should send accumulated messages before a large message", () => {
|
test("should send accumulated messages before a large message", () => {
|
||||||
const { sendMock, batchedMessages } = setup();
|
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 = {
|
const largeMessage: SyncMessage = {
|
||||||
action: "known",
|
action: "known",
|
||||||
id: "co_z_large",
|
id: "co_z_large",
|
||||||
header: false,
|
header: false,
|
||||||
sessions: {
|
sessions: {
|
||||||
// Add a large payload to exceed MAX_OUTGOING_MESSAGES_CHUNK_BYTES
|
// Add a large payload to exceed MAX_OUTGOING_MESSAGES_CHUNK_BYTES
|
||||||
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES)
|
payload: "x".repeat(MAX_OUTGOING_MESSAGES_CHUNK_BYTES),
|
||||||
} as CoValueKnownState['sessions'],
|
} as CoValueKnownState["sessions"],
|
||||||
};
|
};
|
||||||
|
|
||||||
batchedMessages.push(smallMessage);
|
batchedMessages.push(smallMessage);
|
||||||
@@ -74,13 +91,24 @@ describe("BatchedOutgoingMessages", () => {
|
|||||||
vi.runAllTimers();
|
vi.runAllTimers();
|
||||||
|
|
||||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||||
expect(sendMock).toHaveBeenNthCalledWith(1, JSON.stringify(smallMessage));
|
expect(sendMock).toHaveBeenNthCalledWith(
|
||||||
expect(sendMock).toHaveBeenNthCalledWith(2, JSON.stringify(largeMessage));
|
1,
|
||||||
|
JSON.stringify(smallMessage),
|
||||||
|
);
|
||||||
|
expect(sendMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
JSON.stringify(largeMessage),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should send remaining messages on close", () => {
|
test("should send remaining messages on close", () => {
|
||||||
const { sendMock, batchedMessages } = setup();
|
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);
|
batchedMessages.push(message);
|
||||||
expect(sendMock).not.toHaveBeenCalled();
|
expect(sendMock).not.toHaveBeenCalled();
|
||||||
@@ -93,13 +121,23 @@ describe("BatchedOutgoingMessages", () => {
|
|||||||
|
|
||||||
test("should clear timeout when pushing new messages", () => {
|
test("should clear timeout when pushing new messages", () => {
|
||||||
const { sendMock, batchedMessages } = setup();
|
const { sendMock, batchedMessages } = setup();
|
||||||
const message1: SyncMessage = { action: "known", id: "co_z1", header: false, sessions: {} };
|
const message1: SyncMessage = {
|
||||||
const message2: SyncMessage = { action: "known", id: "co_z2", header: false, sessions: {} };
|
action: "known",
|
||||||
|
id: "co_z1",
|
||||||
|
header: false,
|
||||||
|
sessions: {},
|
||||||
|
};
|
||||||
|
const message2: SyncMessage = {
|
||||||
|
action: "known",
|
||||||
|
id: "co_z2",
|
||||||
|
header: false,
|
||||||
|
sessions: {},
|
||||||
|
};
|
||||||
|
|
||||||
batchedMessages.push(message1);
|
batchedMessages.push(message1);
|
||||||
|
|
||||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
|
||||||
|
|
||||||
batchedMessages.push(message2);
|
batchedMessages.push(message2);
|
||||||
|
|
||||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||||
@@ -108,7 +146,7 @@ describe("BatchedOutgoingMessages", () => {
|
|||||||
|
|
||||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||||
expect(sendMock).toHaveBeenCalledWith(
|
expect(sendMock).toHaveBeenCalledWith(
|
||||||
`${JSON.stringify(message1)}\n${JSON.stringify(message2)}`
|
`${JSON.stringify(message1)}\n${JSON.stringify(message2)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -132,12 +132,14 @@ describe("createWebSocketPeer", () => {
|
|||||||
expect(mockWebSocket.send).toHaveBeenCalled();
|
expect(mockWebSocket.send).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(message1));
|
expect(mockWebSocket.send).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify(message1),
|
||||||
|
);
|
||||||
|
|
||||||
mockWebSocket.send.mockClear();
|
mockWebSocket.send.mockClear();
|
||||||
void peer.outgoing.push(message2);
|
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();
|
expect(mockWebSocket.send).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -233,7 +235,10 @@ describe("createWebSocketPeer", () => {
|
|||||||
|
|
||||||
const stream: SyncMessage[] = [];
|
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);
|
stream.push(message1);
|
||||||
void peer.outgoing.push(message1);
|
void peer.outgoing.push(message1);
|
||||||
}
|
}
|
||||||
@@ -277,7 +282,10 @@ describe("createWebSocketPeer", () => {
|
|||||||
|
|
||||||
const stream: SyncMessage[] = [];
|
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);
|
stream.push(message1);
|
||||||
void peer.outgoing.push(message1);
|
void peer.outgoing.push(message1);
|
||||||
}
|
}
|
||||||
@@ -356,16 +364,12 @@ describe("createWebSocketPeer", () => {
|
|||||||
|
|
||||||
messageHandler?.(
|
messageHandler?.(
|
||||||
new MessageEvent("message", {
|
new MessageEvent("message", {
|
||||||
data: Array.from(
|
data: Array.from({ length: 5 }, () => message1)
|
||||||
{ length: 5 },
|
|
||||||
() => message1,
|
|
||||||
)
|
|
||||||
.map((msg) => JSON.stringify(msg))
|
.map((msg) => JSON.stringify(msg))
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const message2: SyncMessage = {
|
const message2: SyncMessage = {
|
||||||
action: "content",
|
action: "content",
|
||||||
id: "co_zlow",
|
id: "co_zlow",
|
||||||
@@ -407,7 +411,6 @@ describe("createWebSocketPeer", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const message2: SyncMessage = {
|
const message2: SyncMessage = {
|
||||||
action: "content",
|
action: "content",
|
||||||
id: "co_zlow",
|
id: "co_zlow",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
export interface WebsocketEvents {
|
export interface WebsocketEvents {
|
||||||
close: { code: number; reason: string; };
|
close: { code: number; reason: string };
|
||||||
message: { data: unknown; };
|
message: { data: unknown };
|
||||||
open: void;
|
open: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,11 +14,11 @@ export interface AnyWebSocket {
|
|||||||
addEventListener<K extends keyof WebsocketEvents>(
|
addEventListener<K extends keyof WebsocketEvents>(
|
||||||
type: K,
|
type: K,
|
||||||
listener: (event: WebsocketEvents[K]) => void,
|
listener: (event: WebsocketEvents[K]) => void,
|
||||||
options?: { once: boolean; }
|
options?: { once: boolean },
|
||||||
): void;
|
): void;
|
||||||
removeEventListener<K extends keyof WebsocketEvents>(
|
removeEventListener<K extends keyof WebsocketEvents>(
|
||||||
type: K,
|
type: K,
|
||||||
listener: (event: WebsocketEvents[K]) => void
|
listener: (event: WebsocketEvents[K]) => void,
|
||||||
): void;
|
): void;
|
||||||
close(): void;
|
close(): void;
|
||||||
send(data: string): void;
|
send(data: string): void;
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export class PeerState {
|
|||||||
|
|
||||||
this.processing = true;
|
this.processing = true;
|
||||||
|
|
||||||
|
|
||||||
let entry: QueueEntry | undefined;
|
let entry: QueueEntry | undefined;
|
||||||
while ((entry = this.queue.pull())) {
|
while ((entry = this.queue.pull())) {
|
||||||
// Awaiting the push to send one message at a time
|
// Awaiting the push to send one message at a time
|
||||||
|
|||||||
@@ -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.
|
* 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>;
|
type QueueTuple = Tuple<QueueEntry[], 8>;
|
||||||
|
|
||||||
export class PriorityBasedMessageQueue {
|
export class PriorityBasedMessageQueue {
|
||||||
private queues: QueueTuple = [
|
private queues: QueueTuple = [[], [], [], [], [], [], [], []];
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
];
|
|
||||||
|
|
||||||
private getQueue(priority: CoValuePriority) {
|
private getQueue(priority: CoValuePriority) {
|
||||||
return this.queues[priority];
|
return this.queues[priority];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(private defaultPriority: CoValuePriority) {}
|
||||||
private defaultPriority: CoValuePriority,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public push(msg: SyncMessage) {
|
public push(msg: SyncMessage) {
|
||||||
const { promise, resolve, reject } = promiseWithResolvers<void>();
|
const { promise, resolve, reject } = promiseWithResolvers<void>();
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ function createResolvablePromise<T>() {
|
|||||||
|
|
||||||
class CoValueUnknownState {
|
class CoValueUnknownState {
|
||||||
type = "unknown" as const;
|
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;
|
private resolve: (value: "available" | "unavailable") => void;
|
||||||
|
|
||||||
ready: Promise<"available" | "unavailable">;
|
ready: Promise<"available" | "unavailable">;
|
||||||
@@ -22,10 +25,15 @@ class CoValueUnknownState {
|
|||||||
this.peers = new Map();
|
this.peers = new Map();
|
||||||
|
|
||||||
for (const peerId of peersIds) {
|
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.ready = promise;
|
||||||
this.resolve = resolve;
|
this.resolve = resolve;
|
||||||
@@ -66,20 +74,22 @@ class CoValueUnknownState {
|
|||||||
class CoValueAvailableState {
|
class CoValueAvailableState {
|
||||||
type = "available" as const;
|
type = "available" as const;
|
||||||
|
|
||||||
constructor(public coValue: CoValueCore) { }
|
constructor(public coValue: CoValueCore) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoValueStateAction = {
|
type CoValueStateAction =
|
||||||
type: "not-found";
|
| {
|
||||||
peerId: PeerID;
|
type: "not-found";
|
||||||
} | {
|
peerId: PeerID;
|
||||||
type: "found";
|
}
|
||||||
peerId: PeerID;
|
| {
|
||||||
coValue: CoValueCore;
|
type: "found";
|
||||||
};
|
peerId: PeerID;
|
||||||
|
coValue: CoValueCore;
|
||||||
|
};
|
||||||
|
|
||||||
export class CoValueState {
|
export class CoValueState {
|
||||||
constructor(public state: CoValueUnknownState | CoValueAvailableState) { }
|
constructor(public state: CoValueUnknownState | CoValueAvailableState) {}
|
||||||
|
|
||||||
static Unknown(peersToWaitFor: Set<PeerID>) {
|
static Unknown(peersToWaitFor: Set<PeerID>) {
|
||||||
return new CoValueState(new CoValueUnknownState(peersToWaitFor));
|
return new CoValueState(new CoValueUnknownState(peersToWaitFor));
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { JsonObject } from "../jsonValue.js";
|
|||||||
import { RawBinaryCoStream, RawCoStream } from "./coStream.js";
|
import { RawBinaryCoStream, RawCoStream } from "./coStream.js";
|
||||||
import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
|
import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
|
||||||
import { AgentID, isAgentID } from "../ids.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 { Role } from "../permissions.js";
|
||||||
import { base58 } from "@scure/base";
|
import { base58 } from "@scure/base";
|
||||||
import { CoValueUniqueness } from "../coValueCore.js";
|
import { CoValueUniqueness } from "../coValueCore.js";
|
||||||
@@ -19,7 +23,9 @@ export type GroupShape = {
|
|||||||
[key: RawAccountID | AgentID]: Role;
|
[key: RawAccountID | AgentID]: Role;
|
||||||
[EVERYONE]?: Role;
|
[EVERYONE]?: Role;
|
||||||
readKey?: KeyID;
|
readKey?: KeyID;
|
||||||
[revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`]: Sealed<KeySecret>;
|
[
|
||||||
|
revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`
|
||||||
|
]: Sealed<KeySecret>;
|
||||||
[revelationFor: `${KeyID}_for_${Everyone}`]: KeySecret;
|
[revelationFor: `${KeyID}_for_${Everyone}`]: KeySecret;
|
||||||
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
||||||
KeySecret,
|
KeySecret,
|
||||||
@@ -175,10 +181,12 @@ export class RawGroup<
|
|||||||
const newReadKey = this.core.crypto.newRandomKeySecret();
|
const newReadKey = this.core.crypto.newRandomKeySecret();
|
||||||
|
|
||||||
for (const readerID of currentlyPermittedReaders) {
|
for (const readerID of currentlyPermittedReaders) {
|
||||||
const reader = this.core.node.resolveAccountAgent(
|
const reader = this.core.node
|
||||||
readerID,
|
.resolveAccountAgent(
|
||||||
"Expected to know currently permitted reader",
|
readerID,
|
||||||
)._unsafeUnwrap({ withStackTrace: true });
|
"Expected to know currently permitted reader",
|
||||||
|
)
|
||||||
|
._unsafeUnwrap({ withStackTrace: true });
|
||||||
|
|
||||||
this.set(
|
this.set(
|
||||||
`${newReadKey.id}_for_${readerID}`,
|
`${newReadKey.id}_for_${readerID}`,
|
||||||
@@ -256,7 +264,7 @@ export class RawGroup<
|
|||||||
init?: M["_shape"],
|
init?: M["_shape"],
|
||||||
meta?: M["headerMeta"],
|
meta?: M["headerMeta"],
|
||||||
initPrivacy: "trusting" | "private" = "private",
|
initPrivacy: "trusting" | "private" = "private",
|
||||||
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()
|
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
|
||||||
): M {
|
): M {
|
||||||
const map = this.core.node
|
const map = this.core.node
|
||||||
.createCoValue({
|
.createCoValue({
|
||||||
@@ -266,7 +274,7 @@ export class RawGroup<
|
|||||||
group: this.id,
|
group: this.id,
|
||||||
},
|
},
|
||||||
meta: meta || null,
|
meta: meta || null,
|
||||||
...uniqueness
|
...uniqueness,
|
||||||
})
|
})
|
||||||
.getCurrentContent() as M;
|
.getCurrentContent() as M;
|
||||||
|
|
||||||
@@ -289,7 +297,7 @@ export class RawGroup<
|
|||||||
init?: L["_item"][],
|
init?: L["_item"][],
|
||||||
meta?: L["headerMeta"],
|
meta?: L["headerMeta"],
|
||||||
initPrivacy: "trusting" | "private" = "private",
|
initPrivacy: "trusting" | "private" = "private",
|
||||||
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()
|
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
|
||||||
): L {
|
): L {
|
||||||
const list = this.core.node
|
const list = this.core.node
|
||||||
.createCoValue({
|
.createCoValue({
|
||||||
@@ -299,7 +307,7 @@ export class RawGroup<
|
|||||||
group: this.id,
|
group: this.id,
|
||||||
},
|
},
|
||||||
meta: meta || null,
|
meta: meta || null,
|
||||||
...uniqueness
|
...uniqueness,
|
||||||
})
|
})
|
||||||
.getCurrentContent() as L;
|
.getCurrentContent() as L;
|
||||||
|
|
||||||
@@ -313,7 +321,10 @@ export class RawGroup<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @category 3. Value creation */
|
/** @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
|
return this.core.node
|
||||||
.createCoValue({
|
.createCoValue({
|
||||||
type: "costream",
|
type: "costream",
|
||||||
@@ -322,7 +333,7 @@ export class RawGroup<
|
|||||||
group: this.id,
|
group: this.id,
|
||||||
},
|
},
|
||||||
meta: meta || null,
|
meta: meta || null,
|
||||||
...uniqueness
|
...uniqueness,
|
||||||
})
|
})
|
||||||
.getCurrentContent() as C;
|
.getCurrentContent() as C;
|
||||||
}
|
}
|
||||||
@@ -330,7 +341,7 @@ export class RawGroup<
|
|||||||
/** @category 3. Value creation */
|
/** @category 3. Value creation */
|
||||||
createBinaryStream<C extends RawBinaryCoStream>(
|
createBinaryStream<C extends RawBinaryCoStream>(
|
||||||
meta: C["headerMeta"] = { type: "binary" },
|
meta: C["headerMeta"] = { type: "binary" },
|
||||||
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique()
|
uniqueness: CoValueUniqueness = this.core.crypto.createdNowUnique(),
|
||||||
): C {
|
): C {
|
||||||
return this.core.node
|
return this.core.node
|
||||||
.createCoValue({
|
.createCoValue({
|
||||||
@@ -340,7 +351,7 @@ export class RawGroup<
|
|||||||
group: this.id,
|
group: this.id,
|
||||||
},
|
},
|
||||||
meta: meta,
|
meta: meta,
|
||||||
...uniqueness
|
...uniqueness,
|
||||||
})
|
})
|
||||||
.getCurrentContent() as C;
|
.getCurrentContent() as C;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,27 +5,41 @@ export type JsonValue = JsonAtom | JsonArray | JsonObject | RawCoID;
|
|||||||
export type JsonArray = JsonValue[] | readonly JsonValue[];
|
export type JsonArray = JsonValue[] | readonly JsonValue[];
|
||||||
export type JsonObject = { [key: string]: JsonValue | undefined };
|
export type JsonObject = { [key: string]: JsonValue | undefined };
|
||||||
|
|
||||||
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
|
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> &
|
||||||
type ExcludeEmpty<T> = T extends AtLeastOne<T> ? T : never;
|
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>[];
|
export type CoJsonArray<T> = CoJsonValue<T>[] | readonly CoJsonValue<T>[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since we are forcing Typescript to elaborate the indexes from the given type passing
|
* Since we are forcing Typescript to elaborate the indexes from the given type passing
|
||||||
* non-object values to CoJsonObjectWithIndex will return an empty object
|
* non-object values to CoJsonObjectWithIndex will return an empty object
|
||||||
* E.g.
|
* E.g.
|
||||||
* CoJsonObjectWithIndex<() => void> --> {}
|
* CoJsonObjectWithIndex<() => void> --> {}
|
||||||
* CoJsonObjectWithIndex<RegExp> --> {}
|
* CoJsonObjectWithIndex<RegExp> --> {}
|
||||||
*
|
*
|
||||||
* Applying the ExcludeEmpty type here to make sure we don't accept functions or non-serializable values
|
* 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.
|
* 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 CoJsonValue1L<T> =
|
||||||
export type CoJsonValue2L<T> = ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue3L<T[K]> | undefined }> | JsonValue;
|
| ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue2L<T[K]> | undefined }>
|
||||||
export type CoJsonValue3L<T> = ExcludeEmpty<{ [K in keyof T & string]: CoJsonValue4L<T[K]> | undefined }> | JsonValue;
|
| JsonValue;
|
||||||
export type CoJsonValue4L<T> = ExcludeEmpty<{ [K in keyof T & string]: JsonValue | 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;
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { type CoValueHeader } from "./coValueCore.js";
|
|||||||
/**
|
/**
|
||||||
* The priority of a `CoValue` determines how much priority is given
|
* The priority of a `CoValue` determines how much priority is given
|
||||||
* to its content messages.
|
* to its content messages.
|
||||||
*
|
*
|
||||||
* The priority value is handled as weight in the weighed round robin algorithm
|
* The priority value is handled as weight in the weighed round robin algorithm
|
||||||
* used to determine the order in which messages are sent.
|
* used to determine the order in which messages are sent.
|
||||||
*
|
*
|
||||||
* Follows the HTTP urgency range and order:
|
* Follows the HTTP urgency range and order:
|
||||||
* - https://www.rfc-editor.org/rfc/rfc9218.html#name-urgency
|
* - 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 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) {
|
if (typeof header === "boolean" || !header) {
|
||||||
return CO_VALUE_PRIORITY.MEDIUM;
|
return CO_VALUE_PRIORITY.MEDIUM;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ describe("PeerState", () => {
|
|||||||
|
|
||||||
test("should push outgoing message to peer", async () => {
|
test("should push outgoing message to peer", async () => {
|
||||||
const { mockPeer, peerState } = setup();
|
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);
|
await peerState.pushOutgoingMessage(message);
|
||||||
expect(mockPeer.outgoing.push).toHaveBeenCalledWith(message);
|
expect(mockPeer.outgoing.push).toHaveBeenCalledWith(message);
|
||||||
});
|
});
|
||||||
@@ -54,21 +59,46 @@ describe("PeerState", () => {
|
|||||||
|
|
||||||
test("should perform graceful shutdown", () => {
|
test("should perform graceful shutdown", () => {
|
||||||
const { mockPeer, peerState } = setup();
|
const { mockPeer, peerState } = setup();
|
||||||
const consoleSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
|
const consoleSpy = vi
|
||||||
|
.spyOn(console, "debug")
|
||||||
|
.mockImplementation(() => {});
|
||||||
peerState.gracefulShutdown();
|
peerState.gracefulShutdown();
|
||||||
expect(mockPeer.outgoing.close).toHaveBeenCalled();
|
expect(mockPeer.outgoing.close).toHaveBeenCalled();
|
||||||
expect(peerState.closed).toBe(true);
|
expect(peerState.closed).toBe(true);
|
||||||
expect(consoleSpy).toHaveBeenCalledWith("Gracefully closing", "test-peer");
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
"Gracefully closing",
|
||||||
|
"test-peer",
|
||||||
|
);
|
||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should schedule outgoing messages based on their priority", async () => {
|
test("should schedule outgoing messages based on their priority", async () => {
|
||||||
const { peerState } = setup();
|
const { peerState } = setup();
|
||||||
|
|
||||||
const loadMessage: SyncMessage = { action: "load", id: "co_zhigh", header: false, sessions: {} };
|
const loadMessage: SyncMessage = {
|
||||||
const contentMessageHigh: SyncMessage = { action: "content", id: "co_zhigh", new: {}, priority: CO_VALUE_PRIORITY.HIGH };
|
action: "load",
|
||||||
const contentMessageMid: SyncMessage = { action: "content", id: "co_zmid", new: {}, priority: CO_VALUE_PRIORITY.MEDIUM };
|
id: "co_zhigh",
|
||||||
const contentMessageLow: SyncMessage = { action: "content", id: "co_zlow", new: {}, priority: CO_VALUE_PRIORITY.LOW };
|
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 = [
|
const promises = [
|
||||||
peerState.pushOutgoingMessage(contentMessageLow),
|
peerState.pushOutgoingMessage(contentMessageLow),
|
||||||
@@ -81,12 +111,24 @@ describe("PeerState", () => {
|
|||||||
|
|
||||||
// The first message is pushed directly, the other three are queued because are waiting
|
// The first message is pushed directly, the other three are queued because are waiting
|
||||||
// for the first push to be completed.
|
// 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
|
// Load message are managed as high priority messages and having the same priority as the content message
|
||||||
// they follow the push order.
|
// they follow the push order.
|
||||||
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(2, contentMessageHigh);
|
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
|
||||||
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(3, loadMessage);
|
2,
|
||||||
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(4, contentMessageMid);
|
contentMessageHigh,
|
||||||
|
);
|
||||||
|
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
loadMessage,
|
||||||
|
);
|
||||||
|
expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(
|
||||||
|
4,
|
||||||
|
contentMessageMid,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ test("Can create account with one node, and then load it on another", async () =
|
|||||||
trace: true,
|
trace: true,
|
||||||
peer1role: "server",
|
peer1role: "server",
|
||||||
peer2role: "client",
|
peer2role: "client",
|
||||||
})
|
});
|
||||||
|
|
||||||
console.log("After connected peers");
|
console.log("After connected peers");
|
||||||
|
|
||||||
|
|||||||
@@ -726,7 +726,11 @@ test.skip("When replaying creation and transactions of a coValue as new content,
|
|||||||
crashOnClose: true,
|
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 [inRx2, inTx2] = newQueuePair();
|
||||||
const [outRx2, outTx2] = 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);
|
client1.syncManager.addPeer(serverAsPeerForClient1);
|
||||||
server.syncManager.addPeer(client1AsPeer);
|
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(
|
const [serverAsPeerForClient2, client2AsPeer] = connectedPeers(
|
||||||
"serverFor2",
|
"serverFor2",
|
||||||
@@ -926,7 +934,11 @@ test("Can sync a coValue with private transactions through a server to another c
|
|||||||
client1.syncManager.addPeer(serverAsPeer);
|
client1.syncManager.addPeer(serverAsPeer);
|
||||||
server.syncManager.addPeer(client1AsPeer);
|
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(
|
const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
|
||||||
"server",
|
"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();
|
const map = group.createMap();
|
||||||
map.set("hello", "world", "trusting");
|
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", {
|
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
|
||||||
peer1role: "server",
|
peer1role: "server",
|
||||||
|
|||||||
@@ -2,24 +2,27 @@ import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
|
|||||||
import { AgentSecret } from "cojson";
|
import { AgentSecret } from "cojson";
|
||||||
|
|
||||||
export type MinimalClerkClient = {
|
export type MinimalClerkClient = {
|
||||||
user: {
|
user:
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| {
|
||||||
unsafeMetadata: Record<string, any>;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
fullName: string | null;
|
unsafeMetadata: Record<string, any>;
|
||||||
username: string | null;
|
fullName: string | null;
|
||||||
id: string;
|
username: string | null;
|
||||||
update: (args: {
|
id: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
update: (args: {
|
||||||
unsafeMetadata: Record<string, any>;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
}) => Promise<unknown>;
|
unsafeMetadata: Record<string, any>;
|
||||||
} | null | undefined;
|
}) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class BrowserClerkAuth implements AuthMethod {
|
export class BrowserClerkAuth implements AuthMethod {
|
||||||
constructor(
|
constructor(
|
||||||
public driver: BrowserClerkAuth.Driver,
|
public driver: BrowserClerkAuth.Driver,
|
||||||
private readonly clerkClient: MinimalClerkClient
|
private readonly clerkClient: MinimalClerkClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async start(): Promise<AuthResult> {
|
async start(): Promise<AuthResult> {
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ export class BrowserDemoAuth implements AuthMethod {
|
|||||||
this.driver.onSignedIn({ logOut });
|
this.driver.onSignedIn({ logOut });
|
||||||
},
|
},
|
||||||
onError: (error: string | Error) => {
|
onError: (error: string | Error) => {
|
||||||
this.driver.onError(error)
|
this.driver.onError(error);
|
||||||
},
|
},
|
||||||
logOut: () => {
|
logOut: () => {
|
||||||
delete localStorage[localStorageKey];
|
delete localStorage[localStorageKey];
|
||||||
}
|
},
|
||||||
} satisfies AuthResult;
|
} satisfies AuthResult;
|
||||||
} else {
|
} else {
|
||||||
return new Promise<AuthResult>((resolve) => {
|
return new Promise<AuthResult>((resolve) => {
|
||||||
@@ -96,11 +96,11 @@ export class BrowserDemoAuth implements AuthMethod {
|
|||||||
this.driver.onSignedIn({ logOut });
|
this.driver.onSignedIn({ logOut });
|
||||||
},
|
},
|
||||||
onError: (error: string | Error) => {
|
onError: (error: string | Error) => {
|
||||||
this.driver.onError(error)
|
this.driver.onError(error);
|
||||||
},
|
},
|
||||||
logOut: () => {
|
logOut: () => {
|
||||||
delete localStorage[localStorageKey];
|
delete localStorage[localStorageKey];
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
existingUsers:
|
existingUsers:
|
||||||
@@ -126,11 +126,11 @@ export class BrowserDemoAuth implements AuthMethod {
|
|||||||
this.driver.onSignedIn({ logOut });
|
this.driver.onSignedIn({ logOut });
|
||||||
},
|
},
|
||||||
onError: (error: string | Error) => {
|
onError: (error: string | Error) => {
|
||||||
this.driver.onError(error)
|
this.driver.onError(error);
|
||||||
},
|
},
|
||||||
logOut: () => {
|
logOut: () => {
|
||||||
delete localStorage[localStorageKey];
|
delete localStorage[localStorageKey];
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { AgentSecret, cojsonInternals, CryptoProvider } from "cojson";
|
import { AgentSecret, cojsonInternals, CryptoProvider } from "cojson";
|
||||||
import {
|
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools";
|
||||||
Account,
|
|
||||||
AuthMethod,
|
|
||||||
AuthResult,
|
|
||||||
ID,
|
|
||||||
} from "jazz-tools";
|
|
||||||
import * as bip39 from "@scure/bip39";
|
import * as bip39 from "@scure/bip39";
|
||||||
|
|
||||||
type LocalStorageData = {
|
type LocalStorageData = {
|
||||||
|
|||||||
@@ -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";
|
import { useState, useMemo } from "react";
|
||||||
|
|
||||||
export function useJazzClerkAuth(clerk: MinimalClerkClient & {
|
export function useJazzClerkAuth(
|
||||||
signOut: () => Promise<unknown>;
|
clerk: MinimalClerkClient & {
|
||||||
}) {
|
signOut: () => Promise<unknown>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
const [state, setState] = useState<{ errors: string[] }>({ errors: [] });
|
const [state, setState] = useState<{ errors: string[] }>({ errors: [] });
|
||||||
|
|
||||||
const authMethod = useMemo(() => {
|
const authMethod = useMemo(() => {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const startSync = Command.make(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
websocket: ws as any, // TODO: fix types
|
websocket: ws as any, // TODO: fix types
|
||||||
expectPings: false,
|
expectPings: false,
|
||||||
batchingByDefault: false
|
batchingByDefault: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export function createCoValueObservable<V extends CoValue, Depth>() {
|
|||||||
onUnavailable,
|
onUnavailable,
|
||||||
);
|
);
|
||||||
|
|
||||||
return unsubscribe
|
return unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
const observable = {
|
const observable = {
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export { ImageDefinition } from "./internal.js";
|
|||||||
export { CoValueBase, type CoValueClass } from "./internal.js";
|
export { CoValueBase, type CoValueClass } from "./internal.js";
|
||||||
export type { DepthsIn, DeeplyLoaded } 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 {
|
export {
|
||||||
type AuthMethod,
|
type AuthMethod,
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ export class Ref<out V extends CoValue> {
|
|||||||
"node" in this.controlledAccount
|
"node" in this.controlledAccount
|
||||||
? this.controlledAccount.node
|
? this.controlledAccount.node
|
||||||
: this.controlledAccount._raw.core.node;
|
: this.controlledAccount._raw.core.node;
|
||||||
const raw = await node.load(
|
const raw = await node.load(this.id as unknown as CoID<RawCoValue>);
|
||||||
this.id as unknown as CoID<RawCoValue>,
|
|
||||||
);
|
|
||||||
if (raw === "unavailable") {
|
if (raw === "unavailable") {
|
||||||
return "unavailable";
|
return "unavailable";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export type SchemaFor<Field> = NonNullable<Field> extends CoValue
|
|||||||
export type Encoder<V> = {
|
export type Encoder<V> = {
|
||||||
encode: (value: V) => JsonValue;
|
encode: (value: V) => JsonValue;
|
||||||
decode: (value: JsonValue) => V;
|
decode: (value: JsonValue) => V;
|
||||||
}
|
};
|
||||||
export type OptionalEncoder<V> =
|
export type OptionalEncoder<V> =
|
||||||
| Encoder<V>
|
| Encoder<V>
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ describe("co.json TypeScript validation", () => {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
class ValidNestedMap extends CoMap {
|
class ValidNestedMap extends CoMap {
|
||||||
data = co.json<NestedType>();
|
data = co.json<NestedType>();
|
||||||
@@ -41,7 +41,7 @@ describe("co.json TypeScript validation", () => {
|
|||||||
type TypeWithOptional = {
|
type TypeWithOptional = {
|
||||||
value: string;
|
value: string;
|
||||||
optional?: string | null;
|
optional?: string | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
class ValidMap extends CoMap {
|
class ValidMap extends CoMap {
|
||||||
data = co.json<TypeWithOptional>();
|
data = co.json<TypeWithOptional>();
|
||||||
|
|||||||
@@ -170,18 +170,20 @@ describe("subscribeToCoValue", () => {
|
|||||||
const { me, meOnSecondPeer } = await setupAccount();
|
const { me, meOnSecondPeer } = await setupAccount();
|
||||||
|
|
||||||
const chatRoom = createChatRoom(me, "General");
|
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);
|
chatRoom.messages?.push(message);
|
||||||
|
|
||||||
const updateFn = vi.fn()
|
const updateFn = vi.fn();
|
||||||
|
|
||||||
const unsubscribe = subscribeToCoValue(
|
const unsubscribe = subscribeToCoValue(
|
||||||
ChatRoom,
|
ChatRoom,
|
||||||
chatRoom.id,
|
chatRoom.id,
|
||||||
meOnSecondPeer,
|
meOnSecondPeer,
|
||||||
{
|
{
|
||||||
messages: [{
|
messages: [{}],
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
updateFn,
|
updateFn,
|
||||||
);
|
);
|
||||||
@@ -200,30 +202,37 @@ describe("subscribeToCoValue", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateFn).toHaveBeenCalled();
|
expect(updateFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastValue = updateFn.mock.lastCall[0];
|
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 () => {
|
it("should handle the updates as immutable changes", async () => {
|
||||||
const { me, meOnSecondPeer } = await setupAccount();
|
const { me, meOnSecondPeer } = await setupAccount();
|
||||||
|
|
||||||
const chatRoom = createChatRoom(me, "General");
|
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!");
|
const message2 = createMessage(me, "Let's go!");
|
||||||
chatRoom.messages?.push(message);
|
chatRoom.messages?.push(message);
|
||||||
chatRoom.messages?.push(message2);
|
chatRoom.messages?.push(message2);
|
||||||
|
|
||||||
const updateFn = vi.fn()
|
const updateFn = vi.fn();
|
||||||
|
|
||||||
const unsubscribe = subscribeToCoValue(
|
const unsubscribe = subscribeToCoValue(
|
||||||
ChatRoom,
|
ChatRoom,
|
||||||
chatRoom.id,
|
chatRoom.id,
|
||||||
meOnSecondPeer,
|
meOnSecondPeer,
|
||||||
{
|
{
|
||||||
messages: [{
|
messages: [
|
||||||
reactions: [],
|
{
|
||||||
}],
|
reactions: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
updateFn,
|
updateFn,
|
||||||
);
|
);
|
||||||
@@ -249,12 +258,14 @@ describe("subscribeToCoValue", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateFn).toHaveBeenCalled();
|
expect(updateFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastValue = updateFn.mock.lastCall[0];
|
const lastValue = updateFn.mock.lastCall[0];
|
||||||
expect(lastValue).not.toBe(initialValue);
|
expect(lastValue).not.toBe(initialValue);
|
||||||
expect(lastValue.messages).not.toBe(initialMessagesList);
|
expect(lastValue.messages).not.toBe(initialMessagesList);
|
||||||
expect(lastValue.messages[0]).not.toBe(initialMessage1);
|
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
|
// This shouldn't change
|
||||||
expect(lastValue.messages[1]).toBe(initialMessage2);
|
expect(lastValue.messages[1]).toBe(initialMessage2);
|
||||||
@@ -270,21 +281,26 @@ describe("subscribeToCoValue", () => {
|
|||||||
const { me, meOnSecondPeer } = await setupAccount();
|
const { me, meOnSecondPeer } = await setupAccount();
|
||||||
|
|
||||||
const chatRoom = createChatRoom(me, "General");
|
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!");
|
const message2 = createMessage(me, "Let's go!");
|
||||||
chatRoom.messages?.push(message);
|
chatRoom.messages?.push(message);
|
||||||
chatRoom.messages?.push(message2);
|
chatRoom.messages?.push(message2);
|
||||||
|
|
||||||
const updateFn = vi.fn()
|
const updateFn = vi.fn();
|
||||||
|
|
||||||
const unsubscribe = subscribeToCoValue(
|
const unsubscribe = subscribeToCoValue(
|
||||||
ChatRoom,
|
ChatRoom,
|
||||||
chatRoom.id,
|
chatRoom.id,
|
||||||
meOnSecondPeer,
|
meOnSecondPeer,
|
||||||
{
|
{
|
||||||
messages: [{
|
messages: [
|
||||||
reactions: [],
|
{
|
||||||
}],
|
reactions: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
updateFn,
|
updateFn,
|
||||||
);
|
);
|
||||||
@@ -306,19 +322,18 @@ describe("subscribeToCoValue", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateFn).toHaveBeenCalled();
|
expect(updateFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastValue = updateFn.mock.lastCall[0];
|
const lastValue = updateFn.mock.lastCall[0];
|
||||||
expect(lastValue).not.toBe(initialValue);
|
expect(lastValue).not.toBe(initialValue);
|
||||||
expect(lastValue.name).toBe("Me and Luigi");
|
expect(lastValue.name).toBe("Me and Luigi");
|
||||||
expect(initialValue.name).toBe("General");
|
expect(initialValue.name).toBe("General");
|
||||||
|
|
||||||
expect(lastValue.messages).toBe(initialValue.messages);
|
expect(lastValue.messages).toBe(initialValue.messages);
|
||||||
expect(lastValue.messages[0]).toBe(initialValue.messages[0]);
|
expect(lastValue.messages[0]).toBe(initialValue.messages[0]);
|
||||||
expect(lastValue.messages[1]).toBe(initialValue.messages[1]);
|
expect(lastValue.messages[1]).toBe(initialValue.messages[1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function waitFor(callback: () => boolean | void) {
|
function waitFor(callback: () => boolean | void) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const checkPassed = () => {
|
const checkPassed = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user