Skip to content

Commit

Permalink
INN-2754 Add support for INNGEST_DEV (#488)
Browse files Browse the repository at this point in the history
## Summary
<!-- Succinctly describe your change, providing context, what you've
changed, and why. -->

Adds support for `INNGEST_DEV` and a new `isDev` option on the client.

This lightly refactors the current checks based around `isProd` and
`skipDevServer()`, which were getting a little difficult to read.

- The SDK now has two "modes:" `"dev"` and `"cloud"`.
- Each mode is either **explicit** or **inferred**. An inferred mode
means that the current (`v3.x.x`) version of the SDK can make a decision
to attempt to contact the Dev Server. Future versions will remove this
and default to `"cloud"` mode.
- Setting the `INNGEST_DEV` environment variable or the `isDev` client
option **explicitly** sets the mode to either `"cloud"` or `"dev"`.
- `INNGEST_DEV` accepts some sensible defaults. We'll recommend `1` to
explicitly set `"dev"` mode and `0` to explicitly set `"cloud"` mode,
though it also accepts `"true"`, `"y"`, `"no"`, etc.
- Explicitly setting either mode also sets the event ingestion and
syncing URLs. They continue to be further overwritten by passing
`INNGEST_BASE_URL`, `INNGEST_API_BASE_URL`, and
`INNGEST_EVENT_API_BASE_URL`.

> [!NOTE]
> To support many runtimes and environments, environment variables are
not always accessible a) at all times, and b) on `process.env`.
Sometimes environment variables are accessed via different global
objects, or sometimes runtime objects that are passed to requests.
>
> For this reason, handling environment variables is more complex and
relies on making best guesses during instantiation, then later making
another decision when we have access to the environment.

Supersedes both #424 and #425.

## Checklist
<!-- Tick these items off as you progress. -->
<!-- If an item isn't applicable, ideally please strikeout the item by
wrapping it in "~~"" and suffix it with "N/A My reason for skipping
this." -->
<!-- e.g. "- [ ] ~~Added tests~~ N/A Only touches docs" -->

