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

@hussmartinez/sdk-core

v0.4.4

Published

Headless runtime shared by the @hussmartinez SDK family.

Readme

@hussmartinez/sdk-core

npm version of @hussmartinez/sdk-core Verify workflow status on main License: Apache-2.0

Headless runtime shared by the @hussmartinez SDK family: worker messaging, errors, session primitives, the tool registry, the custom-element base, file policy and the messages helper.

The package is ESM only, has no dependencies, and does nothing on import, so it's safe to load on a server (ADR 0004).

Status: 0.x, so the API may still change between minor versions. Errors, worker messaging, messages and the file policy are available; the session, tools and custom-element base come later.

Errors

Every SDK throws one subclass of SdkError, with a stable code, serialisable details and a standard cause (ADR 0008). Codes are SCREAMING_SNAKE_CASE and never change once released. User-facing text is looked up by code in the messages catalogue, never taken from message (ADR 0014).

import { isSdkError, SdkError } from "@hussmartinez/sdk-core";

type PdfErrorCode = "FILE_TOO_LARGE" | "PDF_ENCRYPTED";

export class PdfError extends SdkError<PdfErrorCode> {
  override readonly name = "PdfError";
}

throw new PdfError("FILE_TOO_LARGE", "The file exceeds the size limit.", {
  details: { limit: 52_428_800, size: 73_400_320 },
});
  • Subclasses narrow code to their own union and set name explicitly, so it survives minification. They keep the base constructor's signature (code, message, options), so errors can be rebuilt after crossing a worker boundary.
  • details accept JSON-compatible values only: strings, finite numbers, booleans, null, arrays and plain objects.
  • isSdkError(value, code?) narrows a caught value, optionally to one code. It recognises errors from other realms and from duplicate copies of sdk-core, where instanceof fails, through a shared brand. Errors that merely look similar, such as Node errors with a string code, aren't accepted.
  • A second copy of sdk-core in use on the page gets a warning, once, in development, naming both versions, because instanceof SdkError fails across copies (ADR 0047). Anything that evaluates sdk-core's module twice counts too, even with one install: test suites that call vi.resetModules, and some dev-server reloads, may warn "2 copies (0.4.1, 0.4.1)".

Across a worker boundary

structuredClone and postMessage drop an error's code and details. Serialise errors explicitly instead:

// In the worker
port.postMessage({ error: serializeError(error) });

// On the page
const error = deserializeError(message.error, { errorClasses: { PdfError } });
error instanceof PdfError; // true
  • serializeError(error, options?) turns any thrown value into plain data and never throws for the value itself. Values that aren't SdkErrors get the code UNKNOWN. Their original name goes in details.originalName, or their typeof in details.thrownType. Stack traces are left out unless you pass includeStack: true, so they don't cross the boundary in production.
  • deserializeError(data, options?) rebuilds the error with its registered class, or as SdkError keeping its name. It treats the data as untrusted: invalid input becomes an UNKNOWN error, details are sanitised again, and class names are only looked up as own keys of errorClasses.
  • Both follow the cause chain up to maxDepth levels (8 by default) and stop at cycles.

Messages

Every visible string comes from a typed English catalogue that hosts can override, in whole or in part, for example from a translator's JSON file (ADR 0014, ADR 0019).

// src/messages/en.ts
export const en = defineMessages({
  ...sdkCoreMessages,
  "toolbar.rotate": "Rotate",
  "result.saved": "Saved {size, fileSize}",
  "pages.selected": plural("count", { one: "{count} page selected", other: "{count} pages selected" }),
  "errors.FILE_TOO_LARGE": "This file is {actual, fileSize}. The limit is {maximum, fileSize}.",
});

