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

@sharering/me-module-sdk

v0.1.2

Published

TypeScript SDK for miniapps in a React Native WebView: a typed postMessage client for the miniapp, and the matching event router for the superapp that hosts it. Ships the ShareRing Me event catalog; the catalog is swappable.

Readme

@sharering/me-module-sdk

A typed postMessage bridge between a React Native superapp and the miniapps it runs in a WebView.

One package, two sides, one contract:

  • the miniapp imports the root entry (plus /react or /vue) and calls the host app;
  • the host app imports /host and answers those calls.

Both are typed from the same event contract, so a handler returning the wrong shape is a compile error rather than a bug found on a handset.

The events themselves are data, not code. events/events.json is the single source of truth, and everything typed — the event map, the payload interfaces, the per-event wrappers, the runtime behaviour tables, the schemas the host validates against — is generated from it. This package ships the ShareRing Me event catalog (wallet, vault, NFT, navigation, passes) as one implementation. A different superapp replaces that file with its own events and gets the same generated surface. What is not per-app is everything below the event names: the envelope, correlation, coalescing, timeouts, caching, error taxonomy and routing. That is the part worth not writing twice.

npm install @sharering/me-module-sdk

MIT licensed. No runtime dependencies.


Features

None of this needs configuring — it is what send() already does. Each entry is something a raw postMessage bridge makes easy to get wrong, because older host builds match a reply to a request by its type alone, with no id to tell two of them apart.

| Behaviour | Why | |---|---| | One in-flight request per event type until correlation is proven | Two same-type replies cannot be told apart, and nothing promises they arrive in send order. Different types always run in parallel. | | requestId, negotiated | A host that echoes it lifts that restriction entirely - unlimited same-type concurrency, exact attribution. Detection costs no extra message: the first side-effect-free read carries the field, and its reply decides. Old builds never see it. | | Late replies are discarded, not reused | When a call times out, its reply may still arrive. Handing it to the next caller of that type is a wrong-data bug with no error anywhere; that is what a naive per-type FIFO queue does. | | Single-flight coalescing | Ten components each calling useAppInfo() on mount produce one message. Only side-effect-free reads are eligible - coalescing an action would swallow a second intentional one. | | Session cache | An event that raises a PIN prompt would otherwise prompt once per caller. bridge.invalidate() clears it. | | Nothing is posted before the bridge exists | The host injects window.ReactNativeWebView after the page starts, and anything posted earlier is silently dropped. Requests wait for it - once, not per call. | | AbortSignal on every call | A component unmounting cancels its request and frees the slot, instead of holding it until timeout. | | Inbound duplicates are dropped | Some Android builds deliver the same message on both window and document. | | Unsolicited events are buffered | A message that arrives before you subscribe is delivered when you do. | | Typed end to end | Request payloads, response shapes and handler return types all come from one generated map. | | Works in a plain browser tab | A mock transport answers from the event catalog, so a miniapp is developable without a device. |


If you are the miniapp

import { getBridge, BridgeUnavailableError } from '@sharering/me-module-sdk';

try {
  const { darkMode, language } = await getBridge().send('COMMON_APP_INFO');
} catch (error) {
  // Expected in a plain browser - and it rejects immediately rather than
  // waiting out the timeout, so you can render a fallback straight away.
  if (error instanceof BridgeUnavailableError) showBrowserFallback();
}

Or the generated per-event wrappers, if you prefer them to string literals:

import { commonAppInfo, walletBalance, navigateTo } from '@sharering/me-module-sdk';

const balances = await walletBalance();
await navigateTo({ to: 'wallet', mode: 'push' });

React (and Next.js)

import { useAppInfo, useBridgeCall, useReadyState } from '@sharering/me-module-sdk/react';

function Profile() {
  const ready = useReadyState();               // 'pending' | 'ready' | 'unavailable'
  const { data: app, loading, refetch } = useAppInfo();
  const [copy, { loading: copying }] = useBridgeCall('COMMON_COPY_TO_CLIPBOARD');

  if (ready === 'unavailable') return <BrowserFallback />;
  if (loading) return <Spinner />;

  return (
    <button disabled={copying} onClick={() => copy({ content: app!.id })}>
      Copy app id
    </button>
  );
}

