effect-inngest 0.1.3 → 0.2.0-beta.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/dist/Client.d.ts +55 -24
- package/dist/Client.js +84 -25
- package/dist/Events.d.ts +36 -61
- package/dist/Events.js +5 -13
- package/dist/Function.d.ts +35 -15
- package/dist/Function.js +14 -8
- package/dist/Group.d.ts +5 -4
- package/dist/Group.js +26 -24
- package/dist/HttpApi.d.ts +49 -54
- package/dist/HttpApi.js +32 -17
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.js +1 -2
- package/dist/internal/checkpoint.d.ts +32 -0
- package/dist/internal/checkpoint.js +112 -0
- package/dist/internal/constants.js +1 -2
- package/dist/internal/driver.d.ts +1 -5
- package/dist/internal/driver.js +164 -52
- package/dist/internal/errors.d.ts +20 -27
- package/dist/internal/errors.js +29 -15
- package/dist/internal/handler.d.ts +4 -13
- package/dist/internal/handler.js +70 -43
- package/dist/internal/helpers.js +12 -9
- package/dist/internal/interrupts.d.ts +1 -2
- package/dist/internal/interrupts.js +1 -3
- package/dist/internal/memo.js +22 -20
- package/dist/internal/protocol.d.ts +28 -1
- package/dist/internal/protocol.js +84 -52
- package/dist/internal/signature.d.ts +5 -9
- package/dist/internal/signature.js +34 -18
- package/dist/internal/step.d.ts +5 -11
- package/dist/internal/step.js +48 -32
- package/package.json +26 -21
- package/src/Client.ts +244 -68
- package/src/Events.ts +3 -3
- package/src/Function.ts +52 -15
- package/src/Group.ts +39 -26
- package/src/HttpApi.ts +38 -27
- package/src/internal/checkpoint.ts +218 -0
- package/src/internal/driver.ts +241 -93
- package/src/internal/errors.ts +21 -14
- package/src/internal/handler.ts +185 -142
- package/src/internal/helpers.ts +9 -9
- package/src/internal/memo.ts +48 -33
- package/src/internal/protocol.ts +89 -45
- package/src/internal/signature.ts +76 -58
- package/src/internal/step.ts +84 -31
- package/dist/_virtual/rolldown_runtime.js +0 -18
package/src/internal/protocol.ts
CHANGED
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
* Wire protocol schemas and opcode factories for Inngest communication.
|
|
3
3
|
* @internal
|
|
4
4
|
*/
|
|
5
|
-
import { Predicate, Struct } from "effect";
|
|
5
|
+
import { Effect, Predicate, SchemaTransformation, Struct } from "effect";
|
|
6
6
|
import * as Schema from "effect/Schema";
|
|
7
7
|
|
|
8
8
|
const stripTopLevelTag = (value: unknown): unknown => {
|
|
9
|
-
if (Predicate.
|
|
10
|
-
return Struct.omit(value as Record<string, unknown>, "_tag");
|
|
9
|
+
if (Predicate.isObject(value)) {
|
|
10
|
+
return Struct.omit(value as Record<string, unknown>, ["_tag"]);
|
|
11
11
|
}
|
|
12
12
|
return value;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
const WireUnknown = Schema.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const WireUnknown = Schema.Unknown.pipe(
|
|
16
|
+
Schema.decodeTo(
|
|
17
|
+
Schema.Unknown,
|
|
18
|
+
SchemaTransformation.transform({
|
|
19
|
+
decode: (value) => value,
|
|
20
|
+
encode: (value) => stripTopLevelTag(value),
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
20
24
|
|
|
21
25
|
export const Opcode = {
|
|
22
26
|
None: "None",
|
|
@@ -48,9 +52,7 @@ export class UserError extends Schema.Class<UserError>("UserError")({
|
|
|
48
52
|
}) {}
|
|
49
53
|
|
|
50
54
|
export const StepResult = Schema.NullOr(
|
|
51
|
-
Schema.Record(
|
|
52
|
-
Schema.annotations({ identifier: "StepResultObject" }),
|
|
53
|
-
),
|
|
55
|
+
Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.annotate({ identifier: "StepResultObject" })),
|
|
54
56
|
);
|
|
55
57
|
|
|
56
58
|
export class FunctionStack extends Schema.Class<FunctionStack>("FunctionStack")({
|
|
@@ -61,36 +63,37 @@ export class FunctionStack extends Schema.Class<FunctionStack>("FunctionStack")(
|
|
|
61
63
|
export class InngestEvent extends Schema.Class<InngestEvent>("InngestEvent")({
|
|
62
64
|
id: Schema.optional(Schema.String),
|
|
63
65
|
name: Schema.String,
|
|
64
|
-
data: Schema.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}),
|
|
66
|
+
data: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)).pipe(
|
|
67
|
+
Schema.withDecodingDefaultType(Effect.succeed({})),
|
|
68
|
+
),
|
|
68
69
|
|
|
69
70
|
ts: Schema.optional(Schema.Number),
|
|
70
|
-
user: Schema.optional(Schema.Record(
|
|
71
|
+
user: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
71
72
|
v: Schema.optional(Schema.String),
|
|
72
73
|
}) {}
|
|
73
74
|
|
|
74
75
|
export class SDKRequestContext extends Schema.Class<SDKRequestContext>("SDKRequestContext")({
|
|
75
76
|
fn_id: Schema.String,
|
|
76
77
|
run_id: Schema.String,
|
|
77
|
-
env: Schema.
|
|
78
|
-
step_id: Schema.
|
|
79
|
-
attempt: Schema.
|
|
80
|
-
max_attempts: Schema.
|
|
81
|
-
stack:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
env: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("dev"))),
|
|
79
|
+
step_id: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("step"))),
|
|
80
|
+
attempt: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(0))),
|
|
81
|
+
max_attempts: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(4))),
|
|
82
|
+
stack: FunctionStack.pipe(
|
|
83
|
+
Schema.withDecodingDefaultType(Effect.succeed(FunctionStack.make({ stack: [], current: 0 }))),
|
|
84
|
+
),
|
|
85
|
+
qi_id: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
|
|
86
|
+
disable_immediate_execution: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
|
|
87
|
+
use_api: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
|
|
85
88
|
}) {}
|
|
86
89
|
|
|
87
90
|
export class SDKRequestBody extends Schema.Class<SDKRequestBody>("SDKRequestBody")({
|
|
88
91
|
event: InngestEvent,
|
|
89
92
|
events: Schema.Array(InngestEvent),
|
|
90
|
-
steps: Schema.
|
|
93
|
+
steps: Schema.Record(Schema.String, StepResult).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
|
|
91
94
|
ctx: SDKRequestContext,
|
|
92
|
-
version: Schema.
|
|
93
|
-
use_api: Schema.
|
|
95
|
+
version: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(1))),
|
|
96
|
+
use_api: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
|
|
94
97
|
}) {}
|
|
95
98
|
|
|
96
99
|
export const Headers = {
|
|
@@ -108,7 +111,7 @@ export const Headers = {
|
|
|
108
111
|
} as const;
|
|
109
112
|
|
|
110
113
|
export class GeneratorOpcode extends Schema.Class<GeneratorOpcode>("GeneratorOpcode")({
|
|
111
|
-
op: Schema.
|
|
114
|
+
op: Schema.Literals([
|
|
112
115
|
Opcode.None,
|
|
113
116
|
Opcode.Step,
|
|
114
117
|
Opcode.StepRun,
|
|
@@ -124,10 +127,10 @@ export class GeneratorOpcode extends Schema.Class<GeneratorOpcode>("GeneratorOpc
|
|
|
124
127
|
Opcode.StepFailed,
|
|
125
128
|
Opcode.SyncRunComplete,
|
|
126
129
|
Opcode.DiscoveryRequest,
|
|
127
|
-
),
|
|
130
|
+
]),
|
|
128
131
|
id: Schema.String,
|
|
129
132
|
name: Schema.String,
|
|
130
|
-
mode: Schema.optional(Schema.
|
|
133
|
+
mode: Schema.optional(Schema.Literals(["sync", "async"])),
|
|
131
134
|
opts: Schema.optional(WireUnknown),
|
|
132
135
|
data: Schema.optional(WireUnknown),
|
|
133
136
|
error: Schema.optional(UserError),
|
|
@@ -158,9 +161,17 @@ export const stepError = (info: StepInfo, error: UserError, noRetry?: boolean):
|
|
|
158
161
|
};
|
|
159
162
|
|
|
160
163
|
export const sleep = (info: StepInfo, duration: string): GeneratorOpcode =>
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
+
// Spec §5.3.2 requires `opts.duration`. The Inngest executor also accepts
|
|
165
|
+
// `name: duration` (de facto inngest-js behavior); we emit both so both
|
|
166
|
+
// the spec-strict path and the official-SDK-lenient path see a value.
|
|
167
|
+
GeneratorOpcode.make({
|
|
168
|
+
op: Opcode.Sleep,
|
|
169
|
+
id: info.hash,
|
|
170
|
+
name: duration,
|
|
171
|
+
displayName: info.name,
|
|
172
|
+
mode: "async",
|
|
173
|
+
opts: { duration },
|
|
174
|
+
});
|
|
164
175
|
|
|
165
176
|
export const waitForEvent = (info: StepInfo, opts: { event: string; timeout: string; if?: string }): GeneratorOpcode =>
|
|
166
177
|
mkOpcode(info, Opcode.WaitForEvent, { mode: "async", opts });
|
|
@@ -170,27 +181,41 @@ export const invokeFunction = (
|
|
|
170
181
|
opts: { function_id: string; payload: unknown; timeout: string },
|
|
171
182
|
): GeneratorOpcode => mkOpcode(info, Opcode.InvokeFunction, { mode: "async", opts, userland: { id: info.id } });
|
|
172
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Terminal opcode emitted by the SDK in checkpoint mode when the function
|
|
186
|
+
* completes successfully. The executor uses this to correlate run completion
|
|
187
|
+
* (spec §10.4.1).
|
|
188
|
+
*/
|
|
189
|
+
export const runComplete = (data: unknown): GeneratorOpcode =>
|
|
190
|
+
GeneratorOpcode.make({ op: Opcode.RunComplete, id: "step", name: "step", data });
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Yield opcode emitted by the SDK in checkpoint mode when `maxRuntime` is
|
|
194
|
+
* exceeded. Tells the executor to schedule a new Call Request to continue
|
|
195
|
+
* execution (spec §10.4.1).
|
|
196
|
+
*/
|
|
197
|
+
export const discoveryRequest = (): GeneratorOpcode =>
|
|
198
|
+
GeneratorOpcode.make({ op: Opcode.DiscoveryRequest, id: "step", name: "step" });
|
|
199
|
+
|
|
173
200
|
const IntrospectionBase = Schema.Struct({
|
|
174
201
|
function_count: Schema.Number,
|
|
175
202
|
has_event_key: Schema.Boolean,
|
|
176
203
|
has_signing_key: Schema.Boolean,
|
|
177
204
|
has_signing_key_fallback: Schema.Boolean,
|
|
178
|
-
mode: Schema.
|
|
205
|
+
mode: Schema.Literals(["cloud", "dev"]),
|
|
179
206
|
schema_version: Schema.Literal("2024-05-24"),
|
|
180
|
-
extra: Schema.optional(Schema.Record(
|
|
207
|
+
extra: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
181
208
|
});
|
|
182
209
|
|
|
183
|
-
export const IntrospectionUnauthenticated =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
functions: Schema.optionalWith(Schema.Array(Schema.Unknown), { exact: true }),
|
|
210
|
+
export const IntrospectionUnauthenticated = IntrospectionBase.pipe(
|
|
211
|
+
Schema.fieldsAssign({
|
|
212
|
+
authentication_succeeded: Schema.Union([Schema.Literal(false), Schema.Null]),
|
|
213
|
+
functions: Schema.optionalKey(Schema.Array(Schema.Unknown)),
|
|
188
214
|
}),
|
|
189
215
|
);
|
|
190
216
|
|
|
191
|
-
export const IntrospectionAuthenticated =
|
|
192
|
-
|
|
193
|
-
Schema.Struct({
|
|
217
|
+
export const IntrospectionAuthenticated = IntrospectionBase.pipe(
|
|
218
|
+
Schema.fieldsAssign({
|
|
194
219
|
authentication_succeeded: Schema.Literal(true),
|
|
195
220
|
api_origin: Schema.String,
|
|
196
221
|
app_id: Schema.String,
|
|
@@ -207,8 +232,27 @@ export const IntrospectionAuthenticated = Schema.extend(
|
|
|
207
232
|
}),
|
|
208
233
|
);
|
|
209
234
|
|
|
210
|
-
export const IntrospectionResponse = Schema.Union(IntrospectionAuthenticated, IntrospectionUnauthenticated);
|
|
235
|
+
export const IntrospectionResponse = Schema.Union([IntrospectionAuthenticated, IntrospectionUnauthenticated]);
|
|
211
236
|
|
|
237
|
+
/**
|
|
238
|
+
* SDK → executor response shape for PUT sync requests per spec §4.3.1.
|
|
239
|
+
*/
|
|
212
240
|
export const RegisterResponse = Schema.Struct({
|
|
213
|
-
message: Schema.
|
|
241
|
+
message: Schema.String,
|
|
242
|
+
modified: Schema.Boolean,
|
|
214
243
|
});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Executor → SDK response shape from `POST /fn/register` per spec §4.3.4.
|
|
247
|
+
* On success the body has `{ ok: true, modified?: boolean }`; on failure
|
|
248
|
+
* the body may carry an `error` string.
|
|
249
|
+
*/
|
|
250
|
+
export const RegisterServerResponse = Schema.Union([
|
|
251
|
+
Schema.Struct({
|
|
252
|
+
ok: Schema.Literal(true),
|
|
253
|
+
modified: Schema.optional(Schema.Boolean),
|
|
254
|
+
}),
|
|
255
|
+
Schema.Struct({
|
|
256
|
+
error: Schema.optional(Schema.String),
|
|
257
|
+
}),
|
|
258
|
+
]);
|
|
@@ -8,13 +8,14 @@ import * as DateTime from "effect/DateTime";
|
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import * as Layer from "effect/Layer";
|
|
10
10
|
import * as Schema from "effect/Schema";
|
|
11
|
+
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
11
12
|
import * as Protocol from "./protocol.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @internal
|
|
15
16
|
*/
|
|
16
|
-
export class SignatureError extends Schema.
|
|
17
|
-
reason: Schema.
|
|
17
|
+
export class SignatureError extends Schema.TaggedErrorClass<SignatureError>()("SignatureError", {
|
|
18
|
+
reason: Schema.Literals(["missing_header", "invalid_format", "expired", "invalid_signature", "missing_signing_key"]),
|
|
18
19
|
message: Schema.String,
|
|
19
20
|
}) {}
|
|
20
21
|
|
|
@@ -40,20 +41,23 @@ export interface SignatureService {
|
|
|
40
41
|
/**
|
|
41
42
|
* @internal
|
|
42
43
|
*/
|
|
43
|
-
export class Signature extends Context.
|
|
44
|
+
export class Signature extends Context.Service<Signature, SignatureService>()("effect-inngest/Signature") {}
|
|
44
45
|
|
|
45
46
|
// ─────────────────────────────────────────────────────────────
|
|
46
47
|
// Schemas (internal)
|
|
47
48
|
// ─────────────────────────────────────────────────────────────
|
|
48
49
|
|
|
49
|
-
const TimestampSeconds = Schema.NumberFromString.pipe(Schema.
|
|
50
|
+
const TimestampSeconds = Schema.NumberFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThan(0)));
|
|
50
51
|
// Case-insensitive hex, normalized to lowercase in parseSignatureHeader
|
|
51
52
|
const SignatureHex = Schema.String.pipe(
|
|
52
|
-
Schema.
|
|
53
|
-
Schema.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
Schema.check(Schema.isPattern(/^[a-fA-F0-9]{64}$/)),
|
|
54
|
+
Schema.decodeTo(
|
|
55
|
+
Schema.String,
|
|
56
|
+
SchemaTransformation.transform({
|
|
57
|
+
decode: (s) => s.toLowerCase(),
|
|
58
|
+
encode: (s) => s,
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
57
61
|
);
|
|
58
62
|
const SignatureParams = Schema.Struct({ t: TimestampSeconds, s: SignatureHex });
|
|
59
63
|
|
|
@@ -66,7 +70,7 @@ const SIGNATURE_VALIDITY_WINDOW_MS = 5 * 60 * 1000;
|
|
|
66
70
|
const parseSignatureHeader = (header: string) => {
|
|
67
71
|
const params = new URLSearchParams(header);
|
|
68
72
|
const raw = { t: params.get("t") ?? "", s: params.get("s") ?? "" };
|
|
69
|
-
return Schema.
|
|
73
|
+
return Schema.decodeUnknownEffect(SignatureParams)(raw).pipe(
|
|
70
74
|
Effect.mapError(
|
|
71
75
|
() =>
|
|
72
76
|
new SignatureError({
|
|
@@ -82,6 +86,18 @@ const extractKeyBytes = (signingKey: string): Buffer => {
|
|
|
82
86
|
return Buffer.from(keyWithoutPrefix, "hex");
|
|
83
87
|
};
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* SHA-256 hex of the signing key with the `signkey-{prefix}-` prefix stripped.
|
|
91
|
+
* Used as the Bearer token for outbound requests to the Inngest API per spec
|
|
92
|
+
* §4.1.1.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export const hashSigningKey = (signingKey: string): string => {
|
|
97
|
+
const keyWithoutPrefix = signingKey.replace(/^signkey-\w+-/, "");
|
|
98
|
+
return Crypto.createHash("sha256").update(keyWithoutPrefix).digest("hex");
|
|
99
|
+
};
|
|
100
|
+
|
|
85
101
|
const computeSignature = (keyBytes: Buffer, body: Uint8Array, timestamp: string): string =>
|
|
86
102
|
Crypto.createHmac("sha256", keyBytes).update(body).update(timestamp).digest("hex");
|
|
87
103
|
|
|
@@ -107,52 +123,54 @@ const checkSignature = (signature: string, body: Uint8Array, timestamp: string,
|
|
|
107
123
|
/**
|
|
108
124
|
* @internal
|
|
109
125
|
*/
|
|
110
|
-
export const SignatureLive: Layer.Layer<Signature> = Layer.
|
|
111
|
-
Signature
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
DateTime.now.pipe(
|
|
152
|
-
Effect.map((now) => {
|
|
153
|
-
const ts = Math.floor(now.epochMillis / 1000);
|
|
154
|
-
return `t=${ts}&s=${computeSignature(extractKeyBytes(signingKey), body, String(ts))}`;
|
|
155
|
-
}),
|
|
156
|
-
),
|
|
126
|
+
export const SignatureLive: Layer.Layer<Signature> = Layer.succeed(Signature, {
|
|
127
|
+
verify: Effect.fn("Signature.verify")(function* ({
|
|
128
|
+
body,
|
|
129
|
+
signatureHeader,
|
|
130
|
+
signingKey,
|
|
131
|
+
signingKeyFallback,
|
|
132
|
+
isDev,
|
|
133
|
+
}: VerifyOptions) {
|
|
134
|
+
if (isDev) return true;
|
|
135
|
+
if (!signingKey) {
|
|
136
|
+
return yield* new SignatureError({
|
|
137
|
+
reason: "missing_signing_key",
|
|
138
|
+
message: "No signing key configured for production mode",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!signatureHeader) {
|
|
143
|
+
return yield* new SignatureError({
|
|
144
|
+
reason: "missing_header",
|
|
145
|
+
message: `Missing ${Protocol.Headers.Signature} header`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { t: timestampSeconds, s: signature } = yield* parseSignatureHeader(signatureHeader);
|
|
150
|
+
const timestampMs = timestampSeconds * 1000;
|
|
151
|
+
const now = yield* DateTime.now;
|
|
152
|
+
|
|
153
|
+
if (Math.abs(now.epochMilliseconds - timestampMs) > SIGNATURE_VALIDITY_WINDOW_MS) {
|
|
154
|
+
return yield* new SignatureError({
|
|
155
|
+
reason: "expired",
|
|
156
|
+
message: `Signature expired: timestamp ${timestampSeconds} is outside the validity window`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const timestamp = String(timestampSeconds);
|
|
161
|
+
const keys = [signingKey, signingKeyFallback].filter(Boolean) as string[];
|
|
162
|
+
const valid = keys.some((key) => checkSignature(signature, body, timestamp, key));
|
|
163
|
+
|
|
164
|
+
if (valid) return true;
|
|
165
|
+
|
|
166
|
+
return yield* new SignatureError({ reason: "invalid_signature", message: "Invalid signature" });
|
|
157
167
|
}),
|
|
158
|
-
|
|
168
|
+
|
|
169
|
+
sign: (body, signingKey) =>
|
|
170
|
+
DateTime.now.pipe(
|
|
171
|
+
Effect.map((now) => {
|
|
172
|
+
const ts = Math.floor(now.epochMilliseconds / 1000);
|
|
173
|
+
return `t=${ts}&s=${computeSignature(extractKeyBytes(signingKey), body, String(ts))}`;
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
});
|
package/src/internal/step.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* @internal
|
|
4
4
|
*/
|
|
5
5
|
import * as Arr from "effect/Array";
|
|
6
|
-
import * as Context from "effect/Context";
|
|
7
6
|
import * as Duration from "effect/Duration";
|
|
8
7
|
import * as Effect from "effect/Effect";
|
|
9
8
|
import * as HashMap from "effect/HashMap";
|
|
@@ -15,6 +14,7 @@ import * as Schema from "effect/Schema";
|
|
|
15
14
|
import { pipe } from "effect/Function";
|
|
16
15
|
import { InngestClient } from "../Client.js";
|
|
17
16
|
import type { InngestFunction } from "../Function.js";
|
|
17
|
+
import type { CheckpointState } from "./checkpoint.js";
|
|
18
18
|
import * as Protocol from "./protocol.js";
|
|
19
19
|
import { StepError, SendEventError, isNonRetriableError, isRetryAfterError } from "./errors.js";
|
|
20
20
|
import { timeStr, formatTimestamp } from "./helpers.js";
|
|
@@ -68,7 +68,7 @@ interface StepOptions {
|
|
|
68
68
|
type StepOptionsOrId = string | StepOptions;
|
|
69
69
|
|
|
70
70
|
interface WaitForEventOptions {
|
|
71
|
-
readonly timeout: Duration.
|
|
71
|
+
readonly timeout: Duration.Input;
|
|
72
72
|
readonly if?: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -76,20 +76,22 @@ interface InvokeOptionsBase<F extends InngestFunction.Any> {
|
|
|
76
76
|
readonly function: F;
|
|
77
77
|
readonly user?: Record<string, unknown>;
|
|
78
78
|
readonly v?: string;
|
|
79
|
-
readonly timeout?: Duration.
|
|
79
|
+
readonly timeout?: Duration.Input;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
type InvokeOptions<F extends InngestFunction.Any> = [InngestFunction.EventType<F>] extends [never]
|
|
83
83
|
? InvokeOptionsBase<F>
|
|
84
84
|
: InvokeOptionsBase<F> & { readonly data: InngestFunction.EventType<F> };
|
|
85
85
|
|
|
86
|
-
type EventSchema = Schema.
|
|
86
|
+
type EventSchema = Schema.Top & { readonly identifier: string };
|
|
87
87
|
type TaggedEvent = { readonly _tag: string };
|
|
88
88
|
|
|
89
|
-
const encodeTaggedEvent = (event: TaggedEvent): Effect.Effect<unknown, never> => {
|
|
89
|
+
const encodeTaggedEvent = (event: TaggedEvent): Effect.Effect<unknown, never, never> => {
|
|
90
90
|
const ctor = event.constructor;
|
|
91
91
|
if (Schema.isSchema(ctor)) {
|
|
92
|
-
return Schema.
|
|
92
|
+
return Schema.encodeEffect(ctor as unknown as Schema.Top)(event).pipe(
|
|
93
|
+
Effect.orElseSucceed(() => event),
|
|
94
|
+
) as Effect.Effect<unknown, never, never>;
|
|
93
95
|
}
|
|
94
96
|
return Effect.succeed(event);
|
|
95
97
|
};
|
|
@@ -99,7 +101,7 @@ interface StepTools {
|
|
|
99
101
|
id: StepOptionsOrId,
|
|
100
102
|
effect: Effect.Effect<A, Err, R>,
|
|
101
103
|
) => Effect.Effect<A, StepError | Err, R>;
|
|
102
|
-
readonly sleep: (id: StepOptionsOrId, duration: Duration.
|
|
104
|
+
readonly sleep: (id: StepOptionsOrId, duration: Duration.Input) => Effect.Effect<void>;
|
|
103
105
|
readonly sleepUntil: (id: StepOptionsOrId, timestamp: Date | number | string) => Effect.Effect<void>;
|
|
104
106
|
readonly waitForEvent: <E extends EventSchema>(
|
|
105
107
|
id: StepOptionsOrId,
|
|
@@ -128,8 +130,6 @@ export interface HandlerContext<F extends InngestFunction.Any> {
|
|
|
128
130
|
readonly run: RunContext;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
export class Step extends Context.Tag("effect-inngest/Step")<Step, StepTools>() {}
|
|
132
|
-
|
|
133
133
|
const normalizeOpts = (opts: StepOptionsOrId): { id: string; name: string } =>
|
|
134
134
|
typeof opts === "string" ? { id: opts, name: opts } : { id: opts.id, name: opts.name ?? opts.id };
|
|
135
135
|
|
|
@@ -140,6 +140,7 @@ export const createStepTools = (
|
|
|
140
140
|
request: Protocol.SDKRequestBody,
|
|
141
141
|
appName: string,
|
|
142
142
|
stepIdCounts: Ref.Ref<HashMap.HashMap<string, number>>,
|
|
143
|
+
checkpoint: Option.Option<CheckpointState> = Option.none(),
|
|
143
144
|
): StepTools => {
|
|
144
145
|
const ctx = request.ctx;
|
|
145
146
|
const steps = request.steps as Record<string, unknown>;
|
|
@@ -159,7 +160,15 @@ export const createStepTools = (
|
|
|
159
160
|
const canExecute = (hash: string) => ctx.step_id === hash || ctx.step_id === "step";
|
|
160
161
|
const isBlocked = (hash: string) => ctx.disable_immediate_execution && ctx.step_id !== hash;
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
// In checkpoint mode, async opcodes (sleep/wait/invoke) MUST flush the
|
|
164
|
+
// buffer before yielding so the executor sees buffered StepRuns prepended
|
|
165
|
+
// to the async opcode in the 206 response (spec §10.2 / §10.4.1 #6).
|
|
166
|
+
const flushIfCheckpoint = Option.match(checkpoint, {
|
|
167
|
+
onNone: () => Effect.void,
|
|
168
|
+
onSome: (state) => state.flush,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const sleep = (opts: StepOptionsOrId, duration: Duration.Input): Effect.Effect<void, StepInterrupt> =>
|
|
163
172
|
Effect.flatMap(getInfo(opts), (info) =>
|
|
164
173
|
pipe(
|
|
165
174
|
getMemo(info.hash),
|
|
@@ -168,7 +177,13 @@ export const createStepTools = (
|
|
|
168
177
|
Match.tag("MemoTimeout", () => Effect.void),
|
|
169
178
|
Match.tag("MemoError", () => Effect.void),
|
|
170
179
|
Match.tag("MemoInput", () => Effect.void),
|
|
171
|
-
Match.tag("MemoNone", () =>
|
|
180
|
+
Match.tag("MemoNone", () =>
|
|
181
|
+
Effect.andThen(flushIfCheckpoint, Effect.fail(sleepInterrupt({ info, duration: timeStr(duration) }))).pipe(
|
|
182
|
+
Effect.withSpan(`inngest.step/sleep/${info.id}`, {
|
|
183
|
+
attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sleep" },
|
|
184
|
+
}),
|
|
185
|
+
),
|
|
186
|
+
),
|
|
172
187
|
Match.exhaustive,
|
|
173
188
|
),
|
|
174
189
|
);
|
|
@@ -182,7 +197,16 @@ export const createStepTools = (
|
|
|
182
197
|
Match.tag("MemoTimeout", () => Effect.void),
|
|
183
198
|
Match.tag("MemoError", () => Effect.void),
|
|
184
199
|
Match.tag("MemoInput", () => Effect.void),
|
|
185
|
-
Match.tag("MemoNone", () =>
|
|
200
|
+
Match.tag("MemoNone", () =>
|
|
201
|
+
Effect.andThen(
|
|
202
|
+
flushIfCheckpoint,
|
|
203
|
+
Effect.fail(sleepInterrupt({ info, duration: formatTimestamp(timestamp) })),
|
|
204
|
+
).pipe(
|
|
205
|
+
Effect.withSpan(`inngest.step/sleepUntil/${info.id}`, {
|
|
206
|
+
attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sleepUntil" },
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
),
|
|
186
210
|
Match.exhaustive,
|
|
187
211
|
),
|
|
188
212
|
);
|
|
@@ -210,8 +234,16 @@ export const createStepTools = (
|
|
|
210
234
|
Match.tag("MemoError", () => Effect.succeed(Option.none())),
|
|
211
235
|
Match.tag("MemoInput", () => Effect.succeed(Option.none())),
|
|
212
236
|
Match.tag("MemoNone", () =>
|
|
213
|
-
Effect.
|
|
214
|
-
|
|
237
|
+
Effect.andThen(
|
|
238
|
+
flushIfCheckpoint,
|
|
239
|
+
Effect.fail(
|
|
240
|
+
waitForEventInterrupt({
|
|
241
|
+
info,
|
|
242
|
+
event: event.identifier,
|
|
243
|
+
timeout: timeStr(options.timeout),
|
|
244
|
+
if: options.if,
|
|
245
|
+
}),
|
|
246
|
+
),
|
|
215
247
|
),
|
|
216
248
|
),
|
|
217
249
|
Match.exhaustive,
|
|
@@ -238,17 +270,20 @@ export const createStepTools = (
|
|
|
238
270
|
const rawData = Predicate.hasProperty(options, "data") ? (options.data as TaggedEvent) : undefined;
|
|
239
271
|
const encodeData = rawData ? encodeTaggedEvent(rawData) : Effect.succeed(undefined);
|
|
240
272
|
return Effect.flatMap(encodeData, (encodedData) =>
|
|
241
|
-
Effect.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
273
|
+
Effect.andThen(
|
|
274
|
+
flushIfCheckpoint,
|
|
275
|
+
Effect.fail(
|
|
276
|
+
invokeInterrupt({
|
|
277
|
+
info,
|
|
278
|
+
functionId: `${appName}-${options.function._tag}`,
|
|
279
|
+
payload: {
|
|
280
|
+
data: encodedData,
|
|
281
|
+
user: options.user ?? {},
|
|
282
|
+
v: options.v ?? "1",
|
|
283
|
+
},
|
|
284
|
+
timeout: options.timeout ? timeStr(options.timeout) : "365d",
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
252
287
|
),
|
|
253
288
|
);
|
|
254
289
|
}),
|
|
@@ -266,8 +301,13 @@ export const createStepTools = (
|
|
|
266
301
|
Match.value,
|
|
267
302
|
Match.tag("MemoData", ({ data }) => Effect.succeed(data as A)),
|
|
268
303
|
Match.tag("MemoError", ({ error }) =>
|
|
304
|
+
// Spec §5.2.2: an uncaught memoized step error MUST be treated as
|
|
305
|
+
// non-retriable. If the user wanted to recover they would have
|
|
306
|
+
// caught the StepError at the call site; reaching the driver's
|
|
307
|
+
// top-level failure path means the error is propagating unhandled.
|
|
269
308
|
stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Step failed", {
|
|
270
309
|
noRetry: true,
|
|
310
|
+
cause: error,
|
|
271
311
|
}),
|
|
272
312
|
),
|
|
273
313
|
Match.tag("MemoTimeout", () => stepError(info.id, "Step timed out", { noRetry: true })),
|
|
@@ -285,15 +325,23 @@ export const createStepTools = (
|
|
|
285
325
|
onFailure: (err) => {
|
|
286
326
|
const noRetry = isNonRetriableError(err) ? true : undefined;
|
|
287
327
|
const retryAfterMs = isRetryAfterError(err) ? Duration.toMillis(err.retryAfter) : undefined;
|
|
288
|
-
return Effect.
|
|
328
|
+
return Effect.andThen(
|
|
289
329
|
Effect.annotateCurrentSpan(errorOtelAttributes(err)),
|
|
290
330
|
Effect.fail(errorInterrupt({ info, error: err, noRetry, retryAfterMs })),
|
|
291
331
|
);
|
|
292
332
|
},
|
|
293
|
-
onSuccess: (data) =>
|
|
333
|
+
onSuccess: (data) =>
|
|
334
|
+
Option.match(checkpoint, {
|
|
335
|
+
// Async (non-checkpoint) mode: surface result via interrupt — driver
|
|
336
|
+
// returns 206 with a single StepRun and yields back to executor.
|
|
337
|
+
onNone: () => Effect.fail(runInterrupt({ info, data })),
|
|
338
|
+
// Checkpoint mode: buffer the StepRun (best-effort flush handled by
|
|
339
|
+
// bufferStep) and continue execution with the value (spec §10.4.1).
|
|
340
|
+
onSome: (state) => Effect.as(state.bufferStep(Protocol.stepRun(info, data)), data as A),
|
|
341
|
+
}),
|
|
294
342
|
}),
|
|
295
|
-
Effect.
|
|
296
|
-
Effect.
|
|
343
|
+
Effect.catchDefect((defect) =>
|
|
344
|
+
Effect.andThen(
|
|
297
345
|
Effect.annotateCurrentSpan(errorOtelAttributes(defect)),
|
|
298
346
|
Effect.fail(errorInterrupt({ info, error: defect })),
|
|
299
347
|
),
|
|
@@ -329,12 +377,17 @@ export const createStepTools = (
|
|
|
329
377
|
Effect.map(encodeTaggedEvent(e), (encoded) => ({ name: e._tag, data: encoded })),
|
|
330
378
|
),
|
|
331
379
|
(eventPayloads) =>
|
|
332
|
-
|
|
380
|
+
InngestClient.use((client) =>
|
|
333
381
|
client.sendEvent(eventPayloads).pipe(
|
|
334
382
|
Effect.withSpan(`inngest.step/sendEvent/${info.id}`, {
|
|
335
383
|
attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sendEvent" },
|
|
336
384
|
}),
|
|
337
|
-
Effect.flatMap((result) =>
|
|
385
|
+
Effect.flatMap((result) =>
|
|
386
|
+
Option.match(checkpoint, {
|
|
387
|
+
onNone: () => Effect.fail(runInterrupt({ info, data: result })),
|
|
388
|
+
onSome: (state) => Effect.as(state.bufferStep(Protocol.stepRun(info, result)), result),
|
|
389
|
+
}),
|
|
390
|
+
),
|
|
338
391
|
),
|
|
339
392
|
),
|
|
340
393
|
);
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//#region rolldown:runtime
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __exportAll = (all, symbols) => {
|
|
4
|
-
let target = {};
|
|
5
|
-
for (var name in all) {
|
|
6
|
-
__defProp(target, name, {
|
|
7
|
-
get: all[name],
|
|
8
|
-
enumerable: true
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
if (symbols) {
|
|
12
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
-
}
|
|
14
|
-
return target;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
//#endregion
|
|
18
|
-
export { __exportAll };
|