@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/README.md
CHANGED
|
@@ -97,9 +97,16 @@ There is no `window` and no `document`. Code that checks for them takes its
|
|
|
97
97
|
headless path, which is right here; `@swiftbrowser/react`'s `Host` (React DOM
|
|
98
98
|
inside the tree) renders nothing, as `react-dom` is aliased to a stub.
|
|
99
99
|
|
|
100
|
-
`fetch` sends the request over the bridge and `URLSession` performs it
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
`fetch` sends the request over the bridge and `URLSession` performs it. A
|
|
101
|
+
body may be a `Blob` or a `FormData` (sent as multipart: a file upload works
|
|
102
|
+
as written for the page), and one holding picked or dropped files still on
|
|
103
|
+
disk (the files themselves, their slices, a `Blob` built from slices of
|
|
104
|
+
several, a form with them) is streamed from disk: the body goes to the host
|
|
105
|
+
as parts, bytes beside the request and ranges of files, and an `InputStream`
|
|
106
|
+
reads them in order as the request sends, so a 2 GB upload holds nothing in
|
|
107
|
+
memory; a host before 0.20 reads the files first. Responses are read whole
|
|
108
|
+
(`response.body` is null; `arrayBuffer()`, `text()`, `json()`, `blob()`).
|
|
109
|
+
An `AbortController`'s abort cancels the task (`fetchCancel`). `URL` is a small
|
|
103
110
|
parser, enough for absolute and relative URLs and query strings, not the
|
|
104
111
|
whole WHATWG algorithm; `URL.createObjectURL` gives a `data:` URL. `WebSocket`
|
|
105
112
|
runs on `URLSessionWebSocketTask`, `crypto.subtle` digests (SHA-1 to
|
|
@@ -116,11 +123,15 @@ background, where the system owns the screen, and holds again on return).
|
|
|
116
123
|
Apple sheet (`ASAuthorizationController`, with the app's
|
|
117
124
|
`com.apple.developer.applesignin` entitlement) and answer the credential;
|
|
118
125
|
`appleCredentialState` asks the provider.
|
|
119
|
-
`fileManager` reaches the app's sandbox through `files` requests
|
|
126
|
+
`fileManager` reaches the app's sandbox through `files` requests (a
|
|
127
|
+
`range` read is `FileHandle`'s seek and read, so a large file goes a piece
|
|
128
|
+
at a time; a picked or dropped file written is copied on disk, `importItem`,
|
|
129
|
+
never through the bridge), and its
|
|
120
130
|
iCloud Drive container (`iCloudDirectory()`) where the app has the
|
|
121
131
|
entitlement: the container's files are read and written under file
|
|
122
132
|
coordination, downloaded first when another device wrote them, and their
|
|
123
|
-
changes come to `__sbFilesChanged` for `fileManager.watch
|
|
133
|
+
changes come to `__sbFilesChanged` for `fileManager.watch`, as the app's
|
|
134
|
+
own writes do.
|
|
124
135
|
`navigator.geolocation` asks `CLLocationManager`: `getCurrentPosition` for
|
|
125
136
|
one fix, `watchPosition` for every fix until `clearWatch`, with the API's
|
|
126
137
|
`GeolocationPositionError` codes (the first ask prompts the viewer; the
|
|
@@ -271,7 +282,15 @@ the host answers instead, with `UIActivityViewController`, the photo picker
|
|
|
271
282
|
to Files), `UIPasteboard` and `UIColorPickerViewController`.
|
|
272
283
|
Nothing to write: `run` fills the web renderer's environment with what the
|
|
273
284
|
page has nothing for, so a screen written for the page shares and picks as
|
|
274
|
-
it did.
|
|
285
|
+
it did. A picked file is a `LazyFile`: a `File` whose name, type and size
|
|
286
|
+
are known and whose bytes stay on disk (a document in the app's tmp, a
|
|
287
|
+
photo copied there, a folder opened in place with its security scope kept)
|
|
288
|
+
until `arrayBuffer()`, `text()` or a `slice` reads them by range through the
|
|
289
|
+
files bridge; `fileManager.write` copies it, a `fetch` body or `FormData`
|
|
290
|
+
streams it from disk, `ShareLink` and `fileExporter` copy it. Files dropped on a
|
|
291
|
+
`dropDestination` from another app arrive the same way, a dropped folder's
|
|
292
|
+
named by their paths in it. The app keeps them readable until it asks the
|
|
293
|
+
host to forget them (`files` `release`, or the next folder pick).
|
|
275
294
|
|
|
276
295
|
```ts
|
|
277
296
|
import { webAuthenticate, secureStorage } from '@swiftbrowser/native';
|
|
@@ -321,6 +340,25 @@ JavaScriptCore lacks. `test/fastRefresh.test.ts` runs the loop in Node.
|
|
|
321
340
|
Xcode scheme or `simctl launch`), as the Fidelity example does to open one
|
|
322
341
|
screen.
|
|
323
342
|
|
|
343
|
+
## In CI
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
npx swiftbrowser-native ios-profile com.example.MyApp --profile "MyApp Ad Hoc" --name MyApp
|
|
347
|
+
npx swiftbrowser-native ios-install-page build/MyApp.ipa --out site --url https://ios.my-app.pages.dev \
|
|
348
|
+
--bundle-id com.example.MyApp --version 1.0.42 [--web https://my-app.pages.dev/] [--commit $GITHUB_SHA]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`ios-profile` makes an ad hoc provisioning profile again through the App
|
|
352
|
+
Store Connect API (`ASC_KEY_ID`, `ASC_ISSUER_ID` and `ASC_KEY_P8` from the
|
|
353
|
+
environment, a key with the Admin or App Manager role) with every enabled
|
|
354
|
+
iOS device and valid distribution certificate of the team, registering the
|
|
355
|
+
bundle ID if the team has none, and installs it where Xcode looks.
|
|
356
|
+
`ios-install-page` writes the over-the-air install page around an exported
|
|
357
|
+
IPA: the IPA, its `manifest.plist`, an `index.html` whose button installs it,
|
|
358
|
+
and the `_headers` Cloudflare Pages serves them with. SwiftBrowser's reusable
|
|
359
|
+
workflow `sb-ios-adhoc.yml` runs both ([docs/native.md](../../docs/native.md),
|
|
360
|
+
"Building and deploying from GitHub Actions").
|
|
361
|
+
|
|
324
362
|
## In the browser
|
|
325
363
|
|
|
326
364
|
```sh
|
|
@@ -349,24 +387,28 @@ the site: a `.symbolset` holding any SVG draws as a symbol there), and
|
|
|
349
387
|
`--worker <name>=<entry>` (repeatable) for the app's workers, published
|
|
350
388
|
beside the bundle for the page's `Worker`.
|
|
351
389
|
|
|
352
|
-
##
|
|
390
|
+
## Registered views
|
|
353
391
|
|
|
354
392
|
```tsx
|
|
355
|
-
import {
|
|
393
|
+
import { RegisteredView } from '@swiftbrowser/react';
|
|
356
394
|
|
|
357
|
-
<
|
|
395
|
+
<RegisteredView name="MapView" props={{ region }} onEvent={(value) => console.log('map said', value)}>
|
|
358
396
|
<Text>Children are SwiftUI views inside the native view</Text>
|
|
359
|
-
</
|
|
397
|
+
</RegisteredView>
|
|
360
398
|
```
|
|
361
399
|
|
|
362
400
|
The Swift app registers the view once:
|
|
363
401
|
|
|
364
402
|
```swift
|
|
365
|
-
|
|
403
|
+
ViewRegistry.register("MapView") { context in
|
|
366
404
|
MapView(region: context.props["region"]) { context.send(["tapped": true]) }
|
|
367
405
|
}
|
|
368
406
|
```
|
|
369
407
|
|
|
408
|
+
and a page can register its own element under the same name
|
|
409
|
+
(`ViewRegistry.register('MapView', (context) => ({ element, update, dispose }))`,
|
|
410
|
+
also from @swiftbrowser/react), so the same element draws on both.
|
|
411
|
+
|
|
370
412
|
Swift that is not a view registers as a function, before the bundle loads
|
|
371
413
|
(`swiftbrowser native init` writes an `ios/NativeFunctions.swift` for them):
|
|
372
414
|
|
|
@@ -10,11 +10,58 @@
|
|
|
10
10
|
// `--catalog <dir.xcassets>`, repeatable, for the app's named images). All
|
|
11
11
|
// three take `--worker <name>=<entry>`, repeatable: a worker bundle
|
|
12
12
|
// `<name>.worker.js` beside app.js, for `createWorker(name)`.
|
|
13
|
+
//
|
|
14
|
+
// For CI (src/ios.ts; the reusable workflow sb-ios-adhoc.yml runs them):
|
|
15
|
+
// `swiftbrowser-native ios-profile <bundle id> --profile <name> [--name <App>]`
|
|
16
|
+
// makes the ad hoc provisioning profile again with every registered device
|
|
17
|
+
// (ASC_KEY_ID, ASC_ISSUER_ID and ASC_KEY_P8 from the environment), and
|
|
18
|
+
// `swiftbrowser-native ios-install-page <App.ipa> --out <dir> --url <base>
|
|
19
|
+
// --bundle-id <id> --version <v> [--title <App>] [--web <url>] [--commit <sha>]`
|
|
20
|
+
// writes the over-the-air install page around an exported IPA.
|
|
13
21
|
|
|
14
22
|
import { basename, resolve } from 'node:path';
|
|
15
23
|
import { buildApp, serve, workerOutfile } from '../dist/build.js';
|
|
16
24
|
|
|
17
25
|
const [command, entry, ...rest] = process.argv.slice(2);
|
|
26
|
+
|
|
27
|
+
if (command === 'ios-profile' || command === 'ios-install-page') {
|
|
28
|
+
const values = {};
|
|
29
|
+
for (let i = 0; i < rest.length; i++) {
|
|
30
|
+
if (rest[i].startsWith('--')) values[rest[i].slice(2)] = rest[++i];
|
|
31
|
+
}
|
|
32
|
+
const ios = await import('../dist/ios.js');
|
|
33
|
+
const fail = (message) => {
|
|
34
|
+
console.error(message);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
};
|
|
37
|
+
if (command === 'ios-profile') {
|
|
38
|
+
if (!entry || !values.profile) fail('usage: swiftbrowser-native ios-profile <bundle id> --profile <name> [--name <App>]');
|
|
39
|
+
const env = (name) => process.env[name] || fail(`${name} is not set (the App Store Connect API key)`);
|
|
40
|
+
await ios.makeAdhocProfile({
|
|
41
|
+
bundleId: entry,
|
|
42
|
+
profileName: values.profile,
|
|
43
|
+
appName: values.name ?? values.profile,
|
|
44
|
+
keyId: env('ASC_KEY_ID'),
|
|
45
|
+
issuer: env('ASC_ISSUER_ID'),
|
|
46
|
+
key: env('ASC_KEY_P8'),
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
if (!entry || !values.out || !values.url || !values['bundle-id'] || !values.version) {
|
|
50
|
+
fail('usage: swiftbrowser-native ios-install-page <App.ipa> --out <dir> --url <base> --bundle-id <id> --version <v> [--title <App>] [--web <url>] [--commit <sha>]');
|
|
51
|
+
}
|
|
52
|
+
const files = ios.writeInstallPage(resolve(values.out), {
|
|
53
|
+
ipa: resolve(entry),
|
|
54
|
+
url: values.url,
|
|
55
|
+
bundleId: values['bundle-id'],
|
|
56
|
+
version: values.version,
|
|
57
|
+
...(values.title ? { title: values.title } : {}),
|
|
58
|
+
...(values.web ? { web: values.web } : {}),
|
|
59
|
+
...(values.commit ? { commit: values.commit } : {}),
|
|
60
|
+
});
|
|
61
|
+
console.log(`install page written to ${values.out}: ${files.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
18
65
|
const flags = { catalog: [], worker: [] };
|
|
19
66
|
for (let i = 0; i < rest.length; i++) {
|
|
20
67
|
const arg = rest[i];
|
package/dist/blob.d.ts
CHANGED
|
@@ -1,25 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Blob`, `File` and `FormData` for the native app: bytes with a type, a
|
|
3
|
-
* named one, and a form whose entries `fetch` sends as multipart. A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
3
|
+
* named one, and a form whose entries `fetch` sends as multipart. A blob is
|
|
4
|
+
* a list of parts, each bytes in memory or a range of a file on disk (a
|
|
5
|
+
* `LazyFile`, as the pickers and drops answer), so a `Blob` built from
|
|
6
|
+
* picked files' slices holds nothing in memory and `fetch` streams it from
|
|
7
|
+
* disk (fetch.ts). `URL.createObjectURL` gives a `data:` URL, which the
|
|
8
|
+
* host's image loader and `URLSession` both read, and `revokeObjectURL` has
|
|
9
|
+
* nothing to free.
|
|
7
10
|
*/
|
|
8
11
|
export type BlobPartLike = string | ArrayBuffer | ArrayBufferView | SimpleBlob;
|
|
12
|
+
/** What a blob, and so a request body, is made of: bytes here, or a range of a file on disk. */
|
|
13
|
+
export type BodyPart = {
|
|
14
|
+
bytes: Uint8Array;
|
|
15
|
+
} | {
|
|
16
|
+
file: LazyFile;
|
|
17
|
+
};
|
|
9
18
|
export interface BlobOptions {
|
|
10
19
|
type?: string;
|
|
11
20
|
}
|
|
12
21
|
export interface FileOptions extends BlobOptions {
|
|
13
22
|
lastModified?: number;
|
|
14
23
|
}
|
|
15
|
-
|
|
24
|
+
export declare const partSize: (part: BodyPart) => number;
|
|
25
|
+
/** Whether any part is a file on disk, which only an asynchronous read reaches. */
|
|
26
|
+
export declare const partsAreLazy: (parts: readonly BodyPart[]) => boolean;
|
|
27
|
+
/** The parts' bytes, whole: those on disk read through the files bridge. */
|
|
28
|
+
export declare function partsBytes(parts: readonly BodyPart[]): Promise<Uint8Array>;
|
|
29
|
+
/** The browser's `Blob`: parts (bytes, or files on disk) and a MIME type. */
|
|
16
30
|
export declare class SimpleBlob {
|
|
17
|
-
private readonly
|
|
31
|
+
private readonly parts_;
|
|
32
|
+
/** The parts joined, once asked for (a blob with nothing on disk). */
|
|
33
|
+
private joined;
|
|
18
34
|
readonly type: string;
|
|
19
35
|
constructor(parts?: Iterable<BlobPartLike>, options?: BlobOptions);
|
|
36
|
+
/** A blob straight from parts (a slice). */
|
|
37
|
+
protected static fromParts(parts: BodyPart[], type: string): SimpleBlob;
|
|
20
38
|
get size(): number;
|
|
21
|
-
/** The
|
|
39
|
+
/** The parts, in order: what a request body sends, each from memory or from disk. */
|
|
40
|
+
parts(): BodyPart[];
|
|
41
|
+
/** Whether any part is still on disk, so `bytes()` cannot answer and `bytesAsync()` reads it. */
|
|
42
|
+
get isLazy(): boolean;
|
|
43
|
+
/** The bytes (a view; callers must not write into it). Throws for a blob holding a file on disk: read it with `bytesAsync`. */
|
|
22
44
|
bytes(): Uint8Array;
|
|
45
|
+
/** The bytes, the parts on disk read now. */
|
|
46
|
+
bytesAsync(): Promise<Uint8Array>;
|
|
23
47
|
slice(start?: number, end?: number, contentType?: string): SimpleBlob;
|
|
24
48
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
25
49
|
text(): Promise<string>;
|
|
@@ -32,11 +56,49 @@ export declare class SimpleFile extends SimpleBlob {
|
|
|
32
56
|
constructor(parts: Iterable<BlobPartLike>, name: string, options?: FileOptions);
|
|
33
57
|
get [Symbol.toStringTag](): string;
|
|
34
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A file the viewer picked or dropped, still on disk: its name, type and size
|
|
61
|
+
* are known, its bytes are read through the host's files bridge only when
|
|
62
|
+
* asked (`arrayBuffer`, `bytesAsync`, or a `fetch` body), a range at a time
|
|
63
|
+
* for a `slice`, so a large file never sits in memory whole. `fileManager.write`
|
|
64
|
+
* copies one on disk instead of reading it. `bytes()` (synchronous) is not
|
|
65
|
+
* available: it throws, as the bytes are not here.
|
|
66
|
+
*/
|
|
67
|
+
export declare class LazyFile extends SimpleFile {
|
|
68
|
+
/** The `file:` URL the host reads it at. */
|
|
69
|
+
readonly url: string;
|
|
70
|
+
/** The range of the file on disk this stands for (`slice` narrows it). */
|
|
71
|
+
readonly start: number;
|
|
72
|
+
readonly end: number;
|
|
73
|
+
private readonly reader;
|
|
74
|
+
constructor(url: string, name: string, size: number, reader: (url: string, start: number, end: number) => Promise<Uint8Array>, options?: FileOptions, range?: {
|
|
75
|
+
start: number;
|
|
76
|
+
end: number;
|
|
77
|
+
});
|
|
78
|
+
/** Whether this is the whole file on disk, not a slice of it. */
|
|
79
|
+
get isWhole(): boolean;
|
|
80
|
+
private sizeOnDisk;
|
|
81
|
+
/** Sets the file's size on disk (the constructor's `size`); the range defaults to all of it. */
|
|
82
|
+
static make(url: string, name: string, size: number, reader: (url: string, start: number, end: number) => Promise<Uint8Array>, options?: FileOptions): LazyFile;
|
|
83
|
+
get size(): number;
|
|
84
|
+
parts(): BodyPart[];
|
|
85
|
+
get isLazy(): boolean;
|
|
86
|
+
bytes(): Uint8Array;
|
|
87
|
+
/** The bytes, read from disk now. */
|
|
88
|
+
bytesAsync(): Promise<Uint8Array>;
|
|
89
|
+
slice(start?: number, end?: number, contentType?: string): LazyFile;
|
|
90
|
+
/** The same file under another name (a form's `filename`), still on disk. */
|
|
91
|
+
renamed(name: string): LazyFile;
|
|
92
|
+
}
|
|
93
|
+
/** A blob's bytes, read from disk where it holds a `LazyFile`. */
|
|
94
|
+
export declare function blobBytes(blob: SimpleBlob): Promise<Uint8Array>;
|
|
35
95
|
export type FormDataEntryValueLike = string | SimpleFile;
|
|
36
96
|
/** The browser's `FormData`, as far as a request body needs it. */
|
|
37
97
|
export declare class SimpleFormData {
|
|
38
98
|
private entries_;
|
|
39
99
|
private static value;
|
|
100
|
+
/** Whether an entry is a picked file still on disk (or a blob holding one), which the form's body streams from there. */
|
|
101
|
+
get hasLazyFiles(): boolean;
|
|
40
102
|
append(name: string, value: unknown, filename?: string): void;
|
|
41
103
|
set(name: string, value: unknown, filename?: string): void;
|
|
42
104
|
get(name: string): FormDataEntryValueLike | null;
|
|
@@ -50,11 +112,23 @@ export declare class SimpleFormData {
|
|
|
50
112
|
[Symbol.iterator](): IterableIterator<[string, FormDataEntryValueLike]>;
|
|
51
113
|
get [Symbol.toStringTag](): string;
|
|
52
114
|
}
|
|
53
|
-
/**
|
|
54
|
-
export declare function
|
|
55
|
-
|
|
115
|
+
/** The boundary of a multipart body, unique by `random`'s bytes. */
|
|
116
|
+
export declare function multipartBoundary(random: (count: number) => Uint8Array): string;
|
|
117
|
+
/**
|
|
118
|
+
* A multipart/form-data body for the form, with the boundary `random` makes
|
|
119
|
+
* unique, as parts: the heads and the fields in memory, a file still on disk
|
|
120
|
+
* as the range of it (`fetch` streams those from disk, or reads them first
|
|
121
|
+
* for a host that cannot).
|
|
122
|
+
*/
|
|
123
|
+
export declare function encodeMultipart(form: SimpleFormData, random: (count: number) => Uint8Array, boundary?: string): {
|
|
124
|
+
parts: BodyPart[];
|
|
56
125
|
contentType: string;
|
|
57
126
|
};
|
|
58
|
-
/** `
|
|
127
|
+
/** `encodeMultipart` with the body whole: a form's files on disk are read first. */
|
|
128
|
+
export declare function encodeMultipartAsync(form: SimpleFormData, random: (count: number) => Uint8Array, boundary?: string): Promise<{
|
|
129
|
+
bytes: Uint8Array;
|
|
130
|
+
contentType: string;
|
|
131
|
+
}>;
|
|
132
|
+
/** `URL.createObjectURL`: a `data:` URL holding the blob; a picked file still on disk is its own `file:` URL. */
|
|
59
133
|
export declare function objectURL(blob: SimpleBlob): string;
|
|
60
134
|
//# sourceMappingURL=blob.d.ts.map
|
package/dist/blob.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../src/blob.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../src/blob.ts"],"names":[],"mappings":"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"}
|
package/dist/blob.js
CHANGED
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Blob`, `File` and `FormData` for the native app: bytes with a type, a
|
|
3
|
-
* named one, and a form whose entries `fetch` sends as multipart. A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
3
|
+
* named one, and a form whose entries `fetch` sends as multipart. A blob is
|
|
4
|
+
* a list of parts, each bytes in memory or a range of a file on disk (a
|
|
5
|
+
* `LazyFile`, as the pickers and drops answer), so a `Blob` built from
|
|
6
|
+
* picked files' slices holds nothing in memory and `fetch` streams it from
|
|
7
|
+
* disk (fetch.ts). `URL.createObjectURL` gives a `data:` URL, which the
|
|
8
|
+
* host's image loader and `URLSession` both read, and `revokeObjectURL` has
|
|
9
|
+
* nothing to free.
|
|
7
10
|
*/
|
|
8
11
|
import { base64Encode, utf8Decode, utf8Encode } from './encoding';
|
|
9
|
-
|
|
12
|
+
/** A constructor part as body parts: a blob's own parts are taken as they are (a file on disk stays there). */
|
|
13
|
+
function toParts(part) {
|
|
10
14
|
if (typeof part === 'string')
|
|
11
|
-
return utf8Encode(part);
|
|
15
|
+
return [{ bytes: utf8Encode(part) }];
|
|
12
16
|
if (part instanceof SimpleBlob)
|
|
13
|
-
return part.
|
|
17
|
+
return part.parts();
|
|
14
18
|
if (part instanceof ArrayBuffer)
|
|
15
|
-
return new Uint8Array(part.slice(0));
|
|
19
|
+
return [{ bytes: new Uint8Array(part.slice(0)) }];
|
|
16
20
|
if (ArrayBuffer.isView(part))
|
|
17
|
-
return new Uint8Array(part.buffer.slice(part.byteOffset, part.byteOffset + part.byteLength));
|
|
18
|
-
return utf8Encode(String(part));
|
|
21
|
+
return [{ bytes: new Uint8Array(part.buffer.slice(part.byteOffset, part.byteOffset + part.byteLength)) }];
|
|
22
|
+
return [{ bytes: utf8Encode(String(part)) }];
|
|
23
|
+
}
|
|
24
|
+
export const partSize = (part) => ('bytes' in part ? part.bytes.byteLength : part.file.size);
|
|
25
|
+
/** Whether any part is a file on disk, which only an asynchronous read reaches. */
|
|
26
|
+
export const partsAreLazy = (parts) => parts.some((part) => 'file' in part);
|
|
27
|
+
/** The parts' bytes, whole: those on disk read through the files bridge. */
|
|
28
|
+
export async function partsBytes(parts) {
|
|
29
|
+
const read = [];
|
|
30
|
+
for (const part of parts)
|
|
31
|
+
read.push('bytes' in part ? part.bytes : await part.file.bytesAsync());
|
|
32
|
+
return concat(read);
|
|
19
33
|
}
|
|
20
34
|
function concat(parts) {
|
|
21
35
|
const total = parts.reduce((sum, part) => sum + part.byteLength, 0);
|
|
@@ -27,33 +41,71 @@ function concat(parts) {
|
|
|
27
41
|
}
|
|
28
42
|
return out;
|
|
29
43
|
}
|
|
30
|
-
/** The browser's `Blob`: bytes and a MIME type. */
|
|
44
|
+
/** The browser's `Blob`: parts (bytes, or files on disk) and a MIME type. */
|
|
31
45
|
export class SimpleBlob {
|
|
32
|
-
|
|
46
|
+
parts_;
|
|
47
|
+
/** The parts joined, once asked for (a blob with nothing on disk). */
|
|
48
|
+
joined = null;
|
|
33
49
|
type;
|
|
34
50
|
constructor(parts = [], options = {}) {
|
|
35
|
-
this.
|
|
51
|
+
this.parts_ = [...parts].flatMap(toParts);
|
|
36
52
|
// As the browser does: lowercased, and empty when it is not printable ASCII.
|
|
37
53
|
const type = options.type ?? '';
|
|
38
54
|
this.type = /^[\x20-\x7e]*$/.test(type) ? type.toLowerCase() : '';
|
|
39
55
|
}
|
|
56
|
+
/** A blob straight from parts (a slice). */
|
|
57
|
+
static fromParts(parts, type) {
|
|
58
|
+
const blob = new SimpleBlob([], { type });
|
|
59
|
+
blob.parts_.push(...parts);
|
|
60
|
+
return blob;
|
|
61
|
+
}
|
|
40
62
|
get size() {
|
|
41
|
-
return this.
|
|
63
|
+
return this.parts_.reduce((sum, part) => sum + partSize(part), 0);
|
|
64
|
+
}
|
|
65
|
+
/** The parts, in order: what a request body sends, each from memory or from disk. */
|
|
66
|
+
parts() {
|
|
67
|
+
return [...this.parts_];
|
|
68
|
+
}
|
|
69
|
+
/** Whether any part is still on disk, so `bytes()` cannot answer and `bytesAsync()` reads it. */
|
|
70
|
+
get isLazy() {
|
|
71
|
+
return partsAreLazy(this.parts_);
|
|
42
72
|
}
|
|
43
|
-
/** The bytes (a view; callers must not write into it). */
|
|
73
|
+
/** The bytes (a view; callers must not write into it). Throws for a blob holding a file on disk: read it with `bytesAsync`. */
|
|
44
74
|
bytes() {
|
|
45
|
-
|
|
75
|
+
if (this.isLazy)
|
|
76
|
+
throw new Error('unsupported: the blob holds a picked file, which is read asynchronously (arrayBuffer); it is not in memory');
|
|
77
|
+
if (!this.joined)
|
|
78
|
+
this.joined = this.parts_.length === 1 && 'bytes' in this.parts_[0] ? this.parts_[0].bytes : concat(this.parts_.map((part) => ('bytes' in part ? part.bytes : new Uint8Array(0))));
|
|
79
|
+
return this.joined;
|
|
80
|
+
}
|
|
81
|
+
/** The bytes, the parts on disk read now. */
|
|
82
|
+
async bytesAsync() {
|
|
83
|
+
return this.isLazy ? partsBytes(this.parts_) : this.bytes();
|
|
46
84
|
}
|
|
47
85
|
slice(start = 0, end = this.size, contentType = '') {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
86
|
+
const size = this.size;
|
|
87
|
+
const from = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
|
|
88
|
+
const to = Math.max(from, end < 0 ? Math.max(size + end, 0) : Math.min(end, size));
|
|
89
|
+
const out = [];
|
|
90
|
+
let at = 0;
|
|
91
|
+
for (const part of this.parts_) {
|
|
92
|
+
const length = partSize(part);
|
|
93
|
+
const partStart = at;
|
|
94
|
+
at += length;
|
|
95
|
+
if (at <= from || partStart >= to)
|
|
96
|
+
continue;
|
|
97
|
+
const a = Math.max(from, partStart) - partStart;
|
|
98
|
+
const b = Math.min(to, at) - partStart;
|
|
99
|
+
out.push('bytes' in part ? { bytes: part.bytes.subarray(a, b) } : { file: part.file.slice(a, b) });
|
|
100
|
+
}
|
|
101
|
+
return SimpleBlob.fromParts(out, contentType);
|
|
51
102
|
}
|
|
52
103
|
async arrayBuffer() {
|
|
53
|
-
|
|
104
|
+
const bytes = await this.bytesAsync();
|
|
105
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
54
106
|
}
|
|
55
107
|
async text() {
|
|
56
|
-
return utf8Decode(this.
|
|
108
|
+
return utf8Decode(await this.bytesAsync());
|
|
57
109
|
}
|
|
58
110
|
get [Symbol.toStringTag]() {
|
|
59
111
|
return 'Blob';
|
|
@@ -72,16 +124,93 @@ export class SimpleFile extends SimpleBlob {
|
|
|
72
124
|
return 'File';
|
|
73
125
|
}
|
|
74
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* A file the viewer picked or dropped, still on disk: its name, type and size
|
|
129
|
+
* are known, its bytes are read through the host's files bridge only when
|
|
130
|
+
* asked (`arrayBuffer`, `bytesAsync`, or a `fetch` body), a range at a time
|
|
131
|
+
* for a `slice`, so a large file never sits in memory whole. `fileManager.write`
|
|
132
|
+
* copies one on disk instead of reading it. `bytes()` (synchronous) is not
|
|
133
|
+
* available: it throws, as the bytes are not here.
|
|
134
|
+
*/
|
|
135
|
+
export class LazyFile extends SimpleFile {
|
|
136
|
+
/** The `file:` URL the host reads it at. */
|
|
137
|
+
url;
|
|
138
|
+
/** The range of the file on disk this stands for (`slice` narrows it). */
|
|
139
|
+
start;
|
|
140
|
+
end;
|
|
141
|
+
reader;
|
|
142
|
+
constructor(url, name, size, reader, options = {}, range = { start: 0, end: size }) {
|
|
143
|
+
super([], name, options);
|
|
144
|
+
this.url = url;
|
|
145
|
+
this.reader = reader;
|
|
146
|
+
this.start = range.start;
|
|
147
|
+
this.end = range.end;
|
|
148
|
+
}
|
|
149
|
+
/** Whether this is the whole file on disk, not a slice of it. */
|
|
150
|
+
get isWhole() {
|
|
151
|
+
return this.start === 0 && this.end === this.sizeOnDisk;
|
|
152
|
+
}
|
|
153
|
+
sizeOnDisk = 0;
|
|
154
|
+
/** Sets the file's size on disk (the constructor's `size`); the range defaults to all of it. */
|
|
155
|
+
static make(url, name, size, reader, options = {}) {
|
|
156
|
+
const file = new LazyFile(url, name, size, reader, options);
|
|
157
|
+
file.sizeOnDisk = size;
|
|
158
|
+
return file;
|
|
159
|
+
}
|
|
160
|
+
get size() {
|
|
161
|
+
return this.end - this.start;
|
|
162
|
+
}
|
|
163
|
+
parts() {
|
|
164
|
+
return [{ file: this }];
|
|
165
|
+
}
|
|
166
|
+
get isLazy() {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
bytes() {
|
|
170
|
+
throw new Error('unsupported: a picked file is read asynchronously (arrayBuffer); it is not in memory');
|
|
171
|
+
}
|
|
172
|
+
/** The bytes, read from disk now. */
|
|
173
|
+
async bytesAsync() {
|
|
174
|
+
return this.reader(this.url, this.start, this.end);
|
|
175
|
+
}
|
|
176
|
+
slice(start = 0, end = this.size, contentType = '') {
|
|
177
|
+
const size = this.size;
|
|
178
|
+
const from = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
|
|
179
|
+
const to = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
|
|
180
|
+
const sliced = new LazyFile(this.url, this.name, this.sizeOnDisk, this.reader, { type: contentType, lastModified: this.lastModified }, {
|
|
181
|
+
start: this.start + from,
|
|
182
|
+
end: this.start + Math.max(from, to),
|
|
183
|
+
});
|
|
184
|
+
sliced.sizeOnDisk = this.sizeOnDisk;
|
|
185
|
+
return sliced;
|
|
186
|
+
}
|
|
187
|
+
/** The same file under another name (a form's `filename`), still on disk. */
|
|
188
|
+
renamed(name) {
|
|
189
|
+
const copy = new LazyFile(this.url, name, this.sizeOnDisk, this.reader, { type: this.type, lastModified: this.lastModified }, { start: this.start, end: this.end });
|
|
190
|
+
copy.sizeOnDisk = this.sizeOnDisk;
|
|
191
|
+
return copy;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** A blob's bytes, read from disk where it holds a `LazyFile`. */
|
|
195
|
+
export async function blobBytes(blob) {
|
|
196
|
+
return blob.bytesAsync();
|
|
197
|
+
}
|
|
75
198
|
/** The browser's `FormData`, as far as a request body needs it. */
|
|
76
199
|
export class SimpleFormData {
|
|
77
200
|
entries_ = [];
|
|
78
201
|
static value(value, filename) {
|
|
79
202
|
if (value instanceof SimpleBlob) {
|
|
80
203
|
const name = filename ?? (value instanceof SimpleFile ? value.name : 'blob');
|
|
204
|
+
if (value instanceof LazyFile)
|
|
205
|
+
return filename === undefined ? value : value.renamed(filename);
|
|
81
206
|
return value instanceof SimpleFile && filename === undefined ? value : new SimpleFile([value], name, { type: value.type });
|
|
82
207
|
}
|
|
83
208
|
return String(value);
|
|
84
209
|
}
|
|
210
|
+
/** Whether an entry is a picked file still on disk (or a blob holding one), which the form's body streams from there. */
|
|
211
|
+
get hasLazyFiles() {
|
|
212
|
+
return this.entries_.some(([, value]) => typeof value !== 'string' && value.isLazy);
|
|
213
|
+
}
|
|
85
214
|
append(name, value, filename) {
|
|
86
215
|
this.entries_.push([String(name), SimpleFormData.value(value, filename)]);
|
|
87
216
|
}
|
|
@@ -130,24 +259,45 @@ export class SimpleFormData {
|
|
|
130
259
|
return 'FormData';
|
|
131
260
|
}
|
|
132
261
|
}
|
|
133
|
-
/**
|
|
134
|
-
export function
|
|
135
|
-
|
|
262
|
+
/** The boundary of a multipart body, unique by `random`'s bytes. */
|
|
263
|
+
export function multipartBoundary(random) {
|
|
264
|
+
return `----SwiftBrowserFormBoundary${Array.from(random(16), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
265
|
+
}
|
|
266
|
+
function multipartHead(boundary, name, value) {
|
|
136
267
|
const escape = (text) => text.replace(/\r/g, '%0D').replace(/\n/g, '%0A').replace(/"/g, '%22');
|
|
268
|
+
const head = [`--${boundary}`, `Content-Disposition: form-data; name="${escape(name)}"` + (typeof value === 'string' ? '' : `; filename="${escape(value.name)}"`)];
|
|
269
|
+
if (typeof value !== 'string')
|
|
270
|
+
head.push(`Content-Type: ${value.type || 'application/octet-stream'}`);
|
|
271
|
+
return utf8Encode(`${head.join('\r\n')}\r\n\r\n`);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* A multipart/form-data body for the form, with the boundary `random` makes
|
|
275
|
+
* unique, as parts: the heads and the fields in memory, a file still on disk
|
|
276
|
+
* as the range of it (`fetch` streams those from disk, or reads them first
|
|
277
|
+
* for a host that cannot).
|
|
278
|
+
*/
|
|
279
|
+
export function encodeMultipart(form, random, boundary = multipartBoundary(random)) {
|
|
137
280
|
const parts = [];
|
|
138
281
|
for (const [name, value] of form.entries()) {
|
|
139
|
-
|
|
140
|
-
if (typeof value
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
parts.push(utf8Encode('\r\n'));
|
|
145
|
-
}
|
|
146
|
-
parts.push(utf8Encode(`--${boundary}--\r\n`));
|
|
147
|
-
return {
|
|
282
|
+
parts.push({ bytes: multipartHead(boundary, name, value) });
|
|
283
|
+
if (typeof value === 'string')
|
|
284
|
+
parts.push({ bytes: utf8Encode(value) });
|
|
285
|
+
else
|
|
286
|
+
parts.push(...value.parts());
|
|
287
|
+
parts.push({ bytes: utf8Encode('\r\n') });
|
|
288
|
+
}
|
|
289
|
+
parts.push({ bytes: utf8Encode(`--${boundary}--\r\n`) });
|
|
290
|
+
return { parts, contentType: `multipart/form-data; boundary=${boundary}` };
|
|
291
|
+
}
|
|
292
|
+
/** `encodeMultipart` with the body whole: a form's files on disk are read first. */
|
|
293
|
+
export async function encodeMultipartAsync(form, random, boundary = multipartBoundary(random)) {
|
|
294
|
+
const { parts, contentType } = encodeMultipart(form, random, boundary);
|
|
295
|
+
return { bytes: await partsBytes(parts), contentType };
|
|
148
296
|
}
|
|
149
|
-
/** `URL.createObjectURL`: a `data:` URL holding the blob. */
|
|
297
|
+
/** `URL.createObjectURL`: a `data:` URL holding the blob; a picked file still on disk is its own `file:` URL. */
|
|
150
298
|
export function objectURL(blob) {
|
|
299
|
+
if (blob instanceof LazyFile && blob.isWhole)
|
|
300
|
+
return blob.url;
|
|
151
301
|
return `data:${blob.type || 'application/octet-stream'};base64,${base64Encode(blob.bytes())}`;
|
|
152
302
|
}
|
|
153
303
|
//# sourceMappingURL=blob.js.map
|