Caching
I started working on a caching dispatcher + handler. Thought I'd post it here in case anyone is interested in working on something like that.
We only needed to cache redirects so I kind of stopped there.
import stream from 'node:stream'
import { LRUCache } from 'lru-cache'
import cacheControlParser from 'cache-control-parser'
class CacheHandler {
constructor({ key, handler, store }) {
this.entry = null
this.key = key
this.handler = handler
this.store = store
this.abort = null
this.resume = null
}
onConnect(abort) {
this.abort = abort
return this.handler.onConnect(abort)
}
onHeaders(statusCode, rawHeaders, resume, statusMessage) {
this.resume = resume
// TODO (fix): Check if content-length fits in cache...
let cacheControl
for (let n = 0; n < rawHeaders.length; n += 2) {
if (
rawHeaders[n].length === 'cache-control'.length &&
rawHeaders[n].toString().toLowerCase() === 'cache-control'
) {
cacheControl = cacheControlParser.parse(rawHeaders[n + 1].toString())
break
}
}
if (
cacheControl &&
cacheControl.public &&
!cacheControl.private &&
!cacheControl['no-store'] &&
// TODO (fix): Support all cache control directives...
// !opts.headers['no-transform'] &&
!cacheControl['no-cache'] &&
!cacheControl['must-understand'] &&
!cacheControl['must-revalidate'] &&
!cacheControl['proxy-revalidate']
) {
const maxAge = cacheControl['s-max-age'] ?? cacheControl['max-age']
const ttl = cacheControl.immutable
? 31556952 // 1 year
: Number(maxAge)
if (ttl > 0) {
this.entry = this.store.create(this.key, ttl * 1e3)
this.entry.statusCode = statusCode
this.entry.statusMessage = statusMessage
this.entry.rawHeaders = rawHeaders
this.entry.on('drain', resume)
this.entry.on('error', this.abort)
}
}
return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
}
onData(chunk) {
let ret = true
if (this.entry) {
this.entry.size += chunk.bodyLength
if (this.entry.size > this.store.maxEntrySize) {
this.entry.destroy()
this.entry = null
} else {
ret = this.entry.write(chunk)
}
}
return this.handler.onData(chunk) !== false && ret !== false
}
onComplete(rawTrailers) {
if (this.entry) {
this.entry.rawTrailers = rawTrailers
this.entry.end()
}
return this.handler.onComplete(rawTrailers)
}
onError(err) {
if (this.entry) {
this.entry.destroy(err)
}
return this.handler.onError(err)
}
}
// TODO (fix): Filsystem backed cache...
class CacheStore {
constructor({ maxSize, maxEntrySize }) {
this.maxSize = maxSize
this.maxEntrySize = maxEntrySize
this.cache = new LRUCache({
maxSize,
sizeCalculation: (value) => value.body.byteLength,
})
}
create (key, ttl) {
const entry = Object.assign(new stream.PassThrough(), {
statusCode: null,
statusMessage: null,
rawHeaders: null,
rawTrailers: null,
size: 0,
}).on('finish', () => {
this.cache.set(key, entry, ttl)
})
return entry
}
get(key, callback) {
callback(null, this.cache.get(key))
}
}
export class CacheDispatcher {
constructor(dispatcher, { maxSize = 0, maxEntrySize = maxSize / 10 }) {
this.dispatcher = dispatcher
this.store = new CacheStore({ maxSize, maxEntrySize })
}
dispatch(opts, handler) {
if (opts.headers?.['cache-control'] || opts.headers?.authorization) {
// TODO (fix): Support all cache control directives...
// const cacheControl = cacheControlParser.parse(opts.headers['cache-control'])
// cacheControl['no-cache']
// cacheControl['no-store']
// cacheControl['max-age']
// cacheControl['max-stale']
// cacheControl['min-fresh']
// cacheControl['no-transform']
// cacheControl['only-if-cached']
this.dispatcher.dispatch(opts, handler)
return
}
// TODO (fix): Support all methods?
if (opts.method !== 'GET' && opts.method !== 'HEAD') {
this.dispatcher.dispatch(opts, handler)
return
}
// TODO (fix): Support body?
opts.body.resume()
// TODO (fix): How to generate key?
const key = `${opts.method}:${opts.path}`
this.store.get(key, (err, value) => {
if (err) {
// TODO (fix): How to handle cache errors?
this.handler.onError(err)
} else if (value) {
const { statusCode, statusMessage, rawHeaders, rawTrailers } = value
const ac = new AbortController()
const signal = ac.signal
let _resume = null
const resume = () => {
_resume?.(null)
_resume = null
}
const abort = () => {
ac.abort()
resume()
}
try {
handler.onConnect(abort)
signal.throwIfAborted()
handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
signal.throwIfAborted()
stream.pipeline(
value,
new stream.Writable({
signal,
write(chunk, encoding, callback) {
try {
if (handler.onData(chunk) === false) {
_resume = callback
} else {
callback(null)
}
} catch (err) {
callback(err)
}
},
}),
(err) => {
if (err) {
handler.onError(err)
} else {
handler.onComplete(rawTrailers)
}
}
)
} catch (err) {
handler.onError(err)
}
} else {
this.dispatcher.dispatch(opts, new CacheHandler({ handler, store: this.store, key }))
}
})
}
}
Hope you find this helpful https://github.com/radix-ui/primitives/issues/1836. It is also in the Dialog docs
Hope you find this helpful https://ui.shadcn.com/docs/components/dialog. It is also in the
Dialogdocs
the dialog works within the dropdown menu, but keeps the dropdown menu open when the dialoog opens.
If you checked the previous issue, you would have found one of the comments containing an example that covers exactly what you're looking for. Here's a link for the comment including a sandbox link https://github.com/radix-ui/primitives/issues/1836#issuecomment-1433597185
If nothing works use alert-dialog
If you checked the previous issue, you would have found one of the comments containing an example that covers exactly what you're looking for. Here's a link for the comment including a sandbox link radix-ui/primitives#1836 (comment)
I tried this but it doesnt work for me, it does not open modal and after clicking on the modal trigger the dropdown does not open anymore. This is the code:
interface Props {
userId: string;
escortId: string;
}
interface DialogItemProps {
onSelect?: () => void;
onOpenChange?: (open: boolean) => void;
}
const DropDownMenu = ({ userId, escortId }: Props) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [hasOpenDialog, setHasOpenDialog] = useState(false);
const dropdownTriggerRef: RefObject<HTMLButtonElement> = useRef(null);
const focusRef = useRef<HTMLButtonElement | null>(null);
const handleDialogItemSelect = () => {
focusRef.current = dropdownTriggerRef.current;
};
const handleDialogItemOpenChange = (open: boolean) => {
setHasOpenDialog(open);
if (open === false) {
setDropdownOpen(false);
}
};
const DialogItem = forwardRef<HTMLDivElement, DialogItemProps>(
({ onSelect, onOpenChange, ...itemProps }, forwardedRef) => {
return (
<Dialog onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<DropdownMenuItem
{...itemProps}
ref={forwardedRef}
className="justify-start cursor-pointer"
onSelect={(ev: Event) => {
ev.preventDefault();
onSelect && onSelect();
}}
>
remove user
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
confirm remove user
</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button
className="rounded"
type="submit"
onClick={() => removeUser(userId)}
>
Remove User
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
);
return (
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="w-8 h-8 p-0"
ref={dropdownTriggerRef}
>
<span className="sr-only">open menu</span>
<FiMoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
hidden={hasOpenDialog}
align="center"
onCloseAutoFocus={(ev) => {
if (focusRef.current) {
focusRef?.current?.focus();
focusRef.current = null;
ev.preventDefault();
}
}}
>
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-start cursor-pointer">
<Link href={`/user/${userId}`}>
user profile
</Link>
</DropdownMenuItem>
<DialogItem
onSelect={handleDialogItemSelect}
onOpenChange={handleDialogItemOpenChange}
/>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default DropDownMenu;
I have the same issue. i can open the dialog, but when i close it. it freezes the whole screen. The example on Shadcn doesnt seem to work
https://github.com/radix-ui/primitives/issues/1836#issuecomment-1674338372 it working
If you checked the previous issue, you would have found one of the comments containing an example that covers exactly what you're looking for. Here's a link for the comment including a sandbox link radix-ui/primitives#1836 (comment)
I tried this but it doesnt work for me, it does not open modal and after clicking on the modal trigger the dropdown does not open anymore. This is the code:
interface Props { userId: string; escortId: string; } interface DialogItemProps { onSelect?: () => void; onOpenChange?: (open: boolean) => void; } const DropDownMenu = ({ userId, escortId }: Props) => { const [dropdownOpen, setDropdownOpen] = useState(false); const [hasOpenDialog, setHasOpenDialog] = useState(false); const dropdownTriggerRef: RefObject<HTMLButtonElement> = useRef(null); const focusRef = useRef<HTMLButtonElement | null>(null); const handleDialogItemSelect = () => { focusRef.current = dropdownTriggerRef.current; }; const handleDialogItemOpenChange = (open: boolean) => { setHasOpenDialog(open); if (open === false) { setDropdownOpen(false); } }; const DialogItem = forwardRef<HTMLDivElement, DialogItemProps>( ({ onSelect, onOpenChange, ...itemProps }, forwardedRef) => { return ( <Dialog onOpenChange={onOpenChange}> <DialogTrigger asChild> <DropdownMenuItem {...itemProps} ref={forwardedRef} className="justify-start cursor-pointer" onSelect={(ev: Event) => { ev.preventDefault(); onSelect && onSelect(); }} > remove user </DropdownMenuItem> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle> confirm remove user </DialogTitle> </DialogHeader> <DialogFooter> <Button className="rounded" type="submit" onClick={() => removeUser(userId)} > Remove User </Button> </DialogFooter> </DialogContent> </Dialog> ); } ); return ( <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenuTrigger asChild> <Button variant="ghost" className="w-8 h-8 p-0" ref={dropdownTriggerRef} > <span className="sr-only">open menu</span> <FiMoreHorizontal className="w-4 h-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent hidden={hasOpenDialog} align="center" onCloseAutoFocus={(ev) => { if (focusRef.current) { focusRef?.current?.focus(); focusRef.current = null; ev.preventDefault(); } }} > <DropdownMenuSeparator /> <DropdownMenuItem className="justify-start cursor-pointer"> <Link href={`/user/${userId}`}> user profile </Link> </DropdownMenuItem> <DialogItem onSelect={handleDialogItemSelect} onOpenChange={handleDialogItemOpenChange} /> </DropdownMenuContent> </DropdownMenu> ); }; export default DropDownMenu;
if (open === false) { setDropdownOpen(false); }
delete this line, it can achieve open results. But i don`t konw how to hide DropdownMenu componet.
@Steveb599 - did you manage to find a flexible solution in the end?
I even tried setting modal={false} on <DropdownMenu modal={false}> in conjunction with removing the onSelect prop callback from the menu item from another issue as a supposed solution, but that instantly opened and closed the modal, even though it closed the dropdown menu, which is of course the worst case scenario and not a working suggestion (in my case).
Also, the other suggested solutions of placing the <Dialog> component outside of and wrapping the <DropdownMenu> is really not ideal as that would only work for single dialogs within a single dropdown menu, and i would rather not add multiple inelegant states to manage multiple dialogs..
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.