Skip to content

Commit

Permalink
implement mock API & EventSub WS usage
Browse files Browse the repository at this point in the history
  • Loading branch information
d-fischer committed Oct 22, 2023
1 parent f40f151 commit a0611ef
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 17 deletions.
1 change: 0 additions & 1 deletion packages/api-call/src/TwitchApiCallOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export interface TwitchApiCallOptions {
* }
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface TwitchApiCallFetchOptions {
/** @private */ _dummy?: never;
}
4 changes: 3 additions & 1 deletion packages/api-call/src/apiCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ import type { TwitchApiCallFetchOptions, TwitchApiCallOptions } from './TwitchAp
*
* Defaults to "Bearer" for Helix and "OAuth" for everything else.
* @param fetchOptions Additional options to be passed to the `fetch` function.
* @param mockServerPort
*/
export async function callTwitchApiRaw(
options: TwitchApiCallOptions,
clientId?: string,
accessToken?: string,
authorizationType?: string,
fetchOptions: TwitchApiCallFetchOptions = {},
mockServerPort?: number,
): Promise<Response> {
const type = options.type ?? 'helix';
const url = getTwitchApiUrl(options.url, type);
const url = getTwitchApiUrl(options.url, type, mockServerPort);
const params = stringify(options.query, { arrayFormat: 'repeat', addQueryPrefix: true });
// eslint-disable-next-line @typescript-eslint/naming-convention
const headers = new Headers({ Accept: 'application/json' });
Expand Down
14 changes: 11 additions & 3 deletions packages/api-call/src/helpers/url.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import type { TwitchApiCallType } from '../TwitchApiCallOptions';

/** @internal */
export function getTwitchApiUrl(url: string, type: TwitchApiCallType): string {
export function getTwitchApiUrl(url: string, type: TwitchApiCallType, mockServerPort?: number): string {
switch (type) {
case 'helix':
return `https://api.twitch.tv/helix/${url.replace(/^\//, '')}`;
case 'helix': {
const unprefixedUrl = url.replace(/^\//, '');
if (mockServerPort) {
if (unprefixedUrl === 'eventsub/subscriptions') {
return `http://localhost:${mockServerPort}/${unprefixedUrl}`;
}
return `http://localhost:${mockServerPort}/mock/${unprefixedUrl}`;
}
return `https://api.twitch.tv/helix/${unprefixedUrl}`;
}
case 'auth':
return `https://id.twitch.tv/oauth2/${url.replace(/^\//, '')}`;
case 'custom':
Expand Down
18 changes: 17 additions & 1 deletion packages/api/src/client/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export interface ApiConfig {
* Defaults to 0 (executes immediately after all synchronous tasks are finished).
*/
batchDelay?: number;

/**
* The port your local mock server (from the Twitch CLI) runs on.
*
* Do not set this if you want to use the real production Twitch API.
*/
mockServerPort?: number;
}

/** @private */
Expand All @@ -49,6 +56,7 @@ export interface TwitchApiCallOptionsInternal {
accessToken?: string;
authorizationType?: string;
fetchOptions?: TwitchApiCallFetchOptions;
mockServerPort?: number;
}

/**
Expand Down Expand Up @@ -88,8 +96,16 @@ export class ApiClient extends BaseApiClient {
accessToken,
authorizationType,
fetchOptions,
mockServerPort,
}: TwitchApiCallOptionsInternal) =>
await callTwitchApiRaw(options, clientId, accessToken, authorizationType, fetchOptions),
await callTwitchApiRaw(
options,
clientId,
accessToken,
authorizationType,
fetchOptions,
mockServerPort,
),
getPartitionKey: req => req.userId ?? null,
}),
);
Expand Down
17 changes: 15 additions & 2 deletions packages/api/src/client/BaseApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ export class BaseApiClient extends EventEmitter {
return this._config.authProvider;
}

/** @private */
get _mockServerPort(): number | undefined {
return this._config.mockServerPort;
}

/** @internal */
get _batchDelay(): number {
return this._config.batchDelay ?? 0;
Expand Down Expand Up @@ -457,7 +462,7 @@ export class BaseApiClient extends EventEmitter {
accessToken?: string,
authorizationType?: string,
) {
const { fetchOptions } = this._config;
const { fetchOptions, mockServerPort } = this._config;
const type = options.type ?? 'helix';
this._logger.debug(`Calling ${type} API: ${options.method ?? 'GET'} ${options.url}`);
this._logger.trace(`Query: ${JSON.stringify(options.query)}`);
Expand All @@ -481,8 +486,16 @@ export class BaseApiClient extends EventEmitter {
accessToken,
authorizationType,
fetchOptions,
mockServerPort,
})
: await callTwitchApiRaw(options, clientId, accessToken, authorizationType, fetchOptions);
: await callTwitchApiRaw(
options,
clientId,
accessToken,
authorizationType,
fetchOptions,
mockServerPort,
);

if (!response.ok && response.status >= 500 && response.status < 600) {
await handleTwitchApiResponseError(response, options);
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/utils/HelixRateLimiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export class HelixRateLimiter extends ResponseBasedRateLimiter<TwitchApiCallOpti
accessToken,
authorizationType,
fetchOptions,
mockServerPort,
}: TwitchApiCallOptionsInternal): Promise<Response> {
return await callTwitchApiRaw(options, clientId, accessToken, authorizationType, fetchOptions);
return await callTwitchApiRaw(options, clientId, accessToken, authorizationType, fetchOptions, mockServerPort);
}

protected needsToRetryAfter(res: Response): number | null {
Expand Down
6 changes: 4 additions & 2 deletions packages/eventsub-base/src/EventSubBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ export abstract class EventSubBase extends EventEmitter {
* @eventListener
*
* @param subscription The subscription that was successfully created.
* @param apiSubscription The subscription data from the API.
*/
readonly onSubscriptionCreateSuccess = this.registerEvent<[subscription: EventSubSubscription]>();
readonly onSubscriptionCreateSuccess =
this.registerEvent<[subscription: EventSubSubscription, apiSubscription: HelixEventSubSubscription]>();

/**
* Fires when the client fails to create a subscription.
Expand Down Expand Up @@ -204,7 +206,7 @@ export abstract class EventSubBase extends EventEmitter {
_registerTwitchSubscription(subscription: EventSubSubscription, data: HelixEventSubSubscription): void {
this._twitchSubscriptions.set(subscription.id, data);
this._subscriptionsByTwitchId.set(data.id, subscription);
this.emit(this.onSubscriptionCreateSuccess, subscription);
this.emit(this.onSubscriptionCreateSuccess, subscription, data);
}

/** @private */
Expand Down
6 changes: 3 additions & 3 deletions packages/eventsub-http/src/EventSubHttpBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export abstract class EventSubHttpBase extends EventSubBase {

/** @private */
async _getCliTestCommandForSubscription(subscription: EventSubSubscription): Promise<string> {
return `twitch event trigger ${subscription._cliName} -F ${await this._buildHookUrl(subscription.id)} -s ${
this._secret
}`;
return `twitch event trigger ${subscription._cliName} -T webhook -F ${await this._buildHookUrl(
subscription.id,
)} -s ${this._secret}`;
}

/** @private */
Expand Down
29 changes: 26 additions & 3 deletions packages/eventsub-ws/src/EventSubWsListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export class EventSubWsListener extends EventSubBase implements EventSubListener
constructor(config: EventSubWsConfig) {
super(config);

this._initialUrl = config.url ?? 'wss://eventsub.wss.twitch.tv/ws';
this._initialUrl = this._apiClient._mockServerPort
? `ws://127.0.0.1:${this._apiClient._mockServerPort}/ws`
: config.url ?? 'wss://eventsub.wss.twitch.tv/ws';
this._loggerOptions = config.logger;
}

Expand All @@ -91,8 +93,29 @@ export class EventSubWsListener extends EventSubBase implements EventSubListener
}

/** @private */
async _getCliTestCommandForSubscription(): Promise<string> {
throw new Error("Testing WebSocket subscriptions currently isn't supported by the CLI");
async _getCliTestCommandForSubscription(subscription: EventSubSubscription): Promise<string> {
if (!this._apiClient._mockServerPort) {
throw new Error(`You must use the mock server from the Twitch CLI to be able to test WebSocket events.
To do so, specify the \`mockServerPort\` option in your \`ApiClient\`.`);
}
const { authUserId } = subscription;
if (!authUserId) {
throw new Error('Can not test a WebSocket subscription for a topic without user authentication');
}
if (!subscription._twitchId) {
throw new Error(
'Subscription must be registered with the mock server before being able to use this method',
);
}
const socket = this._sockets.get(authUserId);
if (!socket) {
throw new HellFreezesOverError(`Can not get appropriate socket for user ${authUserId}`);
}
if (!socket.sessionId) {
throw new HellFreezesOverError(`Socket for user ${authUserId} does not have a session ID yet`);
}
return `twitch event trigger ${subscription._cliName} -T websocket --session ${socket.sessionId} -u ${subscription._twitchId}`;
}

/** @private */
Expand Down

0 comments on commit a0611ef

Please sign in to comment.