Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export default function App() {
Array<{ id: string; usd: number }>
>([]);
const [prefetchInfo, setPrefetchInfo] = React.useState<string>('');
const [postResult, setPostResult] = React.useState<string>('');
const PREFETCH_URL = 'https://httpbin.org/uuid';
const PREFETCH_KEY = 'uuid';

Expand Down Expand Up @@ -233,6 +234,60 @@ export default function App() {
}
}, []);

const sendPostRequest = React.useCallback(async () => {
console.log('Sending POST request with worklet');
const url = 'https://httpbin.org/post';
const requestBody = {
message: 'Hello from Nitro Fetch!',
timestamp: Date.now(),
data: { userId: 123, action: 'test' },
};

const mapper = (payload: { bodyString?: string; status: number }) => {
'worklet';
if (payload.status !== 200) {
return { success: false, error: `HTTP ${payload.status}` };
}
const txt = payload.bodyString ?? '';
const json = JSON.parse(txt) as {
json?: typeof requestBody;
data?: string;
};
// Extract the parsed JSON from httpbin response
const sentData = json.json ?? (json.data ? JSON.parse(json.data) : null);
return {
success: true,
sent: sentData,
received: json,
};
};

try {
setPostResult('Sending POST request...');
const data = await nitroFetchOnWorklet(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
mapper,
{
preferBytes: false,
}
);
console.log('POST request result:', data);
setPostResult(
`Success! Sent: ${JSON.stringify(data.sent, null, 2).substring(0, 100)}...`
);
} catch (e: any) {
console.error('POST request error', e);
setPostResult(`Error: ${e?.message ?? String(e)}`);
}
}, []);

const run = React.useCallback(async () => {
if (running) return;
setRunning(true);
Expand Down Expand Up @@ -304,6 +359,8 @@ export default function App() {
loadPrices();
}}
/>
<View style={{ width: 12 }} />
<Button title="POST Request (Worklet)" onPress={sendPostRequest} />
</View>
<View style={[styles.actions, { marginTop: 0 }]}>
<Button
Expand Down Expand Up @@ -371,6 +428,17 @@ export default function App() {
{prefetchInfo}
</Text>
)}
{!!postResult && (
<Text
style={{
textAlign: 'center',
marginBottom: 8,
paddingHorizontal: 12,
}}
>
POST Result: {postResult}
</Text>
)}
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
Expand Down
178 changes: 167 additions & 11 deletions package/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,31 @@ function headersToPairs(headers?: HeadersInit): NitroHeader[] | undefined {
}
return pairs;
}
// Record<string, string>
for (const [k, v] of Object.entries(headers)) {
pairs.push({ key: k, value: String(v) });
// Check if it's a plain object (Record<string, string>) first
// Plain objects don't have forEach, so check for its absence
if (typeof headers === 'object' && headers !== null) {
// Check if it's a Headers instance by checking for forEach method
const hasForEach = typeof (headers as any).forEach === 'function';

if (hasForEach) {
// Headers-like object (duck typing)
(headers as any).forEach((v: string, k: string) =>
pairs.push({ key: k, value: v })
);
return pairs;
} else {
// Plain object (Record<string, string>)
// Use Object.keys to iterate since Object.entries might not work in worklets
const keys = Object.keys(headers);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = (headers as Record<string, string>)[k];
if (v !== undefined) {
pairs.push({ key: k, value: String(v) });
}
}
return pairs;
}
}
return pairs;
}
Expand Down Expand Up @@ -120,6 +142,146 @@ function buildNitroRequest(
};
}

// Pure JS version of buildNitroRequest that doesnt use anything that breaks worklets. TODO: Merge this to use Same logic for Worklets and normal Fetch
function headersToPairsPure(headers?: HeadersInit): NitroHeader[] | undefined {
'worklet';
if (!headers) return undefined;
const pairs: NitroHeader[] = [];

if (Array.isArray(headers)) {
// Convert tuple pairs to objects if needed
for (const entry of headers as any[]) {
if (Array.isArray(entry) && entry.length >= 2) {
pairs.push({ key: String(entry[0]), value: String(entry[1]) });
} else if (
entry &&
typeof entry === 'object' &&
'key' in entry &&
'value' in entry
) {
pairs.push(entry as NitroHeader);
}
}
return pairs;
}

// Check if it's a plain object (Record<string, string>) first
// Plain objects don't have forEach, so check for its absence
if (typeof headers === 'object' && headers !== null) {
// Check if it's a Headers instance by checking for forEach method
const hasForEach = typeof (headers as any).forEach === 'function';

if (hasForEach) {
// Headers-like object (duck typing)
(headers as any).forEach((v: string, k: string) =>
pairs.push({ key: k, value: v })
);
return pairs;
} else {
// Plain object (Record<string, string>)
// Use Object.keys to iterate since Object.entries might not work in worklets
const keys = Object.keys(headers);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = (headers as Record<string, string>)[k];
if (v !== undefined) {
pairs.push({ key: k, value: String(v) });
}
}
return pairs;
}
}

