Skip to content

Commit bfac4ab

Browse files
committed
fix(core,sdk): keep sdkScope out of the browser import graph
The previous TriggerClient commit added `import { AsyncLocalStorage } from "node:async_hooks"` in `sdkScope/index.ts`, which is reachable from `@trigger.dev/core/v3`. Browser bundles importing from the v3 root (webapp dashboard, ai-chat client components) pulled the node builtin transitively and failed to compile. Split storage out so the v3 root stays browser-safe: - `sdkScope/index.ts` exposes the API plus an `_installSdkScopeStorage` hook with a slot pattern. No node imports. - `sdkScope/storage-node.ts` owns the AsyncLocalStorage and installs itself via the slot on import. Only file in the package that touches `node:async_hooks`. - Exported as `@trigger.dev/core/v3/sdk-scope-storage`. Deliberately NOT re-exported from the v3 root. - `@trigger.dev/sdk` modules that need the scope (TriggerClient, auth) side-effect-import the sub-path. - `@trigger.dev/sdk` is marked `"sideEffects": false` so browser bundles that don't reach TriggerClient or auth tree-shake them and their side-effect imports out entirely. `apiClientManager.runWithConfig` keeps a fallback to in-place global mutation when storage isn't installed (browser, Edge, Cloudflare Workers, or Node consumers that haven't imported TriggerClient/auth). This preserves the pre-existing concurrency-not-safe-but-functional semantics in runtimes that can't run AsyncLocalStorage. On Node where TriggerClient or auth has been imported, the ALS path is used and parallel scopes don't stomp.
1 parent 080e1f0 commit bfac4ab

8 files changed

Lines changed: 89 additions & 11 deletions

File tree

packages/core/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"./v3/runEngineWorker": "./src/v3/runEngineWorker/index.ts",
5656
"./v3/machines": "./src/v3/machines/index.ts",
5757
"./v3/serverOnly": "./src/v3/serverOnly/index.ts",
58-
"./v3/isomorphic": "./src/v3/isomorphic/index.ts"
58+
"./v3/isomorphic": "./src/v3/isomorphic/index.ts",
59+
"./v3/sdk-scope-storage": "./src/v3/sdkScope/storage-node.ts"
5960
},
6061
"sourceDialects": [
6162
"@triggerdotdev/source"
@@ -622,6 +623,17 @@
622623
"types": "./dist/commonjs/v3/isomorphic/index.d.ts",
623624
"default": "./dist/commonjs/v3/isomorphic/index.js"
624625
}
626+
},
627+
"./v3/sdk-scope-storage": {
628+
"import": {
629+
"@triggerdotdev/source": "./src/v3/sdkScope/storage-node.ts",
630+
"types": "./dist/esm/v3/sdkScope/storage-node.d.ts",
631+
"default": "./dist/esm/v3/sdkScope/storage-node.js"
632+
},
633+
"require": {
634+
"types": "./dist/commonjs/v3/sdkScope/storage-node.d.ts",
635+
"default": "./dist/commonjs/v3/sdkScope/storage-node.js"
636+
}
625637
}
626638
},
627639
"type": "module",

