Skip to main content

co_js/
block_storage.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 1io BRANDGUARDIAN GmbH
3
4use crate::actor::JsLocalTaskSpawner;
5use anyhow::anyhow;
6use async_trait::async_trait;
7use cid::Cid;
8use co_actor::{ActorError, ActorHandle, LocalActor, Response};
9use co_primitives::{Block, BlockStorage, DefaultParams, StorageError, StoreParams};
10use wasm_bindgen::prelude::*;
11use wasm_bindgen_futures::JsFuture;
12use web_sys::js_sys::{Function, Promise, Uint8Array};
13
14#[wasm_bindgen]
15extern "C" {
16	#[wasm_bindgen(typescript_type = "(cid: Uint8Array) => Promise<Uint8Array | undefined>")]
17	pub type JsBlockStorageGet;
18
19	#[wasm_bindgen(typescript_type = "(cid: Uint8Array, data: Uint8Array) => void")]
20	pub type JsBlockStorageSet;
21}
22
23#[wasm_bindgen(js_name = "BlockStorage")]
24#[derive(Debug, Clone)]
25pub struct JsBlockStorage {
26	handle: ActorHandle<JsBlockStorageMessage>,
27}
28#[wasm_bindgen(js_class = "BlockStorage")]
29impl JsBlockStorage {
30	#[wasm_bindgen(constructor)]
31	pub fn new(get: JsBlockStorageGet, set: JsBlockStorageSet) -> Result<Self, JsValue> {
32		Ok(Self {
33			handle: LocalActor::spawn_with(
34				JsLocalTaskSpawner::default(),
35				Default::default(),
36				JsBlockStorageActor { get: get.dyn_into()?, set: set.dyn_into()? },
37				(),
38			)
39			.map_err(|err| format!("block storage failed: {:?}", err))?
40			.handle(),
41		})
42	}
43}
44#[async_trait]
45impl BlockStorage for JsBlockStorage {
46	async fn get(&self, cid: &Cid) -> Result<Block, StorageError> {
47		Ok(self
48			.handle
49			.request(|response| JsBlockStorageMessage::Get(*cid, response))
50			.await
51			.map_err(|err| StorageError::Internal(err.into()))??)
52	}
53
54	async fn set(&self, block: Block) -> Result<Cid, StorageError> {
55		Ok(self
56			.handle
57			.request(|response| JsBlockStorageMessage::Set(block, response))
58			.await
59			.map_err(|err| StorageError::Internal(err.into()))??)
60	}
61
62	async fn remove(&self, _cid: &Cid) -> Result<(), StorageError> {
63		Err(StorageError::Internal(anyhow!("Unsupported")))
64	}
65
66	fn max_block_size(&self) -> usize {
67		DefaultParams::MAX_BLOCK_SIZE
68	}
69}
70
71enum JsBlockStorageMessage {
72	Get(Cid, Response<Result<Block, StorageError>>),
73	Set(Block, Response<Result<Cid, StorageError>>),
74}
75
76#[derive(Debug)]
77struct JsBlockStorageActor {
78	/// Typescript: ```typescript
79	/// (cid: Uint8Array) => Promise<Uint8Array | undefined>
80	/// ```
81	get: Function,
82
83	/// Typescript: ```typescript
84	/// (cid: Uint8Array, data: Uint8Array) => void
85	/// ```
86	set: Function,
87}
88impl JsBlockStorageActor {
89	async fn get(&self, cid: &Cid) -> Result<Block, StorageError> {
90		let this = JsValue::null();
91		let js_cid =
92			serde_wasm_bindgen::to_value(cid).map_err(|err| anyhow!("Convert `Cid` to `JsValue` failed: {}", err))?;
93		let call: JsValue = self.get.call1(&this, &js_cid).map_err(|err| anyhow!("Call error: {:?}", err))?;
94		let promise: Promise = call
95			.dyn_into::<Promise>()
96			.map_err(|value| anyhow!("Result is not a `Promise`: {:?}", value))?;
97		let future = JsFuture::from(promise);
98		let result = future.await.map_err(|err| anyhow!("Get block failed: {:?}", err))?;
99		if result.is_null_or_undefined() {
100			return Err(StorageError::NotFound(*cid, anyhow!("Getter returned undefined")));
101		}
102		let bytes = result
103			.clone()
104			.dyn_into::<Uint8Array>()
105			.map_err(|err| anyhow!("Failed to convert result to Uint8Array: {:?}", err))?;
106		Ok(Block::new(*cid, bytes.to_vec()).map_err(|err| anyhow!("Data and Cid are not compatible: {:?}", err))?)
107	}
108
109	async fn set(&self, block: Block) -> Result<Cid, StorageError> {
110		let this = JsValue::null();
111		let (cid, data) = block.into_inner();
112		let js_cid =
113			serde_wasm_bindgen::to_value(&cid).map_err(|err| anyhow!("Convert `Cid` to `JsValue` failed: {}", err))?;
114		let js_data = Uint8Array::from(data.as_ref());
115		let call = self
116			.set
117			.call2(&this, &js_cid, &js_data)
118			.map_err(|err| anyhow!("Call error: {:?}", err))?;
119		let promise: Promise = call
120			.dyn_into::<Promise>()
121			.map_err(|value| anyhow!("Result is not a `Promise`: {:?}", value))?;
122		let future = JsFuture::from(promise);
123		let result = future.await.map_err(|err| anyhow!("Set block failed: {:?}", err))?;
124		let cid_bytes = result
125			.dyn_into::<Uint8Array>()
126			.map_err(|err| anyhow!("Convert storage set result JsValue to Cid failed: {:?}", err.as_string()))?
127			.to_vec();
128		let storage_set_cid = Cid::try_from(cid_bytes).map_err(|err| anyhow!("Get Cid from bytes failed: {}", err))?;
129		Ok(storage_set_cid)
130	}
131}
132impl LocalActor for JsBlockStorageActor {
133	type Message = JsBlockStorageMessage;
134	type State = ();
135	type Initialize = ();
136
137	async fn handle(
138		&self,
139		_handle: &ActorHandle<Self::Message>,
140		message: Self::Message,
141		_state: &mut Self::State,
142	) -> Result<(), ActorError> {
143		match message {
144			JsBlockStorageMessage::Get(cid, response) => {
145				response.respond(self.get(&cid).await);
146			},
147			JsBlockStorageMessage::Set(block, response) => {
148				response.respond(self.set(block).await);
149			},
150		}
151		Ok(())
152	}
153
154	async fn initialize(
155		&self,
156		_handle: &ActorHandle<Self::Message>,
157		_tags: &co_primitives::Tags,
158		_initialize: Self::Initialize,
159	) -> Result<Self::State, co_actor::ActorError> {
160		Ok(())
161	}
162}