npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

turn-taker

v0.1.1

Published

This repository explores managed cross‑tab turn‑taking (via SharedWorker) for coordinating access to shared resources within a single origin, including OPFS‑backed workflows.

Readme

🚧 Experimental / Research Notice

This repository explores managed cross‑tab turn‑taking (via SharedWorker) for coordinating access to shared resources within a single origin, including OPFS‑backed workflows.

In most cases, you don’t need an extra arbiter for OPFS writes: OPFS enforces exclusive write access per file. Only one context can hold a write handle to the same file at a time, so concurrent writers to a single file are prevented by the platform.

What this means for you:

  • Not production‑ready and likely to remain research‑only.
  • No stability guarantees; APIs and behavior may change.
  • Limited hardening for adversarial or crashy environments.
  • Best used for learning, prototyping, and comparing approaches.

When can a turn‑taking layer still help?

  • Coordinating multi‑file operations you want to treat as a unit (ordering/fairness).
  • Scheduling expensive work across tabs to avoid thrash or starvation.
  • Applying global fairness across logical resources beyond a single file.
  • Bridging OPFS with non‑atomic operations where a single queue improves UX.

If you need cross‑tab coordination in production, start with OPFS’s per‑file exclusion. For higher‑level scheduling, consider the Web Locks API (with logical resource keys) or a small BroadcastChannel + leader‑election pattern; reach for a custom turn‑taking layer only if those aren’t sufficient.


TurnTaker Core (turn-taker)

Type-safe coordination for exclusive access across tabs using a SharedWorker. Ideal for OPFS, shared state, or any mutual-exclusion workflow.

Monorepo note: this file documents the published library package. For repo-wide setup, dev, and the demo app, see the workspace root README.md.

Features

  • 🔄 Fair turn-taking (FIFO) with optional priority and preemption
  • 🌐 Multi-tab coordination via SharedWorker
  • 🧵 Multiple concurrent requests per participant via awaitable RequestHandles
  • ⚡ Real-time updates: global + per-request events
  • 🛡️ Structured error codes with end-to-end propagation
  • 🫶 Metadata on grant/release for richer auditing
  • 🫡 Heartbeat monitoring and stale participant cleanup
  • 🛰️ TURN_CHANGE broadcasts so all tabs update immediately

Installation

npm install turn-taker

Copy the prebuilt SharedWorker into your app's public folder (so the browser can load it):

npx turntaker-copy-worker ./public

Quick Start

1. Set up the SharedWorker

You can integrate the SharedWorker in two ways:

Option A — Static URL (SSR/static hosting)

  • Copy the prebuilt worker file somewhere your app can serve it and pass that URL to the client.
  • You can use the provided bin to copy the file: npx turntaker-copy-worker ./public/turntaker-worker.js
import { TurnTakerWorker } from "turn-taker/worker";
// If you need to customize, you can also create your own worker script.

Option B — Inline Blob URL (no static hosting assumption)

Generate a Blob URL at runtime and use that as the worker URL:

import { createInlineSharedWorkerURL } from "turn-taker";
const workerUrl = createInlineSharedWorkerURL();
// pass workerUrl to the client

If you want to roll your own worker script, here’s a canonical version:

import { TurnTakerWorker } from "turn-taker/worker";

// Initialize with optional configuration
const worker = new TurnTakerWorker({
  turnTimeout: 30000, // 30 seconds max turn time
  enablePriority: true, // Enable priority queuing
  enablePreemption: false, // Disable turn preemption
  maxQueueSize: 100, // Max 100 pending requests
  heartbeatInterval: 5000, // 5 second heartbeat
});

// Tip (tests): append ?wid=... to isolate worker instances per test run.
// The worker uses this to name its global scope.

2. Use the Client Library

import { TurnTakerClient, createInlineSharedWorkerURL } from "turn-taker";

const client = new TurnTakerClient({
  // Either a static URL you serve, or the inline blob URL generated at runtime
  url: createInlineSharedWorkerURL(),
  // optional isolation knobs:
  // scope: "app",
  // resourceId: "file-42",
});

client.addEventListener("connected", async () => {
  // Request a turn (awaitable RequestHandle)
  const handle = client.requestTurn({ priority: 1 });

  // Per-request events
  handle.on("queued", (e) => console.log("position", e.detail.position));
  handle.on("granted", (e) => console.log("granted", e.detail));
  handle.on("released", () => console.log("released"));

  // Await grant
  await handle;

  // Do exclusive work here
  await performExclusiveOperation();

  // Release the granted request via its handle
  await handle.release({ result: "success" });
});

