now that we have a cohesive look at workflow APIs, some issues remain:
createActivityHandle
isnt really a handle, in the way thatcreateWorkflowHandle
andcreateChildWorkflowHandle
are. 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/Query
dont add much value since they just create an object- extra
setListener
api that is doing the real work, basically 2 different functions branching bydef.type
- extra
just taking another crack at api design.
simplified query and signal api
status quo
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;
}
}
proposal
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);
// ...
}
renamed Activity api
status quo
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
activities
file (not really, but people will treat it that way)... would be nice to let people componentize or combine as they wish
proposal
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
setListener
intosetQueryListener
andsetSignalListener
.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
activityId
tocreateActivityHandle
will 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
activityTypeProxy
makes 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
/signal
methods will not be implemented in the first step, they can be added later when activities support signaling.