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
107 lines
3.8 KiB
TypeScript
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]>>;
|
|
},
|
|
};
|
|
} |