useBridgeQuery, useBridgeCall and useBridgeEvent take any event type in the catalog. useAppInfo, useDeviceInfo and the rest are one-line conveniences over them, named for events in the shipped catalog.

Every hook is safe to render on a server, so a Next.js static export works. A Next.js server runtime does not — nothing on a server reaches a WebView — so use output: 'export'.

Vue

// main.ts
import { createMeModule } from '@sharering/me-module-sdk/vue';
createApp(App).use(createMeModule()).mount('#app');
<script setup lang="ts">
import { useAppInfo, useBridgeCall } from '@sharering/me-module-sdk/vue';

const { data: app, loading } = useAppInfo();
const { call: copy } = useBridgeCall('COMMON_COPY_TO_CLIPBOARD');
</script>

Developing in a plain browser

There is no host app, so nothing answers. Point the bridge at a mock that replies from the event catalog and the whole miniapp becomes workable in a normal browser tab:

import { configureBridge } from '@sharering/me-module-sdk';
import { createMockTransport } from '@sharering/me-module-sdk/mock';

if (import.meta.env.DEV && !window.ReactNativeWebView) {
  configureBridge({
    transport: createMockTransport({
      latencyMs: 150,
      responses: { WALLET_BALANCE: [{ amount: '12500000000', denom: 'nshr' }] },
      // Reproduce an older host build to exercise the serialized path.
      echoRequestId: false,
    }),
  });
}

Timeouts

One default for every event: 60 seconds. It has to suit the slowest legitimate case — a human answering a biometric/PIN prompt, and then the host talking to a network. A shorter default is the single most common cause of "it randomly fails".

Nothing is lost by that: a plain browser fails immediately with BridgeUnavailableError rather than waiting, and any caller who wants to give up sooner says so:

// The payload is always the second argument and the options always the third,
// so an event taking no payload passes `undefined` through to reach them.
await getBridge().send('COMMON_APP_INFO', undefined, { timeoutMs: 2_000, signal: controller.signal });
await getBridge().send('COMMON_OPEN_BROWSER', 'https://example.com', { timeoutMs: 2_000 });

Errors

Every failure is a distinct class with a stable code, so you can branch without string matching:

| Class | code | Means | |---|---|---| | BridgeUnavailableError | BRIDGE_UNAVAILABLE | Not running inside a host app. Never retry. | | BridgeTimeoutError | TIMEOUT | No reply in the window. | | BridgeResponseError | RESPONSE_ERROR | The host answered with an error - a declined prompt, say. raw is what it sent. | | BridgeAbortError | ABORTED | Your AbortSignal fired. | | BridgeProtocolError | PROTOCOL_ERROR | A message could not be understood or attributed. | | BridgeValidationError | VALIDATION_ERROR | Raised in the host, not the miniapp: an incoming payload failed its schema. |


If you are the host app

Import /host in your React Native app. It hands each incoming request to the handler you registered for that event, replies with whatever the handler returns, and can push an event to a miniapp unprompted.

import { createMeModuleHost } from '@sharering/me-module-sdk/host';

const host = createMeModuleHost({ validate: __DEV__ });

host.handle('COMMON_APP_INFO', () => ({ language, version, id, darkMode }));
host.handle('WALLET_BALANCE', () => coins);            // must return Coin[] - compile-checked
host.handle('WALLET_SIGN_TRANSACTION', async (payload) => {
  await requirePin();                                   // app policy, not the SDK's
  return sign(payload);
});

function MiniappView({ url }) {
  const ref = useRef<WebView>(null);
  useEffect(() => host.attach(ref), []);
  return <WebView ref={ref} source={{ uri: url }} onMessage={host.onMessage} />;
}

host.emit('SOME_PUSH_EVENT', payload);                  // push to the miniapp, unprompted

Two rules it keeps for you:

  • requestId is echoed back verbatim whenever the request carried one. That one field is what lets a miniapp run several calls of the same event at once.
  • Every request gets an answer — a handler that threw, and an event nobody registered, both reply with an error. Dropping a request instead would leave the miniapp waiting out its whole timeout with nothing in any log to explain it.

