@wix/realtime 1.0.14 → 1.0.16

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