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

@deepfates/lync

v0.3.0

Published

The lync format: append-only JSONL event logs merged by set union. Parsing, stores, views, looms, live sync, loom client, indexes, the sync relay, and the lync command. Zero dependencies.

Readme

lync

Most software forgets. Edit a document and yesterday's version is gone. Write with an AI that offers three options and the two you do not pick vanish. Let a tool merge two people's edits and you get one result with no memory of who did what or what was dropped. The story of how a thing came to be is thrown away at every step.

lync keeps it. A .lync file is a list of events, one per line, in plain text. Each line records one thing that happened. Someone wrote a sentence, marked a version as good, or chose one branch over another. You never edit a line. To change something you add a new line that points at the old one and says what changed. The old line stays.

That one rule, only ever add and never edit, is where everything good comes from. You can always see the whole history, every version and every branch you did not take. Merging two copies is safe and boring, because you keep every event from both and there are no edits to fight over. If the same event ever shows up two different ways, lync keeps both and says so out loud instead of quietly picking one. And because the file is plain text with a written-down spec, you can still read it in twenty years, on a computer that has never heard of this software.

A lync file is more than a transcript, because it keeps the branches and the choices. From the same file you can make a readable document to hand someone, or training data built from the exact versions you marked as good.

@deepfates/lync is the reference implementation. One package, zero runtime dependencies, holding the parser, the event stores, the computed views, the loom API, live sync, the lync command, and the sync relay. The format is the durable center. Everything else is a tool that reads and writes it.

The Format

The normative specification is FORMAT.md. It is self-contained: no code in this repository is required to implement it.

The short version:

  • A .lync file is UTF-8 JSONL, no BOM, one event object per LF-terminated line.
  • The required event fields are v, id, kind, at, author, parents, and payload.
  • author.actor is the content producer; author.via is the app or runtime; converters use imported_by and preserve the original actor.
  • Events are immutable. Correction, judgment, selection, and retraction are new events that point at prior events.
  • Merge is union by event id. Same id plus same body bytes is one event seen twice; same id plus different body bytes is a surfaced conflict variant and is excluded from ordinary views.
  • Stored line metadata may splice digest and sig at the end of the line. digest and sig are reserved top-level body names; payloads may use those names freely.
  • Views are computed. This package ships branch tree, transcript, memory, and leaderboard helpers.

Every line has the same envelope:

{"v":1,"id":"root","kind":"notes/text","at":"2026-07-06T04:12:31Z","author":{"actor":"deepfates","via":"[email protected]"},"parents":[],"payload":{"text":"Once..."}}

That line can be copied to another file, merged back later, verified byte for byte, and read by software that has never heard of notes/text. Unknown kinds are carried and traversed; meaning belongs to pacts layered above the format — see pacts/import.md (imports are transcription: deterministic ids, provenance preserved, zero silent drops) and pacts/export.md (exports are projections of the event log, including training data).

Conformance Vectors

The format is meant to be implemented in other languages, and the test vectors are the product that makes a port checkable: test/vectors/v0. Each case is a directory holding a raw input file (input.lync, or a.lync + b.lync for the merge case) and an expected.json with the required classification of every physical line — accepted, nonconforming, garbage, damaged, conflict-variant — plus union ids, pending parents, and graph diagnostics. The thirteen cases cover valid events, splice anchoring, damaged digests, garbage classes, same-id conflicts and duplicates, graph obstacles, critical suppression, spelling-versus-value equality, marked/at semantics, merge union, carried nonconforming lines, and invalid signature splices.

A new implementation ports the vector suite first, this package's test suite second, and its design never. test/vectors/v0/README.md documents the expected-output schema; generate.py regenerates digests deterministically.

The Library

npm install @deepfates/lync

Runs in Node (>=22) and the browser. No dependencies.

Parse, union, view

import { parseLyncFiles } from "@deepfates/lync/events";
import { lyncBranchTreeView, lyncTranscriptView } from "@deepfates/lync/views";

const bytes = new TextEncoder().encode(
  '{"v":1,"id":"root","kind":"notes/text","at":"2026-07-06T04:12:31Z","author":{"actor":"you"},"parents":[],"payload":{"text":"Once..."}}\n',
);

const parsed = parseLyncFiles([{ file: "story.lync", bytes }]);
console.log(parsed.lines[0].class); // "accepted"
console.log(lyncBranchTreeView(parsed).roots);
console.log(lyncTranscriptView(parsed, "root").entries.map((entry) => entry.id));

parseLyncFiles classifies every physical line and keeps the original bytes — accepted events, nonconforming-but-carried lines, damaged lines, garbage, and conflict variants (same id, different bytes) are all preserved and reported, never silently dropped. exportCarriedLyncBytes(parsed) re-emits the carried bytes. LyncUnion performs the same union incrementally and can buffer children until their first missing parent arrives.

