@hatchet-dev/typescript-sdk 1.34.0-alpha.0 → 1.34.0-alpha.2
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/clients/admin/rpc.d.ts +1 -1
- package/clients/admin/rpc.js +3 -3
- package/clients/admin/trigger-request.d.ts +18 -0
- package/clients/admin/trigger-request.js +66 -0
- package/clients/event/event-client.d.ts +4 -17
- package/clients/event/event-client.js +5 -43
- package/clients/event/rpc.d.ts +22 -3
- package/clients/event/rpc.js +66 -3
- package/clients/hatchet-client/hatchet-logger.js +2 -1
- package/clients/transport/fetch-transport.d.ts +73 -0
- package/clients/transport/fetch-transport.js +152 -0
- package/clients/transport/grpc-target.js +48 -3
- package/clients/transport/index.d.ts +2 -1
- package/clients/transport/index.js +6 -1
- package/clients/transport/node-transport.js +1 -2
- package/clients/transport/transport.d.ts +6 -0
- package/clients/transport/transport.js +7 -0
- package/clients/transport/ts-proto-client.js +16 -2
- package/core/client.d.ts +102 -0
- package/core/client.js +250 -0
- package/core/features/events.d.ts +25 -0
- package/core/features/events.js +57 -0
- package/core/features/logs.d.ts +26 -0
- package/core/features/logs.js +51 -0
- package/core/features/rate-limits.d.ts +19 -0
- package/core/features/rate-limits.js +30 -0
- package/core/features/runs.d.ts +33 -0
- package/core/features/runs.js +84 -0
- package/core/features/streams.d.ts +11 -0
- package/core/features/streams.js +23 -0
- package/core/features/workflows.d.ts +22 -0
- package/core/features/workflows.js +46 -0
- package/core/index.d.ts +42 -4
- package/core/index.js +79 -5
- package/core/logger.d.ts +19 -0
- package/core/logger.js +45 -0
- package/core/run-detail.d.ts +14 -0
- package/core/run-detail.js +79 -0
- package/core/run-ref.d.ts +63 -0
- package/core/run-ref.js +217 -0
- package/core/types.d.ts +182 -0
- package/core/types.js +2 -0
- package/dist/check-edge-entry.mjs +72 -14
- package/dist/check-edge-entry.test.ts +57 -0
- package/package.json +5 -2
- package/scripts/check-edge-entry.mjs +72 -14
- package/scripts/check-edge-entry.test.ts +57 -0
- package/util/batch.js +2 -1
- package/util/config-loader/token.d.ts +5 -0
- package/util/config-loader/token.js +8 -0
- package/util/errors/bulk-trigger-partial-error.d.ts +17 -0
- package/util/errors/bulk-trigger-partial-error.js +55 -0
- package/v1/client/admin.d.ts +4 -54
- package/v1/client/admin.js +9 -72
- package/v1/client/client.d.ts +6 -0
- package/v1/client/client.js +7 -0
- package/v1/client/features/runs.d.ts +9 -27
- package/v1/client/features/runs.js +10 -55
- package/version.d.ts +1 -1
- package/version.js +1 -1
package/core/run-ref.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.WorkflowRunRef = exports.MAX_POLL_INTERVAL_MS = exports.INITIAL_POLL_INTERVAL_MS = void 0;
|
|
16
|
+
const hatchet_error_1 = __importDefault(require("../util/errors/hatchet-error"));
|
|
17
|
+
const abort_error_1 = require("../util/abort-error");
|
|
18
|
+
const data_contracts_1 = require("../clients/rest/generated/data-contracts");
|
|
19
|
+
const run_detail_1 = require("./run-detail");
|
|
20
|
+
/** The first wait between two `GetRunDetails` polls. */
|
|
21
|
+
exports.INITIAL_POLL_INTERVAL_MS = 250;
|
|
22
|
+
/** The longest wait between two polls; the interval doubles until it gets here. */
|
|
23
|
+
exports.MAX_POLL_INTERVAL_MS = 5000;
|
|
24
|
+
/**
|
|
25
|
+
* The signal one poll is issued with: it fires when the caller's signal fires or when the
|
|
26
|
+
* deadline arrives, whichever is first. `AbortSignal.any` and `AbortSignal.timeout` are
|
|
27
|
+
* missing on some runtimes the core entry targets, so a plain controller, timer and
|
|
28
|
+
* forwarded abort are used everywhere; the timer is also one a test's fake clock controls.
|
|
29
|
+
*/
|
|
30
|
+
function pollSignal(signal, remainingMs) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
let timedOut = false;
|
|
33
|
+
const forwardAbort = () => controller.abort(signal === null || signal === void 0 ? void 0 : signal.reason);
|
|
34
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', forwardAbort, { once: true });
|
|
35
|
+
const timer = remainingMs === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: setTimeout(() => {
|
|
38
|
+
timedOut = true;
|
|
39
|
+
controller.abort();
|
|
40
|
+
}, remainingMs);
|
|
41
|
+
return {
|
|
42
|
+
signal: controller.signal,
|
|
43
|
+
timedOut: () => timedOut,
|
|
44
|
+
dispose: () => {
|
|
45
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', forwardAbort);
|
|
46
|
+
if (timer !== undefined)
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function wait(ms, signal) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
54
|
+
resolve();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
59
|
+
resolve();
|
|
60
|
+
}, ms);
|
|
61
|
+
const onAbort = () => {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
resolve();
|
|
64
|
+
};
|
|
65
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A reference to a triggered run. `result()` polls `GetRunDetails` until the run is done,
|
|
70
|
+
* which is the one way to wait for a run that works on every platform the core client runs
|
|
71
|
+
* on: no stream is held open, so a Worker request or a browser tab can wait on it.
|
|
72
|
+
*/
|
|
73
|
+
class WorkflowRunRef {
|
|
74
|
+
constructor(workflowRunId, runs, parentWorkflowRunId, standaloneTaskName, defaultSignal) {
|
|
75
|
+
this.runs = runs;
|
|
76
|
+
this.workflowRunId = workflowRunId;
|
|
77
|
+
this.parentWorkflowRunId = parentWorkflowRunId;
|
|
78
|
+
this._standaloneTaskName = standaloneTaskName;
|
|
79
|
+
this.defaultSignal = defaultSignal;
|
|
80
|
+
}
|
|
81
|
+
get runId() {
|
|
82
|
+
return this.getWorkflowRunId();
|
|
83
|
+
}
|
|
84
|
+
getWorkflowRunId() {
|
|
85
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
return this.workflowRunId;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** The run's result; the same as `result()` with no options. */
|
|
90
|
+
get output() {
|
|
91
|
+
return this.result();
|
|
92
|
+
}
|
|
93
|
+
/** The run's current state. */
|
|
94
|
+
details() {
|
|
95
|
+
return this.runs.getDetails(this.workflowRunId);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Waits for the run to finish and returns its output: the standalone task's output for a
|
|
99
|
+
* task, or the outputs keyed by task name for a workflow.
|
|
100
|
+
*
|
|
101
|
+
* The wait polls with backoff, starting at 250 ms and capped at 5 s with jitter. It is
|
|
102
|
+
* unbounded unless `timeoutMs` is set (rejects with a `HatchetError`) or `signal` fires
|
|
103
|
+
* (rejects with an `AbortError`). Both bound the poll in flight as well as the waits
|
|
104
|
+
* between polls, so a stalled `GetRunDetails` cannot hold the result past the deadline.
|
|
105
|
+
*
|
|
106
|
+
* A failed or cancelled run rejects the way the Node client's `result()` does: with the
|
|
107
|
+
* array of the tasks' error messages when any task reported one. When no task reported an
|
|
108
|
+
* error the run resolves with the tasks' outputs whatever its status, as it does on the
|
|
109
|
+
* Node client, since that is the shape an explicit cancellation leaves. A run with no
|
|
110
|
+
* tasks at all rejects with an `Error` naming the run's status.
|
|
111
|
+
*/
|
|
112
|
+
result() {
|
|
113
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
114
|
+
var _a;
|
|
115
|
+
const signal = (_a = options.signal) !== null && _a !== void 0 ? _a : this.defaultSignal;
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const deadline = options.timeoutMs !== undefined ? startedAt + options.timeoutMs : undefined;
|
|
118
|
+
let interval = exports.INITIAL_POLL_INTERVAL_MS;
|
|
119
|
+
const abortError = () => (0, abort_error_1.createAbortError)(`waiting for run ${this.workflowRunId} was aborted`);
|
|
120
|
+
const timeoutError = () => new hatchet_error_1.default(`timed out after ${options.timeoutMs} ms waiting for run ${this.workflowRunId}`);
|
|
121
|
+
for (;;) {
|
|
122
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
123
|
+
throw abortError();
|
|
124
|
+
}
|
|
125
|
+
const remaining = deadline !== undefined ? deadline - Date.now() : undefined;
|
|
126
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
127
|
+
throw timeoutError();
|
|
128
|
+
}
|
|
129
|
+
// Whatever the transport rejected with, an abort by the caller's signal surfaces as the
|
|
130
|
+
// AbortError the check above throws and one by the deadline as the same HatchetError;
|
|
131
|
+
// the deadline also goes to the call so the transport applies a Connect timeout.
|
|
132
|
+
const poll = pollSignal(signal, remaining);
|
|
133
|
+
let detail;
|
|
134
|
+
try {
|
|
135
|
+
detail = yield this.runs.getDetails(this.workflowRunId, { signal: poll.signal, deadline });
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
139
|
+
throw abortError();
|
|
140
|
+
}
|
|
141
|
+
if (poll.timedOut() || (deadline !== undefined && Date.now() >= deadline)) {
|
|
142
|
+
throw timeoutError();
|
|
143
|
+
}
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
poll.dispose();
|
|
148
|
+
}
|
|
149
|
+
if (detail.done) {
|
|
150
|
+
return this.resolveResult(detail);
|
|
151
|
+
}
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (deadline !== undefined && now >= deadline) {
|
|
154
|
+
throw timeoutError();
|
|
155
|
+
}
|
|
156
|
+
// Jitter of up to 20% keeps many waiters from polling in lockstep; the cap holds for
|
|
157
|
+
// the jittered wait too.
|
|
158
|
+
let delay = Math.min(Math.round(interval * (1 + Math.random() * 0.2)), exports.MAX_POLL_INTERVAL_MS);
|
|
159
|
+
if (deadline !== undefined) {
|
|
160
|
+
delay = Math.min(delay, Math.max(0, deadline - now));
|
|
161
|
+
}
|
|
162
|
+
yield wait(delay, signal);
|
|
163
|
+
interval = Math.min(interval * 2, exports.MAX_POLL_INTERVAL_MS);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
resolveResult(detail) {
|
|
168
|
+
if (detail.status === data_contracts_1.V1TaskStatus.COMPLETED) {
|
|
169
|
+
return Promise.resolve(this.outputs(detail));
|
|
170
|
+
}
|
|
171
|
+
const tasks = Object.values(detail.taskRuns);
|
|
172
|
+
const errors = tasks
|
|
173
|
+
.map((task) => task.error)
|
|
174
|
+
.filter((error) => error !== undefined && error !== '');
|
|
175
|
+
if (errors.length > 0) {
|
|
176
|
+
return Promise.reject(errors);
|
|
177
|
+
}
|
|
178
|
+
// A task with no error under a terminal status is what an explicit cancellation
|
|
179
|
+
// (`ctx.cancel()` on a batch member, for one) leaves behind; the Node client resolves it
|
|
180
|
+
// with the task's output rather than failing the wait, and this keeps that contract.
|
|
181
|
+
if (tasks.length > 0) {
|
|
182
|
+
return Promise.resolve(this.outputs(detail));
|
|
183
|
+
}
|
|
184
|
+
const outcome = detail.status === data_contracts_1.V1TaskStatus.CANCELLED ? 'was cancelled' : 'failed';
|
|
185
|
+
return Promise.reject(new Error(`run ${this.workflowRunId} ${outcome}`));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The outputs keyed by task name, or the standalone task's own output, each read the way
|
|
189
|
+
* the Node client reads them; a task output that is not JSON throws its `SyntaxError`.
|
|
190
|
+
*/
|
|
191
|
+
outputs(detail) {
|
|
192
|
+
const outputs = {};
|
|
193
|
+
for (const task of Object.values(detail.taskRuns)) {
|
|
194
|
+
outputs[task.readableId] = (0, run_detail_1.parseTaskOutput)(task);
|
|
195
|
+
}
|
|
196
|
+
if (this._standaloneTaskName) {
|
|
197
|
+
return outputs[this._standaloneTaskName];
|
|
198
|
+
}
|
|
199
|
+
return outputs;
|
|
200
|
+
}
|
|
201
|
+
toJSON() {
|
|
202
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
203
|
+
return JSON.stringify({ workflowRunId: this.workflowRunId });
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
cancel() {
|
|
207
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
208
|
+
yield this.runs.cancel({ ids: [this.workflowRunId] });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
replay() {
|
|
212
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
+
yield this.runs.replay({ ids: [this.workflowRunId] });
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
exports.WorkflowRunRef = WorkflowRunRef;
|
package/core/types.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The option and result types shared by the core client and the Node client. Everything in
|
|
3
|
+
* this module is edge-safe: it imports nothing from Node, so the Node client's types extend
|
|
4
|
+
* these where they need more.
|
|
5
|
+
*/
|
|
6
|
+
import type { Transport } from '@connectrpc/connect';
|
|
7
|
+
import type { FetchTlsConfig } from '../clients/transport/fetch-transport';
|
|
8
|
+
import type { Priority } from '../v1/declaration';
|
|
9
|
+
import type { WorkerLabelComparator } from '../protoc/v1/shared/trigger';
|
|
10
|
+
import type { RateLimitDuration } from '../protoc/workflows/workflows';
|
|
11
|
+
import type { V1TaskStatus } from '../clients/rest/generated/data-contracts';
|
|
12
|
+
import type { RetrierConfig } from '../util/retrier';
|
|
13
|
+
import type { Logger, LogLevel } from '../util/logger/logger';
|
|
14
|
+
import type { UnaryCallOptions } from '../clients/transport/ts-proto-client';
|
|
15
|
+
/** Builds the logger a client component logs through. */
|
|
16
|
+
export type LogConstructor = (context: string, logLevel?: LogLevel) => Logger;
|
|
17
|
+
/**
|
|
18
|
+
* Per-call options for a unary RPC: `signal` aborts the call, `deadline` (a `Date` or epoch
|
|
19
|
+
* milliseconds) bounds it, `metadata` adds request headers. The same options the Node
|
|
20
|
+
* client's RPC clients take.
|
|
21
|
+
*/
|
|
22
|
+
export type CallOptions = UnaryCallOptions;
|
|
23
|
+
/**
|
|
24
|
+
* The core client's configuration. It is explicit by design: nothing is read from the
|
|
25
|
+
* environment, a config file or the filesystem, so the same object works in a Worker, a
|
|
26
|
+
* browser bundle and Node.
|
|
27
|
+
*/
|
|
28
|
+
export interface CoreClientConfig {
|
|
29
|
+
/** The tenant API token. */
|
|
30
|
+
token: string;
|
|
31
|
+
/**
|
|
32
|
+
* The engine's base URL, for example `https://engine.example.com:7070`: an absolute
|
|
33
|
+
* `http://` or `https://` URL with no username, password, query string or fragment, whose
|
|
34
|
+
* scheme agrees with `tls` (`http://` needs `{ strategy: 'none' }`). When neither this nor
|
|
35
|
+
* `hostPort` is set, the address is read from the token's `grpc_broadcast_address` claim,
|
|
36
|
+
* so `{ token }` alone reaches the engine the token was issued for.
|
|
37
|
+
*/
|
|
38
|
+
serverUrl?: string;
|
|
39
|
+
/** The engine's `host:port`, combined with `tls` into the base URL. */
|
|
40
|
+
hostPort?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Defaults to `{ strategy: 'tls' }`. The token travels on every call, so plain HTTP has to
|
|
43
|
+
* be chosen explicitly with `{ strategy: 'none' }`.
|
|
44
|
+
*/
|
|
45
|
+
tls?: FetchTlsConfig;
|
|
46
|
+
/**
|
|
47
|
+
* The prefix applied to workflow names and event keys, the same way the Node client applies
|
|
48
|
+
* `HATCHET_CLIENT_NAMESPACE`: it is lowercased and gets a trailing underscore when missing.
|
|
49
|
+
*/
|
|
50
|
+
namespace?: string;
|
|
51
|
+
/**
|
|
52
|
+
* A ready transport, in place of the fetch transport the client builds from the fields
|
|
53
|
+
* above. It is used as is: the bearer token, the URL and TLS checks and the message size
|
|
54
|
+
* limits apply to the transport the client builds, so a supplied transport owns its own
|
|
55
|
+
* authentication and limits.
|
|
56
|
+
*/
|
|
57
|
+
transport?: Transport;
|
|
58
|
+
/** The `fetch` the transport sends with. Defaults to the runtime's global `fetch`. */
|
|
59
|
+
fetch?: typeof globalThis.fetch;
|
|
60
|
+
/**
|
|
61
|
+
* The largest response accepted, in bytes after HTTP decompression; a larger one rejects
|
|
62
|
+
* with a `ConnectError` of code `ResourceExhausted`. Defaults to 4 MiB, the Node client's
|
|
63
|
+
* `grpc_max_recv_message_length` default.
|
|
64
|
+
*/
|
|
65
|
+
maxReceiveMessageBytes?: number;
|
|
66
|
+
/**
|
|
67
|
+
* The largest request sent, in bytes; a larger one rejects with a `ConnectError` of code
|
|
68
|
+
* `ResourceExhausted` before anything is sent. Defaults to 4 MiB, the Node client's
|
|
69
|
+
* `grpc_max_send_message_length` default.
|
|
70
|
+
*/
|
|
71
|
+
maxSendMessageBytes?: number;
|
|
72
|
+
/** Retry settings for the unary calls, the same shape as the Node client's `retrier`. */
|
|
73
|
+
retrier?: RetrierConfig;
|
|
74
|
+
logger?: LogConstructor;
|
|
75
|
+
logLevel?: LogLevel;
|
|
76
|
+
}
|
|
77
|
+
export type DesiredWorkerLabelOpt = {
|
|
78
|
+
value: string | number;
|
|
79
|
+
required?: boolean;
|
|
80
|
+
weight?: number;
|
|
81
|
+
comparator?: WorkerLabelComparator;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The options a single `TriggerWorkflow` call accepts. The Node client's `admin.runWorkflow`
|
|
85
|
+
* takes exactly this type.
|
|
86
|
+
*/
|
|
87
|
+
export type TriggerRunOptions = {
|
|
88
|
+
parentId?: string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* (optional) the parent task run external id.
|
|
91
|
+
*
|
|
92
|
+
* This is the field understood by the workflows gRPC API (`parent_task_run_external_id`).
|
|
93
|
+
*/
|
|
94
|
+
parentTaskRunExternalId?: string | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* @deprecated Use `parentTaskRunExternalId` instead.
|
|
97
|
+
* Kept for backward compatibility; will be mapped to `parentTaskRunExternalId`.
|
|
98
|
+
*/
|
|
99
|
+
parentStepRunId?: string | undefined;
|
|
100
|
+
childIndex?: number | undefined;
|
|
101
|
+
childKey?: string | undefined;
|
|
102
|
+
additionalMetadata?: Record<string, string> | undefined;
|
|
103
|
+
desiredWorkerId?: string | undefined;
|
|
104
|
+
priority?: Priority;
|
|
105
|
+
desiredWorkerLabels?: Record<string, DesiredWorkerLabelOpt>;
|
|
106
|
+
_standaloneTaskName?: string | undefined;
|
|
107
|
+
};
|
|
108
|
+
/** One entry of a bulk trigger. */
|
|
109
|
+
export type TriggerRun<I = object> = {
|
|
110
|
+
workflowName: string;
|
|
111
|
+
input: I;
|
|
112
|
+
options?: TriggerRunOptions;
|
|
113
|
+
};
|
|
114
|
+
/** Options for pushing an event. */
|
|
115
|
+
export interface PushEventOptions {
|
|
116
|
+
additionalMetadata?: Record<string, string>;
|
|
117
|
+
priority?: number;
|
|
118
|
+
scope?: string;
|
|
119
|
+
}
|
|
120
|
+
/** One event of a bulk push, with per-event overrides of the push options. */
|
|
121
|
+
export interface EventWithMetadata<T> {
|
|
122
|
+
payload: T;
|
|
123
|
+
additionalMetadata?: Record<string, unknown>;
|
|
124
|
+
priority?: number;
|
|
125
|
+
scope?: string;
|
|
126
|
+
}
|
|
127
|
+
export type CreateRateLimitOpts = {
|
|
128
|
+
key: string;
|
|
129
|
+
limit: number;
|
|
130
|
+
duration?: RateLimitDuration;
|
|
131
|
+
};
|
|
132
|
+
/** The state of one task of a run, as `GetRunDetails` reports it. */
|
|
133
|
+
export type TaskRunDetail = {
|
|
134
|
+
externalId: string;
|
|
135
|
+
readableId: string;
|
|
136
|
+
status: V1TaskStatus;
|
|
137
|
+
/** The output decoded as JSON; `null` when the task stored nothing, `null` or text that is not JSON. */
|
|
138
|
+
output: unknown;
|
|
139
|
+
/** The output text as the engine stored it; absent when the task stored nothing. */
|
|
140
|
+
rawOutput?: string;
|
|
141
|
+
error?: string;
|
|
142
|
+
isEvicted: boolean;
|
|
143
|
+
};
|
|
144
|
+
/** The state of a run, as `GetRunDetails` reports it. `taskRuns` is keyed by readable id. */
|
|
145
|
+
export type RunDetail = {
|
|
146
|
+
status: V1TaskStatus;
|
|
147
|
+
done: boolean;
|
|
148
|
+
input: unknown;
|
|
149
|
+
additionalMetadata: unknown;
|
|
150
|
+
isEvicted: boolean;
|
|
151
|
+
taskRuns: Record<string, TaskRunDetail>;
|
|
152
|
+
};
|
|
153
|
+
/** The filter fields shared by the core and Node run filters. */
|
|
154
|
+
export type RunFilterBase = {
|
|
155
|
+
/** Defaults to one hour ago. */
|
|
156
|
+
since?: Date;
|
|
157
|
+
until?: Date;
|
|
158
|
+
statuses?: V1TaskStatus[];
|
|
159
|
+
additionalMetadata?: Record<string, string>;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* The core client's run filter. It takes workflow ids rather than names because resolving a
|
|
163
|
+
* name is a REST call, which the core client does not make.
|
|
164
|
+
*/
|
|
165
|
+
export type RunFilter = RunFilterBase & {
|
|
166
|
+
workflowIds?: string[];
|
|
167
|
+
};
|
|
168
|
+
export type CancelRunOpts = {
|
|
169
|
+
ids?: string[];
|
|
170
|
+
filters?: RunFilter;
|
|
171
|
+
};
|
|
172
|
+
export type ReplayRunOpts = {
|
|
173
|
+
ids?: string[];
|
|
174
|
+
filters?: RunFilter;
|
|
175
|
+
};
|
|
176
|
+
/** Options for waiting on a run's result. */
|
|
177
|
+
export interface ResultOptions {
|
|
178
|
+
/** Rejects with a `HatchetError` once this many milliseconds have passed. No limit by default. */
|
|
179
|
+
timeoutMs?: number;
|
|
180
|
+
/** Rejects with an `AbortError` when the signal fires. */
|
|
181
|
+
signal?: AbortSignal;
|
|
182
|
+
}
|
package/core/types.js
ADDED
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
|
-
// Verifies
|
|
4
|
-
//
|
|
5
|
-
// any
|
|
3
|
+
// Verifies that an entry point imports nothing from Node. Bundles the entry for a
|
|
4
|
+
// workerd-like browser target and fails on any `node:` specifier or Node builtin reached
|
|
5
|
+
// from it, transitively, on any package named with `--forbid`, and on any SDK module in the
|
|
6
|
+
// bundle that touches a Node-only global (which a bundler cannot see, since nothing imports
|
|
7
|
+
// them): `process`, `Buffer`, `setImmediate`, a timer's `.unref()`, `AbortSignal.timeout`
|
|
8
|
+
// and `AbortSignal.any`. A use the module feature-detects (`typeof setImmediate`,
|
|
9
|
+
// `timer.unref?.()`, `globalThis.Buffer`) is allowed.
|
|
6
10
|
//
|
|
7
|
-
// Run after `pnpm run tsc:build
|
|
8
|
-
// entry
|
|
9
|
-
//
|
|
11
|
+
// Run after `pnpm run tsc:build`:
|
|
12
|
+
// node scripts/check-edge-entry.mjs # dist/edge/index.js
|
|
13
|
+
// node scripts/check-edge-entry.mjs dist/core/index.js --forbid axios,nice-grpc
|
|
14
|
+
// The `check:edge` and `check:core` scripts build first and then run the two entries.
|
|
10
15
|
import { builtinModules } from 'node:module';
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
17
|
import { resolve, dirname } from 'node:path';
|
|
13
18
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { build } from 'esbuild';
|
|
19
|
+
import { build, transformSync } from 'esbuild';
|
|
15
20
|
|
|
16
21
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
-
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const forbidden = new Set();
|
|
25
|
+
let entryArg = 'dist/edge/index.js';
|
|
26
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
27
|
+
if (args[i] === '--forbid') {
|
|
28
|
+
for (const pkg of (args[i + 1] ?? '').split(',')) if (pkg) forbidden.add(pkg);
|
|
29
|
+
i += 1;
|
|
30
|
+
} else {
|
|
31
|
+
entryArg = args[i];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entry = resolve(root, entryArg);
|
|
36
|
+
const label = entryArg.replace(/^dist\//, '').replace(/\/index\.js$/, '');
|
|
18
37
|
|
|
19
38
|
if (!existsSync(entry)) {
|
|
20
39
|
console.error(`entry not built: ${entry} is missing. Run \`pnpm run tsc:build\` first.`);
|
|
@@ -63,22 +82,61 @@ const result = await build({
|
|
|
63
82
|
});
|
|
64
83
|
|
|
65
84
|
const bundled = Object.keys(result.metafile.inputs);
|
|
66
|
-
const sdkInputs = bundled.filter((f) => f.
|
|
85
|
+
const sdkInputs = bundled.filter((f) => !f.includes('node_modules/'));
|
|
67
86
|
const packageInputs = [...new Set(bundled.filter((f) => f.includes('node_modules/')).map((f) => {
|
|
68
87
|
const m = f.match(/node_modules\/(?:\.pnpm\/[^/]+\/node_modules\/)?((?:@[^/]+\/)?[^/]+)/);
|
|
69
88
|
return m ? m[1] : f;
|
|
70
89
|
}))].sort();
|
|
71
90
|
|
|
72
|
-
console.log(
|
|
91
|
+
console.log(`${label} entry: bundled ${sdkInputs.length} SDK module(s) and ${packageInputs.length} package(s)`);
|
|
73
92
|
if (packageInputs.length) console.log(`packages: ${packageInputs.join(', ')}`);
|
|
74
93
|
|
|
94
|
+
let failed = false;
|
|
95
|
+
|
|
75
96
|
if (violations.size > 0) {
|
|
76
|
-
|
|
97
|
+
failed = true;
|
|
98
|
+
console.error(`\n${label} entry reaches Node builtins:`);
|
|
77
99
|
for (const [specifier, importers] of [...violations.entries()].sort()) {
|
|
78
100
|
console.error(` ${specifier}`);
|
|
79
101
|
for (const importer of [...importers].sort()) console.error(` from ${importer}`);
|
|
80
102
|
}
|
|
81
|
-
process.exit(1);
|
|
82
103
|
}
|
|
83
104
|
|
|
84
|
-
|
|
105
|
+
const forbiddenHits = packageInputs.filter((pkg) => forbidden.has(pkg));
|
|
106
|
+
if (forbiddenHits.length > 0) {
|
|
107
|
+
failed = true;
|
|
108
|
+
console.error(`\n${label} entry bundles forbidden package(s): ${forbiddenHits.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// These are globals in Node that workerd and browsers lack, or that some runtimes the entry
|
|
112
|
+
// targets lack (`AbortSignal.any`, `AbortSignal.timeout`); a module that reads them fails at
|
|
113
|
+
// runtime without ever importing anything. A module that feature-detects one may use it: a
|
|
114
|
+
// `globalThis.Buffer` access (which the generated bindings guard) and an optional
|
|
115
|
+
// `.unref?.()` call are their own guards, and a `typeof` check of the global anywhere in the
|
|
116
|
+
// module allows its use in that module.
|
|
117
|
+
const nodeGlobals = [
|
|
118
|
+
{ what: 'the process or Buffer globals', pattern: /(?<!globalThis\.)\b(?:process|Buffer)\s*\./ },
|
|
119
|
+
{ what: 'setImmediate', pattern: /(?<![.\w$])setImmediate\s*\(/, guard: /typeof\s+setImmediate\b/ },
|
|
120
|
+
{ what: "a timer's .unref()", pattern: /(?<!\?)\.unref\s*\(/ },
|
|
121
|
+
{
|
|
122
|
+
what: 'AbortSignal.timeout or AbortSignal.any',
|
|
123
|
+
pattern: /\bAbortSignal\s*\.\s*(?:timeout|any)\b/,
|
|
124
|
+
guard: /typeof\s+AbortSignal\s*\.\s*(?:timeout|any)\b/,
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
// Comments describe these globals without touching them, so they are stripped first. esbuild
|
|
128
|
+
// drops them while leaving string literals alone, which a regex over `//` cannot promise.
|
|
129
|
+
const stripComments = (source) => transformSync(source, { loader: 'js', legalComments: 'none' }).code;
|
|
130
|
+
const sources = new Map(sdkInputs.map((f) => [f, stripComments(readFileSync(resolve(root, f), 'utf8'))]));
|
|
131
|
+
for (const { what, pattern, guard } of nodeGlobals) {
|
|
132
|
+
const hits = sdkInputs.filter((f) => pattern.test(sources.get(f)) && !(guard && guard.test(sources.get(f))));
|
|
133
|
+
if (hits.length > 0) {
|
|
134
|
+
failed = true;
|
|
135
|
+
console.error(`\n${label} entry bundles SDK module(s) that use ${what}:`);
|
|
136
|
+
for (const f of hits) console.error(` ${f}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (failed) process.exit(1);
|
|
141
|
+
|
|
142
|
+
console.log(`${label} entry is free of Node builtins${forbidden.size ? ' and forbidden packages' : ''}`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const script = resolve(__dirname, 'check-edge-entry.mjs');
|
|
7
|
+
|
|
8
|
+
/** Runs the checker on a module holding `source`, as if it were an SDK module in the bundle. */
|
|
9
|
+
function check(source: string): { status: number | null; stderr: string } {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), 'check-edge-entry-'));
|
|
11
|
+
try {
|
|
12
|
+
const entry = join(dir, 'entry.js');
|
|
13
|
+
writeFileSync(entry, source);
|
|
14
|
+
const result = spawnSync(process.execPath, [script, entry], { encoding: 'utf8' });
|
|
15
|
+
return { status: result.status, stderr: result.stderr };
|
|
16
|
+
} finally {
|
|
17
|
+
rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('check-edge-entry', () => {
|
|
22
|
+
it('passes a module that uses none of the Node-only globals', () => {
|
|
23
|
+
const result = check(`
|
|
24
|
+
// setImmediate(() => {}) in a comment is fine, as is AbortSignal.any in prose.
|
|
25
|
+
/* process.exit() described here is fine too */
|
|
26
|
+
export const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27
|
+
export const url = 'https://example.com//path';
|
|
28
|
+
`);
|
|
29
|
+
expect(result.stderr).toBe('');
|
|
30
|
+
expect(result.status).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each([
|
|
34
|
+
['setImmediate', 'export const tick = () => setImmediate(() => {});', /setImmediate/],
|
|
35
|
+
['process.nextTick', 'export const tick = () => process.nextTick(() => {});', /process or Buffer/],
|
|
36
|
+
['Buffer', "export const bytes = Buffer.from('x');", /process or Buffer/],
|
|
37
|
+
['Buffer after // inside a string', "export const p = 'a//b'; export const bytes = Buffer.from('x');", /process or Buffer/],
|
|
38
|
+
['.unref(', 'export const timer = setTimeout(() => {}, 1); timer.unref();', /\.unref\(\)/],
|
|
39
|
+
['AbortSignal.timeout', 'export const signal = AbortSignal.timeout(5);', /AbortSignal\.timeout or AbortSignal\.any/],
|
|
40
|
+
['AbortSignal.any', 'export const signal = AbortSignal.any([]);', /AbortSignal\.timeout or AbortSignal\.any/],
|
|
41
|
+
])('fails a module that uses %s', (_name, source, message) => {
|
|
42
|
+
const result = check(source);
|
|
43
|
+
expect(result.status).toBe(1);
|
|
44
|
+
expect(result.stderr).toMatch(message);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each([
|
|
48
|
+
['typeof setImmediate', "export const tick = typeof setImmediate === 'function' ? (f) => setImmediate(f) : (f) => setTimeout(f, 0);"],
|
|
49
|
+
['an optional .unref?.()', 'export const timer = setTimeout(() => {}, 1); timer.unref?.();'],
|
|
50
|
+
['typeof AbortSignal.any', "export const has = typeof AbortSignal.any === 'function';"],
|
|
51
|
+
['globalThis.Buffer', "export const bytes = globalThis.Buffer ? globalThis.Buffer.from('x') : new Uint8Array();"],
|
|
52
|
+
])('allows a feature-detected %s', (_name, source) => {
|
|
53
|
+
const result = check(source);
|
|
54
|
+
expect(result.stderr).toBe('');
|
|
55
|
+
expect(result.status).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hatchet-dev/typescript-sdk",
|
|
3
|
-
"version": "1.34.0-alpha.
|
|
3
|
+
"version": "1.34.0-alpha.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=20"
|
|
6
6
|
},
|
|
@@ -163,12 +163,14 @@
|
|
|
163
163
|
"typedoc-plugin-markdown": "^4.10.0",
|
|
164
164
|
"typedoc-plugin-no-inherit": "^1.6.1",
|
|
165
165
|
"typescript": "^5.8.2",
|
|
166
|
-
"typescript-eslint": "^8.56.1"
|
|
166
|
+
"typescript-eslint": "^8.56.1",
|
|
167
|
+
"undici": "^7.29.1"
|
|
167
168
|
},
|
|
168
169
|
"dependencies": {
|
|
169
170
|
"@bufbuild/protobuf": "^2.15.0",
|
|
170
171
|
"@connectrpc/connect": "~2.1.2",
|
|
171
172
|
"@connectrpc/connect-node": "~2.1.2",
|
|
173
|
+
"@connectrpc/connect-web": "~2.1.2",
|
|
172
174
|
"@types/qs": "^6.15.0",
|
|
173
175
|
"abort-controller-x": "^0.5.0",
|
|
174
176
|
"axios": "^1.15.2",
|
|
@@ -236,6 +238,7 @@
|
|
|
236
238
|
"tsc:build": "pnpm run dump-version && tsc && resolve-tspaths",
|
|
237
239
|
"test:unit": "jest --testMatch='**/*.test.ts'",
|
|
238
240
|
"check:edge": "pnpm run tsc:build && node scripts/check-edge-entry.mjs",
|
|
241
|
+
"check:core": "pnpm run tsc:build && node scripts/check-edge-entry.mjs dist/core/index.js --forbid axios,nice-grpc,@grpc/grpc-js,@connectrpc/connect-node,long,protobufjs,yaml",
|
|
239
242
|
"check:exports": "pnpm run tsc:build && node scripts/check-exports.mjs",
|
|
240
243
|
"test:e2e": "jest --config jest.e2e.config.ts --silent --forceExit --runInBand",
|
|
241
244
|
"test:unit:watch": "jest --testMatch='**/*.test.ts' --watch",
|