Skip to content

Commit

Permalink
feat: ReadableStream support, and response function transformations (
Browse files Browse the repository at this point in the history
  • Loading branch information
james-elicx authored Sep 1, 2023
1 parent f91308e commit d87efba
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-pigs-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'cf-bindings-proxy': minor
---

Support for ReadableStream with R2 `.put(...)`, and `.get(...).body`.
5 changes: 5 additions & 0 deletions .changeset/mean-frogs-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'cf-bindings-proxy': minor
---

Transforms functions from responses when making calls in the proxy, instead of needing further HTTP calls to read their value. Support for handling binding calls that return accessors for `arrayBuffer()`, `blob()`, `text()`, `json()`, `body` + `bodyUsed`.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,22 @@ Note: Functionality and bindings not listed below may still work but have not be

- [x] put
- [x] writeHttpMetadata
- [x] _value type_: ArrayBuffer
- [x] _value type_: string
- [x] _value type_: Blob
- [x] _value type_: ReadableStream
- [x] get
- [x] writeHttpMetadata
- [x] text
- [x] json
- [x] arrayBuffer
- [x] blob
- [ ] body
- [ ] bodyUsed
- [x] body
- [x] bodyUsed
- [x] head
- [x] writeHttpMetadata
- [x] list
- [x] writeHttpMetadata
- [x] delete
- [ ] createMultipartUpload
- [ ] resumeMultipartUpload
- [ ] createMultipartUpload (needs more tests)
- [ ] resumeMultipartUpload (needs more tests)
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 48 additions & 6 deletions src/cli/template/_worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BindingRequest, BindingResponse, PropertyCall } from '../../proxy';
import type { FunctionInfo, TransformRule } from '../../transform';
import { prepareDataForProxy, transformData } from '../../transform';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -22,11 +23,8 @@ const reduceCalls = async (callee: Env, callsToProcess: PropertyCall[]): Promise
);
}

if (arg.transform) {
return transformData(arg.data, arg.transform);
}

return arg.data;
// @ts-expect-error - We don't know the type of the data.
return arg.transform ? transformData(arg.data, arg.transform) : arg.data;
}),
)),
);
Expand All @@ -48,12 +46,56 @@ export default {
: env[__bindingId];

const rawData = await reduceCalls(callee, __calls);
const resp: BindingResponse = { success: true, data: rawData };
const resp: BindingResponse = { success: true, data: rawData, functions: {} };

if (resp.success) {
const transformedResp = await prepareDataForProxy(rawData, { data: rawData });
resp.transform = transformedResp.transform;
resp.data = transformedResp.data;

if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {
// resp.arrayBuffer() => Promise<ArrayBuffer>
if ('arrayBuffer' in rawData && typeof rawData.arrayBuffer === 'function') {
const buffer = await rawData.arrayBuffer();
resp.functions.arrayBuffer = (await prepareDataForProxy(buffer, {
data: buffer,
})) as FunctionInfo<TransformRule<'buffer', 'base64'>>;
}

// NOTE: We can assume that we always have an arrayBuffer if we have any of the following.

// resp.blob() => Promise<Blob>
if ('blob' in rawData && typeof rawData.blob === 'function') {
resp.functions.blob = {
takeDataFrom: 'arrayBuffer',
transform: { from: 'buffer', to: 'blob' },
};
}

// resp.text() => Promise<string>
if ('text' in rawData && typeof rawData.text === 'function') {
resp.functions.text = {
takeDataFrom: 'arrayBuffer',
transform: { from: 'buffer', to: 'text' },
};
}

// resp.json<T>() => Promise<T>
if ('json' in rawData && typeof rawData.json === 'function') {
resp.functions.json = {
takeDataFrom: 'arrayBuffer',
transform: { from: 'buffer', to: 'json' },
};
}

// resp.body => ReadableStream
if ('body' in rawData && typeof rawData.body === 'object') {
resp.functions.body = {
takeDataFrom: 'arrayBuffer',
asAccessor: true,
};
}
}
}

return new Response(JSON.stringify(resp), {
Expand Down
80 changes: 70 additions & 10 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { prepareDataForProxy, transformData } from './transform';
import type { FunctionInfo, Functions, ParseType, TransformRule } from './transform';
import { prepareDataForProxy, transformData, transformFunctionInfo } from './transform';

export type BindingResponse =
| { success: false; data: string; transform?: never }
| { success: true; data: unknown; transform?: { from: string; to: string } };
| { success: false; data: string; transform?: never; functions?: never }
| {
success: true;
data: unknown;
transform?: TransformRule;
functions: { [key in Functions]?: FunctionInfo };
};

/**
* Prepares the binding request to be sent to the proxy.
Expand All @@ -29,11 +35,12 @@ const prepareBindingRequest = async (bindingRequest: BindingRequest): Promise<Bi
*/
const fetchData = async (call: BindingRequest): Promise<unknown> => {
const preparedCall = await prepareBindingRequest(call);
const stringifiedCall = JSON.stringify(preparedCall);

let resp: Response;
try {
resp = await fetch('http://127.0.0.1:8799', {
body: JSON.stringify(preparedCall),
body: stringifiedCall,
method: 'POST',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -42,20 +49,73 @@ const fetchData = async (call: BindingRequest): Promise<unknown> => {
throw new Error('Unable to connect to binding proxy');
}

const { success, data, transform } = await resp.json<BindingResponse>();
const { success, data, transform, functions } = await resp.json<BindingResponse>();

if (!success) {
throw new Error(data || 'Bad response from binding proxy');
}

return transform ? transformData(data, transform) : data;
};
// @ts-expect-error - We don't know the type of the data.
const finalData = transform ? transformData(data, transform) : data;

if (functions && finalData && typeof finalData === 'object' && !Array.isArray(finalData)) {
for (const [key, fnInfo] of Object.entries(functions)) {
const transformFn = await transformFunctionInfo(fnInfo, functions);

if (fnInfo.asAccessor) {
const value =
typeof transformFn === 'function' && !(transformFn instanceof Blob)
? await transformFn()
: transformFn;

if (key === 'body') {
const body = new ReadableStream({
start(controller) {
controller.enqueue(value);
controller.close();
},
});

Object.defineProperties(finalData, {
body: {
get() {
return body;
},
},
bodyUsed: {
get() {
return body.locked;
},
},
});
} else {
Object.defineProperty(finalData, key, {
get() {
return value;
},
});
}
} else {
// @ts-expect-error - this should be fine
finalData[key] = transformFn;
}
}
}

export type PropertyCall = {
prop: string;
args: { data: unknown | BindingRequest[]; transform?: { from: string; to: string } }[];
return finalData;
};

export type PropertyCall<Transform extends TransformRule | undefined = TransformRule | undefined> =
{
prop: string;
args: {
data:
| (Transform extends TransformRule ? ParseType<Transform['from']> : unknown)
| BindingRequest[];
transform?: Transform;
}[];
};

export type BindingRequest = {
__original_call?: BindingRequest;
__bindingId: string;
Expand Down
Loading

0 comments on commit d87efba

Please sign in to comment.