- [x] Added a [docs PR](https://github.com/inngest/website) documenting
these modes and the new environment variables that references this PR
- [x] Added unit/integration tests
- [x] Added changesets if applicable
- [x] Push env-related changes to the OS SDK Spec

## Related
<!-- A space for any related links, issues, or PRs. -->
<!-- Linear issues are autolinked. -->
<!-- e.g. - INN-123 -->
<!-- GitHub issues/PRs can be linked using shorthand. -->
<!-- e.g. "- inngest/inngest#123" -->
<!-- Feel free to remove this section if there are no applicable related
links.-->
- INN-2754
- Supersedes #424
- Supersedes #425
- inngest/website#679
  • Loading branch information
jpwilliams committed Feb 23, 2024
1 parent 2e267dc commit 3d2429d
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 103 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-elephants-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inngest": minor
---

INN-2754 Add support for `INNGEST_DEV` and the `isDev` option, allowing a devleoper to explicitly set either Cloud or Dev mode
17 changes: 9 additions & 8 deletions packages/inngest/etc/inngest.api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 105 additions & 0 deletions packages/inngest/src/components/Inngest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,111 @@ const testEvent: EventPayload = {

const testEventKey = "foo-bar-baz-test";

describe("new Inngest()", () => {
describe("mode", () => {
const createTestClient = ({
env,
opts,
}: {
env?: Record<string, string>;
opts?: Omit<ConstructorParameters<typeof Inngest>[0], "id">;
} = {}): Inngest.Any => {
let ogKeys: Record<string, string | undefined> = {};

if (env) {
ogKeys = Object.keys(env).reduce<Record<string, string | undefined>>(
(acc, key) => {
acc[key] = process.env[key];
process.env[key] = env[key];
return acc;
},
{}
);
}

const inngest = new Inngest({ id: "test", ...opts });

if (env) {
Object.keys(ogKeys).forEach((key) => {
process.env[key] = ogKeys[key];
});
}

return inngest;
};

test("should default to inferred dev mode", () => {
const inngest = createTestClient();
expect(inngest["mode"].isDev).toBe(true);
expect(inngest["mode"].isExplicit).toBe(false);
});

test("`isDev: true` sets explicit dev mode", () => {
const inngest = createTestClient({ opts: { isDev: true } });
expect(inngest["mode"].isDev).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`isDev: false` sets explict cloud mode", () => {
const inngest = createTestClient({ opts: { isDev: false } });
expect(inngest["mode"].isCloud).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`INNGEST_DEV=1 sets explicit dev mode", () => {
const inngest = createTestClient({
env: { [envKeys.InngestDevMode]: "1" },
});
expect(inngest["mode"].isDev).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`INNGEST_DEV=true` sets explicit dev mode", () => {
const inngest = createTestClient({
env: { [envKeys.InngestDevMode]: "true" },
});
expect(inngest["mode"].isDev).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`INNGEST_DEV=false` sets explicit cloud mode", () => {
const inngest = createTestClient({
env: { [envKeys.InngestDevMode]: "false" },
});
expect(inngest["mode"].isCloud).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`INNGEST_DEV=0 sets explicit cloud mode", () => {
const inngest = createTestClient({
env: { [envKeys.InngestDevMode]: "0" },
});
expect(inngest["mode"].isCloud).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`isDev` overwrites `INNGEST_DEV`", () => {
const inngest = createTestClient({
env: { [envKeys.InngestDevMode]: "1" },
opts: { isDev: false },
});
expect(inngest["mode"].isDev).toBe(false);
expect(inngest["mode"].isExplicit).toBe(true);
});

test("`INNGEST_DEV=URL sets explicit dev mode", () => {
const inngest = createTestClient({
env: { [envKeys.InngestDevMode]: "http://localhost:3000" },
});
expect(inngest["mode"].isDev).toBe(true);
expect(inngest["mode"].isExplicit).toBe(true);
expect(inngest["mode"].explicitDevUrl?.href).toBe(
"http://localhost:3000/"
);
});
});
});

describe("send", () => {
describe("runtime", () => {
const originalProcessEnv = process.env;
Expand Down
62 changes: 45 additions & 17 deletions packages/inngest/src/components/Inngest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
import { devServerAvailable, devServerUrl } from "../helpers/devserver";
import {
getFetch,
getMode,
inngestHeaders,
processEnv,
skipDevServer,
type Mode,
} from "../helpers/env";
import { fixEventKeyMissingSteps, prettyError } from "../helpers/errors";
import { stringify } from "../helpers/strings";
Expand Down Expand Up @@ -131,6 +132,18 @@ export class Inngest<TOpts extends ClientOptions = ClientOptions> {
*/
private readonly middleware: Promise<MiddlewareRegisterReturn[]>;

/**
* Whether the client is running in a production environment. This can
* sometimes be `undefined` if the client has expressed no preference or
* perhaps environment variables are only available at a later stage in the
* runtime, for example when receiving a request.
*
* An {@link InngestCommHandler} should prioritize this value over all other
* settings, but should still check for the presence of an environment
* variable if it is not set.
*/
private readonly mode: Mode;

/**
* A client used to interact with the Inngest API by sending or reacting to
* events.
Expand Down Expand Up @@ -159,6 +172,7 @@ export class Inngest<TOpts extends ClientOptions = ClientOptions> {
env,
logger = new DefaultLogger(),
middleware,
isDev,
}: TOpts) {
if (!id) {
// TODO PrettyError
Expand All @@ -167,15 +181,22 @@ export class Inngest<TOpts extends ClientOptions = ClientOptions> {

this.id = id;

this.mode = getMode({
explicitMode:
typeof isDev === "boolean" ? (isDev ? "dev" : "cloud") : undefined,
});

this.apiBaseUrl =
baseUrl ||
processEnv(envKeys.InngestApiBaseUrl) ||
processEnv(envKeys.InngestBaseUrl);
processEnv(envKeys.InngestBaseUrl) ||
this.mode.getExplicitUrl(defaultInngestApiBaseUrl);

this.eventBaseUrl =
baseUrl ||
processEnv(envKeys.InngestEventApiBaseUrl) ||
processEnv(envKeys.InngestBaseUrl);
processEnv(envKeys.InngestBaseUrl) ||
this.mode.getExplicitUrl(defaultInngestEventBaseUrl);

this.setEventKey(eventKey || processEnv(envKeys.InngestEventKey) || "");

Expand Down Expand Up @@ -407,21 +428,9 @@ export class Inngest<TOpts extends ClientOptions = ClientOptions> {
let url = this.sendEventUrl.href;

/**
* INNGEST_BASE_URL is used to set both dev server and prod URLs, so if a
* user has set this it means they have already chosen a URL to hit.
* If in prod mode and key is not present, fail now.
*/
if (!skipDevServer()) {
if (!this.eventBaseUrl) {
const devAvailable = await devServerAvailable(
defaultDevServerHost,
this.fetch
);

if (devAvailable) {
url = devServerUrl(defaultDevServerHost, `e/${this.eventKey}`).href;
}
}
} else if (!this.eventKeySet()) {
if (this.mode.isCloud && !this.eventKeySet()) {
throw new Error(
prettyError({
whatHappened: "Failed to send event",
Expand All @@ -432,6 +441,25 @@ export class Inngest<TOpts extends ClientOptions = ClientOptions> {
);
}

/**
* If dev mode has been inferred, try to hit the dev server first to see if
* it exists. If it does, use it, otherwise fall back to whatever server we
* have configured.
*
* `INNGEST_BASE_URL` is used to set both dev server and prod URLs, so if a
* user has set this it means they have already chosen a URL to hit.
*/
if (this.mode.isDev && this.mode.isInferred && !this.eventBaseUrl) {
const devAvailable = await devServerAvailable(
defaultDevServerHost,
this.fetch
);

if (devAvailable) {
url = devServerUrl(defaultDevServerHost, `e/${this.eventKey}`).href;
}
}

const response = await this.fetch(url, {
method: "POST",
body: stringify(payloads),
Expand Down

0 comments on commit 3d2429d

Please sign in to comment.