Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions test/integration/components/listeners.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,4 +614,122 @@ describe('#listeners', () => {
expect(actual.uuid).to.be.equal('bob');
expect(actual.timetoken).to.not.be.equal(undefined);
});
it('should route messages correctly between subscription sets and individual subscriptions', (done) => {
utils.createPresenceMockScopes({
subKey: 'mySubKey',
presenceType: 'heartbeat',
requests: [{ channels: ['a', 'b', 'c', 'd'] }],
});

utils.createSubscribeMockScopes({
subKey: 'mySubKey',
pnsdk: `PubNub-JS-Nodejs/${pubnub.getVersion()}`,
userId: 'myUUID',
eventEngine: true,
requests: [
{
channels: ['a', 'b', 'c', 'd'],
messages: [
{ channel: 'a', message: { id: 'msg-channel-a', content: 'Message for channel A' } },
{ channel: 'b', message: { id: 'msg-channel-b', content: 'Message for channel B' } },
{ channel: 'c', message: { id: 'msg-channel-c', content: 'Message for channel C' } },
// Intentionally NO message for channel 'd' - this tests message isolation
],
},
// Final empty response to end subscription loop
{ channels: ['a', 'b', 'c', 'd'], messages: [], replyDelay: 500 },
],
});

// Message tracking arrays for individual subscriptions (as requested)
const channelASubscriptionMessagesReceived: Subscription.Message[] = [];
const channelBSubscriptionMessagesReceived: Subscription.Message[] = [];
const channelCSubscriptionMessagesReceived: Subscription.Message[] = [];
const subDReceivedMessages: Subscription.Message[] = []; // Array for subD as requested
const subscriptionSet1MessagesReceived: Subscription.Message[] = [];

// Create individual subscriptions (subA, subB, subD)
const subA = pubnub.channel('a').subscription();
const subB = pubnub.channel('b').subscription();
const subD = pubnub.channel('d').subscription(); // Added subD for channel 'd'
const subscriptionC = pubnub.channel('c').subscription(); // Individual subscription for channel 'c'

// Add message listeners to individual subscriptions to maintain received message arrays
subA.onMessage = (message) => channelASubscriptionMessagesReceived.push(message);
subB.onMessage = (message) => channelBSubscriptionMessagesReceived.push(message);
subD.onMessage = (message) => subDReceivedMessages.push(message); // subD listener as requested
subscriptionC.onMessage = (message) => channelCSubscriptionMessagesReceived.push(message);

// Create subscriptionSet1 by adding individual subscriptions (subA + subB + subD + subscriptionC)
// This creates a subscription set covering channels 'a', 'b', 'c', 'd'
const subscriptionSet1 = subA.addSubscription(subB);
subscriptionSet1.addSubscription(subD); // Add subD to subscriptionSet as requested
subscriptionSet1.addSubscription(subscriptionC);
subscriptionSet1.onMessage = (message) => subscriptionSet1MessagesReceived.push(message);

// Track when we've received enough messages to verify the test
let messageCount = 0;
let testCompleted = false;

const checkCompletion = () => {
messageCount++;
// We expect messages for channels a, b, c - both to individual subscriptions and subscription set
// subD should receive NO messages since no message is sent to channel 'd'
if (messageCount >= 6 && !testCompleted) {
// 3 for set + 3 for individual subscriptions (a,b,c only)
testCompleted = true;

try {
// Verify that each individual subscription receives messages for its respective channel
expect(channelASubscriptionMessagesReceived.length).to.equal(1);
expect((channelASubscriptionMessagesReceived[0].message as any).id).to.equal('msg-channel-a');
expect(channelASubscriptionMessagesReceived[0].channel).to.equal('a');

expect(channelBSubscriptionMessagesReceived.length).to.equal(1);
expect((channelBSubscriptionMessagesReceived[0].message as any).id).to.equal('msg-channel-b');
expect(channelBSubscriptionMessagesReceived[0].channel).to.equal('b');

expect(channelCSubscriptionMessagesReceived.length).to.equal(1);
expect((channelCSubscriptionMessagesReceived[0].message as any).id).to.equal('msg-channel-c');
expect(channelCSubscriptionMessagesReceived[0].channel).to.equal('c');

// subD should NOT receive any messages since no message was sent to channel 'd'
// This confirms that messages are not routed to wrong subscription listeners
expect(subDReceivedMessages.length).to.equal(0, 'subD should not receive messages for channels a,b,c');

// Verify that subscriptionSet1 receives messages for channels a, b, c (but not d since no message sent)
expect(subscriptionSet1MessagesReceived.length).to.equal(3);
const set1MessageIds = subscriptionSet1MessagesReceived.map((m) => (m.message as any).id).sort();
expect(set1MessageIds).to.deep.equal(['msg-channel-a', 'msg-channel-b', 'msg-channel-c']);

// Verify correct channel routing for each message in the subscription set
const channelAMsg = subscriptionSet1MessagesReceived.find((m) => (m.message as any).id === 'msg-channel-a');
const channelBMsg = subscriptionSet1MessagesReceived.find((m) => (m.message as any).id === 'msg-channel-b');
const channelCMsg = subscriptionSet1MessagesReceived.find((m) => (m.message as any).id === 'msg-channel-c');

expect(channelAMsg?.channel).to.equal('a');
expect(channelBMsg?.channel).to.equal('b');
expect(channelCMsg?.channel).to.equal('c');

// Verify that no message for channel 'd' exists in the subscription set
const channelDMsg = subscriptionSet1MessagesReceived.find((m) => m.channel === 'd');
expect(channelDMsg).to.be.undefined;

done();
} catch (error) {
done(error);
}
}
};

// Add listeners to test completion check
subscriptionSet1.addListener({ message: checkCompletion });
subA.addListener({ message: checkCompletion });
subB.addListener({ message: checkCompletion });
subD.addListener({ message: checkCompletion });
subscriptionC.addListener({ message: checkCompletion });

// Subscribe the subscription set (which includes a, b, c, d through individual subscriptions)
subscriptionSet1.subscribe();
});
});
248 changes: 248 additions & 0 deletions test/integration/endpoints/presence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,4 +911,252 @@ describe('presence endpoints', () => {
});
});
});

