box-node-sdk
Version:
Official SDK for Box Platform APIs
202 lines (187 loc) • 5.78 kB
text/typescript
import { BoxSdkError } from './errors';
import {
EventsManager,
GetEventsHeadersInput,
GetEventsQueryParams,
} from '../managers/events';
import { RealtimeServer } from '../schemas/realtimeServer';
import { ByteStream } from '../internal/utilsNode';
enum RealtimeServerEventValue {
NEW_CHANGE = 'new_change',
RECONNECT = 'reconnect',
}
/**
* EventStream is a readable stream that fetches events from the Box API.
* It uses long polling to receive real-time updates.
* This class is designed to be used with Node.js streams.
*
* @param {EventsManager} options.eventsManager - The EventsManager instance which provides relevant methods to fetch events.
* @param {GetEventsQueryParams} options.queryParams - The query parameters to use for fetching events.
* @param {GetEventsHeadersInput} options.headersInput - The headers to include in the request.
*/
export class EventStream extends ByteStream {
_eventsManager: EventsManager;
_queryParams: GetEventsQueryParams;
_headersInput: GetEventsHeadersInput;
_streamPosition: string;
_longPollingInfo: RealtimeServer | undefined;
_longPollingRetries: number = 0;
_started: boolean = false;
_abortController: AbortController | undefined;
_deduplicationFilterSize: number = 1000;
_dedupHash: Map<string, boolean> = new Map();
constructor(options: {
eventsManager: EventsManager;
queryParams: GetEventsQueryParams;
headersInput: GetEventsHeadersInput;
}) {
super({
objectMode: true,
});
this._eventsManager = options.eventsManager;
this._streamPosition = options.queryParams.streamPosition || 'now';
this._queryParams = options.queryParams;
this._headersInput = options.headersInput;
this._abortController = new AbortController();
this._dedupHash = new Map<string, boolean>();
}
_read(size: number): void {
if (this.destroyed) {
return;
}
if (!this._started) {
this._started = true;
this.fetchEvents();
}
}
_destroy(
error: Error | null,
callback: (error?: Error | null) => void,
): void {
this._abortController?.abort('Stream destroyed');
if (!error) {
this.push(null);
}
callback(error);
}
async getLongPollInfo(): Promise<void> {
if (this.destroyed) {
return;
}
try {
const info = await this._eventsManager.getEventsWithLongPolling(
undefined,
this._abortController?.signal,
);
const server =
info.entries?.find((entry) => entry.type === 'realtime_server') ||
undefined;
if (!server) {
throw new BoxSdkError({
message: 'No realtime server found in the response.',
});
}
this._longPollingInfo = server;
this._longPollingRetries = 0;
return this.doLongPoll();
} catch (error: any) {
if (error.name !== 'AbortError') {
this.emit('error', error);
return this.getLongPollInfo();
}
}
}
async doLongPoll(): Promise<void> {
if (this.destroyed) {
return;
}
try {
if (
!this._longPollingInfo ||
this._longPollingRetries >
parseInt(this._longPollingInfo?.maxRetries || '10', 10)
) {
return this.getLongPollInfo();
}
this._longPollingRetries++;
const longPollUrl = this._longPollingInfo?.url!;
const longPollWithStreamPosition = `${longPollUrl}${
longPollUrl.includes('?') ? '&' : '?'
}stream_position=${this._streamPosition}`;
const response =
await this._eventsManager.networkSession.networkClient.fetch({
url: longPollWithStreamPosition,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
responseFormat: 'json',
auth: this._eventsManager.auth,
networkSession: this._eventsManager.networkSession,
cancellationToken: this._abortController?.signal,
});
if (this.destroyed) {
return;
}
if (response.data) {
const message = response.data as {
version: number;
message: string;
};
if (message.message === RealtimeServerEventValue.NEW_CHANGE) {
return this.fetchEvents();
} else if (message.message === RealtimeServerEventValue.RECONNECT) {
return this.getLongPollInfo();
}
return this.doLongPoll();
}
} catch (error: any) {
if (error.name !== 'AbortError') {
this.emit('error', error);
this.doLongPoll();
}
}
}
async fetchEvents(): Promise<void> {
if (this.destroyed) {
return;
}
try {
const events = await this._eventsManager.getEvents(
{
...this._queryParams,
...{
streamPosition: this._streamPosition,
},
},
this._headersInput,
this._abortController?.signal,
);
this._streamPosition = events.nextStreamPosition?.toString() || 'now';
if (events.entries) {
for (const entry of events.entries) {
if (entry.eventId) {
if (this._dedupHash.has(entry.eventId)) {
continue;
}
this._dedupHash.set(entry.eventId, true);
}
this.push(entry);
}
if (this._dedupHash.size >= this._deduplicationFilterSize) {
for (const [key] of this._dedupHash) {
if (!events.entries.some((entry) => entry.eventId === key)) {
this._dedupHash.delete(key);
}
}
}
}
return this.doLongPoll();
} catch (error: any) {
if (error.name !== 'AbortError') {
this.emit('error', error);
this.fetchEvents();
}
}
}
}