@statewalker/webrun-http-browser 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +321 -50
  3. package/demo/demo-1.html +109 -194
  4. package/demo/demo-2.html +119 -580
  5. package/dist/core/data-calls.d.ts +5 -0
  6. package/dist/core/data-calls.d.ts.map +1 -0
  7. package/dist/core/data-channels.d.ts +17 -0
  8. package/dist/core/data-channels.d.ts.map +1 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.d.ts.map +1 -0
  11. package/dist/core/message-target.d.ts +16 -0
  12. package/dist/core/message-target.d.ts.map +1 -0
  13. package/dist/core/registry.d.ts +18 -0
  14. package/dist/core/registry.d.ts.map +1 -0
  15. package/dist/http/http-send-recieve.d.ts +23 -0
  16. package/dist/http/http-send-recieve.d.ts.map +1 -0
  17. package/dist/http/index.d.ts +4 -0
  18. package/dist/http/index.d.ts.map +1 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2541 -807
  22. package/dist/relay/index-sw.d.ts +7 -0
  23. package/dist/relay/index-sw.d.ts.map +1 -0
  24. package/dist/relay/index.d.ts +69 -0
  25. package/dist/relay/index.d.ts.map +1 -0
  26. package/dist/relay/split-service-url.d.ts +13 -0
  27. package/dist/relay/split-service-url.d.ts.map +1 -0
  28. package/dist/relay-sw.d.ts +2 -0
  29. package/dist/relay-sw.d.ts.map +1 -0
  30. package/dist/relay-sw.js +878 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts +28 -0
  32. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
  33. package/dist/sw/index.d.ts +3 -0
  34. package/dist/sw/index.d.ts.map +1 -0
  35. package/dist/sw/sw-dispatcher.d.ts +74 -0
  36. package/dist/sw/sw-dispatcher.d.ts.map +1 -0
  37. package/dist/sw-worker.d.ts +2 -0
  38. package/dist/sw-worker.d.ts.map +1 -0
  39. package/dist/sw-worker.js +813 -0
  40. package/dist/sw.d.ts +2 -0
  41. package/dist/sw.d.ts.map +1 -0
  42. package/dist/sw.js +1076 -0
  43. package/package.json +50 -37
  44. package/public/index.html +61 -97
  45. package/public/index.js +7 -4
  46. package/public/sw-worker.js +4 -0
  47. package/public-relay/heartbeat.js +4 -3
  48. package/public-relay/relay-sw.js +4 -6
  49. package/public-relay/relay.html +16 -11
  50. package/src/core/data-calls.ts +46 -0
  51. package/src/core/data-channels.ts +224 -0
  52. package/src/core/index.ts +9 -0
  53. package/src/core/message-target.ts +18 -0
  54. package/src/core/registry.ts +54 -0
  55. package/src/http/http-send-recieve.ts +81 -0
  56. package/src/http/index.ts +7 -0
  57. package/src/index.ts +3 -0
  58. package/src/relay/index-sw.ts +145 -0
  59. package/src/relay/index.ts +261 -0
  60. package/src/relay/split-service-url.ts +30 -0
  61. package/src/relay-sw.ts +7 -0
  62. package/src/sw/http-sw-dispatcher.ts +122 -0
  63. package/src/sw/index.ts +2 -0
  64. package/src/sw/sw-dispatcher.ts +307 -0
  65. package/src/sw-worker.ts +7 -0
  66. package/src/sw.ts +1 -0
  67. package/CHANGELOG.md +0 -1
  68. package/demo/demo-3.html +0 -167
  69. package/dist/index-sw-umd.js +0 -798
  70. package/dist/index-sw-umd.min.js +0 -2
  71. package/dist/index-sw.js +0 -791
  72. package/dist/index-sw.min.js +0 -2
  73. package/dist/index-umd.js +0 -853
  74. package/dist/index-umd.min.js +0 -2
  75. package/dist/index.min.js +0 -2
  76. package/index.js +0 -1
  77. package/src/core/data-calls.js +0 -36
  78. package/src/core/data-channels.js +0 -192
  79. package/src/core/data-send-recieve.js +0 -28
  80. package/src/core/errors.js +0 -13
  81. package/src/core/index.js +0 -4
  82. package/src/http/HttpError.js +0 -59
  83. package/src/http/http-send-recieve.js +0 -40
  84. package/src/http/http-stubs.js +0 -135
  85. package/src/http/index.js +0 -3
  86. package/src/http/readable-streams.js +0 -33
  87. package/src/index.js +0 -7
  88. package/src/relay/index-sw.js +0 -216
  89. package/src/relay/index.js +0 -263
  90. package/src/relay/splitServiceUrl.js +0 -24
  91. package/src/sw/http-sw-dispatcher.js +0 -128
  92. package/src/sw/sw-dispatcher.js +0 -264
