🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

dc-browser

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dc-browser - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+102
dist/index.d.mts
/**
* Browser-compatible implementation of Node.js Channel.
*
* Each channel is its own object with its own subscribers.
*/
interface AsyncLocalStorageLike<T> {
run<R>(store: T, callback: () => R): R;
}
type TransformFunction<M, S> = (message: M) => S;
type MessageFunction<M, N> = (message: M, name: N) => void;
declare class Channel<M = any, N = string> {
readonly name: N;
private _subscribers;
private _stores;
constructor(name: N);
get hasSubscribers(): boolean;
subscribe(subscription: MessageFunction<M, N>): void;
unsubscribe(subscription: MessageFunction<M, N>): boolean;
bindStore<T>(store: AsyncLocalStorageLike<T>, transform?: TransformFunction<M, T>): void;
unbindStore<T>(store: AsyncLocalStorageLike<T>): boolean;
publish(message: M): void;
runStores<F extends (...args: any[]) => any>(message: M, fn: F, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F>;
}
/**
* Browser-compatible implementation of Node.js TracingChannel.
*
* A TracingChannel is a composite of 5 individual channels:
* - start: Published before sync operation
* - end: Published after sync operation completes
* - asyncStart: Published when async operation's promise starts resolving
* - asyncEnd: Published when async operation's promise finishes resolving
* - error: Published when operation throws/rejects
*/
interface Channels<M> {
readonly start?: Channel<M>;
readonly end?: Channel<M>;
readonly asyncStart?: Channel<M>;
readonly asyncEnd?: Channel<M>;
readonly error?: Channel<M>;
}
interface ChannelHandlers<M> {
start?: (context: M, name: string) => void;
end?: (context: M, name: string) => void;
asyncStart?: (context: M, name: string) => void;
asyncEnd?: (context: M, name: string) => void;
error?: (context: M, name: string) => void;
}
declare class TracingChannel<M> {
#private;
readonly start?: Channel<M>;
readonly end?: Channel<M>;
readonly asyncStart?: Channel<M>;
readonly asyncEnd?: Channel<M>;
readonly error?: Channel<M>;
constructor(nameOrChannels: string | Channels<M>);
get hasSubscribers(): boolean;
subscribe(handlers: ChannelHandlers<M>): void;
unsubscribe(handlers: ChannelHandlers<M>): boolean;
traceSync<F extends (...args: any[]) => any>(fn: F, message: M, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F>;
tracePromise<F extends (...args: any[]) => any>(fn: F, message: M, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): Promise<ReturnType<F>>;
traceCallback<F extends (...args: any[]) => any>(fn: F, position: number | undefined, message: M, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F>;
}
/**
* dc-browser
*
* Browser-compatible polyfill for Node.js diagnostics_channel.
* Matches the API from Node.js core exactly.
*/
/**
* Get or create a channel by name.
* Matches Node.js channel() API.
*/
declare function channel<M>(name: string): Channel<M, string>;
declare function channel<M>(name: symbol): Channel<M, symbol>;
/**
* Check if a channel has subscribers.
* Matches Node.js hasSubscribers() API.
*/
declare function hasSubscribers(name: string | symbol): boolean;
/**
* Subscribe to a channel.
* Matches Node.js subscribe() API.
*/
declare function subscribe<M>(name: string, subscription: MessageFunction<M, string>): void;
declare function subscribe<M>(name: symbol, subscription: MessageFunction<M, symbol>): void;
/**
* Unsubscribe from a channel.
* Matches Node.js unsubscribe() API.
*/
declare function unsubscribe<M>(name: string, subscription: MessageFunction<M, string>): boolean;
declare function unsubscribe<M>(name: symbol, subscription: MessageFunction<M, symbol>): boolean;
/**
* Create a TracingChannel.
* Matches Node.js tracingChannel() API.
*/
declare function tracingChannel<M>(nameOrChannels: string | Channels<M>): TracingChannel<M>;
export { Channel, type ChannelHandlers, type MessageFunction, TracingChannel, type TransformFunction, channel, hasSubscribers, subscribe, tracingChannel, unsubscribe };
/**
* Browser-compatible implementation of Node.js Channel.
*
* Each channel is its own object with its own subscribers.
*/
interface AsyncLocalStorageLike<T> {
run<R>(store: T, callback: () => R): R;
}
type TransformFunction<M, S> = (message: M) => S;
type MessageFunction<M, N> = (message: M, name: N) => void;
declare class Channel<M = any, N = string> {
readonly name: N;
private _subscribers;
private _stores;
constructor(name: N);
get hasSubscribers(): boolean;
subscribe(subscription: MessageFunction<M, N>): void;
unsubscribe(subscription: MessageFunction<M, N>): boolean;
bindStore<T>(store: AsyncLocalStorageLike<T>, transform?: TransformFunction<M, T>): void;
unbindStore<T>(store: AsyncLocalStorageLike<T>): boolean;
publish(message: M): void;
runStores<F extends (...args: any[]) => any>(message: M, fn: F, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F>;
}
/**
* Browser-compatible implementation of Node.js TracingChannel.
*
* A TracingChannel is a composite of 5 individual channels:
* - start: Published before sync operation
* - end: Published after sync operation completes
* - asyncStart: Published when async operation's promise starts resolving
* - asyncEnd: Published when async operation's promise finishes resolving
* - error: Published when operation throws/rejects
*/
interface Channels<M> {
readonly start?: Channel<M>;
readonly end?: Channel<M>;
readonly asyncStart?: Channel<M>;
readonly asyncEnd?: Channel<M>;
readonly error?: Channel<M>;
}
interface ChannelHandlers<M> {
start?: (context: M, name: string) => void;
end?: (context: M, name: string) => void;
asyncStart?: (context: M, name: string) => void;
asyncEnd?: (context: M, name: string) => void;
error?: (context: M, name: string) => void;
}
declare class TracingChannel<M> {
#private;
readonly start?: Channel<M>;
readonly end?: Channel<M>;
readonly asyncStart?: Channel<M>;
readonly asyncEnd?: Channel<M>;
readonly error?: Channel<M>;
constructor(nameOrChannels: string | Channels<M>);
get hasSubscribers(): boolean;
subscribe(handlers: ChannelHandlers<M>): void;
unsubscribe(handlers: ChannelHandlers<M>): boolean;
traceSync<F extends (...args: any[]) => any>(fn: F, message: M, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F>;
tracePromise<F extends (...args: any[]) => any>(fn: F, message: M, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): Promise<ReturnType<F>>;
traceCallback<F extends (...args: any[]) => any>(fn: F, position: number | undefined, message: M, thisArg?: ThisParameterType<F>, ...args: Parameters<F>): ReturnType<F>;
}
/**
* dc-browser
*
* Browser-compatible polyfill for Node.js diagnostics_channel.
* Matches the API from Node.js core exactly.
*/
/**
* Get or create a channel by name.
* Matches Node.js channel() API.
*/
declare function channel<M>(name: string): Channel<M, string>;
declare function channel<M>(name: symbol): Channel<M, symbol>;
/**
* Check if a channel has subscribers.
* Matches Node.js hasSubscribers() API.
*/
declare function hasSubscribers(name: string | symbol): boolean;
/**
* Subscribe to a channel.
* Matches Node.js subscribe() API.
*/
declare function subscribe<M>(name: string, subscription: MessageFunction<M, string>): void;
declare function subscribe<M>(name: symbol, subscription: MessageFunction<M, symbol>): void;
/**
* Unsubscribe from a channel.
* Matches Node.js unsubscribe() API.
*/
declare function unsubscribe<M>(name: string, subscription: MessageFunction<M, string>): boolean;
declare function unsubscribe<M>(name: symbol, subscription: MessageFunction<M, symbol>): boolean;
/**
* Create a TracingChannel.
* Matches Node.js tracingChannel() API.
*/
declare function tracingChannel<M>(nameOrChannels: string | Channels<M>): TracingChannel<M>;
export { Channel, type ChannelHandlers, type MessageFunction, TracingChannel, type TransformFunction, channel, hasSubscribers, subscribe, tracingChannel, unsubscribe };
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Channel: () => Channel,
TracingChannel: () => TracingChannel,
channel: () => channel,
hasSubscribers: () => hasSubscribers,
subscribe: () => subscribe,
tracingChannel: () => tracingChannel,
unsubscribe: () => unsubscribe
});
module.exports = __toCommonJS(index_exports);
// src/channel.ts
function triggerUncaughtException(err, _) {
window.dispatchEvent(
new ErrorEvent("error", {
error: err,
message: err?.message
})
);
}
function defaultTransform(message) {
return message;
}
function wrapStoreRun(store, message, next, transform = defaultTransform) {
return () => {
let context;
try {
context = transform(message);
} catch (err) {
queueMicrotask(() => {
triggerUncaughtException(err, false);
});
return next();
}
return store.run(context, next);
};
}
var Channel = class {
name;
_subscribers;
_stores;
constructor(name) {
this.name = name;
this._subscribers = [];
this._stores = /* @__PURE__ */ new Map();
}
get hasSubscribers() {
return this._subscribers.length > 0;
}
subscribe(subscription) {
if (typeof subscription !== "function") {
throw new TypeError("subscription must be a function");
}
this._subscribers = this._subscribers.slice();
this._subscribers.push(subscription);
}
unsubscribe(subscription) {
const index = this._subscribers.indexOf(subscription);
if (index === -1) {
return false;
}
const before = this._subscribers.slice(0, index);
const after = this._subscribers.slice(index + 1);
this._subscribers = before;
this._subscribers.push(...after);
return true;
}
bindStore(store, transform) {
if (!store || typeof store.run !== "function") {
throw new TypeError("store must have a run method");
}
this._stores.set(store, transform);
}
unbindStore(store) {
if (!this._stores.has(store)) {
return false;
}
this._stores.delete(store);
return true;
}
publish(message) {
const subscribers = this._subscribers;
for (let i = 0; i < (subscribers?.length || 0); i++) {
try {
const onMessage = subscribers[i];
onMessage(message, this.name);
} catch (err) {
queueMicrotask(() => {
triggerUncaughtException(err, false);
});
}
}
}
runStores(message, fn, thisArg, ...args) {
let run = () => {
this.publish(message);
return Reflect.apply(fn, thisArg, args);
};
for (const entry of this._stores.entries()) {
const store = entry[0];
const transform = entry[1];
run = wrapStoreRun(store, message, run, transform);
}
return run();
}
};
// src/tracing-channel.ts
var TracingChannel = class {
start;
end;
asyncStart;
asyncEnd;
error;
constructor(nameOrChannels) {
if (typeof nameOrChannels === "string") {
this.start = new Channel(`tracing:${nameOrChannels}:start`);
this.end = new Channel(`tracing:${nameOrChannels}:end`);
this.asyncStart = new Channel(`tracing:${nameOrChannels}:asyncStart`);
this.asyncEnd = new Channel(`tracing:${nameOrChannels}:asyncEnd`);
this.error = new Channel(`tracing:${nameOrChannels}:error`);
} else {
this.start = nameOrChannels.start;
this.end = nameOrChannels.end;
this.asyncStart = nameOrChannels.asyncStart;
this.asyncEnd = nameOrChannels.asyncEnd;
this.error = nameOrChannels.error;
}
}
get hasSubscribers() {
return this.start?.hasSubscribers || this.end?.hasSubscribers || this.asyncStart?.hasSubscribers || this.asyncEnd?.hasSubscribers || this.error?.hasSubscribers || false;
}
subscribe(handlers) {
if (handlers.start) {
this.start?.subscribe(handlers.start);
}
if (handlers.end) {
this.end?.subscribe(handlers.end);
}
if (handlers.asyncStart) {
this.asyncStart?.subscribe(handlers.asyncStart);
}
if (handlers.asyncEnd) {
this.asyncEnd?.subscribe(handlers.asyncEnd);
}
if (handlers.error) {
this.error?.subscribe(handlers.error);
}
}
unsubscribe(handlers) {
let done = true;
if (handlers.start && !this.start?.unsubscribe(handlers.start)) {
done = false;
}
if (handlers.end && !this.end?.unsubscribe(handlers.end)) {
done = false;
}
if (handlers.asyncStart && !this.asyncStart?.unsubscribe(handlers.asyncStart)) {
done = false;
}
if (handlers.asyncEnd && !this.asyncEnd?.unsubscribe(handlers.asyncEnd)) {
done = false;
}
if (handlers.error && !this.error?.unsubscribe(handlers.error)) {
done = false;
}
return done;
}
#maybeRunStores(channel2, message, fn, thisArg, ...args) {
if (!channel2) {
return Reflect.apply(fn, thisArg, args);
}
return channel2.runStores(message, fn, thisArg, ...args);
}
traceSync(fn, message, thisArg, ...args) {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
return this.#maybeRunStores(
this.start,
message,
() => {
try {
const result = Reflect.apply(fn, thisArg, args);
message.result = result;
return result;
} catch (err) {
message.error = err;
this.error?.publish(message);
throw err;
} finally {
this.end?.publish(message);
}
}
);
}
tracePromise(fn, message, thisArg, ...args) {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
const { start, end, asyncStart, asyncEnd, error } = this;
function reject(err) {
message.error = err;
error?.publish(message);
asyncStart?.publish(message);
asyncEnd?.publish(message);
return Promise.reject(err);
}
function resolve(result) {
message.result = result;
asyncStart?.publish(message);
asyncEnd?.publish(message);
return result;
}
return this.#maybeRunStores(
start,
message,
() => {
try {
let promise = Reflect.apply(fn, thisArg, args);
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise);
}
return promise.then(resolve, reject);
} catch (err) {
message.error = err;
error?.publish(message);
throw err;
} finally {
end?.publish(message);
}
}
);
}
traceCallback(fn, position = -1, message, thisArg, ...args) {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
const { start, end, asyncStart, asyncEnd, error } = this;
const self = this;
function wrappedCallback(err, res) {
if (err) {
message.error = err;
error?.publish(message);
} else {
message.result = res;
}
self.#maybeRunStores(
asyncStart,
message,
() => {
try {
return Reflect.apply(callback, thisArg, arguments);
} finally {
asyncEnd?.publish(message);
}
}
);
}
const callback = args.at(position);
if (typeof callback !== "function") {
throw new TypeError("callback must be a function");
}
args.splice(position, 1, wrappedCallback);
return this.#maybeRunStores(
start,
message,
() => {
try {
return Reflect.apply(fn, thisArg, args);
} catch (err) {
message.error = err;
error?.publish(message);
throw err;
} finally {
end?.publish(message);
}
}
);
}
};
// src/index.ts
var CHANNELS_KEY = /* @__PURE__ */ Symbol.for("dc-browser:channels");
var channels = globalThis[CHANNELS_KEY] || (globalThis[CHANNELS_KEY] = /* @__PURE__ */ new Map());
function channel(name) {
if (typeof name !== "string" && typeof name !== "symbol") {
throw new TypeError("channel name must be a string or symbol");
}
let ch = channels.get(name);
if (!ch) {
ch = new Channel(name);
channels.set(name, ch);
}
return ch;
}
function hasSubscribers(name) {
const ch = channels.get(name);
return ch ? ch.hasSubscribers : false;
}
function subscribe(name, subscription) {
const ch = channel(name);
ch.subscribe(subscription);
}
function unsubscribe(name, subscription) {
const ch = channel(name);
return ch.unsubscribe(subscription);
}
function tracingChannel(nameOrChannels) {
return new TracingChannel(nameOrChannels);
}
// src/channel.ts
function triggerUncaughtException(err, _) {
window.dispatchEvent(
new ErrorEvent("error", {
error: err,
message: err?.message
})
);
}
function defaultTransform(message) {
return message;
}
function wrapStoreRun(store, message, next, transform = defaultTransform) {
return () => {
let context;
try {
context = transform(message);
} catch (err) {
queueMicrotask(() => {
triggerUncaughtException(err, false);
});
return next();
}
return store.run(context, next);
};
}
var Channel = class {
name;
_subscribers;
_stores;
constructor(name) {
this.name = name;
this._subscribers = [];
this._stores = /* @__PURE__ */ new Map();
}
get hasSubscribers() {
return this._subscribers.length > 0;
}
subscribe(subscription) {
if (typeof subscription !== "function") {
throw new TypeError("subscription must be a function");
}
this._subscribers = this._subscribers.slice();
this._subscribers.push(subscription);
}
unsubscribe(subscription) {
const index = this._subscribers.indexOf(subscription);
if (index === -1) {
return false;
}
const before = this._subscribers.slice(0, index);
const after = this._subscribers.slice(index + 1);
this._subscribers = before;
this._subscribers.push(...after);
return true;
}
bindStore(store, transform) {
if (!store || typeof store.run !== "function") {
throw new TypeError("store must have a run method");
}
this._stores.set(store, transform);
}
unbindStore(store) {
if (!this._stores.has(store)) {
return false;
}
this._stores.delete(store);
return true;
}
publish(message) {
const subscribers = this._subscribers;
for (let i = 0; i < (subscribers?.length || 0); i++) {
try {
const onMessage = subscribers[i];
onMessage(message, this.name);
} catch (err) {
queueMicrotask(() => {
triggerUncaughtException(err, false);
});
}
}
}
runStores(message, fn, thisArg, ...args) {
let run = () => {
this.publish(message);
return Reflect.apply(fn, thisArg, args);
};
for (const entry of this._stores.entries()) {
const store = entry[0];
const transform = entry[1];
run = wrapStoreRun(store, message, run, transform);
}
return run();
}
};
// src/tracing-channel.ts
var TracingChannel = class {
start;
end;
asyncStart;
asyncEnd;
error;
constructor(nameOrChannels) {
if (typeof nameOrChannels === "string") {
this.start = new Channel(`tracing:${nameOrChannels}:start`);
this.end = new Channel(`tracing:${nameOrChannels}:end`);
this.asyncStart = new Channel(`tracing:${nameOrChannels}:asyncStart`);
this.asyncEnd = new Channel(`tracing:${nameOrChannels}:asyncEnd`);
this.error = new Channel(`tracing:${nameOrChannels}:error`);
} else {
this.start = nameOrChannels.start;
this.end = nameOrChannels.end;
this.asyncStart = nameOrChannels.asyncStart;
this.asyncEnd = nameOrChannels.asyncEnd;
this.error = nameOrChannels.error;
}
}
get hasSubscribers() {
return this.start?.hasSubscribers || this.end?.hasSubscribers || this.asyncStart?.hasSubscribers || this.asyncEnd?.hasSubscribers || this.error?.hasSubscribers || false;
}
subscribe(handlers) {
if (handlers.start) {
this.start?.subscribe(handlers.start);
}
if (handlers.end) {
this.end?.subscribe(handlers.end);
}
if (handlers.asyncStart) {
this.asyncStart?.subscribe(handlers.asyncStart);
}
if (handlers.asyncEnd) {
this.asyncEnd?.subscribe(handlers.asyncEnd);
}
if (handlers.error) {
this.error?.subscribe(handlers.error);
}
}
unsubscribe(handlers) {
let done = true;
if (handlers.start && !this.start?.unsubscribe(handlers.start)) {
done = false;
}
if (handlers.end && !this.end?.unsubscribe(handlers.end)) {
done = false;
}
if (handlers.asyncStart && !this.asyncStart?.unsubscribe(handlers.asyncStart)) {
done = false;
}
if (handlers.asyncEnd && !this.asyncEnd?.unsubscribe(handlers.asyncEnd)) {
done = false;
}
if (handlers.error && !this.error?.unsubscribe(handlers.error)) {
done = false;
}
return done;
}
#maybeRunStores(channel2, message, fn, thisArg, ...args) {
if (!channel2) {
return Reflect.apply(fn, thisArg, args);
}
return channel2.runStores(message, fn, thisArg, ...args);
}
traceSync(fn, message, thisArg, ...args) {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
return this.#maybeRunStores(
this.start,
message,
() => {
try {
const result = Reflect.apply(fn, thisArg, args);
message.result = result;
return result;
} catch (err) {
message.error = err;
this.error?.publish(message);
throw err;
} finally {
this.end?.publish(message);
}
}
);
}
tracePromise(fn, message, thisArg, ...args) {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
const { start, end, asyncStart, asyncEnd, error } = this;
function reject(err) {
message.error = err;
error?.publish(message);
asyncStart?.publish(message);
asyncEnd?.publish(message);
return Promise.reject(err);
}
function resolve(result) {
message.result = result;
asyncStart?.publish(message);
asyncEnd?.publish(message);
return result;
}
return this.#maybeRunStores(
start,
message,
() => {
try {
let promise = Reflect.apply(fn, thisArg, args);
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise);
}
return promise.then(resolve, reject);
} catch (err) {
message.error = err;
error?.publish(message);
throw err;
} finally {
end?.publish(message);
}
}
);
}
traceCallback(fn, position = -1, message, thisArg, ...args) {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
const { start, end, asyncStart, asyncEnd, error } = this;
const self = this;
function wrappedCallback(err, res) {
if (err) {
message.error = err;
error?.publish(message);
} else {
message.result = res;
}
self.#maybeRunStores(
asyncStart,
message,
() => {
try {
return Reflect.apply(callback, thisArg, arguments);
} finally {
asyncEnd?.publish(message);
}
}
);
}
const callback = args.at(position);
if (typeof callback !== "function") {
throw new TypeError("callback must be a function");
}
args.splice(position, 1, wrappedCallback);
return this.#maybeRunStores(
start,
message,
() => {
try {
return Reflect.apply(fn, thisArg, args);
} catch (err) {
message.error = err;
error?.publish(message);
throw err;
} finally {
end?.publish(message);
}
}
);
}
};
// src/index.ts
var CHANNELS_KEY = /* @__PURE__ */ Symbol.for("dc-browser:channels");
var channels = globalThis[CHANNELS_KEY] || (globalThis[CHANNELS_KEY] = /* @__PURE__ */ new Map());
function channel(name) {
if (typeof name !== "string" && typeof name !== "symbol") {
throw new TypeError("channel name must be a string or symbol");
}
let ch = channels.get(name);
if (!ch) {
ch = new Channel(name);
channels.set(name, ch);
}
return ch;
}
function hasSubscribers(name) {
const ch = channels.get(name);
return ch ? ch.hasSubscribers : false;
}
function subscribe(name, subscription) {
const ch = channel(name);
ch.subscribe(subscription);
}
function unsubscribe(name, subscription) {
const ch = channel(name);
return ch.unsubscribe(subscription);
}
function tracingChannel(nameOrChannels) {
return new TracingChannel(nameOrChannels);
}
export {
Channel,
TracingChannel,
channel,
hasSubscribers,
subscribe,
tracingChannel,
unsubscribe
};
Copyright 2026 Stephen Belanger <admin@stephenbelanger.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Footer
+10
-4
{
"name": "dc-browser",
"version": "1.0.0",
"version": "1.0.1",
"description": "Browser polyfill for Node.js diagnostics_channel",

@@ -19,8 +19,14 @@ "author": "Stephen Belanger <admin@stephenbelanger.com>",

},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsup",
"watch": "tsup --watch",
"lint": "eslint src --ext .ts",
"prepublishOnly": "npm run build && npm test",
"test": "vitest run",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"watch": "tsup --watch"
},

