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

@shenora/react

v0.9.1

Published

React client for Shenora desktop hosts: correlated invoke/send/subscribe over the WebView2 bridge, typed module services, hooks, and a browser fallback for pure-UI development.

Downloads

1,400

Readme

@shenora/react

React client for Shenora desktop hosts (.NET + WinForms + WebView2). The typed bridge between a React frontend and the Shenora host: correlated invoke with timeouts and structured errors, the event hub host notifications stream into, typed module services, React hooks, and a pluggable transport with a browser fallback so the UI can be developed in a plain browser. Headless by design — no UI components, bring your own design system. Versioned in lockstep with the Shenora.* NuGet packages.

import {
  getBridge, useShenoraEvent, useShenoraQuery, BaseModuleService, createShenoraStore,
} from '@shenora/react';

// Once at startup, after your listeners are attached: it starts notification delivery (anything the
// host buffered arrives in the first batch). Drop zones need no particular ordering against it — the
// host clears them when a new DOCUMENT loads, not on this handshake.
await getBridge().notifyReady();

// a typed service per backend module:
interface NoteRequests { GET_ALL: void; ADD: { title: string } }
class NoteService extends BaseModuleService<NoteRequests> {
  constructor() { super('NOTES'); }
  getAll() { return this.send<Note[]>('GET_ALL'); }
  add(title: string) { return this.send<Note>('ADD', { payload: { title } }); }
}

// in components:
const { data, loading, refetch } = useShenoraQuery<Note[]>('NOTES', 'GET_ALL');
useShenoraEvent<Note>('NOTES', 'ADDED', (note) => refetch());

Failed calls reject with OperationError — a structured code (an i18n key: translate errors.{code}) plus interpolation parameters, never raw host exception text.

Long-running work: post, then read a store

invoke awaits a correlated reply and carries a timeout, and its handler's synchronous segment runs on the host's UI thread — so it is for calls that are quick and UI-thread-safe. Everything else posts and streams results back as events:

const useDeploy = createShenoraStore('DEPLOY', {
  initial: { status: 'idle', lines: [] as string[] },
  // Loaded on the FIRST subscriber, so a component that mounts mid-run isn't empty:
  // events it missed cannot be replayed.
  snapshot: { type: 'GET_STATE', apply: (s, d) => ({ ...s, ...(d as object) }) },
  on: {
    PROGRESS: (s, p: { line: string }) => ({ ...s, lines: [...s.lines, p.line] }),
    ENDED: (s, p: { ok: boolean }) => ({ ...s, status: p.ok ? 'done' : 'failed' }),
  },
  actions: ({ post }) => ({ start: (cfg: unknown) => post('START', { payload: cfg }) }),
});

// any number of components share ONE subscription:
const status = useDeploy((s) => s.status);
useDeploy.actions.start({ env: 'prod' });

Use the store for shared or long-lived state and useShenoraEvent for a one-off reaction in a single component. A failed post has no promise to reject, so it is reported through the bridge's onPostError rather than vanishing — it is bridge-wide, so wire it once at startup (getBridge() takes no options):

configureBridge({ onPostError: (failure) => log.error(failure.module, failure.type, failure.error) });

Tracked operations: a ready-made progress store

For work the host tracks with Shenora.Ipc's operation registry (an IModuleContext.Run/Start route on the C# side), useShenoraOperations() is a createShenoraStore instance built the same way as the example above — no per-feature event wiring needed:

import { useShenoraOperations } from '@shenora/react';

const running = useShenoraOperations((s) => s.running);         // every in-flight operation
const waiting = useShenoraOperations((s) => s.waiting);          // stopped, awaiting a decision — "needs you", one bucket
const importJob = useShenoraOperations((s) => s.byId[jobId]);   // one, by id

useShenoraOperations.actions.cancel(jobId);       // only does anything if the op opted into Cancellable
useShenoraOperations.actions.dismiss(jobId);      // decline a waiting offer — refuses a running one
useShenoraOperations.actions.wait(jobId);         // ASK the host to wait running work — refuses anything not running
useShenoraOperations.actions.clearFinished();