const messages = createMessages(en, { locale: props.locale, overrides: props.messages });
messages.t("result.saved", { size: 1_300_000 }); // "Saved 1.3 MB"
messages.t("pages.selected", { count: 3 });       // "3 pages selected"
messages.error(error);                            // text for error.code, never error.message
  • Placeholders: {name} takes a string or a number. {name, number}, {name, fileSize} (decimal units), {name, duration} (milliseconds, shown as "3 hr, 5 min"), {name, megapixels} (pixels, shown in millions, such as "24.2"), {name, date} and {name, time} are formatted with Intl for the locale. Unknown keys and missing or wrongly typed parameters are type errors.
  • Plurals: plain objects, { "$plural": "count", "one": "…", "other": "…" }, whose form is chosen with Intl.PluralRules. Translations may add forms, such as few and many.
  • Errors: messages.error() looks up errors.<CODE> with the error's details as parameters, and falls back to errors.UNKNOWN. Include sdkCoreMessages for sdk-core's own codes.
  • Plain text only: the output is a string, never markup, so render it as text.
  • Locale: it defaults to "en". Always pass the host's locale, and never read it from the browser, because server and client must render the same text.
  • Overrides are checked at runtime too. Unknown keys are ignored, and malformed messages fall back to English, with warnings in development. t() never throws.

File policy

Every SDK enforces limits on hostile input before or while decoding it, and hosts configure them through filePolicy (ADR 0012, ADR 0020).

export const pdfLimits = defineLimits(
  { ...coreLimits, maxPages: { code: "TOO_MANY_PAGES", unit: "count", default: 2_000 } },
  { errorClass: PdfError },
);

const policy = pdfLimits.resolve(props.filePolicy); // such as { maxFileSize: 300_000_000 }
if (checkFile(policy, file) === "warning") {
  announce(messages.t("warnings.maxFileSize", { actual: file.size }));
}
policy.check("maxPages", pageCount);                 // throws PdfError("TOO_MANY_PAGES") above 2,000
await withProcessingTime(policy, (signal) => client.call("open", { bytes }, { signal }), { signal });

| Core limit | Code | Default | Warning from | |---|---|---|---| | maxFileSize | FILE_TOO_LARGE | 100 MB | 50 MB | | maxFiles | TOO_MANY_FILES | 20 | — | | maxDecodedSize | DECODED_SIZE_TOO_LARGE | 500 MB | — | | maxProcessingTime | PROCESSING_TIME_EXCEEDED | 120 s | — |

  • check() throws above the maximum, with details: { limit, maximum, actual, unit }, and returns "warning" from the warning threshold.
  • Helpers:
    • checkFile and checkFiles check before reading;
    • createDecodedSizeBudget stops a decompression bomb mid-stream, in the worker;
    • withProcessingTime runs one operation, such as a worker call, under the time limit, which then rejects with PROCESSING_TIME_EXCEEDED. It cleans up when the operation settles, adds no listeners to the caller's signal, and rejects with UNSUPPORTED_ENVIRONMENT where AbortSignal.any is missing.
  • Settings: each is a number or { maximum, warning }. Infinity disables a limit, which removes a protection, or disables a warning. Unknown limit names throw, so a typo can't leave a default in place.
  • Text: violations have text under errors.<CODE> and warnings under warnings.<limit> in sdkCoreMessages.

Workers

createWorkerClient and exposeWorker are a small, typed, versioned RPC layer for SDK workers (ADR 0011, ADR 0018). One interface describes the methods, and both sides are checked against it:

// contract.ts
export interface PdfWorker {
  render: { input: { bytes: ArrayBuffer; page: number }; output: ImageBitmap; progress: number };
}
// pdf.worker.ts: call exposeWorker first, and load WebAssembly lazily
exposeWorker<PdfWorker>({
  async render({ bytes, page }, { signal, progress }) {
    const engine = await loadEngine();
    return engine.render(bytes, page, { signal, onProgress: progress });
  },
});
// On the page
const client = createWorkerClient<PdfWorker>({
  createWorker: () => new Worker(new URL("./pdf.worker.js", import.meta.url), { type: "module" }),
  errorClasses: { PdfError },
});

