Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
* @param event The event to send to Sentry.
* @param hint May contain additional information about the original exception.
* @param currentScope A scope containing event metadata.
* @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send.
* @returns A PromiseLike that resolves with the event or rejects in case event was/will not be send.
*/
protected _processEvent(
event: Event,
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,12 @@ export {
supportsReferrerPolicy,
supportsReportingObserver,
} from './utils/supports';
export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise';
export {
// eslint-disable-next-line deprecation/deprecation
SyncPromise,
rejectedSyncPromise,
resolvedSyncPromise,
} from './utils/syncpromise';
export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils/time';
export {
TRACEPARENT_REGEXP,
Expand Down
69 changes: 27 additions & 42 deletions packages/core/src/utils/promisebuffer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './syncpromise';
import { rejectedSyncPromise, resolvedSyncPromise } from './syncpromise';

export interface PromiseBuffer<T> {
// exposes the internal array so tests can assert on the state of it.
// XXX: this really should not be public api.
$: Array<PromiseLike<T>>;
$: PromiseLike<T>[];
add(taskProducer: () => PromiseLike<T>): PromiseLike<T>;
drain(timeout?: number): PromiseLike<boolean>;
}
Expand All @@ -14,11 +14,11 @@ export const SENTRY_BUFFER_FULL_ERROR = Symbol.for('SentryBufferFullError');
* Creates an new PromiseBuffer object with the specified limit
* @param limit max number of promises that can be stored in the buffer
*/
export function makePromiseBuffer<T>(limit?: number): PromiseBuffer<T> {
const buffer: Array<PromiseLike<T>> = [];
export function makePromiseBuffer<T>(limit: number = 100): PromiseBuffer<T> {
const buffer: Set<PromiseLike<T>> = new Set();

function isReady(): boolean {
return limit === undefined || buffer.length < limit;
return buffer.size < limit;
}

/**
Expand All @@ -27,8 +27,8 @@ export function makePromiseBuffer<T>(limit?: number): PromiseBuffer<T> {
* @param task Can be any PromiseLike<T>
* @returns Removed promise.
*/
function remove(task: PromiseLike<T>): PromiseLike<T | void> {
return buffer.splice(buffer.indexOf(task), 1)[0] || Promise.resolve(undefined);
function remove(task: PromiseLike<T>): void {
buffer.delete(task);
}

/**
Expand All @@ -48,19 +48,11 @@ export function makePromiseBuffer<T>(limit?: number): PromiseBuffer<T> {

// start the task and add its promise to the queue
const task = taskProducer();
if (buffer.indexOf(task) === -1) {
buffer.push(task);
}
void task
.then(() => remove(task))
// Use `then(null, rejectionHandler)` rather than `catch(rejectionHandler)` so that we can use `PromiseLike`
// rather than `Promise`. `PromiseLike` doesn't have a `.catch` method, making its polyfill smaller. (ES5 didn't
// have promises, so TS has to polyfill when down-compiling.)
.then(null, () =>
remove(task).then(null, () => {
// We have to add another catch here because `remove()` starts a new promise chain.
}),
);
buffer.add(task);
void task.then(
() => remove(task),
() => remove(task),
);
return task;
}

Expand All @@ -74,34 +66,27 @@ export function makePromiseBuffer<T>(limit?: number): PromiseBuffer<T> {
* `false` otherwise
*/
function drain(timeout?: number): PromiseLike<boolean> {
return new SyncPromise<boolean>((resolve, reject) => {
let counter = buffer.length;
if (!buffer.size) {
return resolvedSyncPromise(true);
}

if (!counter) {
return resolve(true);
}
const drainPromise = Promise.all(Array.from(buffer)).then(() => true);

if (!timeout) {
return drainPromise;
}

// wait for `timeout` ms and then resolve to `false` (if not cancelled first)
const capturedSetTimeout = setTimeout(() => {
if (timeout && timeout > 0) {
resolve(false);
}
}, timeout);
const promises = [drainPromise, new Promise<boolean>(resolve => setTimeout(() => resolve(false), timeout))];

// if all promises resolve in time, cancel the timer and resolve to `true`
buffer.forEach(item => {
void resolvedSyncPromise(item).then(() => {
if (!--counter) {
clearTimeout(capturedSetTimeout);
resolve(true);
}
}, reject);
});
});
// Promise.race will resolve to the first promise that resolves or rejects
// So if the drainPromise resolves, the timeout promise will be ignored
return Promise.race(promises);
}

return {
$: buffer,
get $(): PromiseLike<T>[] {
return Array.from(buffer);
},
add,
drain,
};
Expand Down
27 changes: 19 additions & 8 deletions packages/core/src/utils/syncpromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ const STATE_RESOLVED = 1;
const STATE_REJECTED = 2;

type State = typeof STATE_PENDING | typeof STATE_RESOLVED | typeof STATE_REJECTED;
type Executor<T> = (resolve: (value?: T | PromiseLike<T> | null) => void, reject: (reason?: any) => void) => void;
type PromiseTry = <T>(executor: Executor<T>) => PromiseLike<T>;
type PromiseWithTry = PromiseConstructor & { try: PromiseTry };

function hasPromiseTry(Promise: typeof globalThis.Promise): Promise is PromiseWithTry {
return 'try' in Promise && typeof Promise.try === 'function';
}

// Overloads so we can call resolvedSyncPromise without arguments and generic argument
export function resolvedSyncPromise(): PromiseLike<void>;
Expand All @@ -19,9 +26,8 @@ export function resolvedSyncPromise<T>(value: T | PromiseLike<T>): PromiseLike<T
* @returns the resolved sync promise
*/
export function resolvedSyncPromise<T>(value?: T | PromiseLike<T>): PromiseLike<T> {
return new SyncPromise(resolve => {
resolve(value);
});
// eslint-disable-next-line deprecation/deprecation
return hasPromiseTry(Promise) ? Promise.try(() => value) : new SyncPromise(resolve => resolve(value));
}

/**
Expand All @@ -31,16 +37,19 @@ export function resolvedSyncPromise<T>(value?: T | PromiseLike<T>): PromiseLike<
* @returns the rejected sync promise
*/
export function rejectedSyncPromise<T = never>(reason?: any): PromiseLike<T> {
return new SyncPromise((_, reject) => {
reject(reason);
});
return hasPromiseTry(Promise)
? Promise.try(() => {
throw reason;
})
: // eslint-disable-next-line deprecation/deprecation
new SyncPromise((_, reject) => reject(reason));
}

type Executor<T> = (resolve: (value?: T | PromiseLike<T> | null) => void, reject: (reason?: any) => void) => void;

/**
* Thenable class that behaves like a Promise and follows it's interface
* but is not async internally
*
* @deprecated This export will be removed in a future version.
*/
export class SyncPromise<T> implements PromiseLike<T> {
private _state: State;
Expand All @@ -59,6 +68,7 @@ export class SyncPromise<T> implements PromiseLike<T> {
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
): PromiseLike<TResult1 | TResult2> {
// eslint-disable-next-line deprecation/deprecation
return new SyncPromise((resolve, reject) => {
this._handlers.push([
false,
Expand Down Expand Up @@ -100,6 +110,7 @@ export class SyncPromise<T> implements PromiseLike<T> {

/** @inheritdoc */
public finally<TResult>(onfinally?: (() => void) | null): PromiseLike<TResult> {
// eslint-disable-next-line deprecation/deprecation
return new SyncPromise<TResult>((resolve, reject) => {
let val: TResult | any;
let isRejected: boolean;
Expand Down
3 changes: 1 addition & 2 deletions packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
makeSession,
Scope,
setCurrentClient,
SyncPromise,
withMonitor,
} from '../../src';
import * as integrationModule from '../../src/integration';
Expand Down Expand Up @@ -2111,7 +2110,7 @@ describe('Client', () => {
const spy = vi.spyOn(TestClient.instance!, 'eventFromMessage');
spy.mockImplementationOnce(
(message, level) =>
new SyncPromise(resolve => {
new Promise(resolve => {
setTimeout(() => resolve({ message, level }), 150);
}),
);
Expand Down
Loading
Loading