packages/core/src/v3/apiClientManager/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,22 @@ export class APIClientManagerAPI {
111111
fn: R
112112
): Promise<ReturnType<R>> {
113113
const merged: ApiClientConfiguration = { ...this.#getConfig(), ...config };
114-
return sdkScope.withScope({ apiClientConfig: merged, inheritContext: true }, fn);
114+
115+
// Use the AsyncLocalStorage scope when installed (Node-side code
116+
// that has loaded TriggerClient or auth) — concurrency-safe.
117+
if (sdkScope.hasStorage()) {
118+
return sdkScope.withScope({ apiClientConfig: merged, inheritContext: true }, fn);
119+
}
120+
121+
// Fallback: in-place global mutation. Matches pre-existing behavior
122+
// and works in any runtime (browser, Edge, Workers, Node without
123+
// the storage installed). Not concurrency-safe — parallel callers
124+
// with different configs will stomp on each other.
125+
const original = this.#getConfig();
126+
registerGlobal(API_NAME, merged, true);
127+
return fn().finally(() => {
128+
registerGlobal(API_NAME, original, true);
129+
});
115130
}
116131

117132
public setGlobalAPIClientConfiguration(config: ApiClientConfiguration): boolean {
Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
import { AsyncLocalStorage } from "node:async_hooks";
2-
import type { ApiClientConfiguration } from "../apiClientManager/types.js";
1+
import type { SdkScope, SdkScopeStorage } from "./types.js";
32

4-
export type SdkScope = {
5-
apiClientConfig: ApiClientConfiguration;
6-
inheritContext: boolean;
7-
};
3+
export type { SdkScope, SdkScopeStorage } from "./types.js";
4+
5+
// Storage slot. Filled at runtime by a Node-only module
6+
// (`@trigger.dev/core/v3/sdk-scope-storage`) that owns the
7+
// AsyncLocalStorage instance. Left undefined in environments that
8+
// never import that module (browsers, edge runtimes), where
9+
// `sdkScope.withScope` falls through to invoking the callback
10+
// directly. `sdkScope/index.ts` deliberately does not statically
11+
// import `node:async_hooks` or `storage-node.ts` so it is safe to
12+
// include in any browser-side bundle that reaches `@trigger.dev/core/v3`.
13+
let installedStorage: SdkScopeStorage | undefined;
814

9-
const storage = new AsyncLocalStorage<SdkScope>();
15+
export function _installSdkScopeStorage(storage: SdkScopeStorage): void {
16+
installedStorage = storage;
17+
}
1018

1119
export const sdkScope = {
20+
hasStorage(): boolean {
21+
return installedStorage !== undefined;
22+
},
1223
getStore(): SdkScope | undefined {
13-
return storage.getStore();
24+
return installedStorage?.getStore();
1425
},
1526
withScope<R>(scope: SdkScope, fn: () => R): R {
16-
return storage.run(scope, fn);
27+
return installedStorage ? installedStorage.run(scope, fn) : fn();
1728
},
1829
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AsyncLocalStorage } from "node:async_hooks";
2+
import { _installSdkScopeStorage } from "./index.js";
3+
import type { SdkScope } from "./types.js";
4+
5+
// Importing this module installs an AsyncLocalStorage-backed
6+
// `SdkScopeStorage` into the slot exposed by `sdkScope/index.ts`. The
7+
// SDK side-effect-imports this from server-only modules
8+
// (TriggerClient, auth) so that browser-bundled code that never
9+
// touches those modules never pulls `node:async_hooks` either.
10+
const als = new AsyncLocalStorage<SdkScope>();
11+
12+
_installSdkScopeStorage({
13+
getStore: () => als.getStore(),
14+
run: (scope, fn) => als.run(scope, fn),
15+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { ApiClientConfiguration } from "../apiClientManager/types.js";
2+
3+
export type SdkScope = {
4+
apiClientConfig: ApiClientConfiguration;
5+
inheritContext: boolean;
6+
};
7+
8+
export type SdkScopeStorage = {
9+
getStore(): SdkScope | undefined;
10+
run<R>(scope: SdkScope, fn: () => R): R;
11+
};

packages/trigger-sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"directory": "packages/trigger-sdk"
1313
},
1414
"type": "module",
15+
"sideEffects": false,
1516
"files": [
1617
"dist"
1718
],

packages/trigger-sdk/src/v3/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import {
55
} from "@trigger.dev/core/v3";
66
import { generateJWT as internal_generateJWT } from "@trigger.dev/core/v3";
77

8+
// Install the Node AsyncLocalStorage-backed storage so `auth.withAuth`
9+
// (and the public-token helpers that route through it) actually scope
10+
// API client config. See `triggerClient.ts` for the same import.
11+
import "@trigger.dev/core/v3/sdk-scope-storage";
12+
813
/**
914
* Register the global API client configuration. Alternatively, you can set the `TRIGGER_SECRET_KEY` and `TRIGGER_API_URL` environment variables.
1015
* @param options The API client configuration.

packages/trigger-sdk/src/v3/triggerClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import {
33
sdkScope,
44
type SdkScope,
55
} from "@trigger.dev/core/v3";
6+
7+
// Install the Node AsyncLocalStorage-backed storage. Kept as a
8+
// side-effect import so it is never reached from browser bundles
9+
// that don't transitively import TriggerClient (relies on
10+
// `sideEffects: false` in this package + the v3 root not importing
11+
// storage-node statically).
12+
import "@trigger.dev/core/v3/sdk-scope-storage";
13+
614
import { auth } from "./auth.js";
715
import { batch } from "./batch.js";
816
import { deployments } from "./deployments.js";

0 commit comments

Comments
 (0)