@symbiote-native/blob 0.1.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +2 -0
  5. package/build/core/blob.d.ts +10 -0
  6. package/build/core/blob.js +100 -0
  7. package/build/core/index.d.ts +2 -0
  8. package/build/core/index.js +1 -0
  9. package/build/core/native-module.d.ts +14 -0
  10. package/build/core/native-module.js +3 -0
  11. package/build/core/types.d.ts +6 -0
  12. package/build/core/types.js +1 -0
  13. package/build/core/utils.d.ts +7 -0
  14. package/build/core/utils.js +43 -0
  15. package/build-ngc/angular/index.d.ts +1 -0
  16. package/build-ngc/angular/index.js +3 -0
  17. package/build-ngc/angular/index.js.map +1 -0
  18. package/build-ngc/core/blob.d.ts +10 -0
  19. package/build-ngc/core/blob.js +101 -0
  20. package/build-ngc/core/blob.js.map +1 -0
  21. package/build-ngc/core/index.d.ts +2 -0
  22. package/build-ngc/core/index.js +2 -0
  23. package/build-ngc/core/index.js.map +1 -0
  24. package/build-ngc/core/native-module.d.ts +14 -0
  25. package/build-ngc/core/native-module.js +4 -0
  26. package/build-ngc/core/native-module.js.map +1 -0
  27. package/build-ngc/core/types.d.ts +6 -0
  28. package/build-ngc/core/types.js +2 -0
  29. package/build-ngc/core/types.js.map +1 -0
  30. package/build-ngc/core/utils.d.ts +7 -0
  31. package/build-ngc/core/utils.js +44 -0
  32. package/build-ngc/core/utils.js.map +1 -0
  33. package/native-link.json +12 -0
  34. package/package.json +149 -0
  35. package/src/angular/index.ts +2 -0
  36. package/src/core/blob.ts +135 -0
  37. package/src/core/index.ts +2 -0
  38. package/src/core/native-module.ts +21 -0
  39. package/src/core/types.ts +8 -0
  40. package/src/core/utils.ts +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @symbiote-native/blob
