Skip to content

Commit c215b5b

Browse files
committed
refactor: replace RefCell with UnsafeCell for cached attributes and content in InternalBinaryNode
1 parent 882cce4 commit c215b5b

3 files changed

Lines changed: 71 additions & 80 deletions

File tree

src/binary.rs

Lines changed: 59 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use js_sys::{Array, Object, Uint8Array};
22
use std::borrow::Cow;
3-
use std::cell::RefCell;
3+
use std::cell::UnsafeCell;
44
use std::mem;
55
use std::rc::Rc;
66
use wacore_binary::{
@@ -120,16 +120,29 @@ pub struct InternalBinaryNode {
120120
_owned_data: Rc<[u8]>,
121121
node_ref: NodeRef<'static>,
122122
#[wasm_bindgen(skip)]
123-
cached_attrs: RefCell<Option<Attrs>>,
123+
cached_attrs: UnsafeCell<Option<Attrs>>,
124124
#[wasm_bindgen(skip)]
125-
cached_content: RefCell<Option<Content>>,
125+
cached_content: UnsafeCell<Option<Content>>,
126126
}
127127

128128
impl InternalBinaryNode {
129129
#[inline(always)]
130130
fn node_ref(&self) -> &NodeRef<'static> {
131131
&self.node_ref
132132
}
133+
134+
#[inline]
135+
fn convert_attrs(attrs: &[(Cow<'_, str>, ValueRef<'_>)]) -> Attrs {
136+
let obj = Object::new();
137+
for (k, v) in attrs.iter() {
138+
let js_value = match v.as_str() {
139+
Some(s) => JsValue::from_str(s),
140+
None => JsValue::from_str(&v.to_string()),
141+
};
142+
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str(k), &js_value);
143+
}
144+
obj.unchecked_into()
145+
}
133146
}
134147

