Skip to content

Commit 4155dad

Browse files
authored
add default query handler (#1639)
1 parent eb67d04 commit 4155dad

File tree

4 files changed

+94
-3
lines changed

4 files changed

+94
-3
lines changed

packages/test/src/test-integration-split-two.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { msToNumber, tsToMs } from '@temporalio/common/lib/time';
1515
import { decode as payloadDecode, decodeFromPayloadsAtIndex } from '@temporalio/common/lib/internal-non-workflow';
1616

17-
import { condition, defineQuery, setHandler, sleep } from '@temporalio/workflow';
17+
import { condition, defineQuery, defineSignal, setDefaultQueryHandler, setHandler, sleep } from '@temporalio/workflow';
1818
import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration';
1919
import * as activities from './activities';
2020
import * as workflows from './workflows';
@@ -697,3 +697,57 @@ test('Query does not cause condition to be triggered', configMacro, async (t, co
697697
// Worker did not crash
698698
t.pass();
699699
});
700+
701+
const completeSignal = defineSignal('complete');
702+
const definedQuery = defineQuery<QueryNameAndArgs>('query-handler-type');
703+
704+
interface QueryNameAndArgs {
705+
name: string;
706+
queryName?: string;
707+
args: any[];
708+
}
709+
710+
export async function workflowWithMaybeDefinedQuery(useDefinedQuery: boolean): Promise<void> {
711+
let complete = false;
712+
setHandler(completeSignal, () => {
713+
complete = true;
714+
});
715+
setDefaultQueryHandler((queryName: string, ...args: any[]) => {
716+
return { name: 'default', queryName, args };
717+
});
718+
if (useDefinedQuery) {
719+
setHandler(definedQuery, (...args: any[]) => {
720+
return { name: definedQuery.name, args };
721+
});
722+
}
723+
724+
await condition(() => complete);
725+
}
726+
727+
test('default query handler is used if requested query does not exist', configMacro, async (t, config) => {
728+
const { env, createWorkerWithDefaults } = config;
729+
const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env);
730+
const worker = await createWorkerWithDefaults(t, { activities });
731+
const handle = await startWorkflow(workflowWithMaybeDefinedQuery, {
732+
args: [false],
733+
});
734+
await worker.runUntil(async () => {
735+
const args = ['test', 'args'];
736+
const result = await handle.query(definedQuery, ...args);
737+
t.deepEqual(result, { name: 'default', queryName: definedQuery.name, args });
738+
});
739+
});
740+
741+
test('default query handler is not used if requested query exists', configMacro, async (t, config) => {
742+
const { env, createWorkerWithDefaults } = config;
743+
const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env);
744+
const worker = await createWorkerWithDefaults(t, { activities });
745+
const handle = await startWorkflow(workflowWithMaybeDefinedQuery, {
746+
args: [true],
747+
});
748+
await worker.runUntil(async () => {
749+
const args = ['test', 'args'];
750+
const result = await handle.query('query-handler-type', ...args);
751+
t.deepEqual(result, { name: definedQuery.name, args });
752+
});
753+
});

packages/workflow/src/interfaces.ts

+5
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,11 @@ export type Handler<
538538
*/
539539
export type DefaultSignalHandler = (signalName: string, ...args: unknown[]) => void | Promise<void>;
540540

541+
/**
542+
* A handler function accepting query calls for non-registered query names.
543+
*/
544+
export type DefaultQueryHandler = (queryName: string, ...args: unknown[]) => unknown;
545+
541546
/**
542547
* A validation function capable of accepting the arguments for a given UpdateDefinition.
543548
*/

packages/workflow/src/internals.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
WorkflowInfo,
4242
WorkflowCreateOptionsInternal,
4343
ActivationCompletion,
44+
DefaultQueryHandler,
4445
} from './interfaces';
4546
import { type SinkCall } from './sinks';
4647
import { untrackPromise } from './stack-helpers';
@@ -189,6 +190,11 @@ export class Activator implements ActivationHandler {
189190
*/
190191
defaultSignalHandler?: DefaultSignalHandler;
191192

193+
/**
194+
* A query handler that catches calls for non-registered query names.
195+
*/
196+
defaultQueryHandler?: DefaultQueryHandler;
197+
192198
/**
193199
* Source map file for looking up the source files in response to __enhanced_stack_trace
194200
*/
@@ -611,7 +617,11 @@ export class Activator implements ActivationHandler {
611617

612618
// Intentionally non-async function so this handler doesn't show up in the stack trace
613619
protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
614-
const fn = this.queryHandlers.get(queryName)?.handler;
620+
let fn = this.queryHandlers.get(queryName)?.handler;
621+
if (fn === undefined && this.defaultQueryHandler !== undefined) {
622+
fn = this.defaultQueryHandler.bind(this, queryName);
623+
}
624+
// No handler or default registered, fail.
615625
if (fn === undefined) {
616626
const knownQueryTypes = [...this.queryHandlers.keys()].join(' ');
617627
// Fail the query
@@ -621,6 +631,7 @@ export class Activator implements ActivationHandler {
621631
)
622632
);
623633
}
634+
// Execute handler.
624635
try {
625636
const ret = fn(...args);
626637
if (ret instanceof Promise) {

packages/workflow/src/workflow.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
UpdateInfo,
5151
encodeChildWorkflowCancellationType,
5252
encodeParentClosePolicy,
53+
DefaultQueryHandler,
5354
} from './interfaces';
5455
import { LocalActivityDoBackoff } from './errors';
5556
import { assertInWorkflowContext, getActivator, maybeGetActivator } from './global-attributes';
@@ -1300,7 +1301,7 @@ export function setHandler<
13001301
*
13011302
* Signals are dispatched to the default signal handler in the order that they were accepted by the server.
13021303
*
1303-
* If this function is called multiple times for a given signal or query name the last handler will overwrite any previous calls.
1304+
* If this function is called multiple times for a given signal name the last handler will overwrite any previous calls.
13041305
*
13051306
* @param handler a function that will handle signals for non-registered signal names, or `undefined` to unset the handler.
13061307
*/
@@ -1318,6 +1319,26 @@ export function setDefaultSignalHandler(handler: DefaultSignalHandler | undefine
13181319
}
13191320
}
13201321

1322+
/**
1323+
* Set a query handler function that will handle query calls for non-registered query names.
1324+
*
1325+
* Queries are dispatched to the default query handler in the order that they were accepted by the server.
1326+
*
1327+
* If this function is called multiple times for a given query name the last handler will overwrite any previous calls.
1328+
*
1329+
* @param handler a function that will handle queries for non-registered query names, or `undefined` to unset the handler.
1330+
*/
1331+
export function setDefaultQueryHandler(handler: DefaultQueryHandler | undefined): void {
1332+
const activator = assertInWorkflowContext(
1333+
'Workflow.setDefaultQueryHandler(...) may only be used from a Workflow Execution.'
1334+
);
1335+
if (typeof handler === 'function' || handler === undefined) {
1336+
activator.defaultQueryHandler = handler;
1337+
} else {
1338+
throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
1339+
}
1340+
}
1341+
13211342
/**
13221343
* Updates this Workflow's Search Attributes by merging the provided `searchAttributes` with the existing Search
13231344
* Attributes, `workflowInfo().searchAttributes`.

0 commit comments

Comments
 (0)