fox-events
v1.0.3
Published
Lightweight event system based on CustomEvent with IndexedDB persistence
Downloads
91
Maintainers
Readme
Fox Events
Lightweight event system based on CustomEvent with optional IndexedDB persistence and a React Native WebView bridge.
Documentation: fox-events.deno.dev
- Core: publish/subscribe via
emit,on,once; event history viatrail() - Storage: optional persistence with IndexedDB (
fox-events/storage) - Bridge: optional RN ↔ WebView bridge (
fox-events/bridge-react-native)
Install
npm install fox-eventsCore usage
Instance API
Use Fox.channel<Payload>(name) (or new Fox<Payload>(name)) so emit / on / once are correctly typed. Without the generic you get Fox<unknown> and emit(detail: unknown).
import { Fox } from "fox-events";
// Typed channel — emit/on/once use this payload type
const userLogin = Fox.channel<{ userId: string }>("user:login");
// or: new Fox<{ userId: string }>("user:login")
// Emit (sync) — detail is typed as { userId: string }
userLogin.emit({ userId: "u-1" });
// Emit (async)
await userLogin.emitAsync({ userId: "u-2" });
// Subscribe
const unsubscribe = userLogin.on((payload) => {
console.log("Login:", payload.userId);
});
// Unsubscribe when done
unsubscribe();
// Wait for next event once
const payload = await userLogin.once();
console.log("First login:", payload);
// Inspect history
const history = userLogin.trail();
console.log(history.published);
console.log(history.received);
console.log(history.unsubscribed);
console.log(history.toText());Static API (by event name)
import { Fox } from "fox-events";
// Emit by name (creates channel if needed)
Fox.emit("app:ready", { version: "1.0" });
await Fox.emitAsync("app:ready", { version: "1.0" });
// Subscribe by name
const off = Fox.on("app:ready", (payload) => {
console.log("App ready:", payload);
});
off();
// Wait for next event by name
const payload = await Fox.once("app:ready");Persistence (IndexedDB)
Use the storage subpath and pass den.storage + den.autoPersist so published, received, and unsubscribed history are persisted.
import { Fox } from "fox-events";
import { IndexedDBStorage } from "fox-events/storage";
const storage = new IndexedDBStorage();
const channel = new Fox<{ id: string }>("user:action", {
den: {
autoPersist: true,
storage,
},
});
channel.emit({ id: "a-1" });
// History is written to IndexedDB (db: "fox-events", store: "events")Custom storage (same interface)
import type { IEventStorage } from "fox-events/storage";
const customStorage: IEventStorage = {
async get(key) {
const raw = await myBackend.get(key);
return raw ? JSON.parse(raw) : null;
},
async setItem(key, value) {
await myBackend.set(key, JSON.stringify(value));
},
async removeItem(key) {
await myBackend.delete(key);
},
async clear() {
await myBackend.clear();
},
};
const channel = new Fox("custom:event", {
den: { autoPersist: true, storage: customStorage },
});React Native WebView bridge
Use the bridge when the app runs inside a React Native WebView and you want events to flow between RN and the WebView via postMessage.
import { createReactNativeBridge } from "fox-events/bridge-react-native";
// Bidirectional: RN ↔ WebView
const dispose = createReactNativeBridge({
direction: "both",
filter: (eventName) => eventName.startsWith("app:"),
debug: false,
});
// Now:
// - RN postMessage({ name: "app:login", payload }) → Fox.emit("app:login", payload) in WebView
// - Fox.emit("app:login", payload) in WebView → RN via window.ReactNativeWebView.postMessage
// When done (e.g. WebView unmount)
dispose();Direction
inbound– Only RN → WebView:window.addEventListener("message")→Fox.emit(name, payload).outbound– Only WebView → RN: middlewareonEmit→window.ReactNativeWebView.postMessage.both– Both directions (default for the example above).
Message format
Messages follow this contract (same in both directions):
type BridgeMessage<T = unknown> = {
name: string;
payload: T;
source?: "react-native" | "web";
};Malformed messages or messages without name are ignored. The bridge tags outbound messages with source: "web" and avoids echoing inbound messages back to RN.
Inbound-only (RN → WebView)
createReactNativeBridge({
direction: "inbound",
filter: (name) => name.startsWith("native:"),
});Outbound-only (WebView → RN)
createReactNativeBridge({
direction: "outbound",
filter: (name) => name.startsWith("web:"),
});Adapter no React Native (emitir e ouvir no app nativo)
No lado React Native você não tem Fox; use createFoxRNAdapter para uma API Fox-like que envia/recebe mensagens pelo WebView. O adapter expõe emit, on, once e handleMessage.
1. Crie o adapter passando uma função que envia dados para a WebView (ex.: injectJavaScript):
import { useRef, useMemo } from "react";
import { WebView } from "react-native-webview";
import { createFoxRNAdapter } from "fox-events/bridge-react-native";
function App() {
const webViewRef = useRef<WebView>(null);
const fox = useMemo(
() =>
createFoxRNAdapter({
sendToWebView: (msg) => {
const code = `window.postMessage(${JSON.stringify(msg)}, '*'); true;`;
webViewRef.current?.injectJavaScript(code);
},
}),
[]
);
return (
<WebView
ref={webViewRef}
source={{ uri: "https://your-webview-page.com" }}
onMessage={(event) => fox.handleMessage(event.nativeEvent.data)}
/>
);
}2. No app RN: use fox.emit, fox.on, fox.once como se fosse Fox:
// Enviar evento para a WebView (o bridge lá chama Fox.emit(name, payload))
fox.emit("app:command", { action: "reload" });
// Ouvir eventos que vêm da WebView (quando alguém faz Fox.emit na página)
fox.on("app:ready", (payload) => {
console.log("WebView disse que está pronta:", payload);
});
// Esperar um evento uma vez
const payload = await fox.once("app:login");Na WebView (página), ative o bridge com createReactNativeBridge({ direction: "both", ... }) para que mensagens RN ↔ WebView usem o mesmo contrato { name, payload }.
API summary
| API | Description |
|-----|-------------|
| Fox.channel<T>(name, options?) | Typed channel (prefer over new Fox<T>(name) for correct emit/on typing). |
| new Fox<T>(name, options?) | Create a channel; optional den, maxHistory. |
| fox.emit(detail) | Emit synchronously (persistence fire-and-forget). |
| fox.emitAsync(detail) | Emit; awaits persist when den is set, then dispatches. |
| fox.on(callback) | Subscribe; returns unsubscribe function. |
| fox.once() | Promise that resolves on next event. |
| fox.trail() | { published, received, unsubscribed, toText() }. |
| fox.clearTrail() | Clear in-memory history (does not clear persisted data). |
| Fox.emit(name, detail) | Static emit by name. |
| Fox.emitAsync(name, detail) | Static emit by name (async). |
| Fox.on(name, callback) | Static subscribe by name. |
| Fox.once(name, options?) | Static once by name; supports { timeout }. |
| Fox.forScope(scope) | Scoped registry; channel, emit, on, once. |
| useMiddleware({ onEmit }) | Register middleware; no monkey-patching. |
Exports
fox-events–Fox,useMiddleware,Options,DenOptions,OnceOptions,FoxDeps,FoxMiddleware,EventTrail,UnsubscribedHistory,HistorySnapshot,IPublisher,IListener.fox-events/storage–IndexedDBStorage,IEventStorage.fox-events/bridge-react-native–createReactNativeBridge,createFoxRNAdapter,BridgeMessage,BridgeDirection,ReactNativeBridgeOptions,ReactNativeBridgeDispose,FoxRNAdapterOptions,FoxRNAdapter.
Documentation
- Site (PT): fox-events.deno.dev/pt
- Local: docs — full docs with isolated examples
License
MIT