@@ -0,0 +1,122 @@
1
+ import type { HttpHandler } from "@statewalker/webrun-http-streams";
2
+ import { handleHttpRequests, sendHttpRequest } from "../http/http-send-recieve.js";
3
+ import {
4
+ SwPortDispatcher,
5
+ type SwPortDispatcherOptions,
6
+ SwPortHandler,
7
+ type SwPortHandlerOptions,
8
+ } from "./sw-dispatcher.js";
9
+
10
+ export interface SwHttpAdapterOptions extends Omit<SwPortHandlerOptions, "bindPort"> {
11
+ bindPort?: SwPortHandlerOptions["bindPort"];
12
+ }
13
+
14
+ export interface SwHttpRegistration {
15
+ baseUrl: string;
16
+ prefix: string;
17
+ remove(): Promise<void>;
18
+ }
19
+
20
+ export class SwHttpAdapter extends SwPortHandler {
21
+ private readonly _handlers = new Map<string, HttpHandler>();
22
+ private _cleanupRequestChannel?: () => void;
23
+
24
+ constructor(options: SwHttpAdapterOptions) {
25
+ super({
26
+ bindPort: () => {},
27
+ ...options,
28
+ });
29
+ }
30
+
31
+ protected override async _setCommunicationPort(port: MessagePort): Promise<void> {
32
+ this._cleanupRequestChannel?.();
33
+ this._cleanupRequestChannel = handleHttpRequests(port, this._handleHttpRequest.bind(this));
34
+ }
35
+
36
+ private async _handleHttpRequest(request: Request): Promise<Response> {
37
+ const requestUrl = request.url;
38
+ for (const [urlPrefix, handler] of this._handlers) {
39
+ if (requestUrl.indexOf(urlPrefix) === 0) {
40
+ return handler(request);
41
+ }
42
+ }
43
+ return new Response(null, { status: 404, statusText: "Error 404: Not found" });
44
+ }
45
+
46
+ /**
47
+ * Registers an HTTP handler on a prefix. Returns the resulting base URL and a
48
+ * disposer.
49
+ */
50
+ async register(prefix: string, handler: HttpHandler): Promise<SwHttpRegistration> {
51
+ const cleanPrefix = `./${(prefix ?? "").replace(/^[./]+/, "")}`;
52
+ const baseUrl = `${new URL(cleanPrefix, this.rootUrl)}`;
53
+ this._handlers.set(baseUrl, handler);
54
+ const handlers = this._handlers;
55
+ return {
56
+ baseUrl,
57
+ prefix: cleanPrefix,
58
+ async remove() {
59
+ handlers.delete(baseUrl);
60
+ },
61
+ };
62
+ }
63
+ }
64
+
65
+ export class SwHttpDispatcher extends SwPortDispatcher {
66
+ start(): void {
67
+ super.start();
68
+ this.self.addEventListener("fetch", this._handleFetchEvent.bind(this));
69
+ }
70
+
71
+ private _handleFetchEvent(event: FetchEvent): void {
72
+ event.respondWith(
73
+ (async (): Promise<Response> => {
74
+ const request = event.request;
75
+ try {
76
+ const requestUrl = request.url;
77
+ const rootUrl = this.scope;
78
+ if (requestUrl.indexOf(rootUrl) === 0) {
79
+ const key = requestUrl.substring(rootUrl.length).replace(/^\/?([^/]+).*$/, "$1");
80
+ const channelInfo = await this.loadChannelInfo(key);
81
+ if (channelInfo?.port) {
82
+ return await sendHttpRequest(channelInfo.port, request);
83
+ }
84
+ // Key was registered at some point on this SW but has no live
85
+ // handler right now (owner tab closed). Return 404 instead of
86
+ // falling through to the network — the alternative (a dev
87
+ // server's SPA fallback) serves the app shell for every site
88
+ // URL and creates a confusing refresh-loop where stale tabs
89
+ // keep resurrecting registrations.
90
+ if (this.isClaimedKey(key)) {
91
+ return new Response(null, {
92
+ status: 404,
93
+ statusText: "Not Found (no active handler)",
94
+ });
95
+ }
96
+ }
97
+ // URL is outside scope, or the first segment is unrelated to any
98
+ // registered site (Vite's /assets/*, /favicon.ico, etc.) — pass
99
+ // the event's Request straight through. Reconstructing it drops
100
+ // `mode`, which makes the constructor throw
101
+ // `'only-if-cached' can be set only with 'same-origin' mode` for
102
+ // navigation-style requests Chrome issues with that cache mode.
103
+ return await fetch(request);
104
+ } catch (error) {
105
+ console.error(error);
106
+ return new Response(null, {
107
+ status: 500,
108
+ statusText: "Error 500: Internal error",
109
+ });
110
+ }
111
+ })(),
112
+ );
113
+ }
114
+ }
115
+
116
+ export function startHttpDispatcher(options: SwPortDispatcherOptions): () => void {
117
+ const dispatcher = new SwHttpDispatcher(options);
118
+ dispatcher.start();
119
+ return () => {
120
+ void dispatcher.stop();
121
+ };
122
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./http-sw-dispatcher.js";
2
+ export * from "./sw-dispatcher.js";
@@ -0,0 +1,307 @@
1
+ import { get, set } from "idb-keyval";
2
+ import { callChannel, handleChannelCalls } from "../core/data-calls.js";
3
+ import { newRegistry } from "../core/registry.js";
4
+
5
+ export interface SwPortHandlerOptions {
6
+ key: string;
7
+ scope?: string;
8
+ serviceWorkerUrl?: string;
9
+ bindPort: (port: MessagePort) => void | Promise<void>;
10
+ }
11
+
12
+ interface ChannelInfo {
13
+ key: string;
14
+ clientId?: string;
15
+ port?: MessagePort;
16
+ }
17
+
18
+ /**
19
+ * Page-side counterpart: registers a ServiceWorker and sets up a
20
+ * MessageChannel with it, re-establishing the connection after SW wake-ups.
21
+ */
22
+ export class SwPortHandler {
23
+ readonly options: SwPortHandlerOptions;
24
+ private _serviceWorkerUrl?: string;
25
+ private _serviceWorker?: ServiceWorker;
26
+ private _registrationPromise?: Promise<void>;
27
+ private _cleanupRegistrations?: () => void;
28
+
29
+ constructor(options: SwPortHandlerOptions) {
30
+ this.options = options;
31
+ if (!this.key) throw new Error("Key is not defined.");
32
+ if (!this.scope) throw new Error("Scope is not defined");
33
+ }
34
+
35
+ get key(): string {
36
+ return this.options.key;
37
+ }
38
+
39
+ get scope(): string {
40
+ return this.options.scope ?? new URL("./", this.serviceWorkerUrl).pathname;
41
+ }
42
+
43
+ get rootUrl(): URL {
44
+ return new URL(this.scope, import.meta.url);
45
+ }
46
+
47
+ get serviceWorkerUrl(): string {
48
+ if (!this._serviceWorkerUrl) {
49
+ const url = this.options.serviceWorkerUrl
50
+ ? new URL(this.options.serviceWorkerUrl)
51
+ : new URL("./index-sw.js", this.rootUrl);
52
+ this._serviceWorkerUrl = `${url}`;
53
+ }
54
+ return this._serviceWorkerUrl;
55
+ }
56
+
57
+ protected async _newCommunicationChannel(): Promise<[MessagePort, MessagePort]> {
58
+ const messageChannel = new MessageChannel();
59
+ return [messageChannel.port1, messageChannel.port2];
60
+ }
61
+
62
+ protected async _setCommunicationPort(port: MessagePort): Promise<void> {
63
+ await this.options.bindPort(port);
64
+ }
65
+
66
+ protected async _updateCommunicationChannel(): Promise<void> {
67
+ if (!this._serviceWorker) return;
68
+ const [port1, port2] = await this._newCommunicationChannel();
69
+ await this._setCommunicationPort(port1);
70
+ await callChannel(
71
+ this._serviceWorker,
72
+ "UPDATE_COMMUNICATION_PORT",
73
+ this._getRegistrationInfo(),
74
+ port2,
75
+ );
76
+ }
77
+
78
+ protected _getRegistrationInfo(): ChannelInfo {
79
+ return { key: this.key };
80
+ }
81
+
82
+ async start(): Promise<void> {
83
+ if (!this._registrationPromise) {
84
+ this._registrationPromise = (async () => {
85
+ const [register, cleanup] = newRegistry();
86
+ this._cleanupRegistrations = cleanup;
87
+ register(() => {
88
+ this._cleanupRegistrations = undefined;
89
+ });
90
+ const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, {
91
+ scope: this.scope,
92
+ });
93
+ register(() => registration.unregister());
94
+
95
+ register(
96
+ handleChannelCalls(
97
+ navigator.serviceWorker,
98
+ "UPDATE_COMMUNICATION_PORT",
99
+ async (_event, _params, port: MessagePort) => {
100
+ await this._setCommunicationPort(port);
101
+ return this._getRegistrationInfo();
102
+ },
103
+ ),
104
+ );
105
+
106
+ this._serviceWorker = await getServiceWorkerController();
107
+ await awaitServiceWorkerActivation(this._serviceWorker);
108
+ await this._updateCommunicationChannel();
109
+ })();
110
+ }
111
+ return this._registrationPromise;
112
+ }
113
+
114
+ async stop(): Promise<void> {
115
+ this._cleanupRegistrations?.();
116
+ const registrations = await navigator.serviceWorker.getRegistrations();
117
+ for (const registration of registrations) {
118
+ try {
119
+ await registration.unregister();
120
+ } catch (error) {
121
+ console.log("Service Worker registration failed: ", error);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ function getServiceWorkerController(): Promise<ServiceWorker> {
128
+ return new Promise((resolve) => {
129
+ const container = navigator.serviceWorker;
130
+ if (container.controller) {
131
+ resolve(container.controller);
132
+ return;
133
+ }
134
+ const onChange = () => {
135
+ if (!container.controller) return;
136
+ resolve(container.controller);
137
+ container.removeEventListener("controllerchange", onChange);
138
+ };
139
+ container.addEventListener("controllerchange", onChange);
140
+ });
141
+ }
142
+
143
+ function awaitServiceWorkerActivation(worker: ServiceWorker): Promise<void> {
144
+ return new Promise((resolve) => {
145
+ if (worker.state === "activated") {
146
+ resolve();
147
+ return;
148
+ }
149
+ const onStateChange = () => {
150
+ if (worker.state !== "activated") return;
151
+ worker.removeEventListener("statechange", onStateChange);
152
+ resolve();
153
+ };
154
+ worker.addEventListener("statechange", onStateChange);
155
+ });
156
+ }
157
+
158
+ export interface SwPortDispatcherOptions {
159
+ self: ServiceWorkerGlobalScope;
160
+ log?: (...args: unknown[]) => void;
161
+ }
162
+
163
+ /**
164
+ * ServiceWorker-side counterpart: maintains an index of connected clients keyed by
165
+ * clientId, persisted in IndexedDB so it can reclaim ports across SW restarts.
166
+ */
167
+ export class SwPortDispatcher {
168
+ readonly self: ServiceWorkerGlobalScope;
169
+ readonly log: (...args: unknown[]) => void;
170
+ readonly handlersIndex = new Map<string, ChannelInfo>();
171
+ /**
172
+ * Set of keys that were registered at some point on this SW (sticky —
173
+ * survives client closures + SW wake-ups via IndexedDB). Lets the fetch
174
+ * handler distinguish "site URL whose owner is gone → 404" from
175
+ * "unknown URL under scope → pass through to the network".
176
+ */
177
+ readonly claimedKeys = new Set<string>();
178
+ private _cleanup?: () => void;
179
+ private _activationPromise?: Promise<void>;
180
+
181
+ constructor({ self, log = () => {} }: SwPortDispatcherOptions) {
182
+ this.self = self;
183
+ this.log = log;
184
+ }
185
+
186
+ get scope(): string {
187
+ return `${new URL("./", this.self.location.href)}`;
188
+ }
189
+
190
+ /**
191
+ * Look up the active channel for a key. Returns `undefined` when there
192
+ * is no registration (or the registering client has gone away — stale
193
+ * entries are pruned in place).
194
+ */
195
+ async loadChannelInfo(key: string): Promise<ChannelInfo | undefined> {
196
+ await this._checkActivation();
197
+ for (const channelInfo of this.handlersIndex.values()) {
198
+ if (channelInfo.key !== key) continue;
199
+ const clientId = channelInfo.clientId;
200
+ if (!clientId) continue;
201
+ const client = await this.self.clients.get(clientId);
202
+ if (!client) {
203
+ // Owner tab closed: drop the stale entry and keep scanning for
204
+ // another client that still claims this key.
205
+ this.handlersIndex.delete(clientId);
206
+ continue;
207
+ }
208
+ return channelInfo;
209
+ }
210
+ return undefined;
211
+ }
212
+
213
+ /** Whether `key` has ever been registered on this SW. */
214
+ isClaimedKey(key: string): boolean {
215
+ return this.claimedKeys.has(key);
216
+ }
217
+
218
+ start(): void {
219
+ this._cleanup = handleChannelCalls(
220
+ this.self,
221
+ "UPDATE_COMMUNICATION_PORT",
222
+ async (event, channelInfo, port: MessagePort) => {
223
+ const client = event.source as Client | null;
224
+ const clientId = client?.id;
225
+ this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
226
+ await this._updateChannelInfo({ ...(channelInfo as ChannelInfo), port, clientId });
227
+ return { ...(channelInfo as ChannelInfo) };
228
+ },
229
+ );
230
+
231
+ this.self.addEventListener("install", (event) => {
232
+ this.log("Skip waiting on install.", event);
233
+ this.self.skipWaiting();
234
+ });
235
+
236
+ this.self.addEventListener("activate", (event) => {
237
+ this.log("Claim control over all clients.", event);
238
+ event.waitUntil(
239
+ (async () => {
240
+ await this.self.clients.claim();
241
+ await this._checkActivation();
242
+ })(),
243
+ );
244
+ });
245
+ }
246
+
247
+ async stop(): Promise<void> {
248
+ if (this._cleanup) {
249
+ this._cleanup();
250
+ this._cleanup = undefined;
251
+ }
252
+ }
253
+
254
+ private _checkActivation(): Promise<void> {
255
+ if (!this._activationPromise) {
256
+ this._activationPromise = this._activate();
257
+ }
258
+ return this._activationPromise;
259
+ }
260
+
261
+ private async _activate(): Promise<void> {
262
+ this.log("[checkActivation]");
263
+ const clientsIndex = await this._loadClientIds();
264
+ for (const [clientId, client] of clientsIndex.entries()) {
265
+ const messageChannel = new MessageChannel();
266
+ const port = messageChannel.port1;
267
+ const channelInfo = await callChannel<ChannelInfo>(
268
+ client,
269
+ "UPDATE_COMMUNICATION_PORT",
270
+ {},
271
+ messageChannel.port2,
272
+ );
273
+ await this._updateChannelInfo({ ...channelInfo, port, clientId });
274
+ }
275
+ }
276
+
277
+ private async _updateChannelInfo(channelInfo: ChannelInfo): Promise<void> {
278
+ this.log("[updateClientInfo]", channelInfo);
279
+ if (!channelInfo.clientId) return;
280
+ this.handlersIndex.set(channelInfo.clientId, channelInfo);
281
+ if (channelInfo.key && !this.claimedKeys.has(channelInfo.key)) {
282
+ this.claimedKeys.add(channelInfo.key);
283
+ await set("claimedKeys", [...this.claimedKeys].sort());
284
+ }
285
+ await this._updateClientIds([]);
286
+ }
287
+
288
+ private async _loadClientIds(): Promise<Map<string, Client>> {
289
+ const storedKeys = ((await get<string[]>("claimedKeys")) ?? []) as string[];
290
+ for (const key of storedKeys) this.claimedKeys.add(key);
291
+ const stored = ((await get<string[]>("clientIds")) ?? []) as string[];
292
+ return await this._updateClientIds(new Set(stored));
293
+ }
294
+
295
+ private async _updateClientIds(clientIds: Iterable<string>): Promise<Map<string, Client>> {
296
+ const ids = new Set(clientIds);
297
+ for (const clientId of this.handlersIndex.keys()) ids.add(clientId);
298
+ const index = new Map<string, Client>();
299
+ for (const clientId of ids) {
300
+ const client = await this.self.clients.get(clientId);
301
+ if (client) index.set(clientId, client);
302
+ else this.handlersIndex.delete(clientId);
303
+ }
304
+ await set("clientIds", [...index.keys()].sort());
305
+ return index;
306
+ }
307
+ }
@@ -0,0 +1,7 @@
1
+ /// <reference lib="webworker" />
2
+
3
+ import { startHttpDispatcher } from "./sw/http-sw-dispatcher.js";
4
+
5
+ declare const self: ServiceWorkerGlobalScope;
6
+
7
+ startHttpDispatcher({ self, log: console.log });
package/src/sw.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./sw/index.js";
package/CHANGELOG.md DELETED
@@ -1 +0,0 @@
1
- # @statewalker/webrun-http-browser
package/demo/demo-3.html DELETED
@@ -1,167 +0,0 @@
1
- <!DOCTYPE HTML>
2
- <html>
3
-
4
- <head>
5
- <meta charset="utf-8" />
6
- <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
7
- <meta name="viewport" content="width=device-width, initial-scale=1" />
8
- </head>
9
-
10
- <body>
11
- <div id="view"></div>
12
-
13
- <script type="module">
14
- import * as WebRunHttp from "https://unpkg.com/@statewalker/webrun-http-browser@0.3/dist/index.js";
15
- import { Hono } from "https://unpkg.com/hono@3.5/dist/index.js";
16
- import mime from "https://esm.sh/mime-types";
17
-
18
- Promise.resolve().then(main).catch(console.error);
19
- async function main() {
20
- // Load a Hono request router:
21
- const router = getRouter();
22
-
23
- // Create an HTTP Server with the specified name.
24
- // It uses the given Hono router to dispatch requests
25
- // between endpoints.
26
- const serviceBaseUrl = await startHttpService({
27
- serviceKey: "MY_SITE",
28
- router
29
- })
30
- const siteUrl = `${serviceBaseUrl}/`;
31
-
32
- // Now we can visualize the site in an iframe iframe:
33
- const container = document.querySelector("#view");
34
- const iframe = document.createElement("iframe");
35
- iframe.width = "100%";
36
- iframe.height = "500px";
37
- iframe.src = siteUrl;
38
- container.appendChild(iframe);
39
-
40
- // We also append a reference to this site.
41
- // It allows to open the site in a separate window:
42
- const a = document.createElement("a");
43
- a.target = "_blank";
44
- a.href = siteUrl;
45
- a.innerText = siteUrl;
46
- container.appendChild(a);
47
- }
48
-
49
- async function startHttpService({
50
- serviceKey,
51
- router
52
- }) {
53
- // Step 1: Start a ServiceWorker and open a MessageChannel used to recieve HTTP requests and send responses:
54
- const connection = await WebRunHttp.newRemoteRelayChannel();
55
-
56
- // Step 2: Define an HTTP handler
57
- const serviceBaseUrl = `${connection.baseUrl}~${serviceKey}`;
58
- const app = new Hono({
59
- getPath: (req) => req.url.substring(serviceBaseUrl.length)
60
- });
61
- // Add the router
62
- app.route("/", router);
63
- const httpHandler = (request) => app.fetch(request);
64
-
65
- // Step 3: Instantiate a "server" using the communcation channel with the ServiceWorker:
66
- const close = await WebRunHttp.initHttpService(httpHandler, {
67
- key: serviceKey, // Service prefix
68
- port: connection.port
69
- });
70
-
71
- return serviceBaseUrl;
72
- }
73
-
74
- function getRouter() {
75
- const router = new Hono();
76
-
77
- // Server-side REST API handler:
78
- router.get("/api/:url", async (c) => {
79
- const { url } = c.req.param();
80
- const r = await fetch(url);
81
- const result = await r.text();
82
- return c.text(result);
83
- // c.text(`Hello ${url}! Current timestamp: ${Date.now()}`);
84
- });
85
-
86
- // Static files
87
- router.get("/*", newStaticFilesDispatcher({
88
- "/index.css": `
89
- html, body {
90
- padding: 0;
91
- margin: 0;
92
- }
93
- h1 {
94
- color : navy;
95
- }
96
- `,
97
-
98
-
99
- "/index.js": `
100
- document.addEventListener("DOMContentLoaded", () => {
101
- const input = document.querySelector("#url");
102
- input.addEventListener("input", (ev) => {
103
- ev.preventDefault();
104
- ev.stopPropagation();
105
- updateUrl();
106
- })
107
- updateUrl();
108
- async function updateUrl() {
109
- const url = input.value;
110
- const res = await fetch("./api/" + encodeURIComponent(url));
111
- const text = await res.text();
112
- const view = document.querySelector("#messages");
113
- view.innerText = text;
114
- }
115
- })
116
- `,
117
- "/index.html": `<!DOCTYPE HTML>
118
- <html>
119
- <head>
120
- <meta charset="utf-8" />
121
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
122
- <meta name="viewport" content="width=device-width, initial-scale=1" />
123
- <link rel="stylesheet" href="./index.css" />
124
- </head>
125
- <body>
126
- <h1>Hello In-Browser Dynamic Site</h1>
127
- <div>
128
- Please enter remote service URL:
129
- <input type="text" id="url" value="https://dummyjson.com/products">
130
- </div>
131
- <p>Message from the server:</p>
132
- <div id="messages">
133
- </div>
134
- <script type="module" src="./index.js"><\/script>
135
- </body>
136
- </html>
137
- `
138
- }))
139
-
140
- return router;
141
- }
142
-
143
- function newStaticFilesDispatcher(files) {
144
- return function showStaticFiles(context) {
145
- let { url, path } = context.req;
146
- path = path.replace(/#.*$/, "");
147
- if (path.match(/\/$/)) {
148
- path = path + "index.html";
149
- }
150
- const result = files[path];
151
- if (!result) {
152
- return context.notFound();
153
- }
154
- const mimeType = mime.lookup(path) || "application/octet-stream";
155
- context.header("Content-Type", mimeType);
156
- context.status(201);
157
- if (typeof result === "object") {
158
- return context.json(result);
159
- } else {
160
- return context.body(result);
161
- }
162
- };
163
- }
164
- </script>
165
- </body>
166
-
167
- </html>