Skip to content

Commit

Permalink
add browser client and server
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-nagy committed Dec 21, 2023
1 parent 9cb8865 commit 143e7fa
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 35 deletions.
32 changes: 32 additions & 0 deletions packages/browser/src/BrowserClient.sw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { assert } from "sinon";

import * as BrowserClient from "./BrowserClient.js";
import * as Test from "./Test.js";

const { test } = Test;

afterEach(async () => {
await navigator.serviceWorker
.getRegistrations()
.then((registrations) =>
Promise.all(registrations.map((r) => r.unregister()))
);
});

test("a frame making a request to a service worker", async () => {
const [worker] = await Test.createServiceWorker(/* ts */ `
import * as BrowserServer from "http://localhost:8000/packages/browser/src/BrowserServer.ts";
const server = BrowserServer.listen({
address: "foo",
handle(_request) {
return "hi from the worker";
}
});
`);

const client = BrowserClient.from(worker);
const response = await client.fetch("foo", { body: "hi" });

assert.match(response, "hi from the worker");
});
79 changes: 79 additions & 0 deletions packages/browser/src/BrowserClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
filter,
firstValueFrom,
fromEvent
} from "@daniel-nagy/transporter/Observable";
import { assert } from "sinon";

import * as BrowserClient from "./BrowserClient.js";
import * as BrowserServer from "./BrowserServer.js";
import * as Test from "./Test.js";

const { test } = Test;

test("a child frame making a request to a parent frame", async () => {
BrowserServer.listen({
address: "foo",
handle(_request) {
return "hi from the parent";
}
});

const srcDoc = /* html */ `
<script type="module" data-transpile>
import * as BrowserClient from "/packages/browser/src/BrowserClient.ts";
const client = BrowserClient.from(self.parent);
const response = await client.fetch("foo", { body: "hi" });
self.parent.postMessage(
{ type: "received", response },
{ targetOrigin: "*" }
);
</script>
`;

const messageStream = fromEvent<MessageEvent>(self, "message");
document.body.append(await Test.createIframe(srcDoc));

const response = await firstValueFrom(
messageStream.pipe(filter((message) => message.data.type === "received"))
);

assert.match(response.data.response, "hi");
});

test("a frame making a request to a dedicated worker", async () => {
const worker = await Test.createWorker(/* ts */ `
import * as BrowserServer from "http://localhost:8000/packages/browser/src/BrowserServer.ts";
const server = BrowserServer.listen({
address: "foo",
handle(_request) {
return "hi from the worker";
}
});
`);

const client = BrowserClient.from(worker);
const response = await client.fetch("foo", { body: "hi" });

assert.match(response, "hi from the worker");
});

test("a frame making a request to a shared worker", async () => {
const worker = await Test.createSharedWorker(/* ts */ `
import * as BrowserServer from "http://localhost:8000/packages/browser/src/BrowserServer.ts";
const server = BrowserServer.listen({
address: "foo",
handle(_request) {
return "hi from the worker";
}
});
`);

const client = BrowserClient.from(worker);
const response = await client.fetch("foo", { body: "hi" });

assert.match(response, "hi from the worker");
});
93 changes: 93 additions & 0 deletions packages/browser/src/BrowserClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
filter,
firstValueFrom,
fromEvent,
map
} from "@daniel-nagy/transporter/Observable";

import * as Request from "./BrowserRequest.js";
import * as Response from "./BrowserResponse.js";
import * as StructuredCloneable from "./StructuredCloneable.js";

export { BrowserClient as t };

export type Options = {
origin?: string;
};

export class BrowserClient {
public readonly origin: string;
public readonly target: Window | Worker | SharedWorker | ServiceWorker;

constructor({
origin = "*",
target
}: {
origin?: string;
target: Window | Worker | SharedWorker | ServiceWorker;
}) {
this.origin = origin;
this.target = target;
}

async fetch(
address: string,
payload: {
body: StructuredCloneable.t;
}
) {
const messageSink = getMessageSink(this.target);
const messageSource = getMessageSource(this.target);
const request = Request.t({ address, ...payload });

const response = fromEvent<MessageEvent>(messageSource, "message").pipe(
filter(
(message): message is MessageEvent<Response.t> =>
Response.isResponse(message) && message.data.id === request.id
),
map((message) => message.data.body)
);

// Not sure it this is necessary or useful.
if (this.target instanceof ServiceWorker)
await navigator.serviceWorker.ready;

messageSink.postMessage(request, { targetOrigin: this.origin });

return firstValueFrom(response);
}
}

export function from(
target: Window | Worker | SharedWorker | ServiceWorker,
options: Options = {}
) {
if (target instanceof SharedWorker) target.port.start();
return new BrowserClient({ ...options, target });
}

function getMessageSink(
target: Window | Worker | SharedWorker | ServiceWorker
) {
switch (true) {
case target instanceof SharedWorker:
return target.port;
default:
return target;
}
}

function getMessageSource(
target: Window | Worker | SharedWorker | ServiceWorker
) {
switch (true) {
case target instanceof ServiceWorker:
return navigator.serviceWorker;
case target instanceof SharedWorker:
return target.port;
case target instanceof Worker:
return target;
default:
return self;
}
}
35 changes: 35 additions & 0 deletions packages/browser/src/BrowserRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as JsObject from "@daniel-nagy/transporter/JsObject";

import * as StructuredCloneable from "./StructuredCloneable.js";

export { Request as t };

const Type = "Request";

export type Request = {
address: string;
body: StructuredCloneable.t;
id: string;
type: typeof Type;
};

export const Request = ({
address,
body
}: {
address: string;
body: StructuredCloneable.t;
}): Request => ({
address,
body,
id: crypto.randomUUID(),
type: Type
});

export function isRequest(event: MessageEvent): event is MessageEvent<Request> {
return (
JsObject.isObject(event.data) &&
JsObject.has(event.data, "type") &&
event.data.type === Type
);
}
35 changes: 35 additions & 0 deletions packages/browser/src/BrowserResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as JsObject from "@daniel-nagy/transporter/JsObject";

import * as StructuredCloneable from "./StructuredCloneable.js";

export { Response as t };

const Type = "Response";

export type Response = {
body: StructuredCloneable.t;
id: string;
type: typeof Type;
};

export const Response = ({
body,
id
}: {
body: StructuredCloneable.t;
id: string;
}): Response => ({
body,
id,
type: Type
});

export function isResponse(
event: MessageEvent
): event is MessageEvent<Response> {
return (
JsObject.isObject(event.data) &&
JsObject.has(event.data, "type") &&
event.data.type === Type
);
}
Loading

0 comments on commit 143e7fa

Please sign in to comment.