Stores and looms

Event stores share one contract over memory, file, and IndexedDB backends. The loom API gives programs turns and threads instead of raw events, on top of any store:

import { createLyncLooms } from "@deepfates/lync/looms";
import { createMemoryEventStore } from "@deepfates/lync/memory-log";

const looms = createLyncLooms({
  store: createMemoryEventStore(),
  author: { actor: "you", via: "[email protected]" },
});

const info = await looms.create({ title: "Story" });
const loom = await looms.open(info.id);
const first = await loom.appendTurn(null, { text: "Once..." });
await loom.appendTurn(first.id, { text: "Then..." });

The store API accepts raw lines through union(line) and structured event bodies through append(event). It reports conflicts, pending parents, garbage, and accepted events without making file order meaningful.

Indexes

An index tracks a collection of looms — upsert entries, subscribe to changes:

import { loomRef } from "@deepfates/lync";
import { createMemoryLoomIndexes } from "@deepfates/lync/indexes/memory";

const indexes = createMemoryLoomIndexes();
const index = await indexes.create({ title: "My looms" });

index.subscribe((event) => console.log("index changed:", event.type));
await index.addLoom(loomRef("loom-1"), { title: "Story" });

console.log((await index.entries()).map((entry) => entry.title));

Entries carry a loom reference, optional title/kind/meta, and timestamps. export()/import() round-trip a whole index as a snapshot.

The loom client

One object that pairs looms with an index and resolves loom/turn/thread/index references to and from URLs:

import { createLyncLooms } from "@deepfates/lync/looms";
import { createMemoryEventStore } from "@deepfates/lync/memory-log";
import { createMemoryLoomIndexes } from "@deepfates/lync/indexes/memory";
import { createLoomClient } from "@deepfates/lync/client";

const client = createLoomClient({
  looms: createLyncLooms({ store: createMemoryEventStore(), author: { actor: "you" } }),
  indexes: createMemoryLoomIndexes(),
});

const info = await client.looms.create({ title: "Story" });
const ref = client.references.loom(info.id);

// Round-trip a reference through a shareable URL (?ref=...).
// In the browser, pass `window.location` instead of a URL object.
const url = client.references.toUrl(ref, new URL("https://example.com/story"));
const opened = await client.openReference(client.references.fromUrl(new URL(url)));
console.log(opened.kind); // "loom" — opened.loom is ready to appendTurn

@deepfates/lync/client/testing ships createTestLoomClient, a fully in-memory client for tests and embedded experiments — deterministic when you pass createId and now.

Live sync inside an app

The sync protocol runs in a browser or Node app with no CLI. Wrap any event store in createSyncedStore; looms and indexes built over it update live as collaborators append, because they already recompute through the store's subscribe:

import { createMemoryEventStore } from "@deepfates/lync/memory-log";
import { createLyncLooms } from "@deepfates/lync/looms";
import { createSyncedStore, createWebSocketTransport } from "@deepfates/lync/synced-store";

const transport = createWebSocketTransport("wss://host/lync");
const store = createSyncedStore(createMemoryEventStore(), transport, {
  onStatus: (s) => console.log("sync:", s.connection, "live:", s.liveRoots),
});
const looms = createLyncLooms({ store, author: { actor: "alice" } });

const loom = await looms.open(loomId);
loom.subscribe(() => render(loom)); // fires on local AND remote turns
await loom.appendTurn(parentId, { text: "typed live" });

Local appends are pushed to the relay; remote lines are ingested through the same union path and surface reactively. Offline appends queue and flush on reconnect; the store re-subscribes automatically. The transport is an interface — pass your own for tests or a non-WebSocket carrier. The client side uses the platform's built-in WebSocket: no dependency, in the browser or in Node.

Subpath exports

  • @deepfates/lync/events — line parsing, carried-byte export, incremental union
  • @deepfates/lync/store — the event-store contract and serialization
  • @deepfates/lync/memory-log, @deepfates/lync/file-log, @deepfates/lync/idb-log — stores (file-log is node-only; it keeps node:fs off the browser path)
  • @deepfates/lync/views — branch tree, transcript, memory, leaderboard
  • @deepfates/lync/looms — the loom/turn API
  • @deepfates/lync/references — loom/turn/thread/index references and URLs
  • @deepfates/lync/synced-store — live sync decorator and WebSocket transport
  • @deepfates/lync/sync-protocol — the five sync frames, encode/decode
  • @deepfates/lync/uuid — zero-dep UUIDv7 for event ids
  • @deepfates/lync/indexes, @deepfates/lync/indexes/entries, @deepfates/lync/indexes/memory, @deepfates/lync/indexes/types — loom indexes
  • @deepfates/lync/client, @deepfates/lync/client/testing, @deepfates/lync/client/types — the loom client
  • @deepfates/lync/relay — the sync relay (see The Relay)

