@temporalio/common
Version:
Common library for code that's used across the Client, Worker, and/or Workflow
151 lines (134 loc) • 5.58 kB
text/typescript
import { fromProto3JSON, toProto3JSON } from 'proto3-json-serializer';
import * as proto from '@temporalio/proto';
import { patchProtobufRoot } from '@temporalio/proto/lib/patch-protobuf-root';
export type History = proto.temporal.api.history.v1.IHistory;
export type Payload = proto.temporal.api.common.v1.IPayload;
/**
* JSON representation of Temporal's {@link Payload} protobuf object
*/
export interface JSONPayload {
/**
* Mapping of key to base64 encoded value
*/
metadata?: Record<string, string> | null;
/**
* base64 encoded value
*/
data?: string | null;
}
// Cast to any because the generated proto module types are missing the lookupType method
const patched = patchProtobufRoot(proto) as any;
const historyType = patched.lookupType('temporal.api.history.v1.History');
const payloadType = patched.lookupType('temporal.api.common.v1.Payload');
/**
* Convert a proto JSON representation of History to a valid History object
*/
export function historyFromJSON(history: unknown): History {
function pascalCaseToConstantCase(s: string) {
return s.replace(/[^\b][A-Z]/g, (m) => `${m[0]}_${m[1]}`).toUpperCase();
}
function fixEnumValue<O extends Record<string, any>>(obj: O, attr: keyof O, prefix: string) {
return (
obj[attr] && {
[attr]: obj[attr].startsWith(prefix) ? obj[attr] : `${prefix}_${pascalCaseToConstantCase(obj[attr])}`,
}
);
}
// fromProto3JSON doesn't allow null values on 'bytes' fields. This turns out to be a problem for payloads.
// Recursively descend on objects and array, and fix in-place any payload that has a null data field
function fixPayloads<T>(e: T): T {
function isPayload(p: any): p is JSONPayload {
return p && typeof p === 'object' && 'metadata' in p && 'data' in p;
}
if (e && typeof e === 'object') {
if (isPayload(e)) {
if (e.data === null) {
const { data: _data, ...rest } = e;
return rest as T;
}
return e;
}
if (Array.isArray(e)) return e.map(fixPayloads) as T;
return Object.fromEntries(Object.entries(e as object).map(([k, v]) => [k, fixPayloads(v)])) as T;
}
return e;
}
function fixHistoryEvent(e: Record<string, any>) {
const type = Object.keys(e).find((k) => k.endsWith('EventAttributes'));
if (!type) {
throw new TypeError(`Missing attributes in history event: ${JSON.stringify(e)}`);
}
// Fix payloads with null data
e = fixPayloads(e);
return {
...e,
...fixEnumValue(e, 'eventType', 'EVENT_TYPE'),
[type]: {
...e[type],
...(e[type].taskQueue && {
taskQueue: { ...e[type].taskQueue, ...fixEnumValue(e[type].taskQueue, 'kind', 'TASK_QUEUE_KIND') },
}),
...fixEnumValue(e[type], 'parentClosePolicy', 'PARENT_CLOSE_POLICY'),
...fixEnumValue(e[type], 'workflowIdReusePolicy', 'WORKFLOW_ID_REUSE_POLICY'),
...fixEnumValue(e[type], 'initiator', 'CONTINUE_AS_NEW_INITIATOR'),
...fixEnumValue(e[type], 'retryState', 'RETRY_STATE'),
...(e[type].childWorkflowExecutionFailureInfo && {
childWorkflowExecutionFailureInfo: {
...e[type].childWorkflowExecutionFailureInfo,
...fixEnumValue(e[type].childWorkflowExecutionFailureInfo, 'retryState', 'RETRY_STATE'),
},
}),
},
};
}
function fixHistory(h: Record<string, any>) {
return {
events: h.events.map(fixHistoryEvent),
};
}
if (typeof history !== 'object' || history == null || !Array.isArray((history as any).events)) {
throw new TypeError('Invalid history, expected an object with an array of events');
}
const loaded = fromProto3JSON(historyType, fixHistory(history));
if (loaded === null) {
throw new TypeError('Invalid history');
}
return loaded as any;
}
/**
* Convert an History object, e.g. as returned by `WorkflowClient.list().withHistory()`, to a JSON
* string that adheres to the same norm as JSON history files produced by other Temporal tools.
*/
export function historyToJSON(history: History): string {
// toProto3JSON doesn't correctly handle some of our "bytes" fields, passing them untouched to the
// output, after which JSON.stringify() would convert them to an array of numbers. As a workaround,
// recursively walk the object and convert all Buffer instances to base64 strings. Note this only
// works on proto3-json-serializer v2.0.0. v2.0.2 throws an error before we even get the chance
// to fix the buffers. See https://github.com/googleapis/proto3-json-serializer-nodejs/issues/103.
function fixBuffers<T>(e: T): T {
if (e && typeof e === 'object') {
if (e instanceof Buffer) return e.toString('base64') as any;
if (Array.isArray(e)) return e.map(fixBuffers) as T;
return Object.fromEntries(Object.entries(e as object).map(([k, v]) => [k, fixBuffers(v)])) as T;
}
return e;
}
const protoJson = toProto3JSON(proto.temporal.api.history.v1.History.fromObject(history) as any);
return JSON.stringify(fixBuffers(protoJson), null, 2);
}
/**
* Convert from protobuf payload to JSON
*/
export function payloadToJSON(payload: Payload): JSONPayload {
return toProto3JSON(patched.temporal.api.common.v1.Payload.create(payload)) as any;
}
/**
* Convert from JSON to protobuf payload
*/
export function JSONToPayload(json: JSONPayload): Payload {
const loaded = fromProto3JSON(payloadType, json as any);
if (loaded === null) {
throw new TypeError('Invalid payload');
}
return loaded as any;
}