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,183 @@
|
|
|
1
|
+
import { formatTimestamp, timeStr } from "./helpers.js";
|
|
2
|
+
import { InngestClient } from "../Client.js";
|
|
3
|
+
import { SendEventError, StepError, isNonRetriableError, isRetryAfterError } from "./errors.js";
|
|
4
|
+
import { StepInterrupt, errorInterrupt, invokeInterrupt, plannedInterrupt, runInterrupt, sleepInterrupt, waitForEventInterrupt } from "./interrupts.js";
|
|
5
|
+
import { OtelAttributes } from "./constants.js";
|
|
6
|
+
import { decodeMemo } from "./memo.js";
|
|
7
|
+
import * as Duration from "effect/Duration";
|
|
8
|
+
import * as Context from "effect/Context";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Option from "effect/Option";
|
|
11
|
+
import "effect/Schema";
|
|
12
|
+
import * as Predicate from "effect/Predicate";
|
|
13
|
+
import * as Arr from "effect/Array";
|
|
14
|
+
import * as HashMap from "effect/HashMap";
|
|
15
|
+
import * as Match from "effect/Match";
|
|
16
|
+
import * as Ref from "effect/Ref";
|
|
17
|
+
import { pipe } from "effect/Function";
|
|
18
|
+
|
|
19
|
+
//#region src/internal/step.ts
|
|
20
|
+
/**
|
|
21
|
+
* Internal step tools implementation.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
/** @internal */
|
|
25
|
+
const hashStepId = (id, repeatIndex = 0) => {
|
|
26
|
+
const effectiveId = repeatIndex > 0 ? `${id}:${repeatIndex}` : id;
|
|
27
|
+
return Effect.map(Effect.promise(() => globalThis.crypto.subtle.digest("SHA-1", new TextEncoder().encode(effectiveId))), (buffer) => new Uint8Array(buffer).toHex());
|
|
28
|
+
};
|
|
29
|
+
const errorOtelAttributes = (err) => {
|
|
30
|
+
const attrs = {};
|
|
31
|
+
if (err instanceof Error) {
|
|
32
|
+
attrs[OtelAttributes.ExceptionType] = err.name;
|
|
33
|
+
attrs[OtelAttributes.ExceptionMessage] = err.message;
|
|
34
|
+
if (err.stack) attrs[OtelAttributes.ExceptionStacktrace] = err.stack;
|
|
35
|
+
} else if (Predicate.hasProperty(err, "_tag") && typeof err._tag === "string") {
|
|
36
|
+
attrs[OtelAttributes.ExceptionType] = err._tag;
|
|
37
|
+
if (Predicate.hasProperty(err, "message") && typeof err.message === "string") attrs[OtelAttributes.ExceptionMessage] = err.message;
|
|
38
|
+
} else attrs[OtelAttributes.ExceptionMessage] = String(err);
|
|
39
|
+
return attrs;
|
|
40
|
+
};
|
|
41
|
+
var Step = class extends Context.Tag("effect-inngest/Step")() {};
|
|
42
|
+
const normalizeOpts = (opts) => typeof opts === "string" ? {
|
|
43
|
+
id: opts,
|
|
44
|
+
name: opts
|
|
45
|
+
} : {
|
|
46
|
+
id: opts.id,
|
|
47
|
+
name: opts.name ?? opts.id
|
|
48
|
+
};
|
|
49
|
+
const stepError = (stepId, message, opts) => Effect.fail(StepError.make({
|
|
50
|
+
stepId,
|
|
51
|
+
message,
|
|
52
|
+
noRetry: opts?.noRetry,
|
|
53
|
+
cause: opts?.cause
|
|
54
|
+
}));
|
|
55
|
+
const createStepTools = (request, appName, stepIdCounts) => {
|
|
56
|
+
const ctx = request.ctx;
|
|
57
|
+
const steps = request.steps;
|
|
58
|
+
const getInfo = (opts) => {
|
|
59
|
+
const { id, name } = normalizeOpts(opts);
|
|
60
|
+
return Effect.flatMap(Ref.modify(stepIdCounts, (map) => {
|
|
61
|
+
const count = Option.getOrElse(HashMap.get(map, id), () => 0);
|
|
62
|
+
return [count, HashMap.set(map, id, count + 1)];
|
|
63
|
+
}), (count) => Effect.map(hashStepId(id, count), (hash) => ({
|
|
64
|
+
id,
|
|
65
|
+
name,
|
|
66
|
+
hash
|
|
67
|
+
})));
|
|
68
|
+
};
|
|
69
|
+
const getMemo = (hash) => decodeMemo(steps[hash]);
|
|
70
|
+
const canExecute = (hash) => ctx.step_id === hash || ctx.step_id === "step";
|
|
71
|
+
const isBlocked = (hash) => ctx.disable_immediate_execution && ctx.step_id !== hash;
|
|
72
|
+
const sleep = (opts, duration) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", () => Effect.void), Match.tag("MemoTimeout", () => Effect.void), Match.tag("MemoError", () => Effect.void), Match.tag("MemoInput", () => Effect.void), Match.tag("MemoNone", () => Effect.fail(sleepInterrupt({
|
|
73
|
+
info,
|
|
74
|
+
duration: timeStr(duration)
|
|
75
|
+
}))), Match.exhaustive));
|
|
76
|
+
const sleepUntil = (opts, timestamp) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", () => Effect.void), Match.tag("MemoTimeout", () => Effect.void), Match.tag("MemoError", () => Effect.void), Match.tag("MemoInput", () => Effect.void), Match.tag("MemoNone", () => Effect.fail(sleepInterrupt({
|
|
77
|
+
info,
|
|
78
|
+
duration: formatTimestamp(timestamp)
|
|
79
|
+
}))), Match.exhaustive));
|
|
80
|
+
const waitForEvent = (opts, event, options) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => {
|
|
81
|
+
if (data == null) return Effect.succeed(Option.none());
|
|
82
|
+
const event = data;
|
|
83
|
+
const payload = event.data !== void 0 ? event.data : data;
|
|
84
|
+
if (payload == null) return Effect.succeed(Option.none());
|
|
85
|
+
return Effect.succeed(Option.some(payload));
|
|
86
|
+
}), Match.tag("MemoTimeout", () => Effect.succeed(Option.none())), Match.tag("MemoError", () => Effect.succeed(Option.none())), Match.tag("MemoInput", () => Effect.succeed(Option.none())), Match.tag("MemoNone", () => Effect.fail(waitForEventInterrupt({
|
|
87
|
+
info,
|
|
88
|
+
event: event._tag,
|
|
89
|
+
timeout: timeStr(options.timeout),
|
|
90
|
+
if: options.if
|
|
91
|
+
}))), Match.exhaustive));
|
|
92
|
+
const invoke = (opts, options) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Invoke failed", { cause: error })), Match.tag("MemoTimeout", () => stepError(info.id, "Invoke timed out", { noRetry: true })), Match.tag("MemoInput", () => Effect.succeed(void 0)), Match.tag("MemoNone", () => Effect.fail(invokeInterrupt({
|
|
93
|
+
info,
|
|
94
|
+
functionId: `${appName}-${options.function._tag}`,
|
|
95
|
+
payload: {
|
|
96
|
+
data: Predicate.hasProperty(options, "data") ? options.data : void 0,
|
|
97
|
+
user: options.user ?? {},
|
|
98
|
+
v: options.v ?? "1"
|
|
99
|
+
},
|
|
100
|
+
timeout: options.timeout ? timeStr(options.timeout) : "365d"
|
|
101
|
+
}))), Match.exhaustive));
|
|
102
|
+
const run = (opts, effect) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Step failed", { noRetry: true })), Match.tag("MemoTimeout", () => stepError(info.id, "Step timed out", { noRetry: true })), Match.tag("MemoInput", () => stepError(info.id, "Unexpected step result type: input")), Match.tag("MemoNone", () => {
|
|
103
|
+
if (isBlocked(info.hash) || !canExecute(info.hash)) return Effect.fail(plannedInterrupt({ info }));
|
|
104
|
+
return effect.pipe(Effect.withSpan(`inngest.step/run/${info.id}`, { attributes: {
|
|
105
|
+
[OtelAttributes.StepId]: info.id,
|
|
106
|
+
[OtelAttributes.StepType]: "run"
|
|
107
|
+
} }), Effect.matchEffect({
|
|
108
|
+
onFailure: (err) => {
|
|
109
|
+
const noRetry = isNonRetriableError(err) ? true : void 0;
|
|
110
|
+
const retryAfterMs = isRetryAfterError(err) ? Duration.toMillis(err.retryAfter) : void 0;
|
|
111
|
+
return Effect.zipRight(Effect.annotateCurrentSpan(errorOtelAttributes(err)), Effect.fail(errorInterrupt({
|
|
112
|
+
info,
|
|
113
|
+
error: err,
|
|
114
|
+
noRetry,
|
|
115
|
+
retryAfterMs
|
|
116
|
+
})));
|
|
117
|
+
},
|
|
118
|
+
onSuccess: (data) => Effect.fail(runInterrupt({
|
|
119
|
+
info,
|
|
120
|
+
data
|
|
121
|
+
}))
|
|
122
|
+
}), Effect.catchAllDefect((defect) => Effect.zipRight(Effect.annotateCurrentSpan(errorOtelAttributes(defect)), Effect.fail(errorInterrupt({
|
|
123
|
+
info,
|
|
124
|
+
error: defect
|
|
125
|
+
})))));
|
|
126
|
+
}), Match.exhaustive));
|
|
127
|
+
const sendEvent = (opts, payload) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", () => Effect.fail(SendEventError.make({
|
|
128
|
+
message: "SendEvent failed",
|
|
129
|
+
events: []
|
|
130
|
+
}))), Match.tag("MemoTimeout", () => Effect.fail(SendEventError.make({
|
|
131
|
+
message: "SendEvent timed out",
|
|
132
|
+
events: []
|
|
133
|
+
}))), Match.tag("MemoInput", () => Effect.succeed({ ids: [] })), Match.tag("MemoNone", () => {
|
|
134
|
+
if (isBlocked(info.hash) || !canExecute(info.hash)) return Effect.fail(plannedInterrupt({ info }));
|
|
135
|
+
const eventPayloads = Arr.map(Arr.ensure(payload), (e) => ({
|
|
136
|
+
name: e._tag,
|
|
137
|
+
data: e
|
|
138
|
+
}));
|
|
139
|
+
return Effect.flatMap(InngestClient, (client) => client.sendEvent(eventPayloads).pipe(Effect.withSpan(`inngest.step/sendEvent/${info.id}`, { attributes: {
|
|
140
|
+
[OtelAttributes.StepId]: info.id,
|
|
141
|
+
[OtelAttributes.StepType]: "sendEvent"
|
|
142
|
+
} }), Effect.flatMap((result) => Effect.fail(runInterrupt({
|
|
143
|
+
info,
|
|
144
|
+
data: result
|
|
145
|
+
})))));
|
|
146
|
+
}), Match.exhaustive));
|
|
147
|
+
return {
|
|
148
|
+
run,
|
|
149
|
+
sleep,
|
|
150
|
+
sleepUntil,
|
|
151
|
+
waitForEvent,
|
|
152
|
+
invoke,
|
|
153
|
+
sendEvent
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const buildHandlerContext = (fn, step, request) => {
|
|
157
|
+
if (fn.options?.batchEvents != null) return {
|
|
158
|
+
event: request.events.map((e) => e.data),
|
|
159
|
+
step,
|
|
160
|
+
run: {
|
|
161
|
+
id: request.ctx.run_id,
|
|
162
|
+
attempt: request.ctx.attempt,
|
|
163
|
+
maxAttempts: request.ctx.max_attempts
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
let eventData = request.event.data;
|
|
167
|
+
if (request.event.name === "inngest/function.invoked" && typeof eventData === "object" && eventData !== null) {
|
|
168
|
+
const { _inngest, ...payload } = eventData;
|
|
169
|
+
eventData = payload;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
event: eventData,
|
|
173
|
+
step,
|
|
174
|
+
run: {
|
|
175
|
+
id: request.ctx.run_id,
|
|
176
|
+
attempt: request.ctx.attempt,
|
|
177
|
+
maxAttempts: request.ctx.max_attempts
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
export { Step, buildHandlerContext, createStepTools };
|
package/package.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "effect-inngest",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native Effect client library for Inngest - build durable, type-safe workflows",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"background-jobs",
|
|
7
|
+
"durable",
|
|
8
|
+
"effect",
|
|
9
|
+
"event-driven",
|
|
10
|
+
"inngest",
|
|
11
|
+
"queue",
|
|
12
|
+
"serverless",
|
|
13
|
+
"typescript",
|
|
14
|
+
"workflow"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/erikshestopal/effect-inngest#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/erikshestopal/effect-inngest/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Erik Shestopal",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/erikshestopal/effect-inngest.git"
|
|
25
|
+
},
|
|
26
|
+
"workspaces": [
|
|
27
|
+
"examples"
|
|
28
|
+
],
|
|
29
|
+
"files": [
|
|
30
|
+
"dist/**/*.d.ts",
|
|
31
|
+
"dist/**/*.d.ts.map",
|
|
32
|
+
"dist/**/*.js",
|
|
33
|
+
"dist/**/*.js.map",
|
|
34
|
+
"src/**/*.ts"
|
|
35
|
+
],
|
|
36
|
+
"type": "module",
|
|
37
|
+
"sideEffects": [],
|
|
38
|
+
"exports": {
|
|
39
|
+
"./package.json": "./package.json",
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./src/index.ts"
|
|
43
|
+
},
|
|
44
|
+
"./Client": {
|
|
45
|
+
"types": "./dist/Client.d.ts",
|
|
46
|
+
"import": "./src/Client.ts"
|
|
47
|
+
},
|
|
48
|
+
"./Events": {
|
|
49
|
+
"types": "./dist/Events.d.ts",
|
|
50
|
+
"import": "./src/Events.ts"
|
|
51
|
+
},
|
|
52
|
+
"./Function": {
|
|
53
|
+
"types": "./dist/Function.d.ts",
|
|
54
|
+
"import": "./src/Function.ts"
|
|
55
|
+
},
|
|
56
|
+
"./Group": {
|
|
57
|
+
"types": "./dist/Group.d.ts",
|
|
58
|
+
"import": "./src/Group.ts"
|
|
59
|
+
},
|
|
60
|
+
"./HttpApi": {
|
|
61
|
+
"types": "./dist/HttpApi.d.ts",
|
|
62
|
+
"import": "./src/HttpApi.ts"
|
|
63
|
+
},
|
|
64
|
+
"./internal/*": null
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"exports": {
|
|
68
|
+
"./package.json": "./package.json",
|
|
69
|
+
".": {
|
|
70
|
+
"types": "./dist/index.d.ts",
|
|
71
|
+
"import": "./dist/index.js"
|
|
72
|
+
},
|
|
73
|
+
"./Client": {
|
|
74
|
+
"types": "./dist/Client.d.ts",
|
|
75
|
+
"import": "./dist/Client.js"
|
|
76
|
+
},
|
|
77
|
+
"./Events": {
|
|
78
|
+
"types": "./dist/Events.d.ts",
|
|
79
|
+
"import": "./dist/Events.js"
|
|
80
|
+
},
|
|
81
|
+
"./Function": {
|
|
82
|
+
"types": "./dist/Function.d.ts",
|
|
83
|
+
"import": "./dist/Function.js"
|
|
84
|
+
},
|
|
85
|
+
"./Group": {
|
|
86
|
+
"types": "./dist/Group.d.ts",
|
|
87
|
+
"import": "./dist/Group.js"
|
|
88
|
+
},
|
|
89
|
+
"./HttpApi": {
|
|
90
|
+
"types": "./dist/HttpApi.d.ts",
|
|
91
|
+
"import": "./dist/HttpApi.js"
|
|
92
|
+
},
|
|
93
|
+
"./internal/*": null
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"scripts": {
|
|
97
|
+
"build": "tsdown",
|
|
98
|
+
"format": "oxfmt",
|
|
99
|
+
"lint": "oxlint --fix --type-aware --type-check --promise-plugin",
|
|
100
|
+
"typecheck": "tsgo --noEmit",
|
|
101
|
+
"prepare": "effect-language-service patch",
|
|
102
|
+
"changeset": "changeset",
|
|
103
|
+
"changeset:version": "changeset version",
|
|
104
|
+
"changeset:publish": "bun run build && changeset publish"
|
|
105
|
+
},
|
|
106
|
+
"devDependencies": {
|
|
107
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
108
|
+
"@changesets/cli": "^2.29.8",
|
|
109
|
+
"@effect/language-service": "0.72.0",
|
|
110
|
+
"@types/bun": "1.3.6",
|
|
111
|
+
"@typescript/native-preview": "^7.0.0-dev.20260124.1",
|
|
112
|
+
"oxfmt": "0.26.0",
|
|
113
|
+
"oxlint": "1.41.0",
|
|
114
|
+
"oxlint-tsgolint": "0.11.1",
|
|
115
|
+
"tsdown": "0.20.1"
|
|
116
|
+
},
|
|
117
|
+
"peerDependencies": {
|
|
118
|
+
"@effect/platform": ">=0.80.0",
|
|
119
|
+
"effect": ">=3.14.0"
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/Client.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 0.1.0
|
|
3
|
+
*/
|
|
4
|
+
import * as HttpClient from "@effect/platform/HttpClient";
|
|
5
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
|
|
6
|
+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
|
|
7
|
+
import * as Config from "effect/Config";
|
|
8
|
+
import type * as ConfigError from "effect/ConfigError";
|
|
9
|
+
import * as Context from "effect/Context";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Layer from "effect/Layer";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
import * as Predicate from "effect/Predicate";
|
|
14
|
+
import * as Schema from "effect/Schema";
|
|
15
|
+
import * as Protocol from "./internal/protocol.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
* @category type ids
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
const TypeId: unique symbol = Symbol.for("effect-inngest/Client");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
* @category type ids
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
type TypeId = typeof TypeId;
|
|
30
|
+
|
|
31
|
+
const DEFAULT_EVENT_BASE_URL = "https://inn.gs/";
|
|
32
|
+
const DEFAULT_API_BASE_URL = "https://api.inngest.com/";
|
|
33
|
+
const DEFAULT_DEV_SERVER_URL = "http://localhost:8288/";
|
|
34
|
+
const SDK_VERSION = "2.0.0";
|
|
35
|
+
|
|
36
|
+
type ClientMode = "dev" | "cloud";
|
|
37
|
+
|
|
38
|
+
interface ClientConfig {
|
|
39
|
+
/**
|
|
40
|
+
* The ID of this instance, most commonly a reference to the application it
|
|
41
|
+
* resides in.
|
|
42
|
+
*
|
|
43
|
+
* The ID of your client should remain the same for its lifetime; if you'd
|
|
44
|
+
* like to change the name of your client as it appears in the Inngest UI,
|
|
45
|
+
* change the `name` property instead.
|
|
46
|
+
*/
|
|
47
|
+
readonly id: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Inngest event key, used to send events to Inngest Cloud. If not provided,
|
|
51
|
+
* will search for the `INNGEST_EVENT_KEY` environment variable. If neither
|
|
52
|
+
* can be found, a warning will be shown and any attempts to send events in
|
|
53
|
+
* cloud mode will throw an error.
|
|
54
|
+
*/
|
|
55
|
+
readonly eventKey?: string | undefined;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Inngest signing key, used for request signature verification.
|
|
59
|
+
* If not provided, will search for the `INNGEST_SIGNING_KEY` environment variable.
|
|
60
|
+
*/
|
|
61
|
+
readonly signingKey?: string | undefined;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fallback signing key for key rotation scenarios.
|
|
65
|
+
* If not provided, will search for the `INNGEST_SIGNING_KEY_FALLBACK` environment variable.
|
|
66
|
+
*/
|
|
67
|
+
readonly signingKeyFallback?: string | undefined;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The base URL for the Inngest API (registration, run fetching, etc.)
|
|
71
|
+
* Defaults to https://api.inngest.com/ in cloud mode.
|
|
72
|
+
*/
|
|
73
|
+
readonly apiBaseUrl?: string | undefined;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The base URL for sending events.
|
|
77
|
+
* Defaults to https://inn.gs/ in cloud mode.
|
|
78
|
+
*/
|
|
79
|
+
readonly eventBaseUrl?: string | undefined;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Can be used to explicitly set the client to Development Mode or Cloud Mode.
|
|
83
|
+
* If not set, mode is inferred from environment variables (INNGEST_DEV, NODE_ENV, etc.)
|
|
84
|
+
*
|
|
85
|
+
* Development mode will turn off signature verification and default to using
|
|
86
|
+
* a local URL to access a local Dev Server.
|
|
87
|
+
*/
|
|
88
|
+
readonly mode?: ClientMode | undefined;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The Inngest environment to send events to. Defaults to whichever
|
|
92
|
+
* environment this client's event key is associated with.
|
|
93
|
+
*
|
|
94
|
+
* It's likely you never need to change this unless you're trying to sync
|
|
95
|
+
* multiple systems together using branch names.
|
|
96
|
+
*/
|
|
97
|
+
readonly env?: string | undefined;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The application-specific version identifier. This can be an arbitrary value
|
|
101
|
+
* such as a version string, a Git commit SHA, or any other unique identifier.
|
|
102
|
+
*/
|
|
103
|
+
readonly appVersion?: string | undefined;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The host where this app is served. Used for registration.
|
|
107
|
+
*/
|
|
108
|
+
readonly serveHost?: string | undefined;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The path where the Inngest serve handler is mounted. Used for registration.
|
|
112
|
+
*/
|
|
113
|
+
readonly servePath?: string | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const EventPayload = Schema.Struct({
|
|
117
|
+
name: Schema.String,
|
|
118
|
+
data: Schema.Unknown,
|
|
119
|
+
ts: Schema.optional(Schema.Number),
|
|
120
|
+
id: Schema.optional(Schema.String),
|
|
121
|
+
v: Schema.optional(Schema.String),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const SendEventResponse = Schema.Struct({
|
|
125
|
+
ids: Schema.optionalWith(Schema.Array(Schema.String), { default: () => [] }),
|
|
126
|
+
status: Schema.optional(Schema.Number),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
type EventPayload = typeof EventPayload.Type;
|
|
130
|
+
type SendEventResponse = typeof SendEventResponse.Type;
|
|
131
|
+
|
|
132
|
+
class SendEventError extends Schema.TaggedError<SendEventError>()("SendEventError", {
|
|
133
|
+
message: Schema.String,
|
|
134
|
+
events: Schema.Array(Schema.String),
|
|
135
|
+
}) {}
|
|
136
|
+
|
|
137
|
+
interface InngestClientService {
|
|
138
|
+
readonly [TypeId]: TypeId;
|
|
139
|
+
readonly config: ClientConfig;
|
|
140
|
+
readonly mode: ClientMode;
|
|
141
|
+
readonly eventBaseUrl: string;
|
|
142
|
+
readonly apiBaseUrl: string;
|
|
143
|
+
readonly sendEvent: (events: ReadonlyArray<EventPayload>) => Effect.Effect<SendEventResponse, SendEventError>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* InngestClient service for communicating with Inngest.
|
|
148
|
+
*
|
|
149
|
+
* @since 0.1.0
|
|
150
|
+
* @category context
|
|
151
|
+
*/
|
|
152
|
+
export class InngestClient extends Context.Tag("effect-inngest/InngestClient")<InngestClient, InngestClientService>() {}
|
|
153
|
+
|
|
154
|
+
const resolveMode = (config: ClientConfig): ClientMode => config.mode ?? "dev";
|
|
155
|
+
|
|
156
|
+
const resolveEventBaseUrl = (config: ClientConfig, mode: ClientMode): string =>
|
|
157
|
+
config.eventBaseUrl ?? (mode === "dev" ? DEFAULT_DEV_SERVER_URL : DEFAULT_EVENT_BASE_URL);
|
|
158
|
+
|
|
159
|
+
const resolveApiBaseUrl = (config: ClientConfig, mode: ClientMode): string =>
|
|
160
|
+
config.apiBaseUrl ?? (mode === "dev" ? DEFAULT_DEV_SERVER_URL : DEFAULT_API_BASE_URL);
|
|
161
|
+
|
|
162
|
+
const makeClient = (config: ClientConfig, httpClient: HttpClient.HttpClient): InngestClientService => {
|
|
163
|
+
const mode = resolveMode(config);
|
|
164
|
+
const eventBaseUrl = resolveEventBaseUrl(config, mode);
|
|
165
|
+
const apiBaseUrl = resolveApiBaseUrl(config, mode);
|
|
166
|
+
|
|
167
|
+
const sendEvent = (events: ReadonlyArray<EventPayload>): Effect.Effect<SendEventResponse, SendEventError> => {
|
|
168
|
+
if (!config.eventKey && mode === "cloud") {
|
|
169
|
+
return new SendEventError({
|
|
170
|
+
message: "Event key is required to send events in cloud mode",
|
|
171
|
+
events: events.map((e) => e.name),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const key = config.eventKey ?? "local";
|
|
176
|
+
const url = new URL(`e/${key}`, eventBaseUrl).toString();
|
|
177
|
+
const eventNames = events.map((e) => e.name);
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
|
|
180
|
+
const payloads = events.map((e) => ({
|
|
181
|
+
name: e.name,
|
|
182
|
+
data: e.data ?? {},
|
|
183
|
+
ts: e.ts ?? now,
|
|
184
|
+
id: e.id,
|
|
185
|
+
v: e.v,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
const request = HttpClientRequest.post(url).pipe(
|
|
189
|
+
HttpClientRequest.setHeaders({
|
|
190
|
+
"Content-Type": "application/json",
|
|
191
|
+
[Protocol.Headers.SDK]: `effect-ts:v${SDK_VERSION}`,
|
|
192
|
+
...(config.env ? { [Protocol.Headers.Env]: config.env } : {}),
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return HttpClientRequest.schemaBodyJson(Schema.Array(EventPayload))(request, payloads).pipe(
|
|
197
|
+
Effect.flatMap(httpClient.execute),
|
|
198
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(SendEventResponse)),
|
|
199
|
+
Effect.map((response) => ({ ids: response.ids, status: response.status })),
|
|
200
|
+
Effect.scoped,
|
|
201
|
+
Effect.catchAll(
|
|
202
|
+
(error) =>
|
|
203
|
+
new SendEventError({
|
|
204
|
+
message: `Failed to send events: ${Predicate.hasProperty(error, "message") ? (error.message as string) : "Unknown error"}`,
|
|
205
|
+
events: eventNames,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
[TypeId]: TypeId,
|
|
213
|
+
config,
|
|
214
|
+
mode,
|
|
215
|
+
eventBaseUrl,
|
|
216
|
+
apiBaseUrl,
|
|
217
|
+
sendEvent,
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create an InngestClient layer from a config.
|
|
223
|
+
*
|
|
224
|
+
* @since 0.1.0
|
|
225
|
+
* @category layers
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* const ClientLive = InngestClient.layer({
|
|
229
|
+
* id: "my-app",
|
|
230
|
+
* eventKey: "my-event-key",
|
|
231
|
+
* })
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export const layer = (config: ClientConfig): Layer.Layer<InngestClient, never, HttpClient.HttpClient> =>
|
|
235
|
+
Layer.effect(
|
|
236
|
+
InngestClient,
|
|
237
|
+
Effect.map(HttpClient.HttpClient, (httpClient) => makeClient(config, httpClient)),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create an InngestClient layer from Effect Config.
|
|
242
|
+
*
|
|
243
|
+
* @since 0.1.0
|
|
244
|
+
* @category layers
|
|
245
|
+
*/
|
|
246
|
+
export const layerConfig = (
|
|
247
|
+
config: Config.Config.Wrap<ClientConfig>,
|
|
248
|
+
): Layer.Layer<InngestClient, ConfigError.ConfigError, HttpClient.HttpClient> =>
|
|
249
|
+
Layer.effect(
|
|
250
|
+
InngestClient,
|
|
251
|
+
Effect.flatMap(Config.unwrap(config), (resolvedConfig) =>
|
|
252
|
+
Effect.map(HttpClient.HttpClient, (httpClient) => makeClient(resolvedConfig, httpClient)),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create an InngestClient layer from environment variables.
|
|
258
|
+
*
|
|
259
|
+
* Uses:
|
|
260
|
+
* - INNGEST_APP_ID or "app" as id
|
|
261
|
+
* - INNGEST_EVENT_KEY for event key
|
|
262
|
+
* - INNGEST_SIGNING_KEY for signing key
|
|
263
|
+
*
|
|
264
|
+
* Mode is inferred from environment (INNGEST_DEV, NODE_ENV, etc.)
|
|
265
|
+
*
|
|
266
|
+
* @since 0.1.0
|
|
267
|
+
* @category layers
|
|
268
|
+
*/
|
|
269
|
+
export const layerFromEnv: Layer.Layer<InngestClient, ConfigError.ConfigError, HttpClient.HttpClient> = layerConfig(
|
|
270
|
+
Config.all({
|
|
271
|
+
id: Config.string("INNGEST_APP_ID").pipe(Config.withDefault("app")),
|
|
272
|
+
eventKey: Config.string("INNGEST_EVENT_KEY").pipe(Config.option, Config.map(Option.getOrUndefined)),
|
|
273
|
+
signingKey: Config.string("INNGEST_SIGNING_KEY").pipe(Config.option, Config.map(Option.getOrUndefined)),
|
|
274
|
+
signingKeyFallback: Config.string("INNGEST_SIGNING_KEY_FALLBACK").pipe(
|
|
275
|
+
Config.option,
|
|
276
|
+
Config.map(Option.getOrUndefined),
|
|
277
|
+
),
|
|
278
|
+
}),
|
|
279
|
+
);
|
package/src/Events.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal Inngest events that the platform sends automatically.
|
|
3
|
+
* Use these as triggers to react to function lifecycle events.
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error structure used in internal Inngest events.
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
*/
|
|
12
|
+
export class JsonError extends Schema.Class<JsonError>("JsonError")({
|
|
13
|
+
name: Schema.String,
|
|
14
|
+
message: Schema.String,
|
|
15
|
+
stack: Schema.optional(Schema.String),
|
|
16
|
+
cause: Schema.optional(Schema.Unknown),
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sent when a function fails after exhausting all retries.
|
|
21
|
+
* Trigger on this to handle failures (e.g., alerting, cleanup).
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
export class FunctionFailed extends Schema.TaggedClass<FunctionFailed>()("inngest/function.failed", {
|
|
25
|
+
function_id: Schema.String,
|
|
26
|
+
run_id: Schema.String,
|
|
27
|
+
error: JsonError,
|
|
28
|
+
event: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sent when a function finishes with an error.
|
|
33
|
+
* @since 0.1.0
|
|
34
|
+
*/
|
|
35
|
+
export class FunctionFinishedError extends Schema.TaggedClass<FunctionFinishedError>()("inngest/function.finished", {
|
|
36
|
+
function_id: Schema.String,
|
|
37
|
+
run_id: Schema.String,
|
|
38
|
+
correlation_id: Schema.optional(Schema.String),
|
|
39
|
+
error: JsonError,
|
|
40
|
+
}) {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sent when a function finishes successfully.
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
export class FunctionFinishedSuccess extends Schema.TaggedClass<FunctionFinishedSuccess>()(
|
|
47
|
+
"inngest/function.finished",
|
|
48
|
+
{
|
|
49
|
+
function_id: Schema.String,
|
|
50
|
+
run_id: Schema.String,
|
|
51
|
+
correlation_id: Schema.optional(Schema.String),
|
|
52
|
+
result: Schema.Unknown,
|
|
53
|
+
},
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Union of both FunctionFinished variants.
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
export const FunctionFinished = Schema.Union(FunctionFinishedError, FunctionFinishedSuccess);
|
|
61
|
+
export type FunctionFinished = typeof FunctionFinished.Type;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sent when a function is cancelled.
|
|
65
|
+
* @since 0.1.0
|
|
66
|
+
*/
|
|
67
|
+
export class FunctionCancelled extends Schema.TaggedClass<FunctionCancelled>()("inngest/function.cancelled", {
|
|
68
|
+
function_id: Schema.String,
|
|
69
|
+
run_id: Schema.String,
|
|
70
|
+
correlation_id: Schema.optional(Schema.String),
|
|
71
|
+
}) {}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sent when a function is invoked via step.invoke().
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export class FunctionInvoked extends Schema.TaggedClass<FunctionInvoked>()("inngest/function.invoked", {
|
|
78
|
+
data: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
|
|
79
|
+
}) {}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Sent when a cron trigger fires.
|
|
83
|
+
* @since 0.1.0
|
|
84
|
+
*/
|
|
85
|
+
export class ScheduledTimer extends Schema.TaggedClass<ScheduledTimer>()("inngest/scheduled.timer", {
|
|
86
|
+
cron: Schema.String,
|
|
87
|
+
}) {}
|