1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! Cache for Rust function type definitions sent to JavaScript.
//!
//! Type IDs assigned by [`get_or_create_type_id`](TypeCache::get_or_create_type_id)
//! are not safe to send as `TYPE_CACHED` until JS has actually parsed the
//! matching `TYPE_FULL`. Under the strict synchronous ping-pong IPC, JS parses
//! a Rust-to-JS Evaluate before sending its Respond, so the ack lifecycle is
//! a plain stack: each outbound Evaluate pushes its newly-introduced type
//! IDs onto `awaiting_ack_stack`, and the matching JS Respond pops and acks
//! them. No request IDs needed.
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
/// Assigns stable IDs to encoded type definitions for a runtime.
pub(crate) struct TypeCache {
/// Type definition bytes → assigned ID. The bytes are stored only here.
ids: BTreeMap<Vec<u8>, u32>,
/// IDs JS has acked, so future uses can send `TYPE_CACHED`. Keyed by ID
/// directly because both acking and the cached-check are ID-based.
acked: BTreeSet<u32>,
/// Each frame is the set of type IDs introduced by one outbound Rust→JS
/// Evaluate, in send order. The matching JS Respond pops the top frame.
awaiting_ack_stack: Vec<Vec<u32>>,
next_type_id: u32,
}
impl TypeCache {
pub(crate) fn new() -> Self {
Self {
ids: BTreeMap::new(),
acked: BTreeSet::new(),
awaiting_ack_stack: Vec::new(),
next_type_id: 0,
}
}
/// Get or create a type ID for the given type definition bytes.
///
/// Returns `(type_id, can_use_cached)`, where `can_use_cached` is true only
/// after JS has acknowledged parsing a full definition for the ID.
pub(crate) fn get_or_create_type_id(&mut self, type_bytes: &[u8]) -> (u32, bool) {
// `BTreeMap<Vec<u8>, _>` looks up by `&[u8]` via `Borrow`, so the common
// cache-hit path performs no allocation; we only own the bytes on a miss.
if let Some(&id) = self.ids.get(type_bytes) {
(id, self.acked.contains(&id))
} else {
let id = self.next_type_id;
self.next_type_id += 1;
self.ids.insert(type_bytes.to_vec(), id);
(id, false)
}
}
/// Push a frame of type IDs introduced by an outbound Evaluate. The next
/// inbound JS Respond pops and acks them.
pub(crate) fn push_pending_frame(&mut self, type_ids: Vec<u32>) {
self.awaiting_ack_stack.push(type_ids);
}
/// Pop the top pending frame and mark its types as acked.
pub(crate) fn pop_and_ack_pending_frame(&mut self) {
if let Some(type_ids) = self.awaiting_ack_stack.pop() {
self.ack_type_ids(&type_ids);
}
}
/// Mark the given type IDs as acked by JS so future uses can send
/// `TYPE_CACHED`. Used for types introduced in an outbound Respond, which JS
/// processes synchronously before Rust runs again (so no frame is needed).
pub(crate) fn ack_type_ids(&mut self, type_ids: &[u32]) {
self.acked.extend(type_ids.iter().copied());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn type_bytes(tag: u8) -> Vec<u8> {
vec![tag]
}
#[test]
fn type_id_is_cached_after_pending_frame_is_acked() {
let mut cache = TypeCache::new();
let bytes = type_bytes(1);
let (id, cached) = cache.get_or_create_type_id(&bytes);
assert_eq!(id, 0);
assert!(!cached);
cache.push_pending_frame(vec![id]);
assert_eq!(cache.get_or_create_type_id(&bytes), (id, false));
cache.pop_and_ack_pending_frame();
assert_eq!(cache.get_or_create_type_id(&bytes), (id, true));
}
#[test]
fn ack_only_caches_types_in_the_top_frame() {
let mut cache = TypeCache::new();
let first = type_bytes(1);
let second = type_bytes(2);
let (first_id, _) = cache.get_or_create_type_id(&first);
let (second_id, _) = cache.get_or_create_type_id(&second);
// Two outbound Evaluates in flight; the inner one (second_id) gets
// acked first via stack order.
cache.push_pending_frame(vec![first_id]);
cache.push_pending_frame(vec![second_id]);
cache.pop_and_ack_pending_frame();
assert_eq!(cache.get_or_create_type_id(&first), (first_id, false));
assert_eq!(cache.get_or_create_type_id(&second), (second_id, true));
}
#[test]
fn empty_pop_is_a_noop() {
let mut cache = TypeCache::new();
cache.pop_and_ack_pending_frame();
let (id, cached) = cache.get_or_create_type_id(&type_bytes(1));
assert_eq!(id, 0);
assert!(!cached);
}
}