@swiftbrowser/native 0.17.6 → 0.17.8

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 (81) hide show
  1. package/README.md +101 -2
  2. package/dist/blob.d.ts +60 -0
  3. package/dist/blob.d.ts.map +1 -0
  4. package/dist/blob.js +153 -0
  5. package/dist/blob.js.map +1 -0
  6. package/dist/bridge.d.ts +47 -1
  7. package/dist/bridge.d.ts.map +1 -1
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/fetch.d.ts +11 -3
  10. package/dist/fetch.d.ts.map +1 -1
  11. package/dist/fetch.js +26 -2
  12. package/dist/fetch.js.map +1 -1
  13. package/dist/hostRequests.d.ts +49 -0
  14. package/dist/hostRequests.d.ts.map +1 -0
  15. package/dist/hostRequests.js +134 -0
  16. package/dist/hostRequests.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/matchMedia.d.ts +43 -0
  22. package/dist/matchMedia.d.ts.map +1 -0
  23. package/dist/matchMedia.js +63 -0
  24. package/dist/matchMedia.js.map +1 -0
  25. package/dist/mediaSession.d.ts +59 -0
  26. package/dist/mediaSession.d.ts.map +1 -0
  27. package/dist/mediaSession.js +119 -0
  28. package/dist/mediaSession.js.map +1 -0
  29. package/dist/notifications.d.ts +108 -0
  30. package/dist/notifications.d.ts.map +1 -0
  31. package/dist/notifications.js +251 -0
  32. package/dist/notifications.js.map +1 -0
  33. package/dist/run.d.ts.map +1 -1
  34. package/dist/run.js +9 -1
  35. package/dist/run.js.map +1 -1
  36. package/dist/runtime.d.ts +3 -1
  37. package/dist/runtime.d.ts.map +1 -1
  38. package/dist/runtime.js +72 -3
  39. package/dist/runtime.js.map +1 -1
  40. package/dist/secureStorage.d.ts +20 -0
  41. package/dist/secureStorage.d.ts.map +1 -0
  42. package/dist/secureStorage.js +28 -0
  43. package/dist/secureStorage.js.map +1 -0
  44. package/dist/subtle.d.ts +38 -0
  45. package/dist/subtle.d.ts.map +1 -0
  46. package/dist/subtle.js +72 -0
  47. package/dist/subtle.js.map +1 -0
  48. package/dist/webSocket.d.ts +73 -0
  49. package/dist/webSocket.d.ts.map +1 -0
  50. package/dist/webSocket.js +145 -0
  51. package/dist/webSocket.js.map +1 -0
  52. package/package.json +3 -3
  53. package/src/blob.ts +186 -0
  54. package/src/bridge.ts +57 -1
  55. package/src/fetch.ts +29 -3
  56. package/src/hostRequests.ts +148 -0
  57. package/src/index.ts +8 -0
  58. package/src/matchMedia.ts +79 -0
  59. package/src/mediaSession.ts +163 -0
  60. package/src/notifications.ts +317 -0
  61. package/src/run.ts +10 -1
  62. package/src/runtime.ts +73 -3
  63. package/src/secureStorage.ts +39 -0
  64. package/src/subtle.ts +87 -0
  65. package/src/webSocket.ts +175 -0
  66. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +180 -0
  67. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +437 -0
  68. package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
  69. package/swift/Sources/SwiftBrowserNative/Host/NowPlayingBridge.swift +190 -0
  70. package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
  71. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +3 -0
  72. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
  73. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +26 -0
  74. package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
  75. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +2 -0
  76. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +22 -9
  77. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +8 -5
  78. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +304 -0
  79. package/swift/Sources/SwiftBrowserProtocol/Props.swift +43 -0
  80. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +12 -2
  81. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `secureStorage`: `localStorage`'s shape over the Keychain, for what must