// Global events
client.addEventListener("turn-change", (e) => {
  console.log("Current turn:", (e as CustomEvent).detail.currentTurn);
});
client.addEventListener("queue-update", (e) => {
  console.log("Queue:", (e as CustomEvent).detail.queueStatus);
});
client.addEventListener("tt-error", (e) => {
  console.warn("TurnTaker error:", (e as CustomEvent).detail);
});

Real-World Example: OPFS File Coordination

import { TurnTakerClient } from "turn-taker";

class SafeFileManager {
  constructor() {
    this.client = new TurnTakerClient("/turntaker-worker.js");
    this.setupEventHandlers();
  }

  setupEventHandlers() {
    this.client.addEventListener("connected", () => {
      console.log(`Connected as ${this.client.getParticipantId()}`);
    });

    this.client.addEventListener("turn-revoked", (event) => {
      console.warn("Turn was revoked:", event.detail.reason);
      // Clean up any open file handles
    });
  }

  async writeFile(filename, data) {
    try {
      // Request exclusive access
      const handle = this.client.requestTurn({
        priority: 1,
        metadata: { operation: "write", filename },
      });
      await handle;

      // Now we have exclusive access to OPFS
      const opfsRoot = await navigator.storage.getDirectory();
      const fileHandle = await opfsRoot.getFileHandle(filename, {
        create: true,
      });
      const writable = await fileHandle.createWritable();

      await writable.write(data);
      await writable.close();

      console.log(`Successfully wrote ${data.length} bytes to ${filename}`);

      // Release the request via its handle
      await handle.release({
        operation: "write",
        filename,
        bytesWritten: data.length,
      });

      return true;
    } catch (error) {
      // Always cleanup on error
      if (this.client.hasTurn()) await this.client.releaseAll();
      throw error;
    }
  }

  async readFile(filename) {
    try {
      const handle = this.client.requestTurn({
        priority: 0, // Lower priority for reads
        metadata: { operation: "read", filename },
      });
      await handle;

      const opfsRoot = await navigator.storage.getDirectory();
      const fileHandle = await opfsRoot.getFileHandle(filename);
      const file = await fileHandle.getFile();
      const content = await file.text();

      await handle.release({
        operation: "read",
        filename,
        bytesRead: content.length,
      });

      return content;
    } catch (error) {
      if (this.client.hasTurn()) await this.client.releaseAll();
      throw error;
    }
  }
}

// Usage
const fileManager = new SafeFileManager();
await fileManager.writeFile("data.txt", "Hello World!");
const content = await fileManager.readFile("data.txt");

API Reference

TurnTakerClient

Constructor

new TurnTakerClient(options?: {
  url?: string;       // worker URL (default: "/turntaker-worker.js")
  scope?: string;     // SharedWorker name for isolation (optional)
  resourceId?: string;// route to a specific queue inside the worker (optional)
})

Methods

requestTurn(options?) => RequestHandle

Request exclusive access to the shared resource.

const handle = client.requestTurn({
  priority?: number,   // higher = sooner (default: 0)
  timeout?: number,    // ms; overrides config.turnTimeout
  metadata?: any,      // echoed back on grant/release
});

// RequestHandle helpers
await handle;                // resolves on grant -> { requestId, grantedAt, timeout?, metadata? }
handle.getId();              // number | undefined (once queued)
await handle.cancel();       // cancel if still queued
handle.status();             // snapshot: pending|queued|granted|released|revoked|error
handle.on(type, listener);   // per-request events: queued|granted|released|revoked|error
handle.off(type, listener);
withTurn(options, fn, hooks?) => Promise<T>

Run a function while safely holding the turn. Always releases in a finally block.

const result = await client.withTurn(
  { priority: 5, metadata: { op: "write" } },
  async (handle) => {
    // handle is the RequestHandle for this request (events, status, etc.)
    // This function runs only while you hold the turn.
    await performExclusiveOperation();
    return "ok" as const;
  },
  {
    // Optional: payload to include on release
    release: { result: "ok" },

    // Optional: invoked if your turn is revoked (e.g., preempted)
    onRevoked(info) {
      console.warn("Turn revoked:", info.reason);
      // cleanup, flush partial work, etc.
    },
  }
);

Notes:

  • Ensures release even if fn throws; the error is rethrown after releasing.
  • If the turn is revoked during execution, onRevoked runs before withTurn rejects.
  • handle supports per-request events: queued|granted|released|revoked|error.
handle.release(result?)

Release the currently granted request using its handle.

await handle.release({
  result?: any,         // Result data from your operation
  metadata?: any        // Additional metadata
});

Note: Release is done via the request handle; TurnTakerClient.releaseTurn() has been removed.

releaseAll()

Release the current turn (if held) and cancel all queued requests for this client.

const summary = await client.releaseAll();
// { released: boolean, canceled: number }
configure(config)

Update the worker configuration.

