Skip to content

Commit

Permalink
feat: expose ability to override fetch implementation (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey authored Aug 26, 2022
1 parent c767540 commit 9ffc57e
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 65 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ and fallback to the network otherwise
<micro-frame src="..." cache="force-cache"/>
```

## `fetch`

Optionally provide function to override default `fetch` logic.

```marko
<micro-frame src="..." name="..." fetch(url, options, fetch) {
// The 3rd parameter allows us to continue to use micro-frames fetch implementation (which is different server/browser).
// We can use this override to do things like a POST request, eg:
return fetch(url, {
...options,
method: "POST",
headers: {
...headers,
"Content-Type": "application/json"
},
body: JSON.stringify({ "some": "json" })
});
} />
```

## `timeout`

A timeout in `ms` (defaults to 30s) that will prematurely abort the request. This will trigger the `<@catch>` if provided.
Expand Down
118 changes: 55 additions & 63 deletions src/components/micro-frame/component/node.marko
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,41 @@ import path from "path";
import fetch from "make-fetch-happen";

static const cachePath = path.resolve("node_modules/.cache/fetch");
static function createDeferredPromise() {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
}
static const strictSSL = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0";

$ let buf = "";
$ let hasMore = true;
$ let sentData = false;
$ let next = (async () => {
static async function fetchStream(input, out) {
const incomingMessage = out.stream && (out.stream.req || out.stream.request);
if (!incomingMessage) {
throw new Error("Could not get request from stream.");
}
const url = new URL(input.src, `${incomingMessage.protocol}://${incomingMessage.headers.host}`);
const res = await fetch(url, {
cachePath,
cache: input.cache,
strictSSL: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0",
headers: {
...incomingMessage.headers,
...input.headers,
accept: "text/html",
}
});
const { cache } = input;
const headers = {
...incomingMessage.headers,
...input.headers,
accept: "text/html",
};
const res = await (input.fetch
? input.fetch(url, { cache, headers }, fetch)
: fetch(url, {
cache,
headers,
cachePath,
strictSSL,
})
);
if (!res.ok) throw new Error(res.statusText);
next = createDeferredPromise();
res.body
.on("data", data => {
buf += data.toString();
if (next) next = next.resolve();
})
.on("error", err => {
if (next) next = next.reject(err);
else out.error(err);
})
.on("end", () => {
hasMore = false;
if (next) next = next.resolve();
});
if (!res.body || !res.body[Symbol.asyncIterator]) {
throw new Error("Response body must be a stream.");
}
return next;
})();
return res.body[Symbol.asyncIterator]();
}

<div id=component.id data-src=input.src>
<if(input.loading)>
Expand All @@ -63,32 +45,42 @@ $ let next = (async () => {
$!{`<!--${component.id}-->`}
</if>

<macro name="wait">
<await(next)
timeout=input.timeout
catch=(!sentData && input.catch)>
<@then>
$!{buf}
<if(hasMore)>
$ buf = "";
$ sentData = true;
$ next = createDeferredPromise();
<wait/>
</if>
</@then>
</await>
</macro>

<!--
We put the streamed html in a preserved fragment.
This allows Marko to avoid diffing that section.
-->
$ out.bf("@_", component, true);
<wait/>
$ out.ef();
<await(fetchStream(input, out)) timeout=input.timeout catch=input.catch>
<@then|iter|>
<macro name="wait">
<await(iter.next()) timeout=input.timeout>
<@then|{ value, done }|>
<if(done)>
<if(input.loading)>
<!-- Remove all of the <@loading> content after we've received all the data -->
$ out.script(`((e,t,d)=>{t=document.getElementById(e);do{t.removeChild(d=t.firstChild)}while(d.data!==e)})(${JSON.stringify(component.id)});`);
</if>
</if>
<else>
$!{value}
<wait/>
</else>
</@then>
<@catch|err|>
<if(input.catch)>
<!-- Remove everything in the container and render our catch handler -->
<script>document.getElementById(${JSON.stringify(component.id)}).textContent=""</script>
<${input.catch}(err)/>
</if>
<else>
$ throw err;
</else>
</@catch>
</await>
</macro>

<if(input.loading)>
<!-- Remove all of the <@loading> content after we've received all the data -->
$ out.script(`((e,t,d)=>{t=document.getElementById(e);do{t.removeChild(d=t.firstChild)}while(d.data!==e)})(${JSON.stringify(component.id)});`);
</if>
<wait/>
</@then>
</await>
$ out.ef();
</div>
13 changes: 11 additions & 2 deletions src/components/micro-frame/component/web.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ interface Input {
loading?: unknown;
cache?: RequestCache;
headers?: Record<string, string>;
fetch?: (
url: string,
options: RequestInit,
fetch: typeof window.fetch
) => Promise<Response>;
}

interface State {
Expand Down Expand Up @@ -49,11 +54,15 @@ export = {
let err: Error | undefined;

try {
const res = await fetch(this.src, {
const options: RequestInit = {
cache: this.input.cache,
signal: controller.signal,
headers: Object.assign({}, this.input.headers, { accept: "text/html" }),
});
};

const res = await (this.input.fetch
? this.input.fetch(this.src, options, fetch)
: fetch(this.src, options));
if (!res.ok) throw new Error(res.statusText);
const reader = res.body!.getReader();
const decoder = new TextDecoder();
Expand Down

0 comments on commit 9ffc57e

Please sign in to comment.