2
+
3
+ Build, slice and read binary data with the same `Blob` you use in the browser. React Native's own
4
+ `Blob` has gaps, notably in `slice()`; this one follows the W3C File API. One API for every
5
+ [SymbioteNative](../../README.md) adapter (React, Vue, Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-blob`](https://github.com/expo/expo/tree/main/packages/expo-blob), a native,
8
+ JSI-backed `Blob`, the same way [`@symbiote-native/print`](../print) wraps its upstream:
9
+ `expo-modules-core` is a direct dependency and the upstream JS is hand-ported into `core/`. See the
10
+ `symbiote-expo-native-module` project skill for the full mechanism.
11
+
12
+ ## Install
13
+
14
+ **New app:**
15
+
16
+ ```bash
17
+ npx @symbiote-native/cli new my-app --blob
18
+ ```
19
+
20
+ **Existing SymbioteNative app:**
21
+
22
+ ```bash
23
+ npx @symbiote-native/cli add --blob
24
+ ```
25
+
26
+ Either way: installs `@symbiote-native/blob` and wires the native autolinking automatically -
27
+ see [`@symbiote-native/cli`](../cli).
28
+
29
+ <details>
30
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
31
+
32
+ ```bash
33
+ npm install @symbiote-native/blob
34
+ ```
35
+
36
+ `expo-blob` and `expo-modules-core` come along as regular, pinned dependencies - never install
37
+ either yourself, and never add the `expo` meta-package to this project.
38
+
39
+ ### Required one-time step: native autolinking wiring
40
+
41
+ Same one-time app wiring every `expo-modules-core` package shares - see
42
+ [`@symbiote-native/print`'s README](../print/README.md#required-one-time-step-native-autolinking-wiring)
43
+ for the full table; nothing package-specific here.
44
+
45
+ ### No permissions, no config plugin
46
+
47
+ - **No runtime permission on either platform.** A `Blob` is pure in-memory/on-device data.
48
+ - **No manifest edit.** `expo-blob`'s own `AndroidManifest.xml` declares nothing.
49
+ - **No config plugin.** Upstream ships none.
50
+
51
+ </details>
52
+
53
+ ## Shape
54
+
55
+ ```
56
+ src/core/ the whole API: the Blob class. native-module.ts resolves ExpoBlob
57
+ through expo-modules-core's requireNativeModule.
58
+ src/angular/ @symbiote-native/blob/angular
59
+ ```
60
+
61
+ `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/` -
62
+ `Blob` is a plain class with no framework lifecycle to wrap.
63
+
64
+ ## Use it
65
+
66
+ ```ts
67
+ import { Blob } from '@symbiote-native/blob';
68
+
69
+ const blob = new Blob(['hello ', 'world'], { type: 'text/plain' });
70
+ blob.size; // 11
71
+ blob.type; // 'text/plain'
72
+
73
+ const text = await blob.text();
74
+ const bytes = await blob.bytes();
75
+ const buffer = await blob.arrayBuffer();
76
+ const firstFive = blob.slice(0, 5, 'text/plain');
77
+ ```
78
+
79
+ ## API
80
+
81
+ | Export | Notes |
82
+ | ------ | ----- |
83
+ | `Blob` | Constructor: `(blobParts?: IBlobPart[], options?: IBlobPropertyBag)`. `IBlobPart = string \| ArrayBuffer \| ArrayBufferView \| Blob`. Instance: `size`, `type`, `slice(start?, end?, contentType?)`, `bytes()`, `text()`, `arrayBuffer()`, `stream()`, `toString()`. |
84
+
85
+ ## Notes
86
+
87
+ - **This package's `tsconfig.json` adds `"DOM"` to its `lib` array** (every other package here
88
+ stays `ES2022`-only) - the ported API's shape genuinely needs `ReadableStream`,
89
+ `ReadableByteStreamController`, and `BlobPropertyBag`, none of which exist in the plain ES lib.
90
+ Scoped to this package only; nothing else in the repo is affected.
91
+ - **`.stream()` needs a global `ReadableStream` to actually run.** Hermes/React Native ship no
92
+ WHATWG Streams implementation - `new ReadableStream(...)` throws `ReferenceError` unless the
93
+ app installs a polyfill (e.g. `web-streams-polyfill`). Typed and ported for parity; this is an
94
+ environment prerequisite, not a porting gap.
95
+
96
+ ## Common questions
97
+
98
+ - **Why not React Native's `Blob`?** It lacks `text()`, `bytes()`, `arrayBuffer()` and `stream()`
99
+ and has gaps in `slice()`; this one follows the File API.
100
+ - **`ReferenceError: ReadableStream is not defined`.** Hermes has no Streams; install a polyfill
101
+ (`web-streams-polyfill`) or use `arrayBuffer()` / `bytes()`.
102
+ - **Upload a blob.** Pass it as a `fetch` body or append it to a `FormData`.
103
+ - **Does `.stream()` avoid loading everything?** No: it loads the whole blob into memory first.
104
+
105
+ Sources: [Expo docs: Blob](https://docs.expo.dev/versions/latest/sdk/blob/),
106
+ [expo/expo#33463](https://github.com/expo/expo/pull/33463).
107
+
108
+ ## Test it
109
+
110
+ ```bash
111
+ pnpm vitest run packages/blob
112
+ ```
113
+
114
+ Upstream ships no test suite of its own (`expo-blob`'s `src/` has no `__tests__`), so nothing to
115
+ port - every test here is net-new coverage of the ported logic (constructor validation, part
116
+ concatenation, `slice`'s prototype-fixup, `arrayBuffer`'s fresh-buffer guarantee, `stream`'s
117
+ default read path).
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,2 @@
1
+ // No per-instance state or event stream to wrap in an injectable service — plain re-export.
2
+ export * from '../core/index.js';
@@ -0,0 +1,10 @@
1
+ import { expoBlob } from './native-module';
2
+ import type { IBlobPart, IBlobPropertyBag } from './types';
3
+ export declare class Blob extends expoBlob.Blob {
4
+ constructor(blobParts?: IBlobPart[] | Iterable<IBlobPart>, options?: IBlobPropertyBag);
5
+ slice(start?: number, end?: number, contentType?: string): Blob;
6
+ stream(): ReadableStream;
7
+ arrayBuffer(): Promise<ArrayBuffer>;
8
+ toString(): string;
9
+ static get length(): number;
10
+ }
@@ -0,0 +1,100 @@
1
+ import { expoBlob } from './native-module.js';
2
+ import { DEFAULT_CHUNK_SIZE, isTypedArray, normalizedContentType, preprocessOptions, } from './utils.js';
3
+ function inputMapping(blobPart) {
4
+ if (blobPart instanceof ArrayBuffer) {
5
+ return new Uint8Array(blobPart);
6
+ }
7
+ if (blobPart instanceof Blob || isTypedArray(blobPart)) {
8
+ return blobPart;
9
+ }
10
+ return String(blobPart);
11
+ }
12
+ function assertArrayBuffer(buffer) {
13
+ if (!(buffer instanceof ArrayBuffer)) {
14
+ throw new TypeError('Blob data must be backed by an ArrayBuffer');
15
+ }
16
+ }
17
+ function isBlob(value) {
18
+ return value instanceof Blob;
19
+ }
20
+ export class Blob extends expoBlob.Blob {
21
+ constructor(blobParts, options) {
22
+ if (!new.target) {
23
+ throw new TypeError("Blob constructor requires 'new' operator");
24
+ }
25
+ if (blobParts === undefined) {
26
+ super([], preprocessOptions(options));
27
+ }
28
+ else if (blobParts === null || typeof blobParts !== 'object') {
29
+ throw new TypeError('Blob constructor requires blobParts to be a non-null object or undefined');
30
+ }
31
+ else {
32
+ const processedBlobParts = [];
33
+ for (const blobPart of blobParts) {
34
+ processedBlobParts.push(inputMapping(blobPart));
35
+ }
36
+ super(processedBlobParts, preprocessOptions(options));
37
+ }
38
+ }
39
+ slice(start, end, contentType) {
40
+ const normalizedType = normalizedContentType(contentType);
41
+ const slicedBlob = super.slice(start, end, normalizedType);
42
+ Object.setPrototypeOf(slicedBlob, Blob.prototype);
43
+ if (!isBlob(slicedBlob)) {
44
+ throw new Error('unreachable — just reassigned the prototype to Blob');
45
+ }
46
+ return slicedBlob;
47
+ }
48
+ stream() {
49
+ let getBlobBytes = this.bytes.bind(this);
50
+ let offset = 0;
51
+ let cachedBytes = null;
52
+ return new ReadableStream({
53
+ type: 'bytes',
54
+ async pull(controller) {
55
+ if (!cachedBytes) {
56
+ if (!getBlobBytes) {
57
+ throw new Error('Cannot read from a closed stream');
58
+ }
59
+ cachedBytes = await getBlobBytes();
60
+ getBlobBytes = null;
61
+ }
62
+ if (offset >= cachedBytes.length) {
63
+ controller.close();
64
+ cachedBytes = null;
65
+ return;
66
+ }
67
+ if (controller.byobRequest?.view) {
68
+ const view = new Uint8Array(controller.byobRequest.view.buffer, controller.byobRequest.view.byteOffset, controller.byobRequest.view.byteLength);
69
+ const end = Math.min(offset + view.byteLength, cachedBytes.length);
70
+ const chunk = cachedBytes.subarray(offset, end);
71
+ view.set(chunk, 0);
72
+ controller.byobRequest.respond(chunk.length);
73
+ offset = end;
74
+ if (offset >= cachedBytes.length) {
75
+ controller.close();
76
+ cachedBytes = null;
77
+ }
78
+ return;
79
+ }
80
+ const end = Math.min(offset + DEFAULT_CHUNK_SIZE, cachedBytes.length);
81
+ const chunk = cachedBytes.subarray(offset, end);
82
+ assertArrayBuffer(chunk.buffer);
83
+ controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
84
+ offset = end;
85
+ },
86
+ });
87
+ }
88
+ async arrayBuffer() {
89
+ const bytes = await super.bytes();
90
+ assertArrayBuffer(bytes.buffer);
91
+ // The Blob spec requires a NEW ArrayBuffer even when its bounds match the TypedArray's.
92
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
93
+ }
94
+ toString() {
95
+ return '[object Blob]';
96
+ }
97
+ static get length() {
98
+ return 0;
99
+ }
100
+ }
@@ -0,0 +1,2 @@
1
+ export { Blob } from './blob';
2
+ export type { IBlobPart, IBlobPropertyBag } from './types';
@@ -0,0 +1 @@
1
+ export { Blob } from './blob.js';
@@ -0,0 +1,14 @@
1
+ import { SharedObject } from 'expo-modules-core';
2
+ import type { IBlobPart, IBlobPropertyBag } from './types';
3
+ export declare class NativeBlob extends SharedObject {
4
+ readonly size: number;
5
+ readonly type: string;
6
+ constructor(blobParts?: IBlobPart[], options?: IBlobPropertyBag);
7
+ slice(start?: number, end?: number, contentType?: string): NativeBlob;
8
+ bytes(): Promise<Uint8Array>;
9
+ text(): Promise<string>;
10
+ }
11
+ export type INativeBlobModule = {
12
+ Blob: typeof NativeBlob;
13
+ };
14
+ export declare const expoBlob: INativeBlobModule;
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule, SharedObject } from 'expo-modules-core';
2
+ const EXPO_BLOB_MODULE_NAME = 'ExpoBlob';
3
+ export const expoBlob = requireNativeModule(EXPO_BLOB_MODULE_NAME);
@@ -0,0 +1,6 @@
1
+ import type { Blob } from './blob';
2
+ export type IBlobPropertyBag = {
3
+ endings?: 'transparent' | 'native';
4
+ type?: string;
5
+ };
6
+ export type IBlobPart = string | ArrayBuffer | ArrayBufferView | Blob;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { IBlobPropertyBag } from './types';
2
+ /** ASCII-printable-only, lowercased; anything else normalizes to `''`. */
3
+ export declare function normalizedContentType(type?: string): string;
4
+ export declare function isTypedArray(value: unknown): value is ArrayBufferView;
5
+ export declare function preprocessOptions(options?: IBlobPropertyBag): IBlobPropertyBag | undefined;
6
+ /** Not spec-mandated — a widely-adopted default (matches Node's own stream chunk size). */
7
+ export declare const DEFAULT_CHUNK_SIZE = 65536;
@@ -0,0 +1,43 @@
1
+ /** ASCII-printable-only, lowercased; anything else normalizes to `''`. */
2
+ export function normalizedContentType(type) {
3
+ const str = `${type}`;
4
+ const asciiPrintable = /^[\x20-\x7E]+$/;
5
+ if (type === undefined || !asciiPrintable.test(str))
6
+ return '';
7
+ return str.toLowerCase();
8
+ }
9
+ export function isTypedArray(value) {
10
+ return (value instanceof Int8Array ||
11
+ value instanceof Int16Array ||
12
+ value instanceof Int32Array ||
13
+ value instanceof BigInt64Array ||
14
+ value instanceof Uint8Array ||
15
+ value instanceof Uint16Array ||
16
+ value instanceof Uint32Array ||
17
+ value instanceof BigUint64Array ||
18
+ value instanceof Float32Array ||
19
+ value instanceof Float64Array);
20
+ }
21
+ export function preprocessOptions(options) {
22
+ if (!options)
23
+ return options;
24
+ if (!(options instanceof Object)) {
25
+ throw new TypeError(`The 'options' argument must be a dictionary. Received type ${typeof options}`);
26
+ }
27
+ let endings = options.endings;
28
+ let type = options.type;
29
+ if (endings && typeof endings === 'object') {
30
+ endings = String(endings);
31
+ }
32
+ if (type && typeof type === 'object') {
33
+ type = String(type);
34
+ }
35
+ if (endings !== undefined &&
36
+ endings !== 'native' &&
37
+ endings !== 'transparent') {
38
+ throw new TypeError(`Provided '${endings}' endings value is not a valid enum value of EndingType, try 'native' or 'transparent'`);
39
+ }
40
+ return { endings, type: normalizedContentType(type) };
41
+ }
42
+ /** Not spec-mandated — a widely-adopted default (matches Node's own stream chunk size). */
43
+ export const DEFAULT_CHUNK_SIZE = 65_536;
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,3 @@
1
+ // No per-instance state or event stream to wrap in an injectable service — plain re-export.
2
+ export * from '../core';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,cAAc,SAAS,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { expoBlob } from './native-module';
2
+ import type { IBlobPart, IBlobPropertyBag } from './types';
3
+ export declare class Blob extends expoBlob.Blob {
4
+ constructor(blobParts?: IBlobPart[] | Iterable<IBlobPart>, options?: IBlobPropertyBag);
5
+ slice(start?: number, end?: number, contentType?: string): Blob;
6
+ stream(): ReadableStream;
7
+ arrayBuffer(): Promise<ArrayBuffer>;
8
+ toString(): string;
9
+ static get length(): number;
10
+ }
@@ -0,0 +1,101 @@
1
+ import { expoBlob } from './native-module';
2
+ import { DEFAULT_CHUNK_SIZE, isTypedArray, normalizedContentType, preprocessOptions, } from './utils';
3
+ function inputMapping(blobPart) {
4
+ if (blobPart instanceof ArrayBuffer) {
5
+ return new Uint8Array(blobPart);
6
+ }
7
+ if (blobPart instanceof Blob || isTypedArray(blobPart)) {
8
+ return blobPart;
9
+ }
10
+ return String(blobPart);
11
+ }
12
+ function assertArrayBuffer(buffer) {
13
+ if (!(buffer instanceof ArrayBuffer)) {
14
+ throw new TypeError('Blob data must be backed by an ArrayBuffer');
15
+ }
16
+ }
17
+ function isBlob(value) {
18
+ return value instanceof Blob;
19
+ }
20
+ export class Blob extends expoBlob.Blob {
21
+ constructor(blobParts, options) {
22
+ if (!new.target) {
23
+ throw new TypeError("Blob constructor requires 'new' operator");
24
+ }
25
+ if (blobParts === undefined) {
26
+ super([], preprocessOptions(options));
27
+ }
28
+ else if (blobParts === null || typeof blobParts !== 'object') {
29
+ throw new TypeError('Blob constructor requires blobParts to be a non-null object or undefined');
30
+ }
31
+ else {
32
+ const processedBlobParts = [];
33
+ for (const blobPart of blobParts) {
34
+ processedBlobParts.push(inputMapping(blobPart));
35
+ }
36
+ super(processedBlobParts, preprocessOptions(options));
37
+ }
38
+ }
39
+ slice(start, end, contentType) {
40
+ const normalizedType = normalizedContentType(contentType);
41
+ const slicedBlob = super.slice(start, end, normalizedType);
42
+ Object.setPrototypeOf(slicedBlob, Blob.prototype);
43
+ if (!isBlob(slicedBlob)) {
44
+ throw new Error('unreachable — just reassigned the prototype to Blob');
45
+ }
46
+ return slicedBlob;
47
+ }
48
+ stream() {
49
+ let getBlobBytes = this.bytes.bind(this);
50
+ let offset = 0;
51
+ let cachedBytes = null;
52
+ return new ReadableStream({
53
+ type: 'bytes',
54
+ async pull(controller) {
55
+ if (!cachedBytes) {
56
+ if (!getBlobBytes) {
57
+ throw new Error('Cannot read from a closed stream');
58
+ }
59
+ cachedBytes = await getBlobBytes();
60
+ getBlobBytes = null;
61
+ }
62
+ if (offset >= cachedBytes.length) {
63
+ controller.close();
64
+ cachedBytes = null;
65
+ return;
66
+ }
67
+ if (controller.byobRequest?.view) {
68
+ const view = new Uint8Array(controller.byobRequest.view.buffer, controller.byobRequest.view.byteOffset, controller.byobRequest.view.byteLength);
69
+ const end = Math.min(offset + view.byteLength, cachedBytes.length);
70
+ const chunk = cachedBytes.subarray(offset, end);
71
+ view.set(chunk, 0);
72
+ controller.byobRequest.respond(chunk.length);
73
+ offset = end;
74
+ if (offset >= cachedBytes.length) {
75
+ controller.close();
76
+ cachedBytes = null;
77
+ }
78
+ return;
79
+ }
80
+ const end = Math.min(offset + DEFAULT_CHUNK_SIZE, cachedBytes.length);
81
+ const chunk = cachedBytes.subarray(offset, end);
82
+ assertArrayBuffer(chunk.buffer);
83
+ controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
84
+ offset = end;
85
+ },
86
+ });
87
+ }
88
+ async arrayBuffer() {
89
+ const bytes = await super.bytes();
90
+ assertArrayBuffer(bytes.buffer);
91
+ // The Blob spec requires a NEW ArrayBuffer even when its bounds match the TypedArray's.
92
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
93
+ }
94
+ toString() {
95
+ return '[object Blob]';
96
+ }
97
+ static get length() {
98
+ return 0;
99
+ }
100
+ }
101
+ //# sourceMappingURL=blob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob.js","sourceRoot":"","sources":["../../src/core/blob.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { Blob } from './blob';
2
+ export type { IBlobPart, IBlobPropertyBag } from './types';
@@ -0,0 +1,2 @@
1
+ export { Blob } from './blob';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { SharedObject } from 'expo-modules-core';
2
+ import type { IBlobPart, IBlobPropertyBag } from './types';
3
+ export declare class NativeBlob extends SharedObject {
4
+ readonly size: number;
5
+ readonly type: string;
6
+ constructor(blobParts?: IBlobPart[], options?: IBlobPropertyBag);
7
+ slice(start?: number, end?: number, contentType?: string): NativeBlob;
8
+ bytes(): Promise<Uint8Array>;
9
+ text(): Promise<string>;
10
+ }
11
+ export type INativeBlobModule = {
12
+ Blob: typeof NativeBlob;
13
+ };
14
+ export declare const expoBlob: INativeBlobModule;
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule, SharedObject } from 'expo-modules-core';
2
+ const EXPO_BLOB_MODULE_NAME = 'ExpoBlob';
3
+ export const expoBlob = requireNativeModule(EXPO_BLOB_MODULE_NAME);
4
+ //# sourceMappingURL=native-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtE,MAAM,qBAAqB,GAAG,UAAU,CAAC;AAezC,MAAM,CAAC,MAAM,QAAQ,GAAG,mBAAmB,CACzC,qBAAqB,CACtB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Blob } from './blob';
2
+ export type IBlobPropertyBag = {
3
+ endings?: 'transparent' | 'native';
4
+ type?: string;
5
+ };
6
+ export type IBlobPart = string | ArrayBuffer | ArrayBufferView | Blob;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import type { IBlobPropertyBag } from './types';
2
+ /** ASCII-printable-only, lowercased; anything else normalizes to `''`. */
3
+ export declare function normalizedContentType(type?: string): string;
4
+ export declare function isTypedArray(value: unknown): value is ArrayBufferView;
5
+ export declare function preprocessOptions(options?: IBlobPropertyBag): IBlobPropertyBag | undefined;
6
+ /** Not spec-mandated — a widely-adopted default (matches Node's own stream chunk size). */
7
+ export declare const DEFAULT_CHUNK_SIZE = 65536;
@@ -0,0 +1,44 @@
1
+ /** ASCII-printable-only, lowercased; anything else normalizes to `''`. */
2
+ export function normalizedContentType(type) {
3
+ const str = `${type}`;
4
+ const asciiPrintable = /^[\x20-\x7E]+$/;
5
+ if (type === undefined || !asciiPrintable.test(str))
6
+ return '';
7
+ return str.toLowerCase();
8
+ }
9
+ export function isTypedArray(value) {
10
+ return (value instanceof Int8Array ||
11
+ value instanceof Int16Array ||
12
+ value instanceof Int32Array ||
13
+ value instanceof BigInt64Array ||
14
+ value instanceof Uint8Array ||
15
+ value instanceof Uint16Array ||
16
+ value instanceof Uint32Array ||
17
+ value instanceof BigUint64Array ||
18
+ value instanceof Float32Array ||
19
+ value instanceof Float64Array);
20
+ }
21
+ export function preprocessOptions(options) {
22
+ if (!options)
23
+ return options;
24
+ if (!(options instanceof Object)) {
25
+ throw new TypeError(`The 'options' argument must be a dictionary. Received type ${typeof options}`);
26
+ }
27
+ let endings = options.endings;
28
+ let type = options.type;
29
+ if (endings && typeof endings === 'object') {
30
+ endings = String(endings);
31
+ }
32
+ if (type && typeof type === 'object') {
33
+ type = String(type);
34
+ }
35
+ if (endings !== undefined &&
36
+ endings !== 'native' &&
37
+ endings !== 'transparent') {
38
+ throw new TypeError(`Provided '${endings}' endings value is not a valid enum value of EndingType, try 'native' or 'transparent'`);
39
+ }
40
+ return { endings, type: normalizedContentType(type) };
41
+ }
42
+ /** Not spec-mandated — a widely-adopted default (matches Node's own stream chunk size). */
43
+ export const DEFAULT_CHUNK_SIZE = 65_536;
44
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,MAAM,UAAU,qBAAqB,CAAC,IAAa;IACjD,MAAM,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;IACtB,MAAM,cAAc,GAAG,gBAAgB,CAAC;IACxC,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/D,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,CACL,KAAK,YAAY,SAAS;QAC1B,KAAK,YAAY,UAAU;QAC3B,KAAK,YAAY,UAAU;QAC3B,KAAK,YAAY,aAAa;QAC9B,KAAK,YAAY,UAAU;QAC3B,KAAK,YAAY,WAAW;QAC5B,KAAK,YAAY,WAAW;QAC5B,KAAK,YAAY,cAAc;QAC/B,KAAK,YAAY,YAAY;QAC7B,KAAK,YAAY,YAAY,CAC9B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,OAA0B;IAE1B,IAAI,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC;IAC7B,IAAI,CAAC,CAAC,OAAO,YAAY,MAAM,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CACjB,8DAA8D,OAAO,OAAO,EAAE,CAC/E,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,GAAuB,OAAO,CAAC,OAAO,CAAC;IAClD,IAAI,IAAI,GAAuB,OAAO,CAAC,IAAI,CAAC;IAC5C,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC3C,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IACD,IACE,OAAO,KAAK,SAAS;QACrB,OAAO,KAAK,QAAQ;QACpB,OAAO,KAAK,aAAa,EACzB,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,aAAa,OAAO,wFAAwF,CAC7G,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-blob",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.blob.BlobModule",
7
+ "className": "BlobModule",
8
+ "nativeName": "ExpoBlob"
9
+ }
10
+ ]
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,149 @@
1
+ {
2
+ "name": "@symbiote-native/blob",
3
+ "version": "0.1.0",
4
+ "description": "expo-blob wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. A native, JSI-backed W3C Blob implementation.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "react-native",
8
+ "symbiote-native",
9
+ "expo-blob",
10
+ "react",
11
+ "vue",
12
+ "svelte",
13
+ "solid",
14
+ "angular",
15
+ "blob"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
20
+ "directory": "packages/blob"
21
+ },
22
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/blob#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
25
+ },
26
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
27
+ "type": "module",
28
+ "main": "./build/core/index.js",
29
+ "module": "./build/core/index.js",
30
+ "types": "./build/core/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./build/core/index.d.ts",
34
+ "default": "./build/core/index.js"
35
+ },
36
+ "./vue": {
37
+ "types": "./build/core/index.d.ts",
38
+ "default": "./build/core/index.js"
39
+ },
40
+ "./react": {
41
+ "types": "./build/core/index.d.ts",
42
+ "default": "./build/core/index.js"
43
+ },
44
+ "./svelte": {
45
+ "types": "./build/core/index.d.ts",
46
+ "default": "./build/core/index.js"
47
+ },
48
+ "./solid": {
49
+ "types": "./build/core/index.d.ts",
50
+ "default": "./build/core/index.js"
51
+ },
52
+ "./angular": {
53
+ "types": "./build-ngc/angular/index.d.ts",
54
+ "react-native": "./build-ngc/angular/index.js",
55
+ "default": "./src/angular/index.ts"
56
+ }
57
+ },
58
+ "files": [
59
+ "src",
60
+ "build",
61
+ "build-ngc",
62
+ "native-link.json",
63
+ "!src/**/*.test.*",
64
+ "!src/**/*.spec.*",
65
+ "!src/**/*.detox.*"
66
+ ],
67
+ "publishConfig": {
68
+ "access": "public"
69
+ },
70
+ "dependencies": {
71
+ "expo-blob": "57.0.2",
72
+ "expo-modules-core": "57.0.20"
73
+ },
74
+ "peerDependencies": {
75
+ "@angular/core": ">=20",
76
+ "@vue/runtime-core": "^3.5.13",
77
+ "react": ">=19.0.0",
78
+ "react-native": ">=0.86",
79
+ "solid-js": ">=1.9.0",
80
+ "svelte": ">=5.56.0",
81
+ "vue": ">=3.5.0",
82
+ "@symbiote-native/angular": "^3.1.3",
83
+ "@symbiote-native/engine": "^1.4.0",
84
+ "@symbiote-native/solid": "^3.0.5",
85
+ "@symbiote-native/react": "^3.1.0",
86
+ "@symbiote-native/svelte": "^3.0.5",
87
+ "@symbiote-native/vue": "^3.1.0"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "@symbiote-native/angular": {
91
+ "optional": true
92
+ },
93
+ "@symbiote-native/react": {
94
+ "optional": true
95
+ },
96
+ "@symbiote-native/solid": {
97
+ "optional": true
98
+ },
99
+ "@symbiote-native/svelte": {
100
+ "optional": true
101
+ },
102
+ "@symbiote-native/vue": {
103
+ "optional": true
104
+ },
105
+ "@angular/core": {
106
+ "optional": true
107
+ },
108
+ "@vue/runtime-core": {
109
+ "optional": true
110
+ },
111
+ "react": {
112
+ "optional": true
113
+ },
114
+ "solid-js": {
115
+ "optional": true
116
+ },
117
+ "svelte": {
118
+ "optional": true
119
+ },
120
+ "vue": {
121
+ "optional": true
122
+ }
123
+ },
124
+ "devDependencies": {
125
+ "@angular/compiler": "~22.0.8",
126
+ "@angular/compiler-cli": "~22.0.8",
127
+ "@angular/core": "~22.0.8",
128
+ "@types/node": "^26.0.0",
129
+ "@types/react": "^19.2.0",
130
+ "@vue/runtime-core": "^3.5.13",
131
+ "react": "19.2.3",
132
+ "solid-js": "^1.9.14",
133
+ "svelte": "^5.56.0",
134
+ "typescript": "~6.0.0",
135
+ "@symbiote-native/angular": "3.1.3",
136
+ "@symbiote-native/engine": "1.4.0",
137
+ "@symbiote-native/react": "3.1.0",
138
+ "@symbiote-native/svelte": "3.0.5",
139
+ "@symbiote-native/solid": "3.0.5",
140
+ "@symbiote-native/test-utils": "0.4.5",
141
+ "@symbiote-native/vue": "3.1.0"
142
+ },
143
+ "scripts": {
144
+ "typecheck": "tsc --build",
145
+ "clean": "rm -rf build-ngc",
146
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
147
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
148
+ }
149
+ }
@@ -0,0 +1,2 @@
1
+ // No per-instance state or event stream to wrap in an injectable service — plain re-export.
2
+ export * from '../core';
@@ -0,0 +1,135 @@
1
+ import { expoBlob } from './native-module';
2
+ import {
3
+ DEFAULT_CHUNK_SIZE,
4
+ isTypedArray,
5
+ normalizedContentType,
6
+ preprocessOptions,
7
+ } from './utils';
8
+ import type { IBlobPart, IBlobPropertyBag } from './types';
9
+
10
+ function inputMapping(blobPart: IBlobPart): IBlobPart {
11
+ if (blobPart instanceof ArrayBuffer) {
12
+ return new Uint8Array(blobPart);
13
+ }
14
+ if (blobPart instanceof Blob || isTypedArray(blobPart)) {
15
+ return blobPart;
16
+ }
17
+ return String(blobPart);
18
+ }
19
+
20
+ function assertArrayBuffer(
21
+ buffer: ArrayBufferLike,
22
+ ): asserts buffer is ArrayBuffer {
23
+ if (!(buffer instanceof ArrayBuffer)) {
24
+ throw new TypeError('Blob data must be backed by an ArrayBuffer');
25
+ }
26
+ }
27
+
28
+ function isBlob(value: unknown): value is Blob {
29
+ return value instanceof Blob;
30
+ }
31
+
32
+ export class Blob extends expoBlob.Blob {
33
+ constructor(
34
+ blobParts?: IBlobPart[] | Iterable<IBlobPart>,
35
+ options?: IBlobPropertyBag,
36
+ ) {
37
+ if (!new.target) {
38
+ throw new TypeError("Blob constructor requires 'new' operator");
39
+ }
40
+
41
+ if (blobParts === undefined) {
42
+ super([], preprocessOptions(options));
43
+ } else if (blobParts === null || typeof blobParts !== 'object') {
44
+ throw new TypeError(
45
+ 'Blob constructor requires blobParts to be a non-null object or undefined',
46
+ );
47
+ } else {
48
+ const processedBlobParts: IBlobPart[] = [];
49
+ for (const blobPart of blobParts) {
50
+ processedBlobParts.push(inputMapping(blobPart));
51
+ }
52
+ super(processedBlobParts, preprocessOptions(options));
53
+ }
54
+ }
55
+
56
+ override slice(start?: number, end?: number, contentType?: string): Blob {
57
+ const normalizedType = normalizedContentType(contentType);
58
+ const slicedBlob = super.slice(start, end, normalizedType);
59
+ Object.setPrototypeOf(slicedBlob, Blob.prototype);
60
+ if (!isBlob(slicedBlob)) {
61
+ throw new Error('unreachable — just reassigned the prototype to Blob');
62
+ }
63
+ return slicedBlob;
64
+ }
65
+
66
+ stream(): ReadableStream {
67
+ let getBlobBytes: (() => Promise<Uint8Array>) | null =
68
+ this.bytes.bind(this);
69
+ let offset = 0;
70
+ let cachedBytes: Uint8Array | null = null;
71
+
72
+ return new ReadableStream({
73
+ type: 'bytes',
74
+ async pull(controller) {
75
+ if (!cachedBytes) {
76
+ if (!getBlobBytes) {
77
+ throw new Error('Cannot read from a closed stream');
78
+ }
79
+ cachedBytes = await getBlobBytes();
80
+ getBlobBytes = null;
81
+ }
82
+
83
+ if (offset >= cachedBytes.length) {
84
+ controller.close();
85
+ cachedBytes = null;
86
+ return;
87
+ }
88
+
89
+ if (controller.byobRequest?.view) {
90
+ const view = new Uint8Array(
91
+ controller.byobRequest.view.buffer,
92
+ controller.byobRequest.view.byteOffset,
93
+ controller.byobRequest.view.byteLength,
94
+ );
95
+ const end = Math.min(offset + view.byteLength, cachedBytes.length);
96
+ const chunk = cachedBytes.subarray(offset, end);
97
+ view.set(chunk, 0);
98
+ controller.byobRequest.respond(chunk.length);
99
+ offset = end;
100
+ if (offset >= cachedBytes.length) {
101
+ controller.close();
102
+ cachedBytes = null;
103
+ }
104
+ return;
105
+ }
106
+
107
+ const end = Math.min(offset + DEFAULT_CHUNK_SIZE, cachedBytes.length);
108
+ const chunk = cachedBytes.subarray(offset, end);
109
+ assertArrayBuffer(chunk.buffer);
110
+ controller.enqueue(
111
+ new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
112
+ );
113
+ offset = end;
114
+ },
115
+ });
116
+ }
117
+
118
+ async arrayBuffer(): Promise<ArrayBuffer> {
119
+ const bytes = await super.bytes();
120
+ assertArrayBuffer(bytes.buffer);
121
+ // The Blob spec requires a NEW ArrayBuffer even when its bounds match the TypedArray's.
122
+ return bytes.buffer.slice(
123
+ bytes.byteOffset,
124
+ bytes.byteOffset + bytes.byteLength,
125
+ );
126
+ }
127
+
128
+ override toString(): string {
129
+ return '[object Blob]';
130
+ }
131
+
132
+ static override get length(): number {
133
+ return 0;
134
+ }
135
+ }
@@ -0,0 +1,2 @@
1
+ export { Blob } from './blob';
2
+ export type { IBlobPart, IBlobPropertyBag } from './types';
@@ -0,0 +1,21 @@
1
+ import { requireNativeModule, SharedObject } from 'expo-modules-core';
2
+ import type { IBlobPart, IBlobPropertyBag } from './types';
3
+
4
+ const EXPO_BLOB_MODULE_NAME = 'ExpoBlob';
5
+
6
+ export declare class NativeBlob extends SharedObject {
7
+ readonly size: number;
8
+ readonly type: string;
9
+ constructor(blobParts?: IBlobPart[], options?: IBlobPropertyBag);
10
+ slice(start?: number, end?: number, contentType?: string): NativeBlob;
11
+ bytes(): Promise<Uint8Array>;
12
+ text(): Promise<string>;
13
+ }
14
+
15
+ export type INativeBlobModule = {
16
+ Blob: typeof NativeBlob;
17
+ };
18
+
19
+ export const expoBlob = requireNativeModule<INativeBlobModule>(
20
+ EXPO_BLOB_MODULE_NAME,
21
+ );
@@ -0,0 +1,8 @@
1
+ import type { Blob } from './blob';
2
+
3
+ export type IBlobPropertyBag = {
4
+ endings?: 'transparent' | 'native';
5
+ type?: string;
6
+ };
7
+
8
+ export type IBlobPart = string | ArrayBuffer | ArrayBufferView | Blob;
@@ -0,0 +1,58 @@
1
+ import type { IBlobPropertyBag } from './types';
2
+
3
+ /** ASCII-printable-only, lowercased; anything else normalizes to `''`. */
4
+ export function normalizedContentType(type?: string): string {
5
+ const str = `${type}`;
6
+ const asciiPrintable = /^[\x20-\x7E]+$/;
7
+ if (type === undefined || !asciiPrintable.test(str)) return '';
8
+ return str.toLowerCase();
9
+ }
10
+
11
+ export function isTypedArray(value: unknown): value is ArrayBufferView {
12
+ return (
13
+ value instanceof Int8Array ||
14
+ value instanceof Int16Array ||
15
+ value instanceof Int32Array ||
16
+ value instanceof BigInt64Array ||
17
+ value instanceof Uint8Array ||
18
+ value instanceof Uint16Array ||
19
+ value instanceof Uint32Array ||
20
+ value instanceof BigUint64Array ||
21
+ value instanceof Float32Array ||
22
+ value instanceof Float64Array
23
+ );
24
+ }
25
+
26
+ export function preprocessOptions(
27
+ options?: IBlobPropertyBag,
28
+ ): IBlobPropertyBag | undefined {
29
+ if (!options) return options;
30
+ if (!(options instanceof Object)) {
31
+ throw new TypeError(
32
+ `The 'options' argument must be a dictionary. Received type ${typeof options}`,
33
+ );
34
+ }
35
+
36
+ let endings: string | undefined = options.endings;
37
+ let type: string | undefined = options.type;
38
+ if (endings && typeof endings === 'object') {
39
+ endings = String(endings);
40
+ }
41
+ if (type && typeof type === 'object') {
42
+ type = String(type);
43
+ }
44
+ if (
45
+ endings !== undefined &&
46
+ endings !== 'native' &&
47
+ endings !== 'transparent'
48
+ ) {
49
+ throw new TypeError(
50
+ `Provided '${endings}' endings value is not a valid enum value of EndingType, try 'native' or 'transparent'`,
51
+ );
52
+ }
53
+
54
+ return { endings, type: normalizedContentType(type) };
55
+ }
56
+
57
+ /** Not spec-mandated — a widely-adopted default (matches Node's own stream chunk size). */
58
+ export const DEFAULT_CHUNK_SIZE = 65_536;