Skip to content
Draft
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
182 changes: 124 additions & 58 deletions packages/core/src/ThreadedRuntime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ViewStyle,
} from 'react-native';
import NativeThreadedRuntimeSurface from './NativeThreadedRuntimeSurface';
import { assertJson, type JsonValue } from './json';

const DEFAULT_RUNTIME_NAME = 'background-list';
const DEFAULT_BUSINESS_RUNTIME_NAME = 'business-runtime';
Expand All @@ -31,13 +32,17 @@ export type ThreadedComponent<Props extends object = Record<string, never>> =
type ThreadedComponentLoader = () => ComponentType<any>;
type ThreadedComponentRegistry = Map<string, ThreadedComponentLoader>;
type RuntimeFunctionLoader = () => RuntimeFunction<any>;
export type ThreadedHeadlessTaskContext<Payload> = {
payload: Payload;
export type ThreadedHeadlessTaskContext<
TPayload extends JsonValue | void = void,
> = {
payload: TPayload;
runtimeName: ThreadedRuntimeName;
taskName: string;
};
export type ThreadedHeadlessTask<Payload = unknown> = (
context: ThreadedHeadlessTaskContext<Payload>,
export type ThreadedHeadlessTask<
TPayload extends JsonValue | void = void,
> = (
context: ThreadedHeadlessTaskContext<TPayload>,
) => void | Promise<void>;

const threadedComponents: ThreadedComponentRegistry = new Map();
Expand Down Expand Up @@ -95,6 +100,24 @@ type ThreadedRuntimeNativeModule = {
getRuntimeNames?: () => Promise<string[]>;
};

export type ThreadableFunctionResult<
TReturn extends JsonValue | void = JsonValue | void,
> = TReturn | Promise<TReturn>;

export type ThreadableFunction<
TArgs extends readonly JsonValue[] = readonly JsonValue[],
TReturn extends JsonValue | void = JsonValue | void,
> = (...args: TArgs) => ThreadableFunctionResult<TReturn>;

/** Args for `T` — use instead of `Parameters<T>` (which widens to `unknown[]`). */
export type ThreadableFunctionArgs<T extends ThreadableFunction> =
T extends ThreadableFunction<infer TArgs extends readonly JsonValue[], any>
? TArgs
: readonly JsonValue[];

export type ThreadableFunctionAwaitedReturn<T extends ThreadableFunction> =
Awaited<ReturnType<T>>;

const nativeRuntime = NativeModules.ThreadedRuntime as
| ThreadedRuntimeNativeModule
| undefined;
Expand Down Expand Up @@ -205,43 +228,49 @@ export type ThreadedRuntimePrewarmOptions = {
kind?: string;
useMainNativeModules?: boolean;
};
export type ThreadedHeadlessTaskOptions<Payload = unknown> = {
payload?: Payload;
export type ThreadedHeadlessTaskOptions<
TPayload extends JsonValue | void = void,
> = {
payload?: TPayload;
runtimeName?: ThreadedRuntimeName;
};

type AnyFunction = (...args: any[]) => any;

export type RuntimeFunctionMetadata = {
id: string;
};

export type RuntimeFunction<TFunction extends AnyFunction> = TFunction & {
export type RuntimeFunction<
TArgs extends readonly JsonValue[] = readonly JsonValue[],
TReturn extends JsonValue | void = JsonValue | void,
> = ThreadableFunction<TArgs, TReturn> & {
__runtimeFunction?: RuntimeFunctionMetadata;
runOn(
runtimeName: ThreadedRuntimeName,
...args: Parameters<TFunction>
): Promise<Awaited<ReturnType<TFunction>>>;
...args: TArgs
): Promise<Awaited<TReturn>>;
};

export type RuntimeFunctionCallBuilder<TFunction extends AnyFunction> = {
export type RuntimeFunctionCallBuilder<
TArgs extends readonly JsonValue[] = readonly JsonValue[],
TReturn extends JsonValue | void = JsonValue | void,
> = {
on(
runtimeName: ThreadedRuntimeName,
): (
...args: Parameters<TFunction>
) => Promise<Awaited<ReturnType<TFunction>>>;
): (...args: TArgs) => Promise<Awaited<TReturn>>;
};

export type RuntimeFunctionFactory = {
<TFunction extends AnyFunction>(fn: TFunction): RuntimeFunction<TFunction>;
withId<TFunction extends AnyFunction>(
<TArgs extends readonly JsonValue[], TReturn extends JsonValue | void>(
fn: ThreadableFunction<TArgs, TReturn>,
): RuntimeFunction<TArgs, TReturn>;
withId<TArgs extends readonly JsonValue[], TReturn extends JsonValue | void>(
id: string,
fn: TFunction,
): RuntimeFunction<TFunction>;
named<TFunction extends AnyFunction>(
fn: ThreadableFunction<TArgs, TReturn>,
): RuntimeFunction<TArgs, TReturn>;
named<TArgs extends readonly JsonValue[], TReturn extends JsonValue | void>(
id: string,
fn: TFunction,
): RuntimeFunction<TFunction>;
fn: ThreadableFunction<TArgs, TReturn>,
): RuntimeFunction<TArgs, TReturn>;
};

export type ThreadedProps<Props extends object = Record<string, never>> = {
Expand Down Expand Up @@ -295,16 +324,21 @@ export function registerLazyThreadedComponent<Props extends object>(
threadedComponents.set(name, loadComponent as ThreadedComponentLoader);
}

export function registerThreadedHeadlessTask<Payload = unknown>(
export function registerThreadedHeadlessTask<
TPayload extends JsonValue | void = void,
>(
name: string,
task: ThreadedHeadlessTask<Payload>,
task: ThreadedHeadlessTask<TPayload>,
) {
threadedHeadlessTasks.set(name, task as ThreadedHeadlessTask<any>);
}

export function registerRuntimeFunction<TFunction extends AnyFunction>(
export function registerRuntimeFunction<
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
id: string,
loadFunction: () => RuntimeFunction<TFunction>,
loadFunction: () => RuntimeFunction<TArgs, TReturn>,
) {
installRuntimeFunctionJsi();
runtimeFunctions.set(id, loadFunction as RuntimeFunctionLoader);
Expand All @@ -314,36 +348,49 @@ export function registerRuntimeFunction<TFunction extends AnyFunction>(
);
}

function attachRuntimeFunction<TFunction extends AnyFunction>(
function attachRuntimeFunction<
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
id: string | null,
fn: TFunction,
): RuntimeFunction<TFunction> {
const runtimeFn = fn as RuntimeFunction<TFunction>;
fn: ThreadableFunction<TArgs, TReturn>,
): RuntimeFunction<TArgs, TReturn> {
const runtimeFn = fn as RuntimeFunction<TArgs, TReturn>;
if (id) {
runtimeFn.__runtimeFunction = { id };
}
runtimeFn.runOn = (runtimeName, ...args) =>
runtimeFn.runOn = (runtimeName, ...args: TArgs) =>
ThreadedRuntime.run(runtimeName, runtimeFn, ...args);
return runtimeFn;
}

const createRuntimeFunction = <TFunction extends AnyFunction>(
fn: TFunction,
): RuntimeFunction<TFunction> => attachRuntimeFunction(null, fn);
const createRuntimeFunction = <
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
fn: ThreadableFunction<TArgs, TReturn>,
): RuntimeFunction<TArgs, TReturn> => attachRuntimeFunction(null, fn);

createRuntimeFunction.withId = function runtimeFunctionWithId<
TFunction extends AnyFunction,
>(id: string, fn: TFunction): RuntimeFunction<TFunction> {
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
id: string,
fn: ThreadableFunction<TArgs, TReturn>,
): RuntimeFunction<TArgs, TReturn> {
return attachRuntimeFunction(id, fn);
};

createRuntimeFunction.named = createRuntimeFunction.withId;

export const runtimeFunction = createRuntimeFunction as RuntimeFunctionFactory;

export function call<TFunction extends AnyFunction>(
fn: RuntimeFunction<TFunction>,
): RuntimeFunctionCallBuilder<TFunction> {
export function call<
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
fn: RuntimeFunction<TArgs, TReturn>,
): RuntimeFunctionCallBuilder<TArgs, TReturn> {
return {
on(runtimeName) {
return (...args) => ThreadedRuntime.run(runtimeName, fn, ...args);
Expand Down Expand Up @@ -571,6 +618,11 @@ async function runRegisteredRuntimeFunction(
const result = await Promise.resolve(
callRegisteredRuntimeFunction(functionId, argsJson),
);

if (result !== undefined) {
assertJson(result, { for: 'result', id: functionId });
}

await completeRuntimeFunctionCall(
callId,
JSON.stringify(result ?? null),
Expand Down Expand Up @@ -700,15 +752,20 @@ export const ThreadedRuntime = {
});
},

runHeadlessTask<Payload = unknown>(
runHeadlessTask<TPayload extends JsonValue | void = void>(
taskName: string,
options: ThreadedHeadlessTaskOptions<Payload> = {},
options: ThreadedHeadlessTaskOptions<TPayload> = {},
) {
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
return Promise.resolve();
}

const runtimeName = options.runtimeName ?? DEFAULT_RUNTIME_NAME;

if (options.payload !== undefined) {
assertJson(options.payload, { for: 'payload', id: taskName });
}

const payloadJson = JSON.stringify(options.payload ?? null);
const nativeDispatch =
nativeRuntime?.dispatchHeadlessTask ?? nativeRuntime?.runHeadlessTask;
Expand All @@ -722,27 +779,30 @@ export const ThreadedRuntime = {
return nativeDispatch(runtimeName, taskName, payloadJson);
},

async run<TFunction extends AnyFunction>(
async run<
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
runtimeName: ThreadedRuntimeName,
fn: RuntimeFunction<TFunction>,
...args: Parameters<TFunction>
): Promise<Awaited<ReturnType<TFunction>>> {
fn: RuntimeFunction<TArgs, TReturn>,
...args: TArgs
): Promise<Awaited<TReturn>> {
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
return Promise.resolve(fn(...args)) as Promise<
Awaited<ReturnType<TFunction>>
>;
return Promise.resolve(fn(...args)) as Promise<Awaited<TReturn>>;
}

const functionId = fn.__runtimeFunction?.id;
if (!functionId) {
return Promise.reject<Awaited<ReturnType<TFunction>>>(
return Promise.reject<Awaited<TReturn>>(
new Error(
'Runtime function is missing generated metadata. Make sure it is ' +
'exported as runtimeFunction(...) and Metro uses withThreadedRuntime(...).',
),
);
}

assertJson(args, { id: functionId, for: 'arguments' });

const argsJson = JSON.stringify(args);
const runtimeNitro = getRuntimeFunctionsNitro();
if (runtimeNitro?.run) {
Expand All @@ -752,7 +812,7 @@ export const ThreadedRuntime = {
functionId,
argsJson,
);
return JSON.parse(resultJson) as Awaited<ReturnType<TFunction>>;
return JSON.parse(resultJson) as Awaited<TReturn>;
} catch (error) {
if (!isRuntimeDispatcherMissing(error)) {
throw error;
Expand All @@ -762,7 +822,7 @@ export const ThreadedRuntime = {

const callRuntimeFunction = nativeRuntime?.callRuntimeFunction;
if (!callRuntimeFunction) {
return Promise.reject<Awaited<ReturnType<TFunction>>>(
return Promise.reject<Awaited<TReturn>>(
new Error(
'ThreadedRuntime native module does not support runtime functions',
),
Expand All @@ -774,22 +834,28 @@ export const ThreadedRuntime = {
functionId,
argsJson,
);
return JSON.parse(resultJson) as Awaited<ReturnType<TFunction>>;
return JSON.parse(resultJson) as Awaited<TReturn>;
},

call<TFunction extends AnyFunction>(
call<
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
runtimeName: ThreadedRuntimeName,
fn: RuntimeFunction<TFunction>,
...args: Parameters<TFunction>
fn: RuntimeFunction<TArgs, TReturn>,
...args: TArgs
) {
return ThreadedRuntime.run(runtimeName, fn, ...args);
},

runtime(runtimeName: ThreadedRuntimeName) {
return {
run<TFunction extends AnyFunction>(
fn: RuntimeFunction<TFunction>,
...args: Parameters<TFunction>
run<
TArgs extends readonly JsonValue[],
TReturn extends JsonValue | void,
>(
fn: RuntimeFunction<TArgs, TReturn>,
...args: TArgs
) {
return ThreadedRuntime.run(runtimeName, fn, ...args);
},
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ export {
type RuntimeFunctionCallBuilder,
type RuntimeFunctionFactory,
type RuntimeFunctionMetadata,
} from './ThreadedRuntime';
type ThreadableFunction,
type ThreadableFunctionArgs,
type ThreadableFunctionAwaitedReturn,
type ThreadableFunctionResult,
} from './ThreadedRuntime';
44 changes: 44 additions & 0 deletions packages/core/src/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export type JsonPrimitive = string | number | boolean | null;

export type JsonValue = JsonPrimitive | JsonObject | JsonArray;

export type JsonObject = {
readonly [key: string]: JsonValue;
};

export type JsonArray = readonly JsonValue[];

const validateJson = (value: unknown): value is JsonValue => {
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value === null
) {
return true;
}
if (Array.isArray(value)) {
return value.every(validateJson);
}
if (typeof value === 'object' && value !== null) {
return Object.values(value).every(validateJson);
}

return false;
};

export type AssertJsonContext = {
readonly id: string;
readonly for: 'arguments' | 'payload' | 'result';
};

export function assertJson(
value: unknown,
context: AssertJsonContext,
): asserts value is JsonValue {
if (__DEV__) {
if (!validateJson(value)) {
throw new Error(`Invalid JSON value for ${context.for} of "${context.id}"`);
}
}
}