Skip to content

Commit

Permalink
feat: concurrency demo done
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolastoulemont committed Oct 31, 2023
1 parent 4a6334b commit c83592a
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 56 deletions.
4 changes: 2 additions & 2 deletions app/components/Callout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode } from 'react'
import { type ReactNode } from 'react'
import * as FiIcons from 'react-icons/fi'
import { CATEGORY_COLOR_VARIANTS, ColorNames } from '~/utils/styles'
import { CATEGORY_COLOR_VARIANTS, type ColorNames } from '~/utils/styles'
interface CalloutProps {
children: ReactNode
icon?: keyof typeof FiIcons
Expand Down
150 changes: 133 additions & 17 deletions app/components/ConcurrencyDemo/ConcurrencyDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,143 @@
// import { usePQueue } from "./usePQueue"
import { useCallback, useState } from 'react'
import { usePQueue } from './usePQueue'
import clsx from 'clsx'
import { FiPlusCircle, FiXCircle } from 'react-icons/fi'
import { sleep } from '~/utils/sleep'

type BlockStatus = 'Initial' | 'Queued' | 'Loading' | 'Done'

const generateBlocks = (status: BlockStatus = 'Initial') =>
Array.from({ length: 20 }).reduce<Record<string, { id: string; status: BlockStatus }>>(
(acc, _, index) => {
const id = `square-${index}`
acc[id] = { id, status }
return acc
},
{}
)

export function ConcurrencyDemo() {
// const {enqueue, dequeue, clearQueue } = usePQueue({ concurrency: 2 })

async function handleClick() {
try {
const res = await fetch("/concurrency-demo?duration=1500")
const data = await res.json()
console.log({ data })
} catch (error) {
console.error(error)
const [concurrency, setConcurrency] = useState(2)
const [numberOfFetchExecuted, setNumberOfFetchExecuted] = useState(0)
const [blocks, setBlocks] = useState(generateBlocks())
const { enqueue, dequeue, clearQueue } = usePQueue({ concurrency })

const updateBlockStatus = useCallback(
(blockId: string, status: BlockStatus) => {
setBlocks((prev) => {
const newBlocks = { ...prev }
newBlocks[blockId] = { id: blockId, status }
return newBlocks
})
},
[setBlocks]
)

const demoPromise = useCallback(
async (block: string) => {
updateBlockStatus(block, 'Loading')
await sleep(250)
await fetch('/concurrency-demo')
setNumberOfFetchExecuted((prev) => prev + 1)
updateBlockStatus(block, 'Done')
},
[updateBlockStatus, setNumberOfFetchExecuted]
)

async function handleQueueAll() {
setNumberOfFetchExecuted(0)
setBlocks(generateBlocks('Queued'))
for (const block in blocks) {
enqueue({
id: block,
fn: async () => await demoPromise(block),
})
}
}

function handleQueueOne(block: string) {
enqueue({
id: block,
fn: async () => demoPromise(block),
})
}

function handleCancel(blockId: string) {
dequeue(blockId)
updateBlockStatus(blockId, 'Initial')
}

function handleReset() {
clearQueue()
setBlocks(generateBlocks())
setNumberOfFetchExecuted(0)
}

return (
<div className="flex flex-col gap-3">
<h3>Hello</h3>
<button
onClick={handleClick}
className="rounded-md border-none bg-blue-700 px-3 py-1 text-white"
>
Fetch
</button>
<div className="flex items-center justify-between gap-3">
<div className="flex gap-3">
<select
name="concurrency-number"
className="block rounded-md border-0 py-2 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6"
value={concurrency}
onChange={(e) => {
setConcurrency(parseInt(e.target.value, 10))
}}
>
<option>2</option>
<option>4</option>
<option>6</option>
<option>8</option>
<option>10</option>
</select>
<button
onClick={handleQueueAll}
className="rounded-lg border-none bg-blue-700 px-3 py-1 text-white"
>
Start
</button>
<button
onClick={handleReset}
className="rounded-lg border-none bg-slate-200 px-3 py-1"
>
Reset
</button>
</div>
<p>Number of fetch executed: {numberOfFetchExecuted}</p>
</div>

<div className="grid grid-cols-10 gap-3">
{Object.values(blocks).map((block) => (
<div
key={block.id}
className={clsx(
'relative flex h-14 w-20 items-center justify-center rounded-lg bg-slate-200',
block.status === 'Queued' && 'opacity-50',
block.status === 'Loading' && 'bg-yellow-400 text-white',
block.status === 'Done' && 'bg-green-600 text-white'
)}
>
{block.status}
{block.status === 'Queued' && (
<button
className="absolute right-[-7.5px] top-[-7.5px]"
onClick={() => handleCancel(block.id)}
>
<FiXCircle className="h-5 w-5 text-black" />
</button>
)}
{block.status === 'Initial' && (
<button
className="absolute right-[-7.5px] top-[-7.5px]"
onClick={() => handleQueueOne(block.id)}
>
<FiPlusCircle className="h-5 w-5 text-black" />
</button>
)}
</div>
))}
</div>
</div>
)
}
44 changes: 21 additions & 23 deletions app/components/ConcurrencyDemo/usePQueue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import PQueue, { AbortError } from "p-queue"
import type {Options, QueueAddOptions} from 'p-queue'
import type PriorityQueue from "p-queue/dist/priority-queue"
import { useState, useEffect, useCallback } from "react"
import PQueue, { AbortError } from 'p-queue'
import type { Options, QueueAddOptions } from 'p-queue'
import type PriorityQueue from 'p-queue/dist/priority-queue'
import { useEffect, useCallback, useMemo } from 'react'

interface Task {
id: string
Expand All @@ -13,24 +13,23 @@ export function usePQueue({
autoStart = true,
...rest
}: Options<PriorityQueue, QueueAddOptions>) {
const [PQ] = useState(() => new PQueue({ concurrency, ...rest }))
const [abortControllersMap, setAbortControllersMap] = useState<
Record<string, AbortController>
>({})
const PQ = useMemo(() => new PQueue({ concurrency, ...rest }), [concurrency, rest])
const AbortControllerMap = useMemo(() => new Map<string, AbortController>(), [])

useEffect(() => {
if (autoStart && Object.keys(abortControllersMap).length > 0) {
if (autoStart && AbortControllerMap.size > 0) {
PQ.start()
}
}, [abortControllersMap, PQ, autoStart])
}, [AbortControllerMap, PQ, autoStart])

PQ.on('idle', () => {
AbortControllerMap.clear()
})

const enqueue = useCallback(
async (task: Task) => {
const controller = new AbortController()
setAbortControllersMap((prev) => ({
...prev,
[task.id]: controller,
}))
AbortControllerMap.set(task.id, controller)

try {
await PQ.add(task.fn, { signal: controller.signal })
Expand All @@ -40,31 +39,30 @@ export function usePQueue({
}
}
},
[PQ]
[PQ, AbortControllerMap]
)

const dequeue = useCallback(
(taskId: Task["id"]) => {
const controller = abortControllersMap[taskId]
(taskId: Task['id']) => {
const controller = AbortControllerMap.get(taskId)
if (controller) {
controller.abort()
const { [taskId]: removed, ...rest } = abortControllersMap
setAbortControllersMap(rest)
AbortControllerMap.delete(taskId)
}
},
[abortControllersMap]
[AbortControllerMap]
)

const clearQueue = useCallback(() => {
PQ.clear()
setAbortControllersMap({})
}, [PQ])
AbortControllerMap.clear()
}, [PQ, AbortControllerMap])

return {
enqueue,
dequeue,
clearQueue,
start: PQ.start,
onIdle: PQ.onIdle
onIdle: PQ.onIdle,
}
}
2 changes: 1 addition & 1 deletion app/routes/$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function CatchBoundary() {
return (
<main className="mx-auto block h-[calc(100vh-56px)] w-full max-w-6xl px-6 sm:px-12">
<section className="flex flex-col space-y-6 text-center sm:mt-12">
<img src="/img/404.png" className="mx-auto w-36" />
<img src="/img/404.png" className="mx-auto w-36" alt="Post not found" />
<h1 className=" mx-auto text-3xl font-bold text-slate-800 dark:text-white sm:mt-12 sm:text-5xl">
{caught.status}
</h1>
Expand Down
13 changes: 2 additions & 11 deletions app/routes/concurrency-demo.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import { json, type LoaderArgs } from "@remix-run/node"
import { sleep } from "~/utils/sleep"

export const loader = async ({ request }: LoaderArgs) => {
const urlSearchParams = new URLSearchParams(request.url.split("?")[1])
const params = Object.fromEntries(urlSearchParams.entries())

if (params.duration && typeof parseInt(params.duration, 10) === "number") {
await sleep(parseInt(params.duration, 10))
return json({ success: true })
}
import { json } from '@remix-run/node'

export const loader = async () => {
return json({ success: true })
}
4 changes: 2 additions & 2 deletions app/utils/sleep.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function sleep(duration: number) {
return new Promise(res => setTimeout(res, duration))
}
return new Promise((res) => setTimeout(res, duration))
}

0 comments on commit c83592a

Please sign in to comment.