client.configure({
  turnTimeout: 60000,
  enablePriority: true,
  maxQueueSize: 50,
});
getStatus()

Get current system status.

const status = await client.getStatus();
console.log(status.currentTurn); // Current turn holder info
console.log(status.queueStatus); // Queue information
console.log(status.participantCount); // Number of connected tabs
Utility Methods
client.hasTurn(); // Returns true if you currently have the turn
client.isConnected(); // Returns true if connected to worker
client.getParticipantId(); // Returns your unique participant ID
client.releaseIfHeld(result?); // Attempts a safe release only if you currently hold the turn
client.disconnect(options?); // Disconnect from the worker

disconnect(options?)

client.disconnect({
  releaseIfHeld: true, // if holding a turn, release first to avoid dangling locks
});

Events

Listen for real-time updates:

client.addEventListener("connected", (event) => {
  console.log("Connected to TurnTaker");
});

client.addEventListener("turn-granted", (event) => {
  console.log("Turn granted:", event.detail);
});

client.addEventListener("turn-released", (event) => {
  console.log("Turn released:", event.detail);
});

client.addEventListener("turn-revoked", (event) => {
  console.log("Turn was revoked:", event.detail.reason);
});

client.addEventListener("queue-update", (event) => {
  console.log("Queue status:", event.detail.queueStatus);
});

client.addEventListener("turn-change", (event) => {
  console.log("Turn changed hands:", event.detail.currentTurn);
});

client.addEventListener("tt-error", (event) => {
  console.error("TurnTaker error:", (event as CustomEvent).detail);
});

Configuration Options

interface TurnTakerConfig {
  turnTimeout: number; // Max time a turn can be held (default: 30000ms)
  enablePriority: boolean; // Enable priority-based queuing (default: false)
  enablePreemption: boolean; // Allow high-priority requests to preempt (default: false)
  maxQueueSize: number; // Maximum number of queued requests (default: 100)
  heartbeatInterval: number; // Heartbeat interval for connection monitoring (default: 5000ms)
}

Notes:
- Changing `heartbeatInterval` takes effect immediately in the worker’s monitor.
- `enablePriority` reorders only queued requests; with `enablePreemption` true,
  a higher-priority request can revoke the current holder.

Advanced Usage

Priority-Based Queuing

When enablePriority is true, requests with higher priority numbers are processed first:

// Low priority background task
await client.requestTurn({ priority: 0 });

// Normal user action
await client.requestTurn({ priority: 5 });

// Critical system operation
await client.requestTurn({ priority: 10 });

Turn Preemption

When enablePreemption is true, higher priority requests can interrupt lower priority ones:

// Configure worker with preemption
client.configure({
  enablePriority: true,
  enablePreemption: true,
});

// Critical request will preempt lower priority turns
await client.requestTurn({
  priority: 10,
  metadata: { urgent: true },
});

Timeout Handling

Set custom timeouts for specific operations:

// Long-running operation with extended timeout
await client.requestTurn({
  timeout: 120000, // 2 minutes
  metadata: { operation: "bulk-import" },
});

Error Handling & Recovery

Errors are structured and propagated via the global tt-error event and per-request error events. Example error code:

  • E_CANNOT_CANCEL_GRANTED — attempting to cancel a request that’s already active.

Branch on code to shape UX:

client.addEventListener("tt-error", (e) => {
  const err = (e as CustomEvent).detail;
  if (err.code === "E_CANNOT_CANCEL_GRANTED") {
    // disable cancel, show a message, etc.
  }
});

Metadata Propagation

Attach context to your requests and receive it back on grant/release:

await client.requestTurn({
  priority: 2,
  metadata: { operation: "sync", batchId: "b42" },
});

client.addEventListener("turn-granted", (e) => {
  console.log("granted with metadata", (e as CustomEvent).detail.metadata);
});

Heartbeats & Stale Cleanup

The client sends periodic heartbeats. If a tab stops heartbeating, the worker removes it after a grace period, advancing the queue automatically. You can also call disconnect() to leave explicitly; if you hold the turn, release before disconnecting.

TURN_CHANGE Broadcasts

Every time the current turn changes, the worker emits a broadcast so all tabs can update their UI without polling.

Patterns

Multiple concurrent requests

const a = client.requestTurn({ metadata: { name: "A" } });
const b = client.requestTurn({ metadata: { name: "B" } });

a.on("queued", (e) => console.log("A queued at", e.detail.position));
b.on("queued", (e) => console.log("B queued at", e.detail.position));

await a; // whichever is granted first
await a.release();

// Optionally, cancel remaining queued
await client.releaseAll();

withTurn helper

async function withTurn<T>(client: any, fn: () => Promise<T>) {
  const handle = client.requestTurn();
  await handle;
  try {
    return await fn();
  } finally {
    if (client.hasTurn()) await handle.release();
  }
}

