@ws-kit/bun
Version:
Bun platform adapter for WS-Kit leveraging native WebSocket API with built-in pub/sub and low-latency message routing
161 lines • 6.45 kB
JavaScript
// SPDX-FileCopyrightText: 2025-present Kriasoft
// SPDX-License-Identifier: MIT
/**
* Bun-native Pub/Sub adapter leveraging server.publish().
*
* Uses Bun's event-loop integrated broadcasting with zero-copy semantics.
* All WebSocket connections that have called `ws.subscribe(topic)` will
* receive messages published via this adapter.
*
* **Performance characteristics**:
* - Zero-copy: Messages are broadcast directly without serialization overhead
* - In-process: All subscribers must be in the same Bun instance
* - Synchronous: Messages are delivered immediately to all subscribers
*
* **Scope**: Messages published to a topic are received by ALL WebSocket
* connections in this Bun process that have subscribed to that topic.
* For multi-process deployments (load-balanced cluster), each instance has
* its own scope—use RedisPubSubAdapter for cross-process broadcasting.
*
* **Note**: For public API, use `bunPubSub()` factory function instead.
*
* @internal Use `bunPubSub(server)` factory for creating instances.
*/
export class BunPubSub {
server;
constructor(server) {
this.server = server;
}
/**
* Publish a message to a topic.
*
* All WebSocket connections subscribed to this topic (via ws.subscribe)
* will receive the message immediately.
*
* Note: Bun's native pub/sub is too primitive to support excludeSelf or
* track subscriber counts. Both are rejected with UNSUPPORTED errors.
* Capability is always "unknown" (can't enumerate subscribers).
*
* @param envelope - Validated message with topic, payload, type, meta
* @param options - Publish options (partitionKey ignored, excludeSelf unsupported)
*/
async publish(envelope, options) {
// Bun.ServerWebSocket.publish doesn't support sender filtering.
// Reject explicitly to encourage explicit server-side filtering in handlers.
if (options?.excludeSelf === true) {
return {
ok: false,
error: "UNSUPPORTED",
retryable: false,
adapter: "BunPubSub",
details: {
feature: "excludeSelf",
reason: "Bun pub/sub has no sender context",
},
};
}
try {
// Serialize complete envelope (type, payload, meta) as JSON
// to preserve full message structure across pub/sub boundary
let data;
if (typeof envelope.payload === "string" &&
!envelope.type &&
!envelope.meta) {
// Optimization: if payload is plain string with no type/meta,
// send it directly as-is
data = envelope.payload;
}
else if (envelope.payload instanceof Uint8Array &&
!envelope.type &&
!envelope.meta) {
// Optimization: pass-through binary payload if no envelope metadata
data = envelope.payload;
}
else if (envelope.payload instanceof ArrayBuffer &&
!envelope.type &&
!envelope.meta) {
// Optimization: pass-through binary payload if no envelope metadata
data = envelope.payload;
}
else {
// General case: serialize full envelope to preserve type and meta
const message = { payload: envelope.payload };
if (envelope.type)
message.type = envelope.type;
if (envelope.meta)
message.meta = envelope.meta;
data = JSON.stringify(message);
}
// Broadcast to all subscribers in this Bun instance
// This is a synchronous operation in Bun's event loop
this.server.publish(envelope.topic, data);
// Return success with unknown capability (Bun doesn't expose subscriber count)
return {
ok: true,
capability: "unknown",
};
}
catch (cause) {
return {
ok: false,
error: "ADAPTER_ERROR",
retryable: true,
adapter: "BunPubSub",
details: {
cause: cause instanceof Error ? cause.message : String(cause),
},
};
}
}
/**
* Subscribe a client to a topic.
*
* **Note**: Bun's pub/sub is connection-based; subscriptions happen via
* `ws.subscribe(topic)` on the WebSocket. This is a no-op since actual
* subscription management is handled per-connection by the platform.
*
* @param clientId - Client identifier (unused; Bun handles per-connection)
* @param topic - Topic name
*/
async subscribe(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clientId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
topic) {
// Bun's pub/sub is connection-based. Subscriptions are managed via
// ws.subscribe(topic) on the WebSocket itself, not through this adapter method.
// This is a no-op.
}
/**
* Unsubscribe a client from a topic.
*
* **Note**: Like subscribe, this is a no-op for Bun as subscriptions are
* connection-based and managed via ws.unsubscribe(topic).
*
* @param clientId - Client identifier (unused)
* @param topic - Topic name (unused)
*/
async unsubscribe(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clientId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
topic) {
// No-op for Bun pub/sub
}
/**
* Get local subscribers for a topic.
*
* **Note**: Bun's pub/sub doesn't expose subscriber tracking. This always
* returns an empty async iterable since we cannot enumerate subscribers.
* The router will attempt delivery to all connections separately.
*
* @param topic - Topic name (unused; no subscriber tracking)
*/
async *getSubscribers(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
topic) {
// Bun doesn't expose subscriber enumeration, so we yield nothing.
// The router will handle delivery to subscribed WebSocket connections.
}
}
//# sourceMappingURL=pubsub.js.map