@temporalio/client
Version:
Temporal.io SDK Client sub-package
393 lines (367 loc) • 14.3 kB
text/typescript
import Long from 'long'; // eslint-disable-line import/no-named-as-default
import {
compilePriority,
compileRetryPolicy,
decodePriority,
decompileRetryPolicy,
extractWorkflowType,
LoadedDataConverter,
} from '@temporalio/common';
import { encodeUserMetadata, decodeUserMetadata } from '@temporalio/common/lib/internal-non-workflow/codec-helpers';
import {
encodeUnifiedSearchAttributes,
decodeSearchAttributes,
decodeTypedSearchAttributes,
} from '@temporalio/common/lib/converter/payload-search-attributes';
import { Headers } from '@temporalio/common/lib/interceptors';
import {
decodeArrayFromPayloads,
decodeMapFromPayloads,
encodeMapToPayloads,
encodeToPayloads,
} from '@temporalio/common/lib/internal-non-workflow';
import { temporal } from '@temporalio/proto';
import {
msOptionalToTs,
msToTs,
optionalDateToTs,
optionalTsToDate,
optionalTsToMs,
requiredTsToDate,
} from '@temporalio/common/lib/time';
import {
CalendarSpec,
CalendarSpecDescription,
CompiledScheduleOptions,
CompiledScheduleUpdateOptions,
Range,
ScheduleOptions,
ScheduleUpdateOptions,
DayOfWeek,
DAYS_OF_WEEK,
Month,
MONTHS,
LooseRange,
ScheduleSpec,
CompiledScheduleAction,
ScheduleSpecDescription,
IntervalSpecDescription,
ScheduleDescriptionAction,
ScheduleExecutionActionResult,
ScheduleExecutionResult,
ScheduleExecutionStartWorkflowActionResult,
encodeScheduleOverlapPolicy,
} from './schedule-types';
const [encodeSecond, decodeSecond] = makeCalendarSpecFieldCoders(
'second',
(x: number) => (typeof x === 'number' && x >= 0 && x <= 59 ? x : undefined),
(x: number) => x,
[{ start: 0, end: 0, step: 0 }], // default to 0
[{ start: 0, end: 59, step: 1 }]
);
const [encodeMinute, decodeMinue] = makeCalendarSpecFieldCoders(
'minute',
(x: number) => (typeof x === 'number' && x >= 0 && x <= 59 ? x : undefined),
(x: number) => x,
[{ start: 0, end: 0, step: 0 }], // default to 0
[{ start: 0, end: 59, step: 1 }]
);
const [encodeHour, decodeHour] = makeCalendarSpecFieldCoders(
'hour',
(x: number) => (typeof x === 'number' && x >= 0 && x <= 23 ? x : undefined),
(x: number) => x,
[{ start: 0, end: 0, step: 0 }], // default to 0
[{ start: 0, end: 23, step: 1 }]
);
const [encodeDayOfMonth, decodeDayOfMonth] = makeCalendarSpecFieldCoders(
'dayOfMonth',
(x: number) => (typeof x === 'number' && x >= 0 && x <= 31 ? x : undefined),
(x: number) => x,
[{ start: 1, end: 31, step: 1 }], // default to *
[{ start: 1, end: 31, step: 1 }]
);
const [encodeMonth, decodeMonth] = makeCalendarSpecFieldCoders(
'month',
function monthNameToNumber(month: Month): number | undefined {
const index = MONTHS.indexOf(month);
return index >= 0 ? index + 1 : undefined;
},
(month: number) => MONTHS[month - 1],
[{ start: 1, end: 12, step: 1 }], // default to *
[{ start: 1, end: 12, step: 1 }]
);
const [encodeYear, decodeYear] = makeCalendarSpecFieldCoders(
'year',
(x: number) => (typeof x === 'number' ? x : undefined),
(x: number) => x,
[], // default to *
[] // special case: * for years is encoded as no range at all
);
const [encodeDayOfWeek, decodeDayOfWeek] = makeCalendarSpecFieldCoders(
'dayOfWeek',
function dayOfWeekNameToNumber(day: DayOfWeek): number | undefined {
const index = DAYS_OF_WEEK.indexOf(day);
return index >= 0 ? index : undefined;
},
(day: number) => DAYS_OF_WEEK[day],
[{ start: 0, end: 6, step: 1 }], // default to *
[{ start: 0, end: 6, step: 1 }]
);
function makeCalendarSpecFieldCoders<Unit>(
fieldName: string,
encodeValueFn: (x: Unit) => number | undefined,
decodeValueFn: (x: number) => Unit,
defaultValue: temporal.api.schedule.v1.IRange[],
matchAllValue: temporal.api.schedule.v1.IRange[]
) {
function encoder(
input: LooseRange<Unit> | LooseRange<Unit>[] | '*' | undefined
): temporal.api.schedule.v1.IRange[] | undefined {
if (input === undefined) return defaultValue;
if (input === '*') return matchAllValue;
return (Array.isArray(input) ? input : [input]).map((item) => {
if (typeof item === 'object' && (item as Range<Unit>).start !== undefined) {
const range = item as Range<Unit>;
const start = encodeValueFn(range.start);
if (start !== undefined) {
return {
start,
end: range.end !== undefined ? encodeValueFn(range.end) ?? start : 1,
step: typeof range.step === 'number' && range.step > 0 ? range.step : 1,
};
}
}
if (item !== undefined) {
const value = encodeValueFn(item as Unit);
if (value !== undefined) return { start: value, end: value, step: 1 };
}
throw new TypeError(`Invalid CalendarSpec component for field ${fieldName}: '${item}' of type '${typeof item}'`);
});
}
function decoder(input: temporal.api.schedule.v1.IRange[] | undefined | null): Range<Unit>[] {
if (!input) return [];
return (input as temporal.api.schedule.v1.Range[]).map((pb): Range<Unit> => {
const start = decodeValueFn(pb.start);
const end = pb.end > pb.start ? decodeValueFn(pb.end) ?? start : start;
const step = pb.step > 0 ? pb.step : 1;
return { start, end, step };
});
}
return [encoder, decoder] as const;
}
export function encodeOptionalStructuredCalendarSpecs(
input: CalendarSpec[] | null | undefined
): temporal.api.schedule.v1.IStructuredCalendarSpec[] | undefined {
if (!input) return undefined;
return input.map((spec) => ({
second: encodeSecond(spec.second),
minute: encodeMinute(spec.minute),
hour: encodeHour(spec.hour),
dayOfMonth: encodeDayOfMonth(spec.dayOfMonth),
month: encodeMonth(spec.month),
year: encodeYear(spec.year),
dayOfWeek: encodeDayOfWeek(spec.dayOfWeek),
comment: spec.comment,
}));
}
export function decodeOptionalStructuredCalendarSpecs(
input: temporal.api.schedule.v1.IStructuredCalendarSpec[] | null | undefined
): CalendarSpecDescription[] {
if (!input) return [];
return (input as temporal.api.schedule.v1.StructuredCalendarSpec[]).map(
(pb): CalendarSpecDescription => ({
second: decodeSecond(pb.second),
minute: decodeMinue(pb.minute),
hour: decodeHour(pb.hour),
dayOfMonth: decodeDayOfMonth(pb.dayOfMonth),
month: decodeMonth(pb.month),
year: decodeYear(pb.year),
dayOfWeek: decodeDayOfWeek(pb.dayOfWeek),
comment: pb.comment,
})
);
}
export function compileScheduleOptions(options: ScheduleOptions): CompiledScheduleOptions {
const workflowType = extractWorkflowType(options.action.workflowType);
return {
...options,
action: {
...options.action,
workflowId: options.action.workflowId ?? `${options.scheduleId}-workflow`,
workflowType,
args: (options.action.args ?? []) as unknown[],
},
};
}
export function compileUpdatedScheduleOptions(
scheduleId: string,
options: ScheduleUpdateOptions
): CompiledScheduleUpdateOptions {
const workflowTypeOrFunc = options.action.workflowType;
const workflowType = extractWorkflowType(workflowTypeOrFunc);
return {
...options,
action: {
...options.action,
workflowId: options.action.workflowId ?? `${scheduleId}-workflow`,
workflowType,
args: (options.action.args ?? []) as unknown[],
},
};
}
export function encodeScheduleSpec(spec: ScheduleSpec): temporal.api.schedule.v1.IScheduleSpec {
return {
structuredCalendar: encodeOptionalStructuredCalendarSpecs(spec.calendars),
interval: spec.intervals?.map((interval) => ({
interval: msToTs(interval.every),
phase: msOptionalToTs(interval.offset),
})),
cronString: spec.cronExpressions,
excludeStructuredCalendar: encodeOptionalStructuredCalendarSpecs(spec.skip),
startTime: optionalDateToTs(spec.startAt),
endTime: optionalDateToTs(spec.endAt),
jitter: msOptionalToTs(spec.jitter),
timezoneName: spec.timezone,
};
}
export async function encodeScheduleAction(
dataConverter: LoadedDataConverter,
action: CompiledScheduleAction,
headers: Headers
): Promise<temporal.api.schedule.v1.IScheduleAction> {
return {
startWorkflow: {
workflowId: action.workflowId,
workflowType: {
name: action.workflowType,
},
input: { payloads: await encodeToPayloads(dataConverter, ...action.args) },
taskQueue: {
kind: temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL,
name: action.taskQueue,
},
workflowExecutionTimeout: msOptionalToTs(action.workflowExecutionTimeout),
workflowRunTimeout: msOptionalToTs(action.workflowRunTimeout),
workflowTaskTimeout: msOptionalToTs(action.workflowTaskTimeout),
retryPolicy: action.retry ? compileRetryPolicy(action.retry) : undefined,
memo: action.memo ? { fields: await encodeMapToPayloads(dataConverter, action.memo) } : undefined,
searchAttributes:
action.searchAttributes || action.typedSearchAttributes // eslint-disable-line deprecation/deprecation
? {
indexedFields: encodeUnifiedSearchAttributes(action.searchAttributes, action.typedSearchAttributes), // eslint-disable-line deprecation/deprecation
}
: undefined,
header: { fields: headers },
userMetadata: await encodeUserMetadata(dataConverter, action.staticSummary, action.staticDetails),
priority: action.priority ? compilePriority(action.priority) : undefined,
},
};
}
export function encodeSchedulePolicies(
policies?: ScheduleOptions['policies']
): temporal.api.schedule.v1.ISchedulePolicies {
return {
catchupWindow: msOptionalToTs(policies?.catchupWindow),
overlapPolicy: policies?.overlap ? encodeScheduleOverlapPolicy(policies.overlap) : undefined,
pauseOnFailure: policies?.pauseOnFailure,
};
}
export function encodeScheduleState(state?: ScheduleOptions['state']): temporal.api.schedule.v1.IScheduleState {
return {
paused: state?.paused,
notes: state?.note,
limitedActions: state?.remainingActions !== undefined,
remainingActions: state?.remainingActions ? Long.fromNumber(state?.remainingActions) : undefined,
};
}
export function decodeScheduleSpec(pb: temporal.api.schedule.v1.IScheduleSpec): ScheduleSpecDescription {
// Note: the server will have compiled calendar and cron_string fields into
// structured_calendar (and maybe interval and timezone_name), so at this
// point, we'll see only structured_calendar, interval, etc.
return {
calendars: decodeOptionalStructuredCalendarSpecs(pb.structuredCalendar),
intervals: (pb.interval ?? []).map(
(x) =>
<IntervalSpecDescription>{
every: optionalTsToMs(x.interval),
offset: optionalTsToMs(x.phase),
}
),
skip: decodeOptionalStructuredCalendarSpecs(pb.excludeStructuredCalendar),
startAt: optionalTsToDate(pb.startTime),
endAt: optionalTsToDate(pb.endTime),
jitter: optionalTsToMs(pb.jitter),
timezone: pb.timezoneName ?? undefined,
};
}
export async function decodeScheduleAction(
dataConverter: LoadedDataConverter,
pb: temporal.api.schedule.v1.IScheduleAction
): Promise<ScheduleDescriptionAction> {
if (pb.startWorkflow) {
const { staticSummary, staticDetails } = await decodeUserMetadata(dataConverter, pb.startWorkflow?.userMetadata);
return {
type: 'startWorkflow',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowId: pb.startWorkflow.workflowId!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowType: pb.startWorkflow.workflowType!.name!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
taskQueue: pb.startWorkflow.taskQueue!.name!,
args: await decodeArrayFromPayloads(dataConverter, pb.startWorkflow.input?.payloads),
memo: await decodeMapFromPayloads(dataConverter, pb.startWorkflow.memo?.fields),
retry: decompileRetryPolicy(pb.startWorkflow.retryPolicy),
searchAttributes: decodeSearchAttributes(pb.startWorkflow.searchAttributes?.indexedFields),
typedSearchAttributes: decodeTypedSearchAttributes(pb.startWorkflow.searchAttributes?.indexedFields),
workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout),
workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout),
workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout),
staticSummary,
staticDetails,
priority: decodePriority(pb.startWorkflow.priority),
};
}
throw new TypeError('Unsupported schedule action');
}
export function decodeScheduleRunningActions(
pb?: temporal.api.common.v1.IWorkflowExecution[] | null
): ScheduleExecutionStartWorkflowActionResult[] {
if (!pb) return [];
return pb.map(
(x): ScheduleExecutionStartWorkflowActionResult => ({
type: 'startWorkflow',
workflow: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowId: x.workflowId!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
firstExecutionRunId: x.runId!,
},
})
);
}
export function decodeScheduleRecentActions(
pb?: temporal.api.schedule.v1.IScheduleActionResult[] | null
): ScheduleExecutionResult[] {
if (!pb) return [];
return (pb as Required<temporal.api.schedule.v1.IScheduleActionResult>[]).map(
(executionResult): ScheduleExecutionResult => {
let action: ScheduleExecutionActionResult | undefined;
if (executionResult.startWorkflowResult) {
action = {
type: 'startWorkflow',
workflow: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
workflowId: executionResult.startWorkflowResult!.workflowId!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
firstExecutionRunId: executionResult.startWorkflowResult!.runId!,
},
};
} else throw new TypeError('Unsupported schedule action');
return {
scheduledAt: requiredTsToDate(executionResult.scheduleTime, 'scheduleTime'),
takenAt: requiredTsToDate(executionResult.actualTime, 'actualTime'),
action,
};
}
);
}