@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.
Files changed (102) hide show
  1. package/README.md +53 -11
  2. package/bin/swiftbrowser-native.mjs +47 -0
  3. package/dist/blob.d.ts +85 -11
  4. package/dist/blob.d.ts.map +1 -1
  5. package/dist/blob.js +184 -34
  6. package/dist/blob.js.map +1 -1
  7. package/dist/bridge.d.ts +66 -8
  8. package/dist/bridge.d.ts.map +1 -1
  9. package/dist/bridge.js.map +1 -1
  10. package/dist/encoding.d.ts +13 -0
  11. package/dist/encoding.d.ts.map +1 -1
  12. package/dist/encoding.js +24 -0
  13. package/dist/encoding.js.map +1 -1
  14. package/dist/fetch.d.ts +27 -13
  15. package/dist/fetch.d.ts.map +1 -1
  16. package/dist/fetch.js +117 -45
  17. package/dist/fetch.js.map +1 -1
  18. package/dist/hostRequests.d.ts +24 -6
  19. package/dist/hostRequests.d.ts.map +1 -1
  20. package/dist/hostRequests.js +108 -15
  21. package/dist/hostRequests.js.map +1 -1
  22. package/dist/index.d.ts +1 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/ios.d.ts +79 -0
  27. package/dist/ios.d.ts.map +1 -0
  28. package/dist/ios.js +181 -0
  29. package/dist/ios.js.map +1 -0
  30. package/dist/runtime.d.ts.map +1 -1
  31. package/dist/runtime.js +16 -7
  32. package/dist/runtime.js.map +1 -1
  33. package/dist/subtle.d.ts +1 -1
  34. package/dist/subtle.d.ts.map +1 -1
  35. package/dist/subtle.js +9 -5
  36. package/dist/subtle.js.map +1 -1
  37. package/dist/webSocket.d.ts +12 -5
  38. package/dist/webSocket.d.ts.map +1 -1
  39. package/dist/webSocket.js +37 -11
  40. package/dist/webSocket.js.map +1 -1
  41. package/package.json +7 -3
  42. package/src/blob.ts +215 -34
  43. package/src/bridge.ts +56 -8
  44. package/src/encoding.ts +22 -0
  45. package/src/fetch.ts +111 -48
  46. package/src/hostRequests.ts +129 -22
  47. package/src/index.ts +1 -2
  48. package/src/ios.ts +253 -0
  49. package/src/runtime.ts +23 -8
  50. package/src/subtle.ts +10 -6
  51. package/src/webSocket.ts +37 -13
  52. package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
  53. package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
  54. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
  55. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
  56. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
  57. package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
  58. package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
  59. package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
  60. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
  61. package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
  62. package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
  63. package/swift/Packages/SwiftBrowserShader/README.md +45 -0
  64. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
  65. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
  66. package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
  67. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
  68. package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
  69. package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
  70. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
  71. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
  72. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
  73. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
  74. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
  75. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
  76. package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
  77. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
  78. package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
  79. package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
  80. package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
  81. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
  82. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
  83. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
  84. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
  85. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
  86. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
  87. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
  88. package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
  89. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
  90. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
  91. package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
  92. package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
  93. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
  94. package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
  95. package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
  96. package/wasm/SwiftBrowserNative.wasm +2 -2
  97. package/dist/native.d.ts +0 -19
  98. package/dist/native.d.ts.map +0 -1
  99. package/dist/native.js +0 -17
  100. package/dist/native.js.map +0 -1
  101. package/src/native.ts +0 -35
  102. 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
- /** Starts a request; the answer arrives at `__sbFetchResult(id, result)`. */
21
- fetch(id: number, request: string | NativeFetchRequest): void;
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
- wsOpen?(id: number, url: string, protocols: string[]): void;
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). Bodies cross
4
- * as base64; a `Response` reads them back as text, JSON, bytes or a `Blob`.
5
- * A `Blob` or `FormData` body (blob.ts) is sent whole, the form as
6
- * multipart; there are no streams.
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 { base64Decode, base64Encode, utf8Decode, utf8Encode } from './encoding';
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
- /** Bytes of a body, and the content type it implies when the caller set none. */
133
- function encodeBody(body: BodyInitLike): { bytes: Uint8Array | null; contentType: string | null } {
134
- if (body === null || body === undefined) return { bytes: null, contentType: null };
135
- if (typeof body === 'string') return { bytes: utf8Encode(body), contentType: 'text/plain;charset=UTF-8' };
136
- if (body instanceof SimpleBlob) return { bytes: body.bytes(), contentType: body.type || null };
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
- protected bytes: Uint8Array | null;
149
+ /** The body's parts: in memory, or on disk until read. */
150
+ protected parts: BodyPart[] | null;
148
151
  bodyUsed = false;
149
- constructor(bytes: Uint8Array | null) {
150
- this.bytes = bytes;
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.bytes ?? new Uint8Array(0);
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 { bytes, contentType } = encodeBody(init.body);
200
- super(bytes ?? base?.rawBytes() ?? null);
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.bytes;
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, { body: this.bytes });
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 { bytes, contentType } = encodeBody(body);
237
- super(bytes);
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
- return new SimpleResponse(this.bytes, { status: this.status, statusText: this.statusText, headers: this.headers, url: this.url });
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
- /** The fetch globals over the bridge. */
276
- export function createFetch(native: Pick<NativeBridge, 'fetch'>): FetchGlobals {
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)) reject(request.signal.reason);
291
- });
292
- const bytes = request.rawBytes();
293
- native.fetch(id, {
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 ? base64Decode(result.body) : null, {
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 ?? {},