135148
#[wasm_bindgen]
@@ -183,72 +196,63 @@ impl InternalBinaryNode {
183196

184197
#[wasm_bindgen(getter)]
185198
pub fn attrs(&self) -> Attrs {
186-
let mut cached = self.cached_attrs.borrow_mut();
187-
if cached.is_none() {
188-
let attrs = &self.node_ref().attrs;
189-
190-
let obj = Object::new();
191-
for (k, v) in attrs.iter() {
192-
let js_value = match v.as_str() {
193-
Some(s) => JsValue::from_str(s),
194-
None => JsValue::from_str(&v.to_string()),
195-
};
196-
let _ = js_sys::Reflect::set(&obj, &JsValue::from_str(k), &js_value);
197-
}
198-
199-
*cached = Some(obj.unchecked_into());
199+
// SAFETY: WASM is single-threaded
200+
let cached = unsafe { &mut *self.cached_attrs.get() };
201+
if let Some(attrs) = cached.as_ref() {
202+
return attrs.clone();
200203
}
201204

202-
cached
203-
.as_ref()
204-
.expect("Cached attributes should be populated before access")
205-
.clone()
206-
.unchecked_into()
205+
let attrs = Self::convert_attrs(&self.node_ref().attrs);
206+
*cached = Some(attrs.clone());
207+
attrs
207208
}
208209

209210
#[wasm_bindgen(setter)]
210211
pub fn set_attrs(&self, new_attrs: Attrs) {
211-
*self.cached_attrs.borrow_mut() = Some(new_attrs);
212+
// SAFETY: WASM is single-threaded
213+
unsafe { *self.cached_attrs.get() = Some(new_attrs) };
212214
}
213215

214216
#[wasm_bindgen(getter)]
215217
pub fn content(&self) -> Option<Content> {
216-
let mut cached = self.cached_content.borrow_mut();
217-
if cached.is_none() {
218-
match self.node_ref().content.as_deref() {
219-
Some(NodeContentRef::Bytes(bytes)) => {
220-
let bytes_ref = bytes.as_ref();
221-
let u8arr = Uint8Array::new_with_length(bytes_ref.len() as u32);
222-
u8arr.copy_from(bytes_ref);
223-
*cached = Some(u8arr.unchecked_into());
224-
}
225-
Some(NodeContentRef::String(s)) => {
226-
*cached = Some(JsValue::from_str(s).unchecked_into());
227-
}
228-
Some(NodeContentRef::Nodes(nodes)) => {
229-
let arr = Array::new_with_length(nodes.len() as u32);
230-
for (i, node_ref) in nodes.iter().enumerate() {
231-
let child = InternalBinaryNode {
232-
_owned_data: Rc::clone(&self._owned_data),
233-
node_ref: node_ref.clone(),
234-
cached_attrs: RefCell::new(None),
235-
cached_content: RefCell::new(None),
236-
};
237-
arr.set(i as u32, child.into());
238-
}
239-
*cached = Some(arr.unchecked_into());
218+
// SAFETY: WASM is single-threaded
219+
let cached = unsafe { &mut *self.cached_content.get() };
220+
if let Some(content) = cached.as_ref() {
221+
return Some(content.clone());
222+
}
223+
224+
let result: Option<Content> = match self.node_ref().content.as_deref() {
225+
Some(NodeContentRef::Bytes(bytes)) => {
226+
let bytes_ref = bytes.as_ref();
227+
let u8arr = Uint8Array::new_with_length(bytes_ref.len() as u32);
228+
u8arr.copy_from(bytes_ref);
229+
Some(u8arr.unchecked_into())
230+
}
231+
Some(NodeContentRef::String(s)) => Some(JsValue::from_str(s).unchecked_into()),
232+
Some(NodeContentRef::Nodes(nodes)) => {
233+
let arr = Array::new_with_length(nodes.len() as u32);
234+
for (i, node_ref) in nodes.iter().enumerate() {
235+
let child = InternalBinaryNode {
236+
_owned_data: Rc::clone(&self._owned_data),
237+
node_ref: node_ref.clone(),
238+
cached_attrs: UnsafeCell::new(None),
239+
cached_content: UnsafeCell::new(None),
240+
};
241+
arr.set(i as u32, child.into());
240242
}
241-
None => *cached = Some(JsValue::undefined().unchecked_into()),
243+
Some(arr.unchecked_into())
242244
}
243-
}
244-
cached
245-
.as_ref()
246-
.map(|v| v.clone().unchecked_into::<Content>())
245+
None => None,
246+
};
247+
248+
*cached = result.clone();
249+
result
247250
}
248251

249252
#[wasm_bindgen(setter)]
250253
pub fn set_content(&self, new_content: Content) {
251-
*self.cached_content.borrow_mut() = Some(new_content);
254+
// SAFETY: WASM is single-threaded
255+
unsafe { *self.cached_content.get() = Some(new_content) };
252256
}
253257
}
254258

@@ -281,7 +285,7 @@ pub fn decode_node(data: Vec<u8>) -> Result<InternalBinaryNode, JsValue> {
281285
Ok(InternalBinaryNode {
282286
_owned_data: owned_data,
283287
node_ref,
284-
cached_attrs: RefCell::new(None),
285-
cached_content: RefCell::new(None),
288+
cached_attrs: UnsafeCell::new(None),
289+
cached_content: UnsafeCell::new(None),
286290
})
287291
}

test/binary.test.ts

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,15 @@ test("should allow attrs mutation and reassignment", () => {
233233
expect(roundtrip.attrs.hello).toBe("world");
234234
});
235235

236-
// Add to the describe("Binary Marshalling") block
237236
test("should allow content mutation and reassignment", () => {
238237
const original: BinaryNode = {
239238
tag: "patch",
240239
attrs: {},
241-
content: [{ tag: "item", attrs: {}, content: "old" }], // Array of children
240+
content: [{ tag: "item", attrs: {}, content: "old" }],
242241
};
243242
const binary = encodeNode(original);
244243
const node = decodeNode(binary);
245244

246-
// Mutation: Push to array
247245
(node.content as BinaryNode[]).push({
248246
tag: "new-item",
249247
attrs: {},
@@ -252,18 +250,15 @@ test("should allow content mutation and reassignment", () => {
252250
expect((node.content as BinaryNode[]).length).toBe(2);
253251
expect((node.content as BinaryNode[])[1].content).toBe("added");
254252

255-
// Reassignment: Replace with string
256253
node.content = "replaced with string";
257254
expect(typeof node.content).toBe("string");
258255
expect(node.content).toBe("replaced with string");
259256

260-
// Reassignment: Back to array
261257
node.content = [
262258
{ tag: "final", attrs: {}, content: new Uint8Array([1, 2, 3]) },
263259
];
264260
expect((node.content as BinaryNode[])[0].content).toBeInstanceOf(Uint8Array);
265261

266-
// Round-trip re-encode (mutations persist)
267262
const reencoded = encodeNode(node);
268263
const roundtrip = decodeNode(reencoded);
269264
expect(Array.isArray(roundtrip.content)).toBe(true);
@@ -273,47 +268,42 @@ test("should allow content mutation and reassignment", () => {
273268
);
274269
});
275270

276-
// Edge: Binary content mutation (e.g., Uint8Array slicing, but rare in Baileys)
277271
test("should handle binary content reassignment", () => {
278272
const binContent = new Uint8Array([10, 20, 30]);
279273
const node: BinaryNode = { tag: "enc", attrs: {}, content: binContent };
280274
const binary = encodeNode(node);
281275
const decoded = decodeNode(binary);
282276

283-
// Reassign to new bytes
284277
decoded.content = new Uint8Array([40, 50, 60]);
285278
expect(decoded.content).toEqual(new Uint8Array([40, 50, 60]));
286279

287-
// Re-encode
288280
const reencoded = encodeNode(decoded);
289281
const roundtrip = decodeNode(reencoded);
290282
expect(roundtrip.content).toEqual(new Uint8Array([40, 50, 60]));
291283
});
284+
292285
test("should handle non-string attribute values during encoding (lenient conversion)", () => {
293-
// Test node with mixed attr types
294286
const original: BinaryNode = {
295287
tag: "test-node",
296288
attrs: {
297289
// @ts-ignore
298-
id: 123, // Number -> "123"
290+
id: 123,
299291
// @ts-ignore
300-
active: true, // Boolean -> "true"
292+
active: true,
301293
// @ts-ignore
302-
inactive: false, // Boolean -> "false"
303-
version: "1.0", // String (unchanged)
294+
inactive: false,
295+
version: "1.0",
304296
// @ts-ignore
305-
foo: null, // Null -> skipped (not present)
297+
foo: null,
306298
// @ts-ignore
307-
bar: undefined, // Undefined -> skipped (not present)
308-
// Note: Objects in attrs will be coerced via toString(), but test simple cases
299+
bar: undefined,
309300
},
310301
content: "test content",
311302
};
312303

313304
const binary = encodeNode(original);
314305
const decoded = decodeNode(binary);
315306

316-
// Verify encoded attrs are all strings, with correct conversions
317307
const attrs = decoded.attrs;
318308
expect(attrs).toBeDefined();
319309
expect(typeof attrs.id).toBe("string");
@@ -323,15 +313,13 @@ test("should handle non-string attribute values during encoding (lenient convers
323313
expect(attrs.inactive).toBe("false");
324314
expect(attrs.version).toBe("1.0");
325315

326-
// Skipped values should be absent
327316
expect(attrs.foo).toBeUndefined();
328317
expect(attrs.bar).toBeUndefined();
329318

330-
// Round-trip: Re-encode and check persistence (non-strings become strings)
331319
const reencoded = encodeNode(decoded);
332320
const roundtrip = decodeNode(reencoded);
333321
const roundtripAttrs = roundtrip.attrs;
334-
expect(roundtripAttrs.id).toBe("123"); // Persists as string
322+
expect(roundtripAttrs.id).toBe("123");
335323
expect(roundtripAttrs.active).toBe("true");
336324
});
337325

@@ -350,7 +338,7 @@ test("should skip empty string values in attrs during encoding", () => {
350338
const decoded = decodeNode(binary);
351339

352340
const attrs = decoded.attrs;
353-
expect(attrs.emptyStr).toBeUndefined(); // Skipped (empty)
354-
expect(attrs.whitespace).toBeUndefined(); // Skipped (after trim? Wait, code uses unwrap_or_default(), so "" -> skipped if empty)
355-
expect(attrs.valid).toBe("ok"); // Preserved
341+
expect(attrs.emptyStr).toBeUndefined();
342+
expect(attrs.whitespace).toBeUndefined();
343+
expect(attrs.valid).toBe("ok");
356344
});

ts/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { initSync } from "../pkg/whatsapp_rust_bridge.js";
22

33
import { base64Wasm } from "./macro.js" with { type: "macro" };
44

5-
// Web-compatible base64 decoding (works in both Node.js and browsers)
65
function base64ToUint8Array(base64: string): Uint8Array {
76
const binaryString = atob(base64);
87
const bytes = new Uint8Array(binaryString.length);

0 commit comments

Comments
 (0)