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

@inkandswitch/edge-handles

v0.1.0

Published

Doc-backed reactive cells with named upstream/downstream connections.

Readme

@inkandswitch/edge-handles

A doc-backed reactive cell with named upstream and downstream connections — the minimum primitive you need to build dataflow systems where the wires themselves are first-class, sharable, persistent documents.

EdgeHandles are deliberately not transformers. They are the shared primitive on which transformers, propagators, and other dataflow systems can be built. The package ships the primitive and a DOM↔handle bridge. Reference transforms (identity, sum, template, color, markdown, …) live in the edge-handles examples tool, kept out of the SDK so consumers ship only what they need.

Surface

// "@inkandswitch/edge-handles"

/** Anything URL-addressable with a value-getter and a change subscription.
 *  Both `Ref` and `EdgeHandle` implement it; edges chain into edges. */
interface Handle<T = unknown> {
  readonly url: HandleUrl;
  value(): T;
  onChange(cb: (v: T) => void): () => void;
}

class EdgeHandle<TValue = unknown> implements Handle<TValue | undefined> {
  readonly url: AutomergeUrl;
  readonly doc: DocHandle<EdgeHandleDoc>;

  readonly source:       Record<string, Handle>;
  readonly target:       Record<string, Handle>;
  readonly sourceErrors: Record<string, Error>;
  readonly targetErrors: Record<string, Error>;

  value(): TValue | undefined;
  change(fnOrValue: ChangeFn<TValue> | TValue): void;
  onValueChange(cb: (v: TValue | undefined) => void): () => void;

  onSourceChange(cb: (value: unknown, key: string) => void): () => void;
  onMembersChange(cb: () => void): () => void;
  onAnyChange(cb: (value: unknown, key: string | undefined) => void): () => void;

  setSource(name: string, h: Handle | HandleUrl): void;
  removeSource(name: string): void;
  setTarget(name: string, h: Handle | HandleUrl): void;
  removeTarget(name: string): void;

  persisted(): boolean;
  setPersisted(on: boolean): void;

  destroy(): void;
}

createEdgeHandle<T>(repo, init?): Promise<EdgeHandle<T>>;
findEdgeHandle<T>(repo, url):    Promise<EdgeHandle<T>>;

The value side mirrors Ref. The wire side is small and explicit: named maps, four mutators, three subscriptions.

onSourceChange only fires on actual per-source value emissions, both args always defined. onMembersChange fires on the initial subscribe and on source/target membership changes. onAnyChange is sugar combining both: fires on subscribe and on any upstream change, with (value, key) set when a specific source emitted and both undefined otherwise.

Quick taste — three numbers feeding a sum

import { Repo } from "@automerge/automerge-repo";
import { createEdgeHandle } from "@inkandswitch/edge-handles";

const repo = new Repo();
const a = repo.create({ value: 1 });
const b = repo.create({ value: 2 });
const c = repo.create({ value: 3 });
const total = repo.create({ value: 0 });

const edge = await createEdgeHandle<number>(repo, {
  source: { a: a.ref("value"), b: b.ref("value"), c: c.ref("value") },
  target: { total: total.ref("value") },
});

// Inline transform: re-sum on any upstream signal.
edge.onAnyChange(() => {
  let n = 0;
  for (const src of Object.values(edge.source)) {
    const v = src.value();
    if (typeof v === "number" && Number.isFinite(v)) n += v;
  }
  edge.change(n);
});                                // total.value is now 6
a.change(d => { d.value = 10 });   // total.value becomes 15

Handles

Both Ref and EdgeHandle implement the Handle interface (url, value(), onChange(cb)). The URL form accepted by setSource/setTarget /createEdgeHandle is:

  • automerge:abc123 — resolves to handle.ref(), the doc's root Ref.
  • automerge:abc123/path...#heads? — a RefUrl, resolves to a path-Ref.
  • automerge:abc123 whose doc has @patchwork.type === "edge-handle" — resolves to another EdgeHandle.

Mutators validate URLs at edit time and throw on malformed input. Failures during resolution (e.g., a peer doc that hasn't synced yet) are captured in edge.sourceErrors / edge.targetErrors rather than silently dropped, and the next doc tick retries — so a slow-to-arrive peer eventually shows up without manual re-wiring.

Persistence

By default an edge is in-memory only — change() doesn't touch the doc. Pass persist: true at create time (optionally with a value) to mirror every change() back to doc.value. Reopening the edge restores the cached value:

const edge = await createEdgeHandle<number>(repo, {
  persist: true,
  value: 0,
});
edge.change(42);                // doc.value is now 42
edge.destroy();
const reopened = await findEdgeHandle<number>(repo, edge.url);
reopened.value();               // 42

setPersisted(true | false) flips the policy at runtime and updates persisted() synchronously.

Garbage collection

The instance cache holds WeakRefs and a FinalizationRegistry tears down doc/endpoint listeners when the edge is collected. You don't need to call destroy() in normal flow — drop your reference and the runtime handles cleanup. destroy() exists for deterministic teardown (tests, hot reloads).

Subpaths

  • ./dom — small bridge helpers for reading handle URLs off DOM nodes (closestHandle, handleFromElement).

Reference transforms (the identity, derive, sum, template, upper/lower/slugify, markdownToHtml, srgbToOklch/oklchToSrgb, accumulator, streamed patterns) live in the edge-handles examples tool — copy and adapt freely, but the SDK doesn't ship them.

Invariants

  1. Handle addressability — every handle URL resolves to a Handle, or a typed Error lands in sourceErrors / targetErrors. Mutators reject malformed URLs at edit time.
  2. Resolution retries — while any resolution errors are outstanding, the next doc tick re-runs resolution (so still-loading peers eventually succeed).
  3. Referential equality (while live)findEdgeHandle(repo, url) returns the same instance for the same (repo, url) pair as long as one is still in memory. After GC, a fresh instance is constructed transparently.
  4. Member-change detection — doc changes that don't touch source/ target (e.g. value-only mirrors) do not re-resolve handles or fire onMembersChange. Value updates that are structurally equal to the cached value don't re-fire onValueChange either.
  5. Cycle safety at write time — a re-entrancy guard inside change() silently drops nested writes so cycles can't blow the stack. (No edit- time cycle rejection; structurally cyclic graphs are allowed.)
  6. Native change semantics — writes delegate to each target's own change; no bespoke merge logic. Fan-out is unconditional (including of undefined) — higher-level semantics belong to consumers.
  7. GC-safe — unreferenced edges are collected; doc/endpoint listeners tear themselves down via the FinalizationRegistry.