diff --git a/.changeset/queue-initial.md b/.changeset/queue-initial.md new file mode 100644 index 000000000..4427fc554 --- /dev/null +++ b/.changeset/queue-initial.md @@ -0,0 +1,14 @@ +--- +"@solid-primitives/queue": minor +--- + +Initial release of `@solid-primitives/queue` + +Six primitives for managing queues: + +- **`makeQueue(initialValues?)`** — non-reactive FIFO queue backed by a plain array. +- **`createQueue(initialValues?)`** — reactive FIFO queue backed by Solid signals. Exposes reactive accessors (`queue`, `first`, `last`, `size`, `isEmpty`) and imperative methods (`add`, `remove`, `clear`). +- **`makePriorityQueue(comparator, initialValues?)`** — non-reactive priority queue; items are dequeued in comparator order rather than insertion order. +- **`createPriorityQueue(comparator, initialValues?)`** — reactive priority queue; same interface as `createQueue`. +- **`createTaskQueue()`** — reactive async task queue. Tasks execute one at a time in FIFO order. `enqueue(task)` returns a `Promise`. Exposes reactive `size` (pending count) and `active` (`boolean`). +- **`createConcurrentTaskQueue(concurrency)`** — reactive async task queue running up to `concurrency` tasks simultaneously. `active` is a count (`Accessor`). diff --git a/packages/queue/CHANGELOG.md b/packages/queue/CHANGELOG.md new file mode 100644 index 000000000..8721ad217 --- /dev/null +++ b/packages/queue/CHANGELOG.md @@ -0,0 +1,12 @@ +# @solid-primitives/queue + +## 0.1.0 + +### Initial release + +- `makeQueue(initialValues?)` — non-reactive FIFO queue backed by a plain array +- `createQueue(initialValues?)` — reactive FIFO queue backed by Solid signals; exposes `first`, `last`, `size`, `isEmpty`, `add`, `remove`, `clear` +- `makePriorityQueue(comparator, initialValues?)` — non-reactive priority queue; items dequeued in comparator order +- `createPriorityQueue(comparator, initialValues?)` — reactive priority queue; same interface as `createQueue` +- `createTaskQueue()` — reactive async task queue; tasks execute one at a time in FIFO order; `enqueue(task)` returns a Promise for the task's result; exposes reactive `size` and `active` +- `createConcurrentTaskQueue(concurrency)` — reactive async task queue running up to `concurrency` tasks simultaneously; `active` is a count (`Accessor`) diff --git a/packages/queue/LICENSE b/packages/queue/LICENSE new file mode 100644 index 000000000..38b41d975 --- /dev/null +++ b/packages/queue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Solid Primitives Working Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/queue/README.md b/packages/queue/README.md new file mode 100644 index 000000000..a7ac24d1c --- /dev/null +++ b/packages/queue/README.md @@ -0,0 +1,276 @@ +

+ Solid Primitives queue +

