-
-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(js-runtime): async context (#684)
* feat(js-runtime): async context * feat(docs): add docs * fix(runtime): tests * chore: add changeset
- Loading branch information
Showing
7 changed files
with
303 additions
and
25 deletions.
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,7 @@ | ||
--- | ||
'@lagon/cli': patch | ||
'@lagon/docs': patch | ||
'@lagon/js-runtime': patch | ||
--- | ||
|
||
Add `AsyncLocalStorage` & `AsyncContext` APIs |
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,215 @@ | ||
use lagon_runtime_http::{Request, Response, RunResult}; | ||
use lagon_runtime_isolate::options::IsolateOptions; | ||
|
||
mod utils; | ||
|
||
// Tests ported from https://github.com/tc39/proposal-async-context/blob/master/tests/async-context.test.ts | ||
#[tokio::test] | ||
async fn inital_undefined() { | ||
utils::setup(); | ||
let (send, receiver) = utils::create_isolate(IsolateOptions::new( | ||
"const ctx = new AsyncContext(); | ||
const actual = ctx.get(); | ||
if (actual !== undefined) { | ||
throw new Error('Expected undefined'); | ||
} | ||
export function handler() { | ||
return new Response(actual === undefined); | ||
}" | ||
.into(), | ||
)); | ||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("true")) | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
async fn return_value() { | ||
utils::setup(); | ||
let (send, receiver) = utils::create_isolate(IsolateOptions::new( | ||
"const ctx = new AsyncContext(); | ||
const expected = { id: 1 }; | ||
const actual = ctx.run({ id: 2 }, () => expected); | ||
if (actual !== expected) { | ||
throw new Error('Expected expected'); | ||
} | ||
export function handler() { | ||
return new Response(); | ||
}" | ||
.into(), | ||
)); | ||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("")) | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
async fn get_returns_current_context_value() { | ||
utils::setup(); | ||
let (send, receiver) = utils::create_isolate(IsolateOptions::new( | ||
"const ctx = new AsyncContext(); | ||
const expected = { id: 1 }; | ||
ctx.run(expected, () => { | ||
if (ctx.get() !== expected) { | ||
throw new Error('Expected expected'); | ||
} | ||
}); | ||
export function handler() { | ||
return new Response(); | ||
}" | ||
.into(), | ||
)); | ||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("")) | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
#[serial_test::serial] | ||
async fn get_within_nesting_contexts() { | ||
utils::setup(); | ||
let (send, receiver) = utils::create_isolate(IsolateOptions::new( | ||
"const ctx = new AsyncContext(); | ||
const first = { id: 1 }; | ||
const second = { id: 2 }; | ||
ctx.run(first, () => { | ||
if (ctx.get() !== first) { | ||
throw new Error('Expected first'); | ||
} | ||
ctx.run(second, () => { | ||
if (ctx.get() !== second) { | ||
throw new Error('Expected second'); | ||
} | ||
}); | ||
if (ctx.get() !== first) { | ||
throw new Error('Expected first'); | ||
} | ||
}); | ||
if (ctx.get() !== undefined) { | ||
throw new Error('Expected undefined'); | ||
} | ||
export function handler() { | ||
return new Response(); | ||
}" | ||
.into(), | ||
)); | ||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("")) | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
#[serial_test::serial] | ||
async fn get_within_nesting_different_contexts() { | ||
utils::setup(); | ||
let (send, receiver) = utils::create_isolate(IsolateOptions::new( | ||
"const a = new AsyncContext(); | ||
const b = new AsyncContext(); | ||
const first = { id: 1 }; | ||
const second = { id: 2 }; | ||
a.run(first, () => { | ||
if (a.get() !== first) { | ||
throw new Error('Expected first'); | ||
} | ||
if (b.get() !== undefined) { | ||
throw new Error('Expected undefined'); | ||
} | ||
b.run(second, () => { | ||
if (a.get() !== first) { | ||
throw new Error('Expected first'); | ||
} | ||
if (b.get() !== second) { | ||
throw new Error('Expected second'); | ||
} | ||
}); | ||
if (a.get() !== first) { | ||
throw new Error('Expected first'); | ||
} | ||
if (b.get() !== undefined) { | ||
throw new Error('Expected undefined'); | ||
} | ||
}); | ||
if (a.get() !== undefined) { | ||
throw new Error('Expected undefined'); | ||
} | ||
if (b.get() !== undefined) { | ||
throw new Error('Expected undefined'); | ||
} | ||
export function handler() { | ||
return new Response(); | ||
}" | ||
.into(), | ||
)); | ||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("")) | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
#[serial_test::serial] | ||
async fn timers() { | ||
utils::setup(); | ||
let log_rx = utils::setup_logger(); | ||
let (send, receiver) = utils::create_isolate( | ||
IsolateOptions::new( | ||
"const store = new AsyncLocalStorage(); | ||
let id = 1; | ||
export async function handler() { | ||
const result = store.run(id++, () => { | ||
setTimeout(() => { | ||
console.log(store.getStore() * 2); | ||
}, 100); | ||
return store.getStore() * 2; | ||
}); | ||
// Make sure the console.log is executed before returning the response | ||
await new Promise((resolve) => setTimeout(resolve, 150)); | ||
return new Response(result); | ||
}" | ||
.into(), | ||
) | ||
.metadata(Some(("".to_owned(), "".to_owned()))), | ||
); | ||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("2")) | ||
); | ||
|
||
send(Request::default()); | ||
|
||
assert_eq!( | ||
receiver.recv_async().await.unwrap(), | ||
RunResult::Response(Response::from("4")) | ||
); | ||
|
||
assert_eq!(log_rx.recv_async().await.unwrap(), "2"); | ||
} |
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
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,43 @@ | ||
(globalThis => { | ||
globalThis.__storage__ = new Map(); | ||
|
||
globalThis.AsyncContext = class { | ||
get() { | ||
return globalThis.__storage__.get(this); | ||
} | ||
|
||
static wrap(callback: (...args: unknown[]) => void): (...args: unknown[]) => void { | ||
const snapshot = globalThis.__storage__; | ||
|
||
return function (...args: unknown[]) { | ||
const prev = globalThis.__storage__; | ||
try { | ||
globalThis.__storage__ = snapshot; | ||
// @ts-expect-error we want to get this from the current function | ||
return callback.apply(this, args); | ||
} finally { | ||
globalThis.__storage__ = prev; | ||
} | ||
}; | ||
} | ||
|
||
run<R>(store: unknown, callback: (...args: unknown[]) => R, ...args: unknown[]): R { | ||
const prev = globalThis.__storage__; | ||
|
||
try { | ||
const n = new Map(globalThis.__storage__); | ||
n.set(this, store); | ||
globalThis.__storage__ = n; | ||
return callback(...args); | ||
} finally { | ||
globalThis.__storage__ = prev; | ||
} | ||
} | ||
}; | ||
|
||
globalThis.AsyncLocalStorage = class extends AsyncContext { | ||
getStore() { | ||
return this.get(); | ||
} | ||
}; | ||
})(globalThis); |
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
9da1136
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.
Successfully deployed to the following URLs:
docs – ./packages/docs
docs.lagon.app
docs-git-main-lagon.vercel.app
lagon-docs.vercel.app
docs-lagon.vercel.app