@hatchet-dev/typescript-sdk 1.34.0-alpha.1 → 1.34.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/README.md +1 -0
- 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/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/protoc/v1/streams.d.ts +79 -0
- package/protoc/v1/streams.js +511 -0
- package/protoc-es/v1/streams_pb.d.ts +133 -0
- package/protoc-es/v1/streams_pb.js +41 -0
- 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 +14 -1
- package/v1/client/client.js +18 -0
- package/v1/client/features/index.d.ts +1 -0
- package/v1/client/features/index.js +1 -0
- package/v1/client/features/runs.d.ts +9 -27
- package/v1/client/features/runs.js +10 -55
- package/v1/client/features/streams.d.ts +47 -0
- package/v1/client/features/streams.js +210 -0
- package/v1/examples/durable-streams/publish-latency.d.ts +15 -0
- package/v1/examples/durable-streams/publish-latency.js +50 -0
- package/v1/examples/durable_callback_ordering/workflow.js +5 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ TypeScript SDK reference (overview: https://docs.hatchet.run/reference/typescrip
|
|
|
49
49
|
- Rate Limits: https://docs.hatchet.run/reference/typescript/feature-clients/ratelimits.md (guide: https://docs.hatchet.run/v1/rate-limits.md)
|
|
50
50
|
- Runs: https://docs.hatchet.run/reference/typescript/feature-clients/runs.md (guide: https://docs.hatchet.run/v1/running-your-task.md)
|
|
51
51
|
- Scheduled Runs: https://docs.hatchet.run/reference/typescript/feature-clients/schedules.md (guide: https://docs.hatchet.run/v1/scheduled-runs.md)
|
|
52
|
+
- Streams: https://docs.hatchet.run/reference/typescript/feature-clients/streams.md
|
|
52
53
|
- Tenant: https://docs.hatchet.run/reference/typescript/feature-clients/tenant.md
|
|
53
54
|
- Webhooks: https://docs.hatchet.run/reference/typescript/feature-clients/webhooks.md (guide: https://docs.hatchet.run/v1/webhooks.md)
|
|
54
55
|
- Workers: https://docs.hatchet.run/reference/typescript/feature-clients/workers.md (guide: https://docs.hatchet.run/v1/workers.md)
|
package/clients/admin/rpc.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WorkflowServiceClient } from '../../protoc/workflows/workflows';
|
|
2
2
|
import { AdminServiceClient } from '../../protoc/v1/workflows';
|
|
3
|
-
import {
|
|
3
|
+
import type { Transport } from '../transport/transport';
|
|
4
4
|
/**
|
|
5
5
|
* The `WorkflowService` client, every RPC of the generated `WorkflowServiceClient` interface,
|
|
6
6
|
* served by a Connect client on the given transport.
|
package/clients/admin/rpc.js
CHANGED
|
@@ -6,18 +6,18 @@ const workflows_1 = require("../../protoc/workflows/workflows");
|
|
|
6
6
|
const workflows_pb_1 = require("../../protoc-es/workflows/workflows_pb");
|
|
7
7
|
const workflows_2 = require("../../protoc/v1/workflows");
|
|
8
8
|
const workflows_pb_2 = require("../../protoc-es/v1/workflows_pb");
|
|
9
|
-
const
|
|
9
|
+
const ts_proto_client_1 = require("../transport/ts-proto-client");
|
|
10
10
|
/**
|
|
11
11
|
* The `WorkflowService` client, every RPC of the generated `WorkflowServiceClient` interface,
|
|
12
12
|
* served by a Connect client on the given transport.
|
|
13
13
|
*/
|
|
14
14
|
function createWorkflowsRpc(transport) {
|
|
15
|
-
return (0,
|
|
15
|
+
return (0, ts_proto_client_1.createTsProtoClient)(workflows_1.WorkflowServiceDefinition, workflows_pb_1.WorkflowService, transport);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* The v1 `AdminService` client, every RPC of the generated `AdminServiceClient` interface,
|
|
19
19
|
* served by a Connect client on the given transport.
|
|
20
20
|
*/
|
|
21
21
|
function createV1AdminRpc(transport) {
|
|
22
|
-
return (0,
|
|
22
|
+
return (0, ts_proto_client_1.createTsProtoClient)(workflows_2.AdminServiceDefinition, workflows_pb_2.AdminService, transport);
|
|
23
23
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ConnectError } from '@connectrpc/connect';
|
|
2
|
+
import { BulkTriggerIdempotencyCollisionError } from '../../util/errors/bulk-trigger-idempotency-collision-error';
|
|
3
|
+
import type { TriggerWorkflowRequest } from '../../protoc/v1/shared/trigger';
|
|
4
|
+
import type { DeepPartial } from '../transport/message-bridge';
|
|
5
|
+
import type { TriggerRunOptions } from '../../core/types';
|
|
6
|
+
/**
|
|
7
|
+
* One builder for both clients, so a run triggered from the core client is indistinguishable
|
|
8
|
+
* to the engine from one triggered by the Node client.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildTriggerWorkflowRequest<Q>(workflowName: string, input: Q, options: TriggerRunOptions | undefined, namespace: string | undefined): DeepPartial<TriggerWorkflowRequest>;
|
|
11
|
+
/**
|
|
12
|
+
* The engine answers a trigger whose idempotency key is already taken with `ALREADY_EXISTS`
|
|
13
|
+
* and attaches the collision details to the status. The Connect client decodes those details
|
|
14
|
+
* from the trailer, so the error carries everything needed to build the SDK's collision errors.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isAlreadyExists(e: unknown): e is ConnectError;
|
|
17
|
+
export declare function extractExistingRunId(e: ConnectError): string;
|
|
18
|
+
export declare function extractBulkTriggerCollision(e: ConnectError): BulkTriggerIdempotencyCollisionError | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTriggerWorkflowRequest = buildTriggerWorkflowRequest;
|
|
4
|
+
exports.isAlreadyExists = isAlreadyExists;
|
|
5
|
+
exports.extractExistingRunId = extractExistingRunId;
|
|
6
|
+
exports.extractBulkTriggerCollision = extractBulkTriggerCollision;
|
|
7
|
+
const connect_1 = require("@connectrpc/connect");
|
|
8
|
+
const idempotency_collision_error_1 = require("../../util/errors/idempotency-collision-error");
|
|
9
|
+
const bulk_trigger_idempotency_collision_error_1 = require("../../util/errors/bulk-trigger-idempotency-collision-error");
|
|
10
|
+
const workflows_pb_1 = require("../../protoc-es/v1/workflows_pb");
|
|
11
|
+
const apply_namespace_1 = require("../../util/apply-namespace");
|
|
12
|
+
/**
|
|
13
|
+
* One builder for both clients, so a run triggered from the core client is indistinguishable
|
|
14
|
+
* to the engine from one triggered by the Node client.
|
|
15
|
+
*/
|
|
16
|
+
function buildTriggerWorkflowRequest(workflowName, input, options, namespace) {
|
|
17
|
+
var _a;
|
|
18
|
+
const opts = options !== null && options !== void 0 ? options : {};
|
|
19
|
+
return {
|
|
20
|
+
name: (0, apply_namespace_1.applyNamespace)(workflowName, namespace).toLowerCase(),
|
|
21
|
+
input: JSON.stringify(input),
|
|
22
|
+
parentId: opts.parentId,
|
|
23
|
+
parentTaskRunExternalId: (_a = opts.parentTaskRunExternalId) !== null && _a !== void 0 ? _a : opts.parentStepRunId,
|
|
24
|
+
childIndex: opts.childIndex,
|
|
25
|
+
childKey: opts.childKey,
|
|
26
|
+
additionalMetadata: opts.additionalMetadata
|
|
27
|
+
? JSON.stringify(opts.additionalMetadata)
|
|
28
|
+
: undefined,
|
|
29
|
+
desiredWorkerId: opts.desiredWorkerId,
|
|
30
|
+
priority: opts.priority,
|
|
31
|
+
desiredWorkerLabels: opts.desiredWorkerLabels
|
|
32
|
+
? convertDesiredWorkerLabels(opts.desiredWorkerLabels)
|
|
33
|
+
: {},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function convertDesiredWorkerLabels(labels) {
|
|
37
|
+
return Object.fromEntries(Object.entries(labels).map(([key, label]) => [
|
|
38
|
+
key,
|
|
39
|
+
{
|
|
40
|
+
strValue: typeof label.value === 'string' ? label.value : undefined,
|
|
41
|
+
intValue: typeof label.value === 'number' ? label.value : undefined,
|
|
42
|
+
required: label.required,
|
|
43
|
+
weight: label.weight,
|
|
44
|
+
comparator: label.comparator,
|
|
45
|
+
},
|
|
46
|
+
]));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The engine answers a trigger whose idempotency key is already taken with `ALREADY_EXISTS`
|
|
50
|
+
* and attaches the collision details to the status. The Connect client decodes those details
|
|
51
|
+
* from the trailer, so the error carries everything needed to build the SDK's collision errors.
|
|
52
|
+
*/
|
|
53
|
+
function isAlreadyExists(e) {
|
|
54
|
+
return e instanceof connect_1.ConnectError && e.code === connect_1.Code.AlreadyExists;
|
|
55
|
+
}
|
|
56
|
+
function extractExistingRunId(e) {
|
|
57
|
+
var _a;
|
|
58
|
+
const [detail] = e.findDetails(workflows_pb_1.IdempotencyCollisionErrorSchema);
|
|
59
|
+
return (_a = detail === null || detail === void 0 ? void 0 : detail.existingRunExternalId) !== null && _a !== void 0 ? _a : '';
|
|
60
|
+
}
|
|
61
|
+
function extractBulkTriggerCollision(e) {
|
|
62
|
+
const [detail] = e.findDetails(workflows_pb_1.BulkTriggerIdempotencyCollisionErrorSchema);
|
|
63
|
+
if (!detail)
|
|
64
|
+
return null;
|
|
65
|
+
return new bulk_trigger_idempotency_collision_error_1.BulkTriggerIdempotencyCollisionError(detail.successfulWorkflowRunExternalIds, detail.collisions.map((c) => new idempotency_collision_error_1.IdempotencyCollisionError(c.existingRunExternalId)));
|
|
66
|
+
}
|
|
@@ -5,23 +5,10 @@ import { ClientConfig } from '../hatchet-client/client-config';
|
|
|
5
5
|
import { Logger } from '../../util/logger';
|
|
6
6
|
import { retrier } from '../../util/retrier';
|
|
7
7
|
import { HatchetClient } from '../../v1';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
DEBUG = "DEBUG"
|
|
13
|
-
}
|
|
14
|
-
export interface PushEventOptions {
|
|
15
|
-
additionalMetadata?: Record<string, string>;
|
|
16
|
-
priority?: number;
|
|
17
|
-
scope?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface EventWithMetadata<T> {
|
|
20
|
-
payload: T;
|
|
21
|
-
additionalMetadata?: Record<string, unknown>;
|
|
22
|
-
priority?: number;
|
|
23
|
-
scope?: string;
|
|
24
|
-
}
|
|
8
|
+
import type { EventWithMetadata, PushEventOptions } from '../../core/types';
|
|
9
|
+
import { LogLevel } from './rpc';
|
|
10
|
+
export type { EventWithMetadata, PushEventOptions };
|
|
11
|
+
export { LogLevel };
|
|
25
12
|
export declare class EventClient {
|
|
26
13
|
config: ClientConfig;
|
|
27
14
|
client: EventsServiceClient;
|
|
@@ -11,25 +11,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.EventClient = exports.LogLevel = void 0;
|
|
13
13
|
const transport_1 = require("../transport");
|
|
14
|
-
const rpc_1 = require("./rpc");
|
|
15
14
|
const hatchet_error_1 = require("../../util/errors/hatchet-error");
|
|
16
15
|
const retrier_1 = require("../../util/retrier");
|
|
17
16
|
const apply_namespace_1 = require("../../util/apply-namespace");
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
(function (LogLevel) {
|
|
21
|
-
LogLevel["INFO"] = "INFO";
|
|
22
|
-
LogLevel["WARN"] = "WARN";
|
|
23
|
-
LogLevel["ERROR"] = "ERROR";
|
|
24
|
-
LogLevel["DEBUG"] = "DEBUG";
|
|
25
|
-
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
26
|
-
function injectSourceInfo(metadata) {
|
|
27
|
-
const ctx = parent_run_context_vars_1.parentRunContextManager.getContext();
|
|
28
|
-
if (!(ctx === null || ctx === void 0 ? void 0 : ctx.parentId) || !(ctx === null || ctx === void 0 ? void 0 : ctx.parentTaskRunExternalId)) {
|
|
29
|
-
return metadata;
|
|
30
|
-
}
|
|
31
|
-
return Object.assign(Object.assign({}, metadata), { hatchet__source_workflow_run_id: ctx.parentId, hatchet__source_step_run_id: ctx.parentTaskRunExternalId });
|
|
32
|
-
}
|
|
17
|
+
const rpc_1 = require("./rpc");
|
|
18
|
+
Object.defineProperty(exports, "LogLevel", { enumerable: true, get: function () { return rpc_1.LogLevel; } });
|
|
33
19
|
class EventClient {
|
|
34
20
|
// The positional channel and factory parameters are part of the public constructor signature;
|
|
35
21
|
// unary event calls go over the transport and do not use them.
|
|
@@ -46,17 +32,8 @@ class EventClient {
|
|
|
46
32
|
* Keep the signature in sync with the instrumentor wrapper.
|
|
47
33
|
*/
|
|
48
34
|
push(type, input, options = {}) {
|
|
49
|
-
var _a;
|
|
50
35
|
const namespacedType = (0, apply_namespace_1.applyNamespace)(type, this.config.namespace);
|
|
51
|
-
const
|
|
52
|
-
const req = {
|
|
53
|
-
key: namespacedType,
|
|
54
|
-
payload: JSON.stringify(input),
|
|
55
|
-
eventTimestamp: new Date(),
|
|
56
|
-
additionalMetadata: Object.keys(enhancedMetadata).length > 0 ? JSON.stringify(enhancedMetadata) : undefined,
|
|
57
|
-
priority: options.priority,
|
|
58
|
-
scope: options.scope,
|
|
59
|
-
};
|
|
36
|
+
const req = (0, rpc_1.buildPushEventRequest)(type, input, options, this.config.namespace);
|
|
60
37
|
return this.retrier(() => __awaiter(this, void 0, void 0, function* () { return this.client.push(req); }), this.logger, this.config.retrier)
|
|
61
38
|
.then((result) => {
|
|
62
39
|
this.logger.info(`Event pushed: ${namespacedType}`);
|
|
@@ -72,22 +49,7 @@ class EventClient {
|
|
|
72
49
|
*/
|
|
73
50
|
bulkPush(type, inputs, options = {}) {
|
|
74
51
|
const namespacedType = (0, apply_namespace_1.applyNamespace)(type, this.config.namespace);
|
|
75
|
-
const
|
|
76
|
-
var _a, _b, _c, _d;
|
|
77
|
-
const baseMeta = (_b = (_a = input.additionalMetadata) !== null && _a !== void 0 ? _a : options.additionalMetadata) !== null && _b !== void 0 ? _b : {};
|
|
78
|
-
const enhanced = injectSourceInfo(baseMeta);
|
|
79
|
-
return {
|
|
80
|
-
key: namespacedType,
|
|
81
|
-
payload: JSON.stringify(input.payload),
|
|
82
|
-
eventTimestamp: new Date(),
|
|
83
|
-
additionalMetadata: Object.keys(enhanced).length > 0 ? JSON.stringify(enhanced) : undefined,
|
|
84
|
-
priority: (_c = input.priority) !== null && _c !== void 0 ? _c : options.priority,
|
|
85
|
-
scope: (_d = input.scope) !== null && _d !== void 0 ? _d : options.scope,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
const req = {
|
|
89
|
-
events,
|
|
90
|
-
};
|
|
52
|
+
const req = (0, rpc_1.buildBulkPushEventRequest)(type, inputs, options, this.config.namespace);
|
|
91
53
|
return this.retrier(() => __awaiter(this, void 0, void 0, function* () { return this.client.bulkPush(req); }), this.logger, this.config.retrier)
|
|
92
54
|
.then((result) => {
|
|
93
55
|
this.logger.info(`Bulk events pushed for type: ${namespacedType}`);
|
|
@@ -110,7 +72,7 @@ class EventClient {
|
|
|
110
72
|
taskRunExternalId,
|
|
111
73
|
createdAt,
|
|
112
74
|
message: log,
|
|
113
|
-
level: level || LogLevel.INFO,
|
|
75
|
+
level: level || rpc_1.LogLevel.INFO,
|
|
114
76
|
taskRetryCount,
|
|
115
77
|
metadata: metadata ? JSON.stringify(metadata) : undefined,
|
|
116
78
|
})
|
package/clients/event/rpc.d.ts
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import { EventsServiceClient } from '../../protoc/events/events';
|
|
2
|
-
import {
|
|
1
|
+
import { BulkPushEventRequest, EventsServiceClient, PushEventRequest } from '../../protoc/events/events';
|
|
2
|
+
import type { Transport } from '../transport/transport';
|
|
3
|
+
import type { EventWithMetadata, PushEventOptions } from '../../core/types';
|
|
4
|
+
/** The levels a task run log line is stored with. */
|
|
5
|
+
export declare enum LogLevel {
|
|
6
|
+
INFO = "INFO",
|
|
7
|
+
WARN = "WARN",
|
|
8
|
+
ERROR = "ERROR",
|
|
9
|
+
DEBUG = "DEBUG"
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* The `EventsService` client, every RPC of the generated `EventsServiceClient` interface,
|
|
5
|
-
* served by a Connect client on the given transport.
|
|
13
|
+
* served by a Connect client on the given transport. The Node and core clients both call
|
|
14
|
+
* through it.
|
|
6
15
|
*/
|
|
7
16
|
export declare function createEventsRpc(transport: Transport): EventsServiceClient;
|
|
17
|
+
/**
|
|
18
|
+
* One builder for both clients, so an event pushed from the core client is indistinguishable
|
|
19
|
+
* to the engine from one pushed by the Node client.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildPushEventRequest<T>(type: string, input: T, options: PushEventOptions, namespace: string | undefined): PushEventRequest;
|
|
22
|
+
/**
|
|
23
|
+
* Builds the `BulkPush` request for events of one type. Each event's own metadata, priority
|
|
24
|
+
* and scope win over the shared options.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildBulkPushEventRequest<T>(type: string, inputs: EventWithMetadata<T>[], options: PushEventOptions, namespace: string | undefined): BulkPushEventRequest;
|
package/clients/event/rpc.js
CHANGED
|
@@ -1,13 +1,76 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogLevel = void 0;
|
|
3
4
|
exports.createEventsRpc = createEventsRpc;
|
|
5
|
+
exports.buildPushEventRequest = buildPushEventRequest;
|
|
6
|
+
exports.buildBulkPushEventRequest = buildBulkPushEventRequest;
|
|
4
7
|
const events_1 = require("../../protoc/events/events");
|
|
5
8
|
const events_pb_1 = require("../../protoc-es/events/events_pb");
|
|
6
|
-
const
|
|
9
|
+
const ts_proto_client_1 = require("../transport/ts-proto-client");
|
|
10
|
+
const apply_namespace_1 = require("../../util/apply-namespace");
|
|
11
|
+
const parent_run_context_vars_1 = require("../../v1/parent-run-context-vars");
|
|
12
|
+
/** The levels a task run log line is stored with. */
|
|
13
|
+
var LogLevel;
|
|
14
|
+
(function (LogLevel) {
|
|
15
|
+
LogLevel["INFO"] = "INFO";
|
|
16
|
+
LogLevel["WARN"] = "WARN";
|
|
17
|
+
LogLevel["ERROR"] = "ERROR";
|
|
18
|
+
LogLevel["DEBUG"] = "DEBUG";
|
|
19
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
7
20
|
/**
|
|
8
21
|
* The `EventsService` client, every RPC of the generated `EventsServiceClient` interface,
|
|
9
|
-
* served by a Connect client on the given transport.
|
|
22
|
+
* served by a Connect client on the given transport. The Node and core clients both call
|
|
23
|
+
* through it.
|
|
10
24
|
*/
|
|
11
25
|
function createEventsRpc(transport) {
|
|
12
|
-
return (0,
|
|
26
|
+
return (0, ts_proto_client_1.createTsProtoClient)(events_1.EventsServiceDefinition, events_pb_1.EventsService, transport);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* When an event is pushed from inside a task, the run that pushed it is recorded in the
|
|
30
|
+
* event's metadata so the dashboard can link the two.
|
|
31
|
+
*/
|
|
32
|
+
function injectSourceInfo(metadata) {
|
|
33
|
+
const ctx = parent_run_context_vars_1.parentRunContextManager.getContext();
|
|
34
|
+
if (!(ctx === null || ctx === void 0 ? void 0 : ctx.parentId) || !(ctx === null || ctx === void 0 ? void 0 : ctx.parentTaskRunExternalId)) {
|
|
35
|
+
return metadata;
|
|
36
|
+
}
|
|
37
|
+
return Object.assign(Object.assign({}, metadata), { hatchet__source_workflow_run_id: ctx.parentId, hatchet__source_step_run_id: ctx.parentTaskRunExternalId });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One builder for both clients, so an event pushed from the core client is indistinguishable
|
|
41
|
+
* to the engine from one pushed by the Node client.
|
|
42
|
+
*/
|
|
43
|
+
function buildPushEventRequest(type, input, options, namespace) {
|
|
44
|
+
var _a;
|
|
45
|
+
const enhancedMetadata = injectSourceInfo((_a = options.additionalMetadata) !== null && _a !== void 0 ? _a : {});
|
|
46
|
+
return {
|
|
47
|
+
key: (0, apply_namespace_1.applyNamespace)(type, namespace),
|
|
48
|
+
payload: JSON.stringify(input),
|
|
49
|
+
eventTimestamp: new Date(),
|
|
50
|
+
additionalMetadata: Object.keys(enhancedMetadata).length > 0 ? JSON.stringify(enhancedMetadata) : undefined,
|
|
51
|
+
priority: options.priority,
|
|
52
|
+
scope: options.scope,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the `BulkPush` request for events of one type. Each event's own metadata, priority
|
|
57
|
+
* and scope win over the shared options.
|
|
58
|
+
*/
|
|
59
|
+
function buildBulkPushEventRequest(type, inputs, options, namespace) {
|
|
60
|
+
const namespacedType = (0, apply_namespace_1.applyNamespace)(type, namespace);
|
|
61
|
+
return {
|
|
62
|
+
events: inputs.map((input) => {
|
|
63
|
+
var _a, _b, _c, _d;
|
|
64
|
+
const baseMeta = (_b = (_a = input.additionalMetadata) !== null && _a !== void 0 ? _a : options.additionalMetadata) !== null && _b !== void 0 ? _b : {};
|
|
65
|
+
const enhanced = injectSourceInfo(baseMeta);
|
|
66
|
+
return {
|
|
67
|
+
key: namespacedType,
|
|
68
|
+
payload: JSON.stringify(input.payload),
|
|
69
|
+
eventTimestamp: new Date(),
|
|
70
|
+
additionalMetadata: Object.keys(enhanced).length > 0 ? JSON.stringify(enhanced) : undefined,
|
|
71
|
+
priority: (_c = input.priority) !== null && _c !== void 0 ? _c : options.priority,
|
|
72
|
+
scope: (_d = input.scope) !== null && _d !== void 0 ? _d : options.scope,
|
|
73
|
+
};
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
13
76
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type Transport } from './transport';
|
|
2
|
+
/**
|
|
3
|
+
* How a fetch-based client reaches the engine. `none` speaks plain HTTP; `tls` speaks HTTPS
|
|
4
|
+
* and verifies the server the way the runtime's `fetch` does. `serverName` is accepted so the
|
|
5
|
+
* config mirrors the Node client's `tls_config`, but `fetch` has no way to verify a
|
|
6
|
+
* certificate against a name other than the URL host, so it is only honored by a transport
|
|
7
|
+
* that can (a custom `transport` passed to the client).
|
|
8
|
+
*/
|
|
9
|
+
export interface FetchTlsConfig {
|
|
10
|
+
strategy: 'none' | 'tls';
|
|
11
|
+
serverName?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface FetchTransportOptions {
|
|
14
|
+
/** The tenant API token, sent as a bearer token on every call. */
|
|
15
|
+
token: string;
|
|
16
|
+
/**
|
|
17
|
+
* The engine's base URL, for example `https://engine.example.com:7070`: an absolute
|
|
18
|
+
* `http://` or `https://` URL with no username, password, query string or fragment. The RPC
|
|
19
|
+
* paths are appended to it as given, minus trailing slashes. Its scheme must agree with
|
|
20
|
+
* `tls`: `http://` needs `{ strategy: 'none' }`, since the token travels on every call.
|
|
21
|
+
*/
|
|
22
|
+
serverUrl?: string;
|
|
23
|
+
/** The engine's `host:port`; used with `tls` to build the base URL when `serverUrl` is unset. */
|
|
24
|
+
hostPort?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Defaults to `tls`. `hostPort` takes its scheme from it (`https://` unless `none`), and a
|
|
27
|
+
* `serverUrl` must agree with it.
|
|
28
|
+
*/
|
|
29
|
+
tls?: FetchTlsConfig;
|
|
30
|
+
/** The `fetch` to send requests with. Defaults to the runtime's global `fetch`. */
|
|
31
|
+
fetch?: typeof globalThis.fetch;
|
|
32
|
+
/**
|
|
33
|
+
* The largest response accepted, in bytes after the runtime's HTTP decompression. Defaults
|
|
34
|
+
* to 4 MiB, the Node transport's `grpc_max_recv_message_length` default.
|
|
35
|
+
*/
|
|
36
|
+
maxReceiveMessageBytes?: number;
|
|
37
|
+
/**
|
|
38
|
+
* The largest request sent, in bytes. Defaults to 4 MiB, the Node transport's
|
|
39
|
+
* `grpc_max_send_message_length` default.
|
|
40
|
+
*/
|
|
41
|
+
maxSendMessageBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
/** The limits `limitMessageSizes` applies, in bytes. */
|
|
44
|
+
export interface MessageSizeLimits {
|
|
45
|
+
receive: number;
|
|
46
|
+
send: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the engine's base URL from a `serverUrl` or from `hostPort` plus the TLS
|
|
50
|
+
* strategy, the two ways a fetch-based client can be pointed at an engine. `hostPort` takes
|
|
51
|
+
* the gRPC target forms the Node client takes (`host:port`, `dns:///host:port`, `ipv4:`,
|
|
52
|
+
* `ipv6:`), so a token or config shared with a Node client works unchanged. A `serverUrl` is
|
|
53
|
+
* checked against the rules its documentation states, so a plaintext URL cannot slip past
|
|
54
|
+
* the TLS default.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveServerUrl(options: Pick<FetchTransportOptions, 'serverUrl' | 'hostPort' | 'tls'>): string;
|
|
57
|
+
/**
|
|
58
|
+
* The transport for runtimes that only have `fetch` (Cloudflare Workers, Vercel Functions,
|
|
59
|
+
* Deno, Bun, browsers). It must not depend on the HTTP version, since the engine answers unary
|
|
60
|
+
* calls over whichever of HTTP/1.1 and HTTP/2 the runtime negotiates, and it enforces the Node
|
|
61
|
+
* transport's message size limits itself because connect-web has none.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createFetchTransport(options: FetchTransportOptions): Transport;
|
|
64
|
+
/**
|
|
65
|
+
* Wraps a `fetch` with the message size limits the Node transport enforces through Connect's
|
|
66
|
+
* `readMaxBytes` and `writeMaxBytes`, which the fetch transport has no option for. A request
|
|
67
|
+
* body over the send limit is refused before anything is sent, and a response body that grows
|
|
68
|
+
* past the receive limit is cancelled as it streams, counted after the runtime's HTTP
|
|
69
|
+
* decompression so a small compressed body cannot expand past it; the limit covers Connect
|
|
70
|
+
* error bodies as well as messages. Both surface as a `ConnectError` with
|
|
71
|
+
* `Code.ResourceExhausted`, as they do on the Node transport.
|
|
72
|
+
*/
|
|
73
|
+
export declare function limitMessageSizes(send: typeof globalThis.fetch, limits: MessageSizeLimits): typeof globalThis.fetch;
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.resolveServerUrl = resolveServerUrl;
|
|
13
|
+
exports.createFetchTransport = createFetchTransport;
|
|
14
|
+
exports.limitMessageSizes = limitMessageSizes;
|
|
15
|
+
const connect_1 = require("@connectrpc/connect");
|
|
16
|
+
const connect_web_1 = require("@connectrpc/connect-web");
|
|
17
|
+
const grpc_target_1 = require("./grpc-target");
|
|
18
|
+
const transport_1 = require("./transport");
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the engine's base URL from a `serverUrl` or from `hostPort` plus the TLS
|
|
21
|
+
* strategy, the two ways a fetch-based client can be pointed at an engine. `hostPort` takes
|
|
22
|
+
* the gRPC target forms the Node client takes (`host:port`, `dns:///host:port`, `ipv4:`,
|
|
23
|
+
* `ipv6:`), so a token or config shared with a Node client works unchanged. A `serverUrl` is
|
|
24
|
+
* checked against the rules its documentation states, so a plaintext URL cannot slip past
|
|
25
|
+
* the TLS default.
|
|
26
|
+
*/
|
|
27
|
+
function resolveServerUrl(options) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
const strategy = (_b = (_a = options.tls) === null || _a === void 0 ? void 0 : _a.strategy) !== null && _b !== void 0 ? _b : 'tls';
|
|
30
|
+
if (options.serverUrl) {
|
|
31
|
+
return validateServerUrl(options.serverUrl, strategy);
|
|
32
|
+
}
|
|
33
|
+
if (options.hostPort) {
|
|
34
|
+
const scheme = strategy === 'none' ? 'http' : 'https';
|
|
35
|
+
return (0, grpc_target_1.grpcTargetBaseUrl)((0, grpc_target_1.parseGrpcTarget)(options.hostPort), scheme);
|
|
36
|
+
}
|
|
37
|
+
throw new Error('a fetch transport needs a serverUrl or a hostPort');
|
|
38
|
+
}
|
|
39
|
+
/*
|
|
40
|
+
* The messages name the field and the rule only: a URL can carry a password, and the token
|
|
41
|
+
* is never part of it, so neither is echoed.
|
|
42
|
+
*/
|
|
43
|
+
function validateServerUrl(serverUrl, strategy) {
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(serverUrl);
|
|
47
|
+
}
|
|
48
|
+
catch (_a) {
|
|
49
|
+
throw new Error('serverUrl must be an absolute http:// or https:// URL');
|
|
50
|
+
}
|
|
51
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
52
|
+
throw new Error('serverUrl must be an absolute http:// or https:// URL');
|
|
53
|
+
}
|
|
54
|
+
if (url.username || url.password) {
|
|
55
|
+
throw new Error('serverUrl must not carry a username or password; the client authenticates with its token');
|
|
56
|
+
}
|
|
57
|
+
if (serverUrl.includes('?') || serverUrl.includes('#')) {
|
|
58
|
+
throw new Error('serverUrl must not carry a query string or fragment; the RPC path is appended to it');
|
|
59
|
+
}
|
|
60
|
+
if (url.protocol === 'http:' && strategy !== 'none') {
|
|
61
|
+
throw new Error("serverUrl is http:// but tls.strategy is 'tls' (the default), which would send the token in plaintext; set tls: { strategy: 'none' } to reach an engine over plain HTTP");
|
|
62
|
+
}
|
|
63
|
+
if (url.protocol === 'https:' && strategy === 'none') {
|
|
64
|
+
throw new Error("serverUrl is https:// but tls.strategy is 'none'; use tls: { strategy: 'tls' } or an http:// serverUrl");
|
|
65
|
+
}
|
|
66
|
+
return serverUrl.replace(/\/+$/, '');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The transport for runtimes that only have `fetch` (Cloudflare Workers, Vercel Functions,
|
|
70
|
+
* Deno, Bun, browsers). It must not depend on the HTTP version, since the engine answers unary
|
|
71
|
+
* calls over whichever of HTTP/1.1 and HTTP/2 the runtime negotiates, and it enforces the Node
|
|
72
|
+
* transport's message size limits itself because connect-web has none.
|
|
73
|
+
*/
|
|
74
|
+
function createFetchTransport(options) {
|
|
75
|
+
var _a, _b, _c;
|
|
76
|
+
const send = (_a = options.fetch) !== null && _a !== void 0 ? _a : ((input, init) => globalThis.fetch(input, init));
|
|
77
|
+
const limits = {
|
|
78
|
+
receive: (_b = options.maxReceiveMessageBytes) !== null && _b !== void 0 ? _b : transport_1.DEFAULT_MAX_MESSAGE_BYTES,
|
|
79
|
+
send: (_c = options.maxSendMessageBytes) !== null && _c !== void 0 ? _c : transport_1.DEFAULT_MAX_MESSAGE_BYTES,
|
|
80
|
+
};
|
|
81
|
+
return (0, connect_web_1.createConnectTransport)({
|
|
82
|
+
baseUrl: resolveServerUrl(options),
|
|
83
|
+
useBinaryFormat: true,
|
|
84
|
+
interceptors: [(0, transport_1.createAuthInterceptor)(options.token)],
|
|
85
|
+
fetch: limitMessageSizes(send, limits),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Wraps a `fetch` with the message size limits the Node transport enforces through Connect's
|
|
90
|
+
* `readMaxBytes` and `writeMaxBytes`, which the fetch transport has no option for. A request
|
|
91
|
+
* body over the send limit is refused before anything is sent, and a response body that grows
|
|
92
|
+
* past the receive limit is cancelled as it streams, counted after the runtime's HTTP
|
|
93
|
+
* decompression so a small compressed body cannot expand past it; the limit covers Connect
|
|
94
|
+
* error bodies as well as messages. Both surface as a `ConnectError` with
|
|
95
|
+
* `Code.ResourceExhausted`, as they do on the Node transport.
|
|
96
|
+
*/
|
|
97
|
+
function limitMessageSizes(send, limits) {
|
|
98
|
+
return (input, init) => __awaiter(this, void 0, void 0, function* () {
|
|
99
|
+
const size = bodySize(init === null || init === void 0 ? void 0 : init.body);
|
|
100
|
+
if (size > limits.send) {
|
|
101
|
+
throw new connect_1.ConnectError(`message size ${size} is larger than configured maxSendMessageBytes ${limits.send}`, connect_1.Code.ResourceExhausted);
|
|
102
|
+
}
|
|
103
|
+
const response = yield send(input, init);
|
|
104
|
+
if (!response.body) {
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
return new Response(limitBody(response.body, limits.receive), {
|
|
108
|
+
status: response.status,
|
|
109
|
+
statusText: response.statusText,
|
|
110
|
+
headers: response.headers,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** The size of the bodies Connect sends; a body of another kind is not measured. */
|
|
115
|
+
function bodySize(body) {
|
|
116
|
+
if (typeof body === 'string') {
|
|
117
|
+
return new TextEncoder().encode(body).byteLength;
|
|
118
|
+
}
|
|
119
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
120
|
+
return body.byteLength;
|
|
121
|
+
}
|
|
122
|
+
if (body instanceof Blob) {
|
|
123
|
+
return body.size;
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
function limitBody(body, max) {
|
|
128
|
+
const reader = body.getReader();
|
|
129
|
+
let received = 0;
|
|
130
|
+
return new ReadableStream({
|
|
131
|
+
pull(controller) {
|
|
132
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
const { done, value } = yield reader.read();
|
|
134
|
+
if (done) {
|
|
135
|
+
controller.close();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
received += value.byteLength;
|
|
139
|
+
if (received > max) {
|
|
140
|
+
const error = new connect_1.ConnectError(`response body is larger than configured maxReceiveMessageBytes ${max}`, connect_1.Code.ResourceExhausted);
|
|
141
|
+
yield reader.cancel(error).catch(() => undefined);
|
|
142
|
+
controller.error(error);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
controller.enqueue(value);
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
cancel(reason) {
|
|
149
|
+
return reader.cancel(reason);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -2,7 +2,52 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseGrpcTarget = parseGrpcTarget;
|
|
4
4
|
exports.grpcTargetBaseUrl = grpcTargetBaseUrl;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* IP literal checks with the same outcome as Node's `net.isIPv4` and `net.isIPv6` for the
|
|
7
|
+
* addresses a gRPC target can name. They are written here rather than imported from `net`
|
|
8
|
+
* because the fetch transport parses targets too, and it runs where `net` does not exist.
|
|
9
|
+
*/
|
|
10
|
+
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
11
|
+
const HEX_GROUP = /^[0-9a-fA-F]{1,4}$/;
|
|
12
|
+
/** What Node accepts after the one `%` of a zone id: nonempty, letters, digits, `-`, `.` and `:`. */
|
|
13
|
+
const ZONE_ID = /^[0-9a-zA-Z\-.:]+$/;
|
|
14
|
+
function isIPv4(address) {
|
|
15
|
+
return IPV4.test(address);
|
|
16
|
+
}
|
|
17
|
+
function isIPv6(address) {
|
|
18
|
+
const zoneAt = address.indexOf('%');
|
|
19
|
+
if (zoneAt !== -1 && !ZONE_ID.test(address.slice(zoneAt + 1))) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const literal = zoneAt === -1 ? address : address.slice(0, zoneAt);
|
|
23
|
+
const halves = literal.split('::');
|
|
24
|
+
if (halves.length > 2) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
// An embedded IPv4 address counts as two groups and may only end the whole literal: the
|
|
28
|
+
// last item of the part after `::`, or of the only part when there is no `::`.
|
|
29
|
+
const groups = (part, endsLiteral) => {
|
|
30
|
+
if (part === '') {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const items = part.split(':');
|
|
34
|
+
const last = items[items.length - 1];
|
|
35
|
+
if (endsLiteral && last.includes('.')) {
|
|
36
|
+
if (!isIPv4(last)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
items.splice(items.length - 1, 1, '0', '0');
|
|
40
|
+
}
|
|
41
|
+
return items.every((item) => HEX_GROUP.test(item)) ? items : null;
|
|
42
|
+
};
|
|
43
|
+
const head = groups(halves[0], halves.length === 1);
|
|
44
|
+
const tail = halves.length === 2 ? groups(halves[1], true) : [];
|
|
45
|
+
if (!head || !tail) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const count = head.length + tail.length;
|
|
49
|
+
return halves.length === 2 ? count < 8 : count === 8;
|
|
50
|
+
}
|
|
6
51
|
/** The port grpc-js dials when a target names none. */
|
|
7
52
|
const DEFAULT_PORT = 443;
|
|
8
53
|
/*
|
|
@@ -40,10 +85,10 @@ function parseGrpcTarget(target) {
|
|
|
40
85
|
if (!hostPort) {
|
|
41
86
|
throw new Error(`host_port ${JSON.stringify(target)} is not a valid host and port`);
|
|
42
87
|
}
|
|
43
|
-
if (scheme === 'ipv4' && !
|
|
88
|
+
if (scheme === 'ipv4' && !isIPv4(hostPort.host)) {
|
|
44
89
|
throw new Error(`host_port ${JSON.stringify(target)} is not an IPv4 address`);
|
|
45
90
|
}
|
|
46
|
-
if (scheme === 'ipv6' && !
|
|
91
|
+
if (scheme === 'ipv6' && !isIPv6(hostPort.host)) {
|
|
47
92
|
throw new Error(`host_port ${JSON.stringify(target)} is not an IPv6 address`);
|
|
48
93
|
}
|
|
49
94
|
return { host: hostPort.host, port: (_a = hostPort.port) !== null && _a !== void 0 ? _a : DEFAULT_PORT, authority: address };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { createAuthInterceptor, type Transport } from './transport';
|
|
1
|
+
export { createAuthInterceptor, DEFAULT_MAX_MESSAGE_BYTES, type Transport } from './transport';
|
|
2
2
|
export { createNodeTransport } from './node-transport';
|
|
3
|
+
export { createFetchTransport, limitMessageSizes, resolveServerUrl, type FetchTlsConfig, type FetchTransportOptions, type MessageSizeLimits, } from './fetch-transport';
|
|
3
4
|
export { fromProtobufEs, toProtobufEs, type DeepPartial, type TsProtoCodec, } from './message-bridge';
|
|
4
5
|
export { createTsProtoClient, headersToMetadata, metadataToHeaders, toConnectCallOptions, type UnaryCallOptions, type UnaryCallOptionsExt, } from './ts-proto-client';
|