diff --git a/crates/bindings-typescript/src/react/index.ts b/crates/bindings-typescript/src/react/index.ts index b32fbdf4b80..3b3e64aba48 100644 --- a/crates/bindings-typescript/src/react/index.ts +++ b/crates/bindings-typescript/src/react/index.ts @@ -2,3 +2,4 @@ export * from './SpacetimeDBProvider.ts'; export { useSpacetimeDB } from './useSpacetimeDB.ts'; export { useTable } from './useTable.ts'; export { useReducer } from './useReducer.ts'; +export { useProcedure } from './useProcedure.ts'; diff --git a/crates/bindings-typescript/src/react/useProcedure.ts b/crates/bindings-typescript/src/react/useProcedure.ts new file mode 100644 index 00000000000..1f688335f34 --- /dev/null +++ b/crates/bindings-typescript/src/react/useProcedure.ts @@ -0,0 +1,60 @@ +import { useCallback, useEffect, useRef } from 'react'; +import type { UntypedProcedureDef } from '../sdk/procedures'; +import { useSpacetimeDB } from './useSpacetimeDB'; +import type { + ProcedureParamsType, + ProcedureReturnType, +} from '../sdk/type_utils'; + +export function useProcedure( + procedureDef: ProcedureDef +): ( + ...params: ProcedureParamsType +) => Promise> { + const { getConnection, isActive } = useSpacetimeDB(); + const procedureName = procedureDef.accessorName; + + // Holds calls made before the connection exists + const queueRef = useRef< + { + params: ProcedureParamsType; + resolve: (val: any) => void; + reject: (err: unknown) => void; + }[] + >([]); + + // Flush when we finally have a connection + useEffect(() => { + const conn = getConnection(); + if (!conn) { + return; + } + const fn = (conn.procedures as any)[procedureName] as ( + ...p: ProcedureParamsType + ) => Promise>; + if (queueRef.current.length) { + const pending = queueRef.current.splice(0); + for (const item of pending) { + fn(...item.params).then(item.resolve, item.reject); + } + } + }, [getConnection, procedureName, isActive]); + + return useCallback( + (...params: ProcedureParamsType) => { + const conn = getConnection(); + if (!conn) { + return new Promise>( + (resolve, reject) => { + queueRef.current.push({ params, resolve, reject }); + } + ); + } + const fn = (conn.procedures as any)[procedureName] as ( + ...p: ProcedureParamsType + ) => Promise>; + return fn(...params); + }, + [getConnection, procedureName] + ); +} diff --git a/crates/bindings-typescript/src/sdk/type_utils.ts b/crates/bindings-typescript/src/sdk/type_utils.ts index 2c767c73259..dcfc3332be1 100644 --- a/crates/bindings-typescript/src/sdk/type_utils.ts +++ b/crates/bindings-typescript/src/sdk/type_utils.ts @@ -1,5 +1,6 @@ -import type { InferTypeOfRow } from '.'; +import type { Infer, InferTypeOfRow } from '.'; import type { Prettify } from '../lib/type_util'; +import type { UntypedProcedureDef } from './procedures'; import type { UntypedReducerDef } from './reducers'; export type IsEmptyObject = [keyof T] extends [never] ? true : false; @@ -8,3 +9,11 @@ export type MaybeParams = IsEmptyObject extends true ? [] : [params: T]; export type ParamsType = MaybeParams< Prettify> >; + +export type ProcedureParamsType

= MaybeParams< + Prettify> +>; + +export type ProcedureReturnType

= Infer< + P['returnType'] +>; diff --git a/crates/codegen/src/typescript.rs b/crates/codegen/src/typescript.rs index 549530451df..507ed4b6593 100644 --- a/crates/codegen/src/typescript.rs +++ b/crates/codegen/src/typescript.rs @@ -295,6 +295,12 @@ impl Lang for TypeScript { out, "export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers);" ); + writeln!(out); + writeln!(out, "/** The procedures available in this remote SpacetimeDB module. */"); + writeln!( + out, + "export const procedures = __convertToAccessorMap(proceduresSchema.procedures);" + ); // Write type aliases for EventContext, ReducerEventContext, SubscriptionEventContext, ErrorContext writeln!(out);