validate: true checks inbound payloads against their schema before calling a handler, so a malformed payload becomes an error rather than reaching code that assumes its shape. It needs ajv and ajv-formats, declared as optional peer dependencies — the package itself has no runtime dependencies at all.

One wrinkle worth knowing rather than discovering: npm installs optional peers anyway, so if you build a miniapp with npm you will find ajv in node_modules even though nothing you import touches it. Yarn and pnpm do not. Either way it never reaches a miniapp's bundle — bundle.test.ts asserts there is no path to it from the miniapp entry.

ESM only. Metro handles it; Jest does not by default — its transformIgnorePatterns skips node_modules, so a test importing /host fails with Cannot use import statement outside a module. One line fixes it:

transformIgnorePatterns: ['node_modules/(?!(react-native|@sharering/me-module-sdk)/)'],

host.dispatch(raw) is the transport-free seam: raw envelope in, raw reply out. A host that is not react-native-webview — a different WebView binding, an iframe, a test harness — builds on that and skips attach/onMessage entirely.

/host runs on the trusted side of the WebView, so its job is deliberately small. It routes, checks payload shape and correlates replies — nothing more. There is no PIN policy, no capability gating and no authorization here: what WALLET_SIGN_TRANSACTION is allowed to do stays in the app, where it is reviewed as app code.


Defining your own events

events/events.json is the single source of truth. One entry per event: routing metadata plus JSON Schema for its request and response.

"COMMON_APP_INFO": {
  "category": "COMMON",
  "coalescable": true,
  "request": null,                               // null = takes no payload
  "response": { "$ref": "#/$defs/AppInfo" }
}

Everything else is derived from that: BridgeEventMap, the payload interfaces, the scheduler's behaviour tables, the per-event wrappers in api.ts, and the schemas /host validates against. Add an event and it appears in all of them, the contract test covers it automatically, and catalog.ts stops compiling until it is documented.

A superapp that is not ShareRing Me forks this package, replaces events.json with its own event set, and re-runs yarn codegen. Nothing under src/ names an event, with one exception: the convenience hooks in /react and /vue (useAppInfo, useDeviceInfo, useAsyncStorage, useStatusBarDimensions) name COMMON_* events from the shipped catalog — rename or delete them to match yours. The generic useBridgeQuery / useBridgeCall / useBridgeEvent hooks work with any catalog. catalog.ts is the prose for the shipped events and is replaced alongside them.

Two guards make the contract safe:

  • events/events.schema.json validates events.json itself, and the generator fails the build on any error. additionalProperties: false is the load-bearing part: a typo'd coalescable would otherwise silently mean "off", with nothing to show for it.
  • Generated output is never committed. src/protocol/generated/ and src/api.ts are gitignored and regenerated by prebuild, pretest, prelint and prepare. There is no committed copy to go stale, so there is no drift to police. (prepare, not postinstall: npm runs postinstall for an installed dependency too, so a consumer would run the generator — and fail on a tsx they never installed.)
yarn codegen     # regenerate now
yarn build       # regenerates, then bundles
yarn test        # regenerates, then runs 160-odd tests

Reference

@sharering/me-module-sdk/catalog is the shipped events as queryable data. Each EventDoc is a whole event — its type, category and fireAndForget flag joined in from /protocol, alongside the summary, caveats and example request and response authored for it — so a page rendering a catalogue never has to join those two halves itself.

import { CATEGORY_BLURB, EVENT_DOCS, EVENTS } from '@sharering/me-module-sdk/catalog';
import { isBridgeEventType } from '@sharering/me-module-sdk/protocol';

EVENTS.filter((event) => event.category === 'WALLET'); // a capability picker
EVENT_DOCS.WALLET_BALANCE.exampleResponse; // a playground's canned reply
isBridgeEventType(fromUrl) ? EVENT_DOCS[fromUrl] : undefined; // a deep link

EVENTS is the same objects in protocol order. There is no findEvent or eventsByCategory beside them: those are find and filter, and a lookup by an untrusted string is the guard above. It is a separate entry point so its ~15KB of English stays out of a miniapp's bundle.