Skip to content

Commit 2ff0e5b

Browse files
Merge pull request #1473 from ably/1397-tree-shakable-MessageInteractions
[SDK-3736] Make `MessageFilter` subscription filtering tree-shakable
2 parents 674e88a + 6a96472 commit 2ff0e5b

File tree

8 files changed

+221
-103
lines changed

8 files changed

+221
-103
lines changed

scripts/moduleReport.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const moduleNames = [
1111
'WebSocketTransport',
1212
'XHRRequest',
1313
'FetchRequest',
14+
'MessageInteractions',
1415
];
1516

1617
// List of all free-standing functions exported by the library along with the

src/common/lib/client/baseclient.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic';
1616
import { throwMissingModuleError } from '../util/utils';
1717
import { MsgPack } from 'common/types/msgpack';
1818
import { HTTPRequestImplementations } from 'platform/web/lib/http/http';
19+
import { FilteredSubscriptions } from './filteredsubscriptions';
1920

2021
type BatchResult<T> = API.Types.BatchResult<T>;
2122
type BatchPublishSpec = API.Types.BatchPublishSpec;
@@ -44,6 +45,7 @@ class BaseClient {
4445
readonly _MsgPack: MsgPack | null;
4546
// Extra HTTP request implementations available to this client, in addition to those in web’s Http.bundledRequestImplementations
4647
readonly _additionalHTTPRequestImplementations: HTTPRequestImplementations;
48+
private readonly __FilteredSubscriptions: typeof FilteredSubscriptions | null;
4749

4850
constructor(options: ClientOptions | string, modules: ModulesMap) {
4951
this._additionalHTTPRequestImplementations = modules;
@@ -98,6 +100,7 @@ class BaseClient {
98100

99101
this._rest = modules.Rest ? new modules.Rest(this) : null;
100102
this._Crypto = modules.Crypto ?? null;
103+
this.__FilteredSubscriptions = modules.MessageInteractions ?? null;
101104
}
102105

103106
private get rest(): Rest {
@@ -107,6 +110,13 @@ class BaseClient {
107110
return this._rest;
108111
}
109112

113+
get _FilteredSubscriptions(): typeof FilteredSubscriptions {
114+
if (!this.__FilteredSubscriptions) {
115+
throwMissingModuleError('MessageInteractions');
116+
}
117+
return this.__FilteredSubscriptions;
118+
}
119+
110120
get channels() {
111121
return this.rest.channels;
112122
}

src/common/lib/client/defaultrealtime.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MsgPack } from 'common/types/msgpack';
1010
import RealtimePresence from './realtimepresence';
1111
import { DefaultPresenceMessage } from '../types/defaultpresencemessage';
1212
import initialiseWebSocketTransport from '../transport/websockettransport';
13+
import { FilteredSubscriptions } from './filteredsubscriptions';
1314

1415
/**
1516
`DefaultRealtime` is the class that the non tree-shakable version of the SDK exports as `Realtime`. It ensures that this version of the SDK includes all of the functionality which is optionally available in the tree-shakable version.
@@ -27,6 +28,7 @@ export class DefaultRealtime extends BaseRealtime {
2728
MsgPack,
2829
RealtimePresence,
2930
WebSocketTransport: initialiseWebSocketTransport,
31+
MessageInteractions: FilteredSubscriptions,
3032
});
3133
}
3234

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as API from '../../../../ably';
2+
import RealtimeChannel from './realtimechannel';
3+
import Message from '../types/message';
4+
5+
export class FilteredSubscriptions {
6+
static subscribeFilter(
7+
channel: RealtimeChannel,
8+
filter: API.Types.MessageFilter,
9+
listener: API.Types.messageCallback<Message>
10+
) {
11+
const filteredListener = (m: Message) => {
12+
const mapping: { [key in keyof API.Types.MessageFilter]: any } = {
13+
name: m.name,
14+
refTimeserial: m.extras?.ref?.timeserial,
15+
refType: m.extras?.ref?.type,
16+
isRef: !!m.extras?.ref?.timeserial,
17+
clientId: m.clientId,
18+
};
19+
// Check if any values are defined in the filter and if they match the value in the message object
20+
if (
21+
Object.entries(filter).find(([key, value]) =>
22+
value !== undefined ? mapping[key as keyof API.Types.MessageFilter] !== value : false
23+
)
24+
) {
25+
return;
26+
}
27+
listener(m);
28+
};
29+
this.addFilteredSubscription(channel, filter, listener, filteredListener);
30+
channel.subscriptions.on(filteredListener);
31+
}
32+
33+
// Adds a new filtered subscription
34+
static addFilteredSubscription(
35+
channel: RealtimeChannel,
36+
filter: API.Types.MessageFilter,
37+
realListener: API.Types.messageCallback<Message>,
38+
filteredListener: API.Types.messageCallback<Message>
39+
) {
40+
if (!channel.filteredSubscriptions) {
41+
channel.filteredSubscriptions = new Map<
42+
API.Types.messageCallback<Message>,
43+
Map<API.Types.MessageFilter, API.Types.messageCallback<Message>[]>
44+
>();
45+
}
46+
if (channel.filteredSubscriptions.has(realListener)) {
47+
const realListenerMap = channel.filteredSubscriptions.get(realListener) as Map<
48+
API.Types.MessageFilter,
49+
API.Types.messageCallback<Message>[]
50+
>;
51+
// Add the filtered listener to the map, or append to the array if this filter has already been used
52+
realListenerMap.set(filter, realListenerMap?.get(filter)?.concat(filteredListener) || [filteredListener]);
53+
} else {
54+
channel.filteredSubscriptions.set(
55+
realListener,
56+
new Map<API.Types.MessageFilter, API.Types.messageCallback<Message>[]>([[filter, [filteredListener]]])
57+
);
58+
}
59+
}
60+
61+
static getAndDeleteFilteredSubscriptions(
62+
channel: RealtimeChannel,
63+
filter: API.Types.MessageFilter | undefined,
64+
realListener: API.Types.messageCallback<Message> | undefined
65+
): API.Types.messageCallback<Message>[] {
66+
// No filtered subscriptions map means there has been no filtered subscriptions yet, so return nothing
67+
if (!channel.filteredSubscriptions) {
68+
return [];
69+
}
70+
// Only a filter is passed in with no specific listener
71+
if (!realListener && filter) {
72+
// Return each listener which is attached to the specified filter object
73+
return Array.from(channel.filteredSubscriptions.entries())
74+
.map(([key, filterMaps]) => {
75+
// Get (then delete) the maps matching this filter
76+
let listenerMaps = filterMaps.get(filter);
77+
filterMaps.delete(filter);
78+
// Clear the parent if nothing is left
79+
if (filterMaps.size === 0) {
80+
channel.filteredSubscriptions?.delete(key);
81+
}
82+
return listenerMaps;
83+
})
84+
.reduce(
85+
(prev, cur) => (cur ? (prev as API.Types.messageCallback<Message>[]).concat(...cur) : prev),
86+
[]
87+
) as API.Types.messageCallback<Message>[];
88+
}
89+
90+
// No subscriptions for this listener
91+
if (!realListener || !channel.filteredSubscriptions.has(realListener)) {
92+
return [];
93+
}
94+
const realListenerMap = channel.filteredSubscriptions.get(realListener) as Map<
95+
API.Types.MessageFilter,
96+
API.Types.messageCallback<Message>[]
97+
>;
98+
// If no filter is specified return all listeners using that function
99+
if (!filter) {
100+
// array.flat is not available unless we support es2019 or higher
101+
const listeners = Array.from(realListenerMap.values()).reduce((prev, cur) => prev.concat(...cur), []);
102+
// remove the listener from the map
103+
channel.filteredSubscriptions.delete(realListener);
104+
return listeners;
105+
}
106+
107+
let listeners = realListenerMap.get(filter);
108+
realListenerMap.delete(filter);
109+
110+
return listeners || [];
111+
}
112+
}

src/common/lib/client/modulesmap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import RealtimePresence from './realtimepresence';
55
import { TransportInitialiser } from '../transport/connectionmanager';
66
import XHRRequest from 'platform/web/lib/http/request/xhrrequest';
77
import fetchRequest from 'platform/web/lib/http/request/fetchrequest';
8+
import { FilteredSubscriptions } from './filteredsubscriptions';
89

910
export interface ModulesMap {
1011
Rest?: typeof Rest;
@@ -16,6 +17,7 @@ export interface ModulesMap {
1617
XHRStreaming?: TransportInitialiser;
1718
XHRRequest?: typeof XHRRequest;
1819
FetchRequest?: typeof fetchRequest;
20+
MessageInteractions?: typeof FilteredSubscriptions;
1921
}
2022

2123
export const allCommonModules: ModulesMap = { Rest };

src/common/lib/client/realtimechannel.ts

Lines changed: 4 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -438,121 +438,22 @@ class RealtimeChannel extends Channel {
438438

439439
// Filtered
440440
if (event && typeof event === 'object' && !Array.isArray(event)) {
441-
this._subscribeFilter(event, listener);
441+
this.client._FilteredSubscriptions.subscribeFilter(this, event, listener);
442442
} else {
443443
this.subscriptions.on(event, listener);
444444
}
445445

446446
return this.attach(callback || noop);
447447
}
448448

449-
_subscribeFilter(filter: API.Types.MessageFilter, listener: API.Types.messageCallback<Message>) {
450-
const filteredListener = (m: Message) => {
451-
const mapping: { [key in keyof API.Types.MessageFilter]: any } = {
452-
name: m.name,
453-
refTimeserial: m.extras?.ref?.timeserial,
454-
refType: m.extras?.ref?.type,
455-
isRef: !!m.extras?.ref?.timeserial,
456-
clientId: m.clientId,
457-
};
458-
// Check if any values are defined in the filter and if they match the value in the message object
459-
if (
460-
Object.entries(filter).find(([key, value]) =>
461-
value !== undefined ? mapping[key as keyof API.Types.MessageFilter] !== value : false
462-
)
463-
) {
464-
return;
465-
}
466-
listener(m);
467-
};
468-
this._addFilteredSubscription(filter, listener, filteredListener);
469-
this.subscriptions.on(filteredListener);
470-
}
471-
472-
// Adds a new filtered subscription
473-
_addFilteredSubscription(
474-
filter: API.Types.MessageFilter,
475-
realListener: API.Types.messageCallback<Message>,
476-
filteredListener: API.Types.messageCallback<Message>
477-
) {
478-
if (!this.filteredSubscriptions) {
479-
this.filteredSubscriptions = new Map<
480-
API.Types.messageCallback<Message>,
481-
Map<API.Types.MessageFilter, API.Types.messageCallback<Message>[]>
482-
>();
483-
}
484-
if (this.filteredSubscriptions.has(realListener)) {
485-
const realListenerMap = this.filteredSubscriptions.get(realListener) as Map<
486-
API.Types.MessageFilter,
487-
API.Types.messageCallback<Message>[]
488-
>;
489-
// Add the filtered listener to the map, or append to the array if this filter has already been used
490-
realListenerMap.set(filter, realListenerMap?.get(filter)?.concat(filteredListener) || [filteredListener]);
491-
} else {
492-
this.filteredSubscriptions.set(
493-
realListener,
494-
new Map<API.Types.MessageFilter, API.Types.messageCallback<Message>[]>([[filter, [filteredListener]]])
495-
);
496-
}
497-
}
498-
499-
_getAndDeleteFilteredSubscriptions(
500-
filter: API.Types.MessageFilter | undefined,
501-
realListener: API.Types.messageCallback<Message> | undefined
502-
): API.Types.messageCallback<Message>[] {
503-
// No filtered subscriptions map means there has been no filtered subscriptions yet, so return nothing
504-
if (!this.filteredSubscriptions) {
505-
return [];
506-
}
507-
// Only a filter is passed in with no specific listener
508-
if (!realListener && filter) {
509-
// Return each listener which is attached to the specified filter object
510-
return Array.from(this.filteredSubscriptions.entries())
511-
.map(([key, filterMaps]) => {
512-
// Get (then delete) the maps matching this filter
513-
let listenerMaps = filterMaps.get(filter);
514-
filterMaps.delete(filter);
515-
// Clear the parent if nothing is left
516-
if (filterMaps.size === 0) {
517-
this.filteredSubscriptions?.delete(key);
518-
}
519-
return listenerMaps;
520-
})
521-
.reduce(
522-
(prev, cur) => (cur ? (prev as API.Types.messageCallback<Message>[]).concat(...cur) : prev),
523-
[]
524-
) as API.Types.messageCallback<Message>[];
525-
}
526-
527-
// No subscriptions for this listener
528-
if (!realListener || !this.filteredSubscriptions.has(realListener)) {
529-
return [];
530-
}
531-
const realListenerMap = this.filteredSubscriptions.get(realListener) as Map<
532-
API.Types.MessageFilter,
533-
API.Types.messageCallback<Message>[]
534-
>;
535-
// If no filter is specified return all listeners using that function
536-
if (!filter) {
537-
// array.flat is not available unless we support es2019 or higher
538-
const listeners = Array.from(realListenerMap.values()).reduce((prev, cur) => prev.concat(...cur), []);
539-
// remove the listener from the map
540-
this.filteredSubscriptions.delete(realListener);
541-
return listeners;
542-
}
543-
544-
let listeners = realListenerMap.get(filter);
545-
realListenerMap.delete(filter);
546-
547-
return listeners || [];
548-
}
549-
550449
unsubscribe(...args: unknown[] /* [event], listener */): void {
551450
const [event, listener] = RealtimeChannel.processListenerArgs(args);
552451

553452
// If we either have a filtered listener, a filter or both we need to do additional processing to find the original function(s)
554453
if ((typeof event === 'object' && !listener) || this.filteredSubscriptions?.has(listener)) {
555-
this._getAndDeleteFilteredSubscriptions(event, listener).forEach((l) => this.subscriptions.off(l));
454+
this.client._FilteredSubscriptions
455+
.getAndDeleteFilteredSubscriptions(this, event, listener)
456+
.forEach((l) => this.subscriptions.off(l));
556457
return;
557458
}
558459

src/platform/web/modules.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ export * from './modules/realtimepresence';
5050
export * from './modules/transports';
5151
export * from './modules/http';
5252
export { Rest } from '../../common/lib/client/rest';
53+
export { FilteredSubscriptions as MessageInteractions } from '../../common/lib/client/filteredsubscriptions';
5354
export { BaseRest, BaseRealtime, ErrorInfo };

0 commit comments

Comments
 (0)