@pellux/goodvibes-transport-realtime 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @pellux/goodvibes-transport-realtime
2
+
3
+ Realtime event-domain connectors for GoodVibes SSE and WebSocket integrations.
4
+
5
+ Install:
6
+
7
+ ```bash
8
+ npm install @pellux/goodvibes-transport-realtime
9
+ ```
10
+
11
+ Example:
12
+
13
+ ```ts
14
+ import {
15
+ createEventSourceConnector,
16
+ createRemoteRuntimeEvents,
17
+ } from '@pellux/goodvibes-transport-realtime';
18
+
19
+ const events = createRemoteRuntimeEvents(
20
+ createEventSourceConnector('https://goodvibes.example.com', 'token', fetch),
21
+ );
22
+ ```
23
+
24
+ Use this package when you want runtime-event subscriptions without pulling in the full umbrella SDK.
@@ -0,0 +1,18 @@
1
+ import { type RuntimeEventFeeds } from '@pellux/goodvibes-transport-core';
2
+ type EventLike = {
3
+ readonly type: string;
4
+ };
5
+ export interface SerializedEventEnvelope<TEvent extends EventLike = EventLike> {
6
+ readonly type: string;
7
+ readonly timestamp?: number;
8
+ readonly ts?: number;
9
+ readonly traceId?: string;
10
+ readonly sessionId?: string;
11
+ readonly source?: string;
12
+ readonly payload: TEvent;
13
+ }
14
+ export type DomainEventConnector<TDomain extends string, TEvent extends EventLike = EventLike> = (domain: TDomain, onEnvelope: (envelope: SerializedEventEnvelope<TEvent>) => void) => void | Promise<() => void>;
15
+ export type DomainEvents<TDomain extends string, TEvent extends EventLike = EventLike> = RuntimeEventFeeds<TDomain, TEvent>;
16
+ export declare function createRemoteDomainEvents<TDomain extends string, TEvent extends EventLike = EventLike>(domains: readonly TDomain[], connect: DomainEventConnector<TDomain, TEvent>): DomainEvents<TDomain, TEvent>;
17
+ export {};
18
+ //# sourceMappingURL=domain-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain-events.d.ts","sourceRoot":"","sources":["../src/domain-events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkD,KAAK,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAE1H,KAAK,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C,MAAM,WAAW,uBAAuB,CAAC,MAAM,SAAS,SAAS,GAAG,SAAS;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,oBAAoB,CAC9B,OAAO,SAAS,MAAM,EACtB,MAAM,SAAS,SAAS,GAAG,SAAS,IAClC,CACF,MAAM,EAAE,OAAO,EACf,UAAU,EAAE,CAAC,QAAQ,EAAE,uBAAuB,CAAC,MAAM,CAAC,KAAK,IAAI,KAC5D,IAAI,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;AAEhC,MAAM,MAAM,YAAY,CACtB,OAAO,SAAS,MAAM,EACtB,MAAM,SAAS,SAAS,GAAG,SAAS,IAClC,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AAmIvC,wBAAgB,wBAAwB,CACtC,OAAO,SAAS,MAAM,EACtB,MAAM,SAAS,SAAS,GAAG,SAAS,EAEpC,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,GAC7C,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAK/B"}
@@ -0,0 +1,116 @@
1
+ import { createRuntimeEventFeeds } from '@pellux/goodvibes-transport-core';
2
+ function addListener(map, type, listener) {
3
+ const listeners = map.get(type) ?? new Set();
4
+ listeners.add(listener);
5
+ map.set(type, listeners);
6
+ return () => {
7
+ const existing = map.get(type);
8
+ if (!existing)
9
+ return;
10
+ existing.delete(listener);
11
+ if (existing.size === 0) {
12
+ map.delete(type);
13
+ }
14
+ };
15
+ }
16
+ function hasAnyListener(map) {
17
+ for (const listeners of map.values()) {
18
+ if (listeners.size > 0)
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ function isExpectedDisconnectError(error) {
24
+ return (error instanceof DOMException && error.name === 'AbortError') || (typeof error === 'object'
25
+ && error !== null
26
+ && 'name' in error
27
+ && error.name === 'AbortError');
28
+ }
29
+ function toEventEnvelope(envelope) {
30
+ return {
31
+ type: envelope.type,
32
+ ts: typeof envelope.ts === 'number'
33
+ ? envelope.ts
34
+ : typeof envelope.timestamp === 'number'
35
+ ? envelope.timestamp
36
+ : Date.now(),
37
+ traceId: typeof envelope.traceId === 'string' ? envelope.traceId : 'transport-trace',
38
+ sessionId: typeof envelope.sessionId === 'string' ? envelope.sessionId : 'transport',
39
+ source: typeof envelope.source === 'string' ? envelope.source : 'transport',
40
+ payload: envelope.payload,
41
+ };
42
+ }
43
+ function createRemoteDomainEventFeed(domain, connect) {
44
+ const payloadListeners = new Map();
45
+ const envelopeListeners = new Map();
46
+ let disconnect = null;
47
+ let connectPromise = null;
48
+ let disconnectPending = false;
49
+ const hasListeners = () => (hasAnyListener(payloadListeners)
50
+ || hasAnyListener(envelopeListeners));
51
+ const maybeConnect = () => {
52
+ if (disconnect || connectPromise)
53
+ return;
54
+ connectPromise = Promise.resolve(connect(domain, (envelope) => {
55
+ const eventType = typeof envelope.type === 'string' ? envelope.type : '';
56
+ if (!eventType)
57
+ return;
58
+ const payload = envelope.payload;
59
+ const typedEnvelope = toEventEnvelope(envelope);
60
+ for (const listener of payloadListeners.get(eventType) ?? []) {
61
+ listener(payload);
62
+ }
63
+ for (const listener of envelopeListeners.get(eventType) ?? []) {
64
+ listener(typedEnvelope);
65
+ }
66
+ })).then((cleanup) => {
67
+ if (typeof cleanup !== 'function')
68
+ return;
69
+ if (disconnectPending && !hasListeners()) {
70
+ cleanup();
71
+ return;
72
+ }
73
+ disconnect = cleanup;
74
+ }).catch((error) => {
75
+ if (!isExpectedDisconnectError(error)) {
76
+ throw error;
77
+ }
78
+ }).finally(() => {
79
+ connectPromise = null;
80
+ disconnectPending = false;
81
+ });
82
+ };
83
+ const maybeDisconnect = () => {
84
+ if (hasListeners())
85
+ return;
86
+ if (disconnect) {
87
+ disconnect();
88
+ disconnect = null;
89
+ return;
90
+ }
91
+ if (connectPromise) {
92
+ disconnectPending = true;
93
+ }
94
+ };
95
+ return {
96
+ on(type, listener) {
97
+ const unsubscribe = addListener(payloadListeners, type, listener);
98
+ maybeConnect();
99
+ return () => {
100
+ unsubscribe();
101
+ maybeDisconnect();
102
+ };
103
+ },
104
+ onEnvelope(type, listener) {
105
+ const unsubscribe = addListener(envelopeListeners, type, listener);
106
+ maybeConnect();
107
+ return () => {
108
+ unsubscribe();
109
+ maybeDisconnect();
110
+ };
111
+ },
112
+ };
113
+ }
114
+ export function createRemoteDomainEvents(domains, connect) {
115
+ return createRuntimeEventFeeds(domains, (domain) => createRemoteDomainEventFeed(domain, connect));
116
+ }
@@ -0,0 +1,6 @@
1
+ export type { DomainEventConnector, DomainEvents, SerializedEventEnvelope, } from './domain-events.js';
2
+ export { createRemoteDomainEvents } from './domain-events.js';
3
+ export type { RemoteRuntimeEvents, SerializedRuntimeEnvelope } from './runtime-events.js';
4
+ export { buildEventSourceUrl, buildWebSocketUrl, createEventSourceConnector, createRemoteRuntimeEvents, createWebSocketConnector, } from './runtime-events.js';
5
+ export type { RuntimeEventConnectorOptions } from './runtime-events.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,YAAY,EACZ,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createRemoteDomainEvents } from './domain-events.js';
2
+ export { buildEventSourceUrl, buildWebSocketUrl, createEventSourceConnector, createRemoteRuntimeEvents, createWebSocketConnector, } from './runtime-events.js';
@@ -0,0 +1,20 @@
1
+ import { type RuntimeEventDomain } from '@pellux/goodvibes-contracts';
2
+ import { type AuthTokenResolver, type StreamReconnectPolicy } from '@pellux/goodvibes-transport-http';
3
+ import { type DomainEventConnector, type DomainEvents, type SerializedEventEnvelope } from './domain-events.js';
4
+ type RuntimeEventRecord = {
5
+ readonly type: string;
6
+ };
7
+ export type SerializedRuntimeEnvelope<TEvent extends RuntimeEventRecord = RuntimeEventRecord> = SerializedEventEnvelope<TEvent>;
8
+ export type RemoteRuntimeEvents<TEvent extends RuntimeEventRecord = RuntimeEventRecord> = DomainEvents<RuntimeEventDomain, TEvent>;
9
+ export interface RuntimeEventConnectorOptions {
10
+ readonly reconnect?: StreamReconnectPolicy;
11
+ readonly onError?: (error: unknown) => void;
12
+ }
13
+ type AuthTokenSource = string | null | undefined | AuthTokenResolver;
14
+ export declare function createRemoteRuntimeEvents<TEvent extends RuntimeEventRecord = RuntimeEventRecord>(connect: DomainEventConnector<RuntimeEventDomain, TEvent>): RemoteRuntimeEvents<TEvent>;
15
+ export declare function buildEventSourceUrl(baseUrl: string, domain: RuntimeEventDomain): string;
16
+ export declare function buildWebSocketUrl(baseUrl: string, domains: readonly RuntimeEventDomain[]): string;
17
+ export declare function createEventSourceConnector(baseUrl: string, token: AuthTokenSource, fetchImpl: typeof fetch, options?: RuntimeEventConnectorOptions): DomainEventConnector<RuntimeEventDomain, RuntimeEventRecord>;
18
+ export declare function createWebSocketConnector(baseUrl: string, token: AuthTokenSource, WebSocketImpl: typeof WebSocket, options?: RuntimeEventConnectorOptions): DomainEventConnector<RuntimeEventDomain, RuntimeEventRecord>;
19
+ export {};
20
+ //# sourceMappingURL=runtime-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-events.d.ts","sourceRoot":"","sources":["../src/runtime-events.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAC7F,OAAO,EAAoB,KAAK,iBAAiB,EAAE,KAAK,qBAAqB,EAA6D,MAAM,kCAAkC,CAAC;AAEnL,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAE5B,KAAK,kBAAkB,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,MAAM,MAAM,yBAAyB,CAAC,MAAM,SAAS,kBAAkB,GAAG,kBAAkB,IAC1F,uBAAuB,CAAC,MAAM,CAAC,CAAC;AAElC,MAAM,MAAM,mBAAmB,CAAC,MAAM,SAAS,kBAAkB,GAAG,kBAAkB,IACpF,YAAY,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;AAE3C,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C;AAED,KAAK,eAAe,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAAC;AASrE,wBAAgB,yBAAyB,CAAC,MAAM,SAAS,kBAAkB,GAAG,kBAAkB,EAC9F,OAAO,EAAE,oBAAoB,CAAC,kBAAkB,EAAE,MAAM,CAAC,GACxD,mBAAmB,CAAC,MAAM,CAAC,CAK7B;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,kBAAkB,GACzB,MAAM,CAIR;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,kBAAkB,EAAE,GACrC,MAAM,CAQR;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,OAAO,KAAK,EACvB,OAAO,GAAE,4BAAiC,GACzC,oBAAoB,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAiB9D;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,EACtB,aAAa,EAAE,OAAO,SAAS,EAC/B,OAAO,GAAE,4BAAiC,GACzC,oBAAoB,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,CAuF9D"}
@@ -0,0 +1,134 @@
1
+ // Synced from goodvibes-tui/src/runtime/transports/runtime-events-client.ts
2
+ import { RUNTIME_EVENT_DOMAINS } from '@pellux/goodvibes-contracts';
3
+ import { resolveAuthToken, openRawServerSentEventStream as openServerSentEventStream } from '@pellux/goodvibes-transport-http';
4
+ import { buildUrl, normalizeBaseUrl } from '@pellux/goodvibes-transport-http';
5
+ import { createRemoteDomainEvents, } from './domain-events.js';
6
+ async function resolveAuthTokenSource(source) {
7
+ if (typeof source === 'function') {
8
+ return await resolveAuthToken(null, source);
9
+ }
10
+ return source ?? null;
11
+ }
12
+ export function createRemoteRuntimeEvents(connect) {
13
+ return createRemoteDomainEvents(RUNTIME_EVENT_DOMAINS, connect);
14
+ }
15
+ export function buildEventSourceUrl(baseUrl, domain) {
16
+ const url = new URL(buildUrl(baseUrl, '/api/control-plane/events'));
17
+ url.searchParams.set('domains', domain);
18
+ return url.toString();
19
+ }
20
+ export function buildWebSocketUrl(baseUrl, domains) {
21
+ const base = normalizeBaseUrl(baseUrl);
22
+ const url = new URL('/api/control-plane/ws', base.replace(/^http(s?):\/\//, 'ws$1://'));
23
+ url.searchParams.set('clientKind', 'web');
24
+ if (domains.length > 0) {
25
+ url.searchParams.set('domains', domains.join(','));
26
+ }
27
+ return url.toString();
28
+ }
29
+ export function createEventSourceConnector(baseUrl, token, fetchImpl, options = {}) {
30
+ const handleError = options.onError ?? (options.reconnect?.enabled ? (() => { }) : undefined);
31
+ return async (domain, onEnvelope) => {
32
+ const url = buildEventSourceUrl(baseUrl, domain);
33
+ return await openServerSentEventStream(fetchImpl, url, {
34
+ onEvent: (eventName, payload) => {
35
+ if (eventName !== domain)
36
+ return;
37
+ if (!payload || typeof payload !== 'object')
38
+ return;
39
+ onEnvelope(payload);
40
+ },
41
+ onError: handleError,
42
+ }, {
43
+ reconnect: options.reconnect,
44
+ getAuthToken: typeof token === 'function' ? token : undefined,
45
+ authToken: typeof token === 'function' ? null : token,
46
+ });
47
+ };
48
+ }
49
+ export function createWebSocketConnector(baseUrl, token, WebSocketImpl, options = {}) {
50
+ return async (domain, onEnvelope) => {
51
+ const url = buildWebSocketUrl(baseUrl, [domain]);
52
+ const reconnect = options.reconnect;
53
+ const enabled = reconnect?.enabled ?? false;
54
+ let stopped = false;
55
+ let reconnectAttempt = 0;
56
+ let socket = null;
57
+ let reconnectTimer = null;
58
+ const closeSocket = () => {
59
+ if (!socket)
60
+ return;
61
+ socket.removeEventListener('open', onOpen);
62
+ socket.removeEventListener('message', onMessage);
63
+ socket.removeEventListener('close', onClose);
64
+ socket.removeEventListener('error', onError);
65
+ socket.close();
66
+ socket = null;
67
+ };
68
+ const scheduleReconnect = () => {
69
+ if (!enabled || stopped)
70
+ return;
71
+ const nextAttempt = reconnectAttempt + 1;
72
+ const maxAttempts = reconnect?.maxAttempts ?? Number.POSITIVE_INFINITY;
73
+ if (nextAttempt >= maxAttempts)
74
+ return;
75
+ reconnectAttempt = nextAttempt;
76
+ const baseDelayMs = reconnect?.baseDelayMs ?? 500;
77
+ const maxDelayMs = reconnect?.maxDelayMs ?? 5_000;
78
+ const backoffFactor = reconnect?.backoffFactor ?? 2;
79
+ const delayMs = Math.min(maxDelayMs, Math.floor(baseDelayMs * (backoffFactor ** Math.max(0, nextAttempt - 1))));
80
+ reconnectTimer = setTimeout(() => {
81
+ reconnectTimer = null;
82
+ void connect();
83
+ }, delayMs);
84
+ };
85
+ const onOpen = async () => {
86
+ reconnectAttempt = 0;
87
+ const authToken = await resolveAuthTokenSource(token);
88
+ if (!authToken || !socket)
89
+ return;
90
+ socket.send(JSON.stringify({
91
+ type: 'auth',
92
+ token: authToken,
93
+ domains: [domain],
94
+ }));
95
+ };
96
+ const onMessage = (event) => {
97
+ try {
98
+ const frame = JSON.parse(event.data);
99
+ if (frame.type === 'event' && frame.event === domain && frame.payload && typeof frame.payload === 'object') {
100
+ onEnvelope(frame.payload);
101
+ }
102
+ }
103
+ catch {
104
+ // Ignore malformed frames.
105
+ }
106
+ };
107
+ const onClose = () => {
108
+ closeSocket();
109
+ scheduleReconnect();
110
+ };
111
+ const onError = (event) => {
112
+ options.onError?.(event);
113
+ };
114
+ const connect = async () => {
115
+ if (stopped)
116
+ return;
117
+ closeSocket();
118
+ const nextSocket = new WebSocketImpl(url);
119
+ socket = nextSocket;
120
+ nextSocket.addEventListener('open', onOpen);
121
+ nextSocket.addEventListener('message', onMessage);
122
+ nextSocket.addEventListener('close', onClose);
123
+ nextSocket.addEventListener('error', onError);
124
+ };
125
+ await connect();
126
+ return () => {
127
+ stopped = true;
128
+ if (reconnectTimer) {
129
+ clearTimeout(reconnectTimer);
130
+ }
131
+ closeSocket();
132
+ };
133
+ };
134
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@pellux/goodvibes-transport-realtime",
3
+ "version": "0.18.3",
4
+ "description": "Realtime event-domain connectors for GoodVibes SSE and WebSocket integrations.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mgd34msu/goodvibes-sdk.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/mgd34msu/goodvibes-sdk/issues"
26
+ },
27
+ "homepage": "https://github.com/mgd34msu/goodvibes-sdk",
28
+ "keywords": [
29
+ "goodvibes",
30
+ "sdk",
31
+ "realtime",
32
+ "websocket",
33
+ "events"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@pellux/goodvibes-contracts": "0.18.3",
40
+ "@pellux/goodvibes-transport-core": "0.18.3",
41
+ "@pellux/goodvibes-transport-http": "0.18.3"
42
+ }
43
+ }