@wix/realtime 1.0.15 → 1.0.17

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.
Files changed (45) hide show
  1. package/cjs/build/constants.d.ts +1 -0
  2. package/cjs/build/constants.js +4 -0
  3. package/cjs/build/index.d.ts +3 -0
  4. package/{build/cjs → cjs/build}/index.js +3 -1
  5. package/cjs/build/publisher/publisher.context.d.ts +37 -0
  6. package/cjs/build/publisher/publisher.context.js +40 -0
  7. package/cjs/build/publisher/publisher.public.d.ts +12 -0
  8. package/cjs/build/publisher/publisher.public.js +74 -0
  9. package/cjs/build/subscriber/errors.d.ts +16 -0
  10. package/cjs/build/subscriber/errors.js +70 -0
  11. package/cjs/build/subscriber/subscriber.context.d.ts +4 -0
  12. package/cjs/build/subscriber/subscriber.context.js +7 -0
  13. package/cjs/build/subscriber/subscriber.public.d.ts +21 -0
  14. package/cjs/build/subscriber/subscriber.public.js +234 -0
  15. package/cjs/build/subscriber/subscription-manager.d.ts +26 -0
  16. package/cjs/build/subscriber/subscription-manager.js +75 -0
  17. package/cjs/build/types.d.ts +1 -0
  18. package/cjs/build/types.js +6 -0
  19. package/cjs/package.json +3 -0
  20. package/dist/statics/docs-ts-model.json +1136 -0
  21. package/es/build/constants.d.ts +1 -0
  22. package/es/build/constants.js +1 -0
  23. package/es/build/index.d.ts +3 -0
  24. package/es/build/index.js +3 -0
  25. package/es/build/publisher/publisher.context.d.ts +37 -0
  26. package/es/build/publisher/publisher.context.js +37 -0
  27. package/es/build/publisher/publisher.public.d.ts +12 -0
  28. package/es/build/publisher/publisher.public.js +38 -0
  29. package/es/build/subscriber/errors.d.ts +16 -0
  30. package/es/build/subscriber/errors.js +59 -0
  31. package/es/build/subscriber/subscriber.context.d.ts +4 -0
  32. package/es/build/subscriber/subscriber.context.js +4 -0
  33. package/es/build/subscriber/subscriber.public.d.ts +21 -0
  34. package/es/build/subscriber/subscriber.public.js +229 -0
  35. package/es/build/subscriber/subscription-manager.d.ts +26 -0
  36. package/es/build/subscriber/subscription-manager.js +71 -0
  37. package/es/build/types.d.ts +1 -0
  38. package/es/build/types.js +1 -0
  39. package/package.json +34 -30
  40. package/types/package.json +7 -0
  41. package/build/cjs/index.d.ts +0 -1
  42. package/build/es/index.d.ts +0 -1
  43. package/build/es/index.js +0 -37
  44. package/build/tsconfig.cjs.tsbuildinfo +0 -1
  45. package/build/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1 @@
