Live video on the AT Protocol

components: add components library (#233)

* components: init @streamplace/components

* fix build

* components: implemented chat!

* components: local chat works now!

* dump tsup, remove old code

* remove example-app for now

* build: add knip for checking missing dependencies

* fix build

* update README

* maybe lexicons postinstall?

* app: remove old chat stuff from redux

* components: migrate replyTo

* components: migrate viewers

* components: migrate livestream

* components: migrate segment

* components: migrate renditions

* components: tweak handleWebsocketMessages

* libraries: pnpm run build as postinstall

* build: perhaps make js-lexicons like this?

* app: mobile tweaks

* websocket: can't send a profile we don't have

* websocket --> websocket-consumer

authored by

Eli Mallon and committed by
GitHub
b08b87de 7a300c2d

+1386 -1006
+5 -1
.vscode/settings.json
··· 16 16 "go.alternateTools": { 17 17 "customFormatter": "golangci-lint" 18 18 }, 19 - "go.formatFlags": ["fmt", "--stdin"] 19 + "go.formatFlags": ["fmt", "--stdin"], 20 + "editor.codeActionsOnSave": { 21 + "source.addMissingImports": "explicit", 22 + "source.organizeImports": "explicit" 23 + } 20 24 }
+21 -44
js/app/components/chat/chat-box.tsx
··· 1 1 import { useNavigation } from "@react-navigation/native"; 2 2 import { 3 + useChat, 4 + useCreateChatMessage, 5 + useLivestream, 6 + useReplyToMessage, 7 + useSetReplyToMessage, 8 + } from "@streamplace/components"; 9 + import { 3 10 Palette, 4 11 SquareArrowOutUpRight, 5 12 X as XIcon, ··· 9 16 import { EmojiPicker } from "components/emoji-picker/emoji-picker"; 10 17 import NameColorPicker from "components/name-color-picker/name-color-picker"; 11 18 import { 12 - chatMessage, 13 19 selectChatProfile, 14 20 selectIsReady, 15 21 selectUserProfile, 16 22 } from "features/bluesky/blueskySlice"; 17 23 import { 18 - addLocalChatMessage, 19 24 LivestreamViewHydrated, 20 - MessageViewHydrated, 21 - useChat, 22 - usePlayerActions, 23 25 usePlayerId, 24 - usePlayerLivestream, 25 - useReplyToMessage, 26 26 } from "features/player/playerSlice"; 27 27 import { 28 28 chatWarn, ··· 32 32 import { useEffect, useRef, useState } from "react"; 33 33 import { Keyboard } from "react-native"; 34 34 import { useAppDispatch, useAppSelector } from "store/hooks"; 35 + import { ChatMessageViewHydrated } from "streamplace"; 35 36 import { Button, Form, Input, isWeb, Text, TextArea, View } from "tamagui"; 36 37 import EmojiSuggestions, { EmojiSuggestion } from "./emoji-suggestions"; 37 38 import MentionSuggestions, { MentionSuggestion } from "./mention-suggestions"; 38 39 39 40 const getParticipantSuggestions = ( 40 - chat: MessageViewHydrated[], 41 + chat: ChatMessageViewHydrated[], 41 42 currentUserDid?: string, 42 43 ) => { 43 44 const participants = new Set<string>(); ··· 76 77 const chatProfile = useAppSelector(selectChatProfile); 77 78 const chatWarned = useAppSelector(selectChatWarned); 78 79 const loggedOut = isReady && !userProfile; 79 - const livestream = useAppSelector(usePlayerLivestream()); 80 - const chat = useAppSelector(useChat()); 80 + const livestream = useLivestream(); 81 + const chat = useChat(); 81 82 const textAreaRef = useRef<Input>(null); 82 83 const dispatch = useAppDispatch(); 83 84 const navigate = useNavigation(); 84 85 const playerId = usePlayerId(); 85 - const playerActions = usePlayerActions(); 86 - const replyTo = useAppSelector(useReplyToMessage()); 86 + const replyTo = useReplyToMessage(); 87 + const setReplyToMessage = useSetReplyToMessage(); 87 88 if (isWeb) usePreloadEmoji({ immediate: true }); 88 89 const [isTextAreaFocused, setIsTextAreaFocused] = useState(false); 89 90 const [pickerState, setPickerState] = useState({ ··· 309 310 } 310 311 }, [showSuggestions]); 311 312 313 + const createChatMessage = useCreateChatMessage(); 314 + 312 315 const updateSuggestions = (text: string, cursorPosition: number) => { 313 316 const atIndex = text.lastIndexOf("@", cursorPosition); 314 317 ··· 385 388 if (!isWeb) Keyboard.dismiss(); 386 389 if (!message.length || !livestream || !userProfile) return; 387 390 388 - // Add local message 389 - dispatch( 390 - addLocalChatMessage({ 391 - playerId, 392 - message, 393 - ...(replyTo ? { replyTo } : {}), 394 - author: { 395 - did: userProfile.did, 396 - handle: userProfile.handle, 397 - }, 398 - chatProfile: chatProfile?.profile?.color 399 - ? { 400 - color: { 401 - red: chatProfile.profile.color.red, 402 - green: chatProfile.profile.color.green, 403 - blue: chatProfile.profile.color.blue, 404 - }, 405 - } 406 - : undefined, 407 - }), 408 - ); 409 - 410 - // Send to server 411 - dispatch( 412 - chatMessage({ 413 - text: message, 414 - livestream, 415 - ...(replyTo ? { replyTo } : {}), 416 - }), 417 - ); 391 + createChatMessage({ 392 + text: message, 393 + reply: replyTo ? replyTo : undefined, 394 + }); 418 395 419 396 setMessage(""); 420 - playerActions.setReplyToMessage(null); 397 + setReplyToMessage(null); 421 398 setShowSuggestions(false); 422 399 if (isWeb && textAreaRef.current) { 423 400 const textarea = textAreaRef.current as unknown as HTMLTextAreaElement; ··· 493 470 <Button 494 471 size="$2" 495 472 circular 496 - onPress={() => playerActions.setReplyToMessage(null)} 473 + onPress={() => setReplyToMessage(null)} 497 474 backgroundColor="transparent" 498 475 > 499 476 <XIcon size={16} />
+27 -27
js/app/components/chat/chat.tsx
··· 1 + import { 2 + useChat, 3 + useProfile, 4 + useSetReplyToMessage, 5 + } from "@streamplace/components"; 1 6 import { Reply, Settings, X } from "@tamagui/lucide-icons"; 2 7 import { 3 8 createBlockRecord, 4 9 selectUserProfile, 5 10 } from "features/bluesky/blueskySlice"; 6 - import { 7 - MessageViewHydrated, 8 - useChat, 9 - usePlayerActions, 10 - usePlayerLivestream, 11 - } from "features/player/playerSlice"; 11 + import { MessageViewHydrated } from "features/player/playerSlice"; 12 12 import usePlatform from "hooks/usePlatform"; 13 13 import { useEffect, useRef, useState } from "react"; 14 14 import { Linking, TouchableOpacity } from "react-native"; 15 15 import { useAppDispatch, useAppSelector } from "store/hooks"; 16 16 17 - import { RichText } from "@atproto/api"; 17 + import { $Typed, RichText } from "@atproto/api"; 18 18 import { 19 19 isMention, 20 20 Link, 21 21 Mention, 22 22 } from "@atproto/api/dist/client/types/app/bsky/richtext/facet"; 23 - import { $Typed } from "@atproto/api/src/client/util"; 23 + import { ChatMessageViewHydrated } from "streamplace"; 24 24 import { Button, ScrollView, Sheet, Text, useMedia, View } from "tamagui"; 25 25 import { RichtextSegment, segmentize } from "../../utils/facet"; 26 26 ··· 32 32 setIsChatVisible: (visible: boolean) => void; 33 33 }) { 34 34 const [open, setOpen] = useState(false); 35 - const [modMessage, setMessage] = useState<MessageViewHydrated | null>(null); 35 + const [modMessage, setMessage] = useState<ChatMessageViewHydrated | null>( 36 + null, 37 + ); 36 38 const [isAtBottom, setIsAtBottom] = useState(true); 37 - const chat = useAppSelector(useChat()); 39 + const chat = useChat(); 38 40 const scrollRef = useRef<ScrollView>(null); 39 - const livestream = useAppSelector(usePlayerLivestream()); 41 + const streamerProfile = useProfile(); 40 42 const userProfile = useAppSelector(selectUserProfile); 41 43 const myStream = !!( 42 44 userProfile && 43 - livestream && 44 45 userProfile.did && 45 - livestream.author.did && 46 - userProfile.did === livestream.author.did 46 + userProfile.did === streamerProfile?.did 47 47 ); 48 48 49 49 const handleScroll = (event: any) => { ··· 187 187 replyHandle: _replyHandle, 188 188 chat, 189 189 }: { 190 - message: MessageViewHydrated; 190 + message: ChatMessageViewHydrated; 191 191 setOpen: (open: boolean) => void; 192 - setMessage: (message: MessageViewHydrated) => void; 192 + setMessage: (message: ChatMessageViewHydrated) => void; 193 193 myStream: boolean; 194 194 replyHandle?: string; 195 - chat: MessageViewHydrated[]; 195 + chat: ChatMessageViewHydrated[]; 196 196 }): JSX.Element { 197 197 const [hover, setHover] = useState(false); 198 - const playerActions = usePlayerActions(); 198 + const setReplyToMessage = useSetReplyToMessage(); 199 199 const { isWeb } = usePlatform(); 200 200 201 201 const moderateMessage = () => { ··· 207 207 }; 208 208 209 209 const handleReply = () => { 210 - playerActions.setReplyToMessage(message); 210 + setReplyToMessage(message); 211 211 }; 212 212 213 - const replyTo = message.replyTo as MessageViewHydrated | undefined; 213 + const replyTo = message.replyTo as ChatMessageViewHydrated | undefined; 214 214 const hasReply = !!replyTo; 215 215 const replyToHandle = replyTo?.author?.handle; 216 216 const replyToText = replyTo?.record?.text; ··· 353 353 message, 354 354 chat = [], 355 355 }: { 356 - message: MessageViewHydrated; 357 - chat?: MessageViewHydrated[]; 356 + message: ChatMessageViewHydrated; 357 + chat?: ChatMessageViewHydrated[]; 358 358 }) => { 359 359 const rt = new RichText({ text: message.record.text }); 360 360 rt.detectFacetsWithoutResolution(); ··· 464 464 ); 465 465 } 466 466 } else { 467 - return <Text>{obj.text}</Text>; 467 + return <Text key={`text-${index}`}>{obj.text}</Text>; 468 468 } 469 469 }; 470 470 ··· 475 475 }: { 476 476 text: string; 477 477 facets: Facet[]; 478 - chat?: MessageViewHydrated[]; 478 + chat?: ChatMessageViewHydrated[]; 479 479 }) => { 480 480 if (!facets?.length) return <Text>{text}</Text>; 481 481 482 482 let segs = segmentize(text, facets); 483 483 484 - console.log(segs); 485 - 486 - return segs.map((seg, i) => segmentedObject(seg, chat, i)); 484 + return segs.map((seg, i) => 485 + segmentedObject(seg, chat as MessageViewHydrated[], i), 486 + ); 487 487 };
+3 -1
js/app/components/edit-livestream.tsx
··· 1 + import { useLivestream } from "@streamplace/components"; 1 2 import { useToastController } from "@tamagui/toast"; 2 3 import { 3 4 selectNewLivestream, ··· 21 22 const [title, setTitle] = useState(""); 22 23 const [loading, setLoading] = useState(false); 23 24 const profile = useAppSelector(selectUserProfile); 25 + const livestream = useLivestream(); 24 26 const newLivestream = useAppSelector(selectNewLivestream); 25 27 26 28 useEffect(() => { ··· 57 59 await dispatch( 58 60 updateLivestreamRecord({ 59 61 title, 60 - playerId, 62 + livestream, 61 63 }), 62 64 ); 63 65 } catch (error) {
+23 -15
js/app/components/livestream/livestream.tsx
··· 1 + import { 2 + useLivestream, 3 + useProfile, 4 + useSegment, 5 + useViewers, 6 + } from "@streamplace/components"; 1 7 import { MessageCircleMore, MessageCircleOff } from "@tamagui/lucide-icons"; 2 8 import { useToastController } from "@tamagui/toast"; 3 9 import Chat from "components/chat/chat"; ··· 51 57 const player = useAppSelector(usePlayer()); 52 58 const profiles = useAppSelector(selectProfiles); 53 59 const toast = useToastController(); 60 + const viewers = useViewers(); 54 61 55 62 const { src, ...extraProps } = props; 56 63 const dispatch = useAppDispatch(); 57 64 const { width, height } = useWindowDimensions(); 58 - const video = player.segment?.video?.[0]; 65 + const segment = useSegment(); 66 + const video = segment?.video?.[0]; 59 67 const [videoWidth, setVideoWidth] = useState(0); 60 68 const [videoHeight, setVideoHeight] = useState(0); 61 69 const { keyboardHeight } = useKeyboard(); ··· 68 76 const [currentUserDID, setCurrentUserDID] = useState<string | null>(null); 69 77 const { fullscreen, setFullscreen } = useFullscreen(); 70 78 71 - const streamerDID = player.livestream?.author?.did; 72 - const streamerProfile = streamerDID ? profiles[streamerDID] : undefined; 79 + const livestream = useLivestream(); 80 + const streamerProfile = useProfile(); 81 + 82 + const streamerDID = livestream?.author?.did; 73 83 const streamerHandle = streamerProfile?.handle; 74 - const startTime = player.livestream?.record?.createdAt 75 - ? new Date(player.livestream?.record?.createdAt) 84 + const startTime = livestream?.record?.createdAt 85 + ? new Date(livestream?.record?.createdAt) 76 86 : undefined; 77 87 78 88 // this would all be really easy if i had library that would give me the ··· 112 122 // 10 second cut off for segements 113 123 const cuttOffDate = new Date(Date.now() - 10 * 1000); 114 124 // 15 second cut off if segment start time not found 115 - const startTime = player.segment?.startTime 116 - ? new Date(player.segment?.startTime) 125 + const startTime = segment?.startTime 126 + ? new Date(segment?.startTime) 117 127 : new Date(Date.now() - 15 * 1000); 118 128 119 129 if (startTime > cuttOffDate) { 120 130 setOffline(false); 121 131 } 122 - }, [player.segment]); 132 + }, [segment]); 123 133 124 134 let slideKeyboard = 0; 125 135 if (isIOS && keyboardHeight > 0) { ··· 178 188 zIndex: 1000, 179 189 }} 180 190 bubbleProps={{ 181 - cursor: "pointer", 182 191 backgroundColor: "$accentBackground", 183 192 gap: "$3", 184 193 maxWidth: 400, ··· 265 274 ) 266 275 } 267 276 aria-label={`View @${streamerHandle} on Bluesky`} 268 - style={{ cursor: "pointer" }} 269 277 > 270 278 {`@${streamerHandle}`} 271 279 </Text> ··· 283 291 {startTime instanceof Date && !offline && ( 284 292 <Timer start={startTime} /> 285 293 )} 286 - <Viewers viewers={player.viewers ?? 0} /> 294 + <Viewers viewers={viewers ?? 0} /> 287 295 <Button 288 296 backgroundColor="transparent" 289 297 onPress={() => setIsChatVisible(!isChatVisible)} ··· 307 315 : undefined 308 316 } 309 317 > 310 - {player.livestream?.record.title} 318 + {livestream?.record.title} 311 319 </H2> 312 320 </View> 313 321 </View> ··· 378 386 ) 379 387 } 380 388 aria-label={`View @${streamerHandle} on Bluesky`} 381 - style={{ cursor: "pointer" }} 389 + style={{ cursor: isWeb ? "pointer" : undefined }} // iOS literally crashes otherwise idk 382 390 numberOfLines={1} 383 391 ellipsizeMode="tail" 384 392 > ··· 395 403 )} 396 404 </View> 397 405 <View style={{ alignItems: "flex-end" }}> 398 - <Viewers viewers={player.viewers ?? 0} /> 406 + <Viewers viewers={viewers ?? 0} /> 399 407 </View> 400 408 </View> 401 409 <View ··· 405 413 marginTop={-15} 406 414 > 407 415 <Text fontSize={18} numberOfLines={1} ellipsizeMode="tail"> 408 - {player.livestream?.record.title} 416 + {livestream?.record.title} 409 417 </Text> 410 418 </View> 411 419 </View>
+5 -6
js/app/components/player/controls.tsx
··· 1 + import { useRenditions, useSegment, useViewers } from "@streamplace/components"; 1 2 import { 2 3 Antenna, 3 4 CheckCircle, ··· 20 21 usePlayer, 21 22 usePlayerActions, 22 23 usePlayerProtocol, 23 - usePlayerRenditions, 24 - usePlayerSegment, 25 24 usePlayerSelectedRendition, 26 25 } from "features/player/playerSlice"; 27 26 import { userMute } from "features/streamplace/streamplaceSlice"; ··· 278 277 props.setPlayTime(Date.now()); 279 278 }; 280 279 281 - const player = useAppSelector(usePlayer()); 280 + const viewers = useViewers(); 282 281 const dispatch = useAppDispatch(); 283 282 const m = useMedia(); 284 283 ··· 416 415 ) : null} 417 416 </Part> 418 417 <Part justifyContent="flex-end"> 419 - <Viewers viewers={player.viewers ?? 0} /> 418 + <Viewers viewers={viewers ?? 0} /> 420 419 </Part> 421 420 </Bar> 422 421 {props.ingest && <LiveBubble />} ··· 486 485 export function PopoverMenu(props: PlayerProps) { 487 486 const [open, setOpen] = useState(false); 488 487 const media = useMedia(); 489 - const renditions = useAppSelector(usePlayerRenditions()); 488 + const renditions = useRenditions(); 490 489 const selectedRendition = useAppSelector(usePlayerSelectedRendition()); 491 490 const protocol = useAppSelector(usePlayerProtocol()); 492 491 const { setSelectedRendition, setProtocol } = usePlayerActions(); ··· 807 806 } 808 807 809 808 export function Offline() { 810 - const segment = useAppSelector(usePlayerSegment()); 809 + const segment = useSegment(); 811 810 return ( 812 811 <View flex={1} justifyContent="center" alignItems="center"> 813 812 <View flexDirection="row">
+4 -7
js/app/components/player/player.tsx
··· 1 - import { 2 - usePlayerRenditions, 3 - usePlayerSegment, 4 - usePlayerSelectedRendition, 5 - } from "features/player/playerSlice"; 1 + import { useRenditions, useSegment } from "@streamplace/components"; 2 + import { usePlayerSelectedRendition } from "features/player/playerSlice"; 6 3 import { selectUserMuted } from "features/streamplace/streamplaceSlice"; 7 4 import usePlatform from "hooks/usePlatform"; 8 5 import useStreamplaceNode from "hooks/useStreamplaceNode"; ··· 94 91 const [offline, setOffline] = useState(true); 95 92 const playing = status === PlayerStatus.PLAYING; 96 93 97 - const segment = useAppSelector(usePlayerSegment()); 94 + const segment = useSegment(); 98 95 const [lastCheck, setLastCheck] = useState(0); 99 96 100 - const renditions = useAppSelector(usePlayerRenditions()); 97 + const renditions = useRenditions(); 101 98 const selectedRendition = useAppSelector(usePlayerSelectedRendition()); 102 99 103 100 useEffect(() => {
+11 -100
js/app/components/player/provider.tsx
··· 1 1 // basically PlayerProvider that sets up our magic context, 2 2 3 - import { 4 - newPlayer, 5 - PlayerContext, 6 - usePlayerActions, 7 - } from "features/player/playerSlice"; 8 - import { selectUrl } from "features/streamplace/streamplaceSlice"; 9 - import { useContext, useEffect, useRef, useState } from "react"; 10 - import useWebSocket, { ReadyState } from "react-use-websocket"; 11 - import { useAppDispatch, useAppSelector } from "store/hooks"; 3 + import { LivestreamProvider } from "@streamplace/components"; 4 + import { newPlayer, PlayerContext } from "features/player/playerSlice"; 5 + import { useContext, useEffect, useState } from "react"; 6 + import { ReadyState } from "react-use-websocket"; 7 + import { useAppDispatch } from "store/hooks"; 12 8 import { PlayerProps } from "./props"; 13 9 14 10 const POLL_INTERVAL = 3000; ··· 21 17 return props.children; 22 18 } 23 19 return ( 24 - <PlayerContextInitializer {...props}> 25 - {props.children} 26 - </PlayerContextInitializer> 20 + <LivestreamProvider src={props.src ?? ""}> 21 + <PlayerContextInitializer {...props}> 22 + {props.children} 23 + </PlayerContextInitializer> 24 + </LivestreamProvider> 27 25 ); 28 26 } 29 27 ··· 50 48 } 51 49 return ( 52 50 <PlayerContext.Provider value={{ playerId }}> 53 - <PlayerDataContext {...props} /> 51 + {props.children} 54 52 </PlayerContext.Provider> 55 53 ); 56 54 } ··· 62 60 [ReadyState.CLOSING]: "CLOSING", 63 61 [ReadyState.UNINSTANTIATED]: "UNINSTANTIATED", 64 62 }; 65 - 66 - export function PlayerDataContext( 67 - props: Partial<PlayerProps> & { children: React.ReactNode }, 68 - ) { 69 - const dispatch = useAppDispatch(); 70 - const { 71 - pollViewers, 72 - pollChat, 73 - pollLivestream, 74 - pollSegment, 75 - handleWebSocketMessages, 76 - } = usePlayerActions(); 77 - 78 - const url = useAppSelector(selectUrl); 79 - let wsUrl = url.replace(/^http\:/, "ws:"); 80 - wsUrl = wsUrl.replace(/^https\:/, "wss:"); 81 - 82 - const ref = useRef<any[]>([]); 83 - const handle = useRef<NodeJS.Timeout | null>(null); 84 - 85 - const { readyState } = useWebSocket(`${wsUrl}/api/websocket/${props.src}`, { 86 - reconnectInterval: 1000, 87 - shouldReconnect: () => true, 88 - 89 - onOpen: () => { 90 - ref.current = []; 91 - }, 92 - 93 - onError: (e) => { 94 - console.log("onError", e); 95 - }, 96 - 97 - // spamming the redux store with messages causes a zillion re-renders, 98 - // so we batch them up a bit 99 - onMessage: (msg) => { 100 - try { 101 - const data = JSON.parse(msg.data); 102 - ref.current.push(data); 103 - if (handle.current) { 104 - return; 105 - } 106 - handle.current = setTimeout(() => { 107 - dispatch(handleWebSocketMessages(ref.current)); 108 - ref.current = []; 109 - handle.current = null; 110 - }, 250); 111 - } catch (e) { 112 - console.log("onMessage parse error", e); 113 - } 114 - }, 115 - }); 116 - 117 - useEffect(() => { 118 - return () => { 119 - if (handle.current) { 120 - clearTimeout(handle.current); 121 - handle.current = null; 122 - } 123 - }; 124 - }, []); 125 - 126 - useEffect(() => { 127 - console.log(`websocket ${readyStateNames[readyState]}`); 128 - }, [readyState]); 129 - 130 - useEffect(() => { 131 - if (readyState === ReadyState.OPEN || !props.src) { 132 - return; 133 - } 134 - let handle; 135 - const poll = async () => { 136 - if (!props.src) { 137 - return; 138 - } 139 - await Promise.all([ 140 - dispatch(pollViewers(props.src)), 141 - dispatch(pollChat(props.src)), 142 - dispatch(pollLivestream(props.src)), 143 - dispatch(pollSegment(props.src)), 144 - ]); 145 - }; 146 - handle = setInterval(poll, POLL_INTERVAL); 147 - return () => clearInterval(handle); 148 - }, [props.src, readyState === ReadyState.OPEN]); 149 - 150 - return props.children; 151 - }
+39 -16
js/app/components/provider/provider.shared.tsx
··· 3 3 LinkingOptions, 4 4 NavigationContainer, 5 5 } from "@react-navigation/native"; 6 + import { StreamplaceProvider as ZustandStreamplaceProvider } from "@streamplace/components"; 6 7 import { ToastProvider, ToastViewport } from "@tamagui/toast"; 7 8 import { useFonts } from "expo-font"; 8 9 import BlueskyProvider from "features/bluesky/blueskyProvider"; 10 + import { selectOAuthSession } from "features/bluesky/blueskySlice"; 9 11 import StreamplaceProvider from "features/streamplace/streamplaceProvider"; 12 + import useStreamplaceNode from "hooks/useStreamplaceNode"; 10 13 import React from "react"; 11 14 import { Provider as ReduxProvider } from "react-redux"; 15 + import { useAppSelector } from "store/hooks"; 12 16 import { store } from "store/store"; 13 17 import { PortalProvider, TamaguiProvider } from "tamagui"; 14 18 import config from "tamagui.config"; ··· 26 30 <ReduxProvider store={store}> 27 31 <StreamplaceProvider> 28 32 <BlueskyProvider> 29 - <PortalProvider> 30 - <ToastProvider 31 - swipeDirection="vertical" 32 - duration={6000} 33 - native={ 34 - [ 35 - /* uncomment the next line to do native toasts on mobile. NOTE: it'll require you making a dev build and won't work with Expo Go */ 36 - // 'mobile' 37 - ] 38 - } 39 - > 40 - <FontProvider>{children}</FontProvider> 41 - <CurrentToast /> 42 - <ToastViewport name="default" top="$8" left={0} right={0} /> 43 - </ToastProvider> 44 - </PortalProvider> 33 + <NewStreamplaceProvider> 34 + <PortalProvider> 35 + <ToastProvider 36 + swipeDirection="vertical" 37 + duration={6000} 38 + native={ 39 + [ 40 + /* uncomment the next line to do native toasts on mobile. NOTE: it'll require you making a dev build and won't work with Expo Go */ 41 + // 'mobile' 42 + ] 43 + } 44 + > 45 + <FontProvider>{children}</FontProvider> 46 + <CurrentToast /> 47 + <ToastViewport name="default" top="$8" left={0} right={0} /> 48 + </ToastProvider> 49 + </PortalProvider> 50 + </NewStreamplaceProvider> 45 51 </BlueskyProvider> 46 52 </StreamplaceProvider> 47 53 </ReduxProvider> ··· 49 55 </TamaguiProvider> 50 56 ); 51 57 } 58 + 59 + export const NewStreamplaceProvider = ({ 60 + children, 61 + }: { 62 + children: React.ReactNode; 63 + }) => { 64 + const { url } = useStreamplaceNode(); 65 + const oauthSession = useAppSelector(selectOAuthSession); 66 + return ( 67 + <ZustandStreamplaceProvider 68 + url={url} 69 + oauthSession={oauthSession || undefined} 70 + > 71 + {children} 72 + </ZustandStreamplaceProvider> 73 + ); 74 + }; 52 75 53 76 export const FontProvider = ({ children }: { children: React.ReactNode }) => { 54 77 const [fontLoaded, fontError] = useFonts({
+4 -2
js/app/features/bluesky/agent.tsx js/streamplace/src/agent.ts
··· 2 2 import { schemas as parentSchemas } from "@atproto/api/dist/client/lexicons"; 3 3 import { SessionManager } from "@atproto/api/dist/session-manager"; 4 4 import { Lexicons } from "@atproto/lexicon"; 5 - import { schemas as appSchemas, PlaceNS } from "streamplace"; 5 + import { PlaceNS } from "./lexicons"; 6 + import { schemas as appSchemas } from "./lexicons/lexicons"; 7 + 6 8 export class StreamplaceAgent extends Agent { 7 9 place = new PlaceNS(this); 8 - lex: Lexicons; 10 + declare lex: Lexicons; 9 11 10 12 constructor(options: string | URL | SessionManager) { 11 13 super(options);
+8 -171
js/app/features/bluesky/blueskySlice.tsx
··· 7 7 RichText, 8 8 } from "@atproto/api"; 9 9 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 - import { 11 - isLink, 12 - isMention, 13 - } from "@atproto/api/dist/client/types/app/bsky/richtext/facet"; 14 10 import { bytesToMultibase, Secp256k1Keypair } from "@atproto/crypto"; 15 11 import { OAuthSession } from "@atproto/oauth-client"; 16 12 import { DID_KEY, hydrate, STORED_KEY_KEY } from "features/base/baseSlice"; 17 13 import { openLoginLink } from "features/platform/platformSlice"; 18 - import { 19 - LivestreamViewHydrated, 20 - MessageViewHydrated, 21 - PlayersState, 22 - } from "features/player/playerSlice"; 14 + import { PlayersState } from "features/player/playerSlice"; 23 15 import { 24 16 setURL, 25 17 StreamplaceState, 26 18 } from "features/streamplace/streamplaceSlice"; 27 19 import Storage from "storage"; 28 20 import { 29 - PlaceStreamChatMessage, 21 + LivestreamViewHydrated, 30 22 PlaceStreamChatProfile, 31 23 PlaceStreamKey, 32 24 PlaceStreamLivestream, 25 + StreamplaceAgent, 33 26 } from "streamplace"; 34 27 import { isWeb } from "tamagui"; 35 28 import { privateKeyToAccount } from "viem/accounts"; 36 29 import { createAppSlice } from "../../hooks/createSlice"; 37 - import { StreamplaceAgent } from "./agent"; 38 30 import { BlueskyState } from "./blueskyTypes"; 39 31 import createOAuthClient from "./oauthClient"; 40 32 ··· 543 535 }, 544 536 ), 545 537 546 - chatPost: create.asyncThunk( 547 - async ( 548 - { 549 - text, 550 - livestream, 551 - }: { text: string; livestream: LivestreamViewHydrated }, 552 - thunkAPI, 553 - ) => { 554 - const { bluesky, streamplace } = thunkAPI.getState() as { 555 - bluesky: BlueskyState; 556 - streamplace: StreamplaceState; 557 - }; 558 - if (!bluesky.pdsAgent) { 559 - throw new Error("No agent"); 560 - } 561 - const did = bluesky.oauthSession?.did; 562 - if (!did) { 563 - throw new Error("No DID"); 564 - } 565 - const profile = bluesky.profiles[did]; 566 - if (!profile) { 567 - throw new Error("No profile"); 568 - } 569 - if (!livestream.record.post) { 570 - throw new Error("No post"); 571 - } 572 - const record: AppBskyFeedPost.Record = { 573 - $type: "app.bsky.feed.post", 574 - text: text, 575 - createdAt: new Date().toISOString(), 576 - reply: { 577 - root: { 578 - cid: livestream.record.post.cid, 579 - uri: livestream.record.post.uri, 580 - }, 581 - parent: { 582 - cid: livestream.record.post.cid, 583 - uri: livestream.record.post.uri, 584 - }, 585 - }, 586 - }; 587 - return await bluesky.pdsAgent.post(record); 588 - }, 589 - { 590 - pending: (state) => { 591 - console.log("chatPost pending"); 592 - }, 593 - fulfilled: (state, action) => { 594 - console.log("chatPost fulfilled", action.payload); 595 - }, 596 - rejected: (state, action) => { 597 - console.error("chatPost rejected", action.error); 598 - // state.status = "failed"; 599 - }, 600 - }, 601 - ), 602 - 603 538 createBlockRecord: create.asyncThunk( 604 539 async ({ subjectDID }: { subjectDID: string }, thunkAPI) => { 605 540 const { bluesky, streamplace } = thunkAPI.getState() as { ··· 642 577 }, 643 578 ), 644 579 645 - chatMessage: create.asyncThunk( 646 - async ( 647 - { 648 - text, 649 - livestream, 650 - replyTo, 651 - }: { 652 - text: string; 653 - livestream: LivestreamViewHydrated; 654 - replyTo?: MessageViewHydrated; 655 - }, 656 - thunkAPI, 657 - ) => { 658 - const { bluesky, streamplace } = thunkAPI.getState() as { 659 - bluesky: BlueskyState; 660 - streamplace: StreamplaceState; 661 - }; 662 - if (!bluesky.pdsAgent) { 663 - throw new Error("No agent"); 664 - } 665 - const did = bluesky.oauthSession?.did; 666 - if (!did) { 667 - throw new Error("No DID"); 668 - } 669 - const profile = bluesky.profiles[did]; 670 - if (!profile) { 671 - throw new Error("No profile"); 672 - } 673 - 674 - const rt = new RichText({ text }); 675 - rt.detectFacetsWithoutResolution(); 676 - 677 - const record: PlaceStreamChatMessage.Record = { 678 - text: text, 679 - createdAt: new Date().toISOString(), 680 - streamer: livestream.author.did, 681 - ...(replyTo 682 - ? { 683 - reply: { 684 - root: { 685 - cid: replyTo.cid, 686 - uri: replyTo.uri, 687 - }, 688 - parent: { 689 - cid: replyTo.cid, 690 - uri: replyTo.uri, 691 - }, 692 - }, 693 - } 694 - : {}), 695 - ...(rt.facets && rt.facets.length > 0 696 - ? { 697 - facets: rt.facets.map((facet) => ({ 698 - index: facet.index, 699 - features: facet.features 700 - .filter( 701 - (feature) => 702 - feature.$type === "app.bsky.richtext.facet#link" || 703 - feature.$type === "app.bsky.richtext.facet#mention", 704 - ) 705 - .map((feature) => { 706 - if (isLink(feature)) { 707 - return { 708 - $type: "app.bsky.richtext.facet#link", 709 - uri: feature.uri, 710 - }; 711 - } else if (isMention(feature)) { 712 - return { 713 - $type: "app.bsky.richtext.facet#mention", 714 - did: feature.did, 715 - }; 716 - } else { 717 - throw new Error("invalid code path"); 718 - } 719 - }), 720 - })), 721 - } 722 - : {}), 723 - }; 724 - await bluesky.pdsAgent.com.atproto.repo.createRecord({ 725 - repo: did, 726 - collection: "place.stream.chat.message", 727 - record, 728 - }); 729 - }, 730 - { 731 - pending: (state) => { 732 - console.log("chatMessage pending"); 733 - }, 734 - fulfilled: (state, action) => { 735 - console.log("chatMessage fulfilled", action.payload); 736 - }, 737 - rejected: (state, action) => { 738 - console.error("chatMessage rejected", action.error); 739 - // state.status = "failed"; 740 - }, 741 - }, 742 - ), 743 - 744 580 createStreamKeyRecord: create.asyncThunk( 745 581 async ({ store }: { store: boolean }, thunkAPI) => { 746 582 const { bluesky } = thunkAPI.getState() as { ··· 991 827 992 828 updateLivestreamRecord: create.asyncThunk( 993 829 async ( 994 - { title, playerId }: { title: string; playerId: string }, 830 + { 831 + title, 832 + livestream, 833 + }: { title: string; livestream: LivestreamViewHydrated | null }, 995 834 thunkAPI, 996 835 ) => { 997 836 const now = new Date(); ··· 1013 852 throw new Error("No profile"); 1014 853 } 1015 854 1016 - let oldRecord = player[playerId].livestream; 855 + let oldRecord = livestream; 1017 856 if (!oldRecord) { 1018 857 throw new Error("No latest record"); 1019 858 } ··· 1344 1183 updateLivestreamRecord, 1345 1184 createChatProfileRecord, 1346 1185 getChatProfileRecordFromPDS, 1347 - chatPost, 1348 - chatMessage, 1349 1186 createBlockRecord, 1350 1187 followUser, 1351 1188 unfollowUser,
+5 -2
js/app/features/bluesky/blueskyTypes.tsx
··· 1 1 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 2 2 import { OAuthSession } from "@streamplace/atproto-oauth-client-react-native"; 3 3 import { StreamKey } from "features/base/baseSlice"; 4 - import { PlaceStreamChatProfile, PlaceStreamLivestream } from "streamplace"; 5 - import { StreamplaceAgent } from "./agent"; 4 + import { 5 + PlaceStreamChatProfile, 6 + PlaceStreamLivestream, 7 + StreamplaceAgent, 8 + } from "streamplace"; 6 9 import { StreamplaceOAuthClient } from "./oauthClient"; 7 10 8 11 type NewLivestream = {
+2 -483
js/app/features/player/playerSlice.tsx
··· 1 1 import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; 2 2 import { createAction } from "@reduxjs/toolkit"; 3 3 import { PROTOCOL_HLS, PROTOCOL_WEBRTC } from "components/player/props"; 4 - import { StreamplaceState } from "features/streamplace/streamplaceSlice"; 5 4 import { uuidv7 } from "hooks/uuid"; 6 5 import { createContext, useContext } from "react"; 7 6 import { useAppDispatch } from "store/hooks"; 8 - import { 9 - PlaceStreamChatDefs, 10 - PlaceStreamChatMessage, 11 - PlaceStreamDefs, 12 - PlaceStreamLivestream, 13 - PlaceStreamSegment, 14 - } from "streamplace"; 7 + import { PlaceStreamChatMessage, PlaceStreamLivestream } from "streamplace"; 15 8 import { createAppSlice } from "../../hooks/createSlice"; 16 9 17 10 export interface PlayerContextType { ··· 32 25 record: PlaceStreamChatMessage.Record; 33 26 indexedAt: string; 34 27 chatProfile?: ChatProfile; 35 - replyTo?: MessageViewHydrated; 36 28 } 37 29 38 30 export const PlayerContext = createContext<PlayerContextType>({ 39 31 playerId: null, 40 32 }); 41 33 42 - interface SegmentMediadataVideo { 43 - width: number; 44 - height: number; 45 - framerate: string; 46 - } 47 - 48 - interface SegmentMediadataAudio { 49 - rate: number; 50 - channels: number; 51 - } 52 - 53 - interface SegmentMediaData { 54 - video: SegmentMediadataVideo[]; 55 - audio: SegmentMediadataAudio[]; 56 - } 57 - 58 34 export interface PlayerState { 59 35 ingestStarted: number | null; 60 36 ingestStarting: boolean; 61 37 ingestConnectionState: RTCPeerConnectionState | null; 62 - viewers: number | null; 63 - chat: { [key: string]: MessageViewHydrated }; 64 - chatList: MessageViewHydrated[]; 65 - livestream: LivestreamViewHydrated | null; 66 - segment: PlaceStreamSegment.Record | null; 67 - renditions: PlaceStreamDefs.Rendition[]; 68 38 selectedRendition: string | null; 69 39 protocol: string; 70 - replyToMessage: MessageViewHydrated | null; 71 40 } 72 41 73 42 export interface PlayersState { ··· 97 66 handle: string; 98 67 } 99 68 100 - export const addLocalChatMessage = createAction( 101 - "player/addLocalChatMessage", 102 - ({ 103 - playerId, 104 - message, 105 - replyTo, 106 - author, 107 - chatProfile, 108 - }: { 109 - playerId: string; 110 - message: string; 111 - replyTo?: MessageViewHydrated; 112 - author: Author; 113 - chatProfile?: ChatProfile; 114 - }) => { 115 - const timestamp = Date.now(); 116 - const createdAt = new Date().toISOString(); 117 - 118 - const localMessage = { 119 - uri: `local-${timestamp}`, 120 - cid: `local-${timestamp}`, 121 - indexedAt: createdAt, 122 - author, 123 - record: { 124 - text: message, 125 - createdAt, 126 - ...(replyTo && { 127 - reply: { 128 - parent: { cid: replyTo.cid, uri: replyTo.uri }, 129 - root: { cid: replyTo.cid, uri: replyTo.uri }, 130 - }, 131 - }), 132 - }, 133 - ...(replyTo && { replyTo }), 134 - ...(chatProfile && { chatProfile }), 135 - } as MessageViewHydrated; 136 - 137 - return { 138 - payload: { playerId, message: localMessage }, 139 - }; 140 - }, 141 - ); 142 - 143 69 export const usePlayerId = () => { 144 70 const { playerId } = useContext(PlayerContext); 145 71 if (!playerId) { ··· 148 74 return playerId; 149 75 }; 150 76 151 - const reduceChat = ( 152 - state: PlayerState, 153 - messages: MessageViewHydrated[], 154 - blocks: PlaceStreamDefs.BlockView[], 155 - ): PlayerState => { 156 - state = { ...state } as PlayerState; 157 - const newChat: { [key: string]: MessageViewHydrated } = { ...state.chat }; 158 - 159 - // Add new messages 160 - for (let message of messages) { 161 - const date = new Date(message.record.createdAt); 162 - const key = `${date.getTime()}-${message.uri}`; 163 - 164 - // Remove existing local message matching the server one 165 - if (!message.uri.startsWith("local-")) { 166 - const existingLocalMessageKey = Object.keys(newChat).find((k) => { 167 - const msg = newChat[k]; 168 - return ( 169 - msg.uri.startsWith("local-") && 170 - msg.record.text === message.record.text && 171 - msg.author.did === message.author.did 172 - ); 173 - }); 174 - 175 - if (existingLocalMessageKey) { 176 - delete newChat[existingLocalMessageKey]; 177 - } 178 - } 179 - 180 - // Handle reply information for local-first messages 181 - if (message.record.reply) { 182 - const reply = message.record.reply as { 183 - parent?: { uri: string; cid: string }; 184 - root?: { uri: string; cid: string }; 185 - }; 186 - 187 - const parentUri = reply?.parent?.uri || reply?.root?.uri; 188 - 189 - if (parentUri) { 190 - // First try to find the parent message in our chat 191 - const parentMsgKey = Object.keys(newChat).find( 192 - (k) => newChat[k].uri === parentUri, 193 - ); 194 - 195 - if (parentMsgKey) { 196 - // Found the parent message, add its info to our message 197 - const parentMsg = newChat[parentMsgKey]; 198 - message = { 199 - ...message, 200 - replyTo: { 201 - cid: parentMsg.cid, 202 - uri: parentMsg.uri, 203 - author: parentMsg.author, 204 - record: parentMsg.record, 205 - chatProfile: parentMsg.chatProfile, 206 - indexedAt: parentMsg.indexedAt, 207 - }, 208 - }; 209 - } 210 - } 211 - } 212 - 213 - newChat[key] = message; 214 - } 215 - 216 - for (const block of blocks) { 217 - for (const [k, v] of Object.entries(newChat)) { 218 - if (v.author.did === block.record.subject) { 219 - delete newChat[k]; 220 - } 221 - } 222 - } 223 - 224 - const newChatList = Object.keys(newChat) 225 - .sort((a, b) => (a > b ? 1 : -1)) 226 - .map((key) => newChat[key]); 227 - 228 - return { 229 - ...state, 230 - chat: newChat, 231 - chatList: newChatList, 232 - }; 233 - }; 234 - 235 77 export const playerSlice = createAppSlice({ 236 78 name: "player", 237 79 initialState, ··· 247 89 ingestStarted: null, 248 90 ingestStarting: false, 249 91 ingestConnectionState: null, 250 - viewers: null, 251 92 protocol: action.payload.forceProtocol ?? PROTOCOL_WEBRTC, 252 - chat: {}, 253 - chatList: [], 254 - livestream: null, 255 - segment: null, 256 - renditions: [], 257 93 selectedRendition: "source", 258 - replyToMessage: null, 259 94 }; 260 95 }, 261 96 ); ··· 302 137 }, 303 138 ), 304 139 305 - handleWebSocketMessages: create.reducer( 306 - ( 307 - state, 308 - action: { 309 - payload: { playerId: string; messages: any[] }; 310 - type: string; 311 - }, 312 - ) => { 313 - for (const message of action.payload.messages) { 314 - if (PlaceStreamLivestream.isLivestreamView(message)) { 315 - state = { 316 - ...state, 317 - [action.payload.playerId]: { 318 - ...state[action.payload.playerId], 319 - livestream: message as LivestreamViewHydrated, 320 - }, 321 - }; 322 - } else if (PlaceStreamLivestream.isViewerCount(message)) { 323 - state = { 324 - ...state, 325 - [action.payload.playerId]: { 326 - ...state[action.payload.playerId], 327 - viewers: message.count, 328 - }, 329 - }; 330 - } else if (PlaceStreamChatDefs.isMessageView(message)) { 331 - // Explicitly map MessageView to MessageViewHydrated 332 - const hydrated: MessageViewHydrated = { 333 - uri: message.uri, 334 - cid: message.cid, 335 - author: message.author, 336 - record: message.record as PlaceStreamChatMessage.Record, 337 - indexedAt: message.indexedAt, 338 - chatProfile: (message as any).chatProfile, 339 - replyTo: (message as any).replyTo, 340 - }; 341 - state = { 342 - ...state, 343 - [action.payload.playerId]: reduceChat( 344 - state[action.payload.playerId] as PlayerState, 345 - [hydrated], 346 - [], 347 - ), 348 - }; 349 - } else if (PlaceStreamSegment.isRecord(message)) { 350 - state = { 351 - ...state, 352 - [action.payload.playerId]: { 353 - ...state[action.payload.playerId], 354 - segment: message as PlaceStreamSegment.Record, 355 - }, 356 - }; 357 - } else if (PlaceStreamDefs.isBlockView(message)) { 358 - const block = message as PlaceStreamDefs.BlockView; 359 - state = { 360 - ...state, 361 - [action.payload.playerId]: reduceChat( 362 - state[action.payload.playerId] as PlayerState, 363 - [], 364 - [block], 365 - ), 366 - }; 367 - } else if (PlaceStreamDefs.isRenditions(message)) { 368 - state = { 369 - ...state, 370 - [action.payload.playerId]: { 371 - ...state[action.payload.playerId], 372 - renditions: message.renditions, 373 - }, 374 - }; 375 - } 376 - } 377 - return state; 378 - }, 379 - ), 380 - 381 - addLocalChatMessage: create.reducer( 382 - ( 383 - state, 384 - action: { 385 - payload: { 386 - playerId: string; 387 - message: MessageViewHydrated; 388 - }; 389 - type: string; 390 - }, 391 - ) => { 392 - const { playerId, message } = action.payload; 393 - if (!state[playerId]) return state; 394 - 395 - const playerState = { ...state[playerId] } as PlayerState; 396 - 397 - const newChat = { ...playerState.chat }; 398 - const date = new Date(message.record.createdAt); 399 - const key = `${date.getTime()}-${message.uri}`; 400 - newChat[key] = message; 401 - 402 - const newChatList = Object.keys(newChat) 403 - .sort((a, b) => (a > b ? 1 : -1)) 404 - .map((key) => newChat[key]); 405 - 406 - return { 407 - ...state, 408 - [playerId]: { 409 - ...playerState, 410 - chat: newChat, 411 - chatList: newChatList, 412 - }, 413 - }; 414 - }, 415 - ), 416 - 417 - setReplyToMessage: create.reducer( 418 - ( 419 - state, 420 - action: { 421 - payload: { playerId: string; message: MessageViewHydrated | null }; 422 - type: string; 423 - }, 424 - ) => { 425 - return { 426 - ...state, 427 - [action.payload.playerId]: { 428 - ...state[action.payload.playerId], 429 - replyToMessage: action.payload.message, 430 - }, 431 - }; 432 - }, 433 - ), 434 - 435 - pollViewers: create.asyncThunk( 436 - async ( 437 - { playerId, user }: { playerId: string; user: string }, 438 - { getState }, 439 - ) => { 440 - const { streamplace } = getState() as { 441 - streamplace: StreamplaceState; 442 - }; 443 - const res = await fetch(`${streamplace.url}/api/view-count/${user}`); 444 - const data = (await res.json()) as PlaceStreamLivestream.ViewerCount; 445 - return { playerId, count: data.count }; 446 - }, 447 - { 448 - pending: (state) => { 449 - // state.status = "loading"; 450 - }, 451 - fulfilled: (state, result) => { 452 - return { 453 - ...state, 454 - [result.payload.playerId]: { 455 - ...state[result.payload.playerId], 456 - viewers: result.payload.count, 457 - }, 458 - }; 459 - }, 460 - rejected: (state, error) => { 461 - console.error("pollViewers rejected", error); 462 - return state; 463 - }, 464 - }, 465 - ), 466 - 467 - pollChat: create.asyncThunk( 468 - async ( 469 - { playerId, user }: { playerId: string; user: string }, 470 - { getState }, 471 - ) => { 472 - const { streamplace } = getState() as { 473 - streamplace: StreamplaceState; 474 - }; 475 - const res = await fetch(`${streamplace.url}/api/chat/${user}`); 476 - const data = (await res.json()) as MessageViewHydrated[]; 477 - return { playerId, chat: data }; 478 - }, 479 - { 480 - pending: (state) => { 481 - // state.status = "loading"; 482 - }, 483 - fulfilled: (state, result) => { 484 - return { 485 - ...state, 486 - [result.payload.playerId]: reduceChat( 487 - state[result.payload.playerId] as PlayerState, 488 - result.payload.chat, 489 - [], 490 - ), 491 - }; 492 - }, 493 - rejected: (state, error) => { 494 - console.error("pollViewers rejected", error); 495 - return state; 496 - }, 497 - }, 498 - ), 499 - 500 - pollLivestream: create.asyncThunk( 501 - async ( 502 - { playerId, user }: { playerId: string; user: string }, 503 - { getState }, 504 - ) => { 505 - const { streamplace } = getState() as { 506 - streamplace: StreamplaceState; 507 - }; 508 - const res = await fetch(`${streamplace.url}/api/livestream/${user}`); 509 - const data = (await res.json()) as LivestreamViewHydrated; 510 - return { playerId, livestream: data }; 511 - }, 512 - { 513 - pending: (state) => { 514 - // state.status = "loading"; 515 - }, 516 - fulfilled: (state, result) => { 517 - return { 518 - ...state, 519 - [result.payload.playerId]: { 520 - ...state[result.payload.playerId], 521 - livestream: result.payload.livestream, 522 - }, 523 - }; 524 - }, 525 - rejected: (state, error) => { 526 - console.error("pollViewers rejected", error); 527 - return state; 528 - }, 529 - }, 530 - ), 531 - 532 - pollSegment: create.asyncThunk( 533 - async ( 534 - { playerId, user }: { playerId: string; user: string }, 535 - { getState }, 536 - ) => { 537 - const { streamplace } = getState() as { 538 - streamplace: StreamplaceState; 539 - }; 540 - const res = await fetch( 541 - `${streamplace.url}/api/segment/recent/${user}`, 542 - ); 543 - const data = (await res.json()) as PlaceStreamSegment.Record; 544 - return { playerId, segment: data }; 545 - }, 546 - { 547 - pending: (state) => { 548 - // state.status = "loading"; 549 - }, 550 - fulfilled: (state, result) => { 551 - return { 552 - ...state, 553 - [result.payload.playerId]: { 554 - ...state[result.payload.playerId], 555 - segment: result.payload.segment, 556 - }, 557 - }; 558 - }, 559 - rejected: (state, error) => { 560 - console.error("pollViewers rejected", error); 561 - return state; 562 - }, 563 - }, 564 - ), 565 - 566 140 setSelectedRendition: create.reducer( 567 141 ( 568 142 state, ··· 612 186 selectors: { 613 187 selectPlayer: (state, playerId: string) => { 614 188 return state[playerId]; 615 - }, 616 - selectChat: (state, playerId: string) => { 617 - return state[playerId].chat; 618 - }, 619 - selectLivestream: (state, playerId: string) => { 620 - return state[playerId].livestream; 621 - }, 622 - selectSegment: (state, playerId: string) => { 623 - return state[playerId].segment; 624 - }, 625 - selectRenditions: (state, playerId: string) => { 626 - return state[playerId].renditions; 627 189 }, 628 190 selectSelectedRendition: (state, playerId: string) => { 629 191 return state[playerId].selectedRendition; ··· 647 209 ingestConnectionState, 648 210 }); 649 211 }, 650 - pollViewers: (user: string) => 651 - playerSlice.actions.pollViewers({ playerId, user }), 652 - pollChat: (user: string) => 653 - playerSlice.actions.pollChat({ playerId, user }), 654 - pollLivestream: (user: string) => 655 - playerSlice.actions.pollLivestream({ playerId, user }), 656 - pollSegment: (user: string) => 657 - playerSlice.actions.pollSegment({ playerId, user }), 658 - handleWebSocketMessages: (messages: any[]) => 659 - playerSlice.actions.handleWebSocketMessages({ playerId, messages }), 660 212 setSelectedRendition: (rendition: string) => 661 213 playerSlice.actions.setSelectedRendition({ playerId, rendition }), 662 214 setProtocol: (protocol: string) => 663 215 playerSlice.actions.setProtocol({ playerId, protocol }), 664 - setReplyToMessage: (message: MessageViewHydrated | null) => 665 - dispatch(playerSlice.actions.setReplyToMessage({ playerId, message })), 666 216 }; 667 217 }; 668 218 669 219 // Action creators are generated for each case reducer function. 670 - export const { selectPlayer, selectChat, selectLivestream, selectSegment } = 671 - playerSlice.selectors; 220 + export const { selectPlayer } = playerSlice.selectors; 672 221 export const usePlayer = (): ((state: { 673 222 player: PlayersState; 674 223 }) => PlayerState) => { 675 224 const playerId = usePlayerId(); 676 225 return (state) => state.player[playerId]; 677 226 }; 678 - export const useChat = (): ((state: { 679 - player: PlayersState; 680 - }) => MessageViewHydrated[] | null) => { 681 - const playerId = usePlayerId(); 682 - return (state) => state.player[playerId].chatList; 683 - }; 684 - export const usePlayerLivestream = (): ((state: { 685 - player: PlayersState; 686 - }) => LivestreamViewHydrated | null) => { 687 - const playerId = usePlayerId(); 688 - return (state) => state.player[playerId].livestream; 689 - }; 690 - export const usePlayerSegment = (): ((state: { 691 - player: PlayersState; 692 - }) => PlaceStreamSegment.Record | null) => { 693 - const playerId = usePlayerId(); 694 - return (state) => state.player[playerId].segment; 695 - }; 696 - export const usePlayerRenditions = (): ((state: { 697 - player: PlayersState; 698 - }) => PlaceStreamDefs.Rendition[]) => { 699 - const playerId = usePlayerId(); 700 - return (state) => state.player[playerId].renditions; 701 - }; 702 227 export const usePlayerSelectedRendition = (): ((state: { 703 228 player: PlayersState; 704 229 }) => string | null) => { ··· 711 236 const playerId = usePlayerId(); 712 237 return (state) => state.player[playerId].protocol; 713 238 }; 714 - export const useReplyToMessage = (): ((state: { 715 - player: PlayersState; 716 - }) => MessageViewHydrated | null) => { 717 - const playerId = usePlayerId(); 718 - return (state) => state.player[playerId].replyToMessage; 719 - };
+2 -2
js/app/features/streamplace/streamplaceSlice.tsx
··· 273 273 throw new Error("no oauthSession"); 274 274 } 275 275 return await bluesky.pdsAgent.place.stream.live.getSegments({ 276 - userDID: bluesky.oauthSession.did, 276 + userDID: bluesky.oauthSession?.did ?? "", 277 277 }); 278 278 }, 279 279 { ··· 289 289 }; 290 290 }, 291 291 rejected: (state, err) => { 292 - console.error("pollMySegments rejected", err); 292 + // console.error("pollMySegments rejected", err); 293 293 return { 294 294 ...state, 295 295 };
+1
js/app/package.json
··· 41 41 "@react-navigation/native-stack": "^6.11.0", 42 42 "@reduxjs/toolkit": "^2.3.0", 43 43 "@streamplace/atproto-oauth-client-react-native": "workspace:*", 44 + "@streamplace/components": "workspace:*", 44 45 "@tamagui/config": "^1.123.17", 45 46 "@tamagui/lucide-icons": "^1.123.17", 46 47 "@tamagui/toast": "^1.123.17",
+11
js/app/src/router.tsx
··· 80 80 import { store } from "store/store"; 81 81 import HomeScreen from "./screens/home"; 82 82 83 + import { 84 + configureReanimatedLogger, 85 + ReanimatedLogLevel, 86 + } from "react-native-reanimated"; 87 + 88 + // slows down the whole app 89 + configureReanimatedLogger({ 90 + level: ReanimatedLogLevel.warn, 91 + strict: false, 92 + }); 93 + 83 94 store.dispatch(loadStateFromStorage()); 84 95 85 96 const Stack = createNativeStackNavigator();
+27 -22
js/app/src/screens/live-dashboard.tsx
··· 1 + import { LivestreamProvider } from "@streamplace/components"; 1 2 import { Camera, FerrisWheel, X } from "@tamagui/lucide-icons"; 2 3 import { Redirect } from "components/aqlink"; 3 4 import CreateLivestream from "components/create-livestream"; ··· 96 97 ); 97 98 } 98 99 return ( 99 - <VideoElementProvider videoElement={videoElement}> 100 - <View f={1} ai="stretch" jc="center"> 101 - <View f={1} fb={0}> 102 - {topPane} 103 - {closeButton} 104 - </View> 105 - <View f={1} ai="center" jc="center" fb={0}> 106 - <ButtonSelector 107 - values={[ 108 - { label: "Create", value: "create" }, 109 - { label: "Update", value: "update" }, 110 - ]} 111 - disabledValues={playerId ? [] : ["update"]} 112 - selectedValue={page} 113 - setSelectedValue={setPage} 114 - maxWidth={250} 115 - width="100%" 116 - /> 117 - {page === "update" ? <UpdateLivestream playerId={playerId} /> : null} 118 - {page === "create" ? <CreateLivestream /> : null} 100 + <LivestreamProvider src={userProfile.did}> 101 + <VideoElementProvider videoElement={videoElement}> 102 + <View f={1} ai="stretch" jc="center"> 103 + <View f={1} fb={0}> 104 + {topPane} 105 + {closeButton} 106 + </View> 107 + <View f={1} ai="center" jc="center" fb={0}> 108 + <ButtonSelector 109 + values={[ 110 + { label: "Create", value: "create" }, 111 + { label: "Update", value: "update" }, 112 + ]} 113 + disabledValues={playerId ? [] : ["update"]} 114 + selectedValue={page} 115 + setSelectedValue={setPage} 116 + maxWidth={250} 117 + width="100%" 118 + /> 119 + {page === "update" ? ( 120 + <UpdateLivestream playerId={playerId} /> 121 + ) : null} 122 + {page === "create" ? <CreateLivestream /> : null} 123 + </View> 119 124 </View> 120 - </View> 121 - </VideoElementProvider> 125 + </VideoElementProvider> 126 + </LivestreamProvider> 122 127 ); 123 128 } 124 129
+1 -1
js/app/store/store.tsx
··· 34 34 // Ignore these field paths in all actions 35 35 ignoredActionPaths: ["payload"], 36 36 // Ignore these paths in the state 37 - ignoredPaths: [/^bluesky\..*/], 37 + ignoredPaths: [/^bluesky\..*/, /^streamplace\..*/], 38 38 }, 39 39 }).prepend(listenerMiddleware.middleware); 40 40 },
+2 -1
js/atproto-oauth-client-react-native/package.json
··· 25 25 } 26 26 }, 27 27 "files": [ 28 - "dist" 28 + "dist", 29 + "src" 29 30 ], 30 31 "dependencies": { 31 32 "@atproto-labs/did-resolver": "0.1.12",
+35
js/components/README.md
··· 1 + # @streamplace/components 2 + 3 + Heavily WIP but looks something like this: 4 + 5 + ```tsx 6 + import { 7 + StreamplaceProvider, 8 + LivestreamProvider, 9 + } from "@streamplace/components"; 10 + 11 + export function Provider() { 12 + <StreamplaceProvider url="https://stream.place" oauthSession={userSession}> 13 + {/* Everything inside of here can access that Streamplace node */} 14 + 15 + <LivestreamProvider src="example.bsky.social" /* or did:plc:xxxx */> 16 + {/* Everything in here has an active subscription to the livestream 17 + context via Websocket; things like chat data and stream title */} 18 + <App /> 19 + </LivestreamProvider> 20 + </StreamplaceProvider>; 21 + } 22 + 23 + export function App() { 24 + const chat = useChat(); 25 + return ( 26 + <View> 27 + {chat.map((msg) => ( 28 + <Text> 29 + @{msg.author.handle}: {msg.record.text} 30 + </Text> 31 + ))} 32 + </View> 33 + ); 34 + } 35 + ```
+38
js/components/package.json
··· 1 + { 2 + "name": "@streamplace/components", 3 + "version": "0.0.1", 4 + "description": "Streamplace React (Native) Components", 5 + "type": "module", 6 + "main": "dist/index.js", 7 + "types": "src/index.tsx", 8 + "exports": { 9 + ".": { 10 + "types": "./dist/index.d.mjs", 11 + "default": "./dist/index.mjs" 12 + } 13 + }, 14 + "scripts": { 15 + "build": "tsc", 16 + "postinstall": "pnpm run build", 17 + "start": "tsc --watch --preserveWatchOutput" 18 + }, 19 + "keywords": [ 20 + "streamplace" 21 + ], 22 + "author": "Streamplace", 23 + "license": "MIT", 24 + "packageManager": "pnpm@10.11.0", 25 + "devDependencies": { 26 + "tsup": "^8.5.0" 27 + }, 28 + "dependencies": { 29 + "@atproto/api": "^0.15.7", 30 + "react-native": "0.76.2", 31 + "react-use-websocket": "^4.13.0", 32 + "streamplace": "workspace:*", 33 + "zustand": "^5.0.5" 34 + }, 35 + "peerDependencies": { 36 + "react": "*" 37 + } 38 + }
+4
js/components/src/index.tsx
··· 1 + export * from "./livestream-provider"; 2 + export * from "./livestream-store"; 3 + export * from "./streamplace-provider"; 4 + export * from "./streamplace-store";
+37
js/components/src/livestream-provider/index.tsx
··· 1 + import React, { useContext, useRef } from "react"; 2 + import { LivestreamContext, makeLivestreamStore } from "../livestream-store"; 3 + import { useLivestreamWebsocket } from "./websocket"; 4 + 5 + export function LivestreamProvider({ 6 + children, 7 + src, 8 + }: { 9 + children: React.ReactNode; 10 + src: string; 11 + }) { 12 + const context = useContext(LivestreamContext); 13 + const store = useRef(makeLivestreamStore()).current; 14 + if (context) { 15 + // this is ok, there's use cases for having one in another 16 + // like having a player component that's independently usable 17 + // but can also be embedded within an entire livestream page 18 + return <>{children}</>; 19 + } 20 + (window as any).livestreamStore = store; 21 + return ( 22 + <LivestreamContext.Provider value={{ store: store }}> 23 + <LivestreamPoller src={src}>{children}</LivestreamPoller> 24 + </LivestreamContext.Provider> 25 + ); 26 + } 27 + 28 + export function LivestreamPoller({ 29 + children, 30 + src, 31 + }: { 32 + children: React.ReactNode; 33 + src: string; 34 + }) { 35 + useLivestreamWebsocket(src); 36 + return <>{children}</>; 37 + }
+47
js/components/src/livestream-provider/websocket.tsx
··· 1 + import { useRef } from "react"; 2 + import useWebSocket from "react-use-websocket"; 3 + import { useHandleWebsocketMessages } from "../livestream-store"; 4 + import { useUrl } from "../streamplace-store"; 5 + 6 + export function useLivestreamWebsocket(src: string) { 7 + const url = useUrl(); 8 + const handleWebSocketMessages = useHandleWebsocketMessages(); 9 + 10 + let wsUrl = url.replace(/^http\:/, "ws:"); 11 + wsUrl = wsUrl.replace(/^https\:/, "wss:"); 12 + 13 + const ref = useRef<any[]>([]); 14 + const handle = useRef<NodeJS.Timeout | null>(null); 15 + 16 + const { readyState } = useWebSocket(`${wsUrl}/api/websocket/${src}`, { 17 + reconnectInterval: 1000, 18 + shouldReconnect: () => true, 19 + 20 + onOpen: () => { 21 + ref.current = []; 22 + }, 23 + 24 + onError: (e) => { 25 + console.log("onError", e); 26 + }, 27 + 28 + // spamming the redux store with messages causes a zillion re-renders, 29 + // so we batch them up a bit 30 + onMessage: (msg) => { 31 + try { 32 + const data = JSON.parse(msg.data); 33 + ref.current.push(data); 34 + if (handle.current) { 35 + return; 36 + } 37 + handle.current = setTimeout(() => { 38 + handleWebSocketMessages(ref.current); 39 + ref.current = []; 40 + handle.current = null; 41 + }, 250); 42 + } catch (e) { 43 + console.log("onMessage parse error", e); 44 + } 45 + }, 46 + }); 47 + }
+212
js/components/src/livestream-store/chat.tsx
··· 1 + import { RichText } from "@atproto/api"; 2 + import { 3 + isLink, 4 + isMention, 5 + } from "@atproto/api/dist/client/types/app/bsky/richtext/facet"; 6 + import { 7 + ChatMessageViewHydrated, 8 + PlaceStreamChatMessage, 9 + PlaceStreamDefs, 10 + } from "streamplace"; 11 + import { useChatProfile, useDID, useHandle } from "../streamplace-store"; 12 + import { usePDSAgent } from "../streamplace-store/xrpc"; 13 + import { LivestreamState } from "./livestream-state"; 14 + import { getStoreFromContext, useLivestreamStore } from "./livestream-store"; 15 + 16 + export const useReplyToMessage = () => 17 + useLivestreamStore((state) => state.replyToMessage); 18 + 19 + export const useSetReplyToMessage = () => { 20 + const store = getStoreFromContext(); 21 + return (message: ChatMessageViewHydrated | null) => { 22 + store.setState({ replyToMessage: message }); 23 + }; 24 + }; 25 + 26 + export type NewChatMessage = { 27 + text: string; 28 + reply?: { 29 + cid: string; 30 + uri: string; 31 + }; 32 + }; 33 + 34 + export const useCreateChatMessage = () => { 35 + const pdsAgent = usePDSAgent(); 36 + const store = getStoreFromContext(); 37 + const userDID = useDID(); 38 + const userHandle = useHandle(); 39 + const chatProfile = useChatProfile(); 40 + 41 + return async (msg: NewChatMessage) => { 42 + if (!pdsAgent || !userDID) { 43 + throw new Error("No PDS agent or user DID found"); 44 + } 45 + 46 + let state = store.getState(); 47 + 48 + const streamerProfile = state.profile; 49 + 50 + if (!streamerProfile) { 51 + throw new Error("Profile not found"); 52 + } 53 + 54 + const rt = new RichText({ text: msg.text }); 55 + rt.detectFacetsWithoutResolution(); 56 + 57 + const record: PlaceStreamChatMessage.Record = { 58 + text: msg.text, 59 + createdAt: new Date().toISOString(), 60 + streamer: streamerProfile.did, 61 + ...(msg.reply 62 + ? { 63 + reply: { 64 + root: { 65 + cid: msg.reply.cid, 66 + uri: msg.reply.uri, 67 + }, 68 + parent: { 69 + cid: msg.reply.cid, 70 + uri: msg.reply.uri, 71 + }, 72 + }, 73 + } 74 + : {}), 75 + ...(rt.facets && rt.facets.length > 0 76 + ? { 77 + facets: rt.facets.map((facet) => ({ 78 + index: facet.index, 79 + features: facet.features 80 + .filter( 81 + (feature) => 82 + feature.$type === "app.bsky.richtext.facet#link" || 83 + feature.$type === "app.bsky.richtext.facet#mention", 84 + ) 85 + .map((feature) => { 86 + if (isLink(feature)) { 87 + return { 88 + $type: "app.bsky.richtext.facet#link", 89 + uri: feature.uri, 90 + }; 91 + } else if (isMention(feature)) { 92 + return { 93 + $type: "app.bsky.richtext.facet#mention", 94 + did: feature.did, 95 + }; 96 + } else { 97 + throw new Error("invalid code path"); 98 + } 99 + }), 100 + })), 101 + } 102 + : {}), 103 + }; 104 + 105 + const localChat: ChatMessageViewHydrated = { 106 + uri: `local-${Date.now()}`, 107 + cid: "", 108 + author: { 109 + did: userDID, 110 + handle: userHandle || userDID, 111 + }, 112 + record: record, 113 + indexedAt: new Date().toISOString(), 114 + chatProfile: chatProfile || undefined, 115 + }; 116 + 117 + state = reduceChat(state, [localChat], []); 118 + store.setState(state); 119 + 120 + await pdsAgent.com.atproto.repo.createRecord({ 121 + repo: userDID, 122 + collection: "place.stream.chat.message", 123 + record, 124 + }); 125 + }; 126 + }; 127 + 128 + export const reduceChat = ( 129 + state: LivestreamState, 130 + messages: ChatMessageViewHydrated[], 131 + blocks: PlaceStreamDefs.BlockView[], 132 + ): LivestreamState => { 133 + state = { ...state } as LivestreamState; 134 + const newChat: { [key: string]: ChatMessageViewHydrated } = { 135 + ...state.chatIndex, 136 + }; 137 + 138 + // Add new messages 139 + for (let message of messages) { 140 + const date = new Date(message.record.createdAt); 141 + const key = `${date.getTime()}-${message.uri}`; 142 + 143 + // Remove existing local message matching the server one 144 + if (!message.uri.startsWith("local-")) { 145 + const existingLocalMessageKey = Object.keys(newChat).find((k) => { 146 + const msg = newChat[k]; 147 + return ( 148 + msg.uri.startsWith("local-") && 149 + msg.record.text === message.record.text && 150 + msg.author.did === message.author.did 151 + ); 152 + }); 153 + 154 + if (existingLocalMessageKey) { 155 + delete newChat[existingLocalMessageKey]; 156 + } 157 + } 158 + 159 + // Handle reply information for local-first messages 160 + if (message.record.reply) { 161 + const reply = message.record.reply as { 162 + parent?: { uri: string; cid: string }; 163 + root?: { uri: string; cid: string }; 164 + }; 165 + 166 + const parentUri = reply?.parent?.uri || reply?.root?.uri; 167 + 168 + if (parentUri) { 169 + // First try to find the parent message in our chat 170 + const parentMsgKey = Object.keys(newChat).find( 171 + (k) => newChat[k].uri === parentUri, 172 + ); 173 + 174 + if (parentMsgKey) { 175 + // Found the parent message, add its info to our message 176 + const parentMsg = newChat[parentMsgKey]; 177 + message = { 178 + ...message, 179 + replyTo: { 180 + cid: parentMsg.cid, 181 + uri: parentMsg.uri, 182 + author: parentMsg.author, 183 + record: parentMsg.record, 184 + chatProfile: parentMsg.chatProfile, 185 + indexedAt: parentMsg.indexedAt, 186 + }, 187 + }; 188 + } 189 + } 190 + } 191 + 192 + newChat[key] = message; 193 + } 194 + 195 + for (const block of blocks) { 196 + for (const [k, v] of Object.entries(newChat)) { 197 + if (v.author.did === block.record.subject) { 198 + delete newChat[k]; 199 + } 200 + } 201 + } 202 + 203 + const newChatList = Object.keys(newChat) 204 + .sort((a, b) => (a > b ? 1 : -1)) 205 + .map((key) => newChat[key]); 206 + 207 + return { 208 + ...state, 209 + chatIndex: newChat, 210 + chat: newChatList, 211 + }; 212 + };
+10
js/components/src/livestream-store/context.tsx
··· 1 + import { createContext } from "react"; 2 + import { LivestreamStore } from "../livestream-store/livestream-store"; 3 + 4 + type LivestreamContextType = { 5 + store: LivestreamStore; 6 + }; 7 + 8 + export const LivestreamContext = createContext<LivestreamContextType | null>( 9 + null, 10 + );
+3
js/components/src/livestream-store/index.tsx
··· 1 + export * from "./chat"; 2 + export * from "./context"; 3 + export * from "./livestream-store";
+18
js/components/src/livestream-store/livestream-state.tsx
··· 1 + import { AppBskyActorDefs } from "@atproto/api"; 2 + import { 3 + ChatMessageViewHydrated, 4 + LivestreamViewHydrated, 5 + PlaceStreamDefs, 6 + PlaceStreamSegment, 7 + } from "streamplace"; 8 + 9 + export interface LivestreamState { 10 + profile: AppBskyActorDefs.ProfileViewBasic | null; 11 + chatIndex: { [key: string]: ChatMessageViewHydrated }; 12 + chat: ChatMessageViewHydrated[]; 13 + livestream: LivestreamViewHydrated | null; 14 + viewers: number | null; 15 + segment: PlaceStreamSegment.Record | null; 16 + renditions: PlaceStreamDefs.Rendition[]; 17 + replyToMessage: ChatMessageViewHydrated | null; 18 + }
+56
js/components/src/livestream-store/livestream-store.tsx
··· 1 + import { useContext } from "react"; 2 + import { createStore, StoreApi, useStore } from "zustand"; 3 + import { LivestreamContext } from "./context"; 4 + import { LivestreamState } from "./livestream-state"; 5 + import { handleWebSocketMessages } from "./websocket-consumer"; 6 + 7 + export type LivestreamStore = StoreApi<LivestreamState>; 8 + 9 + export const makeLivestreamStore = (): StoreApi<LivestreamState> => { 10 + return createStore<LivestreamState>()((set) => ({ 11 + profile: null, 12 + chatIndex: {}, 13 + chat: [], 14 + livestream: null, 15 + viewers: null, 16 + segment: null, 17 + renditions: [], 18 + replyToMessage: null, 19 + })); 20 + }; 21 + 22 + export function getStoreFromContext(): LivestreamStore { 23 + const context = useContext(LivestreamContext); 24 + if (!context) { 25 + throw new Error( 26 + "useLivestreamStore must be used within a LivestreamProvider", 27 + ); 28 + } 29 + return context.store; 30 + } 31 + 32 + export function useLivestreamStore<U>( 33 + selector: (state: LivestreamState) => U, 34 + ): U { 35 + const store = getStoreFromContext(); 36 + return useStore(store, selector); 37 + } 38 + 39 + export const useHandleWebsocketMessages = () => { 40 + const store = getStoreFromContext(); 41 + return (messages: any[]) => { 42 + store.setState((state) => handleWebSocketMessages(state, messages)); 43 + }; 44 + }; 45 + 46 + export const useChat = () => useLivestreamStore((x) => x.chat); 47 + 48 + export const useProfile = () => useLivestreamStore((x) => x.profile); 49 + 50 + export const useViewers = () => useLivestreamStore((x) => x.viewers); 51 + 52 + export const useLivestream = () => useLivestreamStore((x) => x.livestream); 53 + 54 + export const useSegment = () => useLivestreamStore((x) => x.segment); 55 + 56 + export const useRenditions = () => useLivestreamStore((x) => x.renditions);
+62
js/components/src/livestream-store/websocket-consumer.tsx
··· 1 + import { AppBskyActorDefs } from "@atproto/api"; 2 + import { 3 + ChatMessageViewHydrated, 4 + LivestreamViewHydrated, 5 + PlaceStreamChatDefs, 6 + PlaceStreamChatMessage, 7 + PlaceStreamDefs, 8 + PlaceStreamLivestream, 9 + PlaceStreamSegment, 10 + } from "streamplace"; 11 + import { reduceChat } from "./chat"; 12 + import { LivestreamState } from "./livestream-state"; 13 + 14 + export const handleWebSocketMessages = ( 15 + state: LivestreamState, 16 + messages: any[], 17 + ): LivestreamState => { 18 + for (const message of messages) { 19 + if (PlaceStreamLivestream.isLivestreamView(message)) { 20 + state = { 21 + ...state, 22 + livestream: message as LivestreamViewHydrated, 23 + }; 24 + } else if (PlaceStreamLivestream.isViewerCount(message)) { 25 + state = { 26 + ...state, 27 + viewers: message.count, 28 + }; 29 + } else if (PlaceStreamChatDefs.isMessageView(message)) { 30 + // Explicitly map MessageView to MessageViewHydrated 31 + const hydrated: ChatMessageViewHydrated = { 32 + uri: message.uri, 33 + cid: message.cid, 34 + author: message.author, 35 + record: message.record as PlaceStreamChatMessage.Record, 36 + indexedAt: message.indexedAt, 37 + chatProfile: (message as any).chatProfile, 38 + replyTo: (message as any).replyTo, 39 + }; 40 + state = reduceChat(state, [hydrated], []); 41 + } else if (PlaceStreamSegment.isRecord(message)) { 42 + state = { 43 + ...state, 44 + segment: message as PlaceStreamSegment.Record, 45 + }; 46 + } else if (PlaceStreamDefs.isBlockView(message)) { 47 + const block = message as PlaceStreamDefs.BlockView; 48 + state = reduceChat(state, [], [block]); 49 + } else if (PlaceStreamDefs.isRenditions(message)) { 50 + state = { 51 + ...state, 52 + renditions: message.renditions, 53 + }; 54 + } else if (AppBskyActorDefs.isProfileViewBasic(message)) { 55 + state = { 56 + ...state, 57 + profile: message, 58 + }; 59 + } 60 + } 61 + return reduceChat(state, [], []); 62 + };
+10
js/components/src/streamplace-provider/context.tsx
··· 1 + import { createContext } from "react"; 2 + import { StreamplaceStore } from "../streamplace-store/streamplace-store"; 3 + 4 + type StreamplaceContextType = { 5 + store: StreamplaceStore; 6 + }; 7 + 8 + export const StreamplaceContext = createContext<StreamplaceContextType | null>( 9 + null, 10 + );
+32
js/components/src/streamplace-provider/index.tsx
··· 1 + import { SessionManager } from "@atproto/api/dist/session-manager"; 2 + import { useEffect, useRef } from "react"; 3 + import { makeStreamplaceStore } from "../streamplace-store/streamplace-store"; 4 + import { StreamplaceContext } from "./context"; 5 + import Poller from "./poller"; 6 + 7 + export function StreamplaceProvider({ 8 + children, 9 + url, 10 + oauthSession, 11 + }: { 12 + children: React.ReactNode; 13 + url: string; 14 + oauthSession?: SessionManager; 15 + }) { 16 + // todo: handle url changes? 17 + const store = useRef(makeStreamplaceStore({ url })).current; 18 + 19 + useEffect(() => { 20 + store.setState({ url }); 21 + }, [url]); 22 + 23 + useEffect(() => { 24 + store.setState({ oauthSession }); 25 + }, [oauthSession]); 26 + 27 + return ( 28 + <StreamplaceContext.Provider value={{ store: store }}> 29 + <Poller>{children}</Poller> 30 + </StreamplaceContext.Provider> 31 + ); 32 + }
+38
js/components/src/streamplace-provider/poller.tsx
··· 1 + import React, { useEffect } from "react"; 2 + import { StreamplaceAgent } from "streamplace"; 3 + import { 4 + useDID, 5 + useGetBskyProfile, 6 + useGetChatProfile, 7 + useStreamplaceStore, 8 + } from "../streamplace-store"; 9 + import { usePDSAgent } from "../streamplace-store/xrpc"; 10 + 11 + export default function Poller({ children }: { children: React.ReactNode }) { 12 + const url = useStreamplaceStore((state) => state.url); 13 + const setLiveUsers = useStreamplaceStore((state) => state.setLiveUsers); 14 + const did = useDID(); 15 + const pdsAgent = usePDSAgent(); 16 + const getChatProfile = useGetChatProfile(); 17 + const getBskyProfile = useGetBskyProfile(); 18 + 19 + useEffect(() => { 20 + if (pdsAgent && did) { 21 + getChatProfile(); 22 + getBskyProfile(); 23 + } 24 + }, [pdsAgent, did]); 25 + 26 + useEffect(() => { 27 + const agent = new StreamplaceAgent(url); 28 + const go = async () => { 29 + const res = await agent.place.stream.live.getLiveUsers(); 30 + setLiveUsers(res.data.streams || []); 31 + }; 32 + go(); 33 + const handle = setInterval(go, 3000); 34 + return () => clearInterval(handle); 35 + }, [url]); 36 + 37 + return <>{children}</>; 38 + }
js/components/src/streamplace-provider/xrpc.tsx

This is a binary file and will not be displayed.

+2
js/components/src/streamplace-store/index.tsx
··· 1 + export * from "./streamplace-store"; 2 + export * from "./user";
+71
js/components/src/streamplace-store/streamplace-store.tsx
··· 1 + import { SessionManager } from "@atproto/api/dist/session-manager"; 2 + import { useContext } from "react"; 3 + import { PlaceStreamChatProfile, PlaceStreamLivestream } from "streamplace"; 4 + import { createStore, StoreApi, useStore } from "zustand"; 5 + import { StreamplaceContext } from "../streamplace-provider/context"; 6 + 7 + // there are three categories of XRPC that we need to handle: 8 + // 1. Public (probably) OAuth XRPC to the users' PDS for apps that use this API. 9 + // 2. Confidental OAuth to the Streamplace server for doing things that require 10 + // server-side authentication. This isn't very much stuff yet, but you need 11 + // to log into Streamplace to do things like have Streamplace update your 12 + // activity status. 13 + // 3. Anonymous XRPC to the Streamplace server for stuff like `getLiveUsers`. This 14 + // is easy to handle internal to this library. 15 + // For the Streamplace app itself, all three are the same. For apps that aren't 16 + // doing OAuth through the Streamplace node, we need to expose an interface that 17 + // allows them to use atcute or whatever for 1. 18 + 19 + export interface StreamplaceState { 20 + url: string; 21 + liveUsers: PlaceStreamLivestream.LivestreamView[]; 22 + setLiveUsers: (users: PlaceStreamLivestream.LivestreamView[]) => void; 23 + oauthSession: SessionManager | null; 24 + handle: string | null; 25 + chatProfile: PlaceStreamChatProfile.Record | null; 26 + } 27 + 28 + export type StreamplaceStore = StoreApi<StreamplaceState>; 29 + 30 + export const makeStreamplaceStore = ({ 31 + url, 32 + }: { 33 + url: string; 34 + }): StoreApi<StreamplaceState> => { 35 + return createStore<StreamplaceState>()((set) => ({ 36 + url, 37 + liveUsers: [], 38 + setLiveUsers: (users: PlaceStreamLivestream.LivestreamView[]) => { 39 + set({ liveUsers: users }); 40 + }, 41 + oauthSession: null, 42 + handle: null, 43 + chatProfile: null, 44 + })); 45 + }; 46 + 47 + export function getStreamplaceStoreFromContext(): StreamplaceStore { 48 + const context = useContext(StreamplaceContext); 49 + if (!context) { 50 + throw new Error( 51 + "useStreamplaceStore must be used within a StreamplaceProvider", 52 + ); 53 + } 54 + return context.store; 55 + } 56 + 57 + export function useStreamplaceStore<U>( 58 + selector: (state: StreamplaceState) => U, 59 + ): U { 60 + return useStore(getStreamplaceStoreFromContext(), selector); 61 + } 62 + 63 + export const useUrl = () => useStreamplaceStore((x) => x.url); 64 + 65 + export const useDID = () => useStreamplaceStore((x) => x.oauthSession?.did); 66 + 67 + export const useHandle = () => useStreamplaceStore((x) => x.handle); 68 + export const useSetHandle = (): ((handle: string) => void) => { 69 + const store = getStreamplaceStoreFromContext(); 70 + return (handle: string) => store.setState({ handle }); 71 + };
+57
js/components/src/streamplace-store/user.tsx
··· 1 + import { PlaceStreamChatProfile } from "streamplace"; 2 + import { 3 + getStreamplaceStoreFromContext, 4 + useDID, 5 + useStreamplaceStore, 6 + } from "./streamplace-store"; 7 + import { usePDSAgent } from "./xrpc"; 8 + 9 + export function useGetChatProfile() { 10 + const did = useDID(); 11 + const pdsAgent = usePDSAgent(); 12 + const store = getStreamplaceStoreFromContext(); 13 + 14 + return async () => { 15 + if (!did || !pdsAgent) { 16 + throw new Error("No DID or PDS agent"); 17 + } 18 + const res = await pdsAgent.com.atproto.repo.getRecord({ 19 + repo: did, 20 + collection: "place.stream.chat.profile", 21 + rkey: "self", 22 + }); 23 + if (!res.success) { 24 + throw new Error("Failed to get chat profile record"); 25 + } 26 + 27 + if (PlaceStreamChatProfile.isRecord(res.data.value)) { 28 + store.setState({ chatProfile: res.data.value }); 29 + } else { 30 + console.log("not a record", res.data.value); 31 + } 32 + }; 33 + } 34 + 35 + export function useGetBskyProfile() { 36 + const did = useDID(); 37 + const pdsAgent = usePDSAgent(); 38 + const store = getStreamplaceStoreFromContext(); 39 + 40 + return async () => { 41 + if (!did || !pdsAgent) { 42 + throw new Error("No DID or PDS agent"); 43 + } 44 + const res = await pdsAgent.app.bsky.actor.getProfile({ 45 + actor: did, 46 + }); 47 + if (!res.success) { 48 + throw new Error("Failed to get chat profile record"); 49 + } 50 + 51 + store.setState({ handle: res.data.handle }); 52 + }; 53 + } 54 + 55 + export function useChatProfile() { 56 + return useStreamplaceStore((x) => x.chatProfile); 57 + }
+15
js/components/src/streamplace-store/xrpc.tsx
··· 1 + import { useMemo } from "react"; 2 + import { StreamplaceAgent } from "streamplace"; 3 + import { useStreamplaceStore } from "."; 4 + 5 + export function usePDSAgent(): StreamplaceAgent | null { 6 + const oauthSession = useStreamplaceStore((state) => state.oauthSession); 7 + 8 + return useMemo(() => { 9 + if (!oauthSession) { 10 + return null; 11 + } 12 + 13 + return new StreamplaceAgent(oauthSession); 14 + }, [oauthSession]); 15 + }
+9
js/components/tsconfig.json
··· 1 + { 2 + "extends": "../app/tsconfig.base.json", 3 + "compilerOptions": { 4 + "rootDir": "./src", 5 + "outDir": "./dist", 6 + "jsx": "react-jsx" 7 + }, 8 + "include": ["./src"] 9 + }
+4 -4
js/streamplace/package.json
··· 3 3 "version": "0.6.33", 4 4 "description": "Live video on the AT Protocol", 5 5 "main": "dist/index.js", 6 - "types": "dist/index.d.js", 6 + "types": "src/index.ts", 7 7 "type": "module", 8 8 "exports": { 9 9 ".": { ··· 12 12 } 13 13 }, 14 14 "scripts": { 15 - "build": "tsup-node", 15 + "build": "tsc", 16 16 "postinstall": "cd ../.. && make js-lexicons && cd - && pnpm run build", 17 - "start": "tsup-node --watch" 17 + "start": "tsc --watch --preserveWatchOutput" 18 18 }, 19 19 "keywords": [ 20 20 "streamplace", ··· 25 25 "license": "MIT", 26 26 "packageManager": "pnpm@10.11.0", 27 27 "dependencies": { 28 + "@atproto/api": "^0.15.7", 28 29 "@atproto/lexicon": "^0.4.11", 29 30 "@atproto/xrpc": "^0.7.0", 30 31 "multiformats": "^9.9.0" 31 32 }, 32 33 "devDependencies": { 33 - "tsup": "^8.5.0", 34 34 "typescript": "~5.3.3" 35 35 } 36 36 }
+2
js/streamplace/src/index.ts
··· 1 + export * from "./agent"; 1 2 export * from "./lexicons"; 2 3 export { schemas } from "./lexicons/lexicons"; 4 + export * from "./useful-types";
+15
js/streamplace/src/useful-types.ts
··· 1 + import { 2 + PlaceStreamChatDefs, 3 + PlaceStreamChatMessage, 4 + PlaceStreamLivestream, 5 + } from "./lexicons"; 6 + 7 + export interface LivestreamViewHydrated 8 + extends PlaceStreamLivestream.LivestreamView { 9 + record: PlaceStreamLivestream.Record; 10 + } 11 + 12 + export interface ChatMessageViewHydrated 13 + extends PlaceStreamChatDefs.MessageView { 14 + record: PlaceStreamChatMessage.Record; 15 + }
+5 -7
js/streamplace/tsconfig.json
··· 1 1 { 2 - "include": [], 3 - "references": [{ "path": "../app/tsconfig.base.json" }], 2 + "extends": "../app/tsconfig.base.json", 4 3 "compilerOptions": { 5 - "moduleResolution": "bundler", 6 - "module": "es2022", 7 - "jsx": "react-jsx", 8 - "sourceMap": true 9 - } 4 + "rootDir": "./src", 5 + "outDir": "./dist" 6 + }, 7 + "include": ["./src"] 10 8 }
-18
js/streamplace/tsup.config.ts
··· 1 - import { defineConfig, Format } from "tsup"; 2 - 3 - const options = { 4 - splitting: false, 5 - bundle: false, 6 - clean: true, 7 - sourcemap: true, 8 - dts: true, 9 - format: ["esm"] as Format[], 10 - }; 11 - 12 - export default defineConfig([ 13 - { 14 - ...options, 15 - entry: ["./src/**"], 16 - outDir: "dist", 17 - }, 18 - ]);
+8
knip.json
··· 1 + { 2 + "$schema": "https://unpkg.com/knip@5/schema.json", 3 + "project": ["js/streamplace/src/**/*.ts"], 4 + "entry": ["js/streamplace/src/index.ts"], 5 + "ignoreWorkspaces": ["js/app", "js/desktop", "js/config-react-native-webrtc"], 6 + "include": ["unlisted"], 7 + "ignore": [".*tamagui.config.ts.*"] 8 + }
+7 -3
package.json
··· 9 9 }, 10 10 "scripts": { 11 11 "preinstall": "node hack/node-version.js", 12 - "check": "pnpm run check:workspaces && git ls-files | xargs prettier --check --ignore-unknown", 12 + "check": "knip && pnpm run check:workspaces && git ls-files | xargs prettier --check --ignore-unknown", 13 13 "check:workspaces": "cd js/app && pnpm run check", 14 14 "fix": "git ls-files | xargs prettier --write --ignore-unknown", 15 15 "postinstall": "husky", ··· 20 20 "app": "pnpm run -r --stream --filter @streamplace/app... --parallel", 21 21 "desktop": "pnpm run -r --stream --filter streamplace-desktop... --parallel", 22 22 "docs": "cd js/docs && pnpm run", 23 - "start": "pnpm run -r --stream --parallel start" 23 + "start": "pnpm run -r --stream --parallel start", 24 + "knip": "knip" 24 25 }, 25 26 "keywords": [], 26 27 "author": "Streamplace", 27 28 "license": "MIT", 28 29 "devDependencies": { 29 30 "@atproto/lex-cli": "^0.5.6", 31 + "@types/node": "^22.15.17", 30 32 "husky": "^9.1.6", 33 + "knip": "^5.59.1", 31 34 "lerna": "^8.1.9", 32 35 "lint-staged": "^15.2.10", 33 - "prettier": "3.4.2" 36 + "prettier": "3.4.2", 37 + "typescript": "^5.8.3" 34 38 }, 35 39 "workspaces": [ 36 40 "js/*"
+19
pkg/api/websocket.go
··· 22 22 var upgrader = websocket.Upgrader{ 23 23 ReadBufferSize: 1024, 24 24 WriteBufferSize: 1024, 25 + CheckOrigin: func(r *http.Request) bool { 26 + return true 27 + }, 25 28 } 26 29 27 30 var pingPeriod = 5 * time.Second ··· 157 160 log.Debug(ctx, "context done, stopping websocket sender") 158 161 return 159 162 } 163 + } 164 + }() 165 + 166 + go func() { 167 + profile, err := a.Model.GetRepo(repoDID) 168 + if err != nil { 169 + log.Error(ctx, "could not get profile", "error", err) 170 + return 171 + } 172 + if profile != nil { 173 + p := map[string]any{ 174 + "$type": "app.bsky.actor.defs#profileViewBasic", 175 + "did": repoDID, 176 + "handle": profile.Handle, 177 + } 178 + initialBurst <- p 160 179 } 161 180 }() 162 181
+369 -73
pnpm-lock.yaml
··· 16 16 version: 0.1.0 17 17 prettier-plugin-organize-imports: 18 18 specifier: ^4.1.0 19 - version: 4.1.0(prettier@3.4.2)(typescript@5.6.3) 19 + version: 4.1.0(prettier@3.4.2)(typescript@5.8.3) 20 20 devDependencies: 21 21 '@atproto/lex-cli': 22 22 specifier: ^0.5.6 23 23 version: 0.5.7 24 + '@types/node': 25 + specifier: ^22.15.17 26 + version: 22.15.17 24 27 husky: 25 28 specifier: ^9.1.6 26 29 version: 9.1.6 30 + knip: 31 + specifier: ^5.59.1 32 + version: 5.59.1(@types/node@22.15.17)(typescript@5.8.3) 27 33 lerna: 28 34 specifier: ^8.1.9 29 35 version: 8.1.9(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13) ··· 33 39 prettier: 34 40 specifier: 3.4.2 35 41 version: 3.4.2 42 + typescript: 43 + specifier: ^5.8.3 44 + version: 5.8.3 36 45 37 46 js/app: 38 47 dependencies: ··· 90 99 '@streamplace/atproto-oauth-client-react-native': 91 100 specifier: workspace:* 92 101 version: link:../atproto-oauth-client-react-native 102 + '@streamplace/components': 103 + specifier: workspace:* 104 + version: link:../components 93 105 '@tamagui/config': 94 106 specifier: ^1.123.17 95 107 version: 1.123.17(react-dom@18.3.1(react@18.3.1))(react-native-reanimated@3.16.1(@babel/core@7.26.0)(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.27.2(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.27.2(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) ··· 367 379 version: 22.15.17 368 380 typescript: 369 381 specifier: ^5.6.3 370 - version: 5.6.3 382 + version: 5.8.3 383 + 384 + js/components: 385 + dependencies: 386 + '@atproto/api': 387 + specifier: ^0.15.7 388 + version: 0.15.7 389 + react: 390 + specifier: '*' 391 + version: 18.3.1 392 + react-native: 393 + specifier: 0.76.2 394 + version: 0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.27.2(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10) 395 + react-use-websocket: 396 + specifier: ^4.13.0 397 + version: 4.13.0 398 + streamplace: 399 + specifier: workspace:* 400 + version: link:../streamplace 401 + zustand: 402 + specifier: ^5.0.5 403 + version: 5.0.5(@types/react@18.3.12)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) 404 + devDependencies: 405 + tsup: 406 + specifier: ^8.5.0 407 + version: 8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.5.1) 371 408 372 409 js/config-react-native-webrtc: 373 410 dependencies: ··· 447 484 version: 0.5.10 448 485 '@typescript-eslint/eslint-plugin': 449 486 specifier: ^8.13.0 450 - version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 487 + version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 451 488 '@typescript-eslint/parser': 452 489 specifier: ^8.13.0 453 - version: 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 490 + version: 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 454 491 '@vercel/webpack-asset-relocator-loader': 455 492 specifier: 1.7.3 456 493 version: 1.7.3 ··· 462 499 version: 33.0.2 463 500 eslint: 464 501 specifier: ^9.14.0 465 - version: 9.14.0(jiti@1.21.6) 502 + version: 9.14.0(jiti@2.4.2) 466 503 eslint-plugin-import: 467 504 specifier: ^2.31.0 468 - version: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6)) 505 + version: 2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2)) 469 506 fork-ts-checker-webpack-plugin: 470 507 specifier: ^9.0.2 471 508 version: 9.0.2(typescript@5.6.3)(webpack@5.94.0(@swc/core@1.8.0(@swc/helpers@0.5.17))) ··· 489 526 dependencies: 490 527 '@astrojs/starlight': 491 528 specifier: ^0.34.1 492 - version: 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 529 + version: 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 493 530 '@fontsource/atkinson-hyperlegible-next': 494 531 specifier: ^5.2.2 495 532 version: 5.2.2 ··· 498 535 version: link:../app 499 536 astro: 500 537 specifier: ^5.6.1 501 - version: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 538 + version: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 502 539 sharp: 503 540 specifier: ^0.32.5 504 541 version: 0.32.6 505 542 starlight-openapi: 506 543 specifier: ^0.17.0 507 - version: 0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3) 544 + version: 0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3) 508 545 starlight-openapi-rapidoc: 509 546 specifier: ^0.8.1-beta 510 - version: 0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3) 547 + version: 0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3) 511 548 streamplace: 512 549 specifier: workspace:* 513 550 version: link:../streamplace 514 551 515 552 js/streamplace: 516 553 dependencies: 554 + '@atproto/api': 555 + specifier: ^0.15.7 556 + version: 0.15.7 517 557 '@atproto/lexicon': 518 558 specifier: ^0.4.11 519 559 version: 0.4.11 ··· 524 564 specifier: ^9.9.0 525 565 version: 9.9.0 526 566 devDependencies: 527 - tsup: 528 - specifier: ^8.5.0 529 - version: 8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@1.21.6)(postcss@8.5.3)(typescript@5.3.3)(yaml@2.5.1) 530 567 typescript: 531 568 specifier: ~5.3.3 532 569 version: 5.3.3 ··· 2000 2037 '@emnapi/core@1.2.0': 2001 2038 resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==} 2002 2039 2040 + '@emnapi/core@1.4.3': 2041 + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} 2042 + 2003 2043 '@emnapi/runtime@1.4.3': 2004 2044 resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} 2005 2045 2006 2046 '@emnapi/wasi-threads@1.0.1': 2007 2047 resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} 2048 + 2049 + '@emnapi/wasi-threads@1.0.2': 2050 + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} 2008 2051 2009 2052 '@emoji-mart/react@1.1.1': 2010 2053 resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} ··· 3069 3112 resolution: {integrity: sha512-z10PF9JV6SbjFq+/rYabM+8CVlMokgl8RFGvieSGNTmrkQanfHn+15XBrhG3BgUfvmTeSeyShfOHpG0i9zEdcg==} 3070 3113 deprecated: Motion One for Vue is deprecated. Use Oku Motion instead https://oku-ui.com/motion 3071 3114 3115 + '@napi-rs/wasm-runtime@0.2.10': 3116 + resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} 3117 + 3072 3118 '@napi-rs/wasm-runtime@0.2.4': 3073 3119 resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} 3074 3120 ··· 3306 3352 '@oslojs/encoding@1.1.0': 3307 3353 resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} 3308 3354 3355 + '@oxc-resolver/binding-darwin-arm64@9.0.2': 3356 + resolution: {integrity: sha512-MVyRgP2gzJJtAowjG/cHN3VQXwNLWnY+FpOEsyvDepJki1SdAX/8XDijM1yN6ESD1kr9uhBKjGelC6h3qtT+rA==} 3357 + cpu: [arm64] 3358 + os: [darwin] 3359 + 3360 + '@oxc-resolver/binding-darwin-x64@9.0.2': 3361 + resolution: {integrity: sha512-7kV0EOFEZ3sk5Hjy4+bfA6XOQpCwbDiDkkHN4BHHyrBHsXxUR05EcEJPPL1WjItefg+9+8hrBmoK0xRoDs41+A==} 3362 + cpu: [x64] 3363 + os: [darwin] 3364 + 3365 + '@oxc-resolver/binding-freebsd-x64@9.0.2': 3366 + resolution: {integrity: sha512-6OvkEtRXrt8sJ4aVfxHRikjain9nV1clIsWtJ1J3J8NG1ZhjyJFgT00SCvqxbK+pzeWJq6XzHyTCN78ML+lY2w==} 3367 + cpu: [x64] 3368 + os: [freebsd] 3369 + 3370 + '@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2': 3371 + resolution: {integrity: sha512-aYpNL6o5IRAUIdoweW21TyLt54Hy/ZS9tvzNzF6ya1ckOQ8DLaGVPjGpmzxdNja9j/bbV6aIzBH7lNcBtiOTkQ==} 3372 + cpu: [arm] 3373 + os: [linux] 3374 + 3375 + '@oxc-resolver/binding-linux-arm64-gnu@9.0.2': 3376 + resolution: {integrity: sha512-RGFW4vCfKMFEIzb9VCY0oWyyY9tR1/o+wDdNePhiUXZU4SVniRPQaZ1SJ0sUFI1k25pXZmzQmIP6cBmazi/Dew==} 3377 + cpu: [arm64] 3378 + os: [linux] 3379 + 3380 + '@oxc-resolver/binding-linux-arm64-musl@9.0.2': 3381 + resolution: {integrity: sha512-lxx/PibBfzqYvut2Y8N2D0Ritg9H8pKO+7NUSJb9YjR/bfk2KRmP8iaUz3zB0JhPtf/W3REs65oKpWxgflGToA==} 3382 + cpu: [arm64] 3383 + os: [linux] 3384 + 3385 + '@oxc-resolver/binding-linux-riscv64-gnu@9.0.2': 3386 + resolution: {integrity: sha512-yD28ptS/OuNhwkpXRPNf+/FvrO7lwURLsEbRVcL1kIE0GxNJNMtKgIE4xQvtKDzkhk6ZRpLho5VSrkkF+3ARTQ==} 3387 + cpu: [riscv64] 3388 + os: [linux] 3389 + 3390 + '@oxc-resolver/binding-linux-s390x-gnu@9.0.2': 3391 + resolution: {integrity: sha512-WBwEJdspoga2w+aly6JVZeHnxuPVuztw3fPfWrei2P6rNM5hcKxBGWKKT6zO1fPMCB4sdDkFohGKkMHVV1eryQ==} 3392 + cpu: [s390x] 3393 + os: [linux] 3394 + 3395 + '@oxc-resolver/binding-linux-x64-gnu@9.0.2': 3396 + resolution: {integrity: sha512-a2z3/cbOOTUq0UTBG8f3EO/usFcdwwXnCejfXv42HmV/G8GjrT4fp5+5mVDoMByH3Ce3iVPxj1LmS6OvItKMYQ==} 3397 + cpu: [x64] 3398 + os: [linux] 3399 + 3400 + '@oxc-resolver/binding-linux-x64-musl@9.0.2': 3401 + resolution: {integrity: sha512-bHZF+WShYQWpuswB9fyxcgMIWVk4sZQT0wnwpnZgQuvGTZLkYJ1JTCXJMtaX5mIFHf69ngvawnwPIUA4Feil0g==} 3402 + cpu: [x64] 3403 + os: [linux] 3404 + 3405 + '@oxc-resolver/binding-wasm32-wasi@9.0.2': 3406 + resolution: {integrity: sha512-I5cSgCCh5nFozGSHz+PjIOfrqW99eUszlxKLgoNNzQ1xQ2ou9ZJGzcZ94BHsM9SpyYHLtgHljmOZxCT9bgxYNA==} 3407 + engines: {node: '>=14.0.0'} 3408 + cpu: [wasm32] 3409 + 3410 + '@oxc-resolver/binding-win32-arm64-msvc@9.0.2': 3411 + resolution: {integrity: sha512-5IhoOpPr38YWDWRCA5kP30xlUxbIJyLAEsAK7EMyUgqygBHEYLkElaKGgS0X5jRXUQ6l5yNxuW73caogb2FYaw==} 3412 + cpu: [arm64] 3413 + os: [win32] 3414 + 3415 + '@oxc-resolver/binding-win32-x64-msvc@9.0.2': 3416 + resolution: {integrity: sha512-Qc40GDkaad9rZksSQr2l/V9UubigIHsW69g94Gswc2sKYB3XfJXfIfyV8WTJ67u6ZMXsZ7BH1msSC6Aen75mCg==} 3417 + cpu: [x64] 3418 + os: [win32] 3419 + 3309 3420 '@oxc-transform/binding-darwin-arm64@0.47.1': 3310 3421 resolution: {integrity: sha512-GT56Wkk/M1Eo1HCFfj8PxNn/ssBfxFVIrS3A5lJ6GE2k3gbVRORNzVA1znHtB3Tj4hIvWPNzqh+EQ2bhaFypNQ==} 3311 3422 cpu: [arm64] ··· 7314 7425 resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 7315 7426 engines: {node: '>=8.6.0'} 7316 7427 7428 + fast-glob@3.3.3: 7429 + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 7430 + engines: {node: '>=8.6.0'} 7431 + 7317 7432 fast-json-stable-stringify@2.1.0: 7318 7433 resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 7319 7434 ··· 7352 7467 7353 7468 fbjs@3.0.5: 7354 7469 resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} 7470 + 7471 + fd-package-json@2.0.0: 7472 + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} 7355 7473 7356 7474 fd-slicer@1.1.0: 7357 7475 resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} ··· 7526 7644 form-data@4.0.0: 7527 7645 resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 7528 7646 engines: {node: '>= 6'} 7647 + 7648 + formatly@0.2.4: 7649 + resolution: {integrity: sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==} 7650 + engines: {node: '>=18.3.0'} 7651 + hasBin: true 7529 7652 7530 7653 forwarded@0.2.0: 7531 7654 resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} ··· 8606 8729 8607 8730 jiti@1.21.6: 8608 8731 resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} 8732 + hasBin: true 8733 + 8734 + jiti@2.4.2: 8735 + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 8609 8736 hasBin: true 8610 8737 8611 8738 join-component@1.1.0: ··· 8780 8907 resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} 8781 8908 engines: {node: '>= 8'} 8782 8909 8910 + knip@5.59.1: 8911 + resolution: {integrity: sha512-pOMBw6sLQhi/RfnpI6TwBY6NrAtKXDO5wkmMm+pCsSK5eWbVfDnDtPXbLDGNCoZPXiuAojb27y4XOpp4JPNxlA==} 8912 + engines: {node: '>=18.18.0'} 8913 + hasBin: true 8914 + peerDependencies: 8915 + '@types/node': '>=18' 8916 + typescript: '>=5.0.4' 8917 + 8783 8918 launch-editor@2.9.1: 8784 8919 resolution: {integrity: sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==} 8785 8920 ··· 10117 10252 peerDependenciesMeta: 10118 10253 typescript: 10119 10254 optional: true 10255 + 10256 + oxc-resolver@9.0.2: 10257 + resolution: {integrity: sha512-w838ygc1p7rF+7+h5vR9A+Y9Fc4imy6C3xPthCMkdFUgFvUWkmABeNB8RBDQ6+afk44Q60/UMMQ+gfDUW99fBA==} 10120 10258 10121 10259 oxc-transform@0.47.1: 10122 10260 resolution: {integrity: sha512-krLyXKa+2RWT9MFcj2q4ffEFFzX3EoypcLGpXMhTW0ayLxmGxyiLYlDlwFwP+5fbaVeeBu36XsVroOZelddl7g==} ··· 11783 11921 resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 11784 11922 engines: {node: '>=8'} 11785 11923 11924 + strip-json-comments@5.0.1: 11925 + resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} 11926 + engines: {node: '>=14.16'} 11927 + 11786 11928 strip-outer@1.0.1: 11787 11929 resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} 11788 11930 engines: {node: '>=0.10.0'} ··· 12666 12808 walk-up-path@3.0.1: 12667 12809 resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} 12668 12810 12811 + walk-up-path@4.0.0: 12812 + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} 12813 + engines: {node: 20 || >=22} 12814 + 12669 12815 walker@1.0.8: 12670 12816 resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} 12671 12817 ··· 13030 13176 typescript: ^4.9.4 || ^5.0.2 13031 13177 zod: ^3 13032 13178 13179 + zod-validation-error@3.4.1: 13180 + resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==} 13181 + engines: {node: '>=18.0.0'} 13182 + peerDependencies: 13183 + zod: ^3.24.4 13184 + 13033 13185 zod@3.24.4: 13034 13186 resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==} 13035 13187 ··· 13051 13203 use-sync-external-store: 13052 13204 optional: true 13053 13205 13206 + zustand@5.0.5: 13207 + resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} 13208 + engines: {node: '>=12.20.0'} 13209 + peerDependencies: 13210 + '@types/react': '>=18.0.0' 13211 + immer: '>=9.0.6' 13212 + react: '>=18.0.0' 13213 + use-sync-external-store: '>=1.2.0' 13214 + peerDependenciesMeta: 13215 + '@types/react': 13216 + optional: true 13217 + immer: 13218 + optional: true 13219 + react: 13220 + optional: true 13221 + use-sync-external-store: 13222 + optional: true 13223 + 13054 13224 zwitch@2.0.4: 13055 13225 resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 13056 13226 ··· 13101 13271 transitivePeerDependencies: 13102 13272 - supports-color 13103 13273 13104 - '@astrojs/mdx@4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))': 13274 + '@astrojs/mdx@4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))': 13105 13275 dependencies: 13106 13276 '@astrojs/markdown-remark': 6.3.1 13107 13277 '@mdx-js/mdx': 3.1.0(acorn@8.14.1) 13108 13278 acorn: 8.14.1 13109 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 13279 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 13110 13280 es-module-lexer: 1.7.0 13111 13281 estree-util-visit: 2.0.0 13112 13282 hast-util-to-html: 9.0.5 ··· 13130 13300 stream-replace-string: 2.0.0 13131 13301 zod: 3.24.4 13132 13302 13133 - '@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))': 13303 + '@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))': 13134 13304 dependencies: 13135 13305 '@astrojs/markdown-remark': 6.3.1 13136 - '@astrojs/mdx': 4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 13306 + '@astrojs/mdx': 4.2.6(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 13137 13307 '@astrojs/sitemap': 3.3.1 13138 13308 '@pagefind/default-ui': 1.3.0 13139 13309 '@types/hast': 3.0.4 13140 13310 '@types/js-yaml': 4.0.9 13141 13311 '@types/mdast': 4.0.4 13142 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 13143 - astro-expressive-code: 0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 13312 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 13313 + astro-expressive-code: 0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 13144 13314 bcp-47: 2.1.0 13145 13315 hast-util-from-html: 2.0.3 13146 13316 hast-util-select: 6.0.4 ··· 15531 15701 '@emnapi/wasi-threads': 1.0.1 15532 15702 tslib: 2.8.1 15533 15703 15704 + '@emnapi/core@1.4.3': 15705 + dependencies: 15706 + '@emnapi/wasi-threads': 1.0.2 15707 + tslib: 2.8.1 15708 + optional: true 15709 + 15534 15710 '@emnapi/runtime@1.4.3': 15535 15711 dependencies: 15536 15712 tslib: 2.8.1 ··· 15538 15714 '@emnapi/wasi-threads@1.0.1': 15539 15715 dependencies: 15540 15716 tslib: 2.8.1 15717 + 15718 + '@emnapi/wasi-threads@1.0.2': 15719 + dependencies: 15720 + tslib: 2.8.1 15721 + optional: true 15541 15722 15542 15723 '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.3.1)': 15543 15724 dependencies: ··· 15704 15885 '@esbuild/win32-x64@0.25.3': 15705 15886 optional: true 15706 15887 15707 - '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@1.21.6))': 15888 + '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0(jiti@2.4.2))': 15708 15889 dependencies: 15709 - eslint: 9.14.0(jiti@1.21.6) 15890 + eslint: 9.14.0(jiti@2.4.2) 15710 15891 eslint-visitor-keys: 3.4.3 15711 15892 15712 15893 '@eslint-community/regexpp@4.12.1': {} ··· 16701 16882 16702 16883 '@leichtgewicht/ip-codec@2.0.5': {} 16703 16884 16704 - '@lerna/create@8.1.9(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.6.3)': 16885 + '@lerna/create@8.1.9(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.8.3)': 16705 16886 dependencies: 16706 16887 '@npmcli/arborist': 7.5.4 16707 16888 '@npmcli/package-json': 5.2.0 ··· 16719 16900 console-control-strings: 1.1.0 16720 16901 conventional-changelog-core: 5.0.1 16721 16902 conventional-recommended-bump: 7.0.1 16722 - cosmiconfig: 9.0.0(typescript@5.6.3) 16903 + cosmiconfig: 9.0.0(typescript@5.8.3) 16723 16904 dedent: 1.5.3 16724 16905 execa: 5.0.0 16725 16906 fs-extra: 11.2.0 ··· 17049 17230 '@motionone/dom': 10.18.0 17050 17231 tslib: 2.8.1 17051 17232 17233 + '@napi-rs/wasm-runtime@0.2.10': 17234 + dependencies: 17235 + '@emnapi/core': 1.4.3 17236 + '@emnapi/runtime': 1.4.3 17237 + '@tybys/wasm-util': 0.9.0 17238 + optional: true 17239 + 17052 17240 '@napi-rs/wasm-runtime@0.2.4': 17053 17241 dependencies: 17054 17242 '@emnapi/core': 1.2.0 ··· 17353 17541 '@octokit/openapi-types': 18.1.1 17354 17542 17355 17543 '@oslojs/encoding@1.1.0': {} 17544 + 17545 + '@oxc-resolver/binding-darwin-arm64@9.0.2': 17546 + optional: true 17547 + 17548 + '@oxc-resolver/binding-darwin-x64@9.0.2': 17549 + optional: true 17550 + 17551 + '@oxc-resolver/binding-freebsd-x64@9.0.2': 17552 + optional: true 17553 + 17554 + '@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2': 17555 + optional: true 17556 + 17557 + '@oxc-resolver/binding-linux-arm64-gnu@9.0.2': 17558 + optional: true 17559 + 17560 + '@oxc-resolver/binding-linux-arm64-musl@9.0.2': 17561 + optional: true 17562 + 17563 + '@oxc-resolver/binding-linux-riscv64-gnu@9.0.2': 17564 + optional: true 17565 + 17566 + '@oxc-resolver/binding-linux-s390x-gnu@9.0.2': 17567 + optional: true 17568 + 17569 + '@oxc-resolver/binding-linux-x64-gnu@9.0.2': 17570 + optional: true 17571 + 17572 + '@oxc-resolver/binding-linux-x64-musl@9.0.2': 17573 + optional: true 17574 + 17575 + '@oxc-resolver/binding-wasm32-wasi@9.0.2': 17576 + dependencies: 17577 + '@napi-rs/wasm-runtime': 0.2.10 17578 + optional: true 17579 + 17580 + '@oxc-resolver/binding-win32-arm64-msvc@9.0.2': 17581 + optional: true 17582 + 17583 + '@oxc-resolver/binding-win32-x64-msvc@9.0.2': 17584 + optional: true 17356 17585 17357 17586 '@oxc-transform/binding-darwin-arm64@0.47.1': 17358 17587 optional: true ··· 19855 20084 '@types/node': 22.15.17 19856 20085 optional: true 19857 20086 19858 - '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': 20087 + '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19859 20088 dependencies: 19860 20089 '@eslint-community/regexpp': 4.12.1 19861 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 20090 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 19862 20091 '@typescript-eslint/scope-manager': 8.13.0 19863 - '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 19864 - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 20092 + '@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 20093 + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 19865 20094 '@typescript-eslint/visitor-keys': 8.13.0 19866 - eslint: 9.14.0(jiti@1.21.6) 20095 + eslint: 9.14.0(jiti@2.4.2) 19867 20096 graphemer: 1.4.0 19868 20097 ignore: 5.3.1 19869 20098 natural-compare: 1.4.0 ··· 19873 20102 transitivePeerDependencies: 19874 20103 - supports-color 19875 20104 19876 - '@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': 20105 + '@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19877 20106 dependencies: 19878 20107 '@typescript-eslint/scope-manager': 8.13.0 19879 20108 '@typescript-eslint/types': 8.13.0 19880 20109 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) 19881 20110 '@typescript-eslint/visitor-keys': 8.13.0 19882 20111 debug: 4.4.0 19883 - eslint: 9.14.0(jiti@1.21.6) 20112 + eslint: 9.14.0(jiti@2.4.2) 19884 20113 optionalDependencies: 19885 20114 typescript: 5.6.3 19886 20115 transitivePeerDependencies: ··· 19891 20120 '@typescript-eslint/types': 8.13.0 19892 20121 '@typescript-eslint/visitor-keys': 8.13.0 19893 20122 19894 - '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': 20123 + '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19895 20124 dependencies: 19896 20125 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) 19897 - '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 20126 + '@typescript-eslint/utils': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 19898 20127 debug: 4.4.0 19899 20128 ts-api-utils: 1.4.0(typescript@5.6.3) 19900 20129 optionalDependencies: ··· 19920 20149 transitivePeerDependencies: 19921 20150 - supports-color 19922 20151 19923 - '@typescript-eslint/utils@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3)': 20152 + '@typescript-eslint/utils@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3)': 19924 20153 dependencies: 19925 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@1.21.6)) 20154 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.2)) 19926 20155 '@typescript-eslint/scope-manager': 8.13.0 19927 20156 '@typescript-eslint/types': 8.13.0 19928 20157 '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) 19929 - eslint: 9.14.0(jiti@1.21.6) 20158 + eslint: 9.14.0(jiti@2.4.2) 19930 20159 transitivePeerDependencies: 19931 20160 - supports-color 19932 20161 - typescript ··· 20776 21005 20777 21006 astring@1.9.0: {} 20778 21007 20779 - astro-expressive-code@0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)): 21008 + astro-expressive-code@0.41.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)): 20780 21009 dependencies: 20781 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 21010 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 20782 21011 rehype-expressive-code: 0.41.2 20783 21012 20784 - astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1): 21013 + astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1): 20785 21014 dependencies: 20786 21015 '@astrojs/compiler': 2.12.0 20787 21016 '@astrojs/internal-helpers': 0.6.1 ··· 20834 21063 unist-util-visit: 5.0.0 20835 21064 unstorage: 1.16.0(idb-keyval@6.2.1) 20836 21065 vfile: 6.0.3 20837 - vite: 6.3.4(@types/node@22.15.17)(jiti@1.21.6)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1) 20838 - vitefu: 1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@1.21.6)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1)) 21066 + vite: 6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1) 21067 + vitefu: 1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1)) 20839 21068 xxhash-wasm: 1.1.0 20840 21069 yargs-parser: 21.1.1 20841 21070 yocto-spinner: 0.2.2 ··· 21794 22023 optionalDependencies: 21795 22024 typescript: 5.6.3 21796 22025 21797 - cosmiconfig@9.0.0(typescript@5.6.3): 22026 + cosmiconfig@9.0.0(typescript@5.8.3): 21798 22027 dependencies: 21799 22028 env-paths: 2.2.1 21800 22029 import-fresh: 3.3.0 21801 22030 js-yaml: 4.1.0 21802 22031 parse-json: 5.2.0 21803 22032 optionalDependencies: 21804 - typescript: 5.6.3 22033 + typescript: 5.8.3 21805 22034 21806 22035 crc-32@1.2.2: {} 21807 22036 ··· 22550 22779 transitivePeerDependencies: 22551 22780 - supports-color 22552 22781 22553 - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@1.21.6)): 22782 + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@2.4.2)): 22554 22783 dependencies: 22555 22784 debug: 3.2.7 22556 22785 optionalDependencies: 22557 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 22558 - eslint: 9.14.0(jiti@1.21.6) 22786 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 22787 + eslint: 9.14.0(jiti@2.4.2) 22559 22788 eslint-import-resolver-node: 0.3.9 22560 22789 transitivePeerDependencies: 22561 22790 - supports-color 22562 22791 22563 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.14.0(jiti@1.21.6)): 22792 + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint@9.14.0(jiti@2.4.2)): 22564 22793 dependencies: 22565 22794 '@rtsao/scc': 1.1.0 22566 22795 array-includes: 3.1.8 ··· 22569 22798 array.prototype.flatmap: 1.3.2 22570 22799 debug: 3.2.7 22571 22800 doctrine: 2.1.0 22572 - eslint: 9.14.0(jiti@1.21.6) 22801 + eslint: 9.14.0(jiti@2.4.2) 22573 22802 eslint-import-resolver-node: 0.3.9 22574 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@1.21.6)) 22803 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.14.0(jiti@2.4.2)) 22575 22804 hasown: 2.0.2 22576 22805 is-core-module: 2.15.1 22577 22806 is-glob: 4.0.3 ··· 22583 22812 string.prototype.trimend: 1.0.8 22584 22813 tsconfig-paths: 3.15.0 22585 22814 optionalDependencies: 22586 - '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@1.21.6))(typescript@5.6.3) 22815 + '@typescript-eslint/parser': 8.13.0(eslint@9.14.0(jiti@2.4.2))(typescript@5.6.3) 22587 22816 transitivePeerDependencies: 22588 22817 - eslint-import-resolver-typescript 22589 22818 - eslint-import-resolver-webpack ··· 22603 22832 22604 22833 eslint-visitor-keys@4.2.0: {} 22605 22834 22606 - eslint@9.14.0(jiti@1.21.6): 22835 + eslint@9.14.0(jiti@2.4.2): 22607 22836 dependencies: 22608 - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@1.21.6)) 22837 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.2)) 22609 22838 '@eslint-community/regexpp': 4.12.1 22610 22839 '@eslint/config-array': 0.18.0 22611 22840 '@eslint/core': 0.7.0 ··· 22641 22870 optionator: 0.9.4 22642 22871 text-table: 0.2.0 22643 22872 optionalDependencies: 22644 - jiti: 1.21.6 22873 + jiti: 2.4.2 22645 22874 transitivePeerDependencies: 22646 22875 - supports-color 22647 22876 ··· 23128 23357 merge2: 1.4.1 23129 23358 micromatch: 4.0.8 23130 23359 23360 + fast-glob@3.3.3: 23361 + dependencies: 23362 + '@nodelib/fs.stat': 2.0.5 23363 + '@nodelib/fs.walk': 1.2.8 23364 + glob-parent: 5.1.2 23365 + merge2: 1.4.1 23366 + micromatch: 4.0.8 23367 + 23131 23368 fast-json-stable-stringify@2.1.0: {} 23132 23369 23133 23370 fast-levenshtein@2.0.6: {} ··· 23173 23410 ua-parser-js: 1.0.38 23174 23411 transitivePeerDependencies: 23175 23412 - encoding 23413 + 23414 + fd-package-json@2.0.0: 23415 + dependencies: 23416 + walk-up-path: 4.0.0 23176 23417 23177 23418 fd-slicer@1.1.0: 23178 23419 dependencies: ··· 23425 23666 asynckit: 0.4.0 23426 23667 combined-stream: 1.0.8 23427 23668 mime-types: 2.1.35 23669 + 23670 + formatly@0.2.4: 23671 + dependencies: 23672 + fd-package-json: 2.0.0 23428 23673 23429 23674 forwarded@0.2.0: {} 23430 23675 ··· 24769 25014 24770 25015 jiti@1.21.6: {} 24771 25016 25017 + jiti@2.4.2: {} 25018 + 24772 25019 join-component@1.1.0: {} 24773 25020 24774 25021 jose@4.15.9: {} ··· 24954 25201 24955 25202 klona@2.0.6: {} 24956 25203 25204 + knip@5.59.1(@types/node@22.15.17)(typescript@5.8.3): 25205 + dependencies: 25206 + '@nodelib/fs.walk': 1.2.8 25207 + '@types/node': 22.15.17 25208 + fast-glob: 3.3.3 25209 + formatly: 0.2.4 25210 + jiti: 2.4.2 25211 + js-yaml: 4.1.0 25212 + minimist: 1.2.8 25213 + oxc-resolver: 9.0.2 25214 + picocolors: 1.1.1 25215 + picomatch: 4.0.2 25216 + smol-toml: 1.3.4 25217 + strip-json-comments: 5.0.1 25218 + typescript: 5.8.3 25219 + zod: 3.24.4 25220 + zod-validation-error: 3.4.1(zod@3.24.4) 25221 + 24957 25222 launch-editor@2.9.1: 24958 25223 dependencies: 24959 25224 picocolors: 1.1.1 ··· 24961 25226 24962 25227 lerna@8.1.9(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13): 24963 25228 dependencies: 24964 - '@lerna/create': 8.1.9(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.6.3) 25229 + '@lerna/create': 8.1.9(@swc/core@1.8.0(@swc/helpers@0.5.17))(encoding@0.1.13)(typescript@5.8.3) 24965 25230 '@npmcli/arborist': 7.5.4 24966 25231 '@npmcli/package-json': 5.2.0 24967 25232 '@npmcli/run-script': 8.1.0 ··· 24979 25244 conventional-changelog-angular: 7.0.0 24980 25245 conventional-changelog-core: 5.0.1 24981 25246 conventional-recommended-bump: 7.0.1 24982 - cosmiconfig: 9.0.0(typescript@5.6.3) 25247 + cosmiconfig: 9.0.0(typescript@5.8.3) 24983 25248 dedent: 1.5.3 24984 25249 envinfo: 7.13.0 24985 25250 execa: 5.0.0 ··· 25032 25297 strong-log-transformer: 2.1.0 25033 25298 tar: 6.2.1 25034 25299 temp-dir: 1.0.0 25035 - typescript: 5.6.3 25300 + typescript: 5.8.3 25036 25301 upath: 2.0.1 25037 25302 uuid: 10.0.0 25038 25303 validate-npm-package-license: 3.0.4 ··· 27032 27297 transitivePeerDependencies: 27033 27298 - zod 27034 27299 27300 + oxc-resolver@9.0.2: 27301 + optionalDependencies: 27302 + '@oxc-resolver/binding-darwin-arm64': 9.0.2 27303 + '@oxc-resolver/binding-darwin-x64': 9.0.2 27304 + '@oxc-resolver/binding-freebsd-x64': 9.0.2 27305 + '@oxc-resolver/binding-linux-arm-gnueabihf': 9.0.2 27306 + '@oxc-resolver/binding-linux-arm64-gnu': 9.0.2 27307 + '@oxc-resolver/binding-linux-arm64-musl': 9.0.2 27308 + '@oxc-resolver/binding-linux-riscv64-gnu': 9.0.2 27309 + '@oxc-resolver/binding-linux-s390x-gnu': 9.0.2 27310 + '@oxc-resolver/binding-linux-x64-gnu': 9.0.2 27311 + '@oxc-resolver/binding-linux-x64-musl': 9.0.2 27312 + '@oxc-resolver/binding-wasm32-wasi': 9.0.2 27313 + '@oxc-resolver/binding-win32-arm64-msvc': 9.0.2 27314 + '@oxc-resolver/binding-win32-x64-msvc': 9.0.2 27315 + 27035 27316 oxc-transform@0.47.1: 27036 27317 optionalDependencies: 27037 27318 '@oxc-transform/binding-darwin-arm64': 0.47.1 ··· 27385 27666 postcss: 8.5.3 27386 27667 ts-node: 10.9.2(@swc/core@1.8.0(@swc/helpers@0.5.17))(@types/node@22.15.17)(typescript@5.3.3) 27387 27668 27388 - postcss-load-config@6.0.1(jiti@1.21.6)(postcss@8.5.3)(yaml@2.5.1): 27669 + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.5.1): 27389 27670 dependencies: 27390 27671 lilconfig: 3.1.3 27391 27672 optionalDependencies: 27392 - jiti: 1.21.6 27673 + jiti: 2.4.2 27393 27674 postcss: 8.5.3 27394 27675 yaml: 2.5.1 27395 27676 ··· 27461 27742 27462 27743 prelude-ls@1.2.1: {} 27463 27744 27464 - prettier-plugin-organize-imports@4.1.0(prettier@3.4.2)(typescript@5.6.3): 27745 + prettier-plugin-organize-imports@4.1.0(prettier@3.4.2)(typescript@5.8.3): 27465 27746 dependencies: 27466 27747 prettier: 3.4.2 27467 - typescript: 5.6.3 27748 + typescript: 5.8.3 27468 27749 27469 27750 prettier@3.4.2: {} 27470 27751 ··· 28888 29169 dependencies: 28889 29170 type-fest: 0.7.1 28890 29171 28891 - starlight-openapi-rapidoc@0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3): 29172 + starlight-openapi-rapidoc@0.8.1-beta(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3): 28892 29173 dependencies: 28893 - '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 29174 + '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 28894 29175 '@astropub/md': 0.4.0 28895 29176 '@readme/openapi-parser': 2.5.0(openapi-types@12.1.3) 28896 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 29177 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 28897 29178 github-slugger: 2.0.0 28898 29179 transitivePeerDependencies: 28899 29180 - '@astrojs/markdown-remark' 28900 29181 - openapi-types 28901 29182 28902 - starlight-openapi@0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3): 29183 + starlight-openapi@0.17.0(@astrojs/starlight@0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)))(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1))(openapi-types@12.1.3): 28903 29184 dependencies: 28904 - '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 29185 + '@astrojs/starlight': 0.34.2(astro@5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1)) 28905 29186 '@readme/openapi-parser': 2.7.0(openapi-types@12.1.3) 28906 - astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@1.21.6)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 29187 + astro: 5.7.10(@types/node@22.15.17)(encoding@0.1.13)(idb-keyval@6.2.1)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@4.40.1)(terser@5.32.0)(typescript@5.8.3)(yaml@2.5.1) 28907 29188 github-slugger: 2.0.0 28908 29189 url-template: 3.1.1 28909 29190 transitivePeerDependencies: ··· 29027 29308 29028 29309 strip-json-comments@3.1.1: {} 29029 29310 29311 + strip-json-comments@5.0.1: {} 29312 + 29030 29313 strip-outer@1.0.1: 29031 29314 dependencies: 29032 29315 escape-string-regexp: 1.0.5 ··· 29128 29411 chokidar: 3.6.0 29129 29412 didyoumean: 1.2.2 29130 29413 dlv: 1.1.3 29131 - fast-glob: 3.3.2 29414 + fast-glob: 3.3.3 29132 29415 glob-parent: 6.0.2 29133 29416 is-glob: 4.0.3 29134 29417 jiti: 1.21.6 ··· 29490 29773 29491 29774 tslib@2.8.1: {} 29492 29775 29493 - tsup@8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@1.21.6)(postcss@8.5.3)(typescript@5.3.3)(yaml@2.5.1): 29776 + tsup@8.5.0(@swc/core@1.8.0(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.5.1): 29494 29777 dependencies: 29495 29778 bundle-require: 5.1.0(esbuild@0.25.3) 29496 29779 cac: 6.7.14 ··· 29501 29784 fix-dts-default-cjs-exports: 1.0.1 29502 29785 joycon: 3.1.1 29503 29786 picocolors: 1.1.1 29504 - postcss-load-config: 6.0.1(jiti@1.21.6)(postcss@8.5.3)(yaml@2.5.1) 29787 + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.3)(yaml@2.5.1) 29505 29788 resolve-from: 5.0.0 29506 29789 rollup: 4.40.1 29507 29790 source-map: 0.8.0-beta.0 ··· 29512 29795 optionalDependencies: 29513 29796 '@swc/core': 1.8.0(@swc/helpers@0.5.17) 29514 29797 postcss: 8.5.3 29515 - typescript: 5.3.3 29798 + typescript: 5.8.3 29516 29799 transitivePeerDependencies: 29517 29800 - jiti 29518 29801 - supports-color ··· 29936 30219 - utf-8-validate 29937 30220 - zod 29938 30221 29939 - vite@6.3.4(@types/node@22.15.17)(jiti@1.21.6)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1): 30222 + vite@6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1): 29940 30223 dependencies: 29941 30224 esbuild: 0.25.3 29942 30225 fdir: 6.4.4(picomatch@4.0.2) ··· 29947 30230 optionalDependencies: 29948 30231 '@types/node': 22.15.17 29949 30232 fsevents: 2.3.3 29950 - jiti: 1.21.6 30233 + jiti: 2.4.2 29951 30234 lightningcss: 1.29.1 29952 30235 terser: 5.32.0 29953 30236 yaml: 2.5.1 29954 30237 29955 - vitefu@1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@1.21.6)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1)): 30238 + vitefu@1.0.6(vite@6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1)): 29956 30239 optionalDependencies: 29957 - vite: 6.3.4(@types/node@22.15.17)(jiti@1.21.6)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1) 30240 + vite: 6.3.4(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.32.0)(yaml@2.5.1) 29958 30241 29959 30242 vlq@1.0.1: {} 29960 30243 ··· 30002 30285 30003 30286 walk-up-path@3.0.1: {} 30004 30287 30288 + walk-up-path@4.0.0: {} 30289 + 30005 30290 walker@1.0.8: 30006 30291 dependencies: 30007 30292 makeerror: 1.0.12 ··· 30399 30684 typescript: 5.8.3 30400 30685 zod: 3.24.4 30401 30686 30687 + zod-validation-error@3.4.1(zod@3.24.4): 30688 + dependencies: 30689 + zod: 3.24.4 30690 + 30402 30691 zod@3.24.4: {} 30403 30692 30404 30693 zustand@5.0.0(@types/react@18.3.12)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.2.0(react@18.3.1)): ··· 30407 30696 immer: 10.1.1 30408 30697 react: 18.3.1 30409 30698 use-sync-external-store: 1.2.0(react@18.3.1) 30699 + 30700 + zustand@5.0.5(@types/react@18.3.12)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)): 30701 + optionalDependencies: 30702 + '@types/react': 18.3.12 30703 + immer: 10.1.1 30704 + react: 18.3.1 30705 + use-sync-external-store: 1.2.2(react@18.3.1) 30410 30706 30411 30707 zwitch@2.0.4: {}