operation.progress is an exported OperationProgress ({ value: number; total?: number; unit?: string }) — import the type rather than re-declaring the shape — the APP's own unit (bytes-of-a-known-total, items-of-a-known-total, an absolute count with no known total, or a genuine percent), never a kit-assumed percentage: total is the denominator when one is known and undefined when there isn't one, and unit is app-defined and uninterpreted, exactly like kind. The kit ships no percent helper — render a ratio only when you have a total:

import type { OperationProgress } from '@shenora/react';

function format(progress?: OperationProgress): string {
  if (!progress) return 'starting…';
  return progress.total
    ? `${Math.round((progress.value / progress.total) * 100)}%`
    : `${progress.value}${progress.unit ? ` ${progress.unit}` : ''}`;   // no known total
}

That division is your own policy, not the kit's, which is why it lives here instead of in src/.

For the two events the store deliberately does NOT subscribe to — OPERATION_RESUME_REQUESTED and OPERATION_WAIT_REQUESTED, which target the OWNING module's service rather than this generic store — the module name and the event vocabulary are exported too, so you match by symbol and stay inside the host↔client mirror the wire tests pin:

import { OperationEventTypes, OperationModuleName, useShenoraEvent } from '@shenora/react';

useShenoraEvent(OperationModuleName, OperationEventTypes.ResumeRequested, (payload) => …);

It snapshots via LIST on first subscribe (so a progress strip that mounts mid-run isn't empty), then folds OPERATION_UPDATED by id — one subscription however many components read it. The client mirrors the host's three bands (design §5A.2): running (Active), waiting (Waiting), and finished (Terminal). All three are derived from byId on every read, never a second copy to keep in sync — waiting is a single-status filter, exactly like running. OperationStatus carries ONE waiting value reached ONE way — the host's IOperation.Wait on a live operation — so there is no sub-case to tell apart. Filtering by your own module/kind is a plain Array.filter over any of them. A waiting operation carries waitReason — an app-defined string, like kind, OPTIONAL on the host side — for your UI to branch on. The host never prunes a waiting entry on its own: it stays until someone resumes, dismisses, or finishes it. resume/dismiss/wait are all fire-and-forget client requests — the host's own IOperation.Wait/Resume (called by whoever owns the operation, hearing OPERATION_WAIT_REQUESTED/OPERATION_RESUME_REQUESTED) is what actually changes the state; asking is not acting. clearFinished/resume/wait do not touch local state themselves at all: the host's OPERATION_REMOVED { operationIds } is the ONE authoritative removal signal the store folds, deleting exactly the named ids — MaxHistory eviction and clearFinished publish it, so a long-lived store's mirror of bounded host history cannot drift from what the host actually did (this replaced two hand-written optimistic local prunes that a past release carried — one of which was this project's only Critical, a resume prune that dropped a still-waiting row). dismiss never needed one, since the host's Dismiss publishes an ordinary terminal snapshot over the wire, the same as a real cancel. Use createOperationsStore({ module, scope }) instead of the default export if your host renamed OperationRegistryOptions.ModuleName or you need a scope-filtered instance (a secondary window, an auxiliary session) — clearFinished forwards that scope so clearing history in one window cannot wipe another's.

Observing the whole stream

useShenoraEvent and createShenoraStore listen for an exact (module, type). When the vocabulary isn't knowable up front — plug-in-contributed events, a diagnostics tap, or an adoption shim keeping a legacy "every host message" handler alive while features migrate one at a time — subscribe broadly instead. Both mirror the host's IEventBus and return an unsubscribe:

const off = eventBus.subscribeToAll((event) => log.debug(event.module, event.type, event.payload));
eventBus.subscribeToModule('DEPLOY', (event) => audit(event));   // every type from one module

Delivery is narrowest-first — exact pair, then module, then catch-all — so a broad observer never runs ahead of the feature code it is observing. Prefer subscribe when you know the pair: a catch-all wakes for every event on the bus.

Pure-UI development in a plain browser: pass a fallback to configureBridge (gated behind import.meta.env.DEV) to answer requests with canned data. Other shells (WebSocket, mobile/Capacitor) implement the small ShenoraTransport seam and speak the same envelopes. For CDP-driven testing, installDevInterceptor() records IPC/event traffic into ring buffers and exposes window.__shenora.call()/waitEvent().

MIT © Jiarong Gu