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

@nimbus-sh/fabric

v0.23.0

Published

The Cloudflare half of Nimbus — Durable Object facet hosting, dynamic-worker loader pools, the resident-process fabric, and DO alarm/hibernation machinery.

Readme

@nimbus-sh/fabric

Part of Nimbus, my hobby/research cloud OS. This README is edited and maintained with Claude (AI) and presented as-is.

Run long-lived programs on Cloudflare Durable Objects.

A Durable Object gives you one alarm, one 128 MiB isolate, a 30-second CPU turn, and storage that can reset under you. This package turns those into things you can build on: many timers on the one alarm, work that survives a reset, CPU work that spans turns, and real processes in their own isolates.

Use it if you host something that outlives a request. A dev server, a build, an agent, a terminal session.

Install

npm install @nimbus-sh/fabric

Requirements

Use a compatibility date of 2026-08-04 or later, or list nodejs_compat in compatibility_flags. The timer dispatcher needs AsyncLocalStorage, which workerd ships only under nodejs_compat, on by date from 2026-08-04. Without it the module fails to load at deploy time.

composeFabric also needs enhanced_error_serialization: a compatibility date of 2026-04-21 or later, or the flag in compatibility_flags on an older date. A program's filesystem errors reach it across workerd RPC, and only that flag carries their code. On workerd without it composeFabric throws, naming both fixes, rather than every program seeing EIO. It needs enable_request_signal in compatibility_flags too, on every date: a process owns work for a response until its client is done with it, and only that flag shows a client leaving. Without it composeFabric throws, naming the flag. It throws where it is called. At module scope the Worker fails at startup. A library host that composes through NimbusWorkspace.create({ fabric }) deploys and starts, and its first create throws.

Import the root inside a Worker. Outside workerd, import subpaths such as @nimbus-sh/fabric/timers.js, which are typed against plain objects and run in bun or node.

Setup

Declare your composition once, in your Worker's entry module.

import { composeFabric, adoptCtxExports } from '@nimbus-sh/fabric';

// Module scope of the Worker entry, once per isolate:
composeFabric({
  // The name of your supervisor WorkerEntrypoint export. The fabric mints one
  // binding per hosted program from it (env.SUPERVISOR inside the facet).
  supervisorEntrypoint: 'MySupervisorRPC',
  // Only if you use 'staged' boot specs; 'code' boots need no assembler.
  stagedBootAssembler: async (env, stage) => assembleLoaderConfig(env, stage),
});

// ctx.exports is runtime state, not composition. Capture it where the
// platform hands it over — the first fetch, or the DO constructor:
adoptCtxExports(ctx.exports);

composeFabric runs once per isolate. Calling it again with the same values is a no-op; calling it again with different values throws, naming both. A Worker that imports Nimbus's own entry (@nimbus-sh/sdk/worker) inherits that entry's composition, so a host that names its own namespace composes in a Worker that does not import it.

A program's filesystem calls reach the supervisor entrypoint through its answer(method, args), which resolves { value }, or { refusal } for an error with a code (ENOENT, ENOTDIR, EEXIST), and the program's client rethrows the refusal as the error a throw would have delivered. A refusal thrown from an entrypoint is recorded by the platform as an exception ("canceled ... your Worker's code had hung") although its caller was answered. SupervisorRPC implements answer, so an entrypoint that extends it has it; one written from scratch must implement it too.

The route back to the host (namespace, dispatch method, supervisor entrypoint) is minted into every binding the fabric hands a program, in the host's isolate, and the entrypoints that answer those bindings read it from their props. The platform serves a Worker's entrypoints from whichever isolate it likes; an isolate that composed nothing still answers to the right host.

Every instance of the host namespace forwards supervisorOp(envelope) to a hosted runtime, the siblings Nimbus opens by name included: a wide npm install fans out to nbf:* peers of the same namespace, and NIMBUS_PROCESS_HOST=peer places processes on siblings. A sibling that forwards to a bare workspace answers "is a host op, and this handler is a bare workspace's".

Timers

A Durable Object has one alarm, and a second setAlarm() overwrites the first. Route every timer through one dispatcher instead.

import { DurableObject } from 'cloudflare:workers';
import { timers } from '@nimbus-sh/fabric';

