1use std::{
10 cell::{Cell, RefCell},
11 collections::HashMap,
12 fmt::Write,
13 sync::Arc,
14};
15
16use reifydb_auth::{
17 AuthVersion,
18 registry::AuthenticationRegistry,
19 service::{AuthResponse, AuthService, AuthServiceConfig},
20};
21use reifydb_catalog::{
22 CatalogVersion,
23 bootstrap::{
24 bootstrap_configaults, bootstrap_system_procedures, load_materialized_catalog, load_schema_registry,
25 },
26 catalog::Catalog,
27 materialized::MaterializedCatalog,
28 schema::RowSchemaRegistry,
29 system::SystemCatalog,
30};
31use reifydb_cdc::{
32 CdcVersion,
33 produce::producer::{CdcProducerEventListener, spawn_cdc_producer},
34 storage::CdcStore,
35};
36use reifydb_core::{
37 CoreVersion,
38 config::SystemConfig,
39 event::{EventBus, transaction::PostCommitEvent},
40 interface::version::{ComponentType, HasVersion, SystemVersion},
41 util::ioc::IocContainer,
42};
43use reifydb_engine::{EngineVersion, engine::StandardEngine};
44use reifydb_routine::{function::default_functions, procedure::default_procedures};
45use reifydb_rql::RqlVersion;
46use reifydb_runtime::{SharedRuntime, SharedRuntimeConfig};
47use reifydb_store_multi::{
48 MultiStore, MultiStoreVersion,
49 config::{HotConfig, MultiStoreConfig},
50 hot::storage::HotStorage,
51};
52use reifydb_store_single::{SingleStore, SingleStoreVersion};
53use reifydb_sub_api::subsystem::Subsystem;
54use reifydb_sub_flow::{builder::FlowBuilderConfig, subsystem::FlowSubsystem};
55use reifydb_transaction::{
56 TransactionVersion,
57 interceptor::factory::InterceptorFactory,
58 multi::transaction::{MultiTransaction, register_oracle_defaults},
59 single::SingleTransaction,
60};
61use reifydb_type::{params::Params, value::identity::IdentityId};
62use wasm_bindgen::prelude::*;
63
64mod error;
65mod utils;
66
67pub use error::JsError;
68use reifydb_extension::transform::registry::Transforms;
69use reifydb_runtime::context::RuntimeContext;
70
71#[wasm_bindgen]
73pub struct LoginResult {
74 token: String,
75 identity: String,
76}
77
78#[wasm_bindgen]
79impl LoginResult {
80 #[wasm_bindgen(getter)]
81 pub fn token(&self) -> String {
82 self.token.clone()
83 }
84
85 #[wasm_bindgen(getter)]
86 pub fn identity(&self) -> String {
87 self.identity.clone()
88 }
89}
90
91fn console_log(msg: &str) {
93 web_sys::console::log_1(&msg.into());
94}
95
96struct WasmSession {
101 token: RefCell<Option<String>>,
102 identity: Cell<Option<IdentityId>>,
103}
104
105impl WasmSession {
106 fn new() -> Self {
107 Self {
108 token: RefCell::new(None),
109 identity: Cell::new(None),
110 }
111 }
112
113 fn current_identity(&self) -> IdentityId {
114 self.identity.get().unwrap_or_else(IdentityId::root)
115 }
116
117 fn set(&self, identity: IdentityId, token: String) {
118 self.identity.set(Some(identity));
119 *self.token.borrow_mut() = Some(token);
120 }
121
122 fn clear(&self) {
123 self.identity.set(None);
124 *self.token.borrow_mut() = None;
125 }
126
127 fn take_token(&self) -> Option<String> {
128 self.token.borrow().clone()
129 }
130}
131
132#[wasm_bindgen]
133pub struct WasmDB {
134 inner: StandardEngine,
135 flow_subsystem: FlowSubsystem,
136 auth_service: AuthService,
137 session: WasmSession,
138}
139
140#[wasm_bindgen]
141impl WasmDB {
142 #[wasm_bindgen(constructor)]
153 pub fn new() -> Result<WasmDB, JsValue> {
154 #[cfg(feature = "console_error_panic_hook")]
157 console_error_panic_hook::set_once();
158
159 let runtime = SharedRuntime::from_config(
161 SharedRuntimeConfig::default()
162 .async_threads(1)
163 .compute_threads(1)
164 .compute_max_in_flight(8)
165 .deterministic_testing(0),
166 );
167
168 let actor_system = runtime.actor_system();
171
172 let eventbus = EventBus::new(&actor_system);
174 let multi_store = MultiStore::standard(MultiStoreConfig {
175 hot: Some(HotConfig {
176 storage: HotStorage::memory(),
177 }),
178 warm: None,
179 cold: None,
180 retention: Default::default(),
181 merge_config: Default::default(),
182 event_bus: eventbus.clone(),
183 actor_system: actor_system.clone(),
184 });
185 let single_store = SingleStore::testing_memory_with_eventbus(eventbus.clone());
186
187 let single = SingleTransaction::new(single_store.clone(), eventbus.clone());
189 let system_config = SystemConfig::new();
190 register_oracle_defaults(&system_config);
191 let multi = MultiTransaction::new(
192 multi_store.clone(),
193 single.clone(),
194 eventbus.clone(),
195 actor_system.clone(),
196 runtime.clock().clone(),
197 system_config.clone(),
198 )
199 .map_err(|e| JsError::from_error(&e))?;
200
201 let mut ioc = IocContainer::new();
203
204 let materialized_catalog = MaterializedCatalog::new(system_config);
205 ioc = ioc.register(materialized_catalog.clone());
206
207 ioc = ioc.register(runtime.clone());
208
209 ioc = ioc.register(single_store.clone());
211
212 let cdc_store = CdcStore::memory();
214 ioc = ioc.register(cdc_store.clone());
215
216 let ioc_ref = ioc.clone();
218
219 let schema_registry = RowSchemaRegistry::new(single.clone());
221
222 load_materialized_catalog(&multi, &single, &materialized_catalog)
224 .map_err(|e| JsError::from_error(&e))?;
225 bootstrap_configaults(&multi, &single, &materialized_catalog, &eventbus)
226 .map_err(|e| JsError::from_error(&e))?;
227 bootstrap_system_procedures(&multi, &single, &materialized_catalog, &schema_registry, &eventbus)
228 .map_err(|e| JsError::from_error(&e))?;
229 load_schema_registry(&multi, &single, &schema_registry).map_err(|e| JsError::from_error(&e))?;
230
231 let procedures = default_procedures().build();
232
233 let eventbus_clone = eventbus.clone();
235 let inner = StandardEngine::new(
236 multi,
237 single.clone(),
238 eventbus,
239 InterceptorFactory::default(),
240 Catalog::new(materialized_catalog, schema_registry),
241 RuntimeContext::new(runtime.clock().clone(), runtime.rng().clone()),
242 default_functions().build(),
243 procedures,
244 Transforms::empty(),
245 ioc,
246 #[cfg(not(target_arch = "wasm32"))]
247 None,
248 );
249
250 console_log("[WASM] Spawning CDC producer actor...");
252 let cdc_producer_handle = spawn_cdc_producer(
253 &actor_system,
254 cdc_store,
255 multi_store.clone(),
256 inner.clone(),
257 eventbus_clone.clone(),
258 );
259
260 let cdc_listener =
262 CdcProducerEventListener::new(cdc_producer_handle.actor_ref().clone(), runtime.clock().clone());
263 eventbus_clone.register::<PostCommitEvent, _>(cdc_listener);
264 console_log("[WASM] CDC producer actor registered!");
265
266 let flow_config = FlowBuilderConfig {
268 operators_dir: None, num_workers: 1, custom_operators: HashMap::new(),
271 connector_registry: Default::default(),
272 };
273 console_log("[WASM] Creating FlowSubsystem...");
274 let mut flow_subsystem = FlowSubsystem::new(flow_config, inner.clone(), &ioc_ref);
275 console_log("[WASM] Starting FlowSubsystem...");
276 flow_subsystem.start().map_err(|e| JsError::from_error(&e))?;
277 console_log("[WASM] FlowSubsystem started successfully!");
278
279 let mut all_versions = Vec::new();
281 all_versions.push(SystemVersion {
282 name: "reifydb-webassembly".to_string(),
283 version: env!("CARGO_PKG_VERSION").to_string(),
284 description: "ReifyDB WebAssembly Engine".to_string(),
285 r#type: ComponentType::Package,
286 });
287 all_versions.push(CoreVersion.version());
288 all_versions.push(EngineVersion.version());
289 all_versions.push(CatalogVersion.version());
290 all_versions.push(MultiStoreVersion.version());
291 all_versions.push(SingleStoreVersion.version());
292 all_versions.push(TransactionVersion.version());
293 all_versions.push(AuthVersion.version());
294 all_versions.push(RqlVersion.version());
295 all_versions.push(CdcVersion.version());
296 all_versions.push(flow_subsystem.version());
297
298 ioc_ref.register_service(SystemCatalog::new(all_versions));
299
300 let auth_service = AuthService::new(
301 Arc::new(inner.clone()),
302 Arc::new(AuthenticationRegistry::new(runtime.clock().clone())),
303 runtime.rng().clone(),
304 runtime.clock().clone(),
305 AuthServiceConfig::default(),
306 );
307
308 Ok(WasmDB {
309 inner,
310 flow_subsystem,
311 auth_service,
312 session: WasmSession::new(),
313 })
314 }
315
316 #[wasm_bindgen]
328 pub fn query(&self, rql: &str) -> Result<JsValue, JsValue> {
329 let identity = self.session.current_identity();
330 let params = Params::None;
331
332 let frames = self.inner.query_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
334
335 utils::frames_to_js(&frames)
337 }
338
339 #[wasm_bindgen]
355 pub fn admin(&self, rql: &str) -> Result<JsValue, JsValue> {
356 let identity = self.session.current_identity();
357 let params = Params::None;
358
359 let frames = self.inner.admin_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
360
361 utils::frames_to_js(&frames)
362 }
363
364 #[wasm_bindgen]
369 pub fn command(&self, rql: &str) -> Result<JsValue, JsValue> {
370 let identity = self.session.current_identity();
371 let params = Params::None;
372
373 let frames = self.inner.command_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
374
375 utils::frames_to_js(&frames)
376 }
377
378 #[wasm_bindgen(js_name = queryWithParams)]
389 pub fn query_with_params(&self, rql: &str, params_js: JsValue) -> Result<JsValue, JsValue> {
390 let identity = self.session.current_identity();
391
392 let params = utils::parse_params(params_js)?;
394
395 let frames = self.inner.query_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
396
397 utils::frames_to_js(&frames)
398 }
399
400 #[wasm_bindgen(js_name = adminWithParams)]
402 pub fn admin_with_params(&self, rql: &str, params_js: JsValue) -> Result<JsValue, JsValue> {
403 let identity = self.session.current_identity();
404
405 let params = utils::parse_params(params_js)?;
406
407 let frames = self.inner.admin_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
408
409 utils::frames_to_js(&frames)
410 }
411
412 #[wasm_bindgen(js_name = commandWithParams)]
414 pub fn command_with_params(&self, rql: &str, params_js: JsValue) -> Result<JsValue, JsValue> {
415 let identity = self.session.current_identity();
416
417 let params = utils::parse_params(params_js)?;
418
419 let frames = self.inner.command_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
420
421 utils::frames_to_js(&frames)
422 }
423
424 #[wasm_bindgen(js_name = commandText)]
426 pub fn command_text(&self, rql: &str) -> Result<String, JsValue> {
427 let frames = self
428 .inner
429 .command_as(self.session.current_identity(), rql, Params::None)
430 .map_err(|e| JsError::from_error(&e))?;
431 let mut output = String::new();
432 for frame in &frames {
433 writeln!(output, "{}", frame).map_err(|e| JsError::from_str(&e.to_string()))?;
434 }
435 Ok(output)
436 }
437
438 #[wasm_bindgen(js_name = adminText)]
440 pub fn admin_text(&self, rql: &str) -> Result<String, JsValue> {
441 let frames = self
442 .inner
443 .admin_as(self.session.current_identity(), rql, Params::None)
444 .map_err(|e| JsError::from_error(&e))?;
445 let mut output = String::new();
446 for frame in &frames {
447 writeln!(output, "{}", frame).map_err(|e| JsError::from_str(&e.to_string()))?;
448 }
449 Ok(output)
450 }
451
452 #[wasm_bindgen(js_name = queryText)]
454 pub fn query_text(&self, rql: &str) -> Result<String, JsValue> {
455 let frames = self
456 .inner
457 .query_as(self.session.current_identity(), rql, Params::None)
458 .map_err(|e| JsError::from_error(&e))?;
459 let mut output = String::new();
460 for frame in &frames {
461 writeln!(output, "{}", frame).map_err(|e| JsError::from_str(&e.to_string()))?;
462 }
463 Ok(output)
464 }
465
466 #[wasm_bindgen(js_name = loginWithPassword)]
468 pub fn login_with_password(&self, identifier: &str, password: &str) -> Result<LoginResult, JsValue> {
469 let mut credentials = HashMap::new();
470 credentials.insert("identifier".to_string(), identifier.to_string());
471 credentials.insert("password".to_string(), password.to_string());
472
473 let response =
474 self.auth_service.authenticate("password", credentials).map_err(|e| JsError::from_error(&e))?;
475
476 self.handle_auth_response(response)
477 }
478
479 #[wasm_bindgen(js_name = loginWithToken)]
481 pub fn login_with_token(&self, token: &str) -> Result<LoginResult, JsValue> {
482 let mut credentials = HashMap::new();
483 credentials.insert("token".to_string(), token.to_string());
484
485 let response =
486 self.auth_service.authenticate("token", credentials).map_err(|e| JsError::from_error(&e))?;
487
488 self.handle_auth_response(response)
489 }
490
491 #[wasm_bindgen]
493 pub fn logout(&self) -> Result<(), JsValue> {
494 let token = self.session.take_token();
495 match token {
496 Some(t) => {
497 let revoked = self.auth_service.revoke_token(&t);
498 self.session.clear();
499 if revoked {
500 Ok(())
501 } else {
502 Err(JsError::from_str("Failed to revoke session token"))
503 }
504 }
505 None => Ok(()),
506 }
507 }
508}
509
510impl WasmDB {
511 fn handle_auth_response(&self, response: AuthResponse) -> Result<LoginResult, JsValue> {
512 match response {
513 AuthResponse::Authenticated {
514 identity,
515 token,
516 } => {
517 self.session.set(identity, token.clone());
518 Ok(LoginResult {
519 token,
520 identity: identity.to_string(),
521 })
522 }
523 AuthResponse::Failed {
524 reason,
525 } => Err(JsError::from_str(&format!("Authentication failed: {}", reason))),
526 AuthResponse::Challenge {
527 ..
528 } => Err(JsError::from_str("Challenge-response authentication is not supported in WASM mode")),
529 }
530 }
531}
532
533impl Drop for WasmDB {
534 fn drop(&mut self) {
535 let _ = self.flow_subsystem.shutdown();
536 }
537}
538
539impl Default for WasmDB {
540 fn default() -> Self {
541 Self::new().expect("Failed to create WasmDB")
542 }
543}