const bitmap = await client.call("render", { bytes, page: 1 }, { signal, onProgress });
  • Lifetimes: the worker starts on the first call or start(), stops after idleTimeout without calls (30 s by default), and is recreated when needed. destroy() stops it for good.
  • Start-up: exposeWorker must run before any heavy initialisation. The client waits up to startTimeout (10 s) for the worker to announce itself, and that should measure start-up, not WebAssembly compilation.
  • Cancellation: an aborted call rejects at once. An SdkError reason, such as the file policy's PROCESSING_TIME_EXCEEDED, is passed through unchanged. Any other reason gives code ABORTED, or TIMEOUT when the signal timed out, with the reason as cause. These aren't AbortErrors. The handler's signal is aborted. If the call is still running after cancelTimeout (1 s), the worker is terminated, and other calls on it fail with WORKER_TERMINATED. Nothing is retried automatically.
  • Buffers: inputs are copied unless you pass transfer: true, which detaches them. Results are transferred, so handlers must return buffers they no longer need. Views into part of a buffer, such as WebAssembly memory, are always copied, and only their bytes. An input or result that can't be cloned, such as one containing a function, fails that call with WORKER_DATA_NOT_CLONEABLE.
  • Errors: handler errors arrive rebuilt with deserializeError, so PdfError codes survive. Errors that escape the handlers, and failures to start, arrive as WORKER_CRASHED, WORKER_START_FAILED or WORKER_PROTOCOL_MISMATCH.
  • Crash reporting: pass onCrash to hear about those three failures even when no call is pending. SDKs should forward it to their host's onEvent or error reporting. Expected shutdowns, such as the idle timeout, destroy() or a stuck call being stopped, don't trigger it.

Timelines

Pure helpers for a sequence of items, such as the clips of an audio or video timeline, over whole units like samples or frames, never seconds (ADR 0042):

import { locate, sequenceOffsets, snap } from "@hussmartinez/sdk-core";

const offsets = sequenceOffsets([48_000, 24_000]); // [0, 48000, 72000]
locate(offsets, 48_000); // { index: 1, within: 0 }
locate(offsets, 72_000); // undefined: the total length is past the end
snap(47_990, offsets, Math.round(8 * unitsPerPixel)); // 48000
  • sequenceOffsets(lengths) returns every boundary, the starts and then the total, frozen. Lengths must be positive safe integers.
  • locate(offsets, position) finds the item under a position with a binary search. A position on a boundary belongs to the item that starts there, and the total length and beyond return undefined.
  • snap(position, targets, tolerance) returns the nearest target within the tolerance, the earlier one on a tie, or the position itself. The tolerance is in the same units, so a UI converts its pixel tolerance with its zoom.

Invalid units throw RangeError.

Custom elements

@hussmartinez/sdk-core/element builds an SDK's <hm-<kind>-editor> element from a definition and the SDK's own mount function, so sdk-core imports no framework (ADR 0040). Importing it touches no browser API.

import { createEditorElement } from "@hussmartinez/sdk-core/element";

export const definePdfEditorElement = createEditorElement<PdfEditorProps, PdfEditorMethods>({
  kind: "pdf",
  attributes: { theme: "theme", locale: "locale" },
  properties: ["file", "filePolicy"],
  methods: ["undo", "redo"],
  events: ["change", "error"],
  mount: (host, props, { dispatch }) => mountPdfEditor(host, props, dispatch),
});
  • Registration: the returned define(tagName?) registers hm-<kind>-editor, or another name, and returns its class. On a server it throws UNSUPPORTED_ENVIRONMENT.
  • Light DOM: mount renders into the element itself, so the host's stylesheets apply as they do in React.
  • Lifecycle:
    • properties set before the element was upgraded are applied;
    • changes cause one update per microtask;
    • the editor mounts on connect, and unmounts after a removal but not after a move.
  • Methods and events: methods throw InvalidStateError while the element is disconnected. dispatch(name, detail) fires hm-<kind>-editor-<name>.

Standalone scripts use adoptSharedStyleSheet({ name, version, cssText }) to adopt ui's styles and their own once per page, shared with other standalone scripts on the same ui minor.

API report

The public API is recorded in api/sdk-core.api.md and api/sdk-core-element.api.md by api-extractor (ADR 0015). pnpm check:api, part of verify and verify:fast, fails when the API changes without the report. After an intended change, run pnpm api:update and commit the updated report with the change.

Tests

pnpm test runs the unit tests in Node. pnpm test:browser runs the browser tests, with real workers, in Chromium. pnpm test:browser:all runs them in Chromium, Firefox and WebKit, as CI does. Install the browsers once with pnpm exec playwright install.

pnpm check:size checks the size budgets in .size-limit.js: the brotli size of the . and ./element entries, each with the shared code it loads.

License

Apache-2.0