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

@syncropel/sdk

v0.2.0

Published

Integration SDK for the Syncropel protocol — TypeScript. Async client, grammar enforcement, canonical references, fail-open transport.

Downloads

298

Readme

@syncropel/sdk

TypeScript SDK for the Syncropel protocol — emit content-addressed records, query threads, validate grammar, fail open on transport errors.

import { Client, Identity, Ref } from "@syncropel/sdk";

const client = new Client({
  endpoint: "http://localhost:9100",
  identity: Identity.static("did:example:my-app"),
});

const result = await client.emit({
  act: "PUT",
  kind: "music.catalog.track",
  body: { title: "Glow", artists: ["Zonke"] },
  refs: [Ref.musicTrack({ isrc: "USJI19810404" })],
  thread: "music.library",
});

console.log(result.success, result.recordId);

One call validates the grammar, builds the record envelope, retries on transient errors, and resolves cleanly on transport failures so a flaky network never crashes your handler.


Features

  • Async clientemit, query, queryThread, intend, fulfill, plus reserved-kind helpers
  • Grammar enforcementbody.kind validated before any network call
  • Canonical references — 11 community ref constructors (@music.track, @code.file, @social.person, …) for cross-publisher correlation
  • Fail-open transport — every emit resolves to a result; transport errors never reject the promise
  • Identity-aware — every record signed with the configured DID
  • Zero runtime dependencies — uses the platform fetch
  • Universal runtime — Node 18+, Deno, Bun, Cloudflare Workers, modern browsers
  • In-memory MockKernel for tests at @syncropel/sdk/testing — no server needed

Install

npm install @syncropel/sdk

| Runtime | Minimum | |---|---| | Node.js | 18 (built-in fetch + AbortController) | | Deno | 1.30+ | | Bun | 1.0+ | | Cloudflare Workers | anywhere fetch is available | | Modern browsers | anywhere fetch is available |

Ships compiled JavaScript + TypeScript declarations. ESM only.


Quickstart

Spin up a local Syncropel server, then emit your first record:

// hello.ts
import { Client, Identity, Ref } from "@syncropel/sdk";

const client = new Client({
  endpoint: "http://localhost:9100",
  identity: Identity.static("did:example:me"),
});

const result = await client.emit({
  act: "PUT",
  kind: "music.catalog.track",
  body: { title: "Glow" },
  refs: [Ref.musicTrack({ isrc: "USJI19810404" })],
  thread: "music.library",
});
console.log("emitted:", result.recordId);

const records = await client.queryThread({ thread: "music.library" });
console.log(`thread has ${records.length} record(s)`);
npx tsx hello.ts

See syncropel.com for installing the Syncropel server (spl) and a full hosted-vs-local guide.


Concepts in 60 seconds

  • Record — the immutable, content-addressed unit. 8 fields; the SDK builds the envelope for you.
  • Kindbody.kind names what the record is about, e.g. music.catalog.track. Follows a strict grammar: scope.category.entity[.version].
  • Thread — a logical conversation / workflow. Records share a thread when they're part of the same activity.
  • Actor — who emitted the record, expressed as a DID.
  • Ref — a canonical pointer to a real-world entity (a song, a file, a person…) so records about the same thing correlate across apps.

API reference

Client

| Method | Purpose | |---|---| | emit({ act, kind, body, thread, refs?, parents?, dataType?, clock? }) | Primary emit. Returns Promise<EmitResult>. Grammar errors throw; transport errors resolve to success: false. | | intend({ goal, thread?, parents?, kind?, body?, clock? }) | Open a thread. Emits INTEND. Generates a random thread id if none supplied; returns it on result.thread. | | fulfill({ thread, summary, fulfills?, kind?, body?, parents?, clock? }) | Close a thread. Emits KNOW with body.summary + optional body.fulfills (id or list of ids). | | emitCorrection({ corrects, revisedFields, reason, thread, ... }) | Reserved-kind helper for core.correction — supersede earlier records with revised values. | | emitErasure({ erases, reason, thread, ... }) | Reserved-kind helper for core.erasure — mark records as erased (e.g. for compliance). | | emitAlias({ oldKind, newKind, reason, thread, ... }) | Reserved-kind helper for core.alias — declare that one kind supersedes another. | | emitScopeTransfer({ scope, fromPublisher, toPublisher, reason, thread, ... }) | Reserved-kind helper for core.scope_transfer — record ownership of a scope changing publisher. | | emitScopeClaim({ scope, governancePolicy, thread, ... }) | Reserved-kind helper for core.scope_claim — claim a scope with a governance policy. | | queryThread({ thread, limit?, since? }) | All records in a thread. Fail-open. | | query({ kind?, actor?, thread?, since?, limit?, where? }) | Filtered query. At least one of kind / actor / thread required. | | health() | Server health probe. Fail-open. |

Constructor options: endpoint (string), identity (required), timeoutMs (default 30000), maxRetries (default 2), backoffMs (default 250), onEmit (observability hook), apiKey (sets x-api-key header), fetch (custom transport — useful for tests, proxies, observability wrappers).

Identity

| Form | Status | |---|---| | Identity.static(did) | Available | | Identity.key(pathOrBytes) | Planned — throws if called today | | Identity.federated(...) | Planned — throws if called today |