The Command

The package installs a lync bin with seven verbs: init, append, verify, merge, view, serve, and sync.

npm install -g @deepfates/lync
lync init story.lync
printf '%s\n' '{"kind":"notes/text","author":{"actor":"you"},"payload":{"text":"Once..."}}' | lync append story.lync
lync verify story.lync
lync view story.lync --as transcript
printf '%s\n' '{"kind":"notes/text","author":{"actor":"friend"},"payload":{"text":"Then..."}}' | lync append other.lync
lync merge story.lync other.lync -o merged.lync
lync view merged.lync --as tree

append fills the envelope for you: a UUIDv7 id, the current timestamp, v, and parents default in; anything you supply is kept. verify reports what every physical line is — accepted, nonconforming, damaged, garbage, or conflict variant — and never drops bytes; it exits 0 only when every line is accepted. view renders transcript or tree.

Any lync file can converge with any other copy through a relay:

lync serve ./rooms --port 8787              # the relay: one append-only file per root
lync sync story.lync ws://host:8787         # one-shot: push what it lacks, pull what you lack
lync sync story.lync ws://host:8787 --follow  # stay live until Ctrl-C

lync sync uses Node's built-in WebSocket — no install beyond the package. lync serve runs the relay and needs ws present (npm install ws); see below.

The Relay

The relay is deliberately dumb. Events are immutable and merge is union by id, so the protocol has no merge logic: five JSON frames (sub, ev, live, presence, err) that move canonical line bytes. The server never parses a line beyond extracting its id, stores each root as a plain .lync file you can read with any lync tool, and echoes accepted events to every subscriber — echoes are duplicate no-ops under union. seq is a per-root arrival counter used as a resume cursor (<file>.sync.json), so an offline client reconnects exactly where it left off.

Running a relay is the one thing that needs a WebSocket server, and Node does not ship one — so the relay acquires ws lazily at the moment you construct it. @deepfates/lync declares no dependency on ws at all: install it yourself next to your server (npm install ws), and everything else in the package works without it. If you bundle a server that runs the relay, mark ws as external — the acquisition is a dynamic require that bundlers cannot see through.

Standalone:

import { startLyncServe } from "@deepfates/lync/relay";

const server = await startLyncServe({ dir: "./rooms", port: 8787 });
console.log("relay on", server.port);

// Look inside a running relay — read-only, mutates nothing. Each record is
// { root, generation, seq, subscribers, pendingUnpersisted }, where
// pendingUnpersisted is the durability lag: lines in memory but not yet on disk.
for (const room of server.status()) {
  console.log(room.root, "seq", room.seq, "subs", room.subscribers, "lag", room.pendingUnpersisted);
}
// later: await server.close();

On an existing HTTP server:

import { createServer } from "node:http";
import { attachLyncServer } from "@deepfates/lync/relay";

const httpServer = createServer(app);
const lync = attachLyncServer(httpServer, {
  storageDir: "./rooms",
  path: "/lync",              // default
  keepAliveInterval: 30_000,  // optional: ping through idle proxies
  maxConnections: 500,        // optional
  authenticate: (req) => checkSession(req), // optional, after token check
});
httpServer.listen(3000);

For full control, createLyncRelay gives you handleUpgrade to call from your own upgrade listener. All three (createLyncRelay, startLyncServe, attachLyncServer) expose status() — a read-only snapshot of every live room's seq, subscriber count, and pending-unpersisted durability lag.

Guarantees: same-id-different-bytes is never resolved — both variants are kept (a .conflicts sidecar) and both sides are told loudly. Persist failures are broadcast, never swallowed. A truncated final line after a crash is sealed and surfaced as damaged, never eaten. Presence frames are relayed, never stored. --token T (or token in the API) requires Authorization: Bearer T on every upgrade.

The relay is a tool shipped beside the format, not part of it: FORMAT.md deliberately excludes sync protocols, and any transport that moves canonical line bytes and unions by id converges the same files without this relay.

Development

pnpm install
pnpm build
node bin/lync.js --help
pnpm test
pnpm verify

pnpm verify runs the path guard, tests, build + typecheck, and executes every fenced example in this README against the built package (pnpm check:examples). README examples are contract: a block runs as-written unless an <!-- example: fragment --> comment above it declares why it can't run alone.

For a fresh clone, pnpm install && pnpm build is the supported setup sequence. After that, node bin/lync.js --help prints the CLI help. scripts/fresh-clone-smoke.sh verifies that sequence in a temporary clone and runs the CLI story path: init, append, view, concatenate, merge, and verify.