effect-inngest 0.1.2 → 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 +83 -32
- package/dist/_virtual/rolldown_runtime.js +0 -18
package/src/Function.ts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
* @since 0.1.0
|
|
3
3
|
*/
|
|
4
4
|
import { Array as Arr, Duration, Predicate, Schema } from "effect";
|
|
5
|
+
import { pipeArguments, type Pipeable } from "effect/Pipeable";
|
|
6
|
+
import * as Checkpoint from "./internal/checkpoint.js";
|
|
7
|
+
import type { CheckpointingOption } from "./internal/checkpoint.js";
|
|
5
8
|
import { timeStr } from "./internal/helpers.js";
|
|
6
9
|
|
|
10
|
+
export type { CheckpointingOption } from "./internal/checkpoint.js";
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* @since 0.1.0
|
|
9
14
|
* @category type ids
|
|
@@ -16,7 +21,7 @@ export const TypeId: unique symbol = Symbol.for("effect-inngest/Function");
|
|
|
16
21
|
*/
|
|
17
22
|
export type TypeId = typeof TypeId;
|
|
18
23
|
|
|
19
|
-
type EventSchema = Schema.
|
|
24
|
+
type EventSchema = Schema.Top & { readonly identifier: string };
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* An event-based trigger configuration.
|
|
@@ -98,7 +103,7 @@ interface RateLimitOption {
|
|
|
98
103
|
/**
|
|
99
104
|
* The period of time to allow the function to run `limit` times.
|
|
100
105
|
*/
|
|
101
|
-
readonly period: Duration.
|
|
106
|
+
readonly period: Duration.Input;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
interface ThrottleOption {
|
|
@@ -119,7 +124,7 @@ interface ThrottleOption {
|
|
|
119
124
|
* The period of time for the rate limit. Run starts are evenly spaced through
|
|
120
125
|
* the given period. The minimum granularity is 1 second.
|
|
121
126
|
*/
|
|
122
|
-
readonly period: Duration.
|
|
127
|
+
readonly period: Duration.Input;
|
|
123
128
|
|
|
124
129
|
/**
|
|
125
130
|
* The number of runs allowed to start in the given window in a single burst.
|
|
@@ -138,14 +143,14 @@ interface DebounceOption {
|
|
|
138
143
|
/**
|
|
139
144
|
* The period of time to delay after receiving the last trigger to run the function.
|
|
140
145
|
*/
|
|
141
|
-
readonly period: Duration.
|
|
146
|
+
readonly period: Duration.Input;
|
|
142
147
|
|
|
143
148
|
/**
|
|
144
149
|
* The maximum time that a debounce can be extended before running.
|
|
145
150
|
* If events are continually received within the given period, a function
|
|
146
151
|
* will always run after the given timeout period.
|
|
147
152
|
*/
|
|
148
|
-
readonly timeout?: Duration.
|
|
153
|
+
readonly timeout?: Duration.Input;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
interface BatchEventsOption {
|
|
@@ -159,7 +164,7 @@ interface BatchEventsOption {
|
|
|
159
164
|
* If timeout is reached, the function will be invoked with a batch
|
|
160
165
|
* even if it's not filled up to `maxSize`.
|
|
161
166
|
*/
|
|
162
|
-
readonly timeout: Duration.
|
|
167
|
+
readonly timeout: Duration.Input;
|
|
163
168
|
|
|
164
169
|
/**
|
|
165
170
|
* An optional key to use for batching.
|
|
@@ -192,14 +197,14 @@ interface TimeoutsOption {
|
|
|
192
197
|
* This is, essentially, the amount of time that a function sits in the
|
|
193
198
|
* queue before starting.
|
|
194
199
|
*/
|
|
195
|
-
readonly start?: Duration.
|
|
200
|
+
readonly start?: Duration.Input;
|
|
196
201
|
|
|
197
202
|
/**
|
|
198
203
|
* Finish represents the time between a function starting and the function
|
|
199
204
|
* finishing. If a function takes longer than this time to finish, the
|
|
200
205
|
* function is marked as cancelled.
|
|
201
206
|
*/
|
|
202
|
-
readonly finish?: Duration.
|
|
207
|
+
readonly finish?: Duration.Input;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
interface SingletonOption {
|
|
@@ -237,7 +242,7 @@ interface CancellationOption {
|
|
|
237
242
|
* specified, cancellation triggers are valid for up to a year or until the
|
|
238
243
|
* function ends.
|
|
239
244
|
*/
|
|
240
|
-
readonly timeout?: Duration.
|
|
245
|
+
readonly timeout?: Duration.Input;
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
type Retries = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
|
|
@@ -321,6 +326,20 @@ export interface FunctionOptions {
|
|
|
321
326
|
* Batch events configuration.
|
|
322
327
|
*/
|
|
323
328
|
readonly batchEvents?: BatchEventsOption;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Whether to use checkpointing for executions of this function. Overrides
|
|
332
|
+
* the client-level `checkpointing` setting.
|
|
333
|
+
*
|
|
334
|
+
* - `false` disables checkpointing for this function.
|
|
335
|
+
* - `true` enables checkpointing with safe defaults
|
|
336
|
+
* (`bufferedSteps: 1`, `maxInterval: 0`, `maxRuntime: 10s`).
|
|
337
|
+
* - An object lets you tune `bufferedSteps`, `maxInterval`, `maxRuntime`.
|
|
338
|
+
*
|
|
339
|
+
* Defaults to inheriting from the client-level setting (which itself
|
|
340
|
+
* defaults to enabled with safe defaults).
|
|
341
|
+
*/
|
|
342
|
+
readonly checkpointing?: CheckpointingOption;
|
|
324
343
|
}
|
|
325
344
|
|
|
326
345
|
interface RegistrationConfig {
|
|
@@ -379,6 +398,11 @@ interface FunctionRegistration {
|
|
|
379
398
|
readonly key?: string;
|
|
380
399
|
};
|
|
381
400
|
readonly idempotency?: string;
|
|
401
|
+
readonly checkpoint?: {
|
|
402
|
+
readonly batch_steps: number;
|
|
403
|
+
readonly batch_interval: string;
|
|
404
|
+
readonly max_runtime: string;
|
|
405
|
+
};
|
|
382
406
|
}
|
|
383
407
|
|
|
384
408
|
/**
|
|
@@ -390,9 +414,9 @@ interface FunctionRegistration {
|
|
|
390
414
|
export interface InngestFunction<
|
|
391
415
|
Tag extends string,
|
|
392
416
|
Triggers extends Trigger,
|
|
393
|
-
Success extends Schema.
|
|
417
|
+
Success extends Schema.Top,
|
|
394
418
|
Options extends FunctionOptions = FunctionOptions,
|
|
395
|
-
> {
|
|
419
|
+
> extends Pipeable {
|
|
396
420
|
readonly [TypeId]: TypeId;
|
|
397
421
|
readonly _tag: Tag;
|
|
398
422
|
readonly key: string;
|
|
@@ -407,7 +431,7 @@ export interface InngestFunction<
|
|
|
407
431
|
* @category models
|
|
408
432
|
*/
|
|
409
433
|
export declare namespace InngestFunction {
|
|
410
|
-
export type Any = InngestFunction<string, Trigger, Schema.
|
|
434
|
+
export type Any = InngestFunction<string, Trigger, Schema.Top, FunctionOptions>;
|
|
411
435
|
export type Tag<F> = F extends InngestFunction<infer T, any, any, any> ? T : never;
|
|
412
436
|
export type Triggers<F> = F extends InngestFunction<any, infer T, any, any> ? T : never;
|
|
413
437
|
export type Events<F> =
|
|
@@ -428,6 +452,11 @@ const isEventTrigger = (t: Trigger): t is EventTrigger => Predicate.hasProperty(
|
|
|
428
452
|
const Proto = {
|
|
429
453
|
[TypeId]: TypeId,
|
|
430
454
|
|
|
455
|
+
pipe() {
|
|
456
|
+
// eslint-disable-next-line prefer-rest-params
|
|
457
|
+
return pipeArguments(this, arguments);
|
|
458
|
+
},
|
|
459
|
+
|
|
431
460
|
toRegistration(this: InngestFunction.Any, config: RegistrationConfig): FunctionRegistration {
|
|
432
461
|
const triggers: Array<{
|
|
433
462
|
event?: string;
|
|
@@ -437,7 +466,7 @@ const Proto = {
|
|
|
437
466
|
|
|
438
467
|
for (const t of this.triggers) {
|
|
439
468
|
if (isEventTrigger(t)) {
|
|
440
|
-
triggers.push({ event: t.event.
|
|
469
|
+
triggers.push({ event: t.event.identifier, expression: t.if });
|
|
441
470
|
} else {
|
|
442
471
|
triggers.push({ cron: t.cron });
|
|
443
472
|
}
|
|
@@ -484,7 +513,7 @@ const Proto = {
|
|
|
484
513
|
|
|
485
514
|
const concurrency =
|
|
486
515
|
opts.concurrency != null
|
|
487
|
-
?
|
|
516
|
+
? Predicate.isNumber(opts.concurrency)
|
|
488
517
|
? [{ limit: opts.concurrency }]
|
|
489
518
|
: Arr.ensure(opts.concurrency).map((c) => ({
|
|
490
519
|
key: c.key,
|
|
@@ -505,6 +534,13 @@ const Proto = {
|
|
|
505
534
|
|
|
506
535
|
const idempotency = opts.idempotency;
|
|
507
536
|
|
|
537
|
+
// Function-level checkpoint config only — client-level default is
|
|
538
|
+
// applied at runtime by the handler. Mirrors how other client defaults
|
|
539
|
+
// (e.g. retries) interact with registration.
|
|
540
|
+
const resolvedCheckpoint =
|
|
541
|
+
opts.checkpointing !== undefined ? Checkpoint.resolveConfig(opts.checkpointing, undefined) : undefined;
|
|
542
|
+
const checkpoint = resolvedCheckpoint ? Checkpoint.toRegistration(resolvedCheckpoint) : undefined;
|
|
543
|
+
|
|
508
544
|
const fnId = `${config.appId}-${this._tag}`;
|
|
509
545
|
const stepUrl = new URL(config.url);
|
|
510
546
|
stepUrl.searchParams.set("fnId", fnId);
|
|
@@ -532,6 +568,7 @@ const Proto = {
|
|
|
532
568
|
singleton,
|
|
533
569
|
batchEvents,
|
|
534
570
|
idempotency,
|
|
571
|
+
checkpoint,
|
|
535
572
|
};
|
|
536
573
|
},
|
|
537
574
|
};
|
|
@@ -574,7 +611,7 @@ type NormalizeTriggers<T extends TriggerInput> = T extends ReadonlyArray<Trigger
|
|
|
574
611
|
export function make<
|
|
575
612
|
const Tag extends string,
|
|
576
613
|
T extends TriggerInput,
|
|
577
|
-
S extends Schema.
|
|
614
|
+
S extends Schema.Top,
|
|
578
615
|
const O extends FunctionOptions = {},
|
|
579
616
|
>(
|
|
580
617
|
tag: Tag,
|
package/src/Group.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 0.1.0
|
|
3
3
|
*/
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as HttpServerRequest from "
|
|
7
|
-
import * as HttpServerResponse from "
|
|
8
|
-
import * as UrlParams from "
|
|
4
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
5
|
+
import * as HttpEffect from "effect/unstable/http/HttpEffect";
|
|
6
|
+
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
|
|
7
|
+
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
|
|
8
|
+
import * as UrlParams from "effect/unstable/http/UrlParams";
|
|
9
9
|
import * as Context from "effect/Context";
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
11
|
import * as Layer from "effect/Layer";
|
|
@@ -150,7 +150,7 @@ const Proto = {
|
|
|
150
150
|
const fn = functions.get(tag)!;
|
|
151
151
|
contextMap.set(fn.key, { handler, context });
|
|
152
152
|
}
|
|
153
|
-
return Context.
|
|
153
|
+
return Context.makeUnsafe(contextMap);
|
|
154
154
|
}),
|
|
155
155
|
);
|
|
156
156
|
},
|
|
@@ -162,7 +162,7 @@ const Proto = {
|
|
|
162
162
|
const context = yield* Effect.context<never>();
|
|
163
163
|
const contextMap = new Map<string, unknown>();
|
|
164
164
|
contextMap.set(fn.key, { handler, context });
|
|
165
|
-
return Context.
|
|
165
|
+
return Context.makeUnsafe(contextMap);
|
|
166
166
|
}),
|
|
167
167
|
);
|
|
168
168
|
},
|
|
@@ -199,8 +199,8 @@ export const make = <Fns extends ReadonlyArray<InngestFunction.Any>>(...fns: Fns
|
|
|
199
199
|
* )
|
|
200
200
|
* ```
|
|
201
201
|
*/
|
|
202
|
-
export const toHttpApp = (
|
|
203
|
-
|
|
202
|
+
export const toHttpApp = Effect.fn("InngestGroup.toHttpApp")(
|
|
203
|
+
function* (group: InngestGroup.Any) {
|
|
204
204
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
205
205
|
const method = request.method;
|
|
206
206
|
const requestUrl = Option.match(HttpServerRequest.toURL(request), {
|
|
@@ -225,17 +225,30 @@ export const toHttpApp = (group: InngestGroup.Any): HttpApp.Default<never, Innge
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
if (method === "POST") {
|
|
228
|
-
const url = Option.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
const url = yield* Option.match(HttpServerRequest.toURL(request), {
|
|
229
|
+
onNone: () =>
|
|
230
|
+
Effect.fail(
|
|
231
|
+
HttpServerResponse.jsonUnsafe(
|
|
232
|
+
{ error: "Unable to parse request URL" },
|
|
233
|
+
{ status: 400, headers: { [Protocol.Headers.NoRetry]: "true" } },
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
onSome: (u) => Effect.succeed(u),
|
|
233
237
|
});
|
|
234
238
|
|
|
235
|
-
const
|
|
236
|
-
|
|
239
|
+
const ExecuteParamsSchema = UrlParams.schemaRecord.pipe(
|
|
240
|
+
Schema.decodeTo(
|
|
241
|
+
Schema.Struct({
|
|
242
|
+
fnId: Schema.String,
|
|
243
|
+
stepId: Schema.optional(Schema.String),
|
|
244
|
+
}),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const params = yield* Schema.decodeUnknownEffect(ExecuteParamsSchema)(UrlParams.fromInput(url.searchParams)).pipe(
|
|
249
|
+
Effect.catch(() =>
|
|
237
250
|
Effect.fail(
|
|
238
|
-
HttpServerResponse.
|
|
251
|
+
HttpServerResponse.jsonUnsafe(
|
|
239
252
|
{ error: "Missing or invalid fnId query parameter" },
|
|
240
253
|
{ status: 400, headers: { [Protocol.Headers.NoRetry]: "true" } },
|
|
241
254
|
),
|
|
@@ -245,9 +258,9 @@ export const toHttpApp = (group: InngestGroup.Any): HttpApp.Default<never, Innge
|
|
|
245
258
|
|
|
246
259
|
const body = yield* InternalHandler.verifyAndParseRequestBody(request).pipe(
|
|
247
260
|
Effect.provide(SignatureLive),
|
|
248
|
-
Effect.
|
|
261
|
+
Effect.catch((error) =>
|
|
249
262
|
Effect.fail(
|
|
250
|
-
HttpServerResponse.
|
|
263
|
+
HttpServerResponse.jsonUnsafe(
|
|
251
264
|
{ error: error.message },
|
|
252
265
|
{
|
|
253
266
|
status: error._tag === "SignatureError" ? 401 : 400,
|
|
@@ -267,13 +280,13 @@ export const toHttpApp = (group: InngestGroup.Any): HttpApp.Default<never, Innge
|
|
|
267
280
|
}
|
|
268
281
|
|
|
269
282
|
return yield* HttpServerResponse.json({ error: `Method ${method} not allowed` }, { status: 405 });
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
),
|
|
283
|
+
},
|
|
284
|
+
Effect.catchCause((cause) =>
|
|
285
|
+
HttpServerResponse.json({ error: "Internal server error", cause: String(cause) }, { status: 500 }).pipe(
|
|
286
|
+
Effect.orDie,
|
|
275
287
|
),
|
|
276
|
-
)
|
|
288
|
+
),
|
|
289
|
+
);
|
|
277
290
|
|
|
278
291
|
/**
|
|
279
292
|
* Create a standalone web handler from an InngestGroup.
|
|
@@ -307,4 +320,4 @@ export const toWebHandler = <R, E>(
|
|
|
307
320
|
): {
|
|
308
321
|
readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>;
|
|
309
322
|
readonly dispose: () => Promise<void>;
|
|
310
|
-
} =>
|
|
323
|
+
} => HttpEffect.toWebHandlerLayer(toHttpApp(group), options.layer);
|
package/src/HttpApi.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 0.1.0
|
|
3
3
|
*/
|
|
4
|
-
import * as HttpApi from "
|
|
5
|
-
import * as HttpApiBuilder from "
|
|
6
|
-
import * as HttpApiEndpoint from "
|
|
7
|
-
import * as HttpApiGroup from "
|
|
8
|
-
import * as HttpClient from "
|
|
9
|
-
import * as HttpServerRequest from "
|
|
4
|
+
import * as HttpApi from "effect/unstable/httpapi/HttpApi";
|
|
5
|
+
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
|
|
6
|
+
import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
|
|
7
|
+
import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
|
|
8
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
9
|
+
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
11
|
import * as Layer from "effect/Layer";
|
|
12
12
|
import * as Option from "effect/Option";
|
|
@@ -22,25 +22,34 @@ const ExecuteParams = Schema.Struct({
|
|
|
22
22
|
stepId: Schema.optional(Schema.String),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
class FunctionNotFoundError extends Schema.
|
|
25
|
+
class FunctionNotFoundError extends Schema.TaggedErrorClass<FunctionNotFoundError>()("FunctionNotFoundError", {
|
|
26
26
|
message: Schema.String,
|
|
27
27
|
}) {}
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
29
|
+
const CommonErrors = [FunctionNotFoundError, InternalHandler.InvalidRequestError, InternalHandler.SignatureError];
|
|
30
|
+
|
|
31
|
+
const IntrospectEndpoint = HttpApiEndpoint.get("introspect", "/", {
|
|
32
|
+
success: Protocol.IntrospectionResponse,
|
|
33
|
+
error: CommonErrors,
|
|
34
|
+
});
|
|
35
|
+
const RegisterEndpoint = HttpApiEndpoint.put("register", "/", {
|
|
36
|
+
success: Protocol.RegisterResponse,
|
|
37
|
+
error: CommonErrors,
|
|
38
|
+
});
|
|
39
|
+
const ExecuteEndpoint = HttpApiEndpoint.post("execute", "/", {
|
|
40
|
+
query: ExecuteParams,
|
|
41
|
+
success: Schema.Unknown,
|
|
42
|
+
error: CommonErrors,
|
|
43
|
+
});
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
46
|
* @since 0.1.0
|
|
35
47
|
* @category api
|
|
36
48
|
*/
|
|
37
|
-
export
|
|
49
|
+
export class InngestApiGroup extends HttpApiGroup.make("inngest")
|
|
38
50
|
.add(IntrospectEndpoint)
|
|
39
51
|
.add(RegisterEndpoint)
|
|
40
|
-
.add(ExecuteEndpoint)
|
|
41
|
-
.addError(FunctionNotFoundError, { status: 404 })
|
|
42
|
-
.addError(InternalHandler.InvalidRequestError, { status: 400 })
|
|
43
|
-
.addError(InternalHandler.SignatureError, { status: 401 });
|
|
52
|
+
.add(ExecuteEndpoint) {}
|
|
44
53
|
|
|
45
54
|
type InngestApiGroupType = typeof InngestApiGroup;
|
|
46
55
|
|
|
@@ -48,8 +57,8 @@ type InngestApiGroupType = typeof InngestApiGroup;
|
|
|
48
57
|
* @since 0.1.0
|
|
49
58
|
* @category layers
|
|
50
59
|
*/
|
|
51
|
-
export const layerGroup = <ApiId extends string, Groups extends HttpApiGroup.
|
|
52
|
-
api: HttpApi.HttpApi<ApiId, Groups
|
|
60
|
+
export const layerGroup = <ApiId extends string, Groups extends HttpApiGroup.Any>(
|
|
61
|
+
api: HttpApi.HttpApi<ApiId, Groups>,
|
|
53
62
|
group: InngestGroup.Any,
|
|
54
63
|
): Layer.Layer<HttpApiGroup.ApiGroup<ApiId, "inngest">, never, InngestClient | HttpClient.HttpClient> => {
|
|
55
64
|
const toUrl = (req: HttpServerRequest.HttpServerRequest) =>
|
|
@@ -59,24 +68,26 @@ export const layerGroup = <ApiId extends string, Groups extends HttpApiGroup.Htt
|
|
|
59
68
|
});
|
|
60
69
|
|
|
61
70
|
return HttpApiBuilder.group(
|
|
62
|
-
api as unknown as HttpApi.HttpApi<ApiId, InngestApiGroupType
|
|
71
|
+
api as unknown as HttpApi.HttpApi<ApiId, InngestApiGroupType>,
|
|
63
72
|
"inngest",
|
|
64
|
-
(handlers)
|
|
65
|
-
handlers
|
|
73
|
+
Effect.fn(function* (handlers) {
|
|
74
|
+
return handlers
|
|
66
75
|
.handle("introspect", ({ request }) =>
|
|
67
76
|
InternalHandler.handleIntrospection(group, toUrl(request)).pipe(Effect.map((r) => r.body)),
|
|
68
77
|
)
|
|
69
78
|
.handle("register", ({ request }) =>
|
|
70
79
|
InternalHandler.handleRegistration(group, toUrl(request)).pipe(Effect.map((r) => r.body)),
|
|
71
80
|
)
|
|
72
|
-
.handleRaw("execute", ({
|
|
81
|
+
.handleRaw("execute", ({ query, request }) =>
|
|
73
82
|
InternalHandler.verifyAndParseRequestBody(request).pipe(
|
|
74
|
-
Effect.
|
|
75
|
-
Effect.flatMap((payload) =>
|
|
76
|
-
InternalHandler.handleExecution(group, urlParams.fnId, urlParams.stepId, payload),
|
|
77
|
-
),
|
|
83
|
+
Effect.flatMap((payload) => InternalHandler.handleExecution(group, query.fnId, query.stepId, payload)),
|
|
78
84
|
Effect.map((r) => r.body),
|
|
79
85
|
),
|
|
80
|
-
)
|
|
81
|
-
|
|
86
|
+
);
|
|
87
|
+
}),
|
|
88
|
+
).pipe(Layer.provide(SignatureLive)) as unknown as Layer.Layer<
|
|
89
|
+
HttpApiGroup.ApiGroup<ApiId, "inngest">,
|
|
90
|
+
never,
|
|
91
|
+
InngestClient | HttpClient.HttpClient
|
|
92
|
+
>;
|
|
82
93
|
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint configuration + per-execution state for spec §10 async
|
|
3
|
+
* checkpointing.
|
|
4
|
+
*
|
|
5
|
+
* Sync checkpointing (§10.3.2 / §10.4.2) is intentionally out of scope —
|
|
6
|
+
* it requires a durable endpoint primitive the SDK does not yet expose.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
import * as Clock from "effect/Clock";
|
|
11
|
+
import * as Duration from "effect/Duration";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import * as Ref from "effect/Ref";
|
|
15
|
+
import * as Result from "effect/Result";
|
|
16
|
+
import * as Schema from "effect/Schema";
|
|
17
|
+
import { timeStr } from "./helpers.js";
|
|
18
|
+
import type * as Protocol from "./protocol.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tagged error returned by `InngestClient.checkpointAsync` when the API call
|
|
22
|
+
* fails (network error or non-2xx after retries). The driver and step tools
|
|
23
|
+
* treat this as a graceful-fallback signal — buffered steps are restored to
|
|
24
|
+
* the buffer so they get included in the final 206 response.
|
|
25
|
+
*/
|
|
26
|
+
export class CheckpointApiError extends Schema.TaggedErrorClass<CheckpointApiError>()("CheckpointApiError", {
|
|
27
|
+
message: Schema.String,
|
|
28
|
+
status: Schema.optionalKey(Schema.Number),
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* User-facing checkpointing option, accepted on `ClientConfig.checkpointing`
|
|
33
|
+
* and `FunctionOptions.checkpointing`.
|
|
34
|
+
*
|
|
35
|
+
* - `false` disables checkpointing entirely (no API calls; classic 206-per-step).
|
|
36
|
+
* - `true` enables checkpointing with the safe defaults
|
|
37
|
+
* (`bufferedSteps: 1`, `maxInterval: 0`, `maxRuntime: 10 seconds`).
|
|
38
|
+
* - An object lets you tune `bufferedSteps`, `maxInterval`, `maxRuntime`.
|
|
39
|
+
*/
|
|
40
|
+
export type CheckpointingOption =
|
|
41
|
+
| boolean
|
|
42
|
+
| {
|
|
43
|
+
readonly bufferedSteps?: number;
|
|
44
|
+
readonly maxInterval?: Duration.Input;
|
|
45
|
+
readonly maxRuntime?: Duration.Input;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolved internal config — defaults applied, durations normalized.
|
|
50
|
+
*/
|
|
51
|
+
export interface CheckpointConfig {
|
|
52
|
+
readonly bufferedSteps: number;
|
|
53
|
+
readonly maxInterval: Duration.Duration;
|
|
54
|
+
readonly maxRuntime: Duration.Duration;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULTS: CheckpointConfig = {
|
|
58
|
+
bufferedSteps: 1,
|
|
59
|
+
maxInterval: Duration.millis(0),
|
|
60
|
+
maxRuntime: Duration.seconds(10),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const normalize = (option: Exclude<CheckpointingOption, boolean>): CheckpointConfig => ({
|
|
64
|
+
bufferedSteps: option.bufferedSteps ?? DEFAULTS.bufferedSteps,
|
|
65
|
+
maxInterval: option.maxInterval !== undefined ? Duration.fromInputUnsafe(option.maxInterval) : DEFAULTS.maxInterval,
|
|
66
|
+
maxRuntime: option.maxRuntime !== undefined ? Duration.fromInputUnsafe(option.maxRuntime) : DEFAULTS.maxRuntime,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve final config from function-level + client-level settings.
|
|
71
|
+
*
|
|
72
|
+
* Precedence: function-level explicit > client-level explicit > built-in
|
|
73
|
+
* defaults (default-ON, matching the TS reference SDK).
|
|
74
|
+
*
|
|
75
|
+
* Returns `undefined` if either level explicitly disables checkpointing.
|
|
76
|
+
*/
|
|
77
|
+
export const resolveConfig = (
|
|
78
|
+
fnLevel: CheckpointingOption | undefined,
|
|
79
|
+
clientLevel: CheckpointingOption | undefined,
|
|
80
|
+
): CheckpointConfig | undefined => {
|
|
81
|
+
if (fnLevel === false) return undefined;
|
|
82
|
+
// Per the `CheckpointingOption` docstring, `true` means "enable with safe
|
|
83
|
+
// defaults" — it never inherits a client-level object. Explicit object at
|
|
84
|
+
// the fn level overrides everything; otherwise fall through to client.
|
|
85
|
+
if (fnLevel === true) return DEFAULTS;
|
|
86
|
+
if (fnLevel !== undefined) return normalize(fnLevel);
|
|
87
|
+
// fnLevel === undefined — use client-level
|
|
88
|
+
if (clientLevel === false) return undefined;
|
|
89
|
+
if (clientLevel === true) return DEFAULTS;
|
|
90
|
+
if (clientLevel !== undefined) return normalize(clientLevel);
|
|
91
|
+
// Default-ON: undefined at both levels
|
|
92
|
+
return DEFAULTS;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Registration payload fragment per spec §10.1.1.
|
|
97
|
+
*/
|
|
98
|
+
export interface RegistrationFragment {
|
|
99
|
+
readonly batch_steps: number;
|
|
100
|
+
readonly batch_interval: string;
|
|
101
|
+
readonly max_runtime: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const toRegistration = (cfg: CheckpointConfig): RegistrationFragment => ({
|
|
105
|
+
batch_steps: cfg.bufferedSteps,
|
|
106
|
+
batch_interval: timeStr(cfg.maxInterval),
|
|
107
|
+
max_runtime: timeStr(cfg.maxRuntime),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Per-execution state for checkpoint mode. NOT a `Context.Service` — each
|
|
112
|
+
* `execute` call constructs its own and passes it directly to step tools and
|
|
113
|
+
* the driver.
|
|
114
|
+
*
|
|
115
|
+
* Internal `Ref`s are closure-private; only effectful ops are exposed. All
|
|
116
|
+
* ops are safe to sequence from a single fiber; the buffer/interval/runtime
|
|
117
|
+
* primitives are not designed for concurrent fibers (none are spawned today).
|
|
118
|
+
*/
|
|
119
|
+
export interface CheckpointState {
|
|
120
|
+
readonly config: CheckpointConfig;
|
|
121
|
+
readonly runId: string;
|
|
122
|
+
readonly fnId: string;
|
|
123
|
+
readonly qiId: string;
|
|
124
|
+
/** Append a sync opcode; flush if `bufferedSteps` or `maxInterval` reached. */
|
|
125
|
+
readonly bufferStep: (op: typeof Protocol.GeneratorOpcode.Type) => Effect.Effect<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Best-effort flush. On API error the buffered steps are re-prepended so
|
|
128
|
+
* `drain` can include them in the terminal 206 (no step loss per §10.4.3).
|
|
129
|
+
*/
|
|
130
|
+
readonly flush: Effect.Effect<void>;
|
|
131
|
+
/** Atomic snapshot + clear, for terminal response assembly. */
|
|
132
|
+
readonly drain: Effect.Effect<ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>>;
|
|
133
|
+
/** Signal that the handler's `maxRuntime` deadline fired (spec §10.4.1 #7). */
|
|
134
|
+
readonly markRuntimeExceeded: Effect.Effect<void>;
|
|
135
|
+
/** Query whether the `maxRuntime` deadline fired. */
|
|
136
|
+
readonly isRuntimeExceeded: Effect.Effect<boolean>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Construct a `CheckpointState`. Caller supplies a pre-bound `checkpointAsync`
|
|
141
|
+
* callback (typically `(steps) => client.checkpointAsync({runId, fnId, qiId, steps})`),
|
|
142
|
+
* so this module has no dependency on `InngestClient`.
|
|
143
|
+
*/
|
|
144
|
+
export const make = (args: {
|
|
145
|
+
readonly config: CheckpointConfig;
|
|
146
|
+
readonly runId: string;
|
|
147
|
+
readonly fnId: string;
|
|
148
|
+
readonly qiId: string;
|
|
149
|
+
readonly checkpointAsync: (
|
|
150
|
+
steps: ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>,
|
|
151
|
+
) => Effect.Effect<void, CheckpointApiError>;
|
|
152
|
+
}): Effect.Effect<CheckpointState> =>
|
|
153
|
+
Effect.sync(() => {
|
|
154
|
+
const buffer = Ref.makeUnsafe<ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>>([]);
|
|
155
|
+
const intervalStartedAt = Ref.makeUnsafe<Option.Option<number>>(Option.none());
|
|
156
|
+
const runtimeExceeded = Ref.makeUnsafe(false);
|
|
157
|
+
|
|
158
|
+
const maxIntervalMs = Duration.toMillis(args.config.maxInterval);
|
|
159
|
+
|
|
160
|
+
// Extract-then-restore is NOT atomic across multiple fibers; a concurrent
|
|
161
|
+
// `bufferStep` between the `Ref.modify` snapshot and the error-path
|
|
162
|
+
// `Ref.update` restore would reorder opcodes. Request-scoped execution
|
|
163
|
+
// today only runs a single fiber, so this is safe. If parallel fibers
|
|
164
|
+
// are ever added here, guard `flushInner` with a `Semaphore(1)`.
|
|
165
|
+
const flushInner: Effect.Effect<void> = Effect.gen(function* () {
|
|
166
|
+
const steps = yield* Ref.modify(buffer, (current) => [
|
|
167
|
+
current,
|
|
168
|
+
[] as ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>,
|
|
169
|
+
]);
|
|
170
|
+
if (steps.length === 0) return;
|
|
171
|
+
const result = yield* Effect.result(args.checkpointAsync(steps));
|
|
172
|
+
if (Result.isFailure(result)) {
|
|
173
|
+
// Restore at the head so subsequent `drain` includes them in the 206.
|
|
174
|
+
yield* Ref.update(buffer, (current) => [...steps, ...current]);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
yield* Ref.set(intervalStartedAt, Option.none());
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const bufferStep = (op: typeof Protocol.GeneratorOpcode.Type): Effect.Effect<void> =>
|
|
181
|
+
Effect.gen(function* () {
|
|
182
|
+
const len = yield* Ref.modify(buffer, (current) => {
|
|
183
|
+
const next = [...current, op];
|
|
184
|
+
return [next.length, next] as const;
|
|
185
|
+
});
|
|
186
|
+
const now = yield* Clock.currentTimeMillis;
|
|
187
|
+
const start = yield* Ref.get(intervalStartedAt);
|
|
188
|
+
|
|
189
|
+
if (len >= args.config.bufferedSteps) {
|
|
190
|
+
yield* flushInner;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (Option.isSome(start) && maxIntervalMs > 0 && now - start.value >= maxIntervalMs) {
|
|
194
|
+
yield* flushInner;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (Option.isNone(start) && maxIntervalMs > 0) {
|
|
198
|
+
yield* Ref.set(intervalStartedAt, Option.some(now));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const drain: Effect.Effect<ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>> = Ref.modify(buffer, (current) => [
|
|
203
|
+
current,
|
|
204
|
+
[] as ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
config: args.config,
|
|
209
|
+
runId: args.runId,
|
|
210
|
+
fnId: args.fnId,
|
|
211
|
+
qiId: args.qiId,
|
|
212
|
+
bufferStep,
|
|
213
|
+
flush: flushInner,
|
|
214
|
+
drain,
|
|
215
|
+
markRuntimeExceeded: Ref.set(runtimeExceeded, true),
|
|
216
|
+
isRuntimeExceeded: Ref.get(runtimeExceeded),
|
|
217
|
+
};
|
|
218
|
+
});
|