1
+ export declare const DUPLEXER_HOST_URL = "duplexer.wix.com";
@@ -0,0 +1 @@
1
+ export const DUPLEXER_HOST_URL = 'duplexer.wix.com';
@@ -0,0 +1,3 @@
1
+ export * as site from '@wix/sdk_realtime_site';
2
+ export * as publisher from './publisher/publisher.context.js';
3
+ export * as subscriber from './subscriber/subscriber.context.js';
@@ -0,0 +1,3 @@
1
+ export * as site from '@wix/sdk_realtime_site';
2
+ export * as publisher from './publisher/publisher.context.js';
3
+ export * as subscriber from './subscriber/subscriber.context.js';
@@ -0,0 +1,37 @@
1
+ import { publish as publicPublish } from './publisher.public.js';
2
+ import type { BuildRESTFunction, MaybeContext } from '@wix/sdk-types';
3
+ /**
4
+ * Publishes a message to a realtime channel.
5
+ *
6
+ * Must be called from a backend function (server-to-server) as it requires
7
+ * app-level permissions to publish messages via the Duplexer API.
8
+ *
9
+ * There are two ways to use this function depending on the Wix client type:
10
+ *
11
+ * **Manual client (self-hosted / explicit auth) — no elevation needed:**
12
+ * The client's own credentials (instanceId, metaSiteId) are used directly.
13
+ * This ensures messages are scoped to the correct site.
14
+ *
15
+ * ```ts
16
+ * import { publisher } from '@wix/realtime';
17
+ * import { createClient, AppStrategy } from '@wix/sdk';
18
+ *
19
+ * const client = createClient({
20
+ * modules: { publisher },
21
+ * auth: AppStrategy({ appId, appSecret, instanceId }),
22
+ * });
23
+ * await client.publisher.publish({ name: 'my-channel' }, { message: 'hello' });
24
+ * ```
25
+ *
26
+ * **Contextual client (Wix-hosted) — elevation needed:**
27
+ * The contextual httpClient may not have sufficient permissions on its own.
28
+ * Wrap the call with `elevate()` to obtain the host's backend credentials.
29
+ *
30
+ * ```ts
31
+ * import { publisher } from '@wix/realtime';
32
+ * import { elevate } from '@wix/sdk/context';
33
+ *
34
+ * await elevate(publisher.publish)({ name: 'my-channel' }, { message: 'hello' });
35
+ * ```
36
+ */
37
+ export declare const publish: MaybeContext<BuildRESTFunction<typeof publicPublish> & typeof publicPublish>;
@@ -0,0 +1,37 @@
1
+ import { publish as publicPublish } from './publisher.public.js';
2
+ import { createRESTModule } from '@wix/sdk-runtime/rest-modules';
3
+ /**
4
+ * Publishes a message to a realtime channel.
5
+ *
6
+ * Must be called from a backend function (server-to-server) as it requires
7
+ * app-level permissions to publish messages via the Duplexer API.
8
+ *
9
+ * There are two ways to use this function depending on the Wix client type:
10
+ *
11
+ * **Manual client (self-hosted / explicit auth) — no elevation needed:**
12
+ * The client's own credentials (instanceId, metaSiteId) are used directly.
13
+ * This ensures messages are scoped to the correct site.
14
+ *
15
+ * ```ts
16
+ * import { publisher } from '@wix/realtime';
17
+ * import { createClient, AppStrategy } from '@wix/sdk';
18
+ *
19
+ * const client = createClient({
20
+ * modules: { publisher },
21
+ * auth: AppStrategy({ appId, appSecret, instanceId }),
22
+ * });
23
+ * await client.publisher.publish({ name: 'my-channel' }, { message: 'hello' });
24
+ * ```
25
+ *
26
+ * **Contextual client (Wix-hosted) — elevation needed:**
27
+ * The contextual httpClient may not have sufficient permissions on its own.
28
+ * Wrap the call with `elevate()` to obtain the host's backend credentials.
29
+ *
30
+ * ```ts
31
+ * import { publisher } from '@wix/realtime';
32
+ * import { elevate } from '@wix/sdk/context';
33
+ *
34
+ * await elevate(publisher.publish)({ name: 'my-channel' }, { message: 'hello' });
35
+ * ```
36
+ */
37
+ export const publish = /*#__PURE__*/ createRESTModule(publicPublish);
@@ -0,0 +1,12 @@
1
+ import type { Channel } from '@wix/realtime-common/types';
2
+ import type { HttpClient } from '@wix/sdk-types';
3
+ interface PublishSignature {
4
+ (channel: Channel, payload: Record<string, any>): Promise<void>;
5
+ }
6
+ /**
7
+ * Publish a message to a channel.
8
+ * @param channel - The channel, and optionally the resource, to publish to.
9
+ * @param payload - Message payload. Max: 10kb
10
+ */
11
+ export declare function publish(httpClient: HttpClient): PublishSignature;
12
+ export {};
@@ -0,0 +1,38 @@
1
+ import { publish as autoSdkPublish } from '@wix/auto_sdk_duplexer_publisher';
2
+ import { isVeloEnvironment, MESSAGE_EVENT_NAME } from '@wix/realtime-common';
3
+ import * as site from '@wix/sdk_realtime_site';
4
+ import { validateChannel } from '../subscriber/errors.js';
5
+ /**
6
+ * Publish a message to a channel.
7
+ * @param channel - The channel, and optionally the resource, to publish to.
8
+ * @param payload - Message payload. Max: 10kb
9
+ */
10
+ export function publish(httpClient) {
11
+ // autoSdkPublish is the auto-generated Duplexer publish module, wrapped by
12
+ // createRESTModule(descriptor). The raw descriptor has the signature:
13
+ // (httpClient) => (message, options) => Promise<void>
14
+ //
15
+ // We call it directly with our httpClient to bind the publish function to
16
+ // this client's auth credentials. This ensures the correct instanceId and
17
+ // metaSiteId are used in the publish request — critical for site-level
18
+ // message isolation in multi-site scenarios.
19
+ //
20
+ // Without this binding, the auto-generated module would resolve its own
21
+ // httpClient via the global Wix context, which always returns the host
22
+ // site's credentials — ignoring any manual client auth provided via
23
+ // createClient({ auth: AppStrategy({...}) }).
24
+ //
25
+ // For contextual usage (no explicit auth), the caller should wrap this
26
+ // function with elevate() to obtain the host's backend credentials:
27
+ // elevate(realtimePublisher.publish)(channel, payload)
28
+ const boundPublish = autoSdkPublish(httpClient);
29
+ return (channel, payload) => {
30
+ validateChannel(channel);
31
+ if (isVeloEnvironment()) {
32
+ return Promise.resolve(site.publish(channel, payload));
33
+ }
34
+ return boundPublish({ eventName: MESSAGE_EVENT_NAME, payload }, { channels: [channel.name], resourceId: channel.resourceId }).catch((error) => {
35
+ throw new Error(`Failed to publish: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
36
+ });
37
+ };
38
+ }
@@ -0,0 +1,16 @@
1
+ import { RealtimeError, type Channel } from '@wix/realtime-common/types';
2
+ export declare function missingChannelOrSubscriptionId(): RealtimeError;
3
+ export declare function channelMismatch(): RealtimeError;
4
+ export declare function invalidChannel(channel: Channel): RealtimeError;
5
+ export declare function validateChannel(channel: Channel): void;
6
+ export declare function disconnectedError(recoverable?: boolean, message?: string): RealtimeError;
7
+ export declare function connectError(recoverable?: boolean, message?: string): RealtimeError;
8
+ export declare function subscribeFailed(channel: Channel, payload?: {
9
+ status?: number;
10
+ message?: string;
11
+ }): RealtimeError;
12
+ export declare function resubscribeFailed(channel: Channel, payload?: {
13
+ status?: number;
14
+ message?: string;
15
+ }): RealtimeError;
16
+ export declare function subscribeError(error: unknown, channel: Channel): RealtimeError;
@@ -0,0 +1,59 @@
1
+ import { RealtimeError, RealtimeErrorCode, } from '@wix/realtime-common/types';
2
+ export function missingChannelOrSubscriptionId() {
3
+ return new RealtimeError(RealtimeErrorCode.BadInput, 'channel or subscriptionId is required');
4
+ }
5
+ export function channelMismatch() {
6
+ return new RealtimeError(RealtimeErrorCode.BadInput, 'channel does not match subscriptionId');
7
+ }
8
+ import { CHANNEL_NAME_MAX_LENGTH, CHANNEL_NAME_PATTERN, } from '@wix/realtime-common';
9
+ export function invalidChannel(channel) {
10
+ const { name, resourceId } = channel;
11
+ const reasons = [];
12
+ if (!name) {
13
+ reasons.push('name must not be empty');
14
+ }
15
+ else {
16
+ if (!CHANNEL_NAME_PATTERN.test(name)) {
17
+ reasons.push('name must contain only alphanumeric characters, hyphens, or underscores');
18
+ }
19
+ }
20
+ const combinedLength = (name?.length ?? 0) + (resourceId?.length ?? 0);
21
+ if (combinedLength > CHANNEL_NAME_MAX_LENGTH) {
22
+ reasons.push(`combined channel name and resourceId length is ${combinedLength} characters (max ${CHANNEL_NAME_MAX_LENGTH})`);
23
+ }
24
+ return new RealtimeError(RealtimeErrorCode.BadInput, `Invalid channel "${name}": ${reasons.join('; ')}.`);
25
+ }
26
+ export function validateChannel(channel) {
27
+ const { name, resourceId } = channel;
28
+ const combinedLength = (name?.length ?? 0) + (resourceId?.length ?? 0);
29
+ if (!name ||
30
+ !CHANNEL_NAME_PATTERN.test(name) ||
31
+ combinedLength > CHANNEL_NAME_MAX_LENGTH) {
32
+ throw invalidChannel(channel);
33
+ }
34
+ }
35
+ export function disconnectedError(recoverable, message) {
36
+ return new RealtimeError(RealtimeErrorCode.Disconnected, message || 'disconnected', { recoverable });
37
+ }
38
+ export function connectError(recoverable, message) {
39
+ return new RealtimeError(RealtimeErrorCode.ConnectionError, message || 'connection error', { recoverable });
40
+ }
41
+ function subscriptionFailed(prefix, channel, payload) {
42
+ const msg = payload?.message
43
+ ? `${prefix} failed: ${payload.message} (status ${payload.status})`
44
+ : `${prefix} failed`;
45
+ return new RealtimeError(RealtimeErrorCode.SubscriptionFailed, msg, {
46
+ channel,
47
+ recoverable: false,
48
+ status: payload?.status,
49
+ });
50
+ }
51
+ export function subscribeFailed(channel, payload) {
52
+ return subscriptionFailed('subscribe', channel, payload);
53
+ }
54
+ export function resubscribeFailed(channel, payload) {
55
+ return subscriptionFailed('resubscribe', channel, payload);
56
+ }
57
+ export function subscribeError(error, channel) {
58
+ return new RealtimeError(RealtimeErrorCode.SubscriptionFailed, error instanceof Error ? error.message : 'subscribe failed', { channel, recoverable: false });
59
+ }
@@ -0,0 +1,4 @@
1
+ import { subscribe as publicSubscribe, unsubscribe as publicUnsubscribe } from './subscriber.public.js';
2
+ import type { BuildRESTFunction, MaybeContext } from '@wix/sdk-types';
3
+ export declare const subscribe: MaybeContext<BuildRESTFunction<typeof publicSubscribe> & typeof publicSubscribe>;
4
+ export declare const unsubscribe: MaybeContext<BuildRESTFunction<typeof publicUnsubscribe> & typeof publicUnsubscribe>;
@@ -0,0 +1,4 @@
1
+ import { subscribe as publicSubscribe, unsubscribe as publicUnsubscribe, } from './subscriber.public.js';
2
+ import { createRESTModule } from '@wix/sdk-runtime/rest-modules';
3
+ export const subscribe = /*#__PURE__*/ createRESTModule(publicSubscribe);
4
+ export const unsubscribe = /*#__PURE__*/ createRESTModule(publicUnsubscribe);
@@ -0,0 +1,21 @@
1
+ import { type Channel, type MessageHandler, type SubscriptionOptions, type UnsubscribeOptions } from '@wix/realtime-common/types';
2
+ import type { HttpClient } from '@wix/sdk-types';
3
+ interface SubscribeSignature {
4
+ (channel: Channel, messageHandler: MessageHandler, options?: SubscriptionOptions): string;
5
+ }
6
+ /**
7
+ * Subscribe to a channel. Returns a subscriptionId immediately; the
8
+ * actual connection is established asynchronously in the background. Use the
9
+ * optional `options` parameter to handle subscription lifecycle events.
10
+ */
11
+ export declare function subscribe(httpClient: HttpClient): SubscribeSignature;
12
+ interface UnsubscribeSignature {
13
+ (options: UnsubscribeOptions): void;
14
+ }
15
+ /**
16
+ * Unsubscribe from a subscription by its ID, by channel, or both.
17
+ */
18
+ export declare function unsubscribe(_: HttpClient): UnsubscribeSignature;
19
+ /** @internal Resets all module-level state. Only for use in tests. */
20
+ export declare function _resetForTesting(): void;
21
+ export {};
@@ -0,0 +1,229 @@
1
+ import { Duplexer } from '@wix/duplexer-js';
2
+ import { DUPLEXER_EVENTS, MESSAGE_EVENT_NAME, getAppInfoFromAuthToken, tokenToAppInfoCache, getAuthToken, } from '@wix/realtime-common';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { DUPLEXER_HOST_URL } from '../constants.js';
5
+ import { missingChannelOrSubscriptionId, channelMismatch, disconnectedError, connectError, subscribeFailed, resubscribeFailed, subscribeError, validateChannel, } from './errors.js';
6
+ import { SubscriptionManager } from './subscription-manager.js';
7
+ const instanceConnections = new Map();
8
+ const subscriptionManager = new SubscriptionManager();
9
+ /**
10
+ * Subscribe to a channel. Returns a subscriptionId immediately; the
11
+ * actual connection is established asynchronously in the background. Use the
12
+ * optional `options` parameter to handle subscription lifecycle events.
13
+ */
14
+ export function subscribe(httpClient) {
15
+ return (channel, messageHandler, options) => {
16
+ validateChannel(channel);
17
+ const subscriptionId = `sub_${uuidv4()}`;
18
+ subscriptionManager.set(subscriptionId, {
19
+ channelName: channel.name,
20
+ channelResourceId: channel.resourceId,
21
+ removeListeners: () => { },
22
+ onSubscribed: options?.onSubscribed,
23
+ onSubscriptionError: options?.onSubscriptionError,
24
+ });
25
+ void setupSubscription(httpClient, subscriptionId, channel, messageHandler);
26
+ return subscriptionId;
27
+ };
28
+ }
29
+ /**
30
+ * Unsubscribe from a subscription by its ID, by channel, or both.
31
+ */
32
+ export function unsubscribe(_) {
33
+ return ({ channel, subscriptionId }) => {
34
+ if (!channel && !subscriptionId) {
35
+ throw missingChannelOrSubscriptionId();
36
+ }
37
+ if (subscriptionId) {
38
+ unsubscribeById(subscriptionId, channel);
39
+ }
40
+ else if (channel) {
41
+ unsubscribeByChannel(channel);
42
+ }
43
+ };
44
+ }
45
+ /** @internal Resets all module-level state. Only for use in tests. */
46
+ export function _resetForTesting() {
47
+ subscriptionManager.removeAll();
48
+ for (const { socket, duplexer } of instanceConnections.values()) {
49
+ socket.disconnect();
50
+ duplexer.close();
51
+ }
52
+ instanceConnections.clear();
53
+ tokenToAppInfoCache.clear();
54
+ }
55
+ function unsubscribeById(subscriptionId, channel) {
56
+ const subscription = subscriptionManager.get(subscriptionId);
57
+ if (!subscription) {
58
+ return;
59
+ }
60
+ if (channel &&
61
+ (channel.name !== subscription.channelName ||
62
+ channel.resourceId !== subscription.channelResourceId)) {
63
+ throw channelMismatch();
64
+ }
65
+ subscription.removeListeners();
66
+ const { channelName, channelResourceId, instanceId, channelEmitter } = subscription;
67
+ // If subscription is still pending (no channelEmitter/instanceId), mark
68
+ // cancelled so the in-flight setupSubscription knows to bail out.
69
+ if (!channelEmitter || !instanceId) {
70
+ subscription.cancelled = true;
71
+ subscriptionManager.delete(subscriptionId);
72
+ if (instanceId) {
73
+ cleanupConnectionIfIdle(instanceId);
74
+ }
75
+ return;
76
+ }
77
+ // If no other subscriptions remain on this channel, fire-and-forget
78
+ // the duplexer unsubscribe.
79
+ const hasOtherSubscriptionsOnChannel = subscriptionManager.deleteAndStillHasRemainingOnChannel(instanceId, subscriptionId, channelName, channelResourceId);
80
+ if (!hasOtherSubscriptionsOnChannel) {
81
+ const conn = instanceConnections.get(instanceId);
82
+ if (conn) {
83
+ conn.socket.unsubscribe(channelName, channelResourceId);
84
+ }
85
+ }
86
+ cleanupConnectionIfIdle(instanceId);
87
+ }
88
+ function unsubscribeByChannel(channel) {
89
+ const entriesToRemove = subscriptionManager.findByChannel(channel.name, channel.resourceId);
90
+ for (const [id] of entriesToRemove) {
91
+ unsubscribeById(id);
92
+ }
93
+ }
94
+ function closeConnection(instanceId) {
95
+ const conn = instanceConnections.get(instanceId);
96
+ if (conn) {
97
+ conn.socket.removeAllListeners();
98
+ conn.socket.disconnect();
99
+ conn.duplexer.close();
100
+ instanceConnections.delete(instanceId);
101
+ }
102
+ }
103
+ /** Clean up the connection for an instanceId if it has no remaining subscriptions. */
104
+ function cleanupConnectionIfIdle(instanceId) {
105
+ if (!subscriptionManager.hasSubscriptionsForInstanceId(instanceId)) {
106
+ closeConnection(instanceId);
107
+ }
108
+ }
109
+ function getOrCreateConnection(httpClient, appId, instanceId) {
110
+ const existing = instanceConnections.get(instanceId);
111
+ if (existing) {
112
+ return existing;
113
+ }
114
+ const duplexer = new Duplexer(DUPLEXER_HOST_URL, {
115
+ instanceUpdater: {
116
+ getInstance: getAuthToken.bind(null, httpClient),
117
+ },
118
+ });
119
+ const socket = duplexer.connect({
120
+ appDefId: appId,
121
+ });
122
+ const notifySubscriptionError = (realtimeError) => {
123
+ for (const subId of subscriptionManager.getSubscriptionIdsByInstanceId(instanceId)) {
124
+ subscriptionManager
125
+ .get(subId)
126
+ ?.onSubscriptionError?.(realtimeError, subId);
127
+ }
128
+ };
129
+ socket.on(DUPLEXER_EVENTS.disconnected, (error) => {
130
+ notifySubscriptionError(disconnectedError(error?.recoverable, error?.message));
131
+ });
132
+ socket.on(DUPLEXER_EVENTS.connectError, (error) => {
133
+ notifySubscriptionError(connectError(error?.recoverable, error?.message));
134
+ });
135
+ const connection = { duplexer, socket };
136
+ instanceConnections.set(instanceId, connection);
137
+ return connection;
138
+ }
139
+ async function setupSubscription(httpClient, subscriptionId, channel, messageHandler) {
140
+ try {
141
+ const { appId, instanceId } = await getAppInfoFromAuthToken(httpClient);
142
+ const subscriptionEntry = subscriptionManager.get(subscriptionId);
143
+ // Entry was already deleted (e.g., unsubscribe called during the await)
144
+ // or explicitly cancelled — bail out.
145
+ if (!subscriptionEntry || subscriptionEntry.cancelled) {
146
+ subscriptionManager.delete(subscriptionId);
147
+ return;
148
+ }
149
+ // Index the subscription by instanceId now so cleanupConnectionIfIdle
150
+ // correctly counts it while the duplexer subscription is being set up.
151
+ subscriptionManager.set(subscriptionId, {
152
+ ...subscriptionEntry,
153
+ instanceId,
154
+ });
155
+ const { socket } = getOrCreateConnection(httpClient, appId, instanceId);
156
+ const onMessageCallback = (payload, { publisher }) => {
157
+ messageHandler({
158
+ payload,
159
+ publisher: publisher?.uid ? { id: publisher.uid } : undefined,
160
+ }, channel);
161
+ };
162
+ // --- callback definitions (hoisted above subscribe for #6 race fix) ---
163
+ function onSubscribeSucceeded(payload) {
164
+ subscribedChannel.off(DUPLEXER_EVENTS.subscriptionFailed, onSubscribeFailed);
165
+ // If the subscription was removed (via unsubscribe) while waiting for
166
+ // the duplexer to confirm, clean up the duplexer channel to avoid a
167
+ // server-side subscription leak.
168
+ if (!subscriptionManager.get(subscriptionId)) {
169
+ socket.unsubscribe(channel.name, channel.resourceId);
170
+ cleanupConnectionIfIdle(instanceId);
171
+ return;
172
+ }
173
+ subscribedChannel.on(MESSAGE_EVENT_NAME, onMessageCallback);
174
+ // Fire onSubscribed for the initial subscription — re-read from the
175
+ // manager to avoid using a stale closure reference.
176
+ subscriptionManager
177
+ .get(subscriptionId)
178
+ ?.onSubscribed?.(subscriptionId, payload?.isSynced ?? false);
179
+ // Listen for resubscription succeeded (fires after reconnection)
180
+ const onResubscribeSucceeded = (payload) => {
181
+ if (subscriptionManager.get(subscriptionId)) {
182
+ subscriptionManager
183
+ .get(subscriptionId)
184
+ ?.onSubscribed?.(subscriptionId, payload?.isSynced ?? false);
185
+ }
186
+ };
187
+ subscribedChannel.on(DUPLEXER_EVENTS.subscriptionSucceeded, onResubscribeSucceeded);
188
+ const onResubscribeFailed = (payload) => {
189
+ subscriptionManager
190
+ .get(subscriptionId)
191
+ ?.onSubscriptionError?.(resubscribeFailed(channel, payload), subscriptionId);
192
+ };
193
+ subscribedChannel.on(DUPLEXER_EVENTS.subscriptionFailed, onResubscribeFailed);
194
+ const currentEntry = subscriptionManager.get(subscriptionId);
195
+ subscriptionManager.set(subscriptionId, {
196
+ channelName: channel.name,
197
+ channelResourceId: channel.resourceId,
198
+ instanceId,
199
+ channelEmitter: subscribedChannel,
200
+ onSubscribed: currentEntry?.onSubscribed,
201
+ onSubscriptionError: currentEntry?.onSubscriptionError,
202
+ removeListeners: () => {
203
+ subscribedChannel.off(MESSAGE_EVENT_NAME, onMessageCallback);
204
+ subscribedChannel.off(DUPLEXER_EVENTS.subscriptionSucceeded, onResubscribeSucceeded);
205
+ subscribedChannel.off(DUPLEXER_EVENTS.subscriptionFailed, onResubscribeFailed);
206
+ },
207
+ });
208
+ }
209
+ function onSubscribeFailed(payload) {
210
+ subscribedChannel.off(DUPLEXER_EVENTS.subscriptionSucceeded, onSubscribeSucceeded);
211
+ const failedEntry = subscriptionManager.get(subscriptionId);
212
+ subscriptionManager.delete(subscriptionId);
213
+ failedEntry?.onSubscriptionError?.(subscribeFailed(channel, payload), subscriptionId);
214
+ cleanupConnectionIfIdle(instanceId);
215
+ }
216
+ // Subscribe to the duplexer channel and register lifecycle listeners
217
+ // in the same synchronous block so no events can be missed.
218
+ const subscribedChannel = socket.subscribe(channel.name, {
219
+ resourceId: channel.resourceId,
220
+ });
221
+ subscribedChannel.once(DUPLEXER_EVENTS.subscriptionSucceeded, onSubscribeSucceeded);
222
+ subscribedChannel.once(DUPLEXER_EVENTS.subscriptionFailed, onSubscribeFailed);
223
+ }
224
+ catch (error) {
225
+ const sub = subscriptionManager.get(subscriptionId);
226
+ subscriptionManager.delete(subscriptionId);
227
+ sub?.onSubscriptionError?.(subscribeError(error, channel), subscriptionId);
228
+ }
229
+ }
@@ -0,0 +1,26 @@
1
+ import type { Duplexer } from '@wix/duplexer-js';
2
+ import type { SubscriptionHandler, SubscriptionErrorHandler } from '@wix/realtime-common/types';
3
+ export interface SubscriptionEntry {
4
+ channelName: string;
5
+ channelResourceId?: string;
6
+ instanceId?: string;
7
+ channelEmitter?: ReturnType<ReturnType<Duplexer['connect']>['subscribe']>;
8
+ removeListeners: () => void;
9
+ cancelled?: boolean;
10
+ onSubscribed?: SubscriptionHandler;
11
+ onSubscriptionError?: SubscriptionErrorHandler;
12
+ }
13
+ export declare class SubscriptionManager {
14
+ private subscriptionsBySubId;
15
+ private instanceIdToSubscriptionIds;
16
+ set(id: string, entry: SubscriptionEntry): void;
17
+ get(id: string): SubscriptionEntry | undefined;
18
+ delete(id: string): void;
19
+ findByChannel(channelName: string, channelResourceId?: string): [string, SubscriptionEntry][];
20
+ deleteAndStillHasRemainingOnChannel(instanceId: string, subscriptionIdToDelete: string, channelName: string, resourceId?: string): boolean;
21
+ hasSubscriptionsForInstanceId(instanceId: string): boolean;
22
+ getSubscriptionIdsByInstanceId(instanceId: string): string[];
23
+ removeAll(): void;
24
+ private addToInstanceIndex;
25
+ private removeFromInstanceIndex;
26
+ }
@@ -0,0 +1,71 @@
1
+ export class SubscriptionManager {
2
+ subscriptionsBySubId = new Map();
3
+ instanceIdToSubscriptionIds = new Map();
4
+ set(id, entry) {
5
+ this.subscriptionsBySubId.set(id, entry);
6
+ if (entry.instanceId) {
7
+ this.addToInstanceIndex(entry.instanceId, id);
8
+ }
9
+ }
10
+ get(id) {
11
+ return this.subscriptionsBySubId.get(id);
12
+ }
13
+ delete(id) {
14
+ const entry = this.subscriptionsBySubId.get(id);
15
+ if (entry?.instanceId) {
16
+ this.removeFromInstanceIndex(entry.instanceId, id);
17
+ }
18
+ this.subscriptionsBySubId.delete(id);
19
+ }
20
+ findByChannel(channelName, channelResourceId) {
21
+ return [...this.subscriptionsBySubId.entries()].filter(([, sub]) => sub.channelName === channelName &&
22
+ sub.channelResourceId === channelResourceId);
23
+ }
24
+ deleteAndStillHasRemainingOnChannel(instanceId, subscriptionIdToDelete, channelName, resourceId) {
25
+ this.delete(subscriptionIdToDelete);
26
+ const candidateIds = this.instanceIdToSubscriptionIds.get(instanceId);
27
+ if (!candidateIds)
28
+ return false;
29
+ for (const id of candidateIds) {
30
+ const sub = this.subscriptionsBySubId.get(id);
31
+ if (sub &&
32
+ sub.channelName === channelName &&
33
+ sub.channelResourceId === resourceId) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ hasSubscriptionsForInstanceId(instanceId) {
40
+ const ids = this.instanceIdToSubscriptionIds.get(instanceId);
41
+ return !!ids && ids.size > 0;
42
+ }
43
+ getSubscriptionIdsByInstanceId(instanceId) {
44
+ const ids = this.instanceIdToSubscriptionIds.get(instanceId);
45
+ return ids ? [...ids] : [];
46
+ }
47
+ removeAll() {
48
+ for (const [, sub] of this.subscriptionsBySubId) {
49
+ sub.removeListeners();
50
+ }
51
+ this.subscriptionsBySubId.clear();
52
+ this.instanceIdToSubscriptionIds.clear();
53
+ }
54
+ addToInstanceIndex(instanceId, id) {
55
+ let ids = this.instanceIdToSubscriptionIds.get(instanceId);
56
+ if (!ids) {
57
+ ids = new Set();
58
+ this.instanceIdToSubscriptionIds.set(instanceId, ids);
59
+ }
60
+ ids.add(id);
61
+ }
62
+ removeFromInstanceIndex(instanceId, id) {
63
+ const ids = this.instanceIdToSubscriptionIds.get(instanceId);
64
+ if (ids) {
65
+ ids.delete(id);
66
+ if (ids.size === 0) {
67
+ this.instanceIdToSubscriptionIds.delete(instanceId);
68
+ }
69
+ }
70
+ }
71
+ }
@@ -0,0 +1 @@
1
+ export { RealtimeErrorCode, RealtimeError, type Channel, type Publisher, type Message, type MessageHandler, type SubscriptionHandler, type SubscriptionErrorHandler, type SubscriptionOptions, } from '@wix/realtime-common/types';
@@ -0,0 +1 @@
1
+ export { RealtimeErrorCode, RealtimeError, } from '@wix/realtime-common/types';