Scope vs ResourceId

  • scope: SharedWorker name; isolates worker instances at the browser level. Use for test isolation or multi-tenant separation.
  • resourceId: logical queue key inside one worker. Use to shard by file/document/etc.

Examples:

// Same worker (scope), different queues (resourceId)
const a = new TurnTakerClient({
  url: "/turntaker-worker.js",
  scope: "app",
  resourceId: "doc:1",
});
const b = new TurnTakerClient({
  url: "/turntaker-worker.js",
  scope: "app",
  resourceId: "doc:2",
});

// Different workers (different scope)
const test1 = new TurnTakerClient({
  url: "/turntaker-worker.js",
  scope: "wid-123",
});
const test2 = new TurnTakerClient({
  url: "/turntaker-worker.js",
  scope: "wid-456",
});

Architecture

TurnTaker uses a clean separation of concerns:

  • TurnTakerCore: Pure turn-taking logic (testable, framework-agnostic)
  • TurnTakerWorker: SharedWorker integration layer
  • TurnTakerClient: Browser client with event-driven API

This architecture enables:

  • ✅ Comprehensive unit testing of core logic
  • ✅ Easy integration with different transport layers
  • ✅ Clear separation between business logic and web APIs
  • ✅ Framework-agnostic design

Worker bundling

The package ships both the library and a pre-bundled SharedWorker script under packages/core/dist/worker-bundle/turntaker-worker.js. The demo app copies this to apps/web/public/turntaker-worker.js via apps/web/scripts/copy-worker.mjs.

If you host the worker yourself, ensure it’s served as a standalone JS file and referenced by URL (same-origin is recommended for SharedWorker).

Performance

TurnTaker is designed for high performance:

  • Lightweight: ~15KB minified + gzipped
  • Efficient: O(log n) queue operations with priority support
  • Scalable: Tested with 1000+ concurrent participants
  • Memory-conscious: Automatic cleanup of stale participants

Browser Support

  • Chrome 80+ (SharedWorker, OPFS support)
  • Firefox 79+ (SharedWorker support)
  • Safari 16.4+ (SharedWorker support)
  • Edge 80+ (SharedWorker support)

TypeScript Support

TurnTaker is written in TypeScript and includes full type definitions:

import { TurnTakerClient, TurnTakerConfig } from "turn-taker";

const client: TurnTakerClient = new TurnTakerClient("/worker.js");

const config: TurnTakerConfig = {
  turnTimeout: 30000,
  enablePriority: true,
  enablePreemption: false,
  maxQueueSize: 100,
  heartbeatInterval: 5000,
};

Testing

TurnTaker includes a comprehensive test suite. The separated architecture makes unit testing straightforward:

import { TurnTakerCore } from "turn-taker/core";

describe("TurnTaker Core", () => {
  it("should grant turns in FIFO order", () => {
    const core = new TurnTakerCore();

    core.addParticipant("p1");
    core.addParticipant("p2");

    const request1 = core.requestTurn("p1");
    const request2 = core.requestTurn("p2");

    expect(request1.position).toBe(0); // Granted immediately
    expect(request2.position).toBe(1); // Queued
  });
});

### End-to-end (Playwright)

The web app under `apps/web` exposes a `/turns` test page. Tests spin up multiple pages
to simulate tabs and isolate the SharedWorker per test by adding `?wid=<unique>` to the
URL (the page passes this through to the worker URL). This eliminates cross-test leakage
and greatly improves stability across Chromium/Firefox/WebKit.

Tips for reliable tests:
- Prefer explicit disconnect over closing the page to trigger immediate cleanup.
- Allow longer timeouts on slower engines (WebKit/Firefox) for worker spin-up.
- Wait for “position” to be set before asserting handoffs from timeouts.

Common Use Cases

OPFS File Coordination

Prevent "file already open" errors when multiple tabs access the same files.

Database Transactions

Coordinate access to IndexedDB or other browser storage mechanisms.

Shared State Management

Ensure atomic updates to shared application state.

Resource-Intensive Operations

Limit concurrent execution of CPU/memory intensive tasks.

Network Request Coordination

Prevent duplicate API requests from multiple tabs.

Best Practices

  1. Always release turns: Use try/finally blocks to ensure turns are released
  2. Set appropriate timeouts: Match timeouts to your operation duration
  3. Use metadata: Add context to help with debugging and monitoring
  4. Handle revocation gracefully: Always listen for turn-revoked events
  5. Consider priority carefully: Use priority sparingly to maintain fairness
  6. Monitor queue status: Watch for queue buildup that might indicate issues

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for guidelines.

Changelog

See CHANGELOG.md for version history and updates.