Skip to main content

reifydb_webassembly/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4//! WebAssembly bindings for ReifyDB query engine
5//!
6//! This crate provides JavaScript-compatible bindings for running ReifyDB
7//! queries in a browser or Node.js environment with in-memory storage.
8
9use 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/// Result of a successful login, returned to JavaScript.
72#[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
91// Debug helper to log to browser console
92fn console_log(msg: &str) {
93	web_sys::console::log_1(&msg.into());
94}
95
96/// WebAssembly ReifyDB Engine
97///
98/// Provides an in-memory query engine that runs entirely in the browser.
99/// All data is stored in memory and lost when the page is closed.
100struct 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	/// Create a new in-memory ReifyDB engine
143	///
144	/// # Example
145	///
146	/// ```javascript
147	/// import init, { WasmDB } from './pkg/reifydb_engine_wasm.js';
148	///
149	/// await init();
150	/// const db = new WasmDB();
151	/// ```
152	#[wasm_bindgen(constructor)]
153	pub fn new() -> Result<WasmDB, JsValue> {
154		// Set panic hook for better error messages in browser console
155
156		#[cfg(feature = "console_error_panic_hook")]
157		console_error_panic_hook::set_once();
158
159		// WASM runtime with minimal threads (single-threaded)
160		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		// Create actor system at the top level - this will be shared by
169		// the transaction manager (watermark actors) and flow subsystem (poll/coordinator actors)
170		let actor_system = runtime.actor_system();
171
172		// Create event bus and stores
173		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		// Create transactions
188		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		// Setup IoC container
202		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		// Register metrics store for engine
210		ioc = ioc.register(single_store.clone());
211
212		// Register CdcStore (required by sub-flow)
213		let cdc_store = CdcStore::memory();
214		ioc = ioc.register(cdc_store.clone());
215
216		// Clone ioc for FlowSubsystem (engine consumes ioc)
217		let ioc_ref = ioc.clone();
218
219		// Create RowSchemaRegistry for bootstrap
220		let schema_registry = RowSchemaRegistry::new(single.clone());
221
222		// Run shared bootstrap: load catalog, config defaults, system procedures, schemas
223		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		// Build engine with bootstrap-initialized catalog
234		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		// Spawn CDC producer actor on the shared runtime, passing engine as CdcHost
251		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		// Register event listener to forward PostCommitEvent to CDC producer
261		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		// Create and start FlowSubsystem
267		let flow_config = FlowBuilderConfig {
268			operators_dir: None, // No FFI operators in WASM
269			num_workers: 1,      // Single-threaded for WASM
270			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		// Collect all versions and register SystemCatalog
280		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	/// Execute a query and return results as JavaScript objects
317	///
318	/// # Example
319	///
320	/// ```javascript
321	/// const results = await db.query(`
322	///   FROM [{ name: "Alice", age: 30 }]
323	///   FILTER age > 25
324	/// `);
325	/// console.log(results); // [{ name: "Alice", age: 30 }]
326	/// ```
327	#[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		// Execute query
333		let frames = self.inner.query_as(identity, rql, params).map_err(|e| JsError::from_error(&e))?;
334
335		// Convert frames to JavaScript array of objects
336		utils::frames_to_js(&frames)
337	}
338
339	/// Execute an admin operation (DDL + DML + Query) and return results
340	///
341	/// Admin operations include CREATE, ALTER, INSERT, UPDATE, DELETE, etc.
342	///
343	/// # Example
344	///
345	/// ```javascript
346	/// await db.admin("CREATE NAMESPACE demo");
347	/// await db.admin(`
348	///   CREATE TABLE demo.users {
349	///     id: int4,
350	///     name: utf8
351	///   }
352	/// `);
353	/// ```
354	#[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	/// Execute a command (DML) and return results
365	///
366	/// Commands include INSERT, UPDATE, DELETE, etc.
367	/// For DDL operations (CREATE, ALTER), use `admin()` instead.
368	#[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	/// Execute query with JSON parameters
379	///
380	/// # Example
381	///
382	/// ```javascript
383	/// const results = await db.queryWithParams(
384	///   "FROM users FILTER age > $min_age",
385	///   { min_age: 25 }
386	/// );
387	/// ```
388	#[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		// Parse JavaScript params to Rust Params
393		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	/// Execute admin with JSON parameters
401	#[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	/// Execute command with JSON parameters
413	#[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	/// Execute a command and return Display-formatted text output
425	#[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	/// Execute an admin operation and return Display-formatted text output
439	#[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	/// Execute a query and return Display-formatted text output
453	#[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	/// Authenticate with a password and return a session token.
467	#[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	/// Authenticate with a token credential and return a session token.
480	#[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	/// Logout and revoke the current session token.
492	#[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}