@hotmeshio/long-tail-dbos 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +221 -0
- package/README.md +226 -2
- package/build/conditional.d.ts +11 -0
- package/build/conditional.js +41 -0
- package/build/config.d.ts +6 -0
- package/build/config.js +29 -0
- package/build/constants.d.ts +33 -0
- package/build/constants.js +46 -0
- package/build/duration.d.ts +2 -0
- package/build/duration.js +20 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +15 -0
- package/build/lt-client.d.ts +38 -0
- package/build/lt-client.js +78 -0
- package/build/provider/index.d.ts +22 -0
- package/build/provider/index.js +96 -0
- package/build/provider/list.d.ts +20 -0
- package/build/provider/list.js +47 -0
- package/build/provider/status.d.ts +6 -0
- package/build/provider/status.js +38 -0
- package/build/provider/steps-map.d.ts +22 -0
- package/build/provider/steps-map.js +128 -0
- package/build/start.d.ts +10 -0
- package/build/start.js +72 -0
- package/build/steps.d.ts +7 -0
- package/build/steps.js +50 -0
- package/build/types.d.ts +39 -0
- package/build/types.js +2 -0
- package/package.json +65 -4
package/build/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WAIT_DEFAULTS = exports.STEP = exports.toMs = exports.LongTailRequestError = exports.configureLongTail = exports.waitSeconds = exports.conditional = void 0;
|
|
4
|
+
var conditional_1 = require("./conditional");
|
|
5
|
+
Object.defineProperty(exports, "conditional", { enumerable: true, get: function () { return conditional_1.conditional; } });
|
|
6
|
+
Object.defineProperty(exports, "waitSeconds", { enumerable: true, get: function () { return conditional_1.waitSeconds; } });
|
|
7
|
+
var config_1 = require("./config");
|
|
8
|
+
Object.defineProperty(exports, "configureLongTail", { enumerable: true, get: function () { return config_1.configureLongTail; } });
|
|
9
|
+
var lt_client_1 = require("./lt-client");
|
|
10
|
+
Object.defineProperty(exports, "LongTailRequestError", { enumerable: true, get: function () { return lt_client_1.LongTailRequestError; } });
|
|
11
|
+
var duration_1 = require("./duration");
|
|
12
|
+
Object.defineProperty(exports, "toMs", { enumerable: true, get: function () { return duration_1.toMs; } });
|
|
13
|
+
var constants_1 = require("./constants");
|
|
14
|
+
Object.defineProperty(exports, "STEP", { enumerable: true, get: function () { return constants_1.STEP; } });
|
|
15
|
+
Object.defineProperty(exports, "WAIT_DEFAULTS", { enumerable: true, get: function () { return constants_1.WAIT_DEFAULTS; } });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** A non-2xx response or transport failure from Long Tail. */
|
|
2
|
+
export declare class LongTailRequestError extends Error {
|
|
3
|
+
readonly status: number | undefined;
|
|
4
|
+
readonly body: unknown;
|
|
5
|
+
readonly context: {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
constructor(message: string, status: number | undefined, body: unknown, context: {
|
|
10
|
+
method: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}, options?: {
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export interface OpenWaitBody {
|
|
17
|
+
workflowId: string;
|
|
18
|
+
runScope: string;
|
|
19
|
+
workflowType?: string;
|
|
20
|
+
waitKey: string;
|
|
21
|
+
role: string;
|
|
22
|
+
type: string;
|
|
23
|
+
subtype?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
priority?: number;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
data?: Record<string, unknown>;
|
|
28
|
+
expiresAt?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface OpenWaitResponse {
|
|
31
|
+
escalationId: string;
|
|
32
|
+
status: string;
|
|
33
|
+
created: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** POST /api/escalations/waits: idempotent on (workflowId, runScope, waitKey). */
|
|
36
|
+
export declare function postOpenWait(body: OpenWaitBody): Promise<OpenWaitResponse>;
|
|
37
|
+
/** POST /api/escalations/:id/cancel. A row that is already closed is not an error. */
|
|
38
|
+
export declare function postCancel(escalationId: string): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LongTailRequestError = void 0;
|
|
4
|
+
exports.postOpenWait = postOpenWait;
|
|
5
|
+
exports.postCancel = postCancel;
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
/** A non-2xx response or transport failure from Long Tail. */
|
|
9
|
+
class LongTailRequestError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
body;
|
|
12
|
+
context;
|
|
13
|
+
constructor(message, status, body, context, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
this.context = context;
|
|
18
|
+
this.name = 'LongTailRequestError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.LongTailRequestError = LongTailRequestError;
|
|
22
|
+
/** POST /api/escalations/waits: idempotent on (workflowId, runScope, waitKey). */
|
|
23
|
+
function postOpenWait(body) {
|
|
24
|
+
return request('POST', constants_1.LT_PATHS.OPEN_WAIT, body);
|
|
25
|
+
}
|
|
26
|
+
/** POST /api/escalations/:id/cancel. A row that is already closed is not an error. */
|
|
27
|
+
async function postCancel(escalationId) {
|
|
28
|
+
try {
|
|
29
|
+
await request('POST', constants_1.LT_PATHS.CANCEL(escalationId), {});
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err instanceof LongTailRequestError && err.status === 409)
|
|
33
|
+
return;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function request(method, path, body) {
|
|
38
|
+
const { url, apiKey, requestTimeoutMs } = (0, config_1.getLongTailConnection)();
|
|
39
|
+
const target = `${url}${path}`;
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), requestTimeoutMs ?? constants_1.WAIT_DEFAULTS.REQUEST_TIMEOUT_MS);
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetch(target, {
|
|
45
|
+
method,
|
|
46
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
signal: controller.signal,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const reason = controller.signal.aborted ? 'timed out' : 'failed';
|
|
53
|
+
throw new LongTailRequestError(`Long Tail ${method} ${path} ${reason}`, undefined, undefined, { method, url: target }, { cause: err });
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
const parsed = await parseBody(response);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const detail = typeof parsed === 'object' && parsed && 'error' in parsed ? `: ${parsed.error}` : '';
|
|
61
|
+
throw new LongTailRequestError(`Long Tail ${method} ${path} returned ${response.status}${detail}`, response.status, parsed, { method, url: target });
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
async function parseBody(response) {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
if (!text)
|
|
68
|
+
return undefined;
|
|
69
|
+
if ((response.headers.get('content-type') ?? '').includes('application/json')) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(text);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DBOSClient } from '@dbos-inc/dbos-sdk';
|
|
2
|
+
import { type LTDurableProvider } from '@hotmeshio/long-tail';
|
|
3
|
+
import { type WorkflowCounter } from './list';
|
|
4
|
+
export interface DbosProviderConfig {
|
|
5
|
+
/** The DBOS system database the application uses. */
|
|
6
|
+
systemDatabaseUrl: string;
|
|
7
|
+
/** Postgres schema of the DBOS system tables, when not the default. */
|
|
8
|
+
systemDatabaseSchemaName?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The subset of DBOSClient the provider calls. */
|
|
11
|
+
export type DbosClientLike = Pick<DBOSClient, 'getWorkflow' | 'listWorkflows' | 'listWorkflowSteps' | 'send' | 'cancelWorkflow' | 'destroy'>;
|
|
12
|
+
export interface DbosProvider extends LTDurableProvider {
|
|
13
|
+
/** Release the client's and the counter's database pools. */
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** A Long Tail durable provider for workflows running on DBOS. */
|
|
17
|
+
export declare function dbosProvider(config: DbosProviderConfig): Promise<DbosProvider>;
|
|
18
|
+
/** Without a counter the provider does not list. */
|
|
19
|
+
export declare function createDbosProvider(client: DbosClientLike, countWorkflows?: WorkflowCounter): DbosProvider;
|
|
20
|
+
export { buildExecution, type DbosStep } from './steps-map';
|
|
21
|
+
export { toJobStatus, toJobState, isTerminal } from './status';
|
|
22
|
+
export { createWorkflowCounter, countWorkflowsSql, listFilter, LIST_STATE_STATUSES, type WorkflowCounter } from './list';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LIST_STATE_STATUSES = exports.listFilter = exports.countWorkflowsSql = exports.createWorkflowCounter = exports.isTerminal = exports.toJobState = exports.toJobStatus = exports.buildExecution = void 0;
|
|
4
|
+
exports.dbosProvider = dbosProvider;
|
|
5
|
+
exports.createDbosProvider = createDbosProvider;
|
|
6
|
+
const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
|
|
7
|
+
const pg_1 = require("pg");
|
|
8
|
+
const long_tail_1 = require("@hotmeshio/long-tail");
|
|
9
|
+
const constants_1 = require("../constants");
|
|
10
|
+
const list_1 = require("./list");
|
|
11
|
+
const status_1 = require("./status");
|
|
12
|
+
const steps_map_1 = require("./steps-map");
|
|
13
|
+
/** A Long Tail durable provider for workflows running on DBOS. */
|
|
14
|
+
async function dbosProvider(config) {
|
|
15
|
+
if (!config?.systemDatabaseUrl)
|
|
16
|
+
throw new Error('dbosProvider: systemDatabaseUrl is required');
|
|
17
|
+
const client = await dbos_sdk_1.DBOSClient.create({
|
|
18
|
+
systemDatabaseUrl: config.systemDatabaseUrl,
|
|
19
|
+
...(config.systemDatabaseSchemaName ? { systemDatabaseSchemaName: config.systemDatabaseSchemaName } : {}),
|
|
20
|
+
});
|
|
21
|
+
// DBOSClient lists but does not count; the executions list needs a total.
|
|
22
|
+
const pool = new pg_1.Pool({ connectionString: config.systemDatabaseUrl, max: constants_1.COUNT_POOL_SIZE });
|
|
23
|
+
const provider = createDbosProvider(client, (0, list_1.createWorkflowCounter)(pool, config.systemDatabaseSchemaName));
|
|
24
|
+
return {
|
|
25
|
+
...provider,
|
|
26
|
+
async close() {
|
|
27
|
+
await provider.close();
|
|
28
|
+
await pool.end();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Without a counter the provider does not list. */
|
|
33
|
+
function createDbosProvider(client, countWorkflows) {
|
|
34
|
+
return {
|
|
35
|
+
name: constants_1.PROVIDER_NAME,
|
|
36
|
+
capabilities: { native: false, execution: true, terminate: true, list: !!countWorkflows },
|
|
37
|
+
async deliver(ref, outcome) {
|
|
38
|
+
// A send to a finished workflow is stored and never read, so check first.
|
|
39
|
+
const status = await client.getWorkflow(ref.workflowId);
|
|
40
|
+
if (!status || (0, status_1.isTerminal)(status.status))
|
|
41
|
+
return long_tail_1.DELIVERY_RESULT.GONE;
|
|
42
|
+
try {
|
|
43
|
+
await client.send(ref.workflowId, outcome, ref.waitKey, outcome.escalationId);
|
|
44
|
+
return long_tail_1.DELIVERY_RESULT.DELIVERED;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof dbos_sdk_1.Error.DBOSNonExistentWorkflowError)
|
|
48
|
+
return long_tail_1.DELIVERY_RESULT.GONE;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async describe(workflowId) {
|
|
53
|
+
return (0, status_1.toJobStatus)(workflowId, await client.getWorkflow(workflowId));
|
|
54
|
+
},
|
|
55
|
+
async export(workflowId, options) {
|
|
56
|
+
const status = await client.getWorkflow(workflowId);
|
|
57
|
+
if (!status)
|
|
58
|
+
throw new Error(`DBOS workflow ${workflowId} not found`);
|
|
59
|
+
return (0, steps_map_1.buildExecution)(status, await allSteps(client, workflowId), options);
|
|
60
|
+
},
|
|
61
|
+
async terminate(workflowId) {
|
|
62
|
+
await client.cancelWorkflow(workflowId);
|
|
63
|
+
},
|
|
64
|
+
...(countWorkflows ? {
|
|
65
|
+
async list(query) {
|
|
66
|
+
const filter = (0, list_1.listFilter)(query);
|
|
67
|
+
const [rows, total] = await Promise.all([
|
|
68
|
+
client.listWorkflows({ ...filter, limit: query.limit, offset: query.offset, sortDesc: true, loadInput: false, loadOutput: false }),
|
|
69
|
+
countWorkflows(query),
|
|
70
|
+
]);
|
|
71
|
+
return { workflows: rows.map((row) => (0, status_1.toJobStatus)(row.workflowID, row)), total };
|
|
72
|
+
},
|
|
73
|
+
} : {}),
|
|
74
|
+
close: () => client.destroy(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function allSteps(client, workflowId) {
|
|
78
|
+
const steps = [];
|
|
79
|
+
for (let offset = 0;; offset += constants_1.STEP_PAGE_SIZE) {
|
|
80
|
+
const page = (await client.listWorkflowSteps(workflowId, { limit: constants_1.STEP_PAGE_SIZE, offset })) ?? [];
|
|
81
|
+
steps.push(...page);
|
|
82
|
+
if (page.length < constants_1.STEP_PAGE_SIZE)
|
|
83
|
+
return steps;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var steps_map_2 = require("./steps-map");
|
|
87
|
+
Object.defineProperty(exports, "buildExecution", { enumerable: true, get: function () { return steps_map_2.buildExecution; } });
|
|
88
|
+
var status_2 = require("./status");
|
|
89
|
+
Object.defineProperty(exports, "toJobStatus", { enumerable: true, get: function () { return status_2.toJobStatus; } });
|
|
90
|
+
Object.defineProperty(exports, "toJobState", { enumerable: true, get: function () { return status_2.toJobState; } });
|
|
91
|
+
Object.defineProperty(exports, "isTerminal", { enumerable: true, get: function () { return status_2.isTerminal; } });
|
|
92
|
+
var list_2 = require("./list");
|
|
93
|
+
Object.defineProperty(exports, "createWorkflowCounter", { enumerable: true, get: function () { return list_2.createWorkflowCounter; } });
|
|
94
|
+
Object.defineProperty(exports, "countWorkflowsSql", { enumerable: true, get: function () { return list_2.countWorkflowsSql; } });
|
|
95
|
+
Object.defineProperty(exports, "listFilter", { enumerable: true, get: function () { return list_2.listFilter; } });
|
|
96
|
+
Object.defineProperty(exports, "LIST_STATE_STATUSES", { enumerable: true, get: function () { return list_2.LIST_STATE_STATUSES; } });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type GetWorkflowsInput } from '@dbos-inc/dbos-sdk';
|
|
2
|
+
import { type LTWorkflowListQuery, type LTWorkflowListState } from '@hotmeshio/long-tail';
|
|
3
|
+
/** DBOS statuses behind each executions-list state. */
|
|
4
|
+
export declare const LIST_STATE_STATUSES: Record<LTWorkflowListState, string[]>;
|
|
5
|
+
/** The listWorkflows filter for a query, without paging. */
|
|
6
|
+
export declare function listFilter(query: LTWorkflowListQuery): GetWorkflowsInput;
|
|
7
|
+
/**
|
|
8
|
+
* Counts the workflows a query matches, with the conditions DBOS's
|
|
9
|
+
* listWorkflows applies: name, status and an id prefix matched with LIKE.
|
|
10
|
+
*/
|
|
11
|
+
export declare function countWorkflowsSql(schema?: string): string;
|
|
12
|
+
export type WorkflowCounter = (query: LTWorkflowListQuery) => Promise<number>;
|
|
13
|
+
export interface SqlQuerier {
|
|
14
|
+
query(text: string, values: unknown[]): Promise<{
|
|
15
|
+
rows: Array<{
|
|
16
|
+
total: number;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createWorkflowCounter(db: SqlQuerier, schema?: string): WorkflowCounter;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LIST_STATE_STATUSES = void 0;
|
|
4
|
+
exports.listFilter = listFilter;
|
|
5
|
+
exports.countWorkflowsSql = countWorkflowsSql;
|
|
6
|
+
exports.createWorkflowCounter = createWorkflowCounter;
|
|
7
|
+
const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
|
|
8
|
+
const long_tail_1 = require("@hotmeshio/long-tail");
|
|
9
|
+
/** DBOS statuses behind each executions-list state. */
|
|
10
|
+
exports.LIST_STATE_STATUSES = {
|
|
11
|
+
[long_tail_1.WORKFLOW_LIST_STATE.RUNNING]: [dbos_sdk_1.StatusString.PENDING, dbos_sdk_1.StatusString.ENQUEUED, 'DELAYED', 'SUSPENDED'],
|
|
12
|
+
[long_tail_1.WORKFLOW_LIST_STATE.COMPLETED]: [dbos_sdk_1.StatusString.SUCCESS],
|
|
13
|
+
[long_tail_1.WORKFLOW_LIST_STATE.FAILED]: [dbos_sdk_1.StatusString.ERROR, dbos_sdk_1.StatusString.CANCELLED, dbos_sdk_1.StatusString.MAX_RECOVERY_ATTEMPTS_EXCEEDED],
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_SCHEMA = 'dbos';
|
|
16
|
+
const SCHEMA_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
17
|
+
/** The listWorkflows filter for a query, without paging. */
|
|
18
|
+
function listFilter(query) {
|
|
19
|
+
return {
|
|
20
|
+
...(query.workflowType ? { workflowName: query.workflowType } : {}),
|
|
21
|
+
...(query.state ? { status: exports.LIST_STATE_STATUSES[query.state] } : {}),
|
|
22
|
+
...(query.idPrefix ? { workflow_id_prefix: query.idPrefix } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Counts the workflows a query matches, with the conditions DBOS's
|
|
27
|
+
* listWorkflows applies: name, status and an id prefix matched with LIKE.
|
|
28
|
+
*/
|
|
29
|
+
function countWorkflowsSql(schema = DEFAULT_SCHEMA) {
|
|
30
|
+
if (!SCHEMA_NAME.test(schema))
|
|
31
|
+
throw new Error(`Invalid DBOS system schema name: ${schema}`);
|
|
32
|
+
return `SELECT count(*)::int AS total FROM "${schema}".workflow_status
|
|
33
|
+
WHERE ($1::text IS NULL OR name = $1::text)
|
|
34
|
+
AND ($2::text[] IS NULL OR status = ANY($2::text[]))
|
|
35
|
+
AND ($3::text IS NULL OR workflow_uuid LIKE $3::text || '%')`;
|
|
36
|
+
}
|
|
37
|
+
function createWorkflowCounter(db, schema) {
|
|
38
|
+
const sql = countWorkflowsSql(schema);
|
|
39
|
+
return async (query) => {
|
|
40
|
+
const { rows } = await db.query(sql, [
|
|
41
|
+
query.workflowType ?? null,
|
|
42
|
+
query.state ? exports.LIST_STATE_STATUSES[query.state] : null,
|
|
43
|
+
query.idPrefix ?? null,
|
|
44
|
+
]);
|
|
45
|
+
return rows[0]?.total ?? 0;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type WorkflowStatus } from '@dbos-inc/dbos-sdk';
|
|
2
|
+
import { type LTJobState, type LTJobStatus } from '@hotmeshio/long-tail';
|
|
3
|
+
export declare function toJobState(status: string | undefined): LTJobState;
|
|
4
|
+
/** True once the workflow can no longer receive a message. */
|
|
5
|
+
export declare function isTerminal(status: string): boolean;
|
|
6
|
+
export declare function toJobStatus(workflowId: string, status: WorkflowStatus | undefined | null): LTJobStatus;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toJobState = toJobState;
|
|
4
|
+
exports.isTerminal = isTerminal;
|
|
5
|
+
exports.toJobStatus = toJobStatus;
|
|
6
|
+
const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
|
|
7
|
+
const long_tail_1 = require("@hotmeshio/long-tail");
|
|
8
|
+
const RUNNING = [dbos_sdk_1.StatusString.PENDING, dbos_sdk_1.StatusString.ENQUEUED, 'DELAYED', 'SUSPENDED'];
|
|
9
|
+
const FAILED = [dbos_sdk_1.StatusString.ERROR, dbos_sdk_1.StatusString.MAX_RECOVERY_ATTEMPTS_EXCEEDED];
|
|
10
|
+
function toJobState(status) {
|
|
11
|
+
if (!status)
|
|
12
|
+
return long_tail_1.JOB_STATE.UNKNOWN;
|
|
13
|
+
if (RUNNING.includes(status))
|
|
14
|
+
return long_tail_1.JOB_STATE.RUNNING;
|
|
15
|
+
if (status === dbos_sdk_1.StatusString.SUCCESS)
|
|
16
|
+
return long_tail_1.JOB_STATE.COMPLETED;
|
|
17
|
+
if (status === dbos_sdk_1.StatusString.CANCELLED)
|
|
18
|
+
return long_tail_1.JOB_STATE.CANCELLED;
|
|
19
|
+
if (FAILED.includes(status))
|
|
20
|
+
return long_tail_1.JOB_STATE.FAILED;
|
|
21
|
+
return long_tail_1.JOB_STATE.UNKNOWN;
|
|
22
|
+
}
|
|
23
|
+
/** True once the workflow can no longer receive a message. */
|
|
24
|
+
function isTerminal(status) {
|
|
25
|
+
return !RUNNING.includes(status);
|
|
26
|
+
}
|
|
27
|
+
function toJobStatus(workflowId, status) {
|
|
28
|
+
if (!status)
|
|
29
|
+
return { workflowId, state: long_tail_1.JOB_STATE.UNKNOWN, providerState: 'NOT_FOUND' };
|
|
30
|
+
return {
|
|
31
|
+
workflowId,
|
|
32
|
+
workflowType: status.workflowName,
|
|
33
|
+
state: toJobState(status.status),
|
|
34
|
+
providerState: status.status,
|
|
35
|
+
startedAt: new Date(status.createdAt).toISOString(),
|
|
36
|
+
...(status.completedAt ? { closedAt: new Date(status.completedAt).toISOString() } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkflowStatus } from '@dbos-inc/dbos-sdk';
|
|
2
|
+
import { type LTExecutionExportOptions, type LTWorkflowExecution } from '@hotmeshio/long-tail';
|
|
3
|
+
/** The fields of a DBOS StepInfo the timeline reads. */
|
|
4
|
+
export interface DbosStep {
|
|
5
|
+
functionID: number;
|
|
6
|
+
name: string;
|
|
7
|
+
output: unknown;
|
|
8
|
+
error: unknown;
|
|
9
|
+
childWorkflowID: string | null;
|
|
10
|
+
startedAtEpochMs?: number;
|
|
11
|
+
completedAtEpochMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* DBOS status + steps as the Long Tail execution timeline.
|
|
15
|
+
*
|
|
16
|
+
* - User steps become activity scheduled/completed (or failed) pairs.
|
|
17
|
+
* - `lt.open` starts a signal wait; the next `DBOS.recv` ends it.
|
|
18
|
+
* - A timed recv writes its deadline as `DBOS.sleep` at the next function id; it is folded in.
|
|
19
|
+
* - A child start and its `DBOS.getResult` pair on the child id.
|
|
20
|
+
* - Steps record outputs only; DBOS does not store step inputs.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildExecution(status: WorkflowStatus, steps: DbosStep[], options?: LTExecutionExportOptions): LTWorkflowExecution;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildExecution = buildExecution;
|
|
4
|
+
const long_tail_1 = require("@hotmeshio/long-tail");
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
const status_1 = require("./status");
|
|
7
|
+
/**
|
|
8
|
+
* DBOS status + steps as the Long Tail execution timeline.
|
|
9
|
+
*
|
|
10
|
+
* - User steps become activity scheduled/completed (or failed) pairs.
|
|
11
|
+
* - `lt.open` starts a signal wait; the next `DBOS.recv` ends it.
|
|
12
|
+
* - A timed recv writes its deadline as `DBOS.sleep` at the next function id; it is folded in.
|
|
13
|
+
* - A child start and its `DBOS.getResult` pair on the child id.
|
|
14
|
+
* - Steps record outputs only; DBOS does not store step inputs.
|
|
15
|
+
*/
|
|
16
|
+
function buildExecution(status, steps, options = {}) {
|
|
17
|
+
const events = [];
|
|
18
|
+
const byId = new Map(steps.map((s) => [s.functionID, s]));
|
|
19
|
+
const push = (event_type, category, at, attrs, opts = {}) => {
|
|
20
|
+
if (opts.system && options.excludeSystem)
|
|
21
|
+
return;
|
|
22
|
+
events.push({
|
|
23
|
+
event_id: events.length + 1,
|
|
24
|
+
event_type,
|
|
25
|
+
category,
|
|
26
|
+
event_time: iso(at ?? status.createdAt),
|
|
27
|
+
duration_ms: opts.duration ?? null,
|
|
28
|
+
is_system: Boolean(opts.system),
|
|
29
|
+
attributes: { kind: event_type, ...attrs },
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const result = (value) => (options.omitResults ? undefined : value);
|
|
33
|
+
push('workflow_execution_started', 'workflow', status.createdAt, { input: result(status.input) });
|
|
34
|
+
let openWaitKey = null;
|
|
35
|
+
for (const step of steps) {
|
|
36
|
+
const start = step.startedAtEpochMs;
|
|
37
|
+
const end = step.completedAtEpochMs;
|
|
38
|
+
if (step.name === constants_1.STEP.OPEN) {
|
|
39
|
+
const waitKey = step.output?.waitKey ?? null;
|
|
40
|
+
pushActivity(push, step, true, result);
|
|
41
|
+
if (waitKey) {
|
|
42
|
+
openWaitKey = waitKey;
|
|
43
|
+
push('signal_wait_started', 'signal', end ?? start, { signal_name: waitKey, timeline_key: `w:${waitKey}` });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (step.name === constants_1.DBOS_OP.RECV) {
|
|
47
|
+
const key = openWaitKey ?? `recv:${step.functionID}`;
|
|
48
|
+
push('workflow_execution_signaled', 'signal', end, { signal_name: key, timeline_key: `w:${key}`, input: result(step.output) });
|
|
49
|
+
openWaitKey = null;
|
|
50
|
+
}
|
|
51
|
+
else if (step.name === constants_1.DBOS_OP.SLEEP) {
|
|
52
|
+
if (isRecvDeadline(step, byId))
|
|
53
|
+
continue;
|
|
54
|
+
const duration = start !== undefined && end !== undefined ? end - start : null;
|
|
55
|
+
push('timer_started', 'timer', start, { timeline_key: `t:${step.functionID}` }, { duration });
|
|
56
|
+
}
|
|
57
|
+
else if (step.childWorkflowID && step.name !== constants_1.DBOS_OP.GET_RESULT) {
|
|
58
|
+
push('child_workflow_execution_started', 'child_workflow', start, { child_workflow_id: step.childWorkflowID, timeline_key: `c:${step.childWorkflowID}` });
|
|
59
|
+
}
|
|
60
|
+
else if (step.childWorkflowID && step.name === constants_1.DBOS_OP.GET_RESULT) {
|
|
61
|
+
const type = step.error ? 'child_workflow_execution_failed' : 'child_workflow_execution_completed';
|
|
62
|
+
push(type, 'child_workflow', end, { child_workflow_id: step.childWorkflowID, timeline_key: `c:${step.childWorkflowID}`, result: result(step.output), ...(step.error ? { failure: errorText(step.error) } : {}) });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
pushActivity(push, step, constants_1.DBOS_SYSTEM_STEPS.includes(step.name) || step.name.startsWith('DBOS.'), result);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const state = (0, status_1.toJobState)(status.status);
|
|
69
|
+
const closed = state !== 'running' && state !== 'unknown';
|
|
70
|
+
if (closed) {
|
|
71
|
+
const failed = state !== 'completed';
|
|
72
|
+
push(failed ? 'workflow_execution_failed' : 'workflow_execution_completed', 'workflow', status.completedAt ?? status.updatedAt, failed
|
|
73
|
+
? { failure: errorText(status.error) }
|
|
74
|
+
: { result: result(status.output) });
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
workflow_id: status.workflowID,
|
|
78
|
+
workflow_type: status.workflowName,
|
|
79
|
+
workflow_name: status.workflowName,
|
|
80
|
+
task_queue: status.queueName ?? '',
|
|
81
|
+
status: !closed ? 'running' : state === 'completed' ? 'completed' : 'failed',
|
|
82
|
+
provider_status: status.status,
|
|
83
|
+
start_time: iso(status.createdAt),
|
|
84
|
+
close_time: closed && status.completedAt ? iso(status.completedAt) : null,
|
|
85
|
+
duration_ms: closed && status.completedAt ? status.completedAt - status.createdAt : null,
|
|
86
|
+
...(status.parentWorkflowID ? { parent_workflow_id: status.parentWorkflowID } : {}),
|
|
87
|
+
...(closed && state === 'completed' ? { result: result(status.output) } : {}),
|
|
88
|
+
events,
|
|
89
|
+
summary: (0, long_tail_1.summarizeExecution)(events),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function pushActivity(push, step, system, result) {
|
|
93
|
+
const key = `s:${step.functionID}`;
|
|
94
|
+
push('activity_task_scheduled', 'activity', step.startedAtEpochMs, { activity_type: step.name, timeline_key: key }, { system });
|
|
95
|
+
if (step.completedAtEpochMs === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const duration = step.startedAtEpochMs !== undefined ? step.completedAtEpochMs - step.startedAtEpochMs : null;
|
|
98
|
+
if (step.error) {
|
|
99
|
+
push('activity_task_failed', 'activity', step.completedAtEpochMs, { activity_type: step.name, timeline_key: key, failure: errorText(step.error) }, { system, duration });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
push('activity_task_completed', 'activity', step.completedAtEpochMs, { activity_type: step.name, timeline_key: key, result: result(step.output) }, { system, duration });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A timed recv writes its deadline as DBOS.sleep at the next function id. While
|
|
107
|
+
* the recv waits its own row is absent; the deadline row records no elapsed time
|
|
108
|
+
* and a wake time after it completed.
|
|
109
|
+
*/
|
|
110
|
+
function isRecvDeadline(step, byId) {
|
|
111
|
+
const before = byId.get(step.functionID - 1);
|
|
112
|
+
if (before)
|
|
113
|
+
return before.name === constants_1.DBOS_OP.RECV;
|
|
114
|
+
const wake = Number(step.output);
|
|
115
|
+
return step.completedAtEpochMs !== undefined && Number.isFinite(wake) && wake > step.completedAtEpochMs;
|
|
116
|
+
}
|
|
117
|
+
function errorText(error) {
|
|
118
|
+
if (error === undefined || error === null)
|
|
119
|
+
return undefined;
|
|
120
|
+
if (error instanceof Error)
|
|
121
|
+
return error.message;
|
|
122
|
+
if (typeof error === 'object' && 'message' in error)
|
|
123
|
+
return String(error.message);
|
|
124
|
+
return typeof error === 'string' ? error : JSON.stringify(error);
|
|
125
|
+
}
|
|
126
|
+
function iso(ms) {
|
|
127
|
+
return new Date(ms ?? Date.now()).toISOString();
|
|
128
|
+
}
|
package/build/start.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long Tail with DBOS as the durable engine, configured from the environment:
|
|
3
|
+
* DBOS_SYSTEM_DATABASE_URL (defaults to DATABASE_URL, one database), DBOS_SYSTEM_DATABASE_SCHEMA, DBOS_CONSOLE_URL,
|
|
4
|
+
* DATABASE_URL or POSTGRES_*, PORT, JWT_SECRET, LT_ADMIN_USER / LT_ADMIN_PASSWORD / LT_ADMIN_EMAIL, LT_ROLES.
|
|
5
|
+
* LT_START_CONFIG optionally names a JSON file with further start() settings.
|
|
6
|
+
*/
|
|
7
|
+
export declare function startLongTailDbos(env?: NodeJS.ProcessEnv): Promise<{
|
|
8
|
+
instance: import("@hotmeshio/long-tail").LTInstance;
|
|
9
|
+
shutdown(): Promise<void>;
|
|
10
|
+
}>;
|
package/build/start.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startLongTailDbos = startLongTailDbos;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const long_tail_1 = require("@hotmeshio/long-tail");
|
|
6
|
+
const provider_1 = require("./provider");
|
|
7
|
+
const DEFAULT_ADMIN = 'superadmin';
|
|
8
|
+
const ENV_OWNED_KEYS = ['database', 'server', 'durable', 'workers', 'examples'];
|
|
9
|
+
/**
|
|
10
|
+
* Long Tail with DBOS as the durable engine, configured from the environment:
|
|
11
|
+
* DBOS_SYSTEM_DATABASE_URL (defaults to DATABASE_URL, one database), DBOS_SYSTEM_DATABASE_SCHEMA, DBOS_CONSOLE_URL,
|
|
12
|
+
* DATABASE_URL or POSTGRES_*, PORT, JWT_SECRET, LT_ADMIN_USER / LT_ADMIN_PASSWORD / LT_ADMIN_EMAIL, LT_ROLES.
|
|
13
|
+
* LT_START_CONFIG optionally names a JSON file with further start() settings.
|
|
14
|
+
*/
|
|
15
|
+
async function startLongTailDbos(env = process.env) {
|
|
16
|
+
// 1. DBOS provider
|
|
17
|
+
const provider = await (0, provider_1.dbosProvider)({
|
|
18
|
+
systemDatabaseUrl: systemDatabaseUrl(env),
|
|
19
|
+
systemDatabaseSchemaName: env.DBOS_SYSTEM_DATABASE_SCHEMA || undefined,
|
|
20
|
+
});
|
|
21
|
+
// 2. Long Tail
|
|
22
|
+
const instance = await (0, long_tail_1.start)({
|
|
23
|
+
...fileConfig(env.LT_START_CONFIG),
|
|
24
|
+
database: env.DATABASE_URL ? { connectionString: env.DATABASE_URL } : {
|
|
25
|
+
host: env.POSTGRES_HOST, port: env.POSTGRES_PORT ? Number(env.POSTGRES_PORT) : undefined,
|
|
26
|
+
user: env.POSTGRES_USER, password: env.POSTGRES_PASSWORD, database: env.POSTGRES_DB,
|
|
27
|
+
},
|
|
28
|
+
server: { port: env.PORT ? Number(env.PORT) : undefined },
|
|
29
|
+
...(env.JWT_SECRET ? { auth: { secret: env.JWT_SECRET } } : {}),
|
|
30
|
+
...(env.LT_ADMIN_PASSWORD ? { seed: { admin: {
|
|
31
|
+
externalId: env.LT_ADMIN_USER ?? DEFAULT_ADMIN, email: env.LT_ADMIN_EMAIL, password: env.LT_ADMIN_PASSWORD,
|
|
32
|
+
} } } : {}),
|
|
33
|
+
...(env.LT_ROLES ? { roles: env.LT_ROLES.split(',').map((role) => ({ role: role.trim() })).filter((r) => r.role) } : {}),
|
|
34
|
+
durable: { provider, consoleUrl: env.DBOS_CONSOLE_URL || undefined },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
instance,
|
|
38
|
+
async shutdown() {
|
|
39
|
+
await instance.shutdown();
|
|
40
|
+
await provider.close();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function systemDatabaseUrl(env) {
|
|
45
|
+
const value = env.DBOS_SYSTEM_DATABASE_URL || env.DATABASE_URL;
|
|
46
|
+
if (!value)
|
|
47
|
+
throw new Error('DBOS_SYSTEM_DATABASE_URL or DATABASE_URL is required');
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function fileConfig(path) {
|
|
51
|
+
if (!path)
|
|
52
|
+
return {};
|
|
53
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
54
|
+
for (const key of ENV_OWNED_KEYS) {
|
|
55
|
+
if (key in parsed)
|
|
56
|
+
throw new Error(`LT_START_CONFIG may not set "${key}"; it is set by environment or not allowed with DBOS`);
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
if (require.main === module) {
|
|
61
|
+
startLongTailDbos().then((running) => {
|
|
62
|
+
const stop = async () => {
|
|
63
|
+
await running.shutdown();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
};
|
|
66
|
+
process.on('SIGTERM', stop);
|
|
67
|
+
process.on('SIGINT', stop);
|
|
68
|
+
}).catch((err) => {
|
|
69
|
+
console.error(err);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
}
|
package/build/steps.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LTConditionalConfig, OpenedWait } from './types';
|
|
2
|
+
/** Open the escalation this workflow waits on. Runs as a checkpointed step. */
|
|
3
|
+
export declare function openWait(waitKey: string, config: LTConditionalConfig): Promise<OpenedWait>;
|
|
4
|
+
/** Cancel the escalation when the recv deadline passes first. */
|
|
5
|
+
export declare function closeWait(escalationId: string): Promise<void>;
|
|
6
|
+
export declare const ltOpen: (this: unknown, waitKey: string, config: LTConditionalConfig) => Promise<OpenedWait>;
|
|
7
|
+
export declare const ltClose: (this: unknown, escalationId: string) => Promise<void>;
|