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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -0
  3. package/dist/Client.d.ts +167 -0
  4. package/dist/Client.js +144 -0
  5. package/dist/Events.d.ts +110 -0
  6. package/dist/Events.js +93 -0
  7. package/dist/Function.d.ts +384 -0
  8. package/dist/Function.js +104 -0
  9. package/dist/Group.d.ts +152 -0
  10. package/dist/Group.js +164 -0
  11. package/dist/HttpApi.d.ts +75 -0
  12. package/dist/HttpApi.js +47 -0
  13. package/dist/_virtual/rolldown_runtime.js +18 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.js +8 -0
  16. package/dist/internal/constants.js +15 -0
  17. package/dist/internal/driver.d.ts +5 -0
  18. package/dist/internal/driver.js +117 -0
  19. package/dist/internal/errors.d.ts +56 -0
  20. package/dist/internal/errors.js +61 -0
  21. package/dist/internal/handler.d.ts +20 -0
  22. package/dist/internal/handler.js +145 -0
  23. package/dist/internal/helpers.js +44 -0
  24. package/dist/internal/interrupts.d.ts +2 -0
  25. package/dist/internal/interrupts.js +45 -0
  26. package/dist/internal/memo.js +56 -0
  27. package/dist/internal/protocol.d.ts +1 -0
  28. package/dist/internal/protocol.js +191 -0
  29. package/dist/internal/signature.d.ts +18 -0
  30. package/dist/internal/signature.js +97 -0
  31. package/dist/internal/step.d.ts +59 -0
  32. package/dist/internal/step.js +183 -0
  33. package/package.json +121 -0
  34. package/src/Client.ts +279 -0
  35. package/src/Events.ts +87 -0
  36. package/src/Function.ts +493 -0
  37. package/src/Group.ts +314 -0
  38. package/src/HttpApi.ts +82 -0
  39. package/src/index.ts +171 -0
  40. package/src/internal/constants.ts +11 -0
  41. package/src/internal/driver.ts +194 -0
  42. package/src/internal/errors.ts +130 -0
  43. package/src/internal/handler.ts +222 -0
  44. package/src/internal/helpers.ts +58 -0
  45. package/src/internal/interrupts.ts +62 -0
  46. package/src/internal/memo.ts +73 -0
  47. package/src/internal/protocol.ts +218 -0
  48. package/src/internal/signature.ts +158 -0
  49. 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
+ }) {}