# PowerQueue

A resizable ring-buffer queue with O(1) enqueue/dequeue. Useful as a high-performance replacement for `Array#push`/`Array#shift` when building queues under sustained load.

## Constructor

`new PowerQueue(initialCapacity?)`

| option            |     type | default | description                                                                                       |
| ----------------- | -------: | ------: | ------------------------------------------------------------------------------------------------- |
| `initialCapacity` | `number` |    `16` | Initial capacity (will be rounded up to a power-of-two). The queue automatically grows when full. |

## API

- `push(item)` — Enqueue `item` at the tail. Returns the new queue length as a `number`.

- `unshift(item)` — **New in 2.1.0.** Enqueue `item` at the **head**, so it is the next
  value `shift()` returns. The mirror of `push`, and the primitive a return-to-front
  path needs: work that was dequeued for dispatch but could not be delivered goes back
  where it came from rather than behind everything that arrived after it. O(1) —
  moving a ring buffer's head back one slot leaves every existing item on its same
  physical slot, so nothing is copied. See [Returning work to the front](#returning-work-to-the-front).

- `shift()` — Dequeue and return the head item, or `undefined` when the queue is empty.

- `peek()` — Inspect the head item without removing it; returns `any` or `undefined`.

- `clear()` — Remove all items from the queue immediately; useful for shutdown or resetting state.

- `length` (getter) — Current number of items in the queue (`number`).

- `totalWeight` (getter) — **New in 2.0.** Sum of the queued items' weights, where an item's weight is its numeric `weight` property or `1` when it has none (`number`). With weight-less items this is identical to `length`. Maintained incrementally on `push`, `shift`, `fill`, `pushMany`, `unshiftMany` and `clear`, so reading it costs nothing. See [Weighted queues](#weighted-queues) below.

- `capacity` (getter) — Internal buffer capacity (power-of-two sized) used by the ring buffer (`number`).

- `isEmpty` (getter) — `true` when the queue contains no items.

- `pushMany(items)` — Enqueue multiple items in one call. The implementation grows the backing buffer at most once and copies items efficiently; returns the new queue length.

- `fill(item, count)` — Enqueue `count` copies of `item`. Equivalent to
  `pushMany(Array(count).fill(item))` without the temporary array, for the case
  where every value is the same.

- `shrink(minimum = 16)` — Give back the memory a burst grew, and return the
  capacity afterwards.

- `dispose()` — **New in 2.0.** Release the backing buffer and drop every
  reference the queue holds. Also exposed as `[Symbol.dispose]` and
  `[Symbol.asyncDispose]`, so a queue takes part in `using` / `await using` and a
  DI container's teardown like every other long-lived helper here.

  ```js
  {
    using q = new PowerQueue(16);
    q.push(work);
    // q.dispose() runs here, releasing the ring
  }
  ```

  **Why this is separate from `clear()`.** `clear()` empties the slots but
  deliberately keeps the array, which is right for a container whose purpose is
  bounding memory — a queue that drains and refills should not reallocate on
  every cycle. The ring only ever _grows_, though, so a queue that took 5 000
  items once keeps an 8 192-slot buffer for the rest of its life. `dispose()` is
  the one moment the caller is finished with the queue, and that is where the
  array goes.

  The queue is still usable afterwards — `dispose()` is a teardown, not a
  poisoning — and calling it twice is a no-op. If you want a smaller _live_
  queue rather than a released one, that is `shrink()`.

### Returning work to the front

**New in 2.1.0.** `unshift(item)` puts an item back at the head, so it is the next
thing `shift()` returns. The case it exists for is work that was already dequeued and
then could not be delivered:

```javascript
const task = queue.shift();
try {
  worker.postMessage(task);
} catch {
  // The worker died or the post threw. Put it back where it came from, not at the
  // back — at the back it would be reordered behind everything that arrived while
  // it was in flight, which is a silent reordering of a caller's work.
  queue.unshift(task);
}
```

**It is O(1), and the reason is worth stating because the instinct is the opposite.**
Inserting at the head of a ring buffer does not shift the existing items. Moving the
head back by one slot leaves every item on its same physical slot: an item at logical
index `i` sits at physical `head + i`, and its new logical index `i + 1` sits at
physical `(head - 1) + (i + 1)` — the same place. So the only work is decrementing the
head and writing one slot.

`unshift` and `shift` are exact inverses, so a dequeue-then-restore round-trip leaves
the queue byte-for-byte as it was. That is the property the return-to-front path
relies on, and it is pinned by a test.

For **bulk** prepends use `unshiftMany(items)`, which is the amortised-cheap path for
putting a whole batch back.

### Weighted queues

**New in 2.0.** `PowerQueue` bounds **items**; `totalWeight` reports the sum of
the items' weights so a caller can reason about the work queued rather than the
number of envelopes.

```javascript
const queue = new PowerQueue();
queue.push({ id: 'a', weight: 5 });
queue.push({ id: 'b' }); // no `weight`, counts 1
queue.totalWeight; // 6, while queue.length is 2
```

Three things to know:

- **A non-numeric, non-finite or absent `weight` counts as `1`.** So
  `{weight: 0}`, `{weight: -3}`, `{weight: NaN}` and `{weight: '5'}` are all `1`.
  This is a **queue**, not a gate: nothing is granted here, so there is nothing to
  reject and no caller waiting on a number. A weight the queue cannot honour is
  counted as one item rather than dropped or throwing — see
  [Weighted permits](powerPermitGate.md#weighted-permits-weight) if you want the
  refusing behaviour, which is what a gate needs because a waiter that can never
  be granted would hang.
- **It is maintained incrementally**, not recomputed by a walk. `totalWeight` is
  therefore O(1) and safe to read in a hot loop or a monitor tick.
- **`length` is unchanged and still counts items.** The two are equal for
  weight-less items, so existing code is unaffected.

### Reclaiming a burst

The buffer only ever grows, so a queue that held 5 000 items once keeps an
8 192-slot buffer for the rest of its life. `clear()` empties the slots but does
not release them, which is right for a container whose purpose is bounding
memory — so shrinking is **explicit**:

```javascript
queue.push(...burstOfFiveThousand);
await drain(queue);

// Still 8192 here. This is the line that gives it back.
queue.shrink(); // => 16
```

`minimum` is a floor for a queue expected to refill to a known size, so steady
traffic does not reallocate every cycle; it is rounded up to a power of two, and
is never below the current length, so nothing is ever dropped.

`PowerSlidingWindow` uses this on your behalf: its timestamps live in a
`PowerQueue`, and it shrinks when a window ages out. `shrink()` is not automatic
inside `shift()` on purpose — that would put a comparison and a branch on every
dequeue forever to reclaim memory only after a burst, and a caller that has just
finished a burst is the one that knows when to pay.

- `unshiftMany(items)` — Prepend multiple items to the head so that `items[0]` becomes the next value returned by `shift()`. Efficient for bulk prepends.

- `values()` — Non-destructive iterator of values (alias of the default iterator, i.e. `[Symbol.iterator]`).

- `keys()` — Non-destructive iterator of zero-based indexes (0 is the head).

- `entries()` — Non-destructive iterator yielding `[index, value]` pairs.

- `drain()` — Consuming generator that yields items in FIFO order and removes them from the queue as iterated.

- `toArray()` — Return a shallow array snapshot of the queue contents in FIFO order (non-destructive).

## Example

```javascript
import { PowerQueue } from 'performance-helpers/powerQueue';

const q = new PowerQueue(16);

async function processStream(readable) {
  for await (const chunk of readable) {
    q.push(chunk);
    if (q.length >= 32) {
      await flushQueue();
    }
  }
  await flushQueue();
}

async function flushQueue() {
  for (const item of q.drain()) {
    await writeRecord(item);
  }
}
```

### Batch examples

```javascript
const q = new PowerQueue(4);
q.pushMany([1, 2, 3, 4]);
console.log(q.length); // 4

// Prepend so items are processed first-in-first-out when a higher-priority batch arrives.
q.unshiftMany(['priority-a', 'priority-b']);
for (const item of q.drain()) {
  console.log(item);
}
```

## Real-world Example — buffering for worker dispatch

```javascript
import { PowerQueue } from 'performance-helpers/powerQueue';
import { PowerPool } from 'performance-helpers/powerPool';

// Use PowerQueue as a lightweight buffer before dispatching to a worker pool
const q = new PowerQueue(64);
const pool = new PowerPool('./worker.js', { size: 2, maxSize: 4 });

// Efficiently enqueue many items
q.pushMany(itemsArray);

// Drain and dispatch with simple error handling
async function flushQueue() {
  for (const item of q.drain()) {
    try {
      // fire-and-forget; pool will queue or dispatch according to its policy
      pool.postMessage(item);
    } catch (err) {
      console.error('dispatch failed, requeuing or persisting', err);
      // requeue or persist for later retry
      q.push(item);
    }
  }
}

// Use periodically or on demand
setInterval(() => {
  if (!q.isEmpty) flushQueue();
}, 250);
```
