Files
pubsub/src/create_pubsub.ts
glm-5.1 be7fe67145 Fix critical publish() bug, address review findings
CRITICAL: createPubSub.publish() was dispatching CustomEvent with
just the event type (e.g. 'call.responded') instead of the composite
topic string ('call.responded:uuid-123'). This broke all adapters
that rely on topic-scoped dispatch — Redis subscribe/publish
channels didn't match, and WS server fan-out routing would fail.
Fixed to dispatch with the full type:id composite.

Other fixes:
- Add __ prefix runtime guard in publish() (reserved for control)
- Add Redis barrel re-export to src/index.ts (ADR-002 compliance)
- Clarify WS server: adapter's onclose calls removeConnection
  internally; user doesn't need to
- WS client: document null callback no-op, removeEventListener
  edge cases (unregistered callback, null callback)
- WS server: document dispatchEvent always returns true
- Redis spec: document in-flight message edge case after unsubscribe
- Worker adapter: rename createMainThreadEventTarget to
  createWorkerThreadEventTarget, createWorkerEventTarget to
  createWorkerHostEventTarget (fix inverted naming)
- api-surface.md: add PubSub.publish() section documenting the
  type:id composite and __ guard
2026-05-08 05:17:43 +00:00

107 lines
3.8 KiB
TypeScript

/*
* Adapted from @graphql-yoga/subscription
* Original source: https://github.com/graphql-hive/graphql-yoga
* License: MIT
*
* Copyright (c) 2024 The Guild, GraphQL Yoga Contributors
*
* 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.
*/
import { Repeater } from "./repeater.js";
import type { TypedEventTarget, TypedEvent, EventEnvelope } from "./types.js";
export type PubSubEventMap = {
[eventType: string]: unknown;
};
export type PubSubEvent<
TEventMap extends PubSubEventMap,
TType extends Extract<keyof TEventMap, string> = Extract<keyof TEventMap, string>,
> = TypedEvent<TType, EventEnvelope<TType, TEventMap[TType]>>;
export type PubSubEventTarget<TEventMap extends PubSubEventMap> =
TypedEventTarget<
PubSubEvent<TEventMap>
>;
export type PubSubConfig<TEventMap extends PubSubEventMap> = {
eventTarget?: PubSubEventTarget<TEventMap>;
};
export type PubSub<TEventMap extends PubSubEventMap> = {
publish<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
payload: TEventMap[TType],
): void;
subscribe<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
): Repeater<EventEnvelope<TType, TEventMap[TType]>>;
};
export function createPubSub<TEventMap extends PubSubEventMap>(
config?: PubSubConfig<TEventMap>,
): PubSub<TEventMap> {
const target =
config?.eventTarget ?? (new EventTarget() as PubSubEventTarget<TEventMap>);
return {
publish<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
payload: TEventMap[TType],
) {
if (type.startsWith("__")) {
throw new Error(
`Event types starting with "__" are reserved for adapter control messages. Received: "${type}"`,
);
}
const envelope: EventEnvelope<TType, TEventMap[TType]> = { type, id, payload };
const topic = `${type}:${id}`;
const event = new CustomEvent(topic, { detail: envelope }) as PubSubEvent<
TEventMap,
TType
>;
target.dispatchEvent(event);
},
subscribe<TType extends Extract<keyof TEventMap, string>>(
type: TType,
id: string,
): Repeater<EventEnvelope<TType, TEventMap[TType]>> {
const topic = `${type}:${id}`;
return new Repeater(function subscriptionRepeater(
next: (value: unknown) => Promise<void>,
stop: Promise<void>,
) {
function pubsubEventListener(event: CustomEvent) {
next(event.detail);
}
stop.then(function subscriptionRepeaterStopHandler() {
target.removeEventListener(topic as TType, pubsubEventListener as EventListener);
});
target.addEventListener(topic as TType, pubsubEventListener as EventListener, undefined);
}) as Repeater<EventEnvelope<TType, TEventMap[TType]>>;
},
};
}