Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-3734] Make HTTP request implementations tree-shakable #1438

Merged
merged 9 commits into from
Nov 21, 2023
3 changes: 3 additions & 0 deletions scripts/moduleReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const moduleNames = [
'XHRPolling',
'XHRStreaming',
'WebSocketTransport',
'XHRRequest',
'FetchRequest',
'MessageInteractions',
];

// List of all free-standing functions exported by the library along with the
Expand Down
3 changes: 0 additions & 3 deletions src/common/lib/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,6 @@ class Auth {
const body = Utils.toQueryString(authParams).slice(1); /* slice is to remove the initial '?' */
this.client.http.doUri(
HttpMethods.Post,
client,
authOptions.authUrl,
headers,
body,
Expand All @@ -548,7 +547,6 @@ class Auth {
} else {
this.client.http.doUri(
HttpMethods.Get,
client,
authOptions.authUrl,
authHeaders || {},
null,
Expand Down Expand Up @@ -594,7 +592,6 @@ class Auth {
);
this.client.http.do(
HttpMethods.Post,
client,
tokenUri,
requestHeaders,
JSON.stringify(signedTokenParams),
Expand Down
17 changes: 16 additions & 1 deletion src/common/lib/client/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Rest } from './rest';
import { IUntypedCryptoStatic } from 'common/types/ICryptoStatic';
import { throwMissingModuleError } from '../util/utils';
import { MsgPack } from 'common/types/msgpack';
import { HTTPRequestImplementations } from 'platform/web/lib/http/http';
import { FilteredSubscriptions } from './filteredsubscriptions';

type BatchResult<T> = API.Types.BatchResult<T>;
type BatchPublishSpec = API.Types.BatchPublishSpec;
Expand All @@ -41,8 +43,13 @@ class BaseClient {
private readonly _rest: Rest | null;
readonly _Crypto: IUntypedCryptoStatic | null;
readonly _MsgPack: MsgPack | null;
// Extra HTTP request implementations available to this client, in addition to those in web’s Http.bundledRequestImplementations
readonly _additionalHTTPRequestImplementations: HTTPRequestImplementations;
private readonly __FilteredSubscriptions: typeof FilteredSubscriptions | null;

constructor(options: ClientOptions | string, modules: ModulesMap) {
this._additionalHTTPRequestImplementations = modules;

if (!options) {
const msg = 'no options provided';
Logger.logAction(Logger.LOG_ERROR, 'BaseClient()', msg);
Expand Down Expand Up @@ -88,11 +95,12 @@ class BaseClient {
this._currentFallback = null;

this.serverTimeOffset = null;
this.http = new Platform.Http(normalOptions);
this.http = new Platform.Http(this);
this.auth = new Auth(this, normalOptions);

this._rest = modules.Rest ? new modules.Rest(this) : null;
this._Crypto = modules.Crypto ?? null;
this.__FilteredSubscriptions = modules.MessageInteractions ?? null;
}

private get rest(): Rest {
Expand All @@ -102,6 +110,13 @@ class BaseClient {
return this._rest;
}

get _FilteredSubscriptions(): typeof FilteredSubscriptions {
if (!this.__FilteredSubscriptions) {
throwMissingModuleError('MessageInteractions');
}
return this.__FilteredSubscriptions;
}

get channels() {
return this.rest.channels;
}
Expand Down
8 changes: 4 additions & 4 deletions src/common/lib/client/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ class Channel extends EventEmitter {

const options = this.channelOptions;
new PaginatedResource(client, this.basePath + '/messages', headers, envelope, async function (
body: any,
headers: Record<string, string>,
unpacked?: boolean
body,
headers,
unpacked
) {
return await Message.fromResponseBody(body, options, client._MsgPack, unpacked ? undefined : format);
return await Message.fromResponseBody(body as Message[], options, client._MsgPack, unpacked ? undefined : format);
}).get(params as Record<string, unknown>, callback);
}

Expand Down
2 changes: 2 additions & 0 deletions src/common/lib/client/defaultrealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MsgPack } from 'common/types/msgpack';
import RealtimePresence from './realtimepresence';
import { DefaultPresenceMessage } from '../types/defaultpresencemessage';
import initialiseWebSocketTransport from '../transport/websockettransport';
import { FilteredSubscriptions } from './filteredsubscriptions';

/**
`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.
Expand All @@ -27,6 +28,7 @@ export class DefaultRealtime extends BaseRealtime {
MsgPack,
RealtimePresence,
WebSocketTransport: initialiseWebSocketTransport,
MessageInteractions: FilteredSubscriptions,
});
}

Expand Down
112 changes: 112 additions & 0 deletions src/common/lib/client/filteredsubscriptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as API from '../../../../ably';
import RealtimeChannel from './realtimechannel';
import Message from '../types/message';

export class FilteredSubscriptions {
static subscribeFilter(
channel: RealtimeChannel,
filter: API.Types.MessageFilter,
listener: API.Types.messageCallback<Message>
) {
const filteredListener = (m: Message) => {
const mapping: { [key in keyof API.Types.MessageFilter]: any } = {
name: m.name,
refTimeserial: m.extras?.ref?.timeserial,
refType: m.extras?.ref?.type,
isRef: !!m.extras?.ref?.timeserial,
clientId: m.clientId,
};
// Check if any values are defined in the filter and if they match the value in the message object
if (
Object.entries(filter).find(([key, value]) =>
value !== undefined ? mapping[key as keyof API.Types.MessageFilter] !== value : false
)
) {
return;
}
listener(m);
};
this.addFilteredSubscription(channel, filter, listener, filteredListener);
channel.subscriptions.on(filteredListener);
}

// Adds a new filtered subscription
static addFilteredSubscription(
channel: RealtimeChannel,
filter: API.Types.MessageFilter,
realListener: API.Types.messageCallback<Message>,
filteredListener: API.Types.messageCallback<Message>
) {
if (!channel.filteredSubscriptions) {
channel.filteredSubscriptions = new Map<
API.Types.messageCallback<Message>,
Map<API.Types.MessageFilter, API.Types.messageCallback<Message>[]>
>();
}
if (channel.filteredSubscriptions.has(realListener)) {
const realListenerMap = channel.filteredSubscriptions.get(realListener) as Map<
API.Types.MessageFilter,
API.Types.messageCallback<Message>[]
>;
// Add the filtered listener to the map, or append to the array if this filter has already been used
realListenerMap.set(filter, realListenerMap?.get(filter)?.concat(filteredListener) || [filteredListener]);
} else {
channel.filteredSubscriptions.set(
realListener,
new Map<API.Types.MessageFilter, API.Types.messageCallback<Message>[]>([[filter, [filteredListener]]])
);
}
}

static getAndDeleteFilteredSubscriptions(
channel: RealtimeChannel,
filter: API.Types.MessageFilter | undefined,
realListener: API.Types.messageCallback<Message> | undefined
): API.Types.messageCallback<Message>[] {
// No filtered subscriptions map means there has been no filtered subscriptions yet, so return nothing
if (!channel.filteredSubscriptions) {
return [];
}
// Only a filter is passed in with no specific listener
if (!realListener && filter) {
// Return each listener which is attached to the specified filter object
return Array.from(channel.filteredSubscriptions.entries())
.map(([key, filterMaps]) => {
// Get (then delete) the maps matching this filter
let listenerMaps = filterMaps.get(filter);
filterMaps.delete(filter);
// Clear the parent if nothing is left
if (filterMaps.size === 0) {
channel.filteredSubscriptions?.delete(key);
}
return listenerMaps;
})
.reduce(
(prev, cur) => (cur ? (prev as API.Types.messageCallback<Message>[]).concat(...cur) : prev),
[]
) as API.Types.messageCallback<Message>[];
}

// No subscriptions for this listener
if (!realListener || !channel.filteredSubscriptions.has(realListener)) {
return [];
}
const realListenerMap = channel.filteredSubscriptions.get(realListener) as Map<
API.Types.MessageFilter,
API.Types.messageCallback<Message>[]
>;
// If no filter is specified return all listeners using that function
if (!filter) {
// array.flat is not available unless we support es2019 or higher
const listeners = Array.from(realListenerMap.values()).reduce((prev, cur) => prev.concat(...cur), []);
// remove the listener from the map
channel.filteredSubscriptions.delete(realListener);
return listeners;
}

let listeners = realListenerMap.get(filter);
realListenerMap.delete(filter);

return listeners || [];
}
}
6 changes: 6 additions & 0 deletions src/common/lib/client/modulesmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { IUntypedCryptoStatic } from '../../types/ICryptoStatic';
import { MsgPack } from 'common/types/msgpack';
import RealtimePresence from './realtimepresence';
import { TransportInitialiser } from '../transport/connectionmanager';
import XHRRequest from 'platform/web/lib/http/request/xhrrequest';
import fetchRequest from 'platform/web/lib/http/request/fetchrequest';
import { FilteredSubscriptions } from './filteredsubscriptions';

export interface ModulesMap {
Rest?: typeof Rest;
Expand All @@ -12,6 +15,9 @@ export interface ModulesMap {
WebSocketTransport?: TransportInitialiser;
XHRPolling?: TransportInitialiser;
XHRStreaming?: TransportInitialiser;
XHRRequest?: typeof XHRRequest;
FetchRequest?: typeof fetchRequest;
MessageInteractions?: typeof FilteredSubscriptions;
}

export const allCommonModules: ModulesMap = { Rest };
9 changes: 5 additions & 4 deletions src/common/lib/client/paginatedresource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import Resource from './resource';
import ErrorInfo, { IPartialErrorInfo } from '../types/errorinfo';
import { PaginatedResultCallback } from '../../types/utils';
import BaseClient from './baseclient';
import { RequestCallbackHeaders } from 'common/types/http';

export type BodyHandler = (body: unknown, headers: Record<string, string>, unpacked?: boolean) => Promise<any>;
export type BodyHandler = (body: unknown, headers: RequestCallbackHeaders, unpacked?: boolean) => Promise<any>;

function getRelParams(linkUrl: string) {
const urlMatch = linkUrl.match(/^\.\/(\w+)\?(.*)$/);
Expand Down Expand Up @@ -135,7 +136,7 @@ class PaginatedResource {
handlePage<T>(
err: IPartialErrorInfo | null,
body: unknown,
headers: Record<string, string> | undefined,
headers: RequestCallbackHeaders | undefined,
unpacked: boolean | undefined,
statusCode: number | undefined,
callback: PaginatedResultCallback<T>
Expand Down Expand Up @@ -249,14 +250,14 @@ export class PaginatedResult<T> {
export class HttpPaginatedResponse<T> extends PaginatedResult<T> {
statusCode: number;
success: boolean;
headers: Record<string, string>;
headers: RequestCallbackHeaders;
errorCode?: number | null;
errorMessage?: string | null;

constructor(
resource: PaginatedResource,
items: T[],
headers: Record<string, string>,
headers: RequestCallbackHeaders,
statusCode: number,
relParams: any,
err: IPartialErrorInfo | null
Expand Down
16 changes: 6 additions & 10 deletions src/common/lib/client/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,9 @@ class Presence extends EventEmitter {
Utils.mixin(headers, client.options.headers);

const options = this.channel.channelOptions;
new PaginatedResource(client, this.basePath, headers, envelope, async function (
body: any,
headers: Record<string, string>,
unpacked?: boolean
) {
new PaginatedResource(client, this.basePath, headers, envelope, async function (body, headers, unpacked) {
return await PresenceMessage.fromResponseBody(
body,
body as Record<string, unknown>[],
options as CipherOptions,
client._MsgPack,
unpacked ? undefined : format
Expand Down Expand Up @@ -83,12 +79,12 @@ class Presence extends EventEmitter {

const options = this.channel.channelOptions;
new PaginatedResource(client, this.basePath + '/history', headers, envelope, async function (
body: any,
headers: Record<string, string>,
unpacked?: boolean
body,
headers,
unpacked
) {
return await PresenceMessage.fromResponseBody(
body,
body as Record<string, unknown>[],
options as CipherOptions,
client._MsgPack,
unpacked ? undefined : format
Expand Down
30 changes: 17 additions & 13 deletions src/common/lib/client/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,15 @@ class DeviceRegistrations {
Utils.mixin(headers, client.options.headers);

new PaginatedResource(client, '/push/deviceRegistrations', headers, envelope, async function (
body: any,
headers: Record<string, string>,
unpacked?: boolean
body,
headers,
unpacked
) {
return DeviceDetails.fromResponseBody(body, client._MsgPack, unpacked ? undefined : format);
return DeviceDetails.fromResponseBody(
body as Record<string, unknown>[],
client._MsgPack,
unpacked ? undefined : format
);
}).get(params, callback);
}

Expand Down Expand Up @@ -269,11 +273,15 @@ class ChannelSubscriptions {
Utils.mixin(headers, client.options.headers);

new PaginatedResource(client, '/push/channelSubscriptions', headers, envelope, async function (
body: any,
headers: Record<string, string>,
unpacked?: boolean
body,
headers,
unpacked
) {
return PushChannelSubscription.fromResponseBody(body, client._MsgPack, unpacked ? undefined : format);
return PushChannelSubscription.fromResponseBody(
body as Record<string, unknown>[],
client._MsgPack,
unpacked ? undefined : format
);
}).get(params, callback);
}

Expand Down Expand Up @@ -310,11 +318,7 @@ class ChannelSubscriptions {

if (client.options.pushFullWait) Utils.mixin(params, { fullWait: 'true' });

new PaginatedResource(client, '/push/channels', headers, envelope, async function (
body: unknown,
headers: Record<string, string>,
unpacked?: boolean
) {
new PaginatedResource(client, '/push/channels', headers, envelope, async function (body, headers, unpacked) {
const parsedBody = (
!unpacked && format ? Utils.decodeBody(body, client._MsgPack, format) : body
) as Array<string>;
Expand Down
Loading
Loading