describe('heartbeat tests (run for 4+ seconds)', () => {
let pubnubWithEE: PubNub;

before(() => {
nock.disableNetConnect();
});

beforeEach(() => {
nock.cleanAll();
pubnubWithEE = new PubNub({
subscribeKey: 'mySubKey',
publishKey: 'myPublishKey',
uuid: 'myUUID',
// @ts-expect-error Force override default value.
useRequestId: false,
enableEventEngine: true,
presenceTimeout: 10,
heartbeatInterval: 3, // minimal value to avoid prolonged test execution
});
});

afterEach(() => {
pubnubWithEE.destroy(true);
});

it('subscriptions with same channel name', async () => {
utils.createPresenceMockScopes({
subKey: 'mySubKey',
presenceType: 'heartbeat',
requests: [{ channels: ['c1'] }, { channels: ['c1'] }, { channels: ['c1'] }, { channels: ['c1'] }],
});
utils.createSubscribeMockScopes({
subKey: 'mySubKey',
pnsdk: `PubNub-JS-Nodejs/${pubnubWithEE.getVersion()}`,
userId: 'myUUID',
eventEngine: true,
requests: [
{ channels: ['c1'], messages: [{ channel: 'c1', message: { hello: 'world' } }] },
{
channels: ['c1'],
messages: [{ channel: 'c1', message: { next: 'message' } }],
replyDelay: 1000,
},
{ channels: ['c1'], messages: [], replyDelay: 500 },
],
});

const ch1Subscription1 = pubnubWithEE.channel('c1').subscription();

const connectionPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNConnectedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});

ch1Subscription1.subscribe();
await connectionPromise;

assert.deepEqual(pubnubWithEE.getSubscribedChannels(), ['c1']);

const ch1Subscription2 = pubnubWithEE.channel('c1').subscription();

const subscriptionChangedPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNSubscriptionChangedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});
ch1Subscription2.subscribe();
await subscriptionChangedPromise;

