@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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +2 -0
- package/build/core/blob.d.ts +10 -0
- package/build/core/blob.js +100 -0
- package/build/core/index.d.ts +2 -0
- package/build/core/index.js +1 -0
- package/build/core/native-module.d.ts +14 -0
- package/build/core/native-module.js +3 -0
- package/build/core/types.d.ts +6 -0
- package/build/core/types.js +1 -0
- package/build/core/utils.d.ts +7 -0
- package/build/core/utils.js +43 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +3 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/core/blob.d.ts +10 -0
- package/build-ngc/core/blob.js +101 -0
- package/build-ngc/core/blob.js.map +1 -0
- package/build-ngc/core/index.d.ts +2 -0
- package/build-ngc/core/index.js +2 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +14 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/types.d.ts +6 -0
- package/build-ngc/core/types.js +2 -0
- package/build-ngc/core/types.js.map +1 -0
- package/build-ngc/core/utils.d.ts +7 -0
- package/build-ngc/core/utils.js +44 -0
- package/build-ngc/core/utils.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +149 -0
- package/src/angular/index.ts +2 -0
- package/src/core/blob.ts +135 -0
- package/src/core/index.ts +2 -0
- package/src/core/native-module.ts +21 -0
- package/src/core/types.ts +8 -0
- 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,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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/native-link.json
ADDED
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
|
+
}
|
package/src/core/blob.ts
ADDED
|
@@ -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,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,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;
|