export class MySession extends DurableObject {
  _timerChain?: Promise<unknown>;   // serializes the map's read-modify-write

  async fetch(request: Request): Promise<Response> {
    await timers(this, this.ctx).schedule('janitor', Date.now() + 60_000);
    return new Response('ok');
  }

  async alarm(): Promise<void> {
    await timers(this, this.ctx).dispatch({
      janitor: async (now) => {
        await this.cleanUp();
        return { rearmAt: now + 60_000 };   // re-arm through the return value
      },
    });
  }
}

schedule stores a deadline per reason and arms the alarm at the earliest one. dispatch runs the reasons that are due, ignores reasons it does not know, and stops re-arming when none are left, which lets the object hibernate. A handler re-arms itself by returning { rearmAt }.

Generations and reset detection

Workerd hands you a fresh isolate on cold starts, hibernation wakes, and resets. The generation counter tells you which incarnation you are in, and process IDs derive from it.

import { adoptGeneration, generation } from '@nimbus-sh/fabric';

export class MySession extends DurableObject {
  async fetch(request: Request): Promise<Response> {
    await adoptGeneration(this.ctx);   // idempotent per instance
    // generation(this.ctx) is now this incarnation's generation
  }
}

This gives you one reliable test for stale state: an ID at or below the current generation's base came from a previous incarnation.

Adopt the persisted value before bumping it. await put() returning is not durability, so a bump that has not landed can be re-issued to the next boot, and two instances would share a generation.

Fenced work

A reset destroys whatever the current turn had in flight, including a half-built process. Journal the work first, and a later instance can finish it.

import { FencedWork, type FencedWorkRecord } from '@nimbus-sh/fabric';

interface MyLaunch extends FencedWorkRecord {
  argv: string[];   // whatever your redrive needs; the journal never reads it
}

const journal = new FencedWork<MyLaunch>(this.ctx.storage, {
  generationBase: () => this.pidBase,
  waitUntil: (p) => this.ctx.waitUntil(p),
  redrive: (record, attempt) => this.launch(record.argv, attempt),
});

// Before the launch's first byte of real work:
await journal.journal({ pid, command, attempt: 0, phase: 'starting', argv });
// When the PROCESS (not the launch) ends:
await journal.release(pid);
// On the first turn after any reset (the turn pump calls this):
await journal.recoverInterrupted();

Write the row before the work starts and release it when the process ends, not when the launch ends. Resets usually arrive after a launch settles, so a launch-scoped row is already gone when recovery looks for it.

The journal writes through ctx.storage.sync(), because await put() resolves before the write is durable. Recovery re-drives each stale row once.

Turn pacing

One turn gets about 30 seconds of CPU. Yielding inside a turn does not help, because CPU accrues to the invocation. Only re-entering the object resets the budget, and a long turn also blocks the actor's thread and drops WebSockets.

import { TurnBudget, PacedWork, onColdStart, timers } from '@nimbus-sh/fabric';

const pump = new PacedWork(this.ctx, {
  requestTurn: () => { void timers(this, this.ctx).schedule('launch-turn', Date.now()); },
});
// Deferred reconciliation rides the first pump, off the init gate:
onColdStart(this.ctx, () => journal.recoverInterrupted());
const budget = new TurnBudget(pump);

// Inside the launch, after each unit of work:
await budget.spend(bytesJustProcessed);   // suspends every TURN_CHUNK_MAX_BYTES (2 MB)
// In alarm(), as one of the dispatcher's reasons:
'launch-turn': () => pump.pump(),

Account in bytes, not milliseconds: the in-DO clock does not advance without I/O. spend() suspends every 2 MB and resumes on a fresh turn. Scheduling a past deadline re-enters the object immediately.

Isolate pool

Run plain functions in warm dynamic-worker isolates.

import { IsolatePool } from '@nimbus-sh/fabric';

const pool = new IsolatePool(env, this.ctx, {
  concurrency: 4,
  tag: 'checksum',
  omitSupervisor: true,   // this pool needs no callback into the DO
});
try {
  const sums = await pool.map(async (text: string) => {
    const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
    return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
  }, inputs);
} finally {
  pool.dispose();   // releases the pool's long-lived RPC stubs
}