return pairs;
}
// Pure JS version of buildNitroRequest that doesnt use anything that breaks worklets
function normalizeBodyPure(
body: BodyInit | null | undefined
): { bodyString?: string; bodyBytes?: ArrayBuffer } | undefined {
'worklet';
if (body == null) return undefined;
if (typeof body === 'string') return { bodyString: body };

// Check for URLSearchParams (duck typing)
// It should be an object, have a toString method, and typically append/delete methods
// But mainly we care about toString() returning the query string
if (
typeof body === 'object' &&
body !== null &&
typeof (body as any).toString === 'function' &&
Object.prototype.toString.call(body) === '[object URLSearchParams]'
) {
return { bodyString: body.toString() };
}

// Check for ArrayBuffer (using toString tag to avoid instanceof)
if (
typeof ArrayBuffer !== 'undefined' &&
Object.prototype.toString.call(body) === '[object ArrayBuffer]'
) {
return { bodyBytes: body as ArrayBuffer };
}

if (ArrayBuffer.isView(body)) {
const view = body as ArrayBufferView;
// Pass a copy/slice of the underlying bytes without base64
return {
//@ts-ignore
bodyBytes: view.buffer.slice(
view.byteOffset,
view.byteOffset + view.byteLength
),
};
}
// TODO: Blob/FormData support can be added later
throw new Error('Unsupported body type for nitro fetch');
}
// Pure JS version of buildNitroRequest that doesnt use anything that breaks worklets
export function buildNitroRequestPure(
input: RequestInfo | URL,
init?: RequestInit
): NitroRequest {
'worklet';
let url: string;
let method: string | undefined;
let headersInit: HeadersInit | undefined;
let body: BodyInit | null | undefined;

// Check if input is URL-like without instanceof
const isUrlObject =
typeof input === 'object' &&
input !== null &&
Object.prototype.toString.call(input) === '[object URL]';

if (typeof input === 'string' || isUrlObject) {
url = String(input);
method = init?.method;
headersInit = init?.headers;
body = init?.body ?? null;
} else {
// Request object
const req = input as Request;
url = req.url;
method = req.method;
headersInit = req.headers;
// Clone body if needed – Request objects in RN typically allow direct access
body = init?.body ?? null;
}

const headers = headersToPairsPure(headersInit);
const normalized = normalizeBodyPure(body);

return {
url,
method: (method?.toUpperCase() as any) ?? 'GET',
headers,
bodyString: normalized?.bodyString,
// Only include bodyBytes when provided to avoid signaling upload data unintentionally
bodyBytes: undefined as any,
followRedirects: true,
};
}

async function nitroFetchRaw(
input: RequestInfo | URL,
init?: RequestInit
Expand Down Expand Up @@ -357,11 +519,9 @@ export type NitroWorkletMapper<T> = (payload: {
let nitroRuntime: any | undefined;
let WorkletsRef: any | undefined;
function ensureWorkletRuntime(name = 'nitro-fetch'): any | undefined {
console.log('ensuring worklet runtime');
try {
const { Worklets } = require('react-native-worklets-core');
nitroRuntime = nitroRuntime ?? Worklets.createContext(name);
console.log('nitroRuntime:', !!nitroRuntime);
return nitroRuntime;
} catch {
console.warn('react-native-worklets-core not available');
Expand All @@ -388,16 +548,12 @@ export async function nitroFetchOnWorklet<T>(
mapWorklet: NitroWorkletMapper<T>,
options?: { preferBytes?: boolean; runtimeName?: string }
): Promise<T> {
console.log('nitroFetchOnWorklet: starting');
const preferBytes = options?.preferBytes === true; // default true
console.log('nitroFetchOnWorklet: preferBytes:', preferBytes);
let rt: any | undefined;
let Worklets: any | undefined;
try {
rt = ensureWorkletRuntime(options?.runtimeName);
console.log('nitroFetchOnWorklet: runtime created?', !!rt);
Worklets = getWorklets();
console.log('nitroFetchOnWorklet: Worklets available?', !!Worklets);
} catch (e) {
console.error('nitroFetchOnWorklet: setup failed', e);
}
Expand All @@ -418,12 +574,12 @@ export async function nitroFetchOnWorklet<T>(
} as const;
return mapWorklet(payload as any);
}
console.log('nitroFetchOnWorklet: running on worklet thread');
return await rt.runAsync(() => {
'worklet';
const unboxedNitroFetch = boxedNitroFetch.unbox();
const unboxedClient = unboxedNitroFetch.createClient();
const res = unboxedClient.requestSync(buildNitroRequest(input, init));
const request = buildNitroRequestPure(input, init);
const res = unboxedClient.requestSync(request);
const payload = {
url: res.url,
status: res.status,
Expand Down
Loading