@@ -27,0 +33,0 @@ "devDependencies": {

{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(npm run lint)",
"Bash(npm run:*)",
"Bash(npx tsc:*)"
]
}
}
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Channel } from "./channel";
import { AsyncLocalStorage } from "als-browser";
describe("Channel", () => {
let channel: Channel;
beforeEach(() => {
channel = new Channel("test");
});
describe("basic functionality", () => {
it("should create a channel with a name", () => {
expect(channel.name).toBe("test");
expect(channel.hasSubscribers).toBe(false);
});
it("should handle subscribe/unsubscribe", () => {
const handler = vi.fn();
channel.subscribe(handler);
expect(channel.hasSubscribers).toBe(true);
channel.publish({ data: 123 });
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test");
const result = channel.unsubscribe(handler);
expect(result).toBe(true);
expect(channel.hasSubscribers).toBe(false);
});
it("should call all subscribers", () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
channel.subscribe(handler1);
channel.subscribe(handler2);
channel.publish({ data: 456 });
expect(handler1).toHaveBeenCalledWith({ data: 456 }, "test");
expect(handler2).toHaveBeenCalledWith({ data: 456 }, "test");
});
it("should swallow errors in handlers", async () => {
const errorHandler = vi.fn(() => {
throw new Error("handler error");
});
const successHandler = vi.fn();
channel.subscribe(errorHandler);
channel.subscribe(successHandler);
// Mock window.dispatchEvent to capture error events
const dispatchEventSpy = vi.spyOn(window, "dispatchEvent").mockImplementation(() => true);
// Should not throw
channel.publish({ data: 789 });
expect(errorHandler).toHaveBeenCalled();
expect(successHandler).toHaveBeenCalled();
// Wait for microtask to complete
await new Promise<void>(resolve => queueMicrotask(resolve));
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
const errorEvent = dispatchEventSpy.mock.calls[0][0] as ErrorEvent;
expect(errorEvent).toBeInstanceOf(ErrorEvent);
expect(errorEvent.type).toBe("error");
expect(errorEvent.message).toBe("handler error");
expect(errorEvent.error).toBeInstanceOf(Error);
expect(errorEvent.error.message).toBe("handler error");
dispatchEventSpy.mockRestore();
});
});
describe("bindStore/unbindStore", () => {
it("should bind an AsyncLocalStorage", () => {
const store = new AsyncLocalStorage<any>();
expect(() => channel.bindStore(store)).not.toThrow();
});
it("should throw if store doesn't have run method", () => {
expect(() => channel.bindStore({} as any)).toThrow(
"store must have a run method"
);
});
it("should unbind a store", () => {
const store = new AsyncLocalStorage<any>();
channel.bindStore(store);
const result = channel.unbindStore(store);
expect(result).toBe(true);
});
it("should return false when unbinding non-existent store", () => {
const store = new AsyncLocalStorage<any>();
const result = channel.unbindStore(store);
expect(result).toBe(false);
});
it("should support transform function", () => {
const store = new AsyncLocalStorage<number>();
const transform = (msg: any) => msg.value * 2;
expect(() => channel.bindStore(store, transform)).not.toThrow();
});
});
describe("runStores", () => {
it("should run function and publish without bound stores", () => {
const handler = vi.fn();
const fn = vi.fn(() => 42);
channel.subscribe(handler);
const result = channel.runStores({ data: 123 }, fn);
expect(result).toBe(42);
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test");
expect(fn).toHaveBeenCalled();
});
it("should run function within bound store context", () => {
const store = new AsyncLocalStorage<any>();
const handler = vi.fn();
channel.bindStore(store);
channel.subscribe(handler);
const context = { userId: "123" };
const result = channel.runStores(context, () => {
return store.getStore();
});
expect(result).toBe(context);
expect(handler).toHaveBeenCalledWith(context, "test");
});
it("should apply transform when binding store", () => {
const store = new AsyncLocalStorage<number>();
channel.bindStore(store, (msg: any) => msg.value * 2);
const result = channel.runStores({ value: 21 }, () => {
return store.getStore();
});
expect(result).toBe(42);
});
it("should nest multiple bound stores", () => {
const store1 = new AsyncLocalStorage<string>();
const store2 = new AsyncLocalStorage<number>();
channel.bindStore(store1, (msg: any) => msg.name);
channel.bindStore(store2, (msg: any) => msg.count);
const context = { name: "test", count: 42 };
const result = channel.runStores(context, () => {
return {
store1: store1.getStore(),
store2: store2.getStore(),
};
});
expect(result).toEqual({
store1: "test",
store2: 42,
});
});
it("should handle multiple channels with different stores", () => {
const channel1 = new Channel("channel1");
const channel2 = new Channel("channel2");
const store1 = new AsyncLocalStorage<string>();
const store2 = new AsyncLocalStorage<number>();
channel1.bindStore(store1);
channel2.bindStore(store2);
const result1 = channel1.runStores("hello", () => store1.getStore());
const result2 = channel2.runStores(42, () => store2.getStore());
expect(result1).toBe("hello");
expect(result2).toBe(42);
});
it("should handle transform errors gracefully", async () => {
const store = new AsyncLocalStorage<any>();
const transform = () => {
throw new Error("transform error");
};
channel.bindStore(store, transform);
const dispatchEventSpy = vi.spyOn(window, "dispatchEvent").mockImplementation(() => true);
const result = channel.runStores({ data: 123 }, () => {
return 42;
});
// Should still return result even if transform fails
expect(result).toBe(42);
// Wait for microtask to complete
await new Promise<void>(resolve => queueMicrotask(resolve));
// Should have dispatched error event
expect(dispatchEventSpy).toHaveBeenCalled();
const errorEvent = dispatchEventSpy.mock.calls[0][0] as ErrorEvent;
expect(errorEvent.error.message).toBe("transform error");
dispatchEventSpy.mockRestore();
});
});
describe("runStores with thisArg and args", () => {
it("should apply function with thisArg", () => {
const handler = vi.fn();
channel.subscribe(handler);
const context = { value: 42 };
function getThis(this: typeof context) {
return this.value;
}
const result = channel.runStores({ data: 123 }, getThis, context);
expect(result).toBe(42);
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test");
});
it("should apply function with args", () => {
const handler = vi.fn();
channel.subscribe(handler);
function add(a: number, b: number, c: number) {
return a + b + c;
}
const result = channel.runStores({ data: 123 }, add, undefined, 1, 2, 3);
expect(result).toBe(6);
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test");
});
it("should apply function with both thisArg and args", () => {
const handler = vi.fn();
channel.subscribe(handler);
const context = { multiplier: 10 };
function multiplyAdd(this: typeof context, a: number, b: number) {
return (a + b) * this.multiplier;
}
const result = channel.runStores({ data: 123 }, multiplyAdd, context, 2, 3);
expect(result).toBe(50);
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test");
});
it("should work without thisArg or args", () => {
const handler = vi.fn();
channel.subscribe(handler);
const result = channel.runStores({ data: 123 }, () => 42);
expect(result).toBe(42);
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test");
});
});
describe("edge cases", () => {
it("should handle unsubscribe of non-existent handler", () => {
const handler = vi.fn();
const result = channel.unsubscribe(handler);
expect(result).toBe(false);
});
it("should throw when subscribing non-function", () => {
expect(() => channel.subscribe("not a function" as any)).toThrow(
"subscription must be a function"
);
});
it("should handle publishing with no subscribers", () => {
const newChannel = new Channel("no-subs");
expect(() => newChannel.publish({ data: 123 })).not.toThrow();
});
it("should handle symbol channel names", () => {
const sym = Symbol("test");
const symbolChannel = new Channel(sym);
expect(symbolChannel.name).toBe(sym);
const handler = vi.fn();
symbolChannel.subscribe(handler);
symbolChannel.publish({ data: 456 });
expect(handler).toHaveBeenCalledWith({ data: 456 }, sym);
});
it("should preserve subscription order", () => {
const order: number[] = [];
channel.subscribe(() => order.push(1));
channel.subscribe(() => order.push(2));
channel.subscribe(() => order.push(3));
channel.publish({});
expect(order).toEqual([1, 2, 3]);
});
it("should handle multiple unsubscribe calls", () => {
const handler = vi.fn();
channel.subscribe(handler);
expect(channel.unsubscribe(handler)).toBe(true);
expect(channel.unsubscribe(handler)).toBe(false);
expect(channel.unsubscribe(handler)).toBe(false);
});
});
});
/**
* Browser-compatible implementation of Node.js Channel.
*
* Each channel is its own object with its own subscribers.
*/
// Type definition for AsyncLocalStorage to avoid hard dependency
interface AsyncLocalStorageLike<T> {
run<R>(store: T, callback: () => R): R;
}
export type TransformFunction<M, S> = (message: M) => S;
export type MessageFunction<M, N> = (message: M, name: N) => void
function triggerUncaughtException(err: any, _: boolean) {
window.dispatchEvent(
new ErrorEvent('error', {
error: err,
message: (err as any)?.message,
})
);
}
function defaultTransform<M>(message: M): any {
return message;
}
function wrapStoreRun<S, M, N>(
store: AsyncLocalStorageLike<S>,
message: M,
next: () => N,
transform: TransformFunction<M, S> | undefined = defaultTransform
): () => N {
return () => {
let context;
try {
context = transform(message);
} catch (err) {
queueMicrotask(() => {
triggerUncaughtException(err, false);
});
return next();
}
return store.run(context, next);
};
}
export class Channel<M = any, N = string> {
public readonly name: N;
private _subscribers: Array<MessageFunction<M, N>>;
private _stores: Map<AsyncLocalStorageLike<any>, TransformFunction<M, any>>;
constructor(name: N) {
this.name = name;
this._subscribers = [];
this._stores = new Map();
}
get hasSubscribers(): boolean {
return this._subscribers.length > 0;
}
subscribe(subscription: MessageFunction<M, N>): void {
if (typeof subscription !== 'function') {
throw new TypeError('subscription must be a function');
}
// Ensure we don't modify a subscriber list while being iterated
this._subscribers = this._subscribers.slice();
this._subscribers.push(subscription);
}
unsubscribe(subscription: MessageFunction<M, N>): boolean {
const index = this._subscribers.indexOf(subscription);
if (index === -1) {
return false;
}
const before = this._subscribers.slice(0, index);
const after = this._subscribers.slice(index + 1);
this._subscribers = before;
this._subscribers.push(...after);
return true;
}
bindStore<T>(store: AsyncLocalStorageLike<T>, transform?: TransformFunction<M, T>): void {
if (!store || typeof store.run !== 'function') {
throw new TypeError('store must have a run method');
}
this._stores.set(store, transform as TransformFunction<M, any>);
}
unbindStore<T>(store: AsyncLocalStorageLike<T>): boolean {
if (!this._stores.has(store)) {
return false;
}
this._stores.delete(store);
return true;
}
publish(message: M): void {
const subscribers = this._subscribers;
for (let i = 0; i < (subscribers?.length || 0); i++) {
try {
const onMessage = subscribers[i];
onMessage(message, this.name);
} catch (err) {
queueMicrotask(() => {
triggerUncaughtException(err, false);
});
}
}
}
runStores<F extends (...args: any[]) => any>(
message: M,
fn: F,
thisArg?: ThisParameterType<F>,
...args: Parameters<F>
): ReturnType<F> {
let run = () => {
this.publish(message);
return Reflect.apply(fn, thisArg, args);
};
for (const entry of this._stores.entries()) {
const store = entry[0];
const transform = entry[1];
run = wrapStoreRun(store, message, run, transform);
}
return run();
}
}
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
channel,
hasSubscribers,
subscribe,
unsubscribe,
tracingChannel,
Channel,
TracingChannel,
} from "./index";
describe("diagnostics_channel module", () => {
describe("channel()", () => {
it("should create a new channel with string name", () => {
const ch = channel("test-channel");
expect(ch).toBeInstanceOf(Channel);
expect(ch.name).toBe("test-channel");
});
it("should create a new channel with symbol name", () => {
const sym = Symbol("test");
const ch = channel(sym);
expect(ch).toBeInstanceOf(Channel);
expect(ch.name).toBe(sym);
});
it("should reuse existing channel with same name", () => {
const ch1 = channel("reuse-test");
const ch2 = channel("reuse-test");
expect(ch1).toBe(ch2);
});
it("should reuse existing channel with same symbol", () => {
const sym = Symbol("reuse");
const ch1 = channel(sym);
const ch2 = channel(sym);
expect(ch1).toBe(ch2);
});
it("should throw on invalid name type", () => {
expect(() => channel(123 as any)).toThrow(
"channel name must be a string or symbol"
);
expect(() => channel(null as any)).toThrow(
"channel name must be a string or symbol"
);
expect(() => channel(undefined as any)).toThrow(
"channel name must be a string or symbol"
);
});
it("should maintain separate channels for string and symbol with same description", () => {
const sym = Symbol("test");
const ch1 = channel("test");
const ch2 = channel(sym);
expect(ch1).not.toBe(ch2);
});
});
describe("hasSubscribers()", () => {
it("should return false for channel with no subscribers", () => {
const ch = channel("no-subs");
expect(hasSubscribers("no-subs")).toBe(false);
});
it("should return true after subscribing", () => {
const ch = channel("with-subs");
ch.subscribe(() => {});
expect(hasSubscribers("with-subs")).toBe(true);
});
it("should return false for non-existent channel", () => {
expect(hasSubscribers("non-existent")).toBe(false);
});
it("should work with symbol names", () => {
const sym = Symbol("test");
expect(hasSubscribers(sym)).toBe(false);
const ch = channel(sym);
ch.subscribe(() => {});
expect(hasSubscribers(sym)).toBe(true);
});
});
describe("subscribe()", () => {
it("should subscribe to a string-named channel", () => {
const handler = vi.fn();
subscribe("test-sub", handler);
const ch = channel("test-sub");
ch.publish({ data: 123 });
expect(handler).toHaveBeenCalledWith({ data: 123 }, "test-sub");
});
it("should subscribe to a symbol-named channel", () => {
const sym = Symbol("test-sub");
const handler = vi.fn();
subscribe(sym, handler);
const ch = channel(sym);
ch.publish({ data: 456 });
expect(handler).toHaveBeenCalledWith({ data: 456 }, sym);
});
it("should create channel if it doesn't exist", () => {
const handler = vi.fn();
subscribe("new-channel", handler);
expect(hasSubscribers("new-channel")).toBe(true);
});
it("should allow multiple subscribers", () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
subscribe("multi", handler1);
subscribe("multi", handler2);
const ch = channel("multi");
ch.publish({ data: 789 });
expect(handler1).toHaveBeenCalledWith({ data: 789 }, "multi");
expect(handler2).toHaveBeenCalledWith({ data: 789 }, "multi");
});
});
describe("unsubscribe()", () => {
it("should unsubscribe from a string-named channel", () => {
const handler = vi.fn();
subscribe("test-unsub", handler);
const result = unsubscribe("test-unsub", handler);
expect(result).toBe(true);
const ch = channel("test-unsub");
ch.publish({ data: 123 });
expect(handler).not.toHaveBeenCalled();
});
it("should unsubscribe from a symbol-named channel", () => {
const sym = Symbol("test-unsub");
const handler = vi.fn();
subscribe(sym, handler);
const result = unsubscribe(sym, handler);
expect(result).toBe(true);
const ch = channel(sym);
ch.publish({ data: 456 });
expect(handler).not.toHaveBeenCalled();
});
it("should return false when unsubscribing non-existent handler", () => {
const handler = vi.fn();
subscribe("test", () => {});
const result = unsubscribe("test", handler);
expect(result).toBe(false);
});
it("should return false when unsubscribing non-existent channel", () => {
const handler = vi.fn();
const result = unsubscribe("non-existent", handler);
expect(result).toBe(false);
});
it("should only remove specified handler", () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
subscribe("selective", handler1);
subscribe("selective", handler2);
unsubscribe("selective", handler1);
const ch = channel("selective");
ch.publish({ data: 123 });
expect(handler1).not.toHaveBeenCalled();
expect(handler2).toHaveBeenCalledWith({ data: 123 }, "selective");
});
});
describe("tracingChannel()", () => {
it("should create a TracingChannel with string name", () => {
const tc = tracingChannel("trace-test");
expect(tc).toBeInstanceOf(TracingChannel);
expect(tc.start?.name).toBe("tracing:trace-test:start");
expect(tc.end?.name).toBe("tracing:trace-test:end");
expect(tc.asyncStart?.name).toBe("tracing:trace-test:asyncStart");
expect(tc.asyncEnd?.name).toBe("tracing:trace-test:asyncEnd");
expect(tc.error?.name).toBe("tracing:trace-test:error");
});
it("should create a TracingChannel with Channels object", () => {
const startCh = new Channel("custom-start");
const endCh = new Channel("custom-end");
const tc = tracingChannel({
start: startCh,
end: endCh,
});
expect(tc).toBeInstanceOf(TracingChannel);
expect(tc.start).toBe(startCh);
expect(tc.end).toBe(endCh);
expect(tc.asyncStart).toBeUndefined();
expect(tc.asyncEnd).toBeUndefined();
expect(tc.error).toBeUndefined();
});
it("should work with partial Channels object", () => {
const errorCh = new Channel("error-only");
const tc = tracingChannel({
error: errorCh,
});
expect(tc.error).toBe(errorCh);
expect(tc.start).toBeUndefined();
});
});
describe("integration tests", () => {
it("should work end-to-end with string channels", () => {
const handler = vi.fn();
subscribe("integration", handler);
expect(hasSubscribers("integration")).toBe(true);
const ch = channel("integration");
ch.publish({ test: "data" });
expect(handler).toHaveBeenCalledWith({ test: "data" }, "integration");
unsubscribe("integration", handler);
expect(hasSubscribers("integration")).toBe(false);
});
it("should work end-to-end with symbol channels", () => {
const sym = Symbol("integration");
const handler = vi.fn();
subscribe(sym, handler);
expect(hasSubscribers(sym)).toBe(true);
const ch = channel(sym);
ch.publish({ test: "data" });
expect(handler).toHaveBeenCalledWith({ test: "data" }, sym);
unsubscribe(sym, handler);
expect(hasSubscribers(sym)).toBe(false);
});
it("should allow subscribing through different methods", () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
// Subscribe via module function
subscribe("mixed", handler1);
// Subscribe via channel instance
const ch = channel("mixed");
ch.subscribe(handler2);
ch.publish({ data: 123 });
expect(handler1).toHaveBeenCalledWith({ data: 123 }, "mixed");
expect(handler2).toHaveBeenCalledWith({ data: 123 }, "mixed");
});
});
describe("type safety", () => {
it("should maintain type safety for message type", () => {
interface MyMessage {
id: number;
name: string;
}
const handler = vi.fn<(message: MyMessage, name: string) => void>();
subscribe<MyMessage>("typed", handler);
const ch = channel<MyMessage>("typed");
ch.publish({ id: 1, name: "test" });
expect(handler).toHaveBeenCalledWith(
{ id: 1, name: "test" },
"typed"
);
});
it("should maintain type safety with symbols", () => {
interface MyMessage {
value: number;
}
const sym = Symbol("typed");
const handler = vi.fn<(message: MyMessage, name: symbol) => void>();
subscribe<MyMessage>(sym, handler);
const ch = channel<MyMessage>(sym);
ch.publish({ value: 42 });
expect(handler).toHaveBeenCalledWith({ value: 42 }, sym);
});
});
describe("dual-package hazard prevention", () => {
it("should store channels map on globalThis", () => {
const CHANNELS_KEY = Symbol.for("dc-browser:channels");
const globalMap = (globalThis as any)[CHANNELS_KEY];
expect(globalMap).toBeDefined();
expect(globalMap).toBeInstanceOf(Map);
});
it("should share channels across module systems", () => {
// Create a channel
const ch1 = channel("shared-test");
const handler = vi.fn();
ch1.subscribe(handler);
// Access the same channel from globalThis
const CHANNELS_KEY = Symbol.for("dc-browser:channels");
const globalMap = (globalThis as any)[CHANNELS_KEY] as Map<string | symbol, Channel<any, any>>;
const ch2 = globalMap.get("shared-test");
expect(ch2).toBe(ch1);
// Publishing to ch2 should trigger handler from ch1
ch2?.publish({ test: "data" });
expect(handler).toHaveBeenCalledWith({ test: "data" }, "shared-test");
});
});
});
/**
* dc-browser
*
* Browser-compatible polyfill for Node.js diagnostics_channel.
* Matches the API from Node.js core exactly.
*/
import { Channel, type MessageFunction } from "./channel";
import { TracingChannel, type Channels } from "./tracing-channel";
// Symbol key for storing the channels map on globalThis to avoid dual-package hazard
const CHANNELS_KEY = Symbol.for("dc-browser:channels");
// Global registry of channels (type-erased storage)
// Store on globalThis to share between ESM and CJS builds
const channels: Map<string | symbol, Channel<any, any>> =
(globalThis as any)[CHANNELS_KEY] ||
((globalThis as any)[CHANNELS_KEY] = new Map<string | symbol, Channel<any, any>>());
/**
* Get or create a channel by name.
* Matches Node.js channel() API.
*/
export function channel<M>(name: string): Channel<M, string>;
export function channel<M>(name: symbol): Channel<M, symbol>;
export function channel<M, N extends string | symbol>(name: N): Channel<M, N> {
if (typeof name !== 'string' && typeof name !== 'symbol') {
throw new TypeError('channel name must be a string or symbol');
}
let ch = channels.get(name);
if (!ch) {
ch = new Channel(name);
channels.set(name, ch as Channel<any, any>);
}
return ch;
}
/**
* Check if a channel has subscribers.
* Matches Node.js hasSubscribers() API.
*/
export function hasSubscribers(name: string | symbol): boolean {
const ch = channels.get(name);
return ch ? ch.hasSubscribers : false;
}
/**
* Subscribe to a channel.
* Matches Node.js subscribe() API.
*/
export function subscribe<M>(name: string, subscription: MessageFunction<M, string>): void;
export function subscribe<M>(name: symbol, subscription: MessageFunction<M, symbol>): void;
export function subscribe<M, N extends string | symbol>(name: N, subscription: MessageFunction<M, N>): void {
const ch = channel<M>(name as any) as unknown as Channel<M, N>;
ch.subscribe(subscription);
}
/**
* Unsubscribe from a channel.
* Matches Node.js unsubscribe() API.
*/
export function unsubscribe<M>(name: string, subscription: MessageFunction<M, string>): boolean;
export function unsubscribe<M>(name: symbol, subscription: MessageFunction<M, symbol>): boolean;
export function unsubscribe<M, N extends string | symbol>(name: N, subscription: MessageFunction<M, N>): boolean {
const ch = channel<M>(name as any) as unknown as Channel<M, N>;
return ch.unsubscribe(subscription);
}
/**
* Create a TracingChannel.
* Matches Node.js tracingChannel() API.
*/
export function tracingChannel<M>(nameOrChannels: string | Channels<M>): TracingChannel<M> {
return new TracingChannel(nameOrChannels);
}
// Re-export classes
export { Channel, type TransformFunction, type MessageFunction } from "./channel";
export { TracingChannel, type ChannelHandlers } from "./tracing-channel";
import { describe, it, expect, vi } from "vitest";
import { TracingChannel } from "./tracing-channel";
import { Channel } from "./channel";
describe("TracingChannel", () => {
describe("channel creation", () => {
it("should create 5 separate channels from string name", () => {
const tc = new TracingChannel("test");
expect(tc.start?.name).toBe("tracing:test:start");
expect(tc.end?.name).toBe("tracing:test:end");
expect(tc.asyncStart?.name).toBe("tracing:test:asyncStart");
expect(tc.asyncEnd?.name).toBe("tracing:test:asyncEnd");
expect(tc.error?.name).toBe("tracing:test:error");
});
it("should create from Channels object with all channels", () => {
const startCh = new Channel("start");
const endCh = new Channel("end");
const asyncStartCh = new Channel("asyncStart");
const asyncEndCh = new Channel("asyncEnd");
const errorCh = new Channel("error");
const tc = new TracingChannel({
start: startCh,
end: endCh,
asyncStart: asyncStartCh,
asyncEnd: asyncEndCh,
error: errorCh,
});
expect(tc.start).toBe(startCh);
expect(tc.end).toBe(endCh);
expect(tc.asyncStart).toBe(asyncStartCh);
expect(tc.asyncEnd).toBe(asyncEndCh);
expect(tc.error).toBe(errorCh);
});
it("should create from Channels object with partial channels", () => {
const startCh = new Channel("start");
const errorCh = new Channel("error");
const tc = new TracingChannel({
start: startCh,
error: errorCh,
});
expect(tc.start).toBe(startCh);
expect(tc.error).toBe(errorCh);
expect(tc.end).toBeUndefined();
expect(tc.asyncStart).toBeUndefined();
expect(tc.asyncEnd).toBeUndefined();
});
it("should have no subscribers initially", () => {
const tc = new TracingChannel("test");
expect(tc.hasSubscribers).toBe(false);
});
it("should handle hasSubscribers with partial channels", () => {
const startCh = new Channel("start");
const tc = new TracingChannel({ start: startCh });
expect(tc.hasSubscribers).toBe(false);
startCh.subscribe(() => {});
expect(tc.hasSubscribers).toBe(true);
});
});
describe("subscribe/unsubscribe", () => {
it("should subscribe to individual channels", () => {
const tc = new TracingChannel("test");
const startHandler = vi.fn();
const endHandler = vi.fn();
tc.subscribe({
start: startHandler,
end: endHandler,
});
expect(tc.start!.hasSubscribers).toBe(true);
expect(tc.end!.hasSubscribers).toBe(true);
expect(tc.asyncStart!.hasSubscribers).toBe(false);
});
it("should unsubscribe from channels", () => {
const tc = new TracingChannel("test");
const handlers = {
start: vi.fn(),
end: vi.fn(),
};
tc.subscribe(handlers);
expect(tc.hasSubscribers).toBe(true);
const result = tc.unsubscribe(handlers);
expect(result).toBe(true);
expect(tc.hasSubscribers).toBe(false);
});
});
describe("traceSync", () => {
it("should publish start and end with shared context", () => {
const tc = new TracingChannel("test");
const startHandler = vi.fn();
const endHandler = vi.fn();
tc.subscribe({
start: startHandler,
end: endHandler,
});
const result = tc.traceSync(() => 42, { foo: "bar" });
expect(result).toBe(42);
expect(startHandler).toHaveBeenCalledWith(
expect.objectContaining({ foo: "bar" }),
"tracing:test:start"
);
expect(endHandler).toHaveBeenCalledWith(
expect.objectContaining({ foo: "bar", result: 42 }),
"tracing:test:end"
);
// Verify same context object
expect(startHandler.mock.calls[0][0]).toBe(endHandler.mock.calls[0][0]);
});
it("should publish error on exception", () => {
const tc = new TracingChannel("test");
const errorHandler = vi.fn();
const endHandler = vi.fn();
tc.subscribe({
error: errorHandler,
end: endHandler,
});
const testError = new Error("test error");
expect(() => tc.traceSync(() => { throw testError; }, {})).toThrow(testError);
expect(errorHandler).toHaveBeenCalledWith(
expect.objectContaining({ error: testError }),
"tracing:test:error"
);
expect(endHandler).toHaveBeenCalled();
});
});
describe("tracePromise", () => {
it("should publish start, asyncStart, asyncEnd, end", async () => {
const tc = new TracingChannel("test");
const startHandler = vi.fn();
const endHandler = vi.fn();
const asyncStartHandler = vi.fn();
const asyncEndHandler = vi.fn();
tc.subscribe({
start: startHandler,
end: endHandler,
asyncStart: asyncStartHandler,
asyncEnd: asyncEndHandler,
});
const result = await tc.tracePromise(async () => 42, {});
expect(result).toBe(42);
expect(startHandler).toHaveBeenCalled();
expect(endHandler).toHaveBeenCalled();
expect(asyncStartHandler).toHaveBeenCalledWith(
expect.objectContaining({ result: 42 }),
"tracing:test:asyncStart"
);
expect(asyncEndHandler).toHaveBeenCalledWith(
expect.objectContaining({ result: 42 }),
"tracing:test:asyncEnd"
);
});
it("should publish error on async rejection", async () => {
const tc = new TracingChannel("test");
const errorHandler = vi.fn();
tc.subscribe({
error: errorHandler,
});
const testError = new Error("async error");
await expect(tc.tracePromise(async () => {
throw testError;
}, {})).rejects.toThrow(testError);
expect(errorHandler).toHaveBeenCalledWith(
expect.objectContaining({ error: testError }),
"tracing:test:error"
);
});
it("should share context across all events", async () => {
const tc = new TracingChannel("test");
const contexts: any[] = [];
tc.subscribe({
start: (ctx) => contexts.push(ctx),
end: (ctx) => contexts.push(ctx),
asyncStart: (ctx) => contexts.push(ctx),
asyncEnd: (ctx) => contexts.push(ctx),
});
await tc.tracePromise(async () => 42, { shared: "data" });
// All should be the same context object
expect(contexts[0]).toBe(contexts[1]);
expect(contexts[1]).toBe(contexts[2]);
expect(contexts[2]).toBe(contexts[3]);
expect(contexts[0].shared).toBe("data");
});
});
describe("traceCallback", () => {
it("should wrap callback and publish events", async () => {
const tc = new TracingChannel("test");
const startHandler = vi.fn();
const endHandler = vi.fn();
const asyncStartHandler = vi.fn();
const asyncEndHandler = vi.fn();
tc.subscribe({
start: startHandler,
end: endHandler,
asyncStart: asyncStartHandler,
asyncEnd: asyncEndHandler,
});
const fn = (cb: (err: any, result: any) => void) => {
setTimeout(() => cb(null, 42), 10);
};
const { err, result } = await new Promise<{ err: any; result: any }>((resolve) => {
const wrappedCallback = (err: any, result: any) => {
resolve({ err, result });
};
tc.traceCallback(fn, -1, { context: "data" }, undefined, wrappedCallback);
});
expect(err).toBeNull();
expect(result).toBe(42);
expect(startHandler).toHaveBeenCalled();
expect(endHandler).toHaveBeenCalled();
expect(asyncStartHandler).toHaveBeenCalled();
expect(asyncEndHandler).toHaveBeenCalled();
});
it("should publish error on callback error", async () => {
const tc = new TracingChannel("test");
const errorHandler = vi.fn();
tc.subscribe({
error: errorHandler,
});
const testError = new Error("callback error");
const fn = (cb: (err: any, result: any) => void) => {
setTimeout(() => cb(testError, null), 10);
};
const { err } = await new Promise<{ err: any }>((resolve) => {
const wrappedCallback = (err: any, result: any) => {
setTimeout(() => resolve({ err }), 0);
};
tc.traceCallback(fn, -1, { context: "data" }, undefined, wrappedCallback);
});
expect(err).toBe(testError);
expect(errorHandler).toHaveBeenCalledWith(
expect.objectContaining({ error: testError }),
"tracing:test:error"
);
});
it("should handle custom callback position", async () => {
const tc = new TracingChannel("test");
const startHandler = vi.fn();
tc.subscribe({
start: startHandler,
});
const fn = (a: number, cb: (err: any, result: any) => void, b: number) => {
setTimeout(() => cb(null, a + b), 10);
};
const { result } = await new Promise<{ result: any }>((resolve) => {
const wrappedCallback = (err: any, result: any) => {
setTimeout(() => resolve({ result }), 0);
};
tc.traceCallback(fn, 1, { context: "data" }, undefined, 5, wrappedCallback, 10);
});
expect(result).toBe(15);
expect(startHandler).toHaveBeenCalled();
});
it("should throw if callback is not a function", () => {
const tc = new TracingChannel("test");
tc.subscribe({ start: vi.fn() }); // Need subscribers to trigger check
const fn = (notACallback: string) => {};
expect(() => {
tc.traceCallback(fn, -1, {}, undefined, "not a function");
}).toThrow("callback must be a function");
});
});
describe("hasSubscribers optimization", () => {
it("should skip sync tracing when no subscribers", () => {
const tc = new TracingChannel("test");
const fn = vi.fn(() => 42);
const result = tc.traceSync(fn, {});
expect(result).toBe(42);
expect(fn).toHaveBeenCalled();
// No overhead when no subscribers
});
it("should skip async tracing when no subscribers", async () => {
const tc = new TracingChannel("test");
const fn = vi.fn(async () => 42);
const result = await tc.tracePromise(fn, {});
expect(result).toBe(42);
expect(fn).toHaveBeenCalled();
});
it("should skip callback tracing when no subscribers", async () => {
const tc = new TracingChannel("test");
const fn = (cb: (err: any, result: any) => void) => {
setTimeout(() => cb(null, 42), 10);
};
const { result } = await new Promise<{ result: any }>((resolve) => {
const wrappedCallback = (err: any, result: any) => {
resolve({ result });
};
tc.traceCallback(fn, -1, {}, undefined, wrappedCallback);
});
expect(result).toBe(42);
});
});
describe("partial channels behavior", () => {
it("should work with only start channel", () => {
const startCh = new Channel("start");
const tc = new TracingChannel({ start: startCh });
const startHandler = vi.fn();
startCh.subscribe(startHandler);
const result = tc.traceSync(() => 42, { data: "test" });
expect(result).toBe(42);
expect(startHandler).toHaveBeenCalledWith(
expect.objectContaining({ data: "test" }),
"start"
);
});
it("should work with only end channel", () => {
const endCh = new Channel("end");
const tc = new TracingChannel({ end: endCh });
const endHandler = vi.fn();
endCh.subscribe(endHandler);
const result = tc.traceSync(() => 42, { data: "test" });
expect(result).toBe(42);
expect(endHandler).toHaveBeenCalledWith(
expect.objectContaining({ data: "test" }),
"end"
);
});
it("should work with only error channel", () => {
const errorCh = new Channel("error");
const tc = new TracingChannel({ error: errorCh });
const errorHandler = vi.fn();
errorCh.subscribe(errorHandler);
const testError = new Error("test");
expect(() => tc.traceSync(() => { throw testError; }, {})).toThrow(testError);
expect(errorHandler).toHaveBeenCalledWith(
expect.objectContaining({ error: testError }),
"error"
);
});
it("should work with only asyncStart channel", async () => {
const asyncStartCh = new Channel("asyncStart");
const tc = new TracingChannel({ asyncStart: asyncStartCh });
const asyncStartHandler = vi.fn();
asyncStartCh.subscribe(asyncStartHandler);
const result = await tc.tracePromise(async () => 42, { data: "test" });
expect(result).toBe(42);
expect(asyncStartHandler).toHaveBeenCalledWith(
expect.objectContaining({ data: "test", result: 42 }),
"asyncStart"
);
});
it("should work with only asyncEnd channel", async () => {
const asyncEndCh = new Channel("asyncEnd");
const tc = new TracingChannel({ asyncEnd: asyncEndCh });
const asyncEndHandler = vi.fn();
asyncEndCh.subscribe(asyncEndHandler);
const result = await tc.tracePromise(async () => 42, { data: "test" });
expect(result).toBe(42);
expect(asyncEndHandler).toHaveBeenCalledWith(
expect.objectContaining({ data: "test", result: 42 }),
"asyncEnd"
);
});
});
describe("thisArg and args handling", () => {
it("should apply function with thisArg in traceSync", () => {
const tc = new TracingChannel("test");
tc.subscribe({ start: vi.fn() });
const context = { value: 42 };
function getThis(this: typeof context) {
return this.value;
}
const result = tc.traceSync(getThis, {}, context);
expect(result).toBe(42);
});
it("should apply function with args in traceSync", () => {
const tc = new TracingChannel("test");
tc.subscribe({ start: vi.fn() });
function add(a: number, b: number) {
return a + b;
}
const result = tc.traceSync(add, {}, undefined, 5, 10);
expect(result).toBe(15);
});
it("should apply function with thisArg and args in tracePromise", async () => {
const tc = new TracingChannel("test");
tc.subscribe({ start: vi.fn() });
const context = { multiplier: 2 };
async function multiply(this: typeof context, a: number, b: number) {
return (a + b) * this.multiplier;
}
const result = await tc.tracePromise(multiply, {}, context, 5, 10);
expect(result).toBe(30);
});
});
});
/**
* Browser-compatible implementation of Node.js TracingChannel.
*
* A TracingChannel is a composite of 5 individual channels:
* - start: Published before sync operation
* - end: Published after sync operation completes
* - asyncStart: Published when async operation's promise starts resolving
* - asyncEnd: Published when async operation's promise finishes resolving
* - error: Published when operation throws/rejects
*/
import { Channel } from "./channel";
export interface Channels<M> {
readonly start?: Channel<M>;
readonly end?: Channel<M>;
readonly asyncStart?: Channel<M>;
readonly asyncEnd?: Channel<M>;
readonly error?: Channel<M>;
}
export interface ChannelHandlers<M> {
start?: (context: M, name: string) => void;
end?: (context: M, name: string) => void;
asyncStart?: (context: M, name: string) => void;
asyncEnd?: (context: M, name: string) => void;
error?: (context: M, name: string) => void;
}
export class TracingChannel<M> {
public readonly start?: Channel<M>;
public readonly end?: Channel<M>;
public readonly asyncStart?: Channel<M>;
public readonly asyncEnd?: Channel<M>;
public readonly error?: Channel<M>;
constructor(nameOrChannels: string | Channels<M>) {
if (typeof nameOrChannels === 'string') {
// Create 5 separate channels with tracing: prefix
this.start = new Channel(`tracing:${nameOrChannels}:start`);
this.end = new Channel(`tracing:${nameOrChannels}:end`);
this.asyncStart = new Channel(`tracing:${nameOrChannels}:asyncStart`);
this.asyncEnd = new Channel(`tracing:${nameOrChannels}:asyncEnd`);
this.error = new Channel(`tracing:${nameOrChannels}:error`);
} else {
this.start = nameOrChannels.start
this.end = nameOrChannels.end
this.asyncStart = nameOrChannels.asyncStart
this.asyncEnd = nameOrChannels.asyncEnd
this.error = nameOrChannels.error
}
}
get hasSubscribers(): boolean {
return this.start?.hasSubscribers ||
this.end?.hasSubscribers ||
this.asyncStart?.hasSubscribers ||
this.asyncEnd?.hasSubscribers ||
this.error?.hasSubscribers ||
false;
}
subscribe(handlers: ChannelHandlers<M>): void {
if (handlers.start) {
this.start?.subscribe(handlers.start);
}
if (handlers.end) {
this.end?.subscribe(handlers.end);
}
if (handlers.asyncStart) {
this.asyncStart?.subscribe(handlers.asyncStart);
}
if (handlers.asyncEnd) {
this.asyncEnd?.subscribe(handlers.asyncEnd);
}
if (handlers.error) {
this.error?.subscribe(handlers.error);
}
}
unsubscribe(handlers: ChannelHandlers<M>): boolean {
let done = true;
if (handlers.start && !this.start?.unsubscribe(handlers.start)) {
done = false;
}
if (handlers.end && !this.end?.unsubscribe(handlers.end)) {
done = false;
}
if (handlers.asyncStart && !this.asyncStart?.unsubscribe(handlers.asyncStart)) {
done = false;
}
if (handlers.asyncEnd && !this.asyncEnd?.unsubscribe(handlers.asyncEnd)) {
done = false;
}
if (handlers.error && !this.error?.unsubscribe(handlers.error)) {
done = false;
}
return done;
}
#maybeRunStores<F extends (...args: any[]) => any>(
channel: Channel<M> | undefined,
message: M,
fn: F,
thisArg?: ThisParameterType<F>,
...args: Parameters<F>
): ReturnType<F> {
if (!channel) {
return Reflect.apply(fn, thisArg, args);
}
return channel.runStores(message, fn, thisArg, ...args)
}
traceSync<F extends (...args: any[]) => any>(
fn: F,
message: M,
thisArg?: ThisParameterType<F>,
...args: Parameters<F>
): ReturnType<F> {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
return this.#maybeRunStores(
this.start,
message,
() => {
try {
const result = Reflect.apply(fn, thisArg, args);
(message as any).result = result;
return result;
} catch (err) {
(message as any).error = err;
this.error?.publish(message);
throw err;
} finally {
this.end?.publish(message);
}
}
);
}
tracePromise<F extends (...args: any[]) => any>(
fn: F,
message: M,
thisArg?: ThisParameterType<F>,
...args: Parameters<F>
): Promise<ReturnType<F>> {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
const { start, end, asyncStart, asyncEnd, error } = this;
function reject(err: any) {
(message as any).error = err;
error?.publish(message);
asyncStart?.publish(message);
asyncEnd?.publish(message);
return Promise.reject(err);
}
function resolve(result: ReturnType<F>) {
(message as any).result = result;
asyncStart?.publish(message);
asyncEnd?.publish(message);
return result;
}
return this.#maybeRunStores(
start,
message,
() => {
try {
let promise = Reflect.apply(fn, thisArg, args);
// Convert thenables to native promises
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise);
}
return promise.then(resolve, reject);
} catch (err) {
(message as any).error = err;
error?.publish(message);
throw err;
} finally {
end?.publish(message);
}
}
);
}
traceCallback<F extends (...args: any[]) => any>(
fn: F,
position: number = -1,
message: M,
thisArg?: ThisParameterType<F>,
...args: Parameters<F>
): ReturnType<F> {
if (!this.hasSubscribers) {
return Reflect.apply(fn, thisArg, args);
}
const { start, end, asyncStart, asyncEnd, error } = this;
const self = this;
function wrappedCallback(err: any, res: any) {
if (err) {
(message as any).error = err;
error?.publish(message);
} else {
(message as any).result = res;
}
self.#maybeRunStores(
asyncStart,
message,
() => {
try {
return Reflect.apply(callback, thisArg, arguments);
} finally {
asyncEnd?.publish(message);
}
}
);
}
// Get the callback from args
const callback = args.at(position);
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
// Replace callback in args
args.splice(position, 1, wrappedCallback);
return this.#maybeRunStores(
start,
message,
() => {
try {
return Reflect.apply(fn, thisArg, args);
} catch (err) {
(message as any).error = err;
error?.publish(message);
throw err;
} finally {
end?.publish(message);
}
}
);
}
}
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"lib": ["es2022", "DOM"],
"target": "es2022",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
import { defineConfig } from "tsup";
export default defineConfig([
{
entry: ["src/index.ts"],
format: ["cjs", "esm"],
outDir: "dist",
dts: true,
platform: "browser",
},
]);
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
},
});