Functions are serialized with fn.toString(), so they must be self-contained: no captured variables, no this. Bindings arrive as the last parameter. Slots are stable, so a batch of 67 tasks reuses 4 warm isolates instead of paying 67 cold starts.

Ship wasm through the loader's modules map as { wasm: ArrayBuffer }. It is the only path that works. Request-time WebAssembly.compile is blocked by CSP, structured clone refuses a compiled Module, and inlining bytes into the source exhausts the supervisor's memory.

Warm isolates are scoped to one session. A pool may opt into cacheScope: 'global' only if it takes no supervisor binding and keeps no user state.

Fanout handles wider batches. A Durable Object may have 10 distinct Dynamic Workers with in-flight requests at once (DO_DYNAMIC_WORKER_LIMIT), shared across every concurrent request to it; repeated requests to one Dynamic Worker count once. A batch that fits the coordinator's remaining headroom runs there, one Dynamic Worker per task; a wider one shards across up to 32 sibling objects, 4 at a time, each spending its own headroom.

The loader ledger counts what is in flight per DO: pool and one-shot calls, esbuild facet calls, git network ops, and every resident process for as long as it lives. dynamicWorkerHeadroom(ctx) is what is left, claimDynamicWorkers(ctx, n) reserves a width, loaderLedgerStats(ctx) reports it all, and a limit refusal names the workers in flight.

beginLoaderFetch(ctx, key) holds a worker and returns the function that ends the hold. To wait for room rather than be refused, take the hold with beginLoaderFetchWhenFree instead:

import { beginLoaderFetchWhenFree } from '@nimbus-sh/fabric';

const end = await beginLoaderFetchWhenFree(ctx, key, { signal: AbortSignal.timeout(15_000) });
try {
  return await worker.getEntrypoint().run();
} catch (error) {
  end(error); // a limit refusal pauses the ledger; anything else just ends the hold
  throw error;
} finally {
  end();
}

It resolves, holding key, once key is already in flight (requests to one worker count once) or one more distinct worker fits. Waits are let in in the order they asked, one per freed slot, by whoever makes the room: any hold's end, a claim's release, or the end of a refusal's pause, so a wait sees Nimbus's own releases as well as the caller's. The hold is taken as the wait is let in, so no other caller can take the slot first, and it is the caller's to end. An aborted signal rejects the wait with its reason, and the wait holds nothing. A resident process holds its worker for as long as it runs, so bound a wait that room may never reach.

