Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# `queue/queue.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`AsyncQueue` is a minimal FIFO asynchronous queue that serializes
operations on a single tail promise. The `ClipboardManager` uses it to
ensure concurrent `copyText` / `readText` / `copyRichContent` /
`readRichContent` calls execute in arrival order — without it, the
trailing call would win non-deterministically because the browser's
Clipboard API does not provide ordering guarantees across overlapping
requests. The queue is intentionally tiny (one promise reference plus a
drained flag) so it adds no measurable overhead in the common single-call
case.
## Features
- FIFO ordering — callbacks run strictly in arrival order.
- Rejection-tolerant — a rejected operation does not stall later ones.
- `drain()` closes the queue and resolves once the tail settles.
- `reset()` re-opens a drained queue (useful for tests + dispose flows).
- Zero dependencies; no `Promise.race`, no timers, no microtask leaks.
## Usage: Basic
```ts
import { AsyncQueue } from "@suaveplan/clipboard";
const queue = new AsyncQueue();
const results = await Promise.all([
queue.enqueue(async () => "a"),
queue.enqueue(async () => "b"),
queue.enqueue(async () => "c"),
]);
// results === ["a", "b", "c"]
```
## Usage: Advanced
```ts
import { AsyncQueue } from "@suaveplan/clipboard";
const queue = new AsyncQueue();
// Rejection does not break the queue.
queue.enqueue(async () => {
throw new Error("boom");
}).catch(() => undefined);
const next = await queue.enqueue(async () => "still works");
// Close the queue when shutting down.
await queue.drain();
// Re-open later for a new session.
queue.reset();
await queue.enqueue(async () => "round two");
```
## API Reference
### `new AsyncQueue()`
Constructs a queue with an already-resolved tail.
### `queue.enqueue<T>(fn: () => Promise<T>): Promise<T>`
Append `fn` to the queue. The returned promise settles with `fn`'s
outcome once every previously enqueued callback has settled. If the
queue has been drained, returns a rejected promise carrying `"AsyncQueue:
enqueue after drain"`.
### `queue.drain(): Promise<void>`
Closes the queue. Subsequent `enqueue` calls reject. Returns a promise
that resolves once the current tail settles (success or failure).
### `queue.reset(): void`
Re-opens a drained queue. Replaces the tail with a fresh
`Promise.resolve()`.
## Implementation Notes
The tail promise chains through `then(onFulfilled, onRejected)` with both
arms re-invoking `fn`. This means an operation always runs after the
previous one regardless of how the previous resolved — the rejection does
not propagate into the queue's tracked tail (we explicitly catch via
`.then(() => undefined, () => undefined)`). Because the tail is `unknown`-
typed internally, the generic `T` parameter on `enqueue` is preserved end
to end. The drained flag is checked synchronously inside `enqueue` so
post-drain calls reject without scheduling work. The queue is private to
the manager but exported so advanced consumers can reuse it for
caller-defined ordering (e.g. when composing with other browser APIs).