Skip to main content

prolly_store_pglite/
lib.rs

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/// Configuration options for [`PgliteStore`].
355#[derive(Debug, Clone)]
356pub struct PgliteStoreConfig {
357    /// PGlite data directory. Use `memory://` for an in-memory store.
358    pub data_dir: String,
359    /// Node.js executable.
360    pub node_command: PathBuf,
361    /// Working directory used for Node module resolution.
362    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/// Error type for PGlite store operations.
378#[derive(Debug)]
379pub struct PgliteStoreError {
380    message: String,
381    source: Option<Box<dyn std::error::Error + Send + Sync>>,
382}
383
384impl PgliteStoreError {
385    /// Create a new error with a message.
386    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
418/// PGlite-backed storage backend for Prolly Trees.
419pub 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    /// Open an in-memory PGlite store with default configuration.
432    pub fn open_in_memory() -> Result<Self, PgliteStoreError> {
433        Self::open_with_config(PgliteStoreConfig::default())
434    }
435
436    /// Open a PGlite store at the given data directory.
437    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    /// Open a PGlite store with custom configuration.
445    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}