@statewalker/webrun-http-browser 0.5.0 → 0.6.0

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.
@@ -12,19 +12,79 @@ export interface SplitServiceUrl {
12
12
  */
13
13
  export function splitServiceUrl(url: URL | string, separator = "~"): SplitServiceUrl {
14
14
  const str = `${url}`;
15
- const idx = str.indexOf(separator);
16
- let baseUrl = "";
17
- let key = "";
18
- let path = "";
19
- if (idx >= 0) {
20
- baseUrl = str.substring(0, idx + separator.length);
21
- str.substring(idx + separator.length).replace(/^([^/]+)/, (match, $1) => {
22
- baseUrl += match;
23
- if (baseUrl.length < str.length) baseUrl += "/";
24
- key = $1;
25
- path = str.substring(baseUrl.length);
26
- return "";
27
- });
15
+ const empty = { url: str, key: "", baseUrl: "", path: "" };
16
+
17
+ // Strip query and fragment to prevent false positives on `?q=~foo` or `#~FS`.
18
+ // Extract the prefix (origin) and path from the input verbatim, preserving case
19
+ // and form (e.g., `//host` vs. `http://host`, `HTTPS://` vs. `https://`).
20
+ const hashIdx = str.indexOf("#");
21
+ const queryIdx = str.indexOf("?");
22
+ let strippedEnd = str.length;
23
+ if (hashIdx >= 0) strippedEnd = Math.min(strippedEnd, hashIdx);
24
+ if (queryIdx >= 0) strippedEnd = Math.min(strippedEnd, queryIdx);
25
+ const stripped = str.slice(0, strippedEnd);
26
+
27
+ // Identify where the path begins in the input. Three cases:
28
+ // 1. scheme://authority/path — prefix is scheme://authority
29
+ // 2. //authority/path — prefix is //authority
30
+ // 3. relative path — prefix is empty
31
+ let prefixEnd = 0;
32
+ const schemeMatch = stripped.match(/^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//);
33
+ if (schemeMatch) {
34
+ // scheme://authority/path — find the next / after the scheme
35
+ prefixEnd = schemeMatch[0].length;
36
+ const slashIdx = stripped.indexOf("/", prefixEnd);
37
+ if (slashIdx >= 0) {
38
+ prefixEnd = slashIdx;
39
+ } else {
40
+ return empty; // No path
41
+ }
42
+ } else if (stripped.startsWith("//")) {
43
+ // //authority/path — find the next / after the //
44
+ prefixEnd = 2;
45
+ const slashIdx = stripped.indexOf("/", prefixEnd);
46
+ if (slashIdx >= 0) {
47
+ prefixEnd = slashIdx;
48
+ } else {
49
+ return empty; // No path
50
+ }
28
51
  }
52
+ // else prefixEnd = 0: relative URL
53
+
54
+ const prefix = stripped.slice(0, prefixEnd);
55
+ const pathPart = stripped.slice(prefixEnd);
56
+
57
+ // Check if pathPart starts with the separator, anchored at the segment boundary.
58
+ // With an authority the path always begins with `/`, so the separator must be
59
+ // at `/<separator>`. Without one the input is either ROOT-RELATIVE
60
+ // (`/~FS/a/b` -- `location.pathname`, or any root-absolute href) or plain
61
+ // relative (`~FS/a/b`), and both are accepted: treating "no authority" as
62
+ // "relative" made every root-relative caller get nothing back.
63
+ let keyStart: number;
64
+ let rooted = false;
65
+ if (prefix === "") {
66
+ if (pathPart.startsWith(`/${separator}`)) {
67
+ rooted = true;
68
+ keyStart = separator.length + 1;
69
+ } else if (pathPart.startsWith(separator)) {
70
+ keyStart = separator.length;
71
+ } else {
72
+ return empty;
73
+ }
74
+ } else {
75
+ if (!pathPart.startsWith(`/${separator}`)) return empty;
76
+ keyStart = separator.length + 1;
77
+ }
78
+
79
+ const rest = pathPart.slice(keyStart);
80
+ const slash = rest.indexOf("/");
81
+ const key = slash < 0 ? rest : rest.slice(0, slash);
82
+ if (key === "") return empty;
83
+
84
+ // The base keeps the input's own form: `https://host/~FS/`, `//host/~FS/`,
85
+ // `/~FS/` and `~FS/` each round-trip as written.
86
+ const base = prefix === "" ? (rooted ? "/" : "") : `${prefix}/`;
87
+ const baseUrl = `${base}${separator}${key}${slash < 0 ? "" : "/"}`;
88
+ const path = slash < 0 ? "" : rest.slice(slash + 1);
29
89
  return { url: str, key, baseUrl, path };
30
90
  }
package/src/relay-sw.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  /// <reference lib="webworker" />
2
2
 
3
- import { startRelayServiceWorker } from "./relay/index-sw.js";
3
+ import { type RelayServiceWorkerOptions, startRelayServiceWorker } from "./relay/index-sw.js";
4
4
 
5
- declare const self: ServiceWorkerGlobalScope;
5
+ declare const self: ServiceWorkerGlobalScope & { RELAY_OPTIONS?: RelayServiceWorkerOptions };
6
6
 
7
- startRelayServiceWorker(self);
7
+ // OPTIONS FROM A GLOBAL, because this bundle is loaded by `importScripts` from
8
+ // a host's own worker script, which runs first and can set them. A classic
9
+ // worker cannot pass arguments any other way, and module service workers are
10
+ // not reachable through the relay page's registration (the bundle is IIFE, and
11
+ // `getRelayWindowMessageHandler` registers with no `type`).
12
+ //
13
+ // Absent, the options are `{}` -- exactly what this file passed before, so a
14
+ // host that only `importScripts`es the bundle sees no change.
15
+ startRelayServiceWorker(self, self.RELAY_OPTIONS ?? {});
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The relay ServiceWorker runtime, for a host that builds its own worker.
3
+ *
4
+ * TWO WAYS TO SHIP THE RELAY WORKER, and this is the typed one. A host that
5
+ * takes the prebuilt bundle loads `@statewalker/webrun-http-browser/relay-sw`
6
+ * through classic `importScripts` and passes its options in
7
+ * `self.RELAY_OPTIONS`, which cannot be typed from the outside because that
8
+ * bundle ships no declarations. A host that bundles its own worker imports
9
+ * this module instead and calls `startRelayServiceWorker(self, { … })`
10
+ * directly, with `RelayServiceWorkerOptions` to type the options — including
11
+ * `self.RELAY_OPTIONS`, for a host writing the tiny loader script by hand.
12
+ *
13
+ * Kept as its own entry rather than re-exported from the package root: this
14
+ * code only runs inside a ServiceWorker, and the root entry is what pages
15
+ * import.
16
+ */
17
+
18
+ export type { RelayServiceWorkerOptions } from "./relay/index-sw.js";
19
+ export { startRelayServiceWorker } from "./relay/index-sw.js";
20
+ export type { MountSpec } from "./relay/mount-table.js";
@@ -1,31 +0,0 @@
1
- import * as workerTimer from "https://unpkg.com/worker-timers@7.0.74/build/es2019/module.js?module";
2
-
3
- export function startHeartbit({
4
- timeout = 10 * 1000,
5
- url = "./ping", // 10 seconds
6
- }) {
7
- return callPeriodically(async () => {
8
- const _res = await fetch(url);
9
- }, timeout);
10
- }
11
-
12
- export async function callPeriodically(action, timeout) {
13
- let timerId,
14
- stopped = false;
15
- async function run() {
16
- try {
17
- await action();
18
- } catch (err) {
19
- console.error(err);
20
- }
21
- timerId = !stopped ? workerTimer.setTimeout(run, timeout) : 0;
22
- }
23
- run();
24
- return () => {
25
- stopped = true;
26
- if (timerId) {
27
- workerTimer.clearTimeout(timerId);
28
- timerId = 0;
29
- }
30
- };
31
- }