Ref — canonical reference constructors

| Constructor | Canonical | Schemes | |---|---|---| | Ref.musicTrack({ isrc? / spotifyId? / appleId? }) | @music.track | isrc:, spotify:, apple: | | Ref.codeFile({ repo? / gitUrl?, path }) | @code.file | repo:, git: | | Ref.opsIncident({ pagerduty? / linear? / url? }) | @ops.incident | pd:, linear:, url: | | Ref.calEvent({ uid }) | @cal.event | uid: | | Ref.socialPerson({ did? / email? / handle?+name? }) | @social.person | did:, email:, handle: | | Ref.mediaPhoto({ sha256? / url? }) | @media.photo | sha256:, url: | | Ref.mediaVideo({ youtube? / vimeo? / sha256? }) | @media.video | youtube:, vimeo:, sha256: | | Ref.docText({ doi? / url? / platformId? }) | @doc.text | doi:, url:, platform: | | Ref.finTransaction({ stripe? / plaid? / iso20022? }) | @fin.transaction | stripe:, plaid:, iso20022: | | Ref.researchPaper({ doi? / arxiv? / s2? }) | @research.paper | doi:, arxiv:, s2: | | Ref.coreThread({ id }) | @core.thread | id: |

Pass a list as refs: to emit(); the SDK merges them into body._refs. Two records anywhere in the network sharing the same canonical ref are joinable.

EmitResult

interface EmitResult {
  success: boolean;
  recordId?: string;
  clock?: number;
  error?: string;
  retried: number;
  kind: string;
  act: string;
  thread: string;
}

SyncropelKindError

Thrown synchronously from validateKind() and every emit* method when body.kind violates the grammar. Subclasses Error; instanceof works across bundlers.


Fail-open contract

emit() never rejects its promise on network errors, 4xx, 5xx, or timeouts. Every call resolves to an EmitResult and your code inspects .success:

const result = await client.emit({ act: "PUT", kind: "music.catalog.track", body: {}, thread: "t" });
if (!result.success) {
  // Transient failure — log and keep going.
  console.warn(`emit failed: ${result.error} (retried ${result.retried})`);
}

A flaky network can't bring down your handler. You decide whether to drop the failure, retry, or escalate.

Grammar errors are different. SyncropelKindError always throws — it indicates programmer error (an invalid body.kind), and no retry will fix it. Failing loud at development time is correct.

Observability hook

const client = new Client({
  endpoint: "...",
  identity: Identity.static("..."),
  onEmit: (result) => {
    metrics.increment("syncropel.emit", { tags: { success: result.success } });
  },
});

Fires on every success and failure. Hook exceptions are swallowed with a console.warn — a broken metrics pipeline can't break your emit path.


Grammar reference

Every record's body.kind follows the scope.category.entity[.version] grammar. The SDK validates at emit time:

| Kind | Valid? | |---|---| | music.catalog.track | ✓ — publisher scope, 3 segments | | music.catalog.track.v2 | ✓ — versioned | | @music.track | ✓ — community canonical (2-segment allowed for @<community> and core scopes) | | core.alias | ✓ — reserved core primitive | | dj.track_imported | ✗ — 2-segment publisher scope forbidden | | Music.Catalog.Track | ✗ — uppercase forbidden | | music.catalog.track.v2.foo | ✗ — 5 segments |

When a canonical ref exists for your domain, use it:

refs: [Ref.musicTrack({ isrc: "USJI19810404" })]

This makes your record correlatable with every other music record across publishers. Without refs, nothing breaks — you lose cross-app correlation.


Testing your adapter

The SDK ships a canonical mock at @syncropel/sdk/testing — exercise your adapter end-to-end without running a server:

import { Client, Identity, Ref } from "@syncropel/sdk";
import { MockKernel } from "@syncropel/sdk/testing";

test("my adapter imports tracks", async () => {
  const kernel = new MockKernel();
  const client = new Client({
    endpoint: "http://mock",
    identity: Identity.static("did:test:me"),
    fetch: kernel.fetch,
  });

  await myAdapter.importLibrary(client, tracks);

  const emitted = kernel.recordsByKind("music.catalog.track");
  assert.equal(emitted.length, tracks.length);
  for (const r of emitted) {
    const refs = (r.body as any)._refs as Array<{ kind: string }>;
    assert.equal(refs[0].kind, "@music.track");
  }
});

Record IDs produced by MockKernel use the same SHA-256-of-canonical-JSON rule as the real server, so result.recordId matches what you'd see in production. The mock also enforces DuplicateClock on (thread, actor, clock).

Failure injection for fail-open coverage: kernel.failNextPostCall(n) and kernel.failNextGetCall(n).


Versioning + stability

Independent semver. Patch releases ship freely; minor versions can change public behaviour with a CHANGELOG note.

| SDK version | Highlights | |---|---| | 0.1.x | Current. Async Client, grammar enforcement, canonical refs, reserved-kind helpers, MockKernel, fail-open transport. | | 0.2.x (planned) | Subscribe / SSE, batch emit, signing-key identity. | | 1.0.x (planned) | Federated identity, typed kind parameter via literal-union autocomplete. |


License

Apache-2.0.