@swiftbrowser/native 0.14.2 → 0.16.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.
- package/README.md +10 -0
- package/dist/bridge.d.ts +14 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/worker.d.ts +17 -4
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +10 -5
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/src/bridge.ts +15 -2
- package/src/index.ts +2 -2
- package/src/worker.ts +20 -6
- package/swift/Package.swift +5 -1
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +23 -11
- package/swift/Sources/SwiftBrowserNative/Host/WebViewWorkerHost.swift +315 -0
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +41 -19
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/RemoteImage.swift +88 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +3 -10
- package/wasm/SwiftBrowserNative.wasm +2 -2
package/README.md
CHANGED
|
@@ -126,6 +126,16 @@ worker. Drawn in a page (`site`), `createWorker` returns the page's own
|
|
|
126
126
|
is the same in all three places. The iOS app's `WebAssembly` is the engine's
|
|
127
127
|
interpreter (a third-party app has no JIT), and SIMD in it needs iOS 26.2.
|
|
128
128
|
|
|
129
|
+
```ts
|
|
130
|
+
const worker = createWorker('satoru', { engine: 'webview' });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`engine: 'webview'` runs the same bundle in a hidden `WKWebView` instead,
|
|
134
|
+
whose web content process does compile JavaScript and WebAssembly with its
|
|
135
|
+
JIT: much faster for heavy WebAssembly, for a process and a web
|
|
136
|
+
view's memory. Its `fetch` still goes through the app's `URLSession`, with
|
|
137
|
+
the app's cookies; a page's own `Worker` ignores the option.
|
|
138
|
+
|
|
129
139
|
## URLs the app is opened with
|
|
130
140
|
|
|
131
141
|
```ts
|
package/dist/bridge.d.ts
CHANGED
|
@@ -27,14 +27,26 @@ export interface NativeBridge {
|
|
|
27
27
|
locale(): string;
|
|
28
28
|
/** The process's launch arguments (`-screen list` from an Xcode scheme or `simctl launch`). */
|
|
29
29
|
launchArguments?(): string[];
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Starts worker `id` from `<name>.worker.js` in `engine` (`'context'`
|
|
32
|
+
* when absent; hosts before 0.16 take no engine); returns an error
|
|
33
|
+
* message, or nothing. Its messages arrive at `__sbWorkerMessage(id, text)`,
|
|
34
|
+
* its errors at `__sbWorkerError(id, message)`.
|
|
35
|
+
*/
|
|
36
|
+
workerStart?(id: number, name: string, engine?: WorkerEngine): string | null | undefined;
|
|
32
37
|
/** Delivers an encoded message (worker.ts) to worker `id`'s `__sbWorkerReceive(text)`. */
|
|
33
38
|
workerPost?(id: number, text: string): void;
|
|
34
39
|
workerTerminate?(id: number): void;
|
|
35
40
|
/** In a worker's context: an encoded message to the app's context. */
|
|
36
41
|
workerPostMessage?(text: string): void;
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Where a native worker runs: `'context'`, a JavaScriptCore context on a
|
|
45
|
+
* thread of the app's; `'webview'`, a hidden `WKWebView`, whose web content
|
|
46
|
+
* process has WebKit's JIT (an app's own JavaScriptCore has none on iOS),
|
|
47
|
+
* for heavy WebAssembly.
|
|
48
|
+
*/
|
|
49
|
+
export type WorkerEngine = 'context' | 'webview';
|
|
38
50
|
/** The app's launch arguments, or none outside the native app. */
|
|
39
51
|
export declare function launchArguments(): string[];
|
|
40
52
|
export interface NativeFetchRequest {
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,+BAA+B;IAC/B,GAAG,IAAI,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAAC;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,YAAY,IAAI,IAAI,CAAC;IACrB,sCAAsC;IACtC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,gDAAgD;IAChD,MAAM,IAAI,MAAM,CAAC;IACjB,+FAA+F;IAC/F,eAAe,CAAC,IAAI,MAAM,EAAE,CAAC;IAM7B
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,+BAA+B;IAC/B,GAAG,IAAI,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9D,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAAC;IAC9D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACvC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,YAAY,IAAI,IAAI,CAAC;IACrB,sCAAsC;IACtC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,gDAAgD;IAChD,MAAM,IAAI,MAAM,CAAC;IACjB,+FAA+F;IAC/F,eAAe,CAAC,IAAI,MAAM,EAAE,CAAC;IAM7B;;;;;OAKG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACzF,0FAA0F;IAC1F,UAAU,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,eAAe,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,sEAAsE;IACtE,iBAAiB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjD,kEAAkE;AAClE,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAG1C;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,uBAAuB;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,CAAC,GAAE,OAAO,UAAuB,GAAG,YAAY,GAAG,SAAS,CAExF"}
|
package/dist/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAwDA,kEAAkE;AAClE,MAAM,UAAU,eAAe;IAC7B,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,MAAM,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,CAAC;AAC3C,CAAC;AAoBD,yFAAyF;AACzF,MAAM,UAAU,YAAY,CAAC,IAAuB,UAAU;IAC5D,OAAQ,CAA8C,CAAC,UAAU,CAAC;AACpE,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
export { run, type RunOptions, type Running } from './run';
|
|
14
14
|
export { Native, type NativeProps } from './native';
|
|
15
15
|
export { installRuntime, type InstallOptions } from './runtime';
|
|
16
|
-
export { nativeBridge, launchArguments, type NativeBridge, type NativeFetchRequest, type NativeFetchResult } from './bridge';
|
|
16
|
+
export { nativeBridge, launchArguments, type NativeBridge, type NativeFetchRequest, type NativeFetchResult, type WorkerEngine } from './bridge';
|
|
17
17
|
export { SimpleHeaders, SimpleRequest, SimpleResponse, SimpleAbortController, SimpleAbortSignal, createFetch } from './fetch';
|
|
18
18
|
export { SimpleURL, SimpleURLSearchParams } from './url';
|
|
19
19
|
export { SimpleTextEncoder, SimpleTextDecoder, base64Encode, base64Decode, utf8Encode, utf8Decode } from './encoding';
|
|
20
|
-
export { createWorker, NativeWorker, encodeMessage, decodeMessage, workerName, type WorkerLike, type WorkerMessageEvent, type WorkerErrorEvent } from './worker';
|
|
20
|
+
export { createWorker, NativeWorker, encodeMessage, decodeMessage, workerName, type NativeWorkerOptions, type WorkerLike, type WorkerMessageEvent, type WorkerErrorEvent } from './worker';
|
|
21
21
|
export { onOpenURL } from './openURL';
|
|
22
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,GAAG,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,GAAG,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAChJ,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC9H,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC3L,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,GAAG,EAAiC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAoB,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,cAAc,EAAuB,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,GAAG,EAAiC,MAAM,OAAO,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAoB,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,cAAc,EAAuB,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAyF,MAAM,UAAU,CAAC;AAChJ,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC9H,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAA6F,MAAM,UAAU,CAAC;AAC3L,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/worker.d.ts
CHANGED
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
* worker.onmessage = (event) => console.log(event.data);
|
|
17
17
|
* worker.postMessage({ draw: 'something' });
|
|
18
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* `createWorker(name, { engine: 'webview' })` runs the same bundle in a
|
|
21
|
+
* hidden `WKWebView` instead (WebViewWorkerHost.swift): WebKit's web
|
|
22
|
+
* content process compiles with its JIT, which an app's own JavaScriptCore
|
|
23
|
+
* may not on iOS, so heavy WebAssembly runs much faster there.
|
|
19
24
|
*/
|
|
20
|
-
import { type NativeBridge } from './bridge';
|
|
25
|
+
import { type NativeBridge, type WorkerEngine } from './bridge';
|
|
21
26
|
/** What `Worker` and a worker's `self` have in common, as an app uses them. */
|
|
22
27
|
export interface WorkerLike {
|
|
23
28
|
postMessage(data: unknown, transfer?: unknown[]): void;
|
|
@@ -45,6 +50,14 @@ export interface WorkerErrorEvent {
|
|
|
45
50
|
export declare function encodeMessage(data: unknown): string;
|
|
46
51
|
/** The value `encodeMessage` encoded. */
|
|
47
52
|
export declare function decodeMessage(text: string): unknown;
|
|
53
|
+
/** How a worker is started. */
|
|
54
|
+
export interface NativeWorkerOptions {
|
|
55
|
+
/** Where it runs in the native app (bridge.ts): `'context'` (the default) or `'webview'`. A page's own `Worker` ignores it. */
|
|
56
|
+
engine?: WorkerEngine;
|
|
57
|
+
/** A page's `Worker` options, accepted and ignored: a bundle is one script. */
|
|
58
|
+
type?: 'classic' | 'module';
|
|
59
|
+
name?: string;
|
|
60
|
+
}
|
|
48
61
|
type Listener<E> = (event: E) => void;
|
|
49
62
|
/** `Worker` over the bridge, in the app's context. */
|
|
50
63
|
export declare class NativeWorker implements WorkerLike {
|
|
@@ -56,7 +69,7 @@ export declare class NativeWorker implements WorkerLike {
|
|
|
56
69
|
/** `name` is the bundle's: `<name>.worker.js` next to app.js. A URL's last path segment serves as the name (`render.worker.js` → `render`). */
|
|
57
70
|
constructor(name: string | {
|
|
58
71
|
href: string;
|
|
59
|
-
} | URL, native?: NativeBridge | undefined);
|
|
72
|
+
} | URL, options?: NativeWorkerOptions, native?: NativeBridge | undefined);
|
|
60
73
|
postMessage(data: unknown): void;
|
|
61
74
|
terminate(): void;
|
|
62
75
|
addEventListener(type: 'message', listener: Listener<WorkerMessageEvent>): void;
|
|
@@ -74,9 +87,9 @@ export declare function workerName(name: string | {
|
|
|
74
87
|
/**
|
|
75
88
|
* A worker from the bundle `<name>.worker.js`: the page's own `Worker` when
|
|
76
89
|
* the app is drawn in a page (the bundle published with the site), else the
|
|
77
|
-
* host's over the bridge.
|
|
90
|
+
* host's over the bridge, in `options.engine`.
|
|
78
91
|
*/
|
|
79
|
-
export declare function createWorker(name: string): WorkerLike;
|
|
92
|
+
export declare function createWorker(name: string, options?: NativeWorkerOptions): WorkerLike;
|
|
80
93
|
/**
|
|
81
94
|
* A worker's own globals over its bridge: `self`, `postMessage`, `onmessage`
|
|
82
95
|
* and `onerror` as properties the script assigns, `addEventListener`,
|
package/dist/worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
|
package/dist/worker.js
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* worker.onmessage = (event) => console.log(event.data);
|
|
17
17
|
* worker.postMessage({ draw: 'something' });
|
|
18
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* `createWorker(name, { engine: 'webview' })` runs the same bundle in a
|
|
21
|
+
* hidden `WKWebView` instead (WebViewWorkerHost.swift): WebKit's web
|
|
22
|
+
* content process compiles with its JIT, which an app's own JavaScriptCore
|
|
23
|
+
* may not on iOS, so heavy WebAssembly runs much faster there.
|
|
19
24
|
*/
|
|
20
25
|
import { nativeBridge } from './bridge';
|
|
21
26
|
import { base64Decode, base64Encode } from './encoding';
|
|
@@ -91,14 +96,14 @@ export class NativeWorker {
|
|
|
91
96
|
onmessage = null;
|
|
92
97
|
onerror = null;
|
|
93
98
|
/** `name` is the bundle's: `<name>.worker.js` next to app.js. A URL's last path segment serves as the name (`render.worker.js` → `render`). */
|
|
94
|
-
constructor(name, native = nativeBridge()) {
|
|
99
|
+
constructor(name, options = {}, native = nativeBridge()) {
|
|
95
100
|
if (!native?.workerStart || !native.workerPost || !native.workerTerminate) {
|
|
96
101
|
throw new Error('Worker: this host has no workers (SwiftBrowser Native 0.13 or later)');
|
|
97
102
|
}
|
|
98
103
|
this.#native = native;
|
|
99
104
|
this.#id = NativeWorker.nextId++;
|
|
100
105
|
NativeWorker.live.set(this.#id, this);
|
|
101
|
-
const failure = native.workerStart(this.#id, workerName(name));
|
|
106
|
+
const failure = native.workerStart(this.#id, workerName(name), options.engine);
|
|
102
107
|
if (failure) {
|
|
103
108
|
NativeWorker.live.delete(this.#id);
|
|
104
109
|
throw new Error(failure);
|
|
@@ -158,15 +163,15 @@ export function workerName(name) {
|
|
|
158
163
|
/**
|
|
159
164
|
* A worker from the bundle `<name>.worker.js`: the page's own `Worker` when
|
|
160
165
|
* the app is drawn in a page (the bundle published with the site), else the
|
|
161
|
-
* host's over the bridge.
|
|
166
|
+
* host's over the bridge, in `options.engine`.
|
|
162
167
|
*/
|
|
163
|
-
export function createWorker(name) {
|
|
168
|
+
export function createWorker(name, options = {}) {
|
|
164
169
|
const base = typeof __SB_WORKER_BASE__ === 'string' ? __SB_WORKER_BASE__ : undefined;
|
|
165
170
|
const PageWorker = globalThis.Worker;
|
|
166
171
|
if (typeof PageWorker === 'function' && base !== undefined)
|
|
167
172
|
return new PageWorker(`${base}${workerName(name)}.worker.js`);
|
|
168
173
|
if (nativeBridge()?.workerStart)
|
|
169
|
-
return new NativeWorker(name);
|
|
174
|
+
return new NativeWorker(name, options);
|
|
170
175
|
if (typeof PageWorker === 'function')
|
|
171
176
|
return new PageWorker(`${workerName(name)}.worker.js`);
|
|
172
177
|
throw new Error(`createWorker: no host for "${name}" (not in a SwiftBrowser Native app or a page)`);
|
package/dist/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
|
|
6
6
|
"repository": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@babel/core": "^7.29.7",
|
|
62
|
-
"@swiftbrowser/react": "0.
|
|
63
|
-
"@swiftbrowser/web": "0.
|
|
62
|
+
"@swiftbrowser/react": "0.16.0",
|
|
63
|
+
"@swiftbrowser/web": "0.16.0",
|
|
64
64
|
"esbuild": "^0.28.2",
|
|
65
65
|
"react-refresh": "^0.17.0"
|
|
66
66
|
},
|
package/src/bridge.ts
CHANGED
|
@@ -32,8 +32,13 @@ export interface NativeBridge {
|
|
|
32
32
|
// `swiftbrowser-native build --worker <name>=<entry>` wrote next to app.js
|
|
33
33
|
// (`<name>.worker.js`). The app's context has the first three; a worker's
|
|
34
34
|
// context has the last and no `applyOps`.
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Starts worker `id` from `<name>.worker.js` in `engine` (`'context'`
|
|
37
|
+
* when absent; hosts before 0.16 take no engine); returns an error
|
|
38
|
+
* message, or nothing. Its messages arrive at `__sbWorkerMessage(id, text)`,
|
|
39
|
+
* its errors at `__sbWorkerError(id, message)`.
|
|
40
|
+
*/
|
|
41
|
+
workerStart?(id: number, name: string, engine?: WorkerEngine): string | null | undefined;
|
|
37
42
|
/** Delivers an encoded message (worker.ts) to worker `id`'s `__sbWorkerReceive(text)`. */
|
|
38
43
|
workerPost?(id: number, text: string): void;
|
|
39
44
|
workerTerminate?(id: number): void;
|
|
@@ -41,6 +46,14 @@ export interface NativeBridge {
|
|
|
41
46
|
workerPostMessage?(text: string): void;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Where a native worker runs: `'context'`, a JavaScriptCore context on a
|
|
51
|
+
* thread of the app's; `'webview'`, a hidden `WKWebView`, whose web content
|
|
52
|
+
* process has WebKit's JIT (an app's own JavaScriptCore has none on iOS),
|
|
53
|
+
* for heavy WebAssembly.
|
|
54
|
+
*/
|
|
55
|
+
export type WorkerEngine = 'context' | 'webview';
|
|
56
|
+
|
|
44
57
|
/** The app's launch arguments, or none outside the native app. */
|
|
45
58
|
export function launchArguments(): string[] {
|
|
46
59
|
const native = nativeBridge();
|
package/src/index.ts
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
export { run, type RunOptions, type Running } from './run';
|
|
15
15
|
export { Native, type NativeProps } from './native';
|
|
16
16
|
export { installRuntime, type InstallOptions } from './runtime';
|
|
17
|
-
export { nativeBridge, launchArguments, type NativeBridge, type NativeFetchRequest, type NativeFetchResult } from './bridge';
|
|
17
|
+
export { nativeBridge, launchArguments, type NativeBridge, type NativeFetchRequest, type NativeFetchResult, type WorkerEngine } from './bridge';
|
|
18
18
|
export { SimpleHeaders, SimpleRequest, SimpleResponse, SimpleAbortController, SimpleAbortSignal, createFetch } from './fetch';
|
|
19
19
|
export { SimpleURL, SimpleURLSearchParams } from './url';
|
|
20
20
|
export { SimpleTextEncoder, SimpleTextDecoder, base64Encode, base64Decode, utf8Encode, utf8Decode } from './encoding';
|
|
21
|
-
export { createWorker, NativeWorker, encodeMessage, decodeMessage, workerName, type WorkerLike, type WorkerMessageEvent, type WorkerErrorEvent } from './worker';
|
|
21
|
+
export { createWorker, NativeWorker, encodeMessage, decodeMessage, workerName, type NativeWorkerOptions, type WorkerLike, type WorkerMessageEvent, type WorkerErrorEvent } from './worker';
|
|
22
22
|
export { onOpenURL } from './openURL';
|
package/src/worker.ts
CHANGED
|
@@ -16,9 +16,14 @@
|
|
|
16
16
|
* worker.onmessage = (event) => console.log(event.data);
|
|
17
17
|
* worker.postMessage({ draw: 'something' });
|
|
18
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* `createWorker(name, { engine: 'webview' })` runs the same bundle in a
|
|
21
|
+
* hidden `WKWebView` instead (WebViewWorkerHost.swift): WebKit's web
|
|
22
|
+
* content process compiles with its JIT, which an app's own JavaScriptCore
|
|
23
|
+
* may not on iOS, so heavy WebAssembly runs much faster there.
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
|
-
import { nativeBridge, type NativeBridge } from './bridge';
|
|
26
|
+
import { nativeBridge, type NativeBridge, type WorkerEngine } from './bridge';
|
|
22
27
|
import { base64Decode, base64Encode } from './encoding';
|
|
23
28
|
|
|
24
29
|
/** The site's published path for worker bundles (`/native/bundle/App/`), defined by `swiftbrowser-native site`; undefined in an Xcode bundle. */
|
|
@@ -107,6 +112,15 @@ export function decodeMessage(text: string): unknown {
|
|
|
107
112
|
|
|
108
113
|
// MARK: The app's side
|
|
109
114
|
|
|
115
|
+
/** How a worker is started. */
|
|
116
|
+
export interface NativeWorkerOptions {
|
|
117
|
+
/** Where it runs in the native app (bridge.ts): `'context'` (the default) or `'webview'`. A page's own `Worker` ignores it. */
|
|
118
|
+
engine?: WorkerEngine;
|
|
119
|
+
/** A page's `Worker` options, accepted and ignored: a bundle is one script. */
|
|
120
|
+
type?: 'classic' | 'module';
|
|
121
|
+
name?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
type Listener<E> = (event: E) => void;
|
|
111
125
|
|
|
112
126
|
/** `Worker` over the bridge, in the app's context. */
|
|
@@ -122,14 +136,14 @@ export class NativeWorker implements WorkerLike {
|
|
|
122
136
|
onerror: Listener<WorkerErrorEvent> | null = null;
|
|
123
137
|
|
|
124
138
|
/** `name` is the bundle's: `<name>.worker.js` next to app.js. A URL's last path segment serves as the name (`render.worker.js` → `render`). */
|
|
125
|
-
constructor(name: string | { href: string } | URL, native: NativeBridge | undefined = nativeBridge()) {
|
|
139
|
+
constructor(name: string | { href: string } | URL, options: NativeWorkerOptions = {}, native: NativeBridge | undefined = nativeBridge()) {
|
|
126
140
|
if (!native?.workerStart || !native.workerPost || !native.workerTerminate) {
|
|
127
141
|
throw new Error('Worker: this host has no workers (SwiftBrowser Native 0.13 or later)');
|
|
128
142
|
}
|
|
129
143
|
this.#native = native;
|
|
130
144
|
this.#id = NativeWorker.nextId++;
|
|
131
145
|
NativeWorker.live.set(this.#id, this);
|
|
132
|
-
const failure = native.workerStart(this.#id, workerName(name));
|
|
146
|
+
const failure = native.workerStart(this.#id, workerName(name), options.engine);
|
|
133
147
|
if (failure) {
|
|
134
148
|
NativeWorker.live.delete(this.#id);
|
|
135
149
|
throw new Error(failure);
|
|
@@ -189,13 +203,13 @@ export function workerName(name: string | { href: string } | URL): string {
|
|
|
189
203
|
/**
|
|
190
204
|
* A worker from the bundle `<name>.worker.js`: the page's own `Worker` when
|
|
191
205
|
* the app is drawn in a page (the bundle published with the site), else the
|
|
192
|
-
* host's over the bridge.
|
|
206
|
+
* host's over the bridge, in `options.engine`.
|
|
193
207
|
*/
|
|
194
|
-
export function createWorker(name: string): WorkerLike {
|
|
208
|
+
export function createWorker(name: string, options: NativeWorkerOptions = {}): WorkerLike {
|
|
195
209
|
const base = typeof __SB_WORKER_BASE__ === 'string' ? __SB_WORKER_BASE__ : undefined;
|
|
196
210
|
const PageWorker = (globalThis as { Worker?: new (url: string) => WorkerLike }).Worker;
|
|
197
211
|
if (typeof PageWorker === 'function' && base !== undefined) return new PageWorker(`${base}${workerName(name)}.worker.js`);
|
|
198
|
-
if (nativeBridge()?.workerStart) return new NativeWorker(name);
|
|
212
|
+
if (nativeBridge()?.workerStart) return new NativeWorker(name, options);
|
|
199
213
|
if (typeof PageWorker === 'function') return new PageWorker(`${workerName(name)}.worker.js`);
|
|
200
214
|
throw new Error(`createWorker: no host for "${name}" (not in a SwiftBrowser Native app or a page)`);
|
|
201
215
|
}
|
package/swift/Package.swift
CHANGED
|
@@ -23,10 +23,14 @@ let package = Package(
|
|
|
23
23
|
.library(name: "SwiftBrowserProtocol", targets: ["SwiftBrowserProtocol"]),
|
|
24
24
|
.library(name: "SwiftBrowserTree", targets: ["SwiftBrowserTree"]),
|
|
25
25
|
],
|
|
26
|
+
dependencies: [
|
|
27
|
+
// SVG images, which the system's image decoding does not read (Views/Kinds/RemoteImage.swift).
|
|
28
|
+
.package(url: "https://github.com/swhitty/SwiftDraw.git", .upToNextMinor(from: "0.29.0")),
|
|
29
|
+
],
|
|
26
30
|
targets: [
|
|
27
31
|
.target(name: "SwiftBrowserProtocol"),
|
|
28
32
|
.target(name: "SwiftBrowserTree", dependencies: ["SwiftBrowserProtocol"]),
|
|
29
|
-
.target(name: "SwiftBrowserNative", dependencies: ["SwiftBrowserProtocol", "SwiftBrowserTree"]),
|
|
33
|
+
.target(name: "SwiftBrowserNative", dependencies: ["SwiftBrowserProtocol", "SwiftBrowserTree", .product(name: "SwiftDraw", package: "SwiftDraw", condition: .when(platforms: [.iOS, .macOS]))]),
|
|
30
34
|
// Apple platforms only (JavaScriptCore): the workers; empty elsewhere.
|
|
31
35
|
]
|
|
32
36
|
)
|
|
@@ -27,7 +27,7 @@ import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
|
|
|
27
27
|
/// | `openURL(url)` | the system opener |
|
|
28
28
|
/// | `locale()` | the viewer's locale identifier |
|
|
29
29
|
/// | `launchArguments()` | the process's arguments (`-screen list` from a scheme or `simctl launch`) |
|
|
30
|
-
/// | `workerStart(id, name)`, `workerPost(id, text)`, `workerTerminate(id)` | workers (WorkerHost.swift
|
|
30
|
+
/// | `workerStart(id, name, engine)`, `workerPost(id, text)`, `workerTerminate(id)` | workers (JavaScriptCore only): a `JSContext` on a thread (`"context"`, WorkerHost.swift) or a hidden web view with WebKit's JIT (`"webview"`, WebViewWorkerHost.swift); their messages come to `__sbWorkerMessage(id, text)`, their errors to `__sbWorkerError(id, message)` |
|
|
31
31
|
///
|
|
32
32
|
/// Everything JavaScript-facing runs on the main thread; work that is not
|
|
33
33
|
/// (a fetch, a worker) hops back before calling in.
|
|
@@ -37,7 +37,7 @@ final class HostBridge {
|
|
|
37
37
|
private let tree: OpTree
|
|
38
38
|
private var timers: [Int: Timer] = [:]
|
|
39
39
|
#if canImport(JavaScriptCore)
|
|
40
|
-
private var workers: [Int:
|
|
40
|
+
private var workers: [Int: any WorkerEngine] = [:]
|
|
41
41
|
#endif
|
|
42
42
|
private let logger = Logger(subsystem: "swiftbrowser", category: "js")
|
|
43
43
|
private let session = URLSession(configuration: .default)
|
|
@@ -180,16 +180,28 @@ final class HostBridge {
|
|
|
180
180
|
guard let self, let id = arguments[safe: 0]?.int, let name = arguments[safe: 1]?.string else {
|
|
181
181
|
return .string("workerStart: expected an id and a name")
|
|
182
182
|
}
|
|
183
|
+
let onMessage: @Sendable (String) -> Void = { [weak self] text in
|
|
184
|
+
DispatchQueue.main.async {
|
|
185
|
+
MainActor.assumeIsolated { self?.callIn("__sbWorkerMessage", [.number(Double(id)), .string(text)]) }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
let onError: @Sendable (String) -> Void = { [weak self] message in
|
|
189
|
+
DispatchQueue.main.async {
|
|
190
|
+
MainActor.assumeIsolated { self?.callIn("__sbWorkerError", [.number(Double(id)), .string(message)]) }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
183
193
|
do {
|
|
184
|
-
let worker
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
let worker: any WorkerEngine
|
|
195
|
+
switch arguments[safe: 2]?.string ?? "context" {
|
|
196
|
+
case "context":
|
|
197
|
+
worker = try WorkerHost(name: name, onMessage: onMessage, onError: onError)
|
|
198
|
+
#if canImport(WebKit)
|
|
199
|
+
case "webview":
|
|
200
|
+
worker = try WebViewWorkerHost(name: name, onMessage: onMessage, onError: onError)
|
|
201
|
+
#endif
|
|
202
|
+
case let engine:
|
|
203
|
+
return .string("workerStart: no \(engine) engine here (\"context\" or \"webview\")")
|
|
204
|
+
}
|
|
193
205
|
self.workers[id]?.terminate()
|
|
194
206
|
self.workers[id] = worker
|
|
195
207
|
worker.start()
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && canImport(JavaScriptCore) && canImport(WebKit)
|
|
2
|
+
import Foundation
|
|
3
|
+
import os
|
|
4
|
+
import SwiftBrowserProtocol
|
|
5
|
+
import WebKit
|
|
6
|
+
#if canImport(UIKit)
|
|
7
|
+
import UIKit
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
/// A worker in a hidden `WKWebView` (`createWorker(name, { engine: 'webview' })`):
|
|
11
|
+
/// the same `<name>.worker.js` and the same messages as WorkerHost.swift's
|
|
12
|
+
/// `JSContext`, run by WebKit's web content process, which compiles
|
|
13
|
+
/// JavaScript and WebAssembly with its JIT. An app's own JavaScriptCore has
|
|
14
|
+
/// no JIT on iOS (only Apple's processes are allowed one), so heavy
|
|
15
|
+
/// WebAssembly runs much faster here; the cost is a process and a
|
|
16
|
+
/// web view's memory.
|
|
17
|
+
///
|
|
18
|
+
/// The page's `__sbNative` (`bootstrap`, below) is the worker bridge
|
|
19
|
+
/// written in JavaScript over the page's own timers and `crypto`. Its
|
|
20
|
+
/// `workerPostMessage`, `log` and `fetch` come here through the `sbWorker`
|
|
21
|
+
/// message handler, as JSON text. The runtime keeps the page's globals and
|
|
22
|
+
/// installs its own `fetch` family over the bridge, so requests go through
|
|
23
|
+
/// `URLSession`, with the app's cookies, as a `JSContext` worker's do.
|
|
24
|
+
/// Messages in, and fetch answers, go in with `callAsyncJavaScript`.
|
|
25
|
+
///
|
|
26
|
+
/// On iOS the web view sits behind the key window's content, 1×1 and clear:
|
|
27
|
+
/// WebKit suspends the process of a web view outside a window.
|
|
28
|
+
@MainActor
|
|
29
|
+
final class WebViewWorkerHost: NSObject, WorkerEngine, WKNavigationDelegate {
|
|
30
|
+
let name: String
|
|
31
|
+
private let script: String
|
|
32
|
+
private let onMessage: @Sendable (String) -> Void
|
|
33
|
+
private let onError: @Sendable (String) -> Void
|
|
34
|
+
private let logger = Logger(subsystem: "swiftbrowser", category: "worker")
|
|
35
|
+
private let session = URLSession(configuration: .default)
|
|
36
|
+
private var webView: WKWebView?
|
|
37
|
+
/// Whether the script ran in the current page; messages wait in `pending` until it has.
|
|
38
|
+
private var ready = false
|
|
39
|
+
private var pending: [String] = []
|
|
40
|
+
private var terminated = false
|
|
41
|
+
#if os(iOS)
|
|
42
|
+
private var windowObserver: NSObjectProtocol?
|
|
43
|
+
#endif
|
|
44
|
+
|
|
45
|
+
/// The script is `<name>.worker.js` in `bundle`, as WorkerHost's; throws when it is not there.
|
|
46
|
+
init(name: String, bundle: Bundle = .main, onMessage: @escaping @Sendable (String) -> Void, onError: @escaping @Sendable (String) -> Void) throws {
|
|
47
|
+
guard let url = bundle.url(forResource: "\(name).worker", withExtension: "js") else {
|
|
48
|
+
throw JSError(message: "\(name).worker.js is not in the app bundle (build it with `swiftbrowser-native build --worker \(name)=<entry>`)")
|
|
49
|
+
}
|
|
50
|
+
self.name = name
|
|
51
|
+
script = try String(contentsOf: url, encoding: .utf8) + "\n//# sourceURL=\(name).worker.js"
|
|
52
|
+
self.onMessage = onMessage
|
|
53
|
+
self.onError = onError
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func start() {
|
|
57
|
+
let configuration = WKWebViewConfiguration()
|
|
58
|
+
configuration.websiteDataStore = .nonPersistent()
|
|
59
|
+
let content = configuration.userContentController
|
|
60
|
+
content.addUserScript(WKUserScript(source: Self.bootstrap, injectionTime: .atDocumentStart, forMainFrameOnly: true))
|
|
61
|
+
content.add(MessageHandler(host: self), name: "sbWorker")
|
|
62
|
+
let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: configuration)
|
|
63
|
+
webView.navigationDelegate = self
|
|
64
|
+
#if DEBUG
|
|
65
|
+
webView.isInspectable = true // Safari's Develop menu
|
|
66
|
+
#endif
|
|
67
|
+
#if os(iOS)
|
|
68
|
+
webView.isOpaque = false
|
|
69
|
+
webView.backgroundColor = .clear
|
|
70
|
+
webView.scrollView.backgroundColor = .clear
|
|
71
|
+
webView.isUserInteractionEnabled = false
|
|
72
|
+
webView.accessibilityElementsHidden = true
|
|
73
|
+
#endif
|
|
74
|
+
self.webView = webView
|
|
75
|
+
attach()
|
|
76
|
+
load()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func post(_ text: String) {
|
|
80
|
+
guard !terminated else { return }
|
|
81
|
+
guard ready else {
|
|
82
|
+
pending.append(text)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
evaluate("__sbWorkerReceive(text)", ["text": text])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func terminate() {
|
|
89
|
+
terminated = true
|
|
90
|
+
#if os(iOS)
|
|
91
|
+
if let windowObserver { NotificationCenter.default.removeObserver(windowObserver) }
|
|
92
|
+
windowObserver = nil
|
|
93
|
+
#endif
|
|
94
|
+
guard let webView else { return }
|
|
95
|
+
webView.configuration.userContentController.removeScriptMessageHandler(forName: "sbWorker")
|
|
96
|
+
webView.navigationDelegate = nil
|
|
97
|
+
webView.stopLoading()
|
|
98
|
+
webView.removeFromSuperview()
|
|
99
|
+
self.webView = nil
|
|
100
|
+
ready = false
|
|
101
|
+
pending = []
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MARK: The page
|
|
105
|
+
|
|
106
|
+
/// An empty page on an origin of its own (`localStorage` and the like
|
|
107
|
+
/// need one); nothing is fetched from it.
|
|
108
|
+
private func load() {
|
|
109
|
+
ready = false
|
|
110
|
+
let origin = URL(string: "https://worker.swiftbrowser.invalid/\(name)/")
|
|
111
|
+
webView?.loadHTMLString("<!doctype html><meta charset=\"utf-8\"><title>\(name)</title>", baseURL: origin)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
115
|
+
guard webView === self.webView, !ready else { return }
|
|
116
|
+
evaluate("__sbWorkerRun(script)", ["script": script])
|
|
117
|
+
ready = true
|
|
118
|
+
// In order after the script: WebKit runs the calls as they were sent.
|
|
119
|
+
let queued = pending
|
|
120
|
+
pending = []
|
|
121
|
+
for text in queued { post(text) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) {
|
|
125
|
+
fail("\(name): the worker's page did not load: \(error.localizedDescription)")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// The process ended (memory pressure, a crash): the worker starts over
|
|
129
|
+
/// in a new one, losing what was in flight.
|
|
130
|
+
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
131
|
+
guard webView === self.webView else { return }
|
|
132
|
+
fail("\(name): the worker's web content process ended; restarting it")
|
|
133
|
+
load()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Runs `body` as an async function in the page, its arguments as locals.
|
|
137
|
+
private func evaluate(_ body: String, _ arguments: [String: Any]) {
|
|
138
|
+
webView?.callAsyncJavaScript(body, arguments: arguments, in: nil, in: .page) { [weak self] result in
|
|
139
|
+
guard case .failure(let error) = result else { return }
|
|
140
|
+
let message = (error as NSError).userInfo["WKJavaScriptExceptionMessage"] as? String ?? error.localizedDescription
|
|
141
|
+
MainActor.assumeIsolated { self?.fail(message) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func fail(_ message: String) {
|
|
146
|
+
logger.error("\(self.name): \(message, privacy: .public)")
|
|
147
|
+
onError(message)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// MARK: From the page
|
|
151
|
+
|
|
152
|
+
fileprivate func receive(_ text: String) {
|
|
153
|
+
guard let message = try? JSONValue.parse(text) else { return }
|
|
154
|
+
switch message["kind"].string {
|
|
155
|
+
case "message":
|
|
156
|
+
if let text = message["text"].string { onMessage(text) }
|
|
157
|
+
case "error":
|
|
158
|
+
fail(message["message"].string ?? "unknown error")
|
|
159
|
+
case "log":
|
|
160
|
+
let level = message["level"].string ?? "log"
|
|
161
|
+
let text = message["message"].string ?? ""
|
|
162
|
+
switch level {
|
|
163
|
+
case "error": logger.error("\(self.name): \(text, privacy: .public)")
|
|
164
|
+
case "warn": logger.warning("\(self.name): \(text, privacy: .public)")
|
|
165
|
+
case "debug": logger.debug("\(self.name): \(text, privacy: .public)")
|
|
166
|
+
default: logger.info("\(self.name): \(text, privacy: .public)")
|
|
167
|
+
}
|
|
168
|
+
#if DEBUG
|
|
169
|
+
print("[\(name):\(level)] \(text)")
|
|
170
|
+
#endif
|
|
171
|
+
case "fetch":
|
|
172
|
+
guard let id = message["id"].int else { return }
|
|
173
|
+
WorkerHost.perform(message["request"], session: session) { [weak self] result in
|
|
174
|
+
DispatchQueue.main.async {
|
|
175
|
+
MainActor.assumeIsolated {
|
|
176
|
+
self?.evaluate("__sbFetchResult(id, JSON.parse(result))", ["id": id, "result": result.json])
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
default:
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// MARK: The window
|
|
186
|
+
|
|
187
|
+
#if os(iOS)
|
|
188
|
+
/// Puts the web view behind the key window's content, or waits for a window.
|
|
189
|
+
private func attach() {
|
|
190
|
+
guard let webView, webView.window == nil else { return }
|
|
191
|
+
let windows = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.flatMap(\.windows)
|
|
192
|
+
guard let window = windows.first(where: \.isKeyWindow) ?? windows.first else {
|
|
193
|
+
if windowObserver == nil {
|
|
194
|
+
windowObserver = NotificationCenter.default.addObserver(forName: UIWindow.didBecomeKeyNotification, object: nil, queue: .main) { [weak self] _ in
|
|
195
|
+
MainActor.assumeIsolated { self?.attach() }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
window.insertSubview(webView, at: 0)
|
|
201
|
+
if let windowObserver { NotificationCenter.default.removeObserver(windowObserver) }
|
|
202
|
+
windowObserver = nil
|
|
203
|
+
}
|
|
204
|
+
#else
|
|
205
|
+
/// A Mac's web view runs outside a window.
|
|
206
|
+
private func attach() {}
|
|
207
|
+
#endif
|
|
208
|
+
|
|
209
|
+
// MARK: The bridge in the page
|
|
210
|
+
|
|
211
|
+
/// The worker bridge (packages/native/src/bridge.ts) over the page, run
|
|
212
|
+
/// before anything else in it.
|
|
213
|
+
static var bootstrap: String {
|
|
214
|
+
let locale = JSONValue.string(Locale.current.identifier).json
|
|
215
|
+
let arguments = JSONValue.array(ProcessInfo.processInfo.arguments.map(JSONValue.string)).json
|
|
216
|
+
return """
|
|
217
|
+
(() => {
|
|
218
|
+
'use strict';
|
|
219
|
+
const handler = window.webkit.messageHandlers.sbWorker;
|
|
220
|
+
const send = (message) => handler.postMessage(JSON.stringify(message));
|
|
221
|
+
const describe = (error) => {
|
|
222
|
+
if (!(error instanceof Error)) return String(error);
|
|
223
|
+
return error.stack ? `${error}\\n${error.stack}` : String(error);
|
|
224
|
+
};
|
|
225
|
+
const fail = (error) => send({ kind: 'error', message: describe(error) });
|
|
226
|
+
addEventListener('error', (event) => fail(event.error ?? event.message));
|
|
227
|
+
addEventListener('unhandledrejection', (event) => fail(event.reason));
|
|
228
|
+
|
|
229
|
+
// The runtime installs its fetch over the bridge in place of these.
|
|
230
|
+
for (const name of ['fetch', 'Headers', 'Request', 'Response', 'AbortController', 'AbortSignal']) {
|
|
231
|
+
Object.defineProperty(window, name, { value: undefined, writable: true, configurable: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// The page's console, and the app's log.
|
|
235
|
+
const format = (value) => {
|
|
236
|
+
if (typeof value === 'string') return value;
|
|
237
|
+
if (value instanceof Error) return describe(value);
|
|
238
|
+
try {
|
|
239
|
+
return JSON.stringify(value) ?? String(value);
|
|
240
|
+
} catch {
|
|
241
|
+
return String(value);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
for (const level of ['log', 'info', 'warn', 'error', 'debug']) {
|
|
245
|
+
const write = console[level].bind(console);
|
|
246
|
+
console[level] = (...values) => {
|
|
247
|
+
write(...values);
|
|
248
|
+
send({ kind: 'log', level, message: values.map(format).join(' ') });
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const timers = new Map();
|
|
253
|
+
const clearTimer = (id) => {
|
|
254
|
+
clearTimeout(timers.get(id));
|
|
255
|
+
clearInterval(timers.get(id));
|
|
256
|
+
timers.delete(id);
|
|
257
|
+
};
|
|
258
|
+
window.__sbNative = {
|
|
259
|
+
workerPostMessage: (text) => send({ kind: 'message', text: String(text) }),
|
|
260
|
+
log: (level, message) => send({ kind: 'log', level: String(level), message: String(message) }),
|
|
261
|
+
now: () => performance.now(),
|
|
262
|
+
setTimer: (id, delay, repeats) => {
|
|
263
|
+
clearTimer(id);
|
|
264
|
+
const fire = () => {
|
|
265
|
+
if (!repeats) timers.delete(id);
|
|
266
|
+
window.__sbTimerFired?.(id);
|
|
267
|
+
};
|
|
268
|
+
timers.set(id, repeats ? setInterval(fire, delay) : setTimeout(fire, delay));
|
|
269
|
+
},
|
|
270
|
+
clearTimer,
|
|
271
|
+
fetch: (id, request) => send({ kind: 'fetch', id, request: typeof request === 'string' ? JSON.parse(request) : request }),
|
|
272
|
+
// No storage of its own, as a JSContext worker.
|
|
273
|
+
storageGet: () => null,
|
|
274
|
+
storageSet: () => {},
|
|
275
|
+
storageRemove: () => {},
|
|
276
|
+
storageKeys: () => [],
|
|
277
|
+
storageClear: () => {},
|
|
278
|
+
randomBytes: (count) => {
|
|
279
|
+
const bytes = crypto.getRandomValues(new Uint8Array(Math.max(0, Math.min(count | 0, 65536))));
|
|
280
|
+
let text = '';
|
|
281
|
+
for (const byte of bytes) text += String.fromCharCode(byte);
|
|
282
|
+
return btoa(text);
|
|
283
|
+
},
|
|
284
|
+
openURL: () => false,
|
|
285
|
+
locale: () => \(locale),
|
|
286
|
+
launchArguments: () => \(arguments),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// The worker's script, at top level; what it throws is the worker's error.
|
|
290
|
+
window.__sbWorkerRun = (script) => {
|
|
291
|
+
try {
|
|
292
|
+
(0, eval)(script);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
fail(error);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
})();
|
|
298
|
+
"""
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// The page's messages to the host, without the content controller keeping the host alive.
|
|
303
|
+
@MainActor
|
|
304
|
+
private final class MessageHandler: NSObject, WKScriptMessageHandler {
|
|
305
|
+
private weak var host: WebViewWorkerHost?
|
|
306
|
+
|
|
307
|
+
init(host: WebViewWorkerHost) {
|
|
308
|
+
self.host = host
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
312
|
+
if let text = message.body as? String { host?.receive(text) }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
#endif
|
|
@@ -174,8 +174,18 @@ final class WorkerHost: @unchecked Sendable {
|
|
|
174
174
|
|
|
175
175
|
/// As HostBridge's, answering on the worker's thread.
|
|
176
176
|
private func fetch(id: Int, request: JSONValue) {
|
|
177
|
+
Self.perform(request, session: session) { [weak self] result in
|
|
178
|
+
guard let self else { return }
|
|
179
|
+
self.thread.perform { [self] in self.call("__sbFetchResult", [.number(Double(id)), result]) }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Runs a worker's `fetch` request (`{url, method, headers, body}`, the
|
|
184
|
+
/// body base64) on `session` and answers with what `__sbFetchResult`
|
|
185
|
+
/// takes, on the session's queue.
|
|
186
|
+
static func perform(_ request: JSONValue, session: URLSession, completion: @escaping @Sendable (JSONValue) -> Void) {
|
|
177
187
|
guard let urlString = request["url"].string, let url = URL(string: urlString) else {
|
|
178
|
-
|
|
188
|
+
completion(.object(["error": .string("invalid URL")]))
|
|
179
189
|
return
|
|
180
190
|
}
|
|
181
191
|
var urlRequest = URLRequest(url: url)
|
|
@@ -186,31 +196,43 @@ final class WorkerHost: @unchecked Sendable {
|
|
|
186
196
|
if let body = request["body"].string, let data = Data(base64Encoded: body) {
|
|
187
197
|
urlRequest.httpBody = data
|
|
188
198
|
}
|
|
189
|
-
let task = session.dataTask(with: urlRequest) {
|
|
190
|
-
let result: JSONValue
|
|
199
|
+
let task = session.dataTask(with: urlRequest) { data, response, error in
|
|
191
200
|
if let error {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
let http = response as? HTTPURLResponse
|
|
195
|
-
var headers: JSONObject = [:]
|
|
196
|
-
for (name, value) in http?.allHeaderFields ?? [:] {
|
|
197
|
-
headers[String(describing: name).lowercased()] = .string(String(describing: value))
|
|
198
|
-
}
|
|
199
|
-
result = .object([
|
|
200
|
-
"status": .number(Double(http?.statusCode ?? 200)),
|
|
201
|
-
"statusText": .string(http.map { HTTPURLResponse.localizedString(forStatusCode: $0.statusCode) } ?? ""),
|
|
202
|
-
"headers": .object(headers),
|
|
203
|
-
"url": .string(response?.url?.absoluteString ?? urlString),
|
|
204
|
-
"body": .string((data ?? Data()).base64EncodedString()),
|
|
205
|
-
])
|
|
201
|
+
completion(.object(["error": .string(error.localizedDescription)]))
|
|
202
|
+
return
|
|
206
203
|
}
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
let http = response as? HTTPURLResponse
|
|
205
|
+
var headers: JSONObject = [:]
|
|
206
|
+
for (name, value) in http?.allHeaderFields ?? [:] {
|
|
207
|
+
headers[String(describing: name).lowercased()] = .string(String(describing: value))
|
|
208
|
+
}
|
|
209
|
+
completion(.object([
|
|
210
|
+
"status": .number(Double(http?.statusCode ?? 200)),
|
|
211
|
+
"statusText": .string(http.map { HTTPURLResponse.localizedString(forStatusCode: $0.statusCode) } ?? ""),
|
|
212
|
+
"headers": .object(headers),
|
|
213
|
+
"url": .string(response?.url?.absoluteString ?? urlString),
|
|
214
|
+
"body": .string((data ?? Data()).base64EncodedString()),
|
|
215
|
+
]))
|
|
209
216
|
}
|
|
210
217
|
task.resume()
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
/// A worker as the app's bridge (HostBridge.swift) drives it: this file's
|
|
222
|
+
/// `JSContext` on a thread, or a hidden web view (WebViewWorkerHost.swift).
|
|
223
|
+
@MainActor
|
|
224
|
+
protocol WorkerEngine: AnyObject {
|
|
225
|
+
/// Loads the script; messages posted before it ran wait for it.
|
|
226
|
+
func start()
|
|
227
|
+
/// Delivers an encoded message to the worker's `__sbWorkerReceive`.
|
|
228
|
+
func post(_ text: String)
|
|
229
|
+
func terminate()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// In an extension, so the protocol's actor is not inferred for the class:
|
|
233
|
+
// the host's own work is on its thread.
|
|
234
|
+
extension WorkerHost: WorkerEngine {}
|
|
235
|
+
|
|
214
236
|
/// A thread with a run loop that runs blocks sent to it, in order, after
|
|
215
237
|
/// the block it started with.
|
|
216
238
|
final class WorkerThread: @unchecked Sendable {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftUI
|
|
4
|
+
#if canImport(FoundationBridge)
|
|
5
|
+
import FoundationBridge // `AsyncImage`, against the shim
|
|
6
|
+
#endif
|
|
7
|
+
#if canImport(SwiftDraw)
|
|
8
|
+
import SwiftDraw
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
/// An `image` with a URL: SwiftUI's `AsyncImage`, and for an SVG, which the
|
|
12
|
+
/// system's image decoding does not read (a logo found on a website often
|
|
13
|
+
/// is one), SwiftDraw's parser and renderer, drawn as vectors at any size.
|
|
14
|
+
/// SwiftDraw is the package's dependency on Apple platforms only; the round
|
|
15
|
+
/// trip's build of these views has the page, which draws SVG itself.
|
|
16
|
+
struct RemoteImage: View {
|
|
17
|
+
let url: URL?
|
|
18
|
+
let resizable: Bool
|
|
19
|
+
let width: Double
|
|
20
|
+
let height: Double
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
AsyncImage(url: url) { phase in
|
|
24
|
+
if let image = phase.image {
|
|
25
|
+
if resizable { image.resizable() } else { image }
|
|
26
|
+
} else if phase.error != nil, let url {
|
|
27
|
+
undecoded(url)
|
|
28
|
+
} else {
|
|
29
|
+
placeholder
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Nothing, at the image's size when the app knows it.
|
|
35
|
+
@ViewBuilder private var placeholder: some View {
|
|
36
|
+
if width > 0, height > 0 {
|
|
37
|
+
Color.clear.frame(width: width, height: height)
|
|
38
|
+
} else {
|
|
39
|
+
Color.clear
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// What `AsyncImage` could not decode: an SVG, drawn by SwiftDraw, else nothing.
|
|
44
|
+
@ViewBuilder private func undecoded(_ url: URL) -> some View {
|
|
45
|
+
#if canImport(SwiftDraw)
|
|
46
|
+
RemoteSVG(url: url, resizable: resizable) { placeholder }
|
|
47
|
+
#else
|
|
48
|
+
placeholder
|
|
49
|
+
#endif
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#if canImport(SwiftDraw)
|
|
54
|
+
/// The SVG at `url`, loaded through the shared session (so the cookies and
|
|
55
|
+
/// cache `AsyncImage` used apply), or the placeholder while it loads and
|
|
56
|
+
/// when the data is not an SVG.
|
|
57
|
+
struct RemoteSVG<Placeholder: View>: View {
|
|
58
|
+
let url: URL
|
|
59
|
+
let resizable: Bool
|
|
60
|
+
@ViewBuilder let placeholder: () -> Placeholder
|
|
61
|
+
@State private var loaded: LoadedSVG?
|
|
62
|
+
|
|
63
|
+
var body: some View {
|
|
64
|
+
Group {
|
|
65
|
+
if let loaded, loaded.url == url, let svg = loaded.svg {
|
|
66
|
+
if resizable { SVGView(svg: svg).resizable() } else { SVGView(svg: svg) }
|
|
67
|
+
} else {
|
|
68
|
+
placeholder()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
.task(id: url) {
|
|
72
|
+
loaded = LoadedSVG(url: url, svg: await loadSVG(from: url))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
struct LoadedSVG: Equatable {
|
|
78
|
+
let url: URL
|
|
79
|
+
let svg: SVG?
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The SVG at `url`, or nil when it cannot be loaded or is not an SVG.
|
|
83
|
+
func loadSVG(from url: URL, session: URLSession = .shared) async -> SVG? {
|
|
84
|
+
guard let data = try? await session.data(from: url).0 else { return nil }
|
|
85
|
+
return SVG(data: data)
|
|
86
|
+
}
|
|
87
|
+
#endif
|
|
88
|
+
#endif
|
|
@@ -60,7 +60,8 @@ struct TextElement: View {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/// `image`: an SF Symbol (every name is right here, unlike the web's mapped
|
|
63
|
-
/// glyphs), an asset catalog image, a remote image
|
|
63
|
+
/// glyphs), an asset catalog image, a remote image (RemoteImage.swift: SVG
|
|
64
|
+
/// too), or a bitmap recipe (not drawn yet).
|
|
64
65
|
struct ImageElement: View {
|
|
65
66
|
let node: ElementNode
|
|
66
67
|
|
|
@@ -82,15 +83,7 @@ struct ImageElement: View {
|
|
|
82
83
|
Image(name)
|
|
83
84
|
}
|
|
84
85
|
case .url(let url, let resizable, let width, let height):
|
|
85
|
-
|
|
86
|
-
if let image = phase.image {
|
|
87
|
-
if resizable { image.resizable() } else { image }
|
|
88
|
-
} else if width > 0, height > 0 {
|
|
89
|
-
Color.clear.frame(width: width, height: height)
|
|
90
|
-
} else {
|
|
91
|
-
Color.clear
|
|
92
|
-
}
|
|
93
|
-
}
|
|
86
|
+
RemoteImage(url: URL(string: url), resizable: resizable, width: width, height: height)
|
|
94
87
|
case .bitmap(_, _, let width, let height):
|
|
95
88
|
// A `UIImage` recipe the page would decode; not drawn natively yet.
|
|
96
89
|
Rectangle().fill(.quaternary).frame(width: width > 0 ? CGFloat(width) : nil, height: height > 0 ? CGFloat(height) : nil)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/wasm/SwiftBrowserNative.wasm
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 49302688 bytes
|
|
4
|
+
sha256: 05f97ec252da1a85b64465ba5426d2ce1c620c78016ec4f2ae1bb7387a270be0
|