diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8ee34c6..5a2bfa9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -151,6 +151,10 @@ export default defineConfig({ text: 'Variants', link: '/proxy-service/variants', }, + { + text: 'flattenPromise', + link: '/proxy-service/flatten-promise', + }, ], }, packages, diff --git a/docs/proxy-service/flatten-promise.md b/docs/proxy-service/flatten-promise.md new file mode 100644 index 0000000..ca2c552 --- /dev/null +++ b/docs/proxy-service/flatten-promise.md @@ -0,0 +1,23 @@ +# `flattenPromise` + +## Overview + +`flattenPromise` is a utility that makes it easier to work with `Promise` passed into your services. + +For example, it simplifies the implementation of the `TodosRepo` from the [IndexedDB example](./index#usage). + +```ts +function createTodosRepo(idbPromise: Promise) { + const idb = flattenPromise(idbPromise); // [!code ++] + + return { + async create(todo: Todo): Promise { + await (await idbPromise).add('todos', todo); // [!code --] + await idb.add('todos', todo); // [!code ++] + }, + // ... + }; +} +``` + +It works by using a `Proxy` to await the promise internally before calling any methods. diff --git a/packages/proxy-service/src/flattenPromise.test.ts b/packages/proxy-service/src/flattenPromise.test.ts new file mode 100644 index 0000000..ff29216 --- /dev/null +++ b/packages/proxy-service/src/flattenPromise.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { flattenPromise } from './flattenPromise'; + +describe('flattenPromise', () => { + it('should convert Promise to DeepAsync', async () => { + const fnPromise = Promise.resolve((x: number, y: number) => x + y); + + const fn = flattenPromise(fnPromise); + const actual = await fn(1, 2); + + expect(actual).toBe(3); + }); + + it('should convert shallow Promise to DeepAsync', async () => { + const objectPromise = Promise.resolve({ + additionalIncrement: 1, + add(x: number, y: number): number { + return x + y + this.additionalIncrement; + }, + }); + + const object = flattenPromise(objectPromise); + const actual = await object.add(1, 2); + + expect(actual).toBe(4); + }); + + it('should convert nested Promise to DeepAsync', async () => { + const objectPromise = Promise.resolve({ + math: { + additionalIncrement: 1, + add(x: number, y: number): number { + return x + y + this.additionalIncrement; + }, + }, + }); + + const object = flattenPromise(objectPromise); + const actual = await object.math.add(1, 2); + + expect(actual).toBe(4); + }); + + it('should convert Promise to DeepAsync', async () => { + const instancePromise = Promise.resolve( + new (class { + additionalIncrement = 1; + add(x: number, y: number): number { + return x + y + this.additionalIncrement; + } + })(), + ); + + const instance = flattenPromise(instancePromise); + const actual = await instance.add(1, 2); + + expect(actual).toBe(4); + }); +}); diff --git a/packages/proxy-service/src/flattenPromise.ts b/packages/proxy-service/src/flattenPromise.ts new file mode 100644 index 0000000..b5dec6c --- /dev/null +++ b/packages/proxy-service/src/flattenPromise.ts @@ -0,0 +1,53 @@ +import get from 'get-value'; +import { DeepAsync } from './types'; + +/** + * Given a promise of a variable, return a proxy to that variable that awaits the promise internally + * so you don't have to call `await` twice. + * + * You can unwrap promises of functions, objects, or classes. + * + * This is meant to be used to simplify service implementations, like so: + * + * @example + * function createService(dependencyPromise: Promise) { + * const dependency = flattenPromise(dependencyPromise); + * + * return { + * doSomething() { + * await dependency.someAsyncWork(); + * // Instead of `await (await dependencyPromise).someAsyncWork();` + * } + * } + * } + */ +export function flattenPromise(promise: Promise): DeepAsync { + function createProxy(location?: { propertyPath: string; parentPath?: string }): DeepAsync { + const wrapped = (() => {}) as DeepAsync; + const proxy = new Proxy(wrapped, { + async apply(_target, _thisArg, args) { + const t = (await promise) as any; + const thisArg = (location?.parentPath ? get(t, location.parentPath) : t) as any | undefined; + const fn = (location ? get(t, location.propertyPath) : t) as (...args: any[]) => any; + return fn.apply(thisArg, args); + }, + + // Executed when accessing a property on an object + get(target, propertyName, receiver) { + if (propertyName === '__proxy' || typeof propertyName === 'symbol') { + return Reflect.get(target, propertyName, receiver); + } + return createProxy({ + propertyPath: + location == null ? propertyName : `${location.propertyPath}.${propertyName}`, + parentPath: location?.propertyPath, + }); + }, + }); + // @ts-expect-error: Adding a hidden property + proxy.__proxy = true; + return proxy; + } + + return createProxy(); +} diff --git a/packages/proxy-service/src/index.ts b/packages/proxy-service/src/index.ts index 46021f6..22c0b80 100644 --- a/packages/proxy-service/src/index.ts +++ b/packages/proxy-service/src/index.ts @@ -1,2 +1,2 @@ export { defineProxyService } from './defineProxyService'; -export type { ProxyServiceConfig } from './types'; +export type { ProxyServiceConfig, DeepAsync } from './types';