Skip to content

Commit

Permalink
feat: Export flattenPromise helper function
Browse files Browse the repository at this point in the history
  • Loading branch information
aklinker1 committed May 1, 2023
1 parent 2c4d1cd commit 7f2a6dc
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 1 deletion.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export default defineConfig({
text: 'Variants',
link: '/proxy-service/variants',
},
{
text: 'flattenPromise',
link: '/proxy-service/flatten-promise',
},
],
},
packages,
Expand Down
23 changes: 23 additions & 0 deletions docs/proxy-service/flatten-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# `flattenPromise`

## Overview

`flattenPromise` is a utility that makes it easier to work with `Promise<Dependency>` passed into your services.

For example, it simplifies the implementation of the `TodosRepo` from the [IndexedDB example](./index#usage).

```ts
function createTodosRepo(idbPromise: Promise<IDBPDatabase>) {
const idb = flattenPromise(idbPromise); // [!code ++]

return {
async create(todo: Todo): Promise<void> {
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.
59 changes: 59 additions & 0 deletions packages/proxy-service/src/flattenPromise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';
import { flattenPromise } from './flattenPromise';

describe('flattenPromise', () => {
it('should convert Promise<Function> to DeepAsync<Function>', 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<Object> to DeepAsync<Object>', 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<Object> to DeepAsync<Object>', 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<Class> to DeepAsync<Class>', 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);
});
});
53 changes: 53 additions & 0 deletions packages/proxy-service/src/flattenPromise.ts
Original file line number Diff line number Diff line change
@@ -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<SomeDependency>) {
* const dependency = flattenPromise(dependencyPromise);
*
* return {
* doSomething() {
* await dependency.someAsyncWork();
* // Instead of `await (await dependencyPromise).someAsyncWork();`
* }
* }
* }
*/
export function flattenPromise<T>(promise: Promise<T>): DeepAsync<T> {
function createProxy(location?: { propertyPath: string; parentPath?: string }): DeepAsync<T> {
const wrapped = (() => {}) as DeepAsync<T>;
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();
}
2 changes: 1 addition & 1 deletion packages/proxy-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { defineProxyService } from './defineProxyService';
export type { ProxyServiceConfig } from './types';
export type { ProxyServiceConfig, DeepAsync } from './types';

0 comments on commit 7f2a6dc

Please sign in to comment.