Skip to content

Commit

Permalink
feat: cache api + req/res/url transforms (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-elicx authored Sep 27, 2023
1 parent 50ff06b commit 906b21f
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-lemons-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'cf-bindings-proxy': minor
---

Support for the Cache API, as well as support for transforming Request / Response / URL objects.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@ Note: Functionality and bindings not listed below may still work but have not be
- [x] delete
- [ ] createMultipartUpload (needs more tests)
- [ ] resumeMultipartUpload (needs more tests)

#### Cache API

- [x] put
- [x] match
- [x] delete
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"prettier:format": "prettier --ignore-unknown --ignore-path=.gitignore --write .",
"tsc": "tsc --noEmit",
"test": "vitest run",
"test:kill": "rm -rf .wrangler; sudo kill -9 `sudo lsof -i :8799 -t`",
"test:kill": "rm -rf .wrangler; pkill workerd",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"alter-version": "node ./scripts/alter-version.js",
Expand Down
33 changes: 28 additions & 5 deletions src/cli/template/_worker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CacheStorage } from '@cloudflare/workers-types';
import type { BindingRequest, BindingResponse, PropertyCall } from '../../proxy';
import type { FunctionInfo, TransformRule } from '../../transform';
import { prepareDataForProxy, transformData } from '../../transform';
Expand Down Expand Up @@ -39,11 +40,28 @@ export default {

try {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { __original_call, __bindingId, __calls } = await request.json<BindingRequest>();
const { __original_call, __proxyType, __bindingId, __calls } =
await request.json<BindingRequest>();

const callee = __original_call
? await reduceCalls(env[__original_call.__bindingId], __original_call.__calls)
: env[__bindingId];
const baseId = __original_call ? __original_call.__bindingId : __bindingId;

let base;
switch (__proxyType) {
case 'caches': {
const asCacheStorage = caches as unknown as CacheStorage;
base = baseId === 'default' ? asCacheStorage.default : await asCacheStorage.open(baseId);
break;
}
case 'binding': {
base = env[baseId];
break;
}
default: {
throw new Error('Unknown proxy type');
}
}

const callee = __original_call ? await reduceCalls(base, __original_call.__calls) : base;

const rawData = await reduceCalls(callee, __calls);
const resp: BindingResponse = { success: true, data: rawData, functions: {} };
Expand All @@ -53,7 +71,12 @@ export default {
resp.transform = transformedResp.transform;
resp.data = transformedResp.data;

if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {
if (
rawData &&
typeof rawData === 'object' &&
!Array.isArray(rawData) &&
![Response, Request, URL].find((t) => rawData instanceof t)
) {
// resp.arrayBuffer() => Promise<ArrayBuffer>
if ('arrayBuffer' in rawData && typeof rawData.arrayBuffer === 'function') {
const buffer = await rawData.arrayBuffer();
Expand Down
64 changes: 59 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import type { Cache, CacheStorage } from '@cloudflare/workers-types';
import { createBindingProxy } from './proxy';

/**
* Whether the bindings proxy is enabled and currently active.
*
* The proxy is enabled by default in development mode, but can be disabled by setting
* `DISABLE_BINDINGS_PROXY` to `true`.
*
* Alternatively, it can be enabled in other environments by setting `ENABLE_BINDINGS_PROXY` to
* `true`.
* */
export const isProxyEnabled = () =>
process?.env?.ENABLE_BINDINGS_PROXY ||
(!process?.env?.DISABLE_BINDINGS_PROXY && process?.env?.NODE_ENV === 'development');

/**
* Interfaces with a binding from the environment.
*
Expand All @@ -19,21 +33,61 @@ import { createBindingProxy } from './proxy';
* @returns Binding value.
*/
export const binding = <T>(id: string, opts?: BindingOpts): T => {
if (
process?.env?.ENABLE_BINDINGS_PROXY ||
(!process?.env?.DISABLE_BINDINGS_PROXY && process?.env?.NODE_ENV === 'development')
) {
if (isProxyEnabled()) {
return new Proxy(
{},
{
get: (_, prop) => createBindingProxy<T>(id)[prop as keyof T],
get: (_, prop) => createBindingProxy<T>(id, { proxyType: 'binding' })[prop as keyof T],
},
) as T;
}

return (opts?.fallback ?? process?.env)?.[id] as T;
};

type DeriveCacheReturnType<T> = T extends 'default' | undefined ? Cache : Promise<Cache>;

/**
* Interfaces with the Cloudflare Cache API.
*
* By default, the `default` cache is used, however, a custom cache can be provided by passing a
* cache name as the first argument.
*
* @example
* ```ts
* const value = await cacheApi().put(..., ...);
* ```
*
* @example
* ```ts
* const value = await cacheApi('custom').put(..., ...);
* ```
*
* @param cacheName Name of the cache to open, or `undefined` to open the default cache.
* @returns Cache instance.
*/
export const cacheApi = <T extends string | undefined = undefined>(
cacheName?: T,
): DeriveCacheReturnType<T> => {
if (isProxyEnabled()) {
return new Proxy(
{},
{
get: (_, prop: keyof Cache) =>
createBindingProxy<Cache>(cacheName ?? 'default', { proxyType: 'caches' })[prop],
},
) as DeriveCacheReturnType<T>;
}

const cachesInstance = caches as unknown as CacheStorage;

return (
cacheName === 'default' || cacheName === undefined
? cachesInstance.default
: cachesInstance.open(cacheName)
) as DeriveCacheReturnType<T>;
};

type BindingOpts = {
fallback: Record<string, unknown>;
};
39 changes: 28 additions & 11 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type BindingResponse =
/**
* Prepares the binding request to be sent to the proxy.
*
* @param bindingRequest
* @param bindingRequest The binding request to prepare.
*/
const prepareBindingRequest = async (bindingRequest: BindingRequest): Promise<BindingRequest> => {
return {
Expand Down Expand Up @@ -116,8 +116,11 @@ export type PropertyCall<Transform extends TransformRule | undefined = Transform
}[];
};

export type ProxyType = 'binding' | 'caches';

export type BindingRequest = {
__original_call?: BindingRequest;
__proxyType: ProxyType;
__bindingId: string;
__calls: PropertyCall[];
__chainUntil: string[];
Expand All @@ -132,6 +135,7 @@ export type BindingRequest = {
* @returns A proxy object.
*/
const createResponseProxy = <T extends object>(
proxyType: ProxyType,
bindingId: string,
originalProxy: BindingRequest,
data: T,
Expand Down Expand Up @@ -166,7 +170,10 @@ const createResponseProxy = <T extends object>(
}

// eslint-disable-next-line @typescript-eslint/no-use-before-define
const newProxy = createBindingProxy<BindingRequest>(bindingId, true);
const newProxy = createBindingProxy<BindingRequest>(bindingId, {
notChainable: true,
proxyType,
});

newProxy.__original_call = originalProxy;

Expand Down Expand Up @@ -194,21 +201,30 @@ const shouldChainUntil = (prop: string): string[] => {
return [];
};

const buildDefaultBindingRequest = (__proxyType: ProxyType, __bindingId: string) =>
({ __proxyType, __bindingId, __calls: [], __chainUntil: [] } as BindingRequest);

type CreateBindingOpts = { notChainable?: boolean; proxyType?: ProxyType };

/**
* Creates a proxy object for the binding.
*
* @param bindingId Binding ID.
* @param notChainable Whether or not the proxy should be chainable.
* @returns A proxy object.
*/
export const createBindingProxy = <T>(bindingId: string, notChainable = false): T => {
return new Proxy({ __bindingId: bindingId, __calls: [], __chainUntil: [] } as BindingRequest, {
export const createBindingProxy = <T>(
bindingId: string,
{ notChainable = false, proxyType = 'binding' }: CreateBindingOpts = {},
): T => {
return new Proxy(buildDefaultBindingRequest(proxyType, bindingId), {
get(target, prop: string) {
// internal properties
if (typeof prop === 'string' && prop.startsWith('__'))
return target[prop as keyof BindingRequest];
// ignore toJSON calls
if (prop === 'toJSON') return undefined;
// if the current proxy is not chainable, ignore calls
if (notChainable) return undefined;
// ignore then calls if there are no calls yet
if (target.__calls.length === 0 && prop === 'then') return undefined;
Expand All @@ -221,7 +237,7 @@ export const createBindingProxy = <T>(bindingId: string, notChainable = false):

// if we haven't reached the point where we should stop chaining, return a new proxy
if (target.__chainUntil.length && !target.__chainUntil.includes(prop)) {
const newProxy = createBindingProxy<BindingRequest>(bindingId);
const newProxy = createBindingProxy<BindingRequest>(bindingId, { proxyType });

newProxy.__chainUntil = target.__chainUntil;
newProxy.__calls = target.__calls;
Expand All @@ -237,15 +253,16 @@ export const createBindingProxy = <T>(bindingId: string, notChainable = false):

const data = await fetchData(target);

if (typeof data !== 'object' || !data) {
return data;
}

if (Array.isArray(data)) {
if (
typeof data !== 'object' ||
!data ||
Array.isArray(data) ||
[URL, Request, Response].find((t) => data instanceof t)
) {
return data;
}

return createResponseProxy(bindingId, target, data);
return createResponseProxy(proxyType, bindingId, target, data);
};
},
}) as T;
Expand Down
Loading

0 comments on commit 906b21f

Please sign in to comment.