3
+ * not sit in plain text (a session token). The host's `secureGet`,
4
+ * `secureSet` and `secureRemove` are generic-password items of the app's
5
+ * (`kSecAttrAccessibleAfterFirstUnlock`, so a background fetch can read
6
+ * them). In the page (the round trip, or a browser) there is no Keychain:
7
+ * the values go to `localStorage` under a prefix, as a web app keeps them.
8
+ */
9
+ import { type NativeBridge } from './bridge';
10
+ export type SecureStorageBridge = Pick<NativeBridge, 'secureGet' | 'secureSet' | 'secureRemove'>;
11
+ export interface SecureStorage {
12
+ getItem(key: string): string | null;
13
+ setItem(key: string, value: string): void;
14
+ removeItem(key: string): void;
15
+ }
16
+ /** Secure storage over `native`, or over `localStorage` when the host has no Keychain. */
17
+ export declare function createSecureStorage(native: SecureStorageBridge | undefined, fallback: () => Storage | undefined): SecureStorage;
18
+ /** The app's secure storage: the Keychain in the native app, `localStorage` in the page. */
19
+ export declare const secureStorage: SecureStorage;
20
+ //# sourceMappingURL=secureStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secureStorage.d.ts","sourceRoot":"","sources":["../src/secureStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAE3D,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,YAAY,EAAE,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC,CAAC;AAEjG,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAID,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,GAAG,SAAS,GAAG,aAAa,CAa/H;AAED,4FAA4F;AAC5F,eAAO,MAAM,aAAa,EAAE,aAA2H,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `secureStorage`: `localStorage`'s shape over the Keychain, for what must
3
+ * not sit in plain text (a session token). The host's `secureGet`,
4
+ * `secureSet` and `secureRemove` are generic-password items of the app's
5
+ * (`kSecAttrAccessibleAfterFirstUnlock`, so a background fetch can read
6
+ * them). In the page (the round trip, or a browser) there is no Keychain:
7
+ * the values go to `localStorage` under a prefix, as a web app keeps them.
8
+ */
9
+ import { nativeBridge } from './bridge';
10
+ const PREFIX = 'swiftbrowser.secure.';
11
+ /** Secure storage over `native`, or over `localStorage` when the host has no Keychain. */
12
+ export function createSecureStorage(native, fallback) {
13
+ if (native?.secureGet && native.secureSet && native.secureRemove) {
14
+ return {
15
+ getItem: (key) => native.secureGet(String(key)),
16
+ setItem: (key, value) => native.secureSet(String(key), String(value)),
17
+ removeItem: (key) => native.secureRemove(String(key)),
18
+ };
19
+ }
20
+ return {
21
+ getItem: (key) => fallback()?.getItem(PREFIX + String(key)) ?? null,
22
+ setItem: (key, value) => fallback()?.setItem(PREFIX + String(key), String(value)),
23
+ removeItem: (key) => fallback()?.removeItem(PREFIX + String(key)),
24
+ };
25
+ }
26
+ /** The app's secure storage: the Keychain in the native app, `localStorage` in the page. */
27
+ export const secureStorage = createSecureStorage(nativeBridge(), () => (typeof localStorage === 'undefined' ? undefined : localStorage));
28
+ //# sourceMappingURL=secureStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secureStorage.js","sourceRoot":"","sources":["../src/secureStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAqB,MAAM,UAAU,CAAC;AAU3D,MAAM,MAAM,GAAG,sBAAsB,CAAC;AAEtC,0FAA0F;AAC1F,MAAM,UAAU,mBAAmB,CAAC,MAAuC,EAAE,QAAmC;IAC9G,IAAI,MAAM,EAAE,SAAS,IAAI,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACjE,OAAO;YACL,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,SAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChD,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,SAAU,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,YAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;SACvD,CAAC;IACJ,CAAC;IACD,OAAO;QACL,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI;QACnE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QACjF,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,EAAE,EAAE,UAAU,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;KAClE,CAAC;AACJ,CAAC;AAED,4FAA4F;AAC5F,MAAM,CAAC,MAAM,aAAa,GAAkB,mBAAmB,CAAC,YAAY,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,OAAO,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `crypto.subtle` for the native app, as far as apps use it for a PKCE
3
+ * challenge or a signature: `digest` (SHA-1, SHA-256, SHA-384, SHA-512) and
4
+ * HMAC keys with `importKey`, `sign` and `verify`, over the bridge's
5
+ * `digest` and `hmac` (CryptoKit on the Swift side). Both are synchronous
6
+ * there; the promises resolve at once.
7
+ */
8
+ import type { NativeBridge } from './bridge';
9
+ export type SubtleBridge = Pick<NativeBridge, 'digest' | 'hmac'>;
10
+ type Algorithm = string | {
11
+ name: string;
12
+ hash?: string | {
13
+ name: string;
14
+ };
15
+ };
16
+ interface HmacKey {
17
+ type: 'secret';
18
+ extractable: boolean;
19
+ algorithm: {
20
+ name: 'HMAC';
21
+ hash: {
22
+ name: string;
23
+ };
24
+ length: number;
25
+ };
26
+ usages: string[];
27
+ /** The key's bytes, kept here: the host signs with them each time. */
28
+ readonly bytes: Uint8Array;
29
+ }
30
+ /** `crypto.subtle` over the bridge. */
31
+ export declare function createSubtleCrypto(native: SubtleBridge): {
32
+ digest(algorithm: Algorithm, data: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;
33
+ importKey(format: string, keyData: ArrayBuffer | ArrayBufferView, algorithm: Algorithm, extractable: boolean, usages: string[]): Promise<HmacKey>;
34
+ sign(algorithm: Algorithm, key: HmacKey, data: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;
35
+ verify(algorithm: Algorithm, key: HmacKey, signature: ArrayBuffer | ArrayBufferView, data: ArrayBuffer | ArrayBufferView): Promise<boolean>;
36
+ };
37
+ export {};
38
+ //# sourceMappingURL=subtle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subtle.d.ts","sourceRoot":"","sources":["../src/subtle.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,MAAM,CAAC,CAAC;AAEjE,KAAK,SAAS,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE7E,UAAU,OAAO;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AA6BD,uCAAuC;AACvC,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY;sBAE3B,SAAS,QAAQ,WAAW,GAAG,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;sBAQrE,MAAM,WAAW,WAAW,GAAG,eAAe,aAAa,SAAS,eAAe,OAAO,UAAU,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;oBAQjI,SAAS,OAAO,OAAO,QAAQ,WAAW,GAAG,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;sBAOjF,SAAS,OAAO,OAAO,aAAa,WAAW,GAAG,eAAe,QAAQ,WAAW,GAAG,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;EASpJ"}
package/dist/subtle.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `crypto.subtle` for the native app, as far as apps use it for a PKCE
3
+ * challenge or a signature: `digest` (SHA-1, SHA-256, SHA-384, SHA-512) and
4
+ * HMAC keys with `importKey`, `sign` and `verify`, over the bridge's
5
+ * `digest` and `hmac` (CryptoKit on the Swift side). Both are synchronous
6
+ * there; the promises resolve at once.
7
+ */
8
+ import { base64Decode, base64Encode } from './encoding';
9
+ const DIGESTS = new Set(['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']);
10
+ function algorithmName(algorithm) {
11
+ return (typeof algorithm === 'string' ? algorithm : algorithm.name).toUpperCase();
12
+ }
13
+ function hashName(algorithm) {
14
+ if (typeof algorithm === 'string')
15
+ return algorithm.toUpperCase();
16
+ const hash = algorithm.hash;
17
+ return (typeof hash === 'string' ? hash : hash?.name ?? '').toUpperCase();
18
+ }
19
+ function bytesOf(data) {
20
+ return data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
21
+ }
22
+ function toBuffer(base64) {
23
+ const bytes = base64Decode(base64);
24
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
25
+ }
26
+ function notSupported(what) {
27
+ const error = new Error(`crypto.subtle: ${what} is not supported in the native app`);
28
+ error.name = 'NotSupportedError';
29
+ return error;
30
+ }
31
+ /** `crypto.subtle` over the bridge. */
32
+ export function createSubtleCrypto(native) {
33
+ return {
34
+ async digest(algorithm, data) {
35
+ const name = algorithmName(algorithm);
36
+ if (!DIGESTS.has(name))
37
+ throw notSupported(`the ${name} digest`);
38
+ const result = native.digest?.(name, base64Encode(bytesOf(data)));
39
+ if (!result)
40
+ throw notSupported(`the ${name} digest`);
41
+ return toBuffer(result);
42
+ },
43
+ async importKey(format, keyData, algorithm, extractable, usages) {
44
+ if (format !== 'raw' || algorithmName(algorithm) !== 'HMAC')
45
+ throw notSupported(`importing a ${format} ${algorithmName(algorithm)} key`);
46
+ const hash = hashName(algorithm);
47
+ if (!DIGESTS.has(hash))
48
+ throw notSupported(`HMAC with ${hash}`);
49
+ const bytes = bytesOf(keyData);
50
+ return { type: 'secret', extractable, algorithm: { name: 'HMAC', hash: { name: hash }, length: bytes.byteLength * 8 }, usages: [...usages], bytes };
51
+ },
52
+ async sign(algorithm, key, data) {
53
+ if (algorithmName(algorithm) !== 'HMAC' || key.algorithm?.name !== 'HMAC')
54
+ throw notSupported(`signing with ${algorithmName(algorithm)}`);
55
+ const result = native.hmac?.(key.algorithm.hash.name, base64Encode(key.bytes), base64Encode(bytesOf(data)));
56
+ if (!result)
57
+ throw notSupported('HMAC');
58
+ return toBuffer(result);
59
+ },
60
+ async verify(algorithm, key, signature, data) {
61
+ const expected = new Uint8Array(await this.sign(algorithm, key, data));
62
+ const given = bytesOf(signature);
63
+ if (expected.byteLength !== given.byteLength)
64
+ return false;
65
+ let difference = 0;
66
+ for (let i = 0; i < expected.byteLength; i += 1)
67
+ difference |= expected[i] ^ given[i];
68
+ return difference === 0;
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=subtle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subtle.js","sourceRoot":"","sources":["../src/subtle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `WebSocket` for the native app over the bridge's `wsOpen`, `wsSend` and
3
+ * `wsClose` (`URLSessionWebSocketTask` on the Swift side). What the socket
4
+ * receives arrives at `__sbWebSocketEvent(id, json)`: `{ type: "open",
5
+ * protocol }`, `{ type: "message", text }` or `{ type: "message", binary }`
6
+ * (base64), `{ type: "error", message }` and `{ type: "close", code, reason,
7
+ * wasClean }`. The browser's event model as apps use it: `onopen`,
8
+ * `onmessage`, `onerror`, `onclose`, `addEventListener`, `readyState`,
9
+ * `binaryType` (`blob` by default, or `arraybuffer`).
10
+ */
11
+ import { SimpleBlob } from './blob';
12
+ import type { NativeBridge } from './bridge';
13
+ export interface WebSocketEventLike {
14
+ type: string;
15
+ target: NativeWebSocket;
16
+ data?: unknown;
17
+ code?: number;
18
+ reason?: string;
19
+ wasClean?: boolean;
20
+ message?: string;
21
+ }
22
+ type Handler = (event: WebSocketEventLike) => void;
23
+ /** The bridge's event for socket `id`. */
24
+ export interface NativeWebSocketEvent {
25
+ type: 'open' | 'message' | 'error' | 'close';
26
+ protocol?: string;
27
+ text?: string;
28
+ /** Base64. */
29
+ binary?: string;
30
+ message?: string;
31
+ code?: number;
32
+ reason?: string;
33
+ wasClean?: boolean;
34
+ }
35
+ export type WebSocketBridge = Required<Pick<NativeBridge, 'wsOpen' | 'wsSend' | 'wsClose'>>;
36
+ export declare class NativeWebSocket {
37
+ static readonly CONNECTING = 0;
38
+ static readonly OPEN = 1;
39
+ static readonly CLOSING = 2;
40
+ static readonly CLOSED = 3;
41
+ readonly CONNECTING = 0;
42
+ readonly OPEN = 1;
43
+ readonly CLOSING = 2;
44
+ readonly CLOSED = 3;
45
+ readonly url: string;
46
+ readyState: number;
47
+ protocol: string;
48
+ extensions: string;
49
+ bufferedAmount: number;
50
+ binaryType: 'blob' | 'arraybuffer';
51
+ onopen: Handler | null;
52
+ onmessage: Handler | null;
53
+ onerror: Handler | null;
54
+ onclose: Handler | null;
55
+ private readonly id;
56
+ private readonly listeners;
57
+ constructor(url: string | {
58
+ href: string;
59
+ }, protocols?: string | string[]);
60
+ send(data: string | ArrayBuffer | ArrayBufferView | SimpleBlob): void;
61
+ close(code?: number, reason?: string): void;
62
+ addEventListener(type: string, handler: Handler): void;
63
+ removeEventListener(type: string, handler: Handler): void;
64
+ dispatchEvent(event: WebSocketEventLike): boolean;
65
+ /** The bridge's event for this socket. */
66
+ receive(event: NativeWebSocketEvent): void;
67
+ /** The bridge's `__sbWebSocketEvent(id, json)`. */
68
+ static receive(id: number, json: string | NativeWebSocketEvent): void;
69
+ /** Points the sockets at the bridge (the runtime, once per install). */
70
+ static install(native: WebSocketBridge): void;
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=webSocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSocket.d.ts","sourceRoot":"","sources":["../src/webSocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `WebSocket` for the native app over the bridge's `wsOpen`, `wsSend` and
3
+ * `wsClose` (`URLSessionWebSocketTask` on the Swift side). What the socket
4
+ * receives arrives at `__sbWebSocketEvent(id, json)`: `{ type: "open",
5
+ * protocol }`, `{ type: "message", text }` or `{ type: "message", binary }`
6
+ * (base64), `{ type: "error", message }` and `{ type: "close", code, reason,
7
+ * wasClean }`. The browser's event model as apps use it: `onopen`,
8
+ * `onmessage`, `onerror`, `onclose`, `addEventListener`, `readyState`,
9
+ * `binaryType` (`blob` by default, or `arraybuffer`).
10
+ */
11
+ import { SimpleBlob } from './blob';
12
+ import { base64Decode, base64Encode } from './encoding';
13
+ const sockets = new Map();
14
+ let nextId = 1;
15
+ let bridge = null;
16
+ export class NativeWebSocket {
17
+ static CONNECTING = 0;
18
+ static OPEN = 1;
19
+ static CLOSING = 2;
20
+ static CLOSED = 3;
21
+ CONNECTING = 0;
22
+ OPEN = 1;
23
+ CLOSING = 2;
24
+ CLOSED = 3;
25
+ url;
26
+ readyState = 0;
27
+ protocol = '';
28
+ extensions = '';
29
+ bufferedAmount = 0;
30
+ binaryType = 'blob';
31
+ onopen = null;
32
+ onmessage = null;
33
+ onerror = null;
34
+ onclose = null;
35
+ id;
36
+ listeners = new Map();
37
+ constructor(url, protocols = []) {
38
+ if (!bridge)
39
+ throw new TypeError('WebSocket: no native bridge');
40
+ const href = typeof url === 'string' ? url : url.href;
41
+ if (!/^wss?:\/\//i.test(href))
42
+ throw new SyntaxError(`WebSocket: the URL's scheme must be ws or wss: ${href}`);
43
+ this.url = href;
44
+ this.id = nextId++;
45
+ sockets.set(this.id, this);
46
+ bridge.wsOpen(this.id, href, typeof protocols === 'string' ? [protocols] : [...protocols]);
47
+ }
48
+ send(data) {
49
+ if (this.readyState === NativeWebSocket.CONNECTING)
50
+ throw new DOMExceptionLike('InvalidStateError', 'WebSocket: still connecting');
51
+ if (this.readyState !== NativeWebSocket.OPEN)
52
+ return;
53
+ if (typeof data === 'string') {
54
+ bridge?.wsSend(this.id, JSON.stringify({ text: data }));
55
+ return;
56
+ }
57
+ const bytes = data instanceof SimpleBlob ? data.bytes() : data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
58
+ bridge?.wsSend(this.id, JSON.stringify({ binary: base64Encode(bytes) }));
59
+ }
60
+ close(code, reason) {
61
+ if (code !== undefined && code !== 1000 && !(code >= 3000 && code <= 4999))
62
+ throw new DOMExceptionLike('InvalidAccessError', 'WebSocket: the close code must be 1000 or in 3000–4999');
63
+ if (this.readyState === NativeWebSocket.CLOSING || this.readyState === NativeWebSocket.CLOSED)
64
+ return;
65
+ this.readyState = NativeWebSocket.CLOSING;
66
+ bridge?.wsClose(this.id, code ?? 1000, reason ?? '');
67
+ }
68
+ addEventListener(type, handler) {
69
+ let set = this.listeners.get(type);
70
+ if (!set) {
71
+ set = new Set();
72
+ this.listeners.set(type, set);
73
+ }
74
+ set.add(handler);
75
+ }
76
+ removeEventListener(type, handler) {
77
+ this.listeners.get(type)?.delete(handler);
78
+ }
79
+ dispatchEvent(event) {
80
+ const own = { open: this.onopen, message: this.onmessage, error: this.onerror, close: this.onclose }[event.type];
81
+ own?.(event);
82
+ for (const handler of this.listeners.get(event.type) ?? [])
83
+ handler(event);
84
+ return true;
85
+ }
86
+ /** The bridge's event for this socket. */
87
+ receive(event) {
88
+ switch (event.type) {
89
+ case 'open':
90
+ if (this.readyState !== NativeWebSocket.CONNECTING)
91
+ return;
92
+ this.readyState = NativeWebSocket.OPEN;
93
+ this.protocol = event.protocol ?? '';
94
+ this.dispatchEvent({ type: 'open', target: this });
95
+ return;
96
+ case 'message': {
97
+ if (this.readyState !== NativeWebSocket.OPEN)
98
+ return;
99
+ let data = event.text ?? '';
100
+ if (event.binary !== undefined) {
101
+ const bytes = base64Decode(event.binary);
102
+ data = this.binaryType === 'arraybuffer' ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : new SimpleBlob([bytes]);
103
+ }
104
+ this.dispatchEvent({ type: 'message', target: this, data });
105
+ return;
106
+ }
107
+ case 'error':
108
+ this.dispatchEvent({ type: 'error', target: this, message: event.message ?? '' });
109
+ return;
110
+ case 'close':
111
+ if (this.readyState === NativeWebSocket.CLOSED)
112
+ return;
113
+ this.readyState = NativeWebSocket.CLOSED;
114
+ sockets.delete(this.id);
115
+ this.dispatchEvent({ type: 'close', target: this, code: event.code ?? 1006, reason: event.reason ?? '', wasClean: event.wasClean ?? false });
116
+ return;
117
+ }
118
+ }
119
+ /** The bridge's `__sbWebSocketEvent(id, json)`. */
120
+ static receive(id, json) {
121
+ const socket = sockets.get(id);
122
+ if (!socket)
123
+ return;
124
+ let event;
125
+ try {
126
+ event = typeof json === 'string' ? JSON.parse(json) : json;
127
+ }
128
+ catch {
129
+ return;
130
+ }
131
+ socket.receive(event);
132
+ }
133
+ /** Points the sockets at the bridge (the runtime, once per install). */
134
+ static install(native) {
135
+ bridge = native;
136
+ }
137
+ }
138
+ /** A `DOMException` stand-in: an error with the browser's name. */
139
+ class DOMExceptionLike extends Error {
140
+ constructor(name, message) {
141
+ super(message);
142
+ this.name = name;
143
+ }
144
+ }
145
+ //# sourceMappingURL=webSocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webSocket.js","sourceRoot":"","sources":["../src/webSocket.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/native",
3
- "version": "0.17.6",
3
+ "version": "0.17.8",
4
4
  "type": "module",
5
5
  "description": "Run a @swiftbrowser/react app natively: the runtime a JavaScriptCore-hosted app needs (timers, console, fetch and the other web globals over the SwiftBrowser Native bridge), the `run` entry that renders into the native SwiftUI renderer, and the bundler that builds app.js for the Xcode target.",
6
6
  "repository": {
@@ -59,8 +59,8 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@babel/core": "^7.29.7",
62
- "@swiftbrowser/react": "0.17.6",
63
- "@swiftbrowser/web": "0.17.6",
62
+ "@swiftbrowser/react": "0.17.8",
63
+ "@swiftbrowser/web": "0.17.8",
64
64
  "esbuild": "^0.28.2",
65
65
  "react-refresh": "^0.17.0"
66
66
  },
package/src/blob.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
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 body is
4
+ * still whole (no streams); `URL.createObjectURL` gives a `data:` URL, which
5
+ * the host's image loader and `URLSession` both read, and
6
+ * `revokeObjectURL` has nothing to free.
7
+ */
8
+
9
+ import { base64Encode, utf8Decode, utf8Encode } from './encoding';
10
+
11
+ export type BlobPartLike = string | ArrayBuffer | ArrayBufferView | SimpleBlob;
12
+
13
+ export interface BlobOptions {
14
+ type?: string;
15
+ }
16
+
17
+ export interface FileOptions extends BlobOptions {
18
+ lastModified?: number;
19
+ }
20
+
21
+ function partBytes(part: BlobPartLike): Uint8Array {
22
+ if (typeof part === 'string') return utf8Encode(part);
23
+ if (part instanceof SimpleBlob) return part.bytes();
24
+ if (part instanceof ArrayBuffer) return new Uint8Array(part.slice(0));
25
+ if (ArrayBuffer.isView(part)) return new Uint8Array(part.buffer.slice(part.byteOffset, part.byteOffset + part.byteLength));
26
+ return utf8Encode(String(part));
27
+ }
28
+
29
+ function concat(parts: Uint8Array[]): Uint8Array {
30
+ const total = parts.reduce((sum, part) => sum + part.byteLength, 0);
31
+ const out = new Uint8Array(total);
32
+ let offset = 0;
33
+ for (const part of parts) {
34
+ out.set(part, offset);
35
+ offset += part.byteLength;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /** The browser's `Blob`: bytes and a MIME type. */
41
+ export class SimpleBlob {
42
+ private readonly data: Uint8Array;
43
+ readonly type: string;
44
+
45
+ constructor(parts: Iterable<BlobPartLike> = [], options: BlobOptions = {}) {
46
+ this.data = concat([...parts].map(partBytes));
47
+ // As the browser does: lowercased, and empty when it is not printable ASCII.
48
+ const type = options.type ?? '';
49
+ this.type = /^[\x20-\x7e]*$/.test(type) ? type.toLowerCase() : '';
50
+ }
51
+
52
+ get size(): number {
53
+ return this.data.byteLength;
54
+ }
55
+
56
+ /** The bytes (a view; callers must not write into it). */
57
+ bytes(): Uint8Array {
58
+ return this.data;
59
+ }
60
+
61
+ slice(start = 0, end = this.size, contentType = ''): SimpleBlob {
62
+ const from = start < 0 ? Math.max(this.size + start, 0) : Math.min(start, this.size);
63
+ const to = end < 0 ? Math.max(this.size + end, 0) : Math.min(end, this.size);
64
+ return new SimpleBlob([this.data.subarray(from, Math.max(from, to))], { type: contentType });
65
+ }
66
+
67
+ async arrayBuffer(): Promise<ArrayBuffer> {
68
+ return this.data.buffer.slice(this.data.byteOffset, this.data.byteOffset + this.data.byteLength) as ArrayBuffer;
69
+ }
70
+
71
+ async text(): Promise<string> {
72
+ return utf8Decode(this.data);
73
+ }
74
+
75
+ get [Symbol.toStringTag](): string {
76
+ return 'Blob';
77
+ }
78
+ }
79
+
80
+ /** The browser's `File`: a `Blob` with a name. */
81
+ export class SimpleFile extends SimpleBlob {
82
+ readonly name: string;
83
+ readonly lastModified: number;
84
+
85
+ constructor(parts: Iterable<BlobPartLike>, name: string, options: FileOptions = {}) {
86
+ super(parts, options);
87
+ this.name = String(name);
88
+ this.lastModified = options.lastModified ?? Date.now();
89
+ }
90
+
91
+ override get [Symbol.toStringTag](): string {
92
+ return 'File';
93
+ }
94
+ }
95
+
96
+ export type FormDataEntryValueLike = string | SimpleFile;
97
+
98
+ /** The browser's `FormData`, as far as a request body needs it. */
99
+ export class SimpleFormData {
100
+ private entries_: [string, FormDataEntryValueLike][] = [];
101
+
102
+ private static value(value: unknown, filename?: string): FormDataEntryValueLike {
103
+ if (value instanceof SimpleBlob) {
104
+ const name = filename ?? (value instanceof SimpleFile ? value.name : 'blob');
105
+ return value instanceof SimpleFile && filename === undefined ? value : new SimpleFile([value], name, { type: value.type });
106
+ }
107
+ return String(value);
108
+ }
109
+
110
+ append(name: string, value: unknown, filename?: string): void {
111
+ this.entries_.push([String(name), SimpleFormData.value(value, filename)]);
112
+ }
113
+
114
+ set(name: string, value: unknown, filename?: string): void {
115
+ const key = String(name);
116
+ const entry: [string, FormDataEntryValueLike] = [key, SimpleFormData.value(value, filename)];
117
+ const index = this.entries_.findIndex(([entryName]) => entryName === key);
118
+ if (index < 0) {
119
+ this.entries_.push(entry);
120
+ return;
121
+ }
122
+ this.entries_ = this.entries_.filter(([entryName], position) => position === index || entryName !== key);
123
+ this.entries_[index] = entry;
124
+ }
125
+
126
+ get(name: string): FormDataEntryValueLike | null {
127
+ return this.entries_.find(([entryName]) => entryName === String(name))?.[1] ?? null;
128
+ }
129
+
130
+ getAll(name: string): FormDataEntryValueLike[] {
131
+ return this.entries_.filter(([entryName]) => entryName === String(name)).map(([, value]) => value);
132
+ }
133
+
134
+ has(name: string): boolean {
135
+ return this.entries_.some(([entryName]) => entryName === String(name));
136
+ }
137
+
138
+ delete(name: string): void {
139
+ this.entries_ = this.entries_.filter(([entryName]) => entryName !== String(name));
140
+ }
141
+
142
+ *entries(): IterableIterator<[string, FormDataEntryValueLike]> {
143
+ yield* this.entries_;
144
+ }
145
+
146
+ *keys(): IterableIterator<string> {
147
+ for (const [name] of this.entries_) yield name;
148
+ }
149
+
150
+ *values(): IterableIterator<FormDataEntryValueLike> {
151
+ for (const [, value] of this.entries_) yield value;
152
+ }
153
+
154
+ forEach(callback: (value: FormDataEntryValueLike, name: string, form: SimpleFormData) => void): void {
155
+ for (const [name, value] of this.entries_) callback(value, name, this);
156
+ }
157
+
158
+ [Symbol.iterator](): IterableIterator<[string, FormDataEntryValueLike]> {
159
+ return this.entries();
160
+ }
161
+
162
+ get [Symbol.toStringTag](): string {
163
+ return 'FormData';
164
+ }
165
+ }
166
+
167
+ /** A multipart/form-data body for the form, with the boundary `random` makes unique. */
168
+ export function encodeMultipart(form: SimpleFormData, random: (count: number) => Uint8Array): { bytes: Uint8Array; contentType: string } {
169
+ const boundary = `----SwiftBrowserFormBoundary${Array.from(random(16), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
170
+ const escape = (text: string): string => text.replace(/\r/g, '%0D').replace(/\n/g, '%0A').replace(/"/g, '%22');
171
+ const parts: Uint8Array[] = [];
172
+ for (const [name, value] of form.entries()) {
173
+ const head = [`--${boundary}`, `Content-Disposition: form-data; name="${escape(name)}"` + (typeof value === 'string' ? '' : `; filename="${escape(value.name)}"`)];
174
+ if (typeof value !== 'string') head.push(`Content-Type: ${value.type || 'application/octet-stream'}`);
175
+ parts.push(utf8Encode(`${head.join('\r\n')}\r\n\r\n`));
176
+ parts.push(typeof value === 'string' ? utf8Encode(value) : value.bytes());
177
+ parts.push(utf8Encode('\r\n'));
178
+ }
179
+ parts.push(utf8Encode(`--${boundary}--\r\n`));
180
+ return { bytes: concat(parts), contentType: `multipart/form-data; boundary=${boundary}` };
181
+ }
182
+
183
+ /** `URL.createObjectURL`: a `data:` URL holding the blob. */
184
+ export function objectURL(blob: SimpleBlob): string {
185
+ return `data:${blob.type || 'application/octet-stream'};base64,${base64Encode(blob.bytes())}`;
186
+ }