effect-inngest 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 +21 -0
- package/README.md +457 -0
- package/dist/Client.d.ts +167 -0
- package/dist/Client.js +144 -0
- package/dist/Events.d.ts +110 -0
- package/dist/Events.js +93 -0
- package/dist/Function.d.ts +384 -0
- package/dist/Function.js +104 -0
- package/dist/Group.d.ts +152 -0
- package/dist/Group.js +164 -0
- package/dist/HttpApi.d.ts +75 -0
- package/dist/HttpApi.js +47 -0
- package/dist/_virtual/rolldown_runtime.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/internal/constants.js +15 -0
- package/dist/internal/driver.d.ts +5 -0
- package/dist/internal/driver.js +117 -0
- package/dist/internal/errors.d.ts +56 -0
- package/dist/internal/errors.js +61 -0
- package/dist/internal/handler.d.ts +20 -0
- package/dist/internal/handler.js +145 -0
- package/dist/internal/helpers.js +44 -0
- package/dist/internal/interrupts.d.ts +2 -0
- package/dist/internal/interrupts.js +45 -0
- package/dist/internal/memo.js +56 -0
- package/dist/internal/protocol.d.ts +1 -0
- package/dist/internal/protocol.js +191 -0
- package/dist/internal/signature.d.ts +18 -0
- package/dist/internal/signature.js +97 -0
- package/dist/internal/step.d.ts +59 -0
- package/dist/internal/step.js +183 -0
- package/package.json +121 -0
- package/src/Client.ts +279 -0
- package/src/Events.ts +87 -0
- package/src/Function.ts +493 -0
- package/src/Group.ts +314 -0
- package/src/HttpApi.ts +82 -0
- package/src/index.ts +171 -0
- package/src/internal/constants.ts +11 -0
- package/src/internal/driver.ts +194 -0
- package/src/internal/errors.ts +130 -0
- package/src/internal/handler.ts +222 -0
- package/src/internal/helpers.ts +58 -0
- package/src/internal/interrupts.ts +62 -0
- package/src/internal/memo.ts +73 -0
- package/src/internal/protocol.ts +218 -0
- package/src/internal/signature.ts +158 -0
- package/src/internal/step.ts +377 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver execution logic.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
import * as Headers from "@effect/platform/Headers";
|
|
6
|
+
import * as HttpTraceContext from "@effect/platform/HttpTraceContext";
|
|
7
|
+
import { Array as Arr, Chunk, Context, Duration, Effect, HashMap, Option, pipe, Predicate, Ref, Schema } from "effect";
|
|
8
|
+
import type { InngestFunction } from "../Function.js";
|
|
9
|
+
import { InngestClient } from "../Client.js";
|
|
10
|
+
import { isRetryAfterError, isNonRetriableError, isStepError, type RetryAfterError, type StepError } from "./errors.js";
|
|
11
|
+
import * as Protocol from "./protocol.js";
|
|
12
|
+
import { StepInterrupt } from "./interrupts.js";
|
|
13
|
+
import { createStepTools, buildHandlerContext, Step, type HandlerContext } from "./step.js";
|
|
14
|
+
import { OtelAttributes } from "./constants.js";
|
|
15
|
+
|
|
16
|
+
/** Trace context headers extracted from incoming request */
|
|
17
|
+
export interface TraceHeaders {
|
|
18
|
+
readonly traceparent?: string;
|
|
19
|
+
readonly tracestate?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SDK_VERSION = "2.0.0";
|
|
23
|
+
|
|
24
|
+
export class ExecutionResult extends Schema.Class<ExecutionResult>("ExecutionResult")({
|
|
25
|
+
status: Schema.Literal(200, 206, 500),
|
|
26
|
+
body: Schema.Unknown,
|
|
27
|
+
headers: Schema.Record({ key: Schema.String, value: Schema.String }),
|
|
28
|
+
}) {}
|
|
29
|
+
|
|
30
|
+
const isStepInterrupt = Schema.is(StepInterrupt);
|
|
31
|
+
|
|
32
|
+
const isSomeWithValue = (v: unknown): v is { _tag: "Some"; value: unknown } =>
|
|
33
|
+
Predicate.isRecord(v) && Predicate.hasProperty(v, "_tag") && v._tag === "Some" && Predicate.hasProperty(v, "value");
|
|
34
|
+
|
|
35
|
+
const extractStepInterrupt = (value: unknown): Option.Option<StepInterrupt> =>
|
|
36
|
+
pipe(
|
|
37
|
+
Option.liftPredicate(isStepInterrupt)(value),
|
|
38
|
+
Option.orElse(() =>
|
|
39
|
+
pipe(
|
|
40
|
+
Option.liftPredicate(isSomeWithValue)(value),
|
|
41
|
+
Option.flatMap((v) => Option.liftPredicate(isStepInterrupt)(v.value)),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const collectStepInterruptsFromError = (error: unknown): Chunk.Chunk<StepInterrupt> =>
|
|
47
|
+
pipe(
|
|
48
|
+
extractStepInterrupt(error),
|
|
49
|
+
Option.match({
|
|
50
|
+
onSome: Chunk.of,
|
|
51
|
+
onNone: () =>
|
|
52
|
+
Array.isArray(error) ? Chunk.fromIterable(Arr.filterMap(error, extractStepInterrupt)) : Chunk.empty(),
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const toUserError = (error: unknown): typeof Protocol.UserError.Type =>
|
|
57
|
+
Protocol.UserError.make({
|
|
58
|
+
name: Predicate.hasProperty(error, "name") ? String(error.name) : "Error",
|
|
59
|
+
message: Predicate.hasProperty(error, "message") ? String(error.message) : String(error),
|
|
60
|
+
stack: Predicate.hasProperty(error, "stack") ? String(error.stack) : undefined,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const baseHeaders = (): Record<string, string> => ({
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
[Protocol.Headers.SDK]: `effect-inngest:v${SDK_VERSION}`,
|
|
66
|
+
[Protocol.Headers.RequestVersion]: "1",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const execute = <F extends InngestFunction.Any, R>(
|
|
70
|
+
fn: F,
|
|
71
|
+
handler: (ctx: HandlerContext<F>) => Effect.Effect<InngestFunction.Success<F>, unknown, R>,
|
|
72
|
+
request: Protocol.SDKRequestBody,
|
|
73
|
+
appName: string,
|
|
74
|
+
traceHeaders: TraceHeaders = {},
|
|
75
|
+
): Effect.Effect<ExecutionResult, never, R | InngestClient> =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const stepIdCounts = yield* Ref.make(HashMap.empty<string, number>());
|
|
78
|
+
const step = createStepTools(request, appName, stepIdCounts);
|
|
79
|
+
const context = buildHandlerContext<F>(fn, step, request);
|
|
80
|
+
const headers = baseHeaders();
|
|
81
|
+
|
|
82
|
+
const result = yield* handler(context).pipe(
|
|
83
|
+
Effect.provideService(Step, step),
|
|
84
|
+
Effect.map((value) => ExecutionResult.make({ status: 200, body: value, headers })),
|
|
85
|
+
Effect.catchAll((error) => {
|
|
86
|
+
const interrupts = collectStepInterruptsFromError(error);
|
|
87
|
+
|
|
88
|
+
if (!Chunk.isEmpty(interrupts)) {
|
|
89
|
+
const interruptArray = Chunk.toReadonlyArray(interrupts);
|
|
90
|
+
const opcodes = interruptArray.map((interrupt) => interrupt.opcode);
|
|
91
|
+
|
|
92
|
+
const hasNonRetriableError = opcodes.some(
|
|
93
|
+
(op) =>
|
|
94
|
+
op.op === Protocol.Opcode.StepError &&
|
|
95
|
+
Predicate.isRecord(op.error) &&
|
|
96
|
+
Predicate.hasProperty(op.error, "noRetry") &&
|
|
97
|
+
op.error.noRetry === true,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const retryAfterMs = interruptArray.find((i) => i.retryAfterMs !== undefined)?.retryAfterMs;
|
|
101
|
+
const responseHeaders: Record<string, string> = hasNonRetriableError
|
|
102
|
+
? { ...headers, [Protocol.Headers.NoRetry]: "true" }
|
|
103
|
+
: headers;
|
|
104
|
+
if (retryAfterMs !== undefined) {
|
|
105
|
+
responseHeaders[Protocol.Headers.RetryAfter] = String(Math.ceil(retryAfterMs / 1000));
|
|
106
|
+
}
|
|
107
|
+
const encodedOpcodes = Schema.encodeSync(Schema.Array(Protocol.GeneratorOpcode))(opcodes);
|
|
108
|
+
return Effect.succeed(ExecutionResult.make({ status: 206, body: encodedOpcodes, headers: responseHeaders }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for RetryAfterError - use type guard and direct access
|
|
112
|
+
if (isRetryAfterError(error)) {
|
|
113
|
+
const retryAfterMs = Duration.toMillis((error as RetryAfterError).retryAfter);
|
|
114
|
+
const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
|
|
115
|
+
return Effect.succeed(
|
|
116
|
+
ExecutionResult.make({
|
|
117
|
+
status: 500,
|
|
118
|
+
body: { error: toUserError(error) },
|
|
119
|
+
headers: {
|
|
120
|
+
...headers,
|
|
121
|
+
[Protocol.Headers.NoRetry]: "false",
|
|
122
|
+
[Protocol.Headers.RetryAfter]: String(retryAfterSeconds),
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check for NonRetriableError or StepError with noRetry
|
|
129
|
+
const noRetryValue =
|
|
130
|
+
isNonRetriableError(error) || (isStepError(error) && (error as StepError).noRetry === true);
|
|
131
|
+
const noRetry = noRetryValue ? "true" : "false";
|
|
132
|
+
return Effect.succeed(
|
|
133
|
+
ExecutionResult.make({
|
|
134
|
+
status: 500,
|
|
135
|
+
body: { error: toUserError(error) },
|
|
136
|
+
headers: { ...headers, [Protocol.Headers.NoRetry]: noRetry },
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
Effect.catchAllDefect((defect) =>
|
|
142
|
+
Effect.succeed(
|
|
143
|
+
ExecutionResult.make({
|
|
144
|
+
status: 500,
|
|
145
|
+
body: { error: toUserError(defect) },
|
|
146
|
+
headers: { ...headers, [Protocol.Headers.NoRetry]: "false" },
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}).pipe(
|
|
154
|
+
(base) => {
|
|
155
|
+
const headers: Record<string, string> = {};
|
|
156
|
+
if (traceHeaders.traceparent) headers["traceparent"] = traceHeaders.traceparent;
|
|
157
|
+
if (traceHeaders.tracestate) headers["tracestate"] = traceHeaders.tracestate;
|
|
158
|
+
return pipe(
|
|
159
|
+
HttpTraceContext.fromHeaders(Headers.fromInput(headers)),
|
|
160
|
+
Option.match({
|
|
161
|
+
onNone: () => base,
|
|
162
|
+
onSome: (externalSpan) => Effect.withParentSpan(base, externalSpan),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
Effect.withSpan(`inngest.function/${fn._tag}`, {
|
|
167
|
+
kind: "server",
|
|
168
|
+
attributes: {
|
|
169
|
+
[OtelAttributes.RunId]: request.ctx.run_id,
|
|
170
|
+
[OtelAttributes.FunctionId]: fn._tag,
|
|
171
|
+
[OtelAttributes.AppId]: appName,
|
|
172
|
+
[OtelAttributes.Attempt]: request.ctx.attempt,
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
export interface DriverService {
|
|
178
|
+
readonly execute: <F extends InngestFunction.Any, R>(
|
|
179
|
+
fn: F,
|
|
180
|
+
handler: (ctx: HandlerContext<F>) => Effect.Effect<InngestFunction.Success<F>, unknown, R>,
|
|
181
|
+
request: Protocol.SDKRequestBody,
|
|
182
|
+
) => Effect.Effect<ExecutionResult, never, R | InngestClient>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class Driver extends Context.Tag("effect-inngest/Driver")<Driver, DriverService>() {}
|
|
186
|
+
|
|
187
|
+
export const layer = (options: { readonly appName: string }) =>
|
|
188
|
+
Effect.succeed({
|
|
189
|
+
execute: <F extends InngestFunction.Any, R>(
|
|
190
|
+
fn: F,
|
|
191
|
+
handler: (ctx: HandlerContext<F>) => Effect.Effect<InngestFunction.Success<F>, unknown, R>,
|
|
192
|
+
request: Protocol.SDKRequestBody,
|
|
193
|
+
) => execute(fn, handler, request, options.appName),
|
|
194
|
+
}).pipe(Effect.map((service) => Context.make(Driver, service)));
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal error types for the Effect Inngest SDK.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export class SignatureError extends Schema.TaggedError<SignatureError>()("SignatureError", {
|
|
11
|
+
message: Schema.String,
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export class RegistrationError extends Schema.TaggedError<RegistrationError>()("RegistrationError", {
|
|
18
|
+
message: Schema.String,
|
|
19
|
+
functions: Schema.Array(Schema.String),
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export class FunctionNotFoundError extends Schema.TaggedError<FunctionNotFoundError>()("FunctionNotFoundError", {
|
|
26
|
+
message: Schema.String,
|
|
27
|
+
functionId: Schema.String,
|
|
28
|
+
}) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export class SendEventError extends Schema.TaggedError<SendEventError>()("SendEventError", {
|
|
34
|
+
message: Schema.String,
|
|
35
|
+
events: Schema.Array(Schema.String),
|
|
36
|
+
}) {}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export class UseApiFetchError extends Schema.TaggedError<UseApiFetchError>()("UseApiFetchError", {
|
|
42
|
+
message: Schema.String,
|
|
43
|
+
endpoint: Schema.Literal("batch", "actions"),
|
|
44
|
+
runId: Schema.String,
|
|
45
|
+
statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }),
|
|
46
|
+
}) {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export class StepError extends Schema.TaggedError<StepError>()("StepError", {
|
|
52
|
+
message: Schema.String,
|
|
53
|
+
stepId: Schema.String,
|
|
54
|
+
cause: Schema.optional(Schema.Unknown),
|
|
55
|
+
noRetry: Schema.optional(Schema.Boolean),
|
|
56
|
+
}) {}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const isStepError = Schema.is(StepError);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export class TimeoutError extends Schema.TaggedError<TimeoutError>()("TimeoutError", {
|
|
67
|
+
message: Schema.String,
|
|
68
|
+
stepId: Schema.optional(Schema.String),
|
|
69
|
+
timeout: Schema.DurationFromMillis,
|
|
70
|
+
}) {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Thrown to indicate that the error should not be retried.
|
|
74
|
+
* Use this when you know retrying won't help (e.g., validation errors, auth failures).
|
|
75
|
+
*
|
|
76
|
+
* @since 0.1.0
|
|
77
|
+
* @category errors
|
|
78
|
+
*/
|
|
79
|
+
export class NonRetriableError extends Schema.TaggedError<NonRetriableError>()("NonRetriableError", {
|
|
80
|
+
message: Schema.String,
|
|
81
|
+
cause: Schema.optional(Schema.Unknown),
|
|
82
|
+
}) {}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export const isNonRetriableError = Schema.is(NonRetriableError);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Thrown to indicate that the operation should be retried after a specific delay.
|
|
91
|
+
* Use this for rate limiting or when you know when a resource will become available.
|
|
92
|
+
*
|
|
93
|
+
* @since 0.1.0
|
|
94
|
+
* @category errors
|
|
95
|
+
*/
|
|
96
|
+
export class RetryAfterError extends Schema.TaggedError<RetryAfterError>()("RetryAfterError", {
|
|
97
|
+
message: Schema.String,
|
|
98
|
+
retryAfter: Schema.DurationFromMillis,
|
|
99
|
+
cause: Schema.optional(Schema.Unknown),
|
|
100
|
+
}) {}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
export const isRetryAfterError = Schema.is(RetryAfterError);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
export type ServerError = SignatureError | RegistrationError | FunctionNotFoundError;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
export type ClientError = SendEventError | UseApiFetchError;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
export type ExecutionError = StepError | TimeoutError;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
export type RetryControlError = NonRetriableError | RetryAfterError;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
export type InngestError = ServerError | ClientError | ExecutionError | RetryControlError;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal handler implementation.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
import * as HttpClient from "@effect/platform/HttpClient";
|
|
6
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
|
|
7
|
+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
|
|
8
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest";
|
|
9
|
+
import * as Context from "effect/Context";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Predicate from "effect/Predicate";
|
|
12
|
+
import * as Schema from "effect/Schema";
|
|
13
|
+
import { InngestClient } from "../Client.js";
|
|
14
|
+
import type { InngestFunction } from "../Function.js";
|
|
15
|
+
import type { InngestGroup } from "../Group.js";
|
|
16
|
+
import { Signature, SignatureError } from "./signature.js";
|
|
17
|
+
import * as Protocol from "./protocol.js";
|
|
18
|
+
export { SignatureError } from "./signature.js";
|
|
19
|
+
import { execute, type TraceHeaders } from "./driver.js";
|
|
20
|
+
export type { TraceHeaders } from "./driver.js";
|
|
21
|
+
|
|
22
|
+
export class InvalidRequestError extends Schema.TaggedError<InvalidRequestError>()("InvalidRequestError", {
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
const SDK_VERSION = "2.0.0";
|
|
27
|
+
|
|
28
|
+
const baseHeaders = (): Record<string, string> => ({
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
[Protocol.Headers.SDK]: `effect-inngest:v${SDK_VERSION}`,
|
|
31
|
+
[Protocol.Headers.RequestVersion]: "1",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const buildServeUrl = (requestUrl: string, serveHost?: string, servePath?: string): URL => {
|
|
35
|
+
const url = new URL(requestUrl);
|
|
36
|
+
if (servePath) {
|
|
37
|
+
url.pathname = servePath;
|
|
38
|
+
}
|
|
39
|
+
if (serveHost) {
|
|
40
|
+
return new URL(url.pathname + url.search, serveHost);
|
|
41
|
+
}
|
|
42
|
+
return url;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface HandlerResponse<T> {
|
|
46
|
+
readonly status: number;
|
|
47
|
+
readonly headers: Record<string, string>;
|
|
48
|
+
readonly body: T;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const verifyAndParseRequestBody = (
|
|
52
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
53
|
+
): Effect.Effect<
|
|
54
|
+
typeof Protocol.SDKRequestBody.Type,
|
|
55
|
+
SignatureError | InvalidRequestError,
|
|
56
|
+
InngestClient | Signature
|
|
57
|
+
> =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const client = yield* InngestClient;
|
|
60
|
+
const sig = yield* Signature;
|
|
61
|
+
const config = client.config;
|
|
62
|
+
const isDev = client.mode === "dev";
|
|
63
|
+
|
|
64
|
+
const bodyText = yield* request.text.pipe(
|
|
65
|
+
Effect.mapError((error) => new InvalidRequestError({ message: `Failed to read request body: ${String(error)}` })),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
yield* sig.verify({
|
|
69
|
+
body: new TextEncoder().encode(bodyText),
|
|
70
|
+
signatureHeader: request.headers[Protocol.Headers.Signature.toLowerCase()],
|
|
71
|
+
signingKey: config.signingKey,
|
|
72
|
+
signingKeyFallback: config.signingKeyFallback,
|
|
73
|
+
isDev,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return yield* Schema.decodeUnknown(Schema.parseJson(Protocol.SDKRequestBody))(bodyText).pipe(
|
|
77
|
+
Effect.mapError((error) => new InvalidRequestError({ message: `Invalid request body: ${String(error)}` })),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const handleIntrospection = (
|
|
82
|
+
group: InngestGroup.Any,
|
|
83
|
+
_requestUrl: string,
|
|
84
|
+
): Effect.Effect<HandlerResponse<typeof Protocol.IntrospectionResponse.Type>, never, InngestClient> =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
const client = yield* InngestClient;
|
|
87
|
+
const config = client.config;
|
|
88
|
+
|
|
89
|
+
const body: typeof Protocol.IntrospectionResponse.Type = {
|
|
90
|
+
function_count: group.functions.size,
|
|
91
|
+
has_event_key: config.eventKey !== undefined,
|
|
92
|
+
has_signing_key: config.signingKey !== undefined,
|
|
93
|
+
has_signing_key_fallback: config.signingKeyFallback !== undefined,
|
|
94
|
+
mode: client.mode === "dev" ? "dev" : "cloud",
|
|
95
|
+
schema_version: "2024-05-24",
|
|
96
|
+
authentication_succeeded: null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { status: 200, headers: baseHeaders(), body };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const RegisterRequest = Schema.Struct({
|
|
103
|
+
url: Schema.String,
|
|
104
|
+
v: Schema.String,
|
|
105
|
+
deployType: Schema.Literal("ping"),
|
|
106
|
+
sdk: Schema.String,
|
|
107
|
+
appName: Schema.String,
|
|
108
|
+
framework: Schema.String,
|
|
109
|
+
functions: Schema.Array(Schema.Unknown),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const handleRegistration = (
|
|
113
|
+
group: InngestGroup.Any,
|
|
114
|
+
requestUrl: string,
|
|
115
|
+
): Effect.Effect<
|
|
116
|
+
HandlerResponse<typeof Protocol.RegisterResponse.Type>,
|
|
117
|
+
never,
|
|
118
|
+
InngestClient | HttpClient.HttpClient
|
|
119
|
+
> =>
|
|
120
|
+
Effect.gen(function* () {
|
|
121
|
+
const client = yield* InngestClient;
|
|
122
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
123
|
+
const config = client.config;
|
|
124
|
+
const url = buildServeUrl(requestUrl, config.serveHost, config.servePath);
|
|
125
|
+
|
|
126
|
+
const functions = Array.from(group.functions.values()).map((fn) =>
|
|
127
|
+
fn.toRegistration({ appId: config.id, url: url.href }),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const registerUrl = new URL("fn/register", client.apiBaseUrl).toString();
|
|
131
|
+
const request = HttpClientRequest.post(registerUrl).pipe(
|
|
132
|
+
HttpClientRequest.setHeaders({
|
|
133
|
+
...baseHeaders(),
|
|
134
|
+
Authorization: `Bearer ${config.signingKey ?? ""}`,
|
|
135
|
+
}),
|
|
136
|
+
HttpClientRequest.schemaBodyJson(RegisterRequest)({
|
|
137
|
+
url: url.href,
|
|
138
|
+
v: "0.1",
|
|
139
|
+
deployType: "ping" as const,
|
|
140
|
+
sdk: `effect-inngest:v${SDK_VERSION}`,
|
|
141
|
+
appName: config.id,
|
|
142
|
+
framework: "effect",
|
|
143
|
+
functions,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const response = yield* request.pipe(
|
|
148
|
+
Effect.flatMap(httpClient.execute),
|
|
149
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(Protocol.RegisterResponse)),
|
|
150
|
+
Effect.scoped,
|
|
151
|
+
Effect.catchAll((error) =>
|
|
152
|
+
Effect.succeed({
|
|
153
|
+
message: `Registration failed: ${Predicate.hasProperty(error, "message") ? (error.message as string) : "Unknown error"}`,
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
status: 200,
|
|
160
|
+
headers: baseHeaders(),
|
|
161
|
+
body: response,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export interface Handler<Tag extends string> {
|
|
166
|
+
readonly tag: Tag;
|
|
167
|
+
readonly handler: (ctx: any) => Effect.Effect<any, any, any>;
|
|
168
|
+
readonly context: Context.Context<any>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const handleExecution = (
|
|
172
|
+
group: InngestGroup.Any,
|
|
173
|
+
fnId: string,
|
|
174
|
+
urlStepId: string | undefined,
|
|
175
|
+
body: typeof Protocol.SDKRequestBody.Type,
|
|
176
|
+
traceHeaders: TraceHeaders = {},
|
|
177
|
+
): Effect.Effect<HandlerResponse<unknown>, never, InngestClient> =>
|
|
178
|
+
Effect.gen(function* () {
|
|
179
|
+
const client = yield* InngestClient;
|
|
180
|
+
const context = yield* Effect.context<never>();
|
|
181
|
+
|
|
182
|
+
const appId = client.config.id;
|
|
183
|
+
const prefix = `${appId}-`;
|
|
184
|
+
const fnTag = fnId.startsWith(prefix) ? fnId.slice(prefix.length) : fnId;
|
|
185
|
+
|
|
186
|
+
const fn = group.functions.get(fnTag) as InngestFunction.Any | undefined;
|
|
187
|
+
const entry = fn ? (context.unsafeMap.get(fn.key) as Handler<string> | undefined) : undefined;
|
|
188
|
+
|
|
189
|
+
if (!fn || !entry) {
|
|
190
|
+
return {
|
|
191
|
+
status: 404 as const,
|
|
192
|
+
headers: { ...baseHeaders(), [Protocol.Headers.NoRetry]: "true" },
|
|
193
|
+
body: {
|
|
194
|
+
error: Protocol.UserError.make({ name: "FunctionNotFoundError", message: `Unknown function: ${fnId}` }),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// URL stepId takes precedence over body.ctx.step_id
|
|
200
|
+
// This is how Inngest requests execution of a specific step
|
|
201
|
+
const effectiveBody =
|
|
202
|
+
urlStepId && urlStepId !== body.ctx.step_id
|
|
203
|
+
? Protocol.SDKRequestBody.make({
|
|
204
|
+
event: body.event,
|
|
205
|
+
events: body.events,
|
|
206
|
+
steps: body.steps,
|
|
207
|
+
ctx: Protocol.SDKRequestContext.make({
|
|
208
|
+
fn_id: body.ctx.fn_id,
|
|
209
|
+
run_id: body.ctx.run_id,
|
|
210
|
+
step_id: urlStepId,
|
|
211
|
+
attempt: body.ctx.attempt,
|
|
212
|
+
disable_immediate_execution: body.ctx.disable_immediate_execution,
|
|
213
|
+
use_api: body.ctx.use_api,
|
|
214
|
+
stack: body.ctx.stack,
|
|
215
|
+
}),
|
|
216
|
+
use_api: body.use_api,
|
|
217
|
+
})
|
|
218
|
+
: body;
|
|
219
|
+
|
|
220
|
+
const result = yield* Effect.provide(execute(fn, entry.handler, effectiveBody, appId, traceHeaders), entry.context);
|
|
221
|
+
return { status: result.status, headers: result.headers, body: result.body };
|
|
222
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper utilities.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
import * as Duration from "effect/Duration";
|
|
6
|
+
|
|
7
|
+
const millisecond = 1;
|
|
8
|
+
const second = millisecond * 1000;
|
|
9
|
+
const minute = second * 60;
|
|
10
|
+
const hour = minute * 60;
|
|
11
|
+
const day = hour * 24;
|
|
12
|
+
const week = day * 7;
|
|
13
|
+
|
|
14
|
+
const periods = [
|
|
15
|
+
["w", week],
|
|
16
|
+
["d", day],
|
|
17
|
+
["h", hour],
|
|
18
|
+
["m", minute],
|
|
19
|
+
["s", second],
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert a Duration to an Inngest-compatible time string (e.g. `"1d"` or `"2h30m"`).
|
|
24
|
+
*
|
|
25
|
+
* Supports weeks, days, hours, minutes, and seconds. Years/months are converted
|
|
26
|
+
* to their equivalent in weeks/days.
|
|
27
|
+
*/
|
|
28
|
+
export const timeStr = (input: Duration.DurationInput): string => {
|
|
29
|
+
let ms = Duration.toMillis(Duration.decode(input));
|
|
30
|
+
|
|
31
|
+
const [, result] = periods.reduce<[number, string]>(
|
|
32
|
+
([num, str], [suffix, period]) => {
|
|
33
|
+
const numPeriods = Math.floor(num / period);
|
|
34
|
+
|
|
35
|
+
if (numPeriods > 0) {
|
|
36
|
+
return [num % period, `${str}${numPeriods}${suffix}`];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [num, str];
|
|
40
|
+
},
|
|
41
|
+
[ms, ""],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return result || "0s";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format a timestamp as an ISO string for Inngest's sleepUntil.
|
|
49
|
+
*/
|
|
50
|
+
export const formatTimestamp = (timestamp: Date | number | string): string => {
|
|
51
|
+
if (timestamp instanceof Date) {
|
|
52
|
+
return timestamp.toISOString();
|
|
53
|
+
}
|
|
54
|
+
if (typeof timestamp === "number") {
|
|
55
|
+
return new Date(timestamp).toISOString();
|
|
56
|
+
}
|
|
57
|
+
return timestamp;
|
|
58
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StepInterrupt schema and factory functions.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
import * as Predicate from "effect/Predicate";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as Protocol from "./protocol.js";
|
|
8
|
+
|
|
9
|
+
/** @internal */
|
|
10
|
+
export class StepInterrupt extends Schema.TaggedClass<StepInterrupt>()("StepInterrupt", {
|
|
11
|
+
opcode: Protocol.GeneratorOpcode,
|
|
12
|
+
retryAfterMs: Schema.optional(Schema.Number),
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
export interface StepInfo {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly hash: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toUserError = (error: unknown): typeof Protocol.UserError.Type =>
|
|
23
|
+
Protocol.UserError.make({
|
|
24
|
+
name: Predicate.hasProperty(error, "name") ? String(error.name) : "Error",
|
|
25
|
+
message: Predicate.hasProperty(error, "message") ? String(error.message) : String(error),
|
|
26
|
+
stack: Predicate.hasProperty(error, "stack") ? String(error.stack) : undefined,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** @internal */
|
|
30
|
+
export const sleepInterrupt = (opts: { info: StepInfo; duration: string }) =>
|
|
31
|
+
StepInterrupt.make({ opcode: Protocol.sleep(opts.info, opts.duration) });
|
|
32
|
+
|
|
33
|
+
/** @internal */
|
|
34
|
+
export const waitForEventInterrupt = (opts: { info: StepInfo; event: string; timeout: string; if?: string }) =>
|
|
35
|
+
StepInterrupt.make({
|
|
36
|
+
opcode: Protocol.waitForEvent(opts.info, { event: opts.event, timeout: opts.timeout, if: opts.if }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** @internal */
|
|
40
|
+
export const invokeInterrupt = (opts: { info: StepInfo; functionId: string; payload: unknown; timeout: string }) =>
|
|
41
|
+
StepInterrupt.make({
|
|
42
|
+
opcode: Protocol.invokeFunction(opts.info, {
|
|
43
|
+
function_id: opts.functionId,
|
|
44
|
+
payload: opts.payload,
|
|
45
|
+
timeout: opts.timeout,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export const plannedInterrupt = (opts: { info: StepInfo }) =>
|
|
51
|
+
StepInterrupt.make({ opcode: Protocol.stepPlanned(opts.info) });
|
|
52
|
+
|
|
53
|
+
/** @internal */
|
|
54
|
+
export const runInterrupt = (opts: { info: StepInfo; data: unknown }) =>
|
|
55
|
+
StepInterrupt.make({ opcode: Protocol.stepRun(opts.info, opts.data) });
|
|
56
|
+
|
|
57
|
+
/** @internal */
|
|
58
|
+
export const errorInterrupt = (opts: { info: StepInfo; error: unknown; noRetry?: boolean; retryAfterMs?: number }) =>
|
|
59
|
+
StepInterrupt.make({
|
|
60
|
+
opcode: Protocol.stepError(opts.info, toUserError(opts.error), opts.noRetry),
|
|
61
|
+
retryAfterMs: opts.retryAfterMs,
|
|
62
|
+
});
|