+ +# @solid-primitives/queue + +[![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/queue?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/queue) +[![version](https://img.shields.io/npm/v/@solid-primitives/queue?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/queue) +[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process) + +Queue primitives for Solid.js. + +- **`makeQueue`** — non-reactive FIFO queue backed by a plain array. No Solid lifecycle hooks; suitable for non-reactive contexts. +- **`createQueue`** — reactive FIFO queue backed by Solid signals. All accessor properties (`queue`, `first`, `last`, `size`, `isEmpty`) track reactively. +- **`makePriorityQueue`** — non-reactive priority queue. Items are dequeued by comparator order rather than insertion order. +- **`createPriorityQueue`** — reactive priority queue backed by Solid signals. +- **`createTaskQueue`** — reactive queue of async tasks that execute one at a time in FIFO order. Each `enqueue` call returns a Promise. +- **`createConcurrentTaskQueue`** — like `createTaskQueue` but runs up to `concurrency` tasks simultaneously. + +## Installation + +```bash +npm install @solid-primitives/queue +# or +yarn add @solid-primitives/queue +# or +pnpm add @solid-primitives/queue +``` + +## `makeQueue` + +Creates a plain, non-reactive FIFO queue. + +```ts +import { makeQueue } from "@solid-primitives/queue"; + +const q = makeQueue([1, 2, 3]); + +q.first; // 1 +q.last; // 3 +q.size; // 3 +q.isEmpty; // false + +q.add(4, 5); +q.remove(); // 1 +q.first; // 2 + +q.clear(); +q.isEmpty; // true +``` + +### Type + +```ts +type Queue = { + readonly first: T | undefined; + readonly last: T | undefined; + readonly size: number; + readonly isEmpty: boolean; + add: (...items: T[]) => void; + remove: () => T | undefined; + clear: () => void; +}; + +function makeQueue(initialValues?: T[]): Queue; +``` + +## `createQueue` + +Creates a reactive FIFO queue. All accessor properties establish reactive dependencies when read inside a tracking scope (JSX, `createMemo`, `createEffect`, etc.). + +Mutations (`add`, `remove`, `clear`) are batched by Solid's scheduler and applied on the next microtask. In tests, call `flush()` after mutations before reading reactive values. + +```ts +import { createQueue } from "@solid-primitives/queue"; + +const { queue, first, last, size, isEmpty, add, remove, clear } = createQueue(["a", "b", "c"]); + +// Read reactive state +size(); // 3 +first(); // "a" +isEmpty(); // false + +// Mutate +add("d", "e"); +remove(); // "a" — returned synchronously + +// In JSX — updates automatically +{item =>
  • {item}
  • }
    +

    Next: {first()}

    +

    Remaining: {size()}

    +``` + +### Type + +```ts +type ReactiveQueue = { + readonly queue: Accessor; + readonly first: Accessor; + readonly last: Accessor; + readonly size: Accessor; + readonly isEmpty: Accessor; + add: (...items: T[]) => void; + remove: () => T | undefined; + clear: () => void; +}; + +function createQueue(initialValues?: T[]): ReactiveQueue; +``` + +### Notes + +- `remove()` returns the dequeued item **synchronously**, even though the reactive signal update is batched. +- Initial values are **copied** — the source array is never mutated. +- Calling `add` or `remove` inside a Solid reactive computation (memo, effect compute phase) will throw in development. Call mutations from event handlers or effect **apply** phases. + +## `makePriorityQueue` + +Creates a plain, non-reactive priority queue. Items are kept in sorted order; `remove()` always returns the highest-priority item (smallest by the comparator). + +```ts +import { makePriorityQueue } from "@solid-primitives/queue"; + +const q = makePriorityQueue((a, b) => a - b, [3, 1, 2]); + +q.first; // 1 +q.last; // 3 +q.remove(); // 1 +q.first; // 2 + +q.add(0); +q.first; // 0 +``` + +### Type + +```ts +function makePriorityQueue( + comparator: (a: T, b: T) => number, + initialValues?: T[], +): Queue; +``` + +## `createPriorityQueue` + +Creates a reactive priority queue. All accessor properties establish reactive dependencies. Mutations are batched; call `flush()` in tests before reading reactive values. + +```ts +import { createPriorityQueue } from "@solid-primitives/queue"; + +const { queue, first, size, add, remove } = createPriorityQueue( + (a, b) => a.priority - b.priority, + initialItems, +); + +first(); // highest-priority item + +add({ priority: 0, label: "urgent" }); +// In JSX +{item => } +``` + +### Type + +```ts +function createPriorityQueue( + comparator: (a: T, b: T) => number, + initialValues?: T[], +): ReactiveQueue; +``` + +### Notes + +- `remove()` returns the dequeued item **synchronously**, even though the reactive signal update is batched. +- Initial values are **copied** — the source array is never mutated. +- `queue()` returns items in priority order (lowest comparator value first). + +## `createTaskQueue` + +Creates a reactive queue that runs async tasks one at a time in FIFO order. + +Each task is a zero-argument function returning a plain value or a Promise. Tasks execute sequentially: the next task starts only after the current one resolves or rejects. `enqueue` returns a `Promise` that settles with the task's result. + +`size` counts tasks **waiting** (not including the one currently executing). +`active` is `true` while any task is running. + +```ts +import { createTaskQueue } from "@solid-primitives/queue"; + +const { enqueue, size, active } = createTaskQueue(); + +// Each call runs after the previous one finishes +const [alice, bob] = await Promise.all([ + enqueue(() => fetchUser("alice")), + enqueue(() => fetchUser("bob")), +]); + +// In JSX + +

    Processing… ({size()} remaining)

    +
    +``` + +### Type + +```ts +type Task = () => Promise | T; + +type ReactiveTaskQueue = { + /** Number of tasks waiting to start (excludes the task currently executing). */ + readonly size: Accessor; + /** `true` while a task is executing. */ + readonly active: Accessor; + /** Adds a task to the back of the queue; resolves/rejects with its result. */ + enqueue: (task: Task) => Promise; + /** + * Removes all waiting tasks and rejects their Promises with `"Queue cleared"`. + * The currently-executing task (if any) runs to completion unaffected. + */ + clear: () => void; +}; + +function createTaskQueue(): ReactiveTaskQueue; +``` + +### Notes + +- Tasks added while the queue is draining are picked up automatically — `enqueue` never restarts the drain. +- `clear()` does **not** cancel the active task; only unstarted tasks are rejected. +- All tasks share the same return type `T`. For heterogeneous task types use `createTaskQueue()`. + +## `createConcurrentTaskQueue` + +Creates a reactive task queue that runs up to `concurrency` tasks at a time. Tasks beyond the limit wait until a slot opens. + +`size` counts tasks **waiting** (not including those executing). +`active` is the **number** of tasks currently executing (0 when idle). + +```ts +import { createConcurrentTaskQueue } from "@solid-primitives/queue"; + +const { enqueue, active, size } = createConcurrentTaskQueue(3); + +// Up to 3 fetches run at once; the rest wait +urls.forEach(url => enqueue(() => fetch(url))); + +// In JSX + 0}> +

    Fetching… ({active()} active, {size()} waiting)

    +
    +``` + +### Type + +```ts +type ReactiveConcurrentTaskQueue = { + /** Number of tasks waiting to start (excludes tasks currently executing). */ + readonly size: Accessor; + /** Number of tasks currently executing (0 when idle). */ + readonly active: Accessor; + enqueue: (task: Task) => Promise; + clear: () => void; +}; + +function createConcurrentTaskQueue(concurrency: number): ReactiveConcurrentTaskQueue; +``` + +### Notes + +- `active` is a **count** (`Accessor`), unlike `createTaskQueue` where it is a boolean. +- `clear()` rejects all **waiting** tasks; tasks currently executing run to completion. +- For heterogeneous task types use `createConcurrentTaskQueue()`. + +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md) diff --git a/packages/queue/dev/index.tsx b/packages/queue/dev/index.tsx new file mode 100644 index 000000000..4b9854767 --- /dev/null +++ b/packages/queue/dev/index.tsx @@ -0,0 +1,53 @@ +import { type Component, createSignal } from "solid-js"; +import { For } from "@solidjs/web"; +import { createQueue } from "../src/index.js"; + +const App: Component = () => { + const [input, setInput] = createSignal(""); + const { queue, first, size, isEmpty, add, remove, clear } = createQueue(); + + const enqueue = () => { + const val = input().trim(); + if (val) { + add(val); + setInput(""); + } + }; + + return ( +
    +
    +

    Queue Primitive

    +

    FIFO queue — items added to back, removed from front

    +
    + setInput(e.currentTarget.value)} + onKeyDown={e => e.key === "Enter" && enqueue()} + /> + +
    +
    + + +
    +

    + Size: {size()} | Next up: {first() ?? "—"} +

    +
      + {item =>
    • {item}
    • }
      +
    +
    +
    + ); +}; + +export default App; diff --git a/packages/queue/package.json b/packages/queue/package.json new file mode 100644 index 000000000..090ed863a --- /dev/null +++ b/packages/queue/package.json @@ -0,0 +1,70 @@ +{ + "name": "@solid-primitives/queue", + "version": "0.1.0", + "description": "Queue primitives — FIFO, priority, and concurrent task queues", + "author": "David Di Biase ", + "contributors": [], + "license": "MIT", + "homepage": "https://primitives.solidjs.community/package/queue", + "repository": { + "type": "git", + "url": "git+https://github.com/solidjs-community/solid-primitives.git" + }, + "bugs": { + "url": "https://github.com/solidjs-community/solid-primitives/issues" + }, + "primitive": { + "name": "queue", + "stage": 0, + "list": [ + "makeQueue", + "createQueue", + "makePriorityQueue", + "createPriorityQueue", + "createTaskQueue", + "createConcurrentTaskQueue" + ], + "category": "Utilities" + }, + "keywords": [ + "solid", + "primitives", + "queue", + "fifo", + "data-structures", + "reactive" + ], + "private": false, + "sideEffects": false, + "files": [ + "dist" + ], + "type": "module", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "browser": {}, + "exports": { + "import": { + "@solid-primitives/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "typesVersions": {}, + "scripts": { + "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts", + "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts", + "vitest": "vitest -c ../../configs/vitest.config.solid2.ts", + "test": "pnpm run vitest", + "test:ssr": "pnpm run vitest --mode ssr" + }, + "dependencies": { + "@solid-primitives/utils": "workspace:^" + }, + "peerDependencies": { + "solid-js": "^2.0.0-beta.14" + }, + "devDependencies": { + "solid-js": "2.0.0-beta.14" + } +} diff --git a/packages/queue/src/index.ts b/packages/queue/src/index.ts new file mode 100644 index 000000000..b8891ee9f --- /dev/null +++ b/packages/queue/src/index.ts @@ -0,0 +1,3 @@ +export * from "./queue.js"; +export * from "./priority-queue.js"; +export * from "./task-queue.js"; diff --git a/packages/queue/src/priority-queue.ts b/packages/queue/src/priority-queue.ts new file mode 100644 index 000000000..9b1db8f6f --- /dev/null +++ b/packages/queue/src/priority-queue.ts @@ -0,0 +1,53 @@ +import { createQueue, type ReactiveQueue } from "./queue.js"; + +type WithAddPush = { + add: (...items: T[]) => void; + push: (comparator: (a: T, b: T) => number, ...items: T[]) => void; +}; + +/** + * Turns any queue into a priority queue by overriding `add` to call + * `push(comparator, ...)` so every insertion maintains sorted order. + * + * Pass pre-sorted initial values when creating the queue: + * ```ts + * const cmp = (a: number, b: number) => a - b; + * const q = makePriorityQueue(makeQueue([3, 1, 2].sort(cmp)), cmp); + * q.first; // 1 + * q.add(0); + * q.first; // 0 + * ``` + */ +export function makePriorityQueue>( + q: Q, + comparator: (a: T, b: T) => number, +): Q { + q.add = (...items) => q.push(comparator, ...items); + return q; +} + +/** + * Creates a reactive priority queue backed by Solid signals. + * + * All accessor properties (`queue`, `first`, `last`, `size`, `isEmpty`) are + * reactive. Every `add` call maintains the order defined by `comparator`. + * Call `flush()` in tests before reading reactive values. + * + * @param comparator - sort predicate (same contract as `Array.prototype.sort`) + * @param initialValues - optional starting items (copied, not mutated) + * + * @example + * ```ts + * const { first, add, remove } = createPriorityQueue((a, b) => a - b, [3, 1, 2]); + * first(); // 1 + * add(0); + * flush(); + * first(); // 0 + * ``` + */ +export function createPriorityQueue( + comparator: (a: T, b: T) => number, + initialValues: T[] = [], +): ReactiveQueue { + return makePriorityQueue(createQueue([...initialValues].sort(comparator)), comparator); +} diff --git a/packages/queue/src/queue.ts b/packages/queue/src/queue.ts new file mode 100644 index 000000000..c1b6293c5 --- /dev/null +++ b/packages/queue/src/queue.ts @@ -0,0 +1,131 @@ +import { createMemo, createSignal, type Accessor } from "solid-js"; +import { INTERNAL_OPTIONS } from "@solid-primitives/utils"; + +export type Queue = { + readonly first: T | undefined; + readonly last: T | undefined; + readonly size: number; + readonly isEmpty: boolean; + /** Appends items to the back in insertion (FIFO) order. */ + add: (...items: T[]) => void; + /** Inserts items maintaining the order defined by `comparator`. */ + push: (comparator: (a: T, b: T) => number, ...items: T[]) => void; + remove: () => T | undefined; + clear: () => void; +}; + +export type ReactiveQueue = { + readonly queue: Accessor; + readonly first: Accessor; + readonly last: Accessor; + readonly size: Accessor; + readonly isEmpty: Accessor; + /** Appends items to the back in insertion (FIFO) order. */ + add: (...items: T[]) => void; + /** Inserts items maintaining the order defined by `comparator`. */ + push: (comparator: (a: T, b: T) => number, ...items: T[]) => void; + remove: () => T | undefined; + clear: () => void; +}; + +/** + * Creates a plain (non-reactive) FIFO queue. + * + * No Solid lifecycle hooks are used, so cleanup is not automatic. + * Useful for non-reactive contexts or as the base of a larger primitive. + * + * @param initialValues - optional starting items (copied, not mutated) + * @returns a {@link Queue} object with imperative add/remove/clear methods and + * synchronous getter properties + */ +export function makeQueue(initialValues: T[] = []): Queue { + const items = [...initialValues]; + + return { + get first() { + return items[0]; + }, + get last() { + return items[items.length - 1]; + }, + get size() { + return items.length; + }, + get isEmpty() { + return items.length === 0; + }, + add(...newItems: T[]) { + items.push(...newItems); + }, + push(comparator: (a: T, b: T) => number, ...newItems: T[]) { + items.push(...newItems); + items.sort(comparator); + }, + remove() { + return items.shift(); + }, + clear() { + items.length = 0; + }, + }; +} + +/** + * Creates a reactive FIFO queue backed by Solid signals. + * + * All accessor properties (`queue`, `first`, `last`, `size`, `isEmpty`) are + * reactive — reading them inside a tracking scope establishes a dependency. + * Mutations (`add`, `remove`, `clear`) are batched and applied on the next + * microtask flush; call `flush()` in tests to observe values synchronously. + * + * @param initialValues - optional starting items (copied, not mutated) + * @returns a {@link ReactiveQueue} object + * + * @example + * ```ts + * const { queue, first, size, isEmpty, add, remove, clear } = createQueue([1, 2, 3]); + * + * size(); // 3 + * first(); // 1 + * + * add(4, 5); + * remove(); // 1 — removed synchronously; reactive accessors update after flush + * ``` + */ +export function createQueue(initialValues: T[] = []): ReactiveQueue { + const [items, setItems] = createSignal([...initialValues], INTERNAL_OPTIONS); + + const first = createMemo(() => items()[0]); + const last = createMemo(() => { + const arr = items(); + return arr[arr.length - 1]; + }); + const size = createMemo(() => items().length); + const isEmpty = createMemo(() => items().length === 0); + + return { + queue: items, + first, + last, + size, + isEmpty, + add(...newItems: T[]) { + setItems(prev => [...prev, ...newItems]); + }, + push(comparator: (a: T, b: T) => number, ...newItems: T[]) { + setItems(prev => [...prev, ...newItems].sort(comparator)); + }, + remove() { + let removed: T | undefined; + setItems(prev => { + if (prev.length === 0) return prev; + removed = prev[0]!; + return prev.slice(1); + }); + return removed; + }, + clear() { + setItems([]); + }, + }; +} diff --git a/packages/queue/src/task-queue.ts b/packages/queue/src/task-queue.ts new file mode 100644 index 000000000..1048c9223 --- /dev/null +++ b/packages/queue/src/task-queue.ts @@ -0,0 +1,194 @@ +import { createSignal, type Accessor } from "solid-js"; +import { INTERNAL_OPTIONS } from "@solid-primitives/utils"; + +/** + * A unit of async work submitted to a task queue. + * May return a plain value or a Promise. + */ +export type Task = () => Promise | T; + +export type ReactiveTaskQueue = { + /** Number of tasks waiting to start (does not count the task currently executing). */ + readonly size: Accessor; + /** `true` while a task is executing. */ + readonly active: Accessor; + /** + * Adds a task to the back of the queue and returns a Promise that resolves + * (or rejects) with that task's result. + * + * Tasks execute one at a time in the order they were enqueued. + * Calling `enqueue` while a task is running will not start a second drain — + * the new task is picked up automatically by the in-progress drain loop. + */ + enqueue: (task: Task) => Promise; + /** + * Removes all waiting tasks from the queue and rejects their Promises. + * The currently-executing task (if any) runs to completion unaffected. + */ + clear: () => void; +}; + +export type ReactiveConcurrentTaskQueue = { + /** Number of tasks waiting to start (excludes tasks currently executing). */ + readonly size: Accessor; + /** Number of tasks currently executing (0 when idle). */ + readonly active: Accessor; + /** + * Adds a task to the back of the queue; resolves/rejects with its result. + * Up to `concurrency` tasks run simultaneously; additional tasks wait until + * a running slot opens. + */ + enqueue: (task: Task) => Promise; + /** + * Removes all waiting tasks and rejects their Promises with `"Queue cleared"`. + * Tasks currently executing run to completion unaffected. + */ + clear: () => void; +}; + +type TaskEntry = { + fn: Task; + resolve: (value: T) => void; + reject: (reason: unknown) => void; +}; + +/** + * Creates a reactive task queue that executes async tasks one at a time in + * FIFO order. + * + * Each task is a zero-argument function returning a value or Promise. Tasks + * are drained sequentially: the next task starts only after the current one + * resolves or rejects. Signal writes within the drain loop are batched + * atomically per task step. + * + * `size` counts tasks *waiting* (not including the one currently executing). + * `active` is `true` while any task is running. + * + * @example + * ```ts + * const { enqueue, size, active } = createTaskQueue(); + * + * const user = await enqueue(() => fetchUser(id)); + * + * // Enqueue several — they run serially, not concurrently + * const [a, b] = await Promise.all([ + * enqueue(() => fetchUser(1)), + * enqueue(() => fetchUser(2)), + * ]); + * ``` + */ +export function createTaskQueue(): ReactiveTaskQueue { + // Plain array for synchronous access inside the drain loop — avoids the need + // to read a signal and deal with batching when deciding what to execute next. + const tasks: TaskEntry[] = []; + const [size, setSize] = createSignal(0, INTERNAL_OPTIONS); + const [isActive, setIsActive] = createSignal(false, INTERNAL_OPTIONS); + let draining = false; + + // Runs tasks one at a time. A plain async function (not Solid's `action`) + // so signal writes are committed on the normal microtask schedule and are + // immediately visible via `flush()` in tests — the `action` transition system + // would hold all writes until each yield resolves, making live state unobservable. + const drain = async (): Promise => { + while (tasks.length > 0) { + const entry = tasks.shift()!; + setSize(tasks.length); + try { + entry.resolve((await entry.fn()) as T); + } catch (err) { + entry.reject(err); + } + } + setIsActive(false); + draining = false; + }; + + return { + size, + active: isActive, + enqueue(task) { + return new Promise((resolve, reject) => { + tasks.push({ fn: task, resolve, reject }); + setSize(tasks.length); + if (!draining) { + draining = true; + setIsActive(true); + drain(); + } + }); + }, + clear() { + const removed = tasks.splice(0); + setSize(0); + for (const entry of removed) { + entry.reject(new Error("Queue cleared")); + } + }, + }; +} + +/** + * Creates a reactive task queue that executes up to `concurrency` tasks at a + * time. Tasks beyond the limit queue and start as slots open. + * + * `size` counts tasks *waiting* (not including those executing). + * `active` is the number of tasks currently executing (0 when idle). + * + * @param concurrency - maximum number of tasks to run simultaneously + * + * @example + * ```ts + * const { enqueue, active, size } = createConcurrentTaskQueue(3); + * + * urls.forEach(url => enqueue(() => fetch(url))); + * + * // In JSX + * 0}>Fetching ({active()} active, {size()} waiting) + * ``` + */ +export function createConcurrentTaskQueue(concurrency: number): ReactiveConcurrentTaskQueue { + const pending: TaskEntry[] = []; + const [size, setSize] = createSignal(0, INTERNAL_OPTIONS); + const [activeCount, setActiveCount] = createSignal(0, INTERNAL_OPTIONS); + let running = 0; + + const runNext = () => { + while (running < concurrency && pending.length > 0) { + const entry = pending.shift()!; + running++; + setSize(pending.length); + setActiveCount(running); + + (async () => { + try { + entry.resolve((await entry.fn()) as T); + } catch (err) { + entry.reject(err); + } finally { + running--; + setActiveCount(running); + runNext(); + } + })(); + } + }; + + return { + size, + active: activeCount, + enqueue(task) { + return new Promise((resolve, reject) => { + pending.push({ fn: task, resolve, reject }); + setSize(pending.length); + runNext(); + }); + }, + clear() { + const removed = pending.splice(0); + setSize(0); + for (const entry of removed) { + entry.reject(new Error("Queue cleared")); + } + }, + }; +} diff --git a/packages/queue/test/index.test.ts b/packages/queue/test/index.test.ts new file mode 100644 index 000000000..6f61ece83 --- /dev/null +++ b/packages/queue/test/index.test.ts @@ -0,0 +1,770 @@ +import { describe, test, expect } from "vitest"; +import { createRoot, createEffect, flush } from "solid-js"; +import { + makeQueue, + createQueue, + createTaskQueue, + makePriorityQueue, + createPriorityQueue, + createConcurrentTaskQueue, +} from "../src/index.js"; + +describe("makeQueue", () => { + test("creates an empty queue", () => { + const q = makeQueue(); + expect(q.size).toBe(0); + expect(q.isEmpty).toBe(true); + expect(q.first).toBeUndefined(); + expect(q.last).toBeUndefined(); + }); + + test("creates a queue with initial values", () => { + const q = makeQueue([1, 2, 3]); + expect(q.size).toBe(3); + expect(q.isEmpty).toBe(false); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + }); + + test("does not mutate the initial values array", () => { + const initial = [1, 2, 3]; + const q = makeQueue(initial); + q.add(4); + expect(initial).toHaveLength(3); + }); + + test("add appends to the back", () => { + const q = makeQueue([1, 2]); + q.add(3); + expect(q.size).toBe(3); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + }); + + test("add accepts multiple items at once", () => { + const q = makeQueue(); + q.add(1, 2, 3); + expect(q.size).toBe(3); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + }); + + test("remove returns and removes the front item", () => { + const q = makeQueue([1, 2, 3]); + const removed = q.remove(); + expect(removed).toBe(1); + expect(q.size).toBe(2); + expect(q.first).toBe(2); + }); + + test("remove on an empty queue returns undefined", () => { + const q = makeQueue(); + expect(q.remove()).toBeUndefined(); + expect(q.size).toBe(0); + }); + + test("clear empties the queue", () => { + const q = makeQueue([1, 2, 3]); + q.clear(); + expect(q.size).toBe(0); + expect(q.isEmpty).toBe(true); + expect(q.first).toBeUndefined(); + expect(q.last).toBeUndefined(); + }); + + test("clear on an already-empty queue is a no-op", () => { + const q = makeQueue(); + q.clear(); + expect(q.size).toBe(0); + }); + + test("last reflects the back of the queue", () => { + const q = makeQueue([10]); + expect(q.last).toBe(10); + q.add(20); + expect(q.last).toBe(20); + q.remove(); + expect(q.last).toBe(20); + }); + + test("push inserts items in comparator order", () => { + const asc = (a: number, b: number) => a - b; + const q = makeQueue(); + q.push(asc, 3, 1, 2); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + expect(q.size).toBe(3); + }); + + test("push and add can be used on the same queue", () => { + const asc = (a: number, b: number) => a - b; + const q = makeQueue([10]); + q.push(asc, 5, 15); // inserts sorted: [5, 10, 15] + expect(q.first).toBe(5); + q.add(1); // appends to back: [5, 10, 15, 1] + expect(q.last).toBe(1); + }); +}); + +describe("createQueue", () => { + test("creates an empty reactive queue", () => { + createRoot(dispose => { + const q = createQueue(); + expect(q.size()).toBe(0); + expect(q.isEmpty()).toBe(true); + expect(q.first()).toBeUndefined(); + expect(q.last()).toBeUndefined(); + expect(q.queue()).toEqual([]); + dispose(); + }); + }); + + test("creates a queue with initial values", () => { + createRoot(dispose => { + const q = createQueue([1, 2, 3]); + expect(q.size()).toBe(3); + expect(q.isEmpty()).toBe(false); + expect(q.first()).toBe(1); + expect(q.last()).toBe(3); + expect(q.queue()).toEqual([1, 2, 3]); + dispose(); + }); + }); + + test("does not mutate the initial values array", () => { + createRoot(dispose => { + const initial = [1, 2, 3]; + const q = createQueue(initial); + q.add(4); + flush(); + expect(initial).toHaveLength(3); + dispose(); + }); + }); + + test("add appends items and updates reactive accessors", () => { + createRoot(dispose => { + const q = createQueue(); + q.add(1, 2, 3); + flush(); + expect(q.size()).toBe(3); + expect(q.first()).toBe(1); + expect(q.last()).toBe(3); + expect(q.isEmpty()).toBe(false); + expect(q.queue()).toEqual([1, 2, 3]); + dispose(); + }); + }); + + test("remove returns the front item synchronously", () => { + createRoot(dispose => { + const q = createQueue([1, 2, 3]); + const removed = q.remove(); + expect(removed).toBe(1); + dispose(); + }); + }); + + test("remove updates reactive accessors after flush", () => { + createRoot(dispose => { + const q = createQueue([1, 2, 3]); + q.remove(); + flush(); + expect(q.size()).toBe(2); + expect(q.first()).toBe(2); + expect(q.queue()).toEqual([2, 3]); + dispose(); + }); + }); + + test("remove on an empty queue returns undefined and is a no-op", () => { + createRoot(dispose => { + const q = createQueue(); + const removed = q.remove(); + flush(); + expect(removed).toBeUndefined(); + expect(q.size()).toBe(0); + dispose(); + }); + }); + + test("clear empties the queue", () => { + createRoot(dispose => { + const q = createQueue([1, 2, 3]); + q.clear(); + flush(); + expect(q.size()).toBe(0); + expect(q.isEmpty()).toBe(true); + expect(q.first()).toBeUndefined(); + expect(q.last()).toBeUndefined(); + expect(q.queue()).toEqual([]); + dispose(); + }); + }); + + test("multiple mutations compose correctly", () => { + createRoot(dispose => { + const q = createQueue(); + q.add(1, 2, 3); + q.remove(); + flush(); + expect(q.queue()).toEqual([2, 3]); + expect(q.first()).toBe(2); + expect(q.size()).toBe(2); + dispose(); + }); + }); + + test("last accessor reflects the back of the queue", () => { + createRoot(dispose => { + const q = createQueue([10]); + expect(q.last()).toBe(10); + q.add(20); + flush(); + expect(q.last()).toBe(20); + q.remove(); + flush(); + expect(q.last()).toBe(20); + dispose(); + }); + }); + + test("reactive effects track size changes", () => { + createRoot(dispose => { + const q = createQueue(); + const sizes: number[] = []; + + createEffect( + () => q.size(), + size => { + sizes.push(size); + }, + ); + + flush(); + expect(sizes).toEqual([0]); + + q.add(1); + flush(); + expect(sizes).toEqual([0, 1]); + + q.add(2, 3); + flush(); + expect(sizes).toEqual([0, 1, 3]); + + q.remove(); + flush(); + expect(sizes).toEqual([0, 1, 3, 2]); + + q.clear(); + flush(); + expect(sizes).toEqual([0, 1, 3, 2, 0]); + + dispose(); + }); + }); + + test("reactive effects track first changes", () => { + createRoot(dispose => { + const q = createQueue([1, 2, 3]); + const firsts: (number | undefined)[] = []; + + createEffect( + () => q.first(), + first => { + firsts.push(first); + }, + ); + + flush(); + expect(firsts).toEqual([1]); + + q.remove(); + flush(); + expect(firsts).toEqual([1, 2]); + + q.clear(); + flush(); + expect(firsts).toEqual([1, 2, undefined]); + + dispose(); + }); + }); + + test("isEmpty transitions correctly", () => { + createRoot(dispose => { + const q = createQueue(); + expect(q.isEmpty()).toBe(true); + + q.add("a"); + flush(); + expect(q.isEmpty()).toBe(false); + + q.remove(); + flush(); + expect(q.isEmpty()).toBe(true); + + dispose(); + }); + }); + + test("push inserts items in comparator order", () => { + createRoot(dispose => { + const asc = (a: number, b: number) => a - b; + const q = createQueue(); + q.push(asc, 3, 1, 2); + flush(); + expect(q.queue()).toEqual([1, 2, 3]); + expect(q.first()).toBe(1); + dispose(); + }); + }); +}); + +describe("createTaskQueue", () => { + test("enqueue returns a Promise resolving with the task's return value", async () => { + const q = createTaskQueue(); + const result = await q.enqueue(async () => 42); + expect(result).toBe(42); + }); + + test("supports synchronous tasks", async () => { + const q = createTaskQueue(); + const result = await q.enqueue(() => "hello"); + expect(result).toBe("hello"); + }); + + test("processes tasks sequentially, not concurrently", async () => { + const q = createTaskQueue(); + const log: number[] = []; + + let startTask2!: () => void; + const blockingTask = new Promise(r => { + startTask2 = () => r(1); + }); + + const p1 = q.enqueue(() => blockingTask.then(v => { log.push(1); return v; })); + const p2 = q.enqueue(async () => { log.push(2); return 2; }); + + // task2 must not start until task1 finishes + expect(log).toEqual([]); + + startTask2(); + const [r1, r2] = await Promise.all([p1, p2]); + + expect(log).toEqual([1, 2]); + expect(r1).toBe(1); + expect(r2).toBe(2); + }); + + test("size reflects pending task count (excludes the running task)", async () => { + const q = createTaskQueue(); + + let releaseTask1!: () => void; + const p1 = q.enqueue(() => new Promise(r => { releaseTask1 = r; })); + q.enqueue(async () => {}); + q.enqueue(async () => {}); + + // task1 running (shifted off), task2 + task3 pending + flush(); + expect(q.size()).toBe(2); + expect(q.active()).toBe(true); + + releaseTask1(); + await p1; + flush(); + expect(q.size()).toBe(1); + + await q.enqueue(async () => {}); // drains task2 and task3 + flush(); + expect(q.size()).toBe(0); + expect(q.active()).toBe(false); + }); + + test("active becomes true immediately and false after queue drains", async () => { + const q = createTaskQueue(); + expect(q.active()).toBe(false); + + let release!: () => void; + const p = q.enqueue(() => new Promise(r => { release = r; })); + flush(); + expect(q.active()).toBe(true); + + release(); + await p; + flush(); + expect(q.active()).toBe(false); + }); + + test("task errors reject that task's Promise without stopping the queue", async () => { + const q = createTaskQueue(); + + await expect( + q.enqueue(async () => { throw new Error("boom"); }), + ).rejects.toThrow("boom"); + + // Queue should still be functional + const result = await q.enqueue(async () => 99); + expect(result).toBe(99); + }); + + test("clear rejects pending tasks while the active task continues", async () => { + const q = createTaskQueue(); + + let release!: () => void; + const p1 = q.enqueue(() => new Promise(r => { release = () => r(1); })); + const p2 = q.enqueue(async () => 2); + const p3 = q.enqueue(async () => 3); + + q.clear(); + flush(); + expect(q.size()).toBe(0); + + // waiting tasks are rejected + await expect(p2).rejects.toThrow("Queue cleared"); + await expect(p3).rejects.toThrow("Queue cleared"); + + // active task still resolves + release(); + expect(await p1).toBe(1); + }); + + test("new tasks enqueued after clear are processed normally", async () => { + const q = createTaskQueue(); + + let release!: () => void; + q.enqueue(() => new Promise(r => { release = () => r(0); })); + const toBeCleared = q.enqueue(async () => 1); + + q.clear(); + await expect(toBeCleared).rejects.toThrow("Queue cleared"); + release(); + + const result = await q.enqueue(async () => 42); + expect(result).toBe(42); + }); + + test("tasks added while draining are picked up without restarting the drain", async () => { + const q = createTaskQueue(); + const results: number[] = []; + + const p1 = q.enqueue(async () => { results.push(1); return 1; }); + // Enqueue task2 before task1 resolves — drain is already running + const p2 = q.enqueue(async () => { results.push(2); return 2; }); + + await Promise.all([p1, p2]); + expect(results).toEqual([1, 2]); + }); +}); + +describe("makePriorityQueue", () => { + const asc = (a: number, b: number) => a - b; + + test("turns an empty queue into a priority queue", () => { + const q = makePriorityQueue(makeQueue(), asc); + expect(q.size).toBe(0); + expect(q.isEmpty).toBe(true); + expect(q.first).toBeUndefined(); + expect(q.last).toBeUndefined(); + }); + + test("preserves pre-sorted initial values", () => { + const q = makePriorityQueue(makeQueue([1, 2, 3]), asc); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + expect(q.size).toBe(3); + expect(q.isEmpty).toBe(false); + }); + + test("does not mutate the initial values array", () => { + const initial = [3, 1, 2]; + makePriorityQueue(makeQueue([...initial].sort(asc)), asc); + expect(initial).toEqual([3, 1, 2]); + }); + + test("add inserts items in priority order", () => { + const q = makePriorityQueue(makeQueue(), asc); + q.add(3, 1, 2); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + expect(q.size).toBe(3); + }); + + test("remove returns and removes the highest-priority item", () => { + const q = makePriorityQueue(makeQueue([1, 2, 3]), asc); + expect(q.remove()).toBe(1); + expect(q.first).toBe(2); + expect(q.size).toBe(2); + }); + + test("remove on an empty queue returns undefined", () => { + const q = makePriorityQueue(makeQueue(), asc); + expect(q.remove()).toBeUndefined(); + }); + + test("clear empties the queue", () => { + const q = makePriorityQueue(makeQueue([1, 2, 3]), asc); + q.clear(); + expect(q.size).toBe(0); + expect(q.isEmpty).toBe(true); + expect(q.first).toBeUndefined(); + }); + + test("works with a descending comparator", () => { + const desc = (a: number, b: number) => b - a; + const q = makePriorityQueue(makeQueue([3, 2, 1]), desc); + expect(q.first).toBe(3); + expect(q.remove()).toBe(3); + expect(q.first).toBe(2); + }); + + test("maintains priority order across multiple add/remove cycles", () => { + const q = makePriorityQueue(makeQueue(), asc); + q.add(5, 3); + expect(q.remove()).toBe(3); + q.add(1, 4); + expect(q.first).toBe(1); + expect(q.last).toBe(5); + }); + + test("works on a reactive queue too", () => { + createRoot(dispose => { + const q = makePriorityQueue(createQueue(), asc); + q.add(3, 1, 2); + flush(); + expect(q.first()).toBe(1); + expect(q.last()).toBe(3); + dispose(); + }); + }); +}); + +describe("createPriorityQueue", () => { + const asc = (a: number, b: number) => a - b; + + test("creates an empty reactive priority queue", () => { + createRoot(dispose => { + const q = createPriorityQueue(asc); + expect(q.size()).toBe(0); + expect(q.isEmpty()).toBe(true); + expect(q.first()).toBeUndefined(); + expect(q.queue()).toEqual([]); + dispose(); + }); + }); + + test("creates with initial values sorted by comparator", () => { + createRoot(dispose => { + const q = createPriorityQueue(asc, [3, 1, 2]); + expect(q.queue()).toEqual([1, 2, 3]); + expect(q.first()).toBe(1); + expect(q.last()).toBe(3); + dispose(); + }); + }); + + test("does not mutate the initial values array", () => { + createRoot(dispose => { + const initial = [3, 1, 2]; + createPriorityQueue(asc, initial); + expect(initial).toEqual([3, 1, 2]); + dispose(); + }); + }); + + test("add inserts in priority order and updates reactive accessors", () => { + createRoot(dispose => { + const q = createPriorityQueue(asc); + q.add(3, 1, 2); + flush(); + expect(q.queue()).toEqual([1, 2, 3]); + expect(q.first()).toBe(1); + expect(q.last()).toBe(3); + dispose(); + }); + }); + + test("remove returns the highest-priority item synchronously", () => { + createRoot(dispose => { + const q = createPriorityQueue(asc, [3, 1, 2]); + expect(q.remove()).toBe(1); + dispose(); + }); + }); + + test("remove updates reactive accessors after flush", () => { + createRoot(dispose => { + const q = createPriorityQueue(asc, [3, 1, 2]); + q.remove(); + flush(); + expect(q.queue()).toEqual([2, 3]); + expect(q.first()).toBe(2); + expect(q.size()).toBe(2); + dispose(); + }); + }); + + test("reactive effects track first changes after priority reordering", () => { + createRoot(dispose => { + const q = createPriorityQueue(asc, [5, 3]); + const firsts: (number | undefined)[] = []; + + createEffect( + () => q.first(), + first => { firsts.push(first); }, + ); + + flush(); + expect(firsts).toEqual([3]); + + q.add(1); // becomes new first + flush(); + expect(firsts).toEqual([3, 1]); + + q.remove(); + flush(); + expect(firsts).toEqual([3, 1, 3]); + + dispose(); + }); + }); + + test("works with a descending comparator", () => { + createRoot(dispose => { + const desc = (a: number, b: number) => b - a; + const q = createPriorityQueue(desc, [1, 2, 3]); + expect(q.first()).toBe(3); + q.remove(); + flush(); + expect(q.first()).toBe(2); + dispose(); + }); + }); +}); + +describe("createConcurrentTaskQueue", () => { + test("resolves with the task's return value", async () => { + const q = createConcurrentTaskQueue(2); + expect(await q.enqueue(async () => 42)).toBe(42); + }); + + test("supports synchronous tasks", async () => { + const q = createConcurrentTaskQueue(2); + expect(await q.enqueue(() => "hello")).toBe("hello"); + }); + + test("runs up to concurrency tasks simultaneously", async () => { + const q = createConcurrentTaskQueue(2); + + let releaseA!: () => void; + let releaseB!: () => void; + let releaseC!: () => void; + + const pA = q.enqueue(() => new Promise(r => { releaseA = () => r(1); })); + const pB = q.enqueue(() => new Promise(r => { releaseB = () => r(2); })); + const pC = q.enqueue(() => new Promise(r => { releaseC = () => r(3); })); + + flush(); + expect(q.active()).toBe(2); // A and B running + expect(q.size()).toBe(1); // C pending + + releaseA(); + await pA; + flush(); + expect(q.active()).toBe(2); // B still running, C picked up slot + expect(q.size()).toBe(0); + + releaseB(); + releaseC(); + await Promise.all([pB, pC]); + flush(); + expect(q.active()).toBe(0); + }); + + test("active reflects number of running tasks", async () => { + const q = createConcurrentTaskQueue(3); + + let r1!: () => void, r2!: () => void, r3!: () => void; + const p1 = q.enqueue(() => new Promise(r => { r1 = r; })); + const p2 = q.enqueue(() => new Promise(r => { r2 = r; })); + const p3 = q.enqueue(() => new Promise(r => { r3 = r; })); + + flush(); + expect(q.active()).toBe(3); + expect(q.size()).toBe(0); + + r1(); + await p1; + flush(); + expect(q.active()).toBe(2); + + r2(); r3(); + await Promise.all([p2, p3]); + flush(); + expect(q.active()).toBe(0); + }); + + test("size reflects pending count (excludes running tasks)", async () => { + const q = createConcurrentTaskQueue(1); + + let release!: () => void; + const p1 = q.enqueue(() => new Promise(r => { release = r; })); + q.enqueue(async () => {}); + q.enqueue(async () => {}); + + flush(); + expect(q.active()).toBe(1); + expect(q.size()).toBe(2); + + release(); + await p1; + // task2 and task3 complete on their own — wait for queue to drain + await q.enqueue(async () => {}); + flush(); + expect(q.active()).toBe(0); + expect(q.size()).toBe(0); + }); + + test("task error rejects that Promise without stopping the queue", async () => { + const q = createConcurrentTaskQueue(2); + + await expect( + q.enqueue(async () => { throw new Error("boom"); }), + ).rejects.toThrow("boom"); + + expect(await q.enqueue(async () => 99)).toBe(99); + }); + + test("clear rejects pending tasks while running tasks complete", async () => { + const q = createConcurrentTaskQueue(1); + + let release!: () => void; + const p1 = q.enqueue(() => new Promise(r => { release = () => r(1); })); + const p2 = q.enqueue(async () => 2); + const p3 = q.enqueue(async () => 3); + + q.clear(); + flush(); + expect(q.size()).toBe(0); + + await expect(p2).rejects.toThrow("Queue cleared"); + await expect(p3).rejects.toThrow("Queue cleared"); + + release(); + expect(await p1).toBe(1); + }); + + test("new tasks enqueued after clear are processed normally", async () => { + const q = createConcurrentTaskQueue(1); + + let release!: () => void; + q.enqueue(() => new Promise(r => { release = () => r(0); })); + const toBeCleared = q.enqueue(async () => 1); + + q.clear(); + await expect(toBeCleared).rejects.toThrow("Queue cleared"); + release(); + + expect(await q.enqueue(async () => 42)).toBe(42); + }); +}); diff --git a/packages/queue/test/server.test.ts b/packages/queue/test/server.test.ts new file mode 100644 index 000000000..10d9138d9 --- /dev/null +++ b/packages/queue/test/server.test.ts @@ -0,0 +1,50 @@ +import { describe, test, expect } from "vitest"; +import { makeQueue, createQueue, createTaskQueue } from "../src/index.js"; + +describe("makeQueue — SSR", () => { + test("works without a browser environment", () => { + const q = makeQueue([1, 2, 3]); + expect(q.size).toBe(3); + expect(q.first).toBe(1); + expect(q.last).toBe(3); + + q.add(4); + expect(q.size).toBe(4); + + const removed = q.remove(); + expect(removed).toBe(1); + expect(q.size).toBe(3); + + q.clear(); + expect(q.isEmpty).toBe(true); + }); +}); + +describe("createQueue — SSR", () => { + test("works without a browser environment", () => { + const q = createQueue([1, 2, 3]); + expect(q.size()).toBe(3); + expect(q.first()).toBe(1); + expect(q.last()).toBe(3); + expect(q.isEmpty()).toBe(false); + }); + + test("initial empty queue is stable", () => { + const q = createQueue(); + expect(q.size()).toBe(0); + expect(q.isEmpty()).toBe(true); + expect(q.first()).toBeUndefined(); + expect(q.last()).toBeUndefined(); + }); +}); + +describe("createTaskQueue — SSR", () => { + test("enqueues and resolves tasks without a browser environment", async () => { + const q = createTaskQueue(); + expect(q.size()).toBe(0); + expect(q.active()).toBe(false); + + const result = await q.enqueue(async () => 7); + expect(result).toBe(7); + }); +}); diff --git a/packages/queue/tsconfig.json b/packages/queue/tsconfig.json new file mode 100644 index 000000000..dc1970e16 --- /dev/null +++ b/packages/queue/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "dist", + "rootDir": "src" + }, + "references": [ + { + "path": "../utils" + } + ], + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee1923c5f..12dfc4a34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -795,6 +795,16 @@ importers: specifier: ^1.9.7 version: 1.9.7 + packages/queue: + dependencies: + '@solid-primitives/utils': + specifier: workspace:^ + version: link:../utils + devDependencies: + solid-js: + specifier: 2.0.0-beta.14 + version: 2.0.0-beta.14 + packages/raf: dependencies: '@solid-primitives/utils':