1#![doc = include_str!("../README.md")]
2
3use std::collections::{hash_map::Entry, HashMap};
4use std::io::{BufRead, BufReader, Write};
5use std::path::PathBuf;
6use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
7use std::sync::{Mutex, MutexGuard};
8
9use serde_json::{json, Map, Value};
10
11use prolly::{
12 BatchOp, Cid, Error, ManifestStore, ManifestStoreScan, ManifestUpdate, NamedRootManifest,
13 NodeStoreScan, RootCondition, RootManifest, RootWrite, Store, TransactionConflict,
14 TransactionNodeWrite, TransactionUpdate, TransactionalStore,
15};
16
17struct OrderedBatchReadPlan<'a> {
18 unique_keys: Vec<&'a [u8]>,
19 positions: Option<Vec<usize>>,
20}
21
22impl<'a> OrderedBatchReadPlan<'a> {
23 fn new(keys: &[&'a [u8]]) -> Self {
24 let mut unique_indexes = HashMap::with_capacity(keys.len());
25 let mut unique_keys = Vec::with_capacity(keys.len());
26 let mut positions = None;
27 for key in keys {
28 match unique_indexes.entry(*key) {
29 Entry::Occupied(entry) => positions
30 .get_or_insert_with(|| (0..unique_keys.len()).collect::<Vec<_>>())
31 .push(*entry.get()),
32 Entry::Vacant(entry) => {
33 let index = unique_keys.len();
34 unique_keys.push(*key);
35 if let Some(positions) = positions.as_mut() {
36 positions.push(index);
37 }
38 entry.insert(index);
39 }
40 }
41 }
42 Self {
43 unique_keys,
44 positions,
45 }
46 }
47
48 fn unique_keys(&self) -> &[&'a [u8]] {
49 &self.unique_keys
50 }
51
52 fn expand_owned<T: Clone>(&self, values: Vec<Option<T>>) -> Vec<Option<T>> {
53 match &self.positions {
54 Some(positions) => positions
55 .iter()
56 .map(|&index| values[index].clone())
57 .collect(),
58 None => values,
59 }
60 }
61}
62
63fn cid_from_store_key(key: &[u8], context: &str) -> Result<Cid, String> {
64 let bytes: [u8; 32] = key.try_into().map_err(|_| {
65 format!(
66 "{context} key has invalid CID length {}, expected 32",
67 key.len()
68 )
69 })?;
70 Ok(Cid(bytes))
71}
72
73fn sort_cids(cids: &mut [Cid]) {
74 cids.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
75}
76
77fn sort_named_root_manifests(roots: &mut [NamedRootManifest]) {
78 roots.sort_by(|left, right| left.name.cmp(&right.name));
79}
80
81const SIDECAR_SCRIPT: &str = r#"
82import { PGlite } from '@electric-sql/pglite';
83import readline from 'node:readline';
84
85const dataDir = process.env.PROLLY_PGLITE_DATA_DIR || 'memory://';
86const db = await PGlite.create({ dataDir });
87
88await db.exec(`
89CREATE TABLE IF NOT EXISTS prolly_nodes (
90 cid bytea PRIMARY KEY,
91 node bytea NOT NULL
92);
93CREATE TABLE IF NOT EXISTS prolly_hints (
94 namespace bytea NOT NULL,
95 key bytea NOT NULL,
96 value bytea NOT NULL,
97 PRIMARY KEY(namespace, key)
98);
99CREATE TABLE IF NOT EXISTS prolly_roots (
100 name bytea PRIMARY KEY,
101 manifest bytea NOT NULL
102);
103`);
104
105function send(value) {
106 process.stdout.write(JSON.stringify(value) + '\n');
107}
108
109async function readNode(key) {
110 const result = await db.query(
111 "SELECT encode(node, 'hex') AS node FROM prolly_nodes WHERE cid = decode($1, 'hex')",
112 [key]
113 );
114 return result.rows.length === 0 ? null : result.rows[0].node;
115}
116
117async function upsertNode(key, value) {
118 await db.query(
119 "INSERT INTO prolly_nodes (cid, node) VALUES (decode($1, 'hex'), decode($2, 'hex')) " +
120 "ON CONFLICT(cid) DO UPDATE SET node = excluded.node",
121 [key, value]
122 );
123}
124
125async function deleteNode(key) {
126 await db.query("DELETE FROM prolly_nodes WHERE cid = decode($1, 'hex')", [key]);
127}
128
129async function listNodeCids() {
130 const result = await db.query(
131 "SELECT encode(cid, 'hex') AS cid FROM prolly_nodes ORDER BY cid"
132 );
133 return result.rows.map((row) => row.cid);
134}
135
136async function readHint(namespace, key) {
137 const result = await db.query(
138 "SELECT encode(value, 'hex') AS value FROM prolly_hints " +
139 "WHERE namespace = decode($1, 'hex') AND key = decode($2, 'hex')",
140 [namespace, key]
141 );
142 return result.rows.length === 0 ? null : result.rows[0].value;
143}
144
145async function upsertHint(namespace, key, value) {
146 await db.query(
147 "INSERT INTO prolly_hints (namespace, key, value) " +
148 "VALUES (decode($1, 'hex'), decode($2, 'hex'), decode($3, 'hex')) " +
149 "ON CONFLICT(namespace, key) DO UPDATE SET value = excluded.value",
150 [namespace, key, value]
151 );
152}
153
154async function readRoot(name) {
155 const result = await db.query(
156 "SELECT encode(manifest, 'hex') AS manifest FROM prolly_roots WHERE name = decode($1, 'hex')",
157 [name]
158 );
159 return result.rows.length === 0 ? null : result.rows[0].manifest;
160}
161
162async function listRoots() {
163 const result = await db.query(
164 "SELECT encode(name, 'hex') AS name, encode(manifest, 'hex') AS manifest " +
165 "FROM prolly_roots ORDER BY name"
166 );
167 return result.rows;
168}
169
170async function upsertRoot(name, manifest) {
171 await db.query(
172 "INSERT INTO prolly_roots (name, manifest) VALUES (decode($1, 'hex'), decode($2, 'hex')) " +
173 "ON CONFLICT(name) DO UPDATE SET manifest = excluded.manifest",
174 [name, manifest]
175 );
176}
177
178async function deleteRoot(name) {
179 await db.query("DELETE FROM prolly_roots WHERE name = decode($1, 'hex')", [name]);
180}
181
182async function inTransaction(fn) {
183 await db.exec('BEGIN');
184 try {
185 const value = await fn();
186 await db.exec('COMMIT');
187 return value;
188 } catch (error) {
189 try {
190 await db.exec('ROLLBACK');
191 } catch {
192 // Preserve the original error.
193 }
194 throw error;
195 }
196}
197
198async function handle(request) {
199 switch (request.op) {
200 case 'get':
201 return { value: await readNode(request.key) };
202 case 'put':
203 await upsertNode(request.key, request.value);
204 return {};
205 case 'delete':
206 await deleteNode(request.key);
207 return {};
208 case 'batch':
209 await inTransaction(async () => {
210 for (const op of request.ops || []) {
211 if (op.kind === 'upsert') {
212 await upsertNode(op.key, op.value);
213 } else if (op.kind === 'delete') {
214 await deleteNode(op.key);
215 } else {
216 throw new Error(`unknown batch op: ${op.kind}`);
217 }
218 }
219 });
220 return {};
221 case 'batch_get': {
222 const values = {};
223 for (const key of request.keys || []) {
224 const value = await readNode(key);
225 if (value !== null) {
226 values[key] = value;
227 }
228 }
229 return { values };
230 }
231 case 'batch_get_ordered': {
232 const values = [];
233 for (const key of request.keys || []) {
234 values.push(await readNode(key));
235 }
236 return { values };
237 }
238 case 'list_node_cids':
239 return { cids: await listNodeCids() };
240 case 'batch_put':
241 await inTransaction(async () => {
242 for (const entry of request.entries || []) {
243 await upsertNode(entry.key, entry.value);
244 }
245 });
246 return {};
247 case 'get_hint':
248 return { value: await readHint(request.namespace, request.key) };
249 case 'put_hint':
250 await upsertHint(request.namespace, request.key, request.value);
251 return {};
252 case 'batch_put_with_hint':
253 await inTransaction(async () => {
254 for (const entry of request.entries || []) {
255 await upsertNode(entry.key, entry.value);
256 }
257 await upsertHint(request.namespace, request.key, request.value);
258 });
259 return {};
260 case 'get_root':
261 return { manifest: await readRoot(request.name) };
262 case 'list_roots':
263 return { roots: await listRoots() };
264 case 'put_root':
265 await upsertRoot(request.name, request.manifest);
266 return {};
267 case 'delete_root':
268 await deleteRoot(request.name);
269 return {};
270 case 'compare_and_swap_root':
271 return await inTransaction(async () => {
272 const current = await readRoot(request.name);
273 if (current !== request.expected) {
274 return { applied: false, current };
275 }
276 if (request.new === null) {
277 await deleteRoot(request.name);
278 } else {
279 await upsertRoot(request.name, request.new);
280 }
281 return { applied: true, current: request.new };
282 });
283 case 'commit_transaction':
284 return await inTransaction(async () => {
285 for (const condition of request.root_conditions || []) {
286 const current = await readRoot(condition.name);
287 if (current !== condition.expected) {
288 return {
289 applied: false,
290 conflict: {
291 name: condition.name,
292 expected: condition.expected,
293 current
294 }
295 };
296 }
297 }
298
299 for (const op of request.node_writes || []) {
300 if (op.kind === 'upsert') {
301 await upsertNode(op.key, op.value);
302 } else if (op.kind === 'delete') {
303 await deleteNode(op.key);
304 } else {
305 throw new Error(`unknown transaction node op: ${op.kind}`);
306 }
307 }
308
309 for (const op of request.root_writes || []) {
310 if (op.kind === 'put') {
311 await upsertRoot(op.name, op.manifest);
312 } else if (op.kind === 'delete') {
313 await deleteRoot(op.name);
314 } else {
315 throw new Error(`unknown transaction root op: ${op.kind}`);
316 }
317 }
318
319 return { applied: true };
320 });
321 case 'shutdown':
322 await db.close();
323 return { shutdown: true };
324 default:
325 throw new Error(`unknown op: ${request.op}`);
326 }
327}
328
329send({ ready: true });
330
331const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
332for await (const line of rl) {
333 if (!line.trim()) {
334 continue;
335 }
336 let request;
337 try {
338 request = JSON.parse(line);
339 const result = await handle(request);
340 send({ id: request.id, ok: true, result });
341 if (request.op === 'shutdown') {
342 process.exit(0);
343 }
344 } catch (error) {
345 send({
346 id: request?.id ?? null,
347 ok: false,
348 error: error && error.stack ? error.stack : String(error)
349 });
350 }
351}
352"#;
353
354#[derive(Debug, Clone)]
356pub struct PgliteStoreConfig {
357 pub data_dir: String,
359 pub node_command: PathBuf,
361 pub node_working_dir: Option<PathBuf>,
363}
364
365impl Default for PgliteStoreConfig {
366 fn default() -> Self {
367 Self {
368 data_dir: "memory://".to_string(),
369 node_command: std::env::var_os("PROLLY_PGLITE_NODE")
370 .map(PathBuf::from)
371 .unwrap_or_else(|| PathBuf::from("node")),
372 node_working_dir: std::env::var_os("PROLLY_PGLITE_NODE_CWD").map(PathBuf::from),
373 }
374 }
375}
376
377#[derive(Debug)]
379pub struct PgliteStoreError {
380 message: String,
381 source: Option<Box<dyn std::error::Error + Send + Sync>>,
382}
383
384impl PgliteStoreError {
385 pub fn new(message: impl Into<String>) -> Self {
387 Self {
388 message: message.into(),
389 source: None,
390 }
391 }
392
393 fn with_source(
394 message: impl Into<String>,
395 source: impl std::error::Error + Send + Sync + 'static,
396 ) -> Self {
397 Self {
398 message: message.into(),
399 source: Some(Box::new(source)),
400 }
401 }
402}
403
404impl std::fmt::Display for PgliteStoreError {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 write!(f, "PGlite error: {}", self.message)
407 }
408}
409
410impl std::error::Error for PgliteStoreError {
411 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
412 self.source
413 .as_ref()
414 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
415 }
416}
417
418pub struct PgliteStore {
420 process: Mutex<PgliteProcess>,
421}
422
423struct PgliteProcess {
424 child: Child,
425 stdin: ChildStdin,
426 stdout: BufReader<ChildStdout>,
427 next_id: u64,
428}
429
430impl PgliteStore {
431 pub fn open_in_memory() -> Result<Self, PgliteStoreError> {
433 Self::open_with_config(PgliteStoreConfig::default())
434 }
435
436 pub fn open(data_dir: impl Into<String>) -> Result<Self, PgliteStoreError> {
438 Self::open_with_config(PgliteStoreConfig {
439 data_dir: data_dir.into(),
440 ..PgliteStoreConfig::default()
441 })
442 }
443
444 pub fn open_with_config(config: PgliteStoreConfig) -> Result<Self, PgliteStoreError> {
446 let mut command = Command::new(&config.node_command);
447 command
448 .arg("--input-type=module")
449 .arg("-e")
450 .arg(SIDECAR_SCRIPT)
451 .env("PROLLY_PGLITE_DATA_DIR", &config.data_dir)
452 .stdin(Stdio::piped())
453 .stdout(Stdio::piped())
454 .stderr(Stdio::piped());
455 if let Some(working_dir) = config.node_working_dir.as_deref() {
456 command.current_dir(working_dir);
457 }
458
459 let mut child = command.spawn().map_err(|e| {
460 PgliteStoreError::with_source(
461 format!(
462 "failed to spawn `{}` PGlite sidecar",
463 config.node_command.display()
464 ),
465 e,
466 )
467 })?;
468 let stdin = child
469 .stdin
470 .take()
471 .ok_or_else(|| PgliteStoreError::new("PGlite sidecar stdin unavailable"))?;
472 let stdout = child
473 .stdout
474 .take()
475 .ok_or_else(|| PgliteStoreError::new("PGlite sidecar stdout unavailable"))?;
476
477 let mut process = PgliteProcess {
478 child,
479 stdin,
480 stdout: BufReader::new(stdout),
481 next_id: 1,
482 };
483 let ready = process.read_response_line()?;
484 if ready.get("ready").and_then(Value::as_bool) != Some(true) {
485 return Err(PgliteStoreError::new(format!(
486 "PGlite sidecar returned invalid startup response: {ready}"
487 )));
488 }
489
490 Ok(Self {
491 process: Mutex::new(process),
492 })
493 }
494
495 fn process(&self) -> Result<MutexGuard<'_, PgliteProcess>, PgliteStoreError> {
496 self.process
497 .lock()
498 .map_err(|e| PgliteStoreError::new(format!("lock poisoned: {}", e)))
499 }
500
501 fn request(&self, op: &str, mut fields: Map<String, Value>) -> Result<Value, PgliteStoreError> {
502 let mut process = self.process()?;
503 let id = process.next_id;
504 process.next_id += 1;
505 fields.insert("id".to_string(), json!(id));
506 fields.insert("op".to_string(), json!(op));
507
508 let line = serde_json::to_string(&Value::Object(fields))
509 .map_err(|e| PgliteStoreError::with_source("failed to encode PGlite request", e))?;
510 process
511 .stdin
512 .write_all(line.as_bytes())
513 .map_err(|e| PgliteStoreError::with_source("failed to write PGlite request", e))?;
514 process
515 .stdin
516 .write_all(b"\n")
517 .map_err(|e| PgliteStoreError::with_source("failed to terminate PGlite request", e))?;
518 process
519 .stdin
520 .flush()
521 .map_err(|e| PgliteStoreError::with_source("failed to flush PGlite request", e))?;
522
523 let response = process.read_response_line()?;
524 if response.get("id").and_then(Value::as_u64) != Some(id) {
525 return Err(PgliteStoreError::new(format!(
526 "PGlite sidecar response id mismatch for request {id}: {response}"
527 )));
528 }
529 if response.get("ok").and_then(Value::as_bool) != Some(true) {
530 let message = response
531 .get("error")
532 .and_then(Value::as_str)
533 .unwrap_or("unknown PGlite sidecar error");
534 return Err(PgliteStoreError::new(message.to_string()));
535 }
536 Ok(response.get("result").cloned().unwrap_or(Value::Null))
537 }
538}
539
540impl Drop for PgliteStore {
541 fn drop(&mut self) {
542 if let Ok(process) = self.process.get_mut() {
543 let id = process.next_id;
544 let line = json!({ "id": id, "op": "shutdown" }).to_string();
545 let _ = process.stdin.write_all(line.as_bytes());
546 let _ = process.stdin.write_all(b"\n");
547 let _ = process.stdin.flush();
548 let _ = process.read_response_line();
549 if process.child.try_wait().ok().flatten().is_none() {
550 let _ = process.child.kill();
551 let _ = process.child.wait();
552 }
553 }
554 }
555}
556
557impl PgliteProcess {
558 fn read_response_line(&mut self) -> Result<Value, PgliteStoreError> {
559 let mut line = String::new();
560 let bytes = self
561 .stdout
562 .read_line(&mut line)
563 .map_err(|e| PgliteStoreError::with_source("failed to read PGlite response", e))?;
564 if bytes == 0 {
565 let mut stderr = String::new();
566 if let Some(stream) = self.child.stderr.as_mut() {
567 let _ = std::io::Read::read_to_string(stream, &mut stderr);
568 }
569 let detail = if stderr.trim().is_empty() {
570 "sidecar closed stdout".to_string()
571 } else {
572 format!("sidecar closed stdout; stderr: {}", stderr.trim())
573 };
574 return Err(PgliteStoreError::new(detail));
575 }
576 serde_json::from_str(line.trim_end())
577 .map_err(|e| PgliteStoreError::with_source("failed to decode PGlite response", e))
578 }
579}
580
581impl Store for PgliteStore {
582 type Error = PgliteStoreError;
583
584 fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
585 let result = self.request("get", map_with_key(key))?;
586 hex_option(result.get("value"))
587 }
588
589 fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
590 let mut fields = map_with_key(key);
591 fields.insert("value".to_string(), json!(hex::encode(value)));
592 self.request("put", fields)?;
593 Ok(())
594 }
595
596 fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
597 self.request("delete", map_with_key(key))?;
598 Ok(())
599 }
600
601 fn batch(&self, ops: &[BatchOp]) -> Result<(), Self::Error> {
602 let ops = ops
603 .iter()
604 .map(|op| match op {
605 BatchOp::Upsert { key, value } => json!({
606 "kind": "upsert",
607 "key": hex::encode(key),
608 "value": hex::encode(value)
609 }),
610 BatchOp::Delete { key } => json!({
611 "kind": "delete",
612 "key": hex::encode(key)
613 }),
614 })
615 .collect::<Vec<_>>();
616 let mut fields = Map::new();
617 fields.insert("ops".to_string(), Value::Array(ops));
618 self.request("batch", fields)?;
619 Ok(())
620 }
621
622 fn batch_get(&self, keys: &[&[u8]]) -> Result<HashMap<Vec<u8>, Vec<u8>>, Self::Error> {
623 let plan = OrderedBatchReadPlan::new(keys);
624 let mut fields = Map::new();
625 let key_hex = plan
626 .unique_keys()
627 .iter()
628 .map(hex::encode)
629 .collect::<Vec<_>>();
630 fields.insert("keys".to_string(), json!(key_hex));
631 let result = self.request("batch_get", fields)?;
632 let values = result
633 .get("values")
634 .and_then(Value::as_object)
635 .ok_or_else(|| PgliteStoreError::new("PGlite batch_get response missing values"))?;
636 let mut decoded = HashMap::with_capacity(values.len());
637 for (key, value) in values {
638 decoded.insert(
639 hex_decode(key, "batch_get key")?,
640 hex_value(value, "value")?,
641 );
642 }
643 Ok(decoded)
644 }
645
646 fn batch_get_ordered(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
647 let plan = OrderedBatchReadPlan::new(keys);
648 let mut fields = Map::new();
649 let key_hex = plan
650 .unique_keys()
651 .iter()
652 .map(hex::encode)
653 .collect::<Vec<_>>();
654 fields.insert("keys".to_string(), json!(key_hex));
655 let result = self.request("batch_get_ordered", fields)?;
656 let values = result
657 .get("values")
658 .and_then(Value::as_array)
659 .ok_or_else(|| {
660 PgliteStoreError::new("PGlite batch_get_ordered response missing values")
661 })?;
662 let unique_values = values
663 .iter()
664 .map(|value| hex_option(Some(value)))
665 .collect::<Result<Vec<_>, _>>()?;
666 Ok(plan.expand_owned(unique_values))
667 }
668
669 fn batch_get_ordered_unique(
670 &self,
671 keys: &[&[u8]],
672 ) -> Result<Vec<Option<Vec<u8>>>, Self::Error> {
673 let mut fields = Map::new();
674 let key_hex = keys.iter().map(hex::encode).collect::<Vec<_>>();
675 fields.insert("keys".to_string(), json!(key_hex));
676 let result = self.request("batch_get_ordered", fields)?;
677 let values = result
678 .get("values")
679 .and_then(Value::as_array)
680 .ok_or_else(|| {
681 PgliteStoreError::new("PGlite unique ordered batch response missing values")
682 })?;
683 values
684 .iter()
685 .map(|value| hex_option(Some(value)))
686 .collect::<Result<Vec<_>, _>>()
687 }
688
689 fn prefers_batch_reads(&self) -> bool {
690 true
691 }
692
693 fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<(), Self::Error> {
694 let entries = entries
695 .iter()
696 .map(|(key, value)| {
697 json!({
698 "key": hex::encode(key),
699 "value": hex::encode(value)
700 })
701 })
702 .collect::<Vec<_>>();
703 let mut fields = Map::new();
704 fields.insert("entries".to_string(), Value::Array(entries));
705 self.request("batch_put", fields)?;
706 Ok(())
707 }
708
709 fn supports_hints(&self) -> bool {
710 true
711 }
712
713 fn get_hint(&self, namespace: &[u8], key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
714 let result = self.request("get_hint", hint_fields(namespace, key))?;
715 hex_option(result.get("value"))
716 }
717
718 fn put_hint(&self, namespace: &[u8], key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
719 let mut fields = hint_fields(namespace, key);
720 fields.insert("value".to_string(), json!(hex::encode(value)));
721 self.request("put_hint", fields)?;
722 Ok(())
723 }
724
725 fn batch_put_with_hint(
726 &self,
727 entries: &[(&[u8], &[u8])],
728 namespace: &[u8],
729 key: &[u8],
730 value: &[u8],
731 ) -> Result<(), Self::Error> {
732 let entries = entries
733 .iter()
734 .map(|(key, value)| {
735 json!({
736 "key": hex::encode(key),
737 "value": hex::encode(value)
738 })
739 })
740 .collect::<Vec<_>>();
741 let mut fields = hint_fields(namespace, key);
742 fields.insert("entries".to_string(), Value::Array(entries));
743 fields.insert("value".to_string(), json!(hex::encode(value)));
744 self.request("batch_put_with_hint", fields)?;
745 Ok(())
746 }
747}
748
749impl NodeStoreScan for PgliteStore {
750 type Error = PgliteStoreError;
751
752 fn list_node_cids(&self) -> Result<Vec<Cid>, Self::Error> {
753 let result = self.request("list_node_cids", Map::new())?;
754 let raw_cids = result
755 .get("cids")
756 .and_then(Value::as_array)
757 .ok_or_else(|| PgliteStoreError::new("PGlite list_node_cids response missing cids"))?;
758 let mut cids = raw_cids
759 .iter()
760 .map(|value| {
761 let key = hex_value(value, "cid")?;
762 cid_from_store_key(&key, "PGlite node").map_err(PgliteStoreError::new)
763 })
764 .collect::<Result<Vec<_>, _>>()?;
765 sort_cids(&mut cids);
766 Ok(cids)
767 }
768}
769
770impl ManifestStore for PgliteStore {
771 type Error = PgliteStoreError;
772
773 fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error> {
774 let result = self.request("get_root", root_fields(name))?;
775 root_manifest_option(result.get("manifest"))
776 }
777
778 fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error> {
779 let mut fields = root_fields(name);
780 fields.insert("manifest".to_string(), json!(root_manifest_hex(manifest)?));
781 self.request("put_root", fields)?;
782 Ok(())
783 }
784
785 fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error> {
786 self.request("delete_root", root_fields(name))?;
787 Ok(())
788 }
789
790 fn compare_and_swap_root(
791 &self,
792 name: &[u8],
793 expected: Option<&RootManifest>,
794 new: Option<&RootManifest>,
795 ) -> Result<ManifestUpdate, Self::Error> {
796 let mut fields = root_fields(name);
797 fields.insert(
798 "expected".to_string(),
799 optional_root_manifest_json(expected)?,
800 );
801 fields.insert("new".to_string(), optional_root_manifest_json(new)?);
802
803 let result = self.request("compare_and_swap_root", fields)?;
804 let applied = result
805 .get("applied")
806 .and_then(Value::as_bool)
807 .ok_or_else(|| PgliteStoreError::new("PGlite CAS response missing applied flag"))?;
808 if applied {
809 return Ok(ManifestUpdate::Applied);
810 }
811
812 Ok(ManifestUpdate::Conflict {
813 current: root_manifest_option(result.get("current"))?,
814 })
815 }
816}
817
818impl ManifestStoreScan for PgliteStore {
819 fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error> {
820 let result = self.request("list_roots", Map::new())?;
821 let raw_roots = result
822 .get("roots")
823 .and_then(Value::as_array)
824 .ok_or_else(|| PgliteStoreError::new("PGlite list_roots response missing roots"))?;
825 let mut roots = Vec::with_capacity(raw_roots.len());
826 for value in raw_roots {
827 let name = hex_value(
828 value
829 .get("name")
830 .ok_or_else(|| PgliteStoreError::new("PGlite root listing missing name"))?,
831 "root name",
832 )?;
833 let manifest = root_manifest_option(value.get("manifest"))?
834 .ok_or_else(|| PgliteStoreError::new("PGlite root listing missing manifest"))?;
835 roots.push(NamedRootManifest::new(name, manifest));
836 }
837 sort_named_root_manifests(&mut roots);
838 Ok(roots)
839 }
840}
841
842impl TransactionalStore for PgliteStore {
843 fn supports_transactions(&self) -> bool {
844 true
845 }
846
847 fn commit_transaction(
848 &self,
849 node_writes: &[TransactionNodeWrite],
850 root_conditions: &[RootCondition],
851 root_writes: &[RootWrite],
852 ) -> Result<TransactionUpdate, Error> {
853 let nodes_written = node_writes.len();
854 let roots_written = root_writes.len();
855 let node_writes = node_writes
856 .iter()
857 .map(|write| match write {
858 TransactionNodeWrite::Upsert { key, value } => json!({
859 "kind": "upsert",
860 "key": hex::encode(key),
861 "value": hex::encode(value)
862 }),
863 TransactionNodeWrite::Delete { key } => json!({
864 "kind": "delete",
865 "key": hex::encode(key)
866 }),
867 })
868 .collect::<Vec<_>>();
869 let root_conditions = root_conditions
870 .iter()
871 .map(|condition| {
872 Ok(json!({
873 "name": hex::encode(&condition.name),
874 "expected": optional_root_manifest_json(condition.expected.as_ref())?
875 }))
876 })
877 .collect::<Result<Vec<_>, PgliteStoreError>>()
878 .map_err(|err| Error::Store(Box::new(err)))?;
879 let root_writes = root_writes
880 .iter()
881 .map(|write| match write {
882 RootWrite::Put { name, manifest } => Ok(json!({
883 "kind": "put",
884 "name": hex::encode(name),
885 "manifest": root_manifest_hex(manifest)?
886 })),
887 RootWrite::Delete { name } => Ok(json!({
888 "kind": "delete",
889 "name": hex::encode(name)
890 })),
891 })
892 .collect::<Result<Vec<_>, PgliteStoreError>>()
893 .map_err(|err| Error::Store(Box::new(err)))?;
894
895 let mut fields = Map::new();
896 fields.insert("node_writes".to_string(), Value::Array(node_writes));
897 fields.insert("root_conditions".to_string(), Value::Array(root_conditions));
898 fields.insert("root_writes".to_string(), Value::Array(root_writes));
899
900 let result = self
901 .request("commit_transaction", fields)
902 .map_err(|err| Error::Store(Box::new(err)))?;
903 let applied = result
904 .get("applied")
905 .and_then(Value::as_bool)
906 .ok_or_else(|| {
907 Error::Store(Box::new(PgliteStoreError::new(
908 "PGlite transaction response missing applied flag",
909 )))
910 })?;
911 if applied {
912 return Ok(TransactionUpdate::Applied {
913 nodes_written,
914 roots_written,
915 });
916 }
917
918 let conflict = result.get("conflict").ok_or_else(|| {
919 Error::Store(Box::new(PgliteStoreError::new(
920 "PGlite transaction conflict response missing conflict",
921 )))
922 })?;
923 let name = hex_value(
924 conflict.get("name").ok_or_else(|| {
925 Error::Store(Box::new(PgliteStoreError::new(
926 "PGlite transaction conflict missing name",
927 )))
928 })?,
929 "transaction conflict name",
930 )
931 .map_err(|err| Error::Store(Box::new(err)))?;
932 let expected = root_manifest_option(conflict.get("expected"))
933 .map_err(|err| Error::Store(Box::new(err)))?;
934 let current = root_manifest_option(conflict.get("current"))
935 .map_err(|err| Error::Store(Box::new(err)))?;
936 Ok(TransactionUpdate::Conflict(Box::new(
937 TransactionConflict::new(name, expected, current),
938 )))
939 }
940}
941
942fn map_with_key(key: &[u8]) -> Map<String, Value> {
943 let mut fields = Map::new();
944 fields.insert("key".to_string(), json!(hex::encode(key)));
945 fields
946}
947
948fn root_fields(name: &[u8]) -> Map<String, Value> {
949 let mut fields = Map::new();
950 fields.insert("name".to_string(), json!(hex::encode(name)));
951 fields
952}
953
954fn hint_fields(namespace: &[u8], key: &[u8]) -> Map<String, Value> {
955 let mut fields = Map::new();
956 fields.insert("namespace".to_string(), json!(hex::encode(namespace)));
957 fields.insert("key".to_string(), json!(hex::encode(key)));
958 fields
959}
960
961fn optional_root_manifest_json(manifest: Option<&RootManifest>) -> Result<Value, PgliteStoreError> {
962 manifest
963 .map(root_manifest_hex)
964 .transpose()
965 .map(|manifest| manifest.map_or(Value::Null, Value::String))
966}
967
968fn root_manifest_hex(manifest: &RootManifest) -> Result<String, PgliteStoreError> {
969 manifest
970 .to_bytes()
971 .map(hex::encode)
972 .map_err(|e| PgliteStoreError::new(format!("failed to encode root manifest: {e}")))
973}
974
975fn root_manifest_option(value: Option<&Value>) -> Result<Option<RootManifest>, PgliteStoreError> {
976 let Some(value) = value else {
977 return Ok(None);
978 };
979 if value.is_null() {
980 return Ok(None);
981 }
982 let bytes = hex_value(value, "manifest")?;
983 RootManifest::from_bytes(&bytes)
984 .map(Some)
985 .map_err(|e| PgliteStoreError::new(format!("failed to decode root manifest: {e}")))
986}
987
988fn hex_option(value: Option<&Value>) -> Result<Option<Vec<u8>>, PgliteStoreError> {
989 let Some(value) = value else {
990 return Ok(None);
991 };
992 if value.is_null() {
993 return Ok(None);
994 }
995 hex_value(value, "value").map(Some)
996}
997
998fn hex_value(value: &Value, name: &str) -> Result<Vec<u8>, PgliteStoreError> {
999 let raw = value.as_str().ok_or_else(|| {
1000 PgliteStoreError::new(format!(
1001 "PGlite response field `{name}` is not a hex string"
1002 ))
1003 })?;
1004 hex_decode(raw, name)
1005}
1006
1007fn hex_decode(raw: &str, name: &str) -> Result<Vec<u8>, PgliteStoreError> {
1008 hex::decode(raw).map_err(|e| {
1009 PgliteStoreError::with_source(format!("failed to decode PGlite `{name}` hex"), e)
1010 })
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015 use super::*;
1016
1017 #[test]
1018 fn pglite_store_round_trips_nodes_and_hints_when_enabled() {
1019 if std::env::var("PROLLY_PGLITE_TEST").ok().as_deref() != Some("1") {
1020 return;
1021 }
1022
1023 let store = PgliteStore::open_in_memory().unwrap();
1024 store.put(b"a", b"1").unwrap();
1025 store.put(b"b", b"2").unwrap();
1026 assert_eq!(store.get(b"a").unwrap(), Some(b"1".to_vec()));
1027 assert_eq!(
1028 store.batch_get_ordered(&[b"a", b"missing", b"b"]).unwrap(),
1029 vec![Some(b"1".to_vec()), None, Some(b"2".to_vec())]
1030 );
1031 store.put_hint(b"ns", b"k", b"hint").unwrap();
1032 assert_eq!(store.get_hint(b"ns", b"k").unwrap(), Some(b"hint".to_vec()));
1033 store.delete(b"a").unwrap();
1034 assert_eq!(store.get(b"a").unwrap(), None);
1035 }
1036
1037 #[test]
1038 fn pglite_store_persists_across_reopen_when_enabled() {
1039 if std::env::var("PROLLY_PGLITE_TEST").ok().as_deref() != Some("1") {
1040 return;
1041 }
1042
1043 let path =
1044 std::env::temp_dir().join(format!("trail-pglite-store-test-{}", std::process::id()));
1045 let _ = std::fs::remove_dir_all(&path);
1046 {
1047 let store = PgliteStore::open(path.to_string_lossy().to_string()).unwrap();
1048 store.put(b"a", b"1").unwrap();
1049 }
1050 {
1051 let store = PgliteStore::open(path.to_string_lossy().to_string()).unwrap();
1052 assert_eq!(store.get(b"a").unwrap(), Some(b"1".to_vec()));
1053 }
1054 let _ = std::fs::remove_dir_all(&path);
1055 }
1056}