1use std::{cell::RefCell, rc::Rc};
4
5use sim_lib_control::{
6 AdmissionLimit, CheckpointError, CheckpointReceipt, FrameError, FrameLimits, JobId, JobQueues,
7 ResumableFrame, ResumePacket, ResumeResult, RuntimeJobClass, WorkLimit,
8};
9
10use crate::JavascriptValue;
11
12#[derive(Clone, Debug, PartialEq)]
14pub struct JavascriptException {
15 pub name: String,
17 pub message: String,
19}
20
21impl JavascriptException {
22 pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
24 Self {
25 name: name.into(),
26 message: message.into(),
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
33pub enum JavascriptJobClass {
34 Microtask,
36 Finalization,
38}
39
40impl From<JavascriptJobClass> for RuntimeJobClass {
41 fn from(value: JavascriptJobClass) -> Self {
42 match value {
43 JavascriptJobClass::Microtask => RuntimeJobClass::LanguageMicrotask("javascript"),
44 JavascriptJobClass::Finalization => RuntimeJobClass::Finalization,
45 }
46 }
47}
48
49pub struct JavascriptJobs {
51 queues: JobQueues<RuntimeJobClass>,
52}
53
54impl JavascriptJobs {
55 pub fn new(admission: AdmissionLimit) -> Self {
57 Self {
58 queues: JobQueues::new(admission),
59 }
60 }
61
62 pub fn enqueue_microtask(
64 &mut self,
65 job: impl FnOnce(&mut JobQueues<RuntimeJobClass>) + 'static,
66 ) -> Result<JobId, CheckpointError> {
67 self.queues
68 .enqueue(RuntimeJobClass::LanguageMicrotask("javascript"), job)
69 .map(|receipt| receipt.id)
70 }
71
72 pub fn enqueue_finalization(
74 &mut self,
75 job: impl FnOnce(&mut JobQueues<RuntimeJobClass>) + 'static,
76 ) -> Result<JobId, CheckpointError> {
77 self.queues
78 .enqueue(RuntimeJobClass::Finalization, job)
79 .map(|receipt| receipt.id)
80 }
81
82 pub fn cancel(&mut self, id: JobId) {
84 self.queues.cancel(id);
85 }
86
87 pub fn microtask_checkpoint(
89 &mut self,
90 work: WorkLimit,
91 ) -> Result<CheckpointReceipt<RuntimeJobClass>, CheckpointError> {
92 self.queues
93 .checkpoint(RuntimeJobClass::LanguageMicrotask("javascript"), work)
94 }
95
96 pub fn finalization_checkpoint(
98 &mut self,
99 work: WorkLimit,
100 ) -> Result<CheckpointReceipt<RuntimeJobClass>, CheckpointError> {
101 self.queues.checkpoint(RuntimeJobClass::Finalization, work)
102 }
103}
104
105#[derive(Clone, Debug, PartialEq)]
107pub enum JavascriptPromiseState {
108 Pending,
110 Fulfilled(JavascriptValue),
112 Rejected(JavascriptException),
114}
115
116#[derive(Clone)]
118pub struct JavascriptPromise(Rc<RefCell<JavascriptPromiseState>>);
119
120impl Default for JavascriptPromise {
121 fn default() -> Self {
122 Self(Rc::new(RefCell::new(JavascriptPromiseState::Pending)))
123 }
124}
125
126impl JavascriptPromise {
127 pub fn state(&self) -> JavascriptPromiseState {
129 self.0.borrow().clone()
130 }
131
132 pub fn resolve(
134 &self,
135 jobs: &mut JavascriptJobs,
136 value: JavascriptValue,
137 ) -> Result<JobId, CheckpointError> {
138 let state = Rc::clone(&self.0);
139 jobs.enqueue_microtask(move |_| {
140 let mut state = state.borrow_mut();
141 if matches!(*state, JavascriptPromiseState::Pending) {
142 *state = JavascriptPromiseState::Fulfilled(value);
143 }
144 })
145 }
146
147 pub fn reject(
149 &self,
150 jobs: &mut JavascriptJobs,
151 error: JavascriptException,
152 ) -> Result<JobId, CheckpointError> {
153 let state = Rc::clone(&self.0);
154 jobs.enqueue_microtask(move |_| {
155 let mut state = state.borrow_mut();
156 if matches!(*state, JavascriptPromiseState::Pending) {
157 *state = JavascriptPromiseState::Rejected(error);
158 }
159 })
160 }
161}
162
163pub struct JavascriptGenerator {
165 frame: ResumableFrame<Box<JavascriptResumeDriver>>,
166}
167
168type JavascriptResumeDriver = dyn FnMut(
169 ResumePacket<JavascriptValue, JavascriptException>,
170 &mut sim_lib_control::StepBudget,
171) -> Result<
172 ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>,
173 FrameError,
174>;
175
176impl JavascriptGenerator {
177 pub fn new(
179 limits: FrameLimits,
180 dispatch: impl FnMut(
181 ResumePacket<JavascriptValue, JavascriptException>,
182 &mut sim_lib_control::StepBudget,
183 ) -> Result<
184 ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>,
185 FrameError,
186 > + 'static,
187 ) -> Self {
188 Self {
189 frame: ResumableFrame::new(limits, Box::new(dispatch) as Box<JavascriptResumeDriver>),
190 }
191 }
192
193 pub fn resume(
195 &mut self,
196 packet: ResumePacket<JavascriptValue, JavascriptException>,
197 ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>, FrameError>
198 {
199 self.frame.resume(packet)
200 }
201}
202
203pub struct JavascriptAsyncFunction {
205 generator: JavascriptGenerator,
206 promise: JavascriptPromise,
207}
208
209impl JavascriptAsyncFunction {
210 pub fn new(generator: JavascriptGenerator) -> Self {
212 Self {
213 generator,
214 promise: JavascriptPromise::default(),
215 }
216 }
217
218 pub fn promise(&self) -> JavascriptPromise {
220 self.promise.clone()
221 }
222
223 pub fn resume(
225 &mut self,
226 packet: ResumePacket<JavascriptValue, JavascriptException>,
227 jobs: &mut JavascriptJobs,
228 ) -> Result<ResumeResult<JavascriptValue, JavascriptValue, JavascriptException>, FrameError>
229 {
230 let result = self.generator.resume(packet)?;
231 match &result {
232 ResumeResult::Returned(value) => {
233 self.promise
234 .resolve(jobs, value.clone())
235 .map_err(|_| FrameError::WorkExhausted)?;
236 }
237 ResumeResult::Failed(error) => {
238 self.promise
239 .reject(jobs, error.clone())
240 .map_err(|_| FrameError::WorkExhausted)?;
241 }
242 ResumeResult::Yielded(_) => {}
243 }
244 Ok(result)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use std::{cell::RefCell, rc::Rc};
251
252 use super::*;
253
254 #[test]
255 fn generator_exception_and_async_settlement_use_shared_packets() {
256 let mut generator =
257 JavascriptGenerator::new(FrameLimits { depth: 2, work: 4 }, |packet, budget| {
258 budget.charge_work()?;
259 Ok(match packet {
260 ResumePacket::Start => ResumeResult::Yielded(JavascriptValue::Number(1.0)),
261 ResumePacket::Send(value) => ResumeResult::Returned(value),
262 ResumePacket::Throw(error) => ResumeResult::Failed(error),
263 ResumePacket::Close => ResumeResult::Returned(JavascriptValue::Undefined),
264 })
265 });
266 assert!(matches!(
267 generator.resume(ResumePacket::Start).unwrap(),
268 ResumeResult::Yielded(JavascriptValue::Number(1.0))
269 ));
270 let error = JavascriptException::new("RangeError", "bounded");
271 assert_eq!(
272 generator
273 .resume(ResumePacket::Throw(error.clone()))
274 .unwrap(),
275 ResumeResult::Failed(error)
276 );
277
278 let generator = JavascriptGenerator::new(FrameLimits { depth: 1, work: 1 }, |_, _| {
279 Ok(ResumeResult::Returned(JavascriptValue::Number(42.0)))
280 });
281 let mut function = JavascriptAsyncFunction::new(generator);
282 let promise = function.promise();
283 let mut jobs = JavascriptJobs::new(AdmissionLimit(4));
284 function.resume(ResumePacket::Start, &mut jobs).unwrap();
285 assert_eq!(promise.state(), JavascriptPromiseState::Pending);
286 jobs.microtask_checkpoint(WorkLimit(4)).unwrap();
287 assert_eq!(
288 promise.state(),
289 JavascriptPromiseState::Fulfilled(JavascriptValue::Number(42.0))
290 );
291 }
292
293 #[test]
294 fn checkpoint_drains_reentrant_microtasks_and_isolates_finalization() {
295 let trace = Rc::new(RefCell::new(Vec::new()));
296 let mut jobs = JavascriptJobs::new(AdmissionLimit(8));
297 let outer_trace = Rc::clone(&trace);
298 jobs.enqueue_microtask(move |queues| {
299 outer_trace.borrow_mut().push("reaction-1");
300 let inner_trace = Rc::clone(&outer_trace);
301 queues
302 .enqueue(
303 RuntimeJobClass::LanguageMicrotask("javascript"),
304 move |_| {
305 inner_trace.borrow_mut().push("reaction-2");
306 },
307 )
308 .unwrap();
309 })
310 .unwrap();
311 let final_trace = Rc::clone(&trace);
312 jobs.enqueue_finalization(move |_| final_trace.borrow_mut().push("finalize"))
313 .unwrap();
314 let receipt = jobs.microtask_checkpoint(WorkLimit(3)).unwrap();
315 assert_eq!(receipt.completed.len(), 2);
316 assert_eq!(&*trace.borrow(), &["reaction-1", "reaction-2"]);
317 jobs.finalization_checkpoint(WorkLimit(1)).unwrap();
318 assert_eq!(&*trace.borrow(), &["reaction-1", "reaction-2", "finalize"]);
319 }
320
321 #[test]
322 fn exhaustion_and_cancellation_fail_closed_without_implicit_work() {
323 let ran = Rc::new(RefCell::new(0));
324 let mut jobs = JavascriptJobs::new(AdmissionLimit(2));
325 let first_ran = Rc::clone(&ran);
326 let first = jobs
327 .enqueue_microtask(move |_| *first_ran.borrow_mut() += 1)
328 .unwrap();
329 let second_ran = Rc::clone(&ran);
330 jobs.enqueue_microtask(move |_| *second_ran.borrow_mut() += 1)
331 .unwrap();
332 assert_eq!(*ran.borrow(), 0);
333 assert_eq!(
334 jobs.enqueue_microtask(|_| {}).unwrap_err(),
335 CheckpointError::AdmissionExhausted
336 );
337 jobs.cancel(first);
338 assert_eq!(
339 jobs.microtask_checkpoint(WorkLimit(1)).unwrap_err(),
340 CheckpointError::WorkExhausted
341 );
342 assert_eq!(*ran.borrow(), 0);
343 jobs.microtask_checkpoint(WorkLimit(1)).unwrap();
344 assert_eq!(*ran.borrow(), 1);
345 }
346}