Skip to content

Commit

Permalink
Introduce Promise Helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Feb 25, 2025
1 parent 6497ae6 commit 278c647
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 272 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-rocks-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/promise-helpers': patch
---

New promise helpers
1 change: 1 addition & 0 deletions packages/cookie-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"typings": "dist/typings/index.d.ts",
"dependencies": {
"@whatwg-node/promise-helpers": "^0.0.0",
"tslib": "^2.6.3"
},
"publishConfig": {
Expand Down
15 changes: 8 additions & 7 deletions packages/cookie-store/src/CookieStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fakePromise } from '@whatwg-node/promise-helpers';
import { CookieChangeEvent } from './CookieChangeEvent.js';
import { parse } from './parse.js';
import {
Expand All @@ -21,15 +22,15 @@ export class CookieStore extends EventTarget {
this.cookieMap = parse(cookieString);
}

async get(
get(
init?: CookieStoreGetOptions['name'] | CookieStoreGetOptions | undefined,
): Promise<Cookie | undefined> {
if (init == null) {
throw new TypeError('CookieStoreGetOptions must not be empty');
} else if (init instanceof Object && !Object.keys(init).length) {
throw new TypeError('CookieStoreGetOptions must not be empty');
}
return (await this.getAll(init))[0];
return this.getAll(init).then(cookies => cookies[0]);
}

async set(init: CookieListItem | string, possibleValue?: string): Promise<void> {
Expand Down Expand Up @@ -95,21 +96,21 @@ export class CookieStore extends EventTarget {
}
}

async getAll(init?: CookieStoreGetOptions['name'] | CookieStoreGetOptions): Promise<Cookie[]> {
getAll(init?: CookieStoreGetOptions['name'] | CookieStoreGetOptions): Promise<Cookie[]> {
const cookies = Array.from(this.cookieMap.values());
if (init == null || Object.keys(init).length === 0) {
return cookies;
return fakePromise(cookies);
}
let name: string | undefined;
if (typeof init === 'string') {
name = init as string;
} else {
name = init.name;
}
return cookies.filter(cookie => cookie.name === name);
return fakePromise(cookies.filter(cookie => cookie.name === name));
}

async delete(init: CookieStoreDeleteOptions['name'] | CookieStoreDeleteOptions): Promise<void> {
delete(init: CookieStoreDeleteOptions['name'] | CookieStoreDeleteOptions): Promise<void> {
const item: CookieListItem = {
name: '',
value: '',
Expand All @@ -128,6 +129,6 @@ export class CookieStore extends EventTarget {

item.expires = 0;

await this.set(item);
return this.set(item);
}
}
1 change: 1 addition & 0 deletions packages/disposablestack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"typings": "dist/typings/index.d.ts",
"dependencies": {
"@whatwg-node/promise-helpers": "^0.0.0",
"tslib": "^2.6.3"
},
"publishConfig": {
Expand Down
34 changes: 14 additions & 20 deletions packages/disposablestack/src/AsyncDisposableStack.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { handleMaybePromiseLike, MaybePromiseLike } from '@whatwg-node/promise-helpers';
import { PonyfillSuppressedError } from './SupressedError.js';
import { DisposableSymbols } from './symbols.js';
import { isAsyncDisposable, isSyncDisposable, MaybePromise } from './utils.js';
import { isAsyncDisposable, isSyncDisposable } from './utils.js';

const SuppressedError = globalThis.SuppressedError || PonyfillSuppressedError;

export class PonyfillAsyncDisposableStack implements AsyncDisposableStack {
private callbacks: (() => MaybePromise<void>)[] = [];
private callbacks: (() => MaybePromiseLike<void>)[] = [];
get disposed(): boolean {
return this.callbacks.length === 0;
}
Expand All @@ -19,14 +20,14 @@ export class PonyfillAsyncDisposableStack implements AsyncDisposableStack {
return value;
}

adopt<T>(value: T, onDisposeAsync: (value: T) => MaybePromise<void>): T {
adopt<T>(value: T, onDisposeAsync: (value: T) => MaybePromiseLike<void>): T {
if (onDisposeAsync) {
this.callbacks.push(() => onDisposeAsync(value));
}
return value;
}

defer(onDisposeAsync: () => MaybePromise<void>): void {
defer(onDisposeAsync: () => MaybePromiseLike<void>): void {
if (onDisposeAsync) {
this.callbacks.push(onDisposeAsync);
}
Expand All @@ -45,24 +46,17 @@ export class PonyfillAsyncDisposableStack implements AsyncDisposableStack {

private _error?: Error | undefined;

private _iterateCallbacks(): MaybePromise<void> {
private _iterateCallbacks(): MaybePromiseLike<void> {
const cb = this.callbacks.pop();
if (cb) {
try {
const res$ = cb();
if (res$?.then) {
return res$.then(
() => this._iterateCallbacks(),
error => {
this._error = this._error ? new SuppressedError(error, this._error) : error;
return this._iterateCallbacks();
},
);
}
} catch (error: any) {
this._error = this._error ? new SuppressedError(error, this._error) : error;
}
return this._iterateCallbacks();
return handleMaybePromiseLike(
cb,
() => this._iterateCallbacks(),
error => {
this._error = this._error ? new SuppressedError(error, this._error) : error;
return this._iterateCallbacks();
},
);
}
}

Expand Down
2 changes: 0 additions & 2 deletions packages/disposablestack/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ export function isSyncDisposable(obj: any): obj is Disposable {
export function isAsyncDisposable(obj: any): obj is AsyncDisposable {
return obj?.[DisposableSymbols.asyncDispose] != null;
}

export type MaybePromise<T> = T | PromiseLike<T>;
1 change: 1 addition & 0 deletions packages/node-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"typings": "dist/typings/index.d.ts",
"dependencies": {
"@whatwg-node/disposablestack": "^0.0.5",
"@whatwg-node/promise-helpers": "^0.0.0",
"busboy": "^1.6.0",
"tslib": "^2.6.3"
},
Expand Down
38 changes: 1 addition & 37 deletions packages/node-fetch/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,7 @@ export function defaultHeadersSerializer(
return headerArray;
}

function isPromise<T>(val: T | Promise<T>): val is Promise<T> {
return (val as any)?.then != null;
}

export function fakePromise<T>(value: T): Promise<T> {
if (isPromise(value)) {
return value;
}
// Write a fake promise to avoid the promise constructor
// being called with `new Promise` in the browser.
return {
then(resolve: (value: T) => any) {
if (resolve) {
const callbackResult = resolve(value);
if (isPromise(callbackResult)) {
return callbackResult;
}
return fakePromise(callbackResult);
}
return this;
},
catch() {
return this;
},
finally(cb) {
if (cb) {
const callbackResult = cb();
if (isPromise(callbackResult)) {
return callbackResult.then(() => value);
}
return fakePromise(value);
}
return this;
},
[Symbol.toStringTag]: 'Promise',
};
}
export { fakePromise } from '@whatwg-node/promise-helpers';

export function isArrayBufferView(obj: any): obj is ArrayBufferView {
return obj != null && obj.buffer != null && obj.byteLength != null && obj.byteOffset != null;
Expand Down
50 changes: 50 additions & 0 deletions packages/promise-helpers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@whatwg-node/promise-helpers",
"version": "0.0.0",
"type": "module",
"description": "Promise helpers",
"repository": {
"type": "git",
"url": "ardatan/whatwg-node",
"directory": "packages/promise-helpers"
},
"author": "Arda TANRIKULU <[email protected]>",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"exports": {
".": {
"require": {
"types": "./dist/typings/index.d.cts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
},
"default": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./package.json": "./package.json"
},
"typings": "dist/typings/index.d.ts",
"dependencies": {
"tslib": "^2.6.3"
},
"publishConfig": {
"directory": "dist",
"access": "public"
},
"sideEffects": false,
"buildOptions": {
"input": "./src/index.ts"
},
"typescript": {
"definition": "dist/typings/index.d.ts"
}
}
132 changes: 132 additions & 0 deletions packages/promise-helpers/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
export type MaybePromise<T> = Promise<T> | T;
export type MaybePromiseLike<T> = PromiseLike<T> | T;

export function isPromise<T>(value: MaybePromise<T>): value is Promise<T> {
return isPromiseLike(value);
}

export function isPromiseLike<T>(value: MaybePromiseLike<T>): value is PromiseLike<T> {
return (value as PromiseLike<T>)?.then != null;
}

export function handleMaybePromiseLike<TInput, TOutput>(
inputFactory: () => MaybePromiseLike<TInput>,
outputSuccessFactory: (value: TInput) => MaybePromiseLike<TOutput>,
outputErrorFactory?: (err: any) => MaybePromiseLike<TOutput>,
): MaybePromiseLike<TOutput> {
function _handle() {
const input$ = inputFactory();
if (isPromiseLike(input$)) {
return input$.then(outputSuccessFactory, outputErrorFactory);
}
return outputSuccessFactory(input$);
}
if (!outputErrorFactory) {
return _handle();
}
try {
return _handle();
} catch (err) {
return outputErrorFactory(err);
}
}

export function handleMaybePromise<TInput, TOutput>(
inputFactory: () => MaybePromise<TInput>,
outputSuccessFactory: (value: TInput) => MaybePromise<TOutput>,
outputErrorFactory?: (err: any) => MaybePromise<TOutput>,
): MaybePromise<TOutput> {
return handleMaybePromiseLike(
inputFactory,
outputSuccessFactory,
outputErrorFactory,
) as MaybePromise<TOutput>;
}

export function fakePromise<T>(value: T): Promise<T> {
if (isPromise(value)) {
return value;
}
// Write a fake promise to avoid the promise constructor
// being called with `new Promise` in the browser.
return {
then(resolve: (value: T) => any) {
if (resolve) {
const callbackResult = resolve(value);
if (isPromise(callbackResult)) {
return callbackResult;
}
return fakePromise(callbackResult);
}
return this;
},
catch() {
return this;
},
finally(cb) {
if (cb) {
const callbackResult = cb();
if (isPromise(callbackResult)) {
return callbackResult.then(() => value);
}
return fakePromise(value);
}
return this;
},
[Symbol.toStringTag]: 'Promise',
};
}

export interface DeferredPromise<T = void> {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (reason: any) => void;
}

export function createDeferredPromise<T = void>(): DeferredPromise<T> {
if (Promise.withResolvers) {
return Promise.withResolvers<T>();
}
let resolveFn: (value: T) => void;
let rejectFn: (reason: any) => void;
const promise = new Promise<T>(function deferredPromiseExecutor(resolve, reject) {
resolveFn = resolve;
rejectFn = reject;
});
return {
promise,
get resolve() {
return resolveFn;
},
get reject() {
return rejectFn;
},
};
}

export function iterateAsyncVoid<TInput>(
iterable: Iterable<TInput>,
callback: (input: TInput, stopEarly: () => void) => Promise<void> | void,
): Promise<void> | void {
const iterator = iterable[Symbol.iterator]();
let stopEarlyFlag = false;
function stopEarlyFn() {
stopEarlyFlag = true;
}
function iterate(): Promise<void> | void {
const { done: endOfIterator, value } = iterator.next();
if (endOfIterator) {
return;
}
return handleMaybePromise(
() => callback(value, stopEarlyFn),
() => {
if (stopEarlyFlag) {
return;
}
return iterate();
},
);
}
return iterate();
}
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"@whatwg-node/disposablestack": "^0.0.5",
"@whatwg-node/fetch": "^0.10.5",
"@whatwg-node/promise-helpers": "^0.0.0",
"tslib": "^2.6.3"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit 278c647

Please sign in to comment.