now that we have a cohesive look at workflow APIs, some issues remain:
createActivityHandleisnt really a handle, in the way thatcreateWorkflowHandleandcreateChildWorkflowHandleare. it returns a function that you call.- users are confused by the proxy destructure which a very fancy way of doing type safety and ensuring consistent naming
defineSignal/Querydont add much value since they just create an object- extra
setListenerapi that is doing the real work, basically 2 different functions branching bydef.type
- extra
just taking another crack at api design.
export const unblockSignal = wf.defineSignal('unblock');
export const isBlockedQuery = wf.defineQuery('isBlocked');
export async function unblockOrCancel() {
let isBlocked = true;
wf.setListener(unblockSignal, () => void (isBlocked = false));
wf.setListener(isBlockedQuery, () => isBlocked);
console.log('Blocked');
try {
await wf.condition(() => !isBlocked);
console.log('Unblocked');
}
catch (err) {
if (err instanceof wf.CancelledFailure) {
console.log('Cancelled');
}
throw err;
}
}if we eliminate setListener...
// using exportable definitions
export const onSetState = useSignal<boolean>('setState');
export const getIsBlocked = useQuery<boolean>('isBlocked');
export async function unblockOrCancel() {
let isBlocked = true;
onSetState((newState) => void (isBlocked = newState)); // boolean
getIsBlocked(() => isBlocked);
// ...
}if they dont like on (because of the subscribe implication) they can name it setStateHandler or BlockedQueryResolver or whatever they like... we dont prescribe the naming.
for those who
- don't need to export the signaldef/querydefs for strong typing for invocation (eg in the nextjs example its pretty inconvenient to import the types from the temporal folder into the nextjs folder, most people wont even bother)
- don't need to reassign the listener
this enables inlining and reduces naming need:
// using strings
export async function unblockOrCancel() {
let isBlocked = true;
useSignal('unblock', () => void (isBlocked = false));
useQuery('isBlocked', () => isBlocked);
// ...
}import { createActivityHandle } from '@temporalio/workflow';
import type * as activities from './activities';
const { greet } = createActivityHandle<typeof activities>({
startToCloseTimeout: '1 minute',
});
/** A workflow that simply calls an activity */
export async function example(name) {
return await greet(name);
}- this is decent tbh, but for some people (who would like to use typescript, but are not typescript gods),
<typeof activities>is a foreign language and hard to decipher. - i am worried that people will just copy paste this and not really intuitively understand how to manipulate activities to suit their code style.
- it also requires people to barrel all types into a single
activitiesfile (not really, but people will treat it that way)... would be nice to let people componentize or combine as they wish
i'd like to make clear that this is "just" a function and that we are importing from worker that must have this activity registered.
import { useActivity } from '@temporalio/workflow';
import type greet from './activities/greet'; // very clear that barrel file is optional
const invokeGreet = useActivity<greet>('greet', {
startToCloseTimeout: '1 minute',
// retries, etc
});
/** A workflow that simply calls an activity */
export async function example(name) {
return await invokeGreet('world')
}this means that you cant do the fancy multiple destructures, but hopefully usage will be much simplier because "less magic"...
import { useActivity, ActivityOptions } from '@temporalio/workflow';
import type foo, bar, baz from './activities'
const options: ActivityOptions = {
startToCloseTimeout: '1 minute',
// retries, etc
}
const invokeFoo = useActivity<foo>('foo', options)
const invokeBar = useActivity<bar>('bar', options)
const invokeBaz = useActivity<baz>('baz', options)
/** A workflow that simply calls an activity */
export async function example(name) {
await invokeFoo('world')
await invokeBar('world')
await invokeBaz(123, 345)
}
Signals and Queries
For the first point, I think we clarified in the proposal why we want the definitions and why the definitions do not have a method to attach a listener.
After looking at Joe's pendulum code I think we can make a more JS friendly API if we break
setListenerintosetQueryListenerandsetSignalListener.Both of the proposed methods will accept a string as well as a definition.
Example for queries:
Activity Handles
As for the second point.
I've been thinking about this too.
There are a few drawbacks to how we create activity handles and how they are used.
a) Mixing the types with the handle creation is a bit confusing, and it could be simplified.
b) Passing an
activityIdtocreateActivityHandlewill cause all activities returned from that call to share the same activityId.c) In the future we will probably support signaling and activities from workflows, the current API will not allow this.
An alternative proposal is to split the activity creation and type inference helpers.
For TS users:
For JS users:
Notes
activityTypeProxymakes up for the fact that activity implementations cannot be imported directly from the workflow and let's the user infer both activity name and type.start/result/signalmethods will not be implemented in the first step, they can be added later when activities support signaling.