/**
* use$ - Workflow context hook
*
* Provides access to the dotdo workflow context ($) for
* durable operations, event handlers, and scheduling.
*
* @example
* ```tsx
* import { use$ } from '@dotdo/react'
*
* function TaskActions({ taskId }: { taskId: string }) {
* const $ = use$()
*
* const handleComplete = async () => {
* // Durable operation with retries
* await $.do(async () => {
* await $.Task(taskId).complete()
* await $.notify.user('Task completed!')
* })
* }
*
* const handleScheduleReminder = async () => {
* // Schedule a future action
* await $.in('1 hour').remind.task(taskId)
* }
*
* return (
*
*
*
*
* )
* }
* ```
*/
import * as React from 'react'
import type { DOClient } from '@dotdo/client'
import { useDotdoContext } from '../context'
/**
* Workflow context interface
* Mirrors the server-side $ context for client-side operations
*/
export interface WorkflowContext {
/** Fire-and-forget event */
send: (event: T) => void
/** Single attempt operation */
try: (action: () => Promise) => Promise
/** Durable operation with retries */
do: (action: () => Promise) => Promise
/** Schedule an action for later */
in: (delay: string | number) => WorkflowContext
/** Access to underlying client for direct RPC */
client: DOClient
/** Dynamic property access for Noun.verb pattern */
[key: string]: unknown
}
/**
* Hook to access the workflow context ($).
*
* Provides a client-side mirror of the server-side $ context
* for durable operations and scheduling.
*
* @returns WorkflowContext instance
* @throws Error if used outside of DotdoProvider
*/
export function use$(): WorkflowContext {
const { client, ns } = useDotdoContext()
if (!client) {
throw new Error('Client not initialized. Ensure DO provider is properly configured.')
}
// Create the workflow context
const $ = React.useMemo(() => {
const rpcUrl = ns + '/rpc'
const rpc = async (method: string, args: unknown[]): Promise => {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: crypto.randomUUID(),
type: 'call',
calls: [{
promiseId: 'p-1',
target: { type: 'root' },
method,
args: args.map(arg => ({ type: 'value', value: arg })),
}],
}),
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const result = await response.json()
if (result.error) {
throw new Error(result.error.message)
}
return result.results?.[0]?.value
}
const context: WorkflowContext = {
client: client as DOClient,
send: (event: T) => {
// Fire-and-forget via RPC
rpc('$.send', [event]).catch(() => {
// Silently ignore errors for fire-and-forget
})
},
try: async (action: () => Promise): Promise => {
// Execute action with single attempt
return action()
},
do: async (action: () => Promise): Promise => {
// Execute via server-side $.do for durability
// For client-side, we just execute directly
// Real durability comes from server-side workflows
return action()
},
in: (delay: string | number) => {
// Create a scheduled context
const scheduledContext = { ...context }
// The actual scheduling happens server-side
// This returns a proxy that will send the scheduled call
return new Proxy(scheduledContext, {
get(_target, prop) {
if (typeof prop === 'string') {
return (...args: unknown[]) => {
return rpc('$.schedule', [{ delay, method: prop, args }])
}
}
return undefined
},
}) as WorkflowContext
},
}
// Create a proxy for dynamic Noun.verb access
return new Proxy(context, {
get(target, prop) {
// Return known properties
if (prop in target) {
return target[prop as keyof WorkflowContext]
}
// For unknown properties, create a Noun proxy
if (typeof prop === 'string') {
return (id?: string) => {
const nounPath = id ? `${prop}(${id})` : prop
return new Proxy({}, {
get(_nounTarget, verb) {
if (typeof verb === 'string') {
return (...args: unknown[]) => {
return rpc(`${nounPath}.${verb}`, args)
}
}
return undefined
},
})
}
}
return undefined
},
}) as WorkflowContext
}, [client, ns])
return $
}