await new Promise((resolve) => setTimeout(resolve, 4000)); // wait for heartbeat to trigger
});
it('subscriptions with same channel name, using pubnub.subscribe()', async () => {
utils.createPresenceMockScopes({
subKey: 'mySubKey',
presenceType: 'heartbeat',
requests: [{ channels: ['c1'] }, { channels: ['c1'] }, { channels: ['c1'] }, { channels: ['c1'] }],
});
utils.createSubscribeMockScopes({
subKey: 'mySubKey',
pnsdk: `PubNub-JS-Nodejs/${pubnubWithEE.getVersion()}`,
userId: 'myUUID',
eventEngine: true,
requests: [
{ channels: ['c1'], messages: [{ channel: 'c1', message: { hello: 'world' } }] },
{
channels: ['c1'],
messages: [{ channel: 'c1', message: { next: 'message' } }],
replyDelay: 1000,
},
{ channels: ['c1'], messages: [], replyDelay: 500 },
],
});

const connectionPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNConnectedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});

pubnubWithEE.subscribe({ channels: ['c1', 'c1'] });
await connectionPromise;

assert.deepEqual(pubnubWithEE.getSubscribedChannels(), ['c1']);

const subscriptionChangedPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNSubscriptionChangedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});
pubnubWithEE.subscribe({ channels: ['c1'] });
await subscriptionChangedPromise;

await new Promise((resolve) => setTimeout(resolve, 4000)); // wait for heartbeat to trigger
});

it('subscriptions with same channel name,groups, using pubnub.subscribe()', async () => {
utils.createPresenceMockScopes({
subKey: 'mySubKey',
presenceType: 'heartbeat',
requests: [
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
],
});
utils.createSubscribeMockScopes({
subKey: 'mySubKey',
pnsdk: `PubNub-JS-Nodejs/${pubnubWithEE.getVersion()}`,
userId: 'myUUID',
eventEngine: true,
requests: [
{ channels: ['c1'], groups: ['cg1'], messages: [], replyDelay: 500 },
{ channels: ['c1'], groups: ['cg1'], messages: [{ channel: 'c1', message: { hello: 'world' } }] },
{
channels: ['c1'],
groups: ['cg1'],
messages: [{ channel: 'c1', message: { next: 'message' } }],
replyDelay: 1000,
},
],
});

const connectionPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNConnectedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});

pubnubWithEE.subscribe({ channels: ['c1', 'c1'], channelGroups: ['cg1', 'cg1'] });
await connectionPromise;

assert.deepEqual(pubnubWithEE.getSubscribedChannels(), ['c1']);

const subscriptionChangedPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNSubscriptionChangedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});
pubnubWithEE.subscribe({ channels: ['c1'], channelGroups: ['cg1', 'cg1'] });
await subscriptionChangedPromise;

await new Promise((resolve) => setTimeout(resolve, 4000)); // wait for heartbeat to trigger
});

it('subscriptions with same channel name,groups, combination', async () => {
utils.createPresenceMockScopes({
subKey: 'mySubKey',
presenceType: 'heartbeat',
requests: [
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
{ channels: ['c1'], groups: ['cg1'] },
],
});
utils.createSubscribeMockScopes({
subKey: 'mySubKey',
pnsdk: `PubNub-JS-Nodejs/${pubnubWithEE.getVersion()}`,
userId: 'myUUID',
eventEngine: true,
requests: [
{ channels: ['c1'], groups: ['cg1'], messages: [], replyDelay: 500 },
{
channels: ['c1'],
groups: ['cg1'],
messages: [{ channel: 'c1', message: { hello: 'world' } }],
replyDelay: 500,
},
{
channels: ['c1'],
groups: ['cg1'],
messages: [{ channel: 'c1', message: { next: 'message' } }],
replyDelay: 3000,
},
],
});

const connectionPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNConnectedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});

pubnubWithEE.subscribe({ channels: ['c1', 'c1'], channelGroups: ['cg1', 'cg1'] });
await connectionPromise;

assert.deepEqual(pubnubWithEE.getSubscribedChannels(), ['c1']);

const subscriptionChangedPromise = new Promise<void>((resolve) => {
pubnubWithEE.onStatus = (status) => {
if (status.category === PubNub.CATEGORIES.PNSubscriptionChangedCategory) {
pubnubWithEE.onStatus = undefined;
resolve();
}
};
});
pubnubWithEE.subscribe({ channelGroups: ['cg1', 'cg1'] });
pubnubWithEE.subscribe({ channels: ['c1'], channelGroups: ['cg1', 'cg1'] });
await subscriptionChangedPromise;

await new Promise((resolve) => setTimeout(resolve, 4000)); // wait for heartbeat to trigger
});
});
});
Loading