The platform counts a worker for a moment after its call returns, which no release can show. So a call refused with "Dynamic worker concurrency limit exceeded" hands the refusal to its end function, and the ledger then admits no new worker for 50 ms, doubling to 2 s while refusals continue (loaderLedgerStats(ctx).pauseMs; the headroom reads 0 meanwhile). IsolatePool retries a refused call this way, for 15 s in all. Holds taken under a claim (beginLoaderFetch(ctx, key, claim), or IsolatePool's claim option, which Fanout passes) count inside its width.

Process fabric

A resident process is a Durable Object facet running a class from a dynamic worker.

import { ProcessFabric, createProcessHost } from '@nimbus-sh/fabric';

const fabric = new ProcessFabric(createProcessHost('facet', this.ctx, env, () => diskReader));

const handle = await fabric.startResidentProcess({
  startContract: 'boot',            // or 'lifetime' — see below
  pid,
  workerKey: `nimbus-process:${this.ctx.id}:${pid}`,
  boot: { kind: 'code', code: spec },   // a ResidentCodeSpec
  startArgs: { port: 3000 },
  onWriterActivated: (writerId) => this.writers.add(writerId),
  onWriterRetired: (writerId) => this.writers.delete(writerId),
});

const payload = await handle.booted();
// Inbound HTTP for the process's ports:
const response = await handle.routeTarget.handleHttpRequest(request);
// Teardown:
handle.kill();
await handle.done;

The worker exports a Durable Object class named NimbusProcess with startProcess(args) and handleHttpRequest(request). startProcess declares one of two contracts. Use 'lifetime' when the call should stay open for the process's life, as an attached terminal does. Use 'boot' when it should return once the process is up and leave the facet resident, as a server does.

Each process takes a fresh facet name. proc-slot-<n>, numbered per incarnation; a released name is not reused, because getting one a released process held, with the next process's class, failed on Cloudflare. An ephemeral process's facet is deleted with it: Cloudflare bounds facets kept, not names used (PLATFORM.md).

Large boot members travel as VFS paths. A structured-clone RPC value caps at 32 MiB. Use vfsWasmModules and vfsTextModules; the host reads the bytes during the load and verifies each image against the digest its path claims.

The substrate is one deployment-wide setting, 'facet' or 'peer', never a per-spawn choice.

| | spawn | memory | CPU | SQLite | |---|---|---|---|---| | facet | 8–16 ms | independent (~208 MiB each) | shared | own | | peer | 242–359 ms | independent | independent | own |

Facets are separate isolates in one actor thread, so they scale memory but not CPU. Awaiting I/O yields the thread; a 9,956 ms CPU burn stalled a sibling for 9,966 ms. A peer costs about 20× the spawn time and buys real CPU isolation, and it verifies it did not co-locate.

A facet shares its session's object, so it can copy the session's store by reflink (ctx.facets.clone, 18–31 ms for 45.73 MB, 34–54 ms for 1 GB) and shares the session's ~10 GiB budget. A peer brings its own storage and cannot reflink.

Call clone through cloneStorage. An unresolvable src empties the destination and reports success, so the wrapper checks the source before the call and the destination after. An emptied facet still shows a 4,096-byte database, so check for your own data rather than a non-zero size.

Image store

ImageStore writes generated boot images into a content-addressed store at .nimbus/process-images/<sha256>.js (processImagePath), through an ImageBlobStore port you implement, and its sweep removes files there only. .nimbus/facet-images/<sha256>.js (facetImagePath) is yours: keep your own content-addressed images there (a durable worker's runner, its text modules). Nimbus reads and verifies them, never writes or removes them. Images an earlier release wrote there stay until you remove them. Put /.nimbus where no user can replace a directory on the way to it: Nimbus's own sessions keep it in root's /, owned by root, 0755.

It registers the whole root set before writing any byte, so a sweep never sees an unclaimed image. Writes are sliced to stay inside the 1 MiB transaction bound; a 22.9 MB map written in one turn reset the session about a quarter of the time. Because a write only grows the file from offset zero, matching size means a complete write, and the reader verifies the digest before the loader sees the bytes. Images stay live while a process boots from them, rooted in the process table, with no TTL.

Binding shims

NimbusLoaderRPC, NimbusLoadedWorker, NimbusLoadedEntrypoint, NimbusAssetsRPC and NimbusDurableObjectNamespace give a dynamically-loaded inner Worker working env bindings.

Three platform rules shape them. A WorkerStub does not serialize, so each hop is its own WorkerEntrypoint class. A stub belongs to the request that minted it, so the shims store code rather than stubs and re-resolve through LOADER.get(id, cb); workerd caches by ID, and the code map is capped at 32 entries. A keyed Worker is cached under its loader's owner namespace, its network and its id: each binding's minter names its owner (once per build in nimbus wrangler dev), so two owners never share a Worker under one id, and a loader without an owner has no keyed Workers. An RPC method is a wildcard property, so call ep.method(request) and never method.call(ep, request), which workerd refuses.

Nesting is capped at depth 4. Raise it with NIMBUS_INNER_LOADER_DEPTH.

A classic Durable Object binding is a local namespace inside the inner Worker (inner-do-env.ts), because a namespace's API is synchronous and an RpcPromise cannot travel as an argument. innerWorkerModules (inner-env.ts) makes an adapter the bundle's first import; it replaces env.MY_DO in the env every handler, entrypoint and object of the isolate sees, so env.MY_DO.get(env.MY_DO.idFromName('x')) needs no await. A stub is new RpcStub(target), with a prototype shaped as a Durable Object stub's: an entrypoint of a dynamically-loaded Worker is not transferable, an RPC stub is. The target relays each member the caller reaches (a call, a read, or a path through both) to NimbusDurableObjectNamespace.callOn or getOn, and the session reaches it on the object's facet. The adapter also exports the build's class check, under a name the bundle never spells. tests/unit/wrangler-dev-do-rpc-workerd.mjs checks the whole stub surface against plain workerd.

Limits, where a stub differs from Cloudflare's:

  • typeof stub is 'function', not 'object': the runtime makes every RPC stub callable.
  • A Worker Loader env cannot carry a stub. env.LOADER.load({ env: { S: stub } }) throws "RpcStub cannot be serialized in this context because it is not a persistent stub", where Cloudflare passes it. The loader shim keeps a child's code and loads it again in each later request, so the child's env can carry nothing made in one request; a namespace is refused there on Cloudflare too.

NimbusKVNamespace, NimbusD1Binding and NimbusR2Bucket are a KV, D1 or R2 binding. The session mints each for one store of one project, named in its props, and each call is a supervisor op (kvCall, d1Query, r2Call) the session serves for that store only. The same adapter replaces each with a local object (inner-storage-env.ts). KV's calls and answers cross RPC as they are, so its KvNamespace only converts options as workerd's client does (an int by ToNumber, jsg-int.ts, where an object argument's valueOf would cross as a stub). The D1 and R2 APIs cannot cross RPC (a statement is bound synchronously; an R2 object has methods): a D1Database as workerd's own over the binding's query, D1's binding contract, and an R2Bucket whose objects it makes from the fields the binding answers. tests/unit/wrangler-dev-storage-bindings-workerd.mjs checks all three against the local bindings wrangler dev gives a Worker.

Measured platform limits

Figures below come from production workerd, June to August 2026. The code above enforces them where it can. PLATFORM.md is the full catalog: every entry dated, graded by evidence, and marked as enforced here or left to you.

Durable Object storage

| Invariant | Evidence | |---|---| | await put() resolves BEFORE durability; ctx.storage.sync() is the barrier; the output gate holds the guarantee | a launch killed in its first chunks left NO journal row (staging, 2026-08-13) | | A reset destroys every write its turn had outstanding; an alarm write rolls back with it and the platform re-delivers the alarm to the replacement instance | the first turn after a reset is a recovery turn, for free | | SQLite value cap is 2 MB per ROW, key length included | single-value ceiling 2,199,981 B with a 12-char key; overflow throws clean, catchable SQLITE_TOOBIG | | One alarm per object; a second setAlarm() silently overwrites | why TIMER_REASONS_KEY is a map | | Input gates stay closed across get/put | set-if-absent is atomic per DO with no CAS loop | | A facet's own SQLite survives a fresh module scope | 7,141 rows / 45.7 MB intact across recycling — keep provenance in rows, never heap | | ~10 GiB storage budget shared by the DO root and every facet and clone under it, with no copy-on-write credit | N clones of X bytes cost X·(N+1); crossing RESETS the object rather than raising an error |

Lifecycle and CPU

| Invariant | Evidence | |---|---| | No pending alarm ⇒ hibernation-eligible after ~10 s idle | why timers.dispatch deletes the map when nothing remains | | One-turn CPU budget ~30 s; yielding inside an invocation buys nothing; only genuine re-entry (an alarm) resets it | killed with exceededCpu at 31.8 s and 32.5 s | | A long turn drops the object's WebSockets even when the work succeeds | the launch turn finished outcome=ok and the terminal died anyway | | The in-DO clock does not advance without I/O | 0 ms across 200,000 consecutive Time.now reads — pace in bytes, hand deadlines to the host | | Isolate generation increments on EVERY fresh isolate: cold start and hibernation wake, not only resets | adoptGeneration adopts persisted truth first | | pid <= generation base ⇒ previous generation | THE reset predicate; PID_GEN_STRIDE = 1,000,000 | | setTimeout/setInterval prevent hibernation | one-shot self-nulling timers only |

Facets and dynamic workers

| Invariant | Evidence | |---|---| | Facet memory independent, ~208–256 MiB each; facet CPU SHARED across siblings | 9,956 ms burn stalled a sibling 9,966 ms; awaited I/O costs siblings 0 ms | | Facets kept are bounded, names used are not: 70,000 names deleted after use, none failed; 32,240 kept, the object failed and its storage would not start (2026-10-07). Local workerd: 65,535 names per DO LIFETIME (facet index) | an ephemeral process's facet is deleted with it; a failed start names its facet and process | | Dynamic-worker module map hard ceiling 67,108,864 bytes, shared across every member | 62 MiB lands, 64 MiB refused; boot cost roughly linear in map bytes and not the bottleneck (40 MiB → 1.42 s across 6,553 modules); every assembly seam refuses an over-ceiling map listing the largest members by size, because the platform's refusal names none | | Request-time WebAssembly.compile/instantiate CSP-blocked; wasm rides the loader modules map as { wasm: ArrayBuffer }, compiled at module load | RPC of a compiled Module refused by structured clone; inlined bytes OOMed the supervisor | | Module scope bans I/O; new Function succeeds at module scope and throws at request time | code reaches a facet through the module map or not at all | | The facet start callback fires at most once | re-running it would re-execute the user's program | | 10 distinct Dynamic Workers with in-flight requests per DO, shared across its concurrent requests; repeated requests to one Dynamic Worker count once (changelog, 2026-08-28) | DO_DYNAMIC_WORKER_LIMIT; Fanout sizes in-DO batches to the live headroom, and a refusal names the workers in flight | | ctx.facets.clone is same-object only; declared by @cloudflare/workers-types 5 and present in workerd ≥ 1.20260926.1 and in production | 18–31 ms / 45.7 MB, 34–54 ms / 1 GB; an unresolvable src silently EMPTIES the destination and reports success — cloneStorage enforces the both-ends validation | | A DO dies at ~200 MiB of live wasm linear memory; reserved and written pages die at the same ceiling | lazy growth buys nothing; bound guest memory by rewriting the memory section | | A wasm stack suspended (JSPI) in one request cannot resume in another | 3 in-context resumes took 6 ms; the first cross-context one hit a 30 s timeout |

RPC and stubs

| Invariant | Evidence | |---|---| | workerd constructs a NEW WorkerEntrypoint instance per RPC call | instance fields are write-only; fold state through return values | | A stub minted in one invocation throws in another ("Cannot perform I/O on behalf of a different request") | store CODE, not stubs; re-resolve via LOADER.get(id, cb) — cached by id, ~free | | WorkerStub does not serialize | one chained WorkerEntrypoint proxy class per hop | | An RPC method is a wildcard property: method.call(ep, r) serializes ep as an argument and is refused | always ep.method(r) | | Entrypoints to dynamically-loaded workers cannot transfer across Workers | HTTP travels as parts with plain ReadableStream bodies, re-piped through an identity stream this isolate owns | | Structured-clone RPC cap 32 MiB | ship ≤ 28 MiB (~6% clone overhead); clone refusal is classified distinctly | | RPC resources need explicit Symbol.dispose, including on error paths | a timeout's reject closure otherwise roots a 28 MiB payload for the full timeout | | A binding minted BY AN ACTOR lives as long as the process; nothing holds a call open | why the facet's SUPERVISOR binding comes from the DO, not a stateless entrypoint |

WebSockets

| Invariant | Evidence | |---|---| | Without setWebSocketAutoResponse(ping/pong), every idle-tab ping wakes the actor | ~2,880 wakes/day per idle tab; the config survives hibernation | | A hibernatable WS owned by a DO cannot be written from a sibling WorkerEntrypoint isolate | sends happen in the DO's own context (relay pattern) | | A WebSocket upgrade cannot ride the RPC hop a resident's HTTP takes | a 101 owns a live socket and RPC reconstructs values rather than handing sockets over; an upgrade takes the separate fetch-semantic entrypoint and stays on fetch for every hop (a facet is fetched directly; a peer fetches its own facet), and a target without that entrypoint answers 501 |

Sharing an isolate

| Invariant | Evidence | |---|---| | The isolate heap ceiling is 128 MiB, and multiple DOs from one script can SHARE one isolate | resets observed below any single object's apparent usage | | exceededMemory and exceededCpu are both uncatchable inside the dying isolate, observable only across an RPC boundary | absence of the error is not evidence of its absence | | process.memoryUsage() returns 0 in DO context | any heap estimate is a lower bound; say so |

Related packages

@nimbus-sh/core is the backend-agnostic OS: filesystem, shell, process contracts. @nimbus-sh/platform holds the limits tables, the error taxonomy, RPC disposal, and the supervisor budget machinery. @nimbus-sh/worker is the reference embedder, with the supervisor entrypoint and the session protocol. For the full hosted product, start from npx create-nimbus-app.

License

MIT.