@swiftbrowser/native 0.18.3 → 0.21.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 +53 -11
- package/bin/swiftbrowser-native.mjs +47 -0
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +66 -8
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/encoding.d.ts +13 -0
- package/dist/encoding.d.ts.map +1 -1
- package/dist/encoding.js +24 -0
- package/dist/encoding.js.map +1 -1
- package/dist/fetch.d.ts +27 -13
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +117 -45
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +24 -6
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +108 -15
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/ios.d.ts +79 -0
- package/dist/ios.d.ts.map +1 -0
- package/dist/ios.js +181 -0
- package/dist/ios.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +16 -7
- package/dist/runtime.js.map +1 -1
- package/dist/subtle.d.ts +1 -1
- package/dist/subtle.d.ts.map +1 -1
- package/dist/subtle.js +9 -5
- package/dist/subtle.js.map +1 -1
- package/dist/webSocket.d.ts +12 -5
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +37 -11
- package/dist/webSocket.js.map +1 -1
- package/package.json +7 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +56 -8
- package/src/encoding.ts +22 -0
- package/src/fetch.ts +111 -48
- package/src/hostRequests.ts +129 -22
- package/src/index.ts +1 -2
- package/src/ios.ts +253 -0
- package/src/runtime.ts +23 -8
- package/src/subtle.ts +10 -6
- package/src/webSocket.ts +37 -13
- package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
- package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
- package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
package/src/bridge.ts
CHANGED
|
@@ -17,8 +17,27 @@ export interface NativeBridge {
|
|
|
17
17
|
now(): number;
|
|
18
18
|
setTimer(id: number, delayMs: number, repeats: boolean): void;
|
|
19
19
|
clearTimer(id: number): void;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Starts a request; the answer arrives at `__sbFetchResult(id, result)`.
|
|
22
|
+
* A body may be a typed array after the request, which names it
|
|
23
|
+
* `{ $bytes: 0 }`, where the host has `hostRequestBytes`; or, where the
|
|
24
|
+
* host `fetchStreams()`, a list of `bodyParts`, each a typed array beside
|
|
25
|
+
* the request or a range of a file on disk, which the host streams in
|
|
26
|
+
* order without reading the files into memory.
|
|
27
|
+
*/
|
|
28
|
+
fetch(id: number, request: string | NativeFetchRequest, ...views: ArrayBufferView[]): void;
|
|
29
|
+
/**
|
|
30
|
+
* Whether `fetch` takes `bodyParts` (a body streamed from files on disk,
|
|
31
|
+
* `InputStream` on the Swift side). Hosts before 0.20 lack it, and then
|
|
32
|
+
* a body holding a picked file is read into memory first.
|
|
33
|
+
*/
|
|
34
|
+
fetchStreams?(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Cancels request `id` (an `AbortController`'s abort): the `URLSession`
|
|
37
|
+
* task stops, its bytes stop coming, and no `__sbFetchResult` follows.
|
|
38
|
+
* Hosts before 0.20 lack it, and then only the promise is rejected.
|
|
39
|
+
*/
|
|
40
|
+
fetchCancel?(id: number): void;
|
|
22
41
|
/**
|
|
23
42
|
* Starts loading the image at `url` for its pixel size (image.ts: the
|
|
24
43
|
* runtime's `Image`), keeping the bytes for the `image` element that
|
|
@@ -34,6 +53,16 @@ export interface NativeBridge {
|
|
|
34
53
|
storageClear(): void;
|
|
35
54
|
/** Base64 of `count` random bytes. */
|
|
36
55
|
randomBytes(count: number): string;
|
|
56
|
+
/**
|
|
57
|
+
* Typed arrays in and out where the host carries bytes (it has
|
|
58
|
+
* `hostRequestBytes`, 0.19): `randomBytes`, `digest` and `hmac` without
|
|
59
|
+
* base64, and UTF-8 for encoding.ts. Older hosts lack them.
|
|
60
|
+
*/
|
|
61
|
+
randomValues?(count: number): Uint8Array;
|
|
62
|
+
digestBytes?(algorithm: string, data: ArrayBufferView): Uint8Array | null | undefined;
|
|
63
|
+
hmacBytes?(algorithm: string, key: ArrayBufferView, data: ArrayBufferView): Uint8Array | null | undefined;
|
|
64
|
+
decodeUTF8?(bytes: ArrayBufferView): string;
|
|
65
|
+
encodeUTF8?(text: string): Uint8Array;
|
|
37
66
|
openURL(url: string): boolean;
|
|
38
67
|
/** The viewer's locale identifier (`en_US`). */
|
|
39
68
|
locale(): string;
|
|
@@ -67,9 +96,10 @@ export interface NativeBridge {
|
|
|
67
96
|
pushRegister?(id: number, applicationServerKey?: string): void;
|
|
68
97
|
|
|
69
98
|
// WebSockets (webSocket.ts) on `URLSessionWebSocketTask`; events come to `__sbWebSocketEvent(id, json)`. Hosts before 0.20 lack them.
|
|
70
|
-
|
|
99
|
+
/** `bytes`: binary messages come as a `Uint8Array` after the event's JSON (`{ binary: { $bytes: 0 } }`), not base64. */
|
|
100
|
+
wsOpen?(id: number, url: string, protocols: string[], bytes?: boolean): void;
|
|
71
101
|
/** `{ text }` or `{ binary }` (base64), as JSON. */
|
|
72
|
-
wsSend?(id: number, json: string): void;
|
|
102
|
+
wsSend?(id: number, json: string, ...views: ArrayBufferView[]): void;
|
|
73
103
|
wsClose?(id: number, code: number, reason: string): void;
|
|
74
104
|
|
|
75
105
|
// `crypto.subtle` (subtle.ts) on CryptoKit, synchronously. Hosts before 0.20 lack them.
|
|
@@ -143,6 +173,15 @@ export interface NativeBridge {
|
|
|
143
173
|
* Hosts before 0.20 lack it.
|
|
144
174
|
*/
|
|
145
175
|
hostRequest?(id: number, kind: string, json: string): void;
|
|
176
|
+
/**
|
|
177
|
+
* `hostRequest` with bytes beside the JSON: each typed array (or
|
|
178
|
+
* `ArrayBuffer`) in the params is lifted out and sent as a further
|
|
179
|
+
* argument, and `{ "$bytes": n }` stands in its place in the JSON; an
|
|
180
|
+
* answer's bytes follow `__sbHostResponse`'s JSON the same way
|
|
181
|
+
* (docs/notes/bytes.md). A host without it (before 0.19) is given base64
|
|
182
|
+
* in the JSON instead, which `hostRequest` makes from the same views.
|
|
183
|
+
*/
|
|
184
|
+
hostRequestBytes?(id: number, kind: string, json: string, ...views: ArrayBufferView[]): void;
|
|
146
185
|
|
|
147
186
|
/**
|
|
148
187
|
* The escape hatch's functions (nativeFunctions.ts): the names the Swift
|
|
@@ -221,8 +260,17 @@ export interface NativeFetchRequest {
|
|
|
221
260
|
url: string;
|
|
222
261
|
method: string;
|
|
223
262
|
headers: Record<string, string>;
|
|
224
|
-
/** Base64, or null. */
|
|
225
|
-
body: string | null;
|
|
263
|
+
/** Base64, `{ $bytes: 0 }` naming the view after the request, or null. */
|
|
264
|
+
body: string | { $bytes: number } | null;
|
|
265
|
+
/**
|
|
266
|
+
* With `fetchStreams`: the body as parts in order, each a view beside the
|
|
267
|
+
* request or `[start, end)` of a file on disk (one the app may read: in
|
|
268
|
+
* its directories, or picked or dropped), sent as one stream of their
|
|
269
|
+
* total length; `body` is null then.
|
|
270
|
+
*/
|
|
271
|
+
bodyParts?: ({ $bytes: number } | { url: string; start: number; end: number })[];
|
|
272
|
+
/** The response's body as a `Uint8Array` after the answer's JSON, not base64. */
|
|
273
|
+
bytes?: boolean;
|
|
226
274
|
}
|
|
227
275
|
|
|
228
276
|
export interface NativeImageProbeResult {
|
|
@@ -236,8 +284,8 @@ export interface NativeFetchResult {
|
|
|
236
284
|
statusText?: string;
|
|
237
285
|
headers?: Record<string, string>;
|
|
238
286
|
url?: string;
|
|
239
|
-
/** Base64. */
|
|
240
|
-
body?: string;
|
|
287
|
+
/** Base64, or `{ $bytes: 0 }` naming the `Uint8Array` after the JSON. */
|
|
288
|
+
body?: string | { $bytes: number };
|
|
241
289
|
error?: string;
|
|
242
290
|
}
|
|
243
291
|
|
package/src/encoding.ts
CHANGED
|
@@ -4,7 +4,20 @@
|
|
|
4
4
|
* base64 (fetch bodies). No dependencies, so the runtime can carry them.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The host's UTF-8, where it has it (`decodeUTF8` and `encodeUTF8`, a
|
|
9
|
+
* native function over a typed array): the loops below run interpreted in
|
|
10
|
+
* an iOS app, so text past a few dozen characters goes to the host instead.
|
|
11
|
+
*/
|
|
12
|
+
let nativeUTF8: { decode(bytes: Uint8Array): string; encode(text: string): Uint8Array } | null = null;
|
|
13
|
+
const NATIVE_UTF8_FROM = 64;
|
|
14
|
+
|
|
15
|
+
export function installNativeUTF8(utf8: typeof nativeUTF8): void {
|
|
16
|
+
nativeUTF8 = utf8;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export function utf8Encode(text: string): Uint8Array {
|
|
20
|
+
if (nativeUTF8 && text.length >= NATIVE_UTF8_FROM) return nativeUTF8.encode(text);
|
|
8
21
|
const out: number[] = [];
|
|
9
22
|
for (let i = 0; i < text.length; i++) {
|
|
10
23
|
let code = text.charCodeAt(i);
|
|
@@ -24,6 +37,7 @@ export function utf8Encode(text: string): Uint8Array {
|
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export function utf8Decode(bytes: Uint8Array): string {
|
|
40
|
+
if (nativeUTF8 && bytes.length >= NATIVE_UTF8_FROM) return nativeUTF8.decode(bytes);
|
|
27
41
|
let out = '';
|
|
28
42
|
for (let i = 0; i < bytes.length; ) {
|
|
29
43
|
const a = bytes[i]!;
|
|
@@ -66,6 +80,14 @@ export function base64Encode(bytes: Uint8Array): string {
|
|
|
66
80
|
return out;
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
/** Bytes that crossed the bridge: a view beside the JSON, or base64 from a host without them. */
|
|
84
|
+
export function bridgedBytes(value: unknown): Uint8Array {
|
|
85
|
+
if (typeof value === 'string') return base64Decode(value);
|
|
86
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
87
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
88
|
+
return new Uint8Array(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
export function base64Decode(text: string): Uint8Array {
|
|
70
92
|
const clean = text.replace(/[^A-Za-z0-9+/]/g, '');
|
|
71
93
|
const out: number[] = [];
|
package/src/fetch.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `fetch`, `Headers`, `Request`, `Response` and `AbortController` over the
|
|
3
|
-
* bridge's `fetch` primitive (`URLSession` on the Swift side).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* bridge's `fetch` primitive (`URLSession` on the Swift side). A request
|
|
4
|
+
* body is a list of parts (blob.ts): bytes here, or ranges of files on disk
|
|
5
|
+
* (picked or dropped files, and blobs and forms holding them), which a host
|
|
6
|
+
* with `fetchStreams` sends as one stream straight from disk, and an older
|
|
7
|
+
* host is given read into memory. A `Response` reads its body back as
|
|
8
|
+
* text, JSON, bytes or a `Blob`; responses are not streamed.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import type { NativeBridge, NativeFetchResult } from './bridge';
|
|
10
|
-
import { encodeMultipart, SimpleBlob, SimpleFormData } from './blob';
|
|
11
|
-
import {
|
|
11
|
+
import type { NativeBridge, NativeFetchRequest, NativeFetchResult } from './bridge';
|
|
12
|
+
import { encodeMultipart, partsAreLazy, partsBytes, partSize, SimpleBlob, SimpleFormData, type BodyPart } from './blob';
|
|
13
|
+
import { base64Encode, bridgedBytes, utf8Decode, utf8Encode } from './encoding';
|
|
12
14
|
|
|
13
15
|
type HeadersInitLike = SimpleHeaders | Record<string, string> | [string, string][];
|
|
14
16
|
|
|
@@ -129,50 +131,51 @@ export function setBodyRandomness(random: (count: number) => Uint8Array): void {
|
|
|
129
131
|
randomBytes = random;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
/**
|
|
133
|
-
function encodeBody(body: BodyInitLike): {
|
|
134
|
-
if (body === null || body === undefined) return {
|
|
135
|
-
if (typeof body === 'string') return { bytes: utf8Encode(body), contentType: 'text/plain;charset=UTF-8' };
|
|
136
|
-
if (body instanceof SimpleBlob) return {
|
|
134
|
+
/** The content type a body implies when the caller set none, and its parts. */
|
|
135
|
+
function encodeBody(body: BodyInitLike): { parts: BodyPart[] | null; contentType: string | null } {
|
|
136
|
+
if (body === null || body === undefined) return { parts: null, contentType: null };
|
|
137
|
+
if (typeof body === 'string') return { parts: [{ bytes: utf8Encode(body) }], contentType: 'text/plain;charset=UTF-8' };
|
|
138
|
+
if (body instanceof SimpleBlob) return { parts: body.parts(), contentType: body.type || null };
|
|
137
139
|
if (body instanceof SimpleFormData) return encodeMultipart(body, randomBytes);
|
|
138
|
-
if (body instanceof ArrayBuffer) return { bytes: new Uint8Array(body), contentType: null };
|
|
139
|
-
if (ArrayBuffer.isView(body)) return { bytes: new Uint8Array(body.buffer, body.byteOffset, body.byteLength), contentType: null };
|
|
140
|
+
if (body instanceof ArrayBuffer) return { parts: [{ bytes: new Uint8Array(body) }], contentType: null };
|
|
141
|
+
if (ArrayBuffer.isView(body)) return { parts: [{ bytes: new Uint8Array(body.buffer, body.byteOffset, body.byteLength) }], contentType: null };
|
|
140
142
|
if (typeof body === 'object' && Symbol.iterator in body && typeof body.toString === 'function') {
|
|
141
|
-
return { bytes: utf8Encode(body.toString()), contentType: 'application/x-www-form-urlencoded;charset=UTF-8' };
|
|
143
|
+
return { parts: [{ bytes: utf8Encode(body.toString()) }], contentType: 'application/x-www-form-urlencoded;charset=UTF-8' };
|
|
142
144
|
}
|
|
143
|
-
return { bytes: utf8Encode(String(body)), contentType: 'text/plain;charset=UTF-8' };
|
|
145
|
+
return { parts: [{ bytes: utf8Encode(String(body)) }], contentType: 'text/plain;charset=UTF-8' };
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
class Body {
|
|
147
|
-
|
|
149
|
+
/** The body's parts: in memory, or on disk until read. */
|
|
150
|
+
protected parts: BodyPart[] | null;
|
|
148
151
|
bodyUsed = false;
|
|
149
|
-
constructor(
|
|
150
|
-
this.
|
|
152
|
+
constructor(parts: BodyPart[] | null) {
|
|
153
|
+
this.parts = parts;
|
|
151
154
|
}
|
|
152
155
|
get body(): null {
|
|
153
156
|
// No streams; read the body whole with the methods below.
|
|
154
157
|
return null;
|
|
155
158
|
}
|
|
156
|
-
private take(): Uint8Array {
|
|
159
|
+
private async take(): Promise<Uint8Array> {
|
|
157
160
|
if (this.bodyUsed) throw new TypeError('body used already');
|
|
158
161
|
this.bodyUsed = true;
|
|
159
|
-
return this.
|
|
162
|
+
return this.parts ? partsBytes(this.parts) : new Uint8Array(0);
|
|
160
163
|
}
|
|
161
164
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
|
162
|
-
const bytes = this.take();
|
|
165
|
+
const bytes = await this.take();
|
|
163
166
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
164
167
|
}
|
|
165
168
|
async bytesArray(): Promise<Uint8Array> {
|
|
166
169
|
return this.take();
|
|
167
170
|
}
|
|
168
171
|
async text(): Promise<string> {
|
|
169
|
-
return utf8Decode(this.take());
|
|
172
|
+
return utf8Decode(await this.take());
|
|
170
173
|
}
|
|
171
174
|
async json(): Promise<unknown> {
|
|
172
|
-
return JSON.parse(utf8Decode(this.take()));
|
|
175
|
+
return JSON.parse(utf8Decode(await this.take()));
|
|
173
176
|
}
|
|
174
177
|
async blob(): Promise<SimpleBlob> {
|
|
175
|
-
return new SimpleBlob([this.take()], { type: this.contentType() });
|
|
178
|
+
return new SimpleBlob([await this.take()], { type: this.contentType() });
|
|
176
179
|
}
|
|
177
180
|
/** The body's content type, for `blob()`; a Request or Response reads its headers. */
|
|
178
181
|
protected contentType(): string {
|
|
@@ -196,24 +199,28 @@ export class SimpleRequest extends Body {
|
|
|
196
199
|
|
|
197
200
|
constructor(input: string | SimpleRequest | { href: string }, init: RequestInitLike = {}) {
|
|
198
201
|
const base = input instanceof SimpleRequest ? input : null;
|
|
199
|
-
const {
|
|
200
|
-
super(
|
|
202
|
+
const { parts, contentType } = encodeBody(init.body);
|
|
203
|
+
super(parts ?? base?.rawBody() ?? null);
|
|
201
204
|
this.url = typeof input === 'string' ? input : input instanceof SimpleRequest ? input.url : input.href;
|
|
202
205
|
this.method = (init.method ?? base?.method ?? 'GET').toUpperCase();
|
|
203
206
|
this.headers = new SimpleHeaders(init.headers ?? base?.headers);
|
|
204
207
|
if (contentType && !this.headers.has('content-type')) this.headers.set('content-type', contentType);
|
|
205
208
|
this.signal = init.signal ?? base?.signal ?? new SimpleAbortSignal();
|
|
206
209
|
}
|
|
207
|
-
/** @internal */
|
|
208
|
-
rawBytes(): Uint8Array | null {
|
|
209
|
-
return this.
|
|
210
|
+
/** @internal The body's bytes, read from disk first where they come from there. */
|
|
211
|
+
async rawBytes(): Promise<Uint8Array | null> {
|
|
212
|
+
return this.parts === null ? null : await partsBytes(this.parts);
|
|
213
|
+
}
|
|
214
|
+
/** @internal The body's parts. */
|
|
215
|
+
rawBody(): BodyPart[] | null {
|
|
216
|
+
return this.parts;
|
|
210
217
|
}
|
|
211
218
|
protected override contentType(): string {
|
|
212
219
|
return this.headers.get('content-type') ?? '';
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
clone(): SimpleRequest {
|
|
216
|
-
return new SimpleRequest(this, {
|
|
223
|
+
return new SimpleRequest(this, {});
|
|
217
224
|
}
|
|
218
225
|
}
|
|
219
226
|
|
|
@@ -233,8 +240,8 @@ export class SimpleResponse extends Body {
|
|
|
233
240
|
readonly redirected = false;
|
|
234
241
|
|
|
235
242
|
constructor(body: BodyInitLike = null, init: ResponseInitLike = {}) {
|
|
236
|
-
const {
|
|
237
|
-
super(
|
|
243
|
+
const { parts, contentType } = encodeBody(body);
|
|
244
|
+
super(parts);
|
|
238
245
|
this.status = init.status ?? 200;
|
|
239
246
|
this.statusText = init.statusText ?? '';
|
|
240
247
|
this.headers = new SimpleHeaders(init.headers);
|
|
@@ -249,7 +256,9 @@ export class SimpleResponse extends Body {
|
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
clone(): SimpleResponse {
|
|
252
|
-
|
|
259
|
+
const copy = new SimpleResponse(null, { status: this.status, statusText: this.statusText, headers: this.headers, url: this.url });
|
|
260
|
+
copy.parts = this.parts;
|
|
261
|
+
return copy;
|
|
253
262
|
}
|
|
254
263
|
static json(data: unknown, init: ResponseInitLike = {}): SimpleResponse {
|
|
255
264
|
const response = new SimpleResponse(JSON.stringify(data), init);
|
|
@@ -261,6 +270,22 @@ export class SimpleResponse extends Body {
|
|
|
261
270
|
}
|
|
262
271
|
}
|
|
263
272
|
|
|
273
|
+
/** In-memory parts as one array (none is on disk); one part is itself. */
|
|
274
|
+
function joinBytes(parts: readonly BodyPart[]): Uint8Array {
|
|
275
|
+
const [only] = parts;
|
|
276
|
+
if (parts.length === 1 && only && 'bytes' in only) return only.bytes;
|
|
277
|
+
const total = parts.reduce((sum, part) => sum + partSize(part), 0);
|
|
278
|
+
const out = new Uint8Array(total);
|
|
279
|
+
let at = 0;
|
|
280
|
+
for (const part of parts) {
|
|
281
|
+
if ('bytes' in part) {
|
|
282
|
+
out.set(part.bytes, at);
|
|
283
|
+
at += part.bytes.byteLength;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
264
289
|
export interface FetchGlobals {
|
|
265
290
|
fetch: (input: string | SimpleRequest | { href: string }, init?: RequestInitLike) => Promise<SimpleResponse>;
|
|
266
291
|
Headers: typeof SimpleHeaders;
|
|
@@ -268,12 +293,18 @@ export interface FetchGlobals {
|
|
|
268
293
|
Response: typeof SimpleResponse;
|
|
269
294
|
AbortController: typeof SimpleAbortController;
|
|
270
295
|
AbortSignal: typeof SimpleAbortSignal;
|
|
271
|
-
/** The bridge's answer; Swift calls it as `__sbFetchResult(id, result)`. */
|
|
272
|
-
__sbFetchResult: (id: number, result: NativeFetchResult | string) => void;
|
|
296
|
+
/** The bridge's answer; Swift calls it as `__sbFetchResult(id, result, ...bytes)`. */
|
|
297
|
+
__sbFetchResult: (id: number, result: NativeFetchResult | string, ...buffers: ArrayBufferView[]) => void;
|
|
273
298
|
}
|
|
274
299
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
300
|
+
/**
|
|
301
|
+
* The fetch globals over the bridge. With `bytes` (a host that carries
|
|
302
|
+
* typed arrays, docs/notes/bytes.md) bodies cross as views beside the JSON
|
|
303
|
+
* both ways; without, as base64. With `streams` (the host `fetchStreams()`,
|
|
304
|
+
* which needs `bytes`) a body holding files on disk goes as parts the host
|
|
305
|
+
* streams from there; without, such a body is read into memory first.
|
|
306
|
+
*/
|
|
307
|
+
export function createFetch(native: Pick<NativeBridge, 'fetch' | 'fetchCancel'>, options: { bytes?: boolean; streams?: boolean } = {}): FetchGlobals {
|
|
277
308
|
let nextId = 1;
|
|
278
309
|
const pending = new Map<number, { resolve: (response: SimpleResponse) => void; reject: (error: unknown) => void }>();
|
|
279
310
|
|
|
@@ -286,20 +317,52 @@ export function createFetch(native: Pick<NativeBridge, 'fetch'>): FetchGlobals {
|
|
|
286
317
|
}
|
|
287
318
|
const id = nextId++;
|
|
288
319
|
pending.set(id, { resolve, reject });
|
|
320
|
+
let sent = false;
|
|
289
321
|
request.signal.addEventListener('abort', () => {
|
|
290
|
-
if (pending.delete(id))
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
url: request.url,
|
|
295
|
-
method: request.method,
|
|
296
|
-
headers: request.headers.toObject(),
|
|
297
|
-
body: bytes ? base64Encode(bytes) : null,
|
|
322
|
+
if (!pending.delete(id)) return;
|
|
323
|
+
// The host stops the task where it can (0.20); an older host finishes it and its answer is ignored.
|
|
324
|
+
if (sent) native.fetchCancel?.(id);
|
|
325
|
+
reject(request.signal.reason);
|
|
298
326
|
});
|
|
327
|
+
const head = { url: request.url, method: request.method, headers: request.headers.toObject() };
|
|
328
|
+
const send = (bytes: Uint8Array | null): void => {
|
|
329
|
+
if (!pending.has(id)) return; // aborted while the body was read
|
|
330
|
+
sent = true;
|
|
331
|
+
if (options.bytes) {
|
|
332
|
+
if (bytes) native.fetch(id, { ...head, body: { $bytes: 0 }, bytes: true }, bytes);
|
|
333
|
+
else native.fetch(id, { ...head, body: null, bytes: true });
|
|
334
|
+
} else {
|
|
335
|
+
native.fetch(id, { ...head, body: bytes ? base64Encode(bytes) : null });
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const parts = request.rawBody();
|
|
339
|
+
if (parts === null || !partsAreLazy(parts)) {
|
|
340
|
+
// Bytes already here go out in this turn (tests and callers see the request at once).
|
|
341
|
+
send(parts === null ? null : joinBytes(parts));
|
|
342
|
+
} else if (options.streams && options.bytes) {
|
|
343
|
+
// Files on disk stay there: the host streams the parts in order, their total length known.
|
|
344
|
+
sent = true;
|
|
345
|
+
const views: Uint8Array[] = [];
|
|
346
|
+
const bodyParts: NonNullable<NativeFetchRequest['bodyParts']> = parts.map((part) => {
|
|
347
|
+
if ('bytes' in part) {
|
|
348
|
+
views.push(part.bytes);
|
|
349
|
+
return { $bytes: views.length - 1 };
|
|
350
|
+
}
|
|
351
|
+
return { url: part.file.url, start: part.file.start, end: part.file.end };
|
|
352
|
+
});
|
|
353
|
+
const headers = { ...head.headers };
|
|
354
|
+
if (!('content-length' in headers)) headers['content-length'] = String(parts.reduce((sum, part) => sum + partSize(part), 0));
|
|
355
|
+
native.fetch(id, { ...head, headers, body: null, bodyParts, bytes: true }, ...views);
|
|
356
|
+
} else {
|
|
357
|
+
// An older host: the files are read here first.
|
|
358
|
+
partsBytes(parts).then(send, (error: unknown) => {
|
|
359
|
+
if (pending.delete(id)) reject(error instanceof Error ? error : new TypeError(`fetch failed: ${String(error)}`));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
299
362
|
});
|
|
300
363
|
};
|
|
301
364
|
|
|
302
|
-
const __sbFetchResult: FetchGlobals['__sbFetchResult'] = (id, raw) => {
|
|
365
|
+
const __sbFetchResult: FetchGlobals['__sbFetchResult'] = (id, raw, ...buffers) => {
|
|
303
366
|
const waiting = pending.get(id);
|
|
304
367
|
if (!waiting) return;
|
|
305
368
|
pending.delete(id);
|
|
@@ -310,7 +373,7 @@ export function createFetch(native: Pick<NativeBridge, 'fetch'>): FetchGlobals {
|
|
|
310
373
|
return;
|
|
311
374
|
}
|
|
312
375
|
waiting.resolve(
|
|
313
|
-
new SimpleResponse(result.body ?
|
|
376
|
+
new SimpleResponse(result.body ? bridgedBytes(typeof result.body === 'string' ? result.body : buffers[result.body.$bytes]) : null, {
|
|
314
377
|
status: result.status ?? 200,
|
|
315
378
|
statusText: result.statusText ?? '',
|
|
316
379
|
headers: result.headers ?? {},
|