-
Notifications
You must be signed in to change notification settings - Fork 343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement importable env #3610
Open
jasnell
wants to merge
8
commits into
main
Choose a base branch
from
jasnell/importable-env
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Implement importable env #3610
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
044a536
Implement importable env
jasnell cf7c4c2
Expose populated env at the global scope, expand test
jasnell 1e87ae6
Implement withEnv to propagate modified workers env
jasnell b654754
Invert the compat flag, make withEnv work even with flag disabled
jasnell ad6a06a
Fixup linting issues
jasnell 1633b40
Simplify instrumentation, store env in jsg::Lock
jasnell 2603d7e
Address issue with dynamic bindings
jasnell a077f3a
Apply suggestions from code review
jasnell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// Get the current environment, if any | ||
export function getCurrent(): object | undefined; | ||
export function withEnv(newEnv: unknown, fn: () => unknown): unknown; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
#include "modules.h" | ||
|
||
#include <workerd/jsg/setup.h> | ||
|
||
namespace workerd::api { | ||
|
||
kj::Maybe<jsg::JsObject> EnvModule::getCurrent(jsg::Lock& js) { | ||
auto& key = jsg::IsolateBase::from(js.v8Isolate).getEnvAsyncContextKey(); | ||
KJ_IF_SOME(frame, jsg::AsyncContextFrame::current(js)) { | ||
KJ_IF_SOME(value, frame.get(key)) { | ||
auto handle = value.getHandle(js); | ||
if (handle->IsObject()) { | ||
return jsg::JsObject(handle.As<v8::Object>()); | ||
} | ||
} | ||
} | ||
// If the compat flag is set to disable importable env, then this | ||
// will return nothing. | ||
if (FeatureFlags::get(js).getDisableImportableEnv()) return kj::none; | ||
|
||
// Otherwise, fallback to provide the stored environment. | ||
return js.getWorkerEnv().map([&](const jsg::Value& val) -> jsg::JsObject { | ||
auto handle = val.getHandle(js); | ||
JSG_REQUIRE(handle->IsObject(), TypeError, "Expected environment to be an object."); | ||
return jsg::JsObject(handle.As<v8::Object>()); | ||
}); | ||
} | ||
|
||
jsg::JsValue EnvModule::withEnv( | ||
jsg::Lock& js, jsg::Value newEnv, jsg::Function<jsg::JsValue()> fn) { | ||
auto& key = jsg::IsolateBase::from(js.v8Isolate).getEnvAsyncContextKey(); | ||
jsg::AsyncContextFrame::StorageScope storage(js, key, kj::mv(newEnv)); | ||
return fn(js); | ||
} | ||
|
||
} // namespace workerd::api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { env, withEnv } from 'cloudflare:workers'; | ||
|
||
// This test runs with the disallow-importable-env flag set, meaning that | ||
// the env imported from cloudflare:workers exists but is not-populated | ||
// with the primary environment. Using withEnv, however, would still work. | ||
|
||
import { strictEqual, notDeepStrictEqual } from 'node:assert'; | ||
|
||
strictEqual(env.FOO, undefined); | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export const test = { | ||
async test(_, argEnv) { | ||
strictEqual(env.FOO, undefined); | ||
notDeepStrictEqual(argEnv, env); | ||
|
||
// withEnv still works as expected tho | ||
const { env: otherEnv2 } = await withEnv({ BAZ: 1 }, async () => { | ||
await scheduler.wait(0); | ||
return import('child'); | ||
}); | ||
strictEqual(otherEnv2.FOO, undefined); | ||
strictEqual(otherEnv2.BAZ, 1); | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
strictEqual(argEnv.BAZ, undefined); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using Workerd = import "/workerd/workerd.capnp"; | ||
|
||
const unitTests :Workerd.Config = ( | ||
services = [ | ||
( name = "disable-importable-env-test", | ||
worker = ( | ||
modules = [ | ||
(name = "worker", esModule = embed "disable-importable-env-test.js"), | ||
(name = "child", esModule = "import {env as live} from 'cloudflare:workers'; export const env = {...live};"), | ||
], | ||
compatibilityDate = "2025-02-01", | ||
compatibilityFlags = [ | ||
"nodejs_compat_v2", | ||
jasnell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"disallow_importable_env", | ||
], | ||
bindings = [ | ||
(name = "FOO", text = "BAR"), | ||
], | ||
) | ||
), | ||
], | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { | ||
strictEqual, | ||
deepStrictEqual, | ||
notStrictEqual, | ||
ok, | ||
rejects, | ||
} from 'node:assert'; | ||
import { env, withEnv } from 'cloudflare:workers'; | ||
|
||
import { AsyncLocalStorage } from 'node:async_hooks'; | ||
const check = new AsyncLocalStorage(); | ||
|
||
// The env is populated at the top level scope. | ||
strictEqual(env.FOO, 'BAR'); | ||
|
||
// Cache exists and is accessible. | ||
ok(env.CACHE); | ||
// But fails when used because we're not in an io-context | ||
await rejects( | ||
env.CACHE.read('hello', async () => {}), | ||
{ | ||
message: /Disallowed operation called within global scope./, | ||
} | ||
); | ||
|
||
export const importableEnv = { | ||
async test(_, argEnv) { | ||
// Accessing the cache initially at the global scope didn't break anything | ||
const cached = await argEnv.CACHE.read('hello', async () => { | ||
return { | ||
value: 123, | ||
expiration: Date.now() + 10000, | ||
}; | ||
}); | ||
strictEqual(cached, 123); | ||
|
||
// They aren't the same objects... | ||
notStrictEqual(env, argEnv); | ||
// But have all the same stuff... | ||
deepStrictEqual(env, argEnv); | ||
|
||
// It is populated inside a request | ||
strictEqual(env.FOO, 'BAR'); | ||
|
||
// And following async operations. | ||
await scheduler.wait(10); | ||
strictEqual(env.FOO, 'BAR'); | ||
|
||
// Mutations to the env carry through as expected... | ||
env.BAR = 123; | ||
const { env: otherEnv } = await import('child'); | ||
strictEqual(otherEnv.FOO, 'BAR'); | ||
strictEqual(otherEnv.BAR, 123); | ||
deepStrictEqual(argEnv, otherEnv); | ||
|
||
// Using withEnv to replace the env... | ||
const { env: otherEnv2 } = await withEnv({ BAZ: 1 }, async () => { | ||
await scheduler.wait(0); | ||
return import('child2'); | ||
}); | ||
strictEqual(otherEnv2.FOO, undefined); | ||
strictEqual(otherEnv2.BAZ, 1); | ||
|
||
// Original env is unmodified | ||
strictEqual(env.BAZ, undefined); | ||
|
||
// Verify that JSRPC calls appropriately see the environment. | ||
const remote = await argEnv.RPC.rpcTarget(undefined); | ||
await Promise.all([remote.test(), remote.test2()]); | ||
}, | ||
|
||
get fetch() { | ||
// It's a weird edge case, yes, but let's make sure that the env is | ||
// available in getters when extracting the default handlers. | ||
strictEqual(env.FOO, 'BAR'); | ||
return async () => {}; | ||
}, | ||
|
||
// Verifies that the environment is available and correctly propagated | ||
// in custom events like jsrpc. | ||
rpcTarget() { | ||
strictEqual(env.FOO, 'BAR'); | ||
return withEnv({ FOO: 'BAZ' }, () => { | ||
// Arguably, returned RPC targets should probably automatically capture | ||
// the current async context and propagate that on subsequent calls, | ||
// but they currently do not... therefore we have to manually capture | ||
// the async context and be sure to enter it ourselves below. | ||
const runInAsyncContext = AsyncLocalStorage.snapshot(); | ||
return { | ||
test() { | ||
// This one runs with the modified env | ||
runInAsyncContext(() => strictEqual(env.FOO, 'BAZ')); | ||
}, | ||
test2() { | ||
// This one runs with the original env | ||
strictEqual(env.FOO, 'BAR'); | ||
}, | ||
}; | ||
}); | ||
}, | ||
}; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this return
false
here ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, right. yeah
false
is more correct here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait... actually no.
true
is correct here. The reason is that this is effectively deleting a non-existent property. If, for instance, you haveconst n = {}; delete n.abc
, that delete will returntrue
because effectively the property is deleted.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I was looking at a MDN snippet:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some disagreement in the ecosystem about exactly what should be returned in which cases but I tend to err on duplicating what I see v8 doing which is returning
true
when deleting a non-existent property.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like returning
false
might throw in strict mode, which we do not want.Maybe a simpler impl for all method could be:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I prefer the more verbose version as I think it's a bit clearer / easier to read. It will also be a bit faster as it avoids the extraneous property lookup on the empty object.