beamdb 0.12.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,7 +46,7 @@ BEAM is a maintained fork of [rod](https://github.com/mmalmi/rod) — a from-scr
46
46
  - **Real-time** — `on()` subscriptions deliver updates as they propagate through the mesh
47
47
  - **Eventually consistent** — last-write-wins conflict resolution via timestamps (matching Gun.js)
48
48
  - **Encrypted** — SEA layer provides Ed25519 signing, X25519 ECDH, and AES-256-GCM encryption
49
- - **Persistent** — `redb` embedded database for disk-backed storage, or `Persy` for high-concurrency workloads, or in-memory for ephemeral use
49
+ - **Persistent** — `redb` embedded database (default), `fjall` LSM-tree (recommended for multi-node), `Persy` for high-concurrency, or in-memory for ephemeral use
50
50
  - **Multi-transport** — WebSocket (relay), UDP multicast (LAN discovery), WebRTC (direct P2P)
51
51
  - **Browser-ready** — compiles to WebAssembly via `wasm-pack`; same engine, same wire protocol, IndexedDB persistence
52
52
 
@@ -56,7 +56,7 @@ BEAM is a maintained fork of [rod](https://github.com/mmalmi/rod) — a from-scr
56
56
 
57
57
  ```toml
58
58
  [dependencies]
59
- beamdb = "0.8"
59
+ beamdb = "0.16"
60
60
  ```
61
61
 
62
62
  Or via the CLI:
@@ -65,14 +65,17 @@ Or via the CLI:
65
65
  cargo add beamdb
66
66
  ```
67
67
 
68
- Feature flags (both off by default):
68
+ Feature flags (all off by default):
69
69
 
70
70
  ```toml
71
71
  # WebRTC direct P2P support
72
- beamdb = { version = "0.8", features = ["webrtc"] }
72
+ beamdb = { version = "0.16", features = ["webrtc"] }
73
+
74
+ # Fjall LSM-tree storage backend (recommended for multi-node deployments)
75
+ beamdb = { version = "0.16", features = ["fjall"] }
73
76
 
74
77
  # Persy storage backend (for high-concurrency workloads)
75
- beamdb = { version = "0.8", features = ["persy"] }
78
+ beamdb = { version = "0.16", features = ["persy"] }
76
79
  ```
77
80
 
78
81
  ---
@@ -112,7 +115,8 @@ await init();
112
115
 
113
116
  // Create a BEAM node
114
117
  const beam = new Beam(); // in-memory (lost on reload)
115
- // or: const beam = Beam.new_persistent(); // IndexedDB (survives reload)
118
+ // or: const beam = Beam.new_persistent(); // IndexedDB (survives reload)
119
+ // or: const beam = Beam.new_with_opfs(); // OPFS (survives reload, faster)
116
120
 
117
121
  // Connect to a relay server
118
122
  beam.connect("wss://relay.example.com/ws");
@@ -223,17 +227,26 @@ compatible WebSocket peer.
223
227
  | Backend | Persistent | Browser API | Use Case |
224
228
  |---------|-----------|-------------|----------|
225
229
  | `MemoryStorage` | No (lost on reload) | Default | Ephemeral data, testing |
226
- | `WasmIdbStorage` | Yes (IndexedDB) | Opt-in | Production browser apps |
230
+ | `WasmIdbStorage` | Yes (IndexedDB) | Opt-in via `new_persistent()` | Production browser apps |
231
+ | `WasmOpfsStorage` | Yes (OPFS) | Opt-in via `new_with_opfs()` | Modern browsers (Chrome 102+, Firefox 111+, Safari 15.2+) |
232
+ | `WasmNodeFsStorage` | Yes (Node.js fs) | Node.js only (`--features node-fs`) | Server-side WASM, Electron |
227
233
 
228
- `WasmIdbStorage` uses a write-through cache: writes go to an in-memory `HashMap`
234
+ **WasmIdbStorage** uses a write-through cache: writes go to an in-memory `HashMap`
229
235
  (fast reads) and are simultaneously written through to IndexedDB (persistence).
236
+ Data is serialized as postcard bytes (base64-encoded for IDB string storage),
237
+ with automatic JSON fallback for backward compatibility with pre-v0.17 databases.
230
238
  On page reload, data is read back from IndexedDB into the cache.
231
239
 
240
+ **WasmOpfsStorage** uses the Origin Private File System for file-based persistence.
241
+ Data is stored as postcard-serialized binary files in OPFS, offering better
242
+ performance than IndexedDB for larger datasets. Requires a secure context
243
+ (HTTPS or localhost).
244
+
232
245
  ### Browser Constraints
233
246
 
234
247
  - **Single-threaded** — all async work runs on the browser's main thread
235
248
  - **Client-only** — connects to relays, does not accept inbound connections
236
- - **No file system** — uses IndexedDB instead of redb/Persy
249
+ - **No native file system** — uses IndexedDB or OPFS instead of redb/Persy
237
250
  - **WebSocket only** — no UDP multicast or WebRTC (browser sandbox limitations)
238
251
 
239
252
  ### Interop with Gun.js
@@ -382,7 +395,8 @@ BEAM is built on an actor model with a central router. Every component — stora
382
395
  │ │ │ │ │ │
383
396
  │ MemoryStorage│ │ WsServer │ │ WebRtcPeer │
384
397
  │ RedbStorage │ │ WsClient │ │ (str0m) │
385
- PersyStorage│ │ Multicast │ │ │
398
+ FjallStorage│ │ Multicast │ │ │
399
+ │ PersyStorage│ │ │ │ │
386
400
  └─────────────┘ └─────────────┘ └────────────┘
387
401
  ```
388
402
 
@@ -412,6 +426,7 @@ BEAM is built on an actor model with a central router. Every component — stora
412
426
  | `sea/session/` | Session persistence: `MemorySessionStorage` (ephemeral) and `EncryptedFileSessionStorage` (disk, AES-GCM) |
413
427
  | `adapters/memory_storage.rs` | In-memory `HashMap` storage (ephemeral, default for `Node::new()`) |
414
428
  | `adapters/redb_storage.rs` | Persistent storage via `redb` embedded database — `BatchPut` atomic transactions, flush ack |
429
+ | `adapters/fjall_storage.rs` | Persistent storage via `fjall` LSM-tree database — WAL journalling, LZ4 compression, recommended for multi-node (feature-gated) |
415
430
  | `adapters/persy_storage.rs` | Persistent storage via `Persy` segment store — high-concurrency writes, optional `background_ops` |
416
431
  | `adapters/ws_server.rs` | WebSocket server: accepts inbound connections, spawns `WsConn` per connection, optional TLS, web UI on port+1 |
417
432
  | `adapters/ws_client.rs` | `OutgoingWebsocketManager` — connects to remote WebSocket peers with retry |
@@ -629,11 +644,11 @@ When `allow_public_space=false`, the node rejects unsigned puts to public space
629
644
 
630
645
  ## Storage Backends
631
646
 
632
- BEAM supports two persistent storage backends for the embedded database layer. Both implement the same `Storage` trait, so the rest of the codebase is unaware of which one is active. The wire protocol is backend-agnostic — nodes with different storage choices converge via the standard mesh.
647
+ BEAM supports three persistent storage backends for the embedded database layer. All implement the same `Actor` trait, so the rest of the codebase is unaware of which one is active. The wire protocol is backend-agnostic — nodes with different storage choices converge via the standard mesh.
633
648
 
634
649
  ### redb (Default)
635
650
 
636
- **What**: Embedded ACID database, single-writer, fsync on every Put.
651
+ **What**: Embedded ACID B+tree database, single-writer, fsync on every Put.
637
652
 
638
653
  **When to use**:
639
654
  - Single-node deployments
@@ -643,16 +658,45 @@ BEAM supports two persistent storage backends for the embedded database layer. B
643
658
 
644
659
  **Trade-offs**:
645
660
  - ✅ Battle-tested, single-crate, well-understood
646
- - ✅ fsync before ack = bulletproof durability
661
+ - ✅ fsync before ack = bulletproof durability — data survives power loss
662
+ - ✅ Best read performance (mmap'd B+tree = direct memory access)
647
663
  - ❌ Single-writer serialization limits concurrent write throughput
648
664
  - ❌ Not ideal for high-fanout mesh workloads
665
+ - ❌ Every Put = fsync (milliseconds, blocking)
666
+
667
+ ### fjall (Recommended for Multi-Node)
668
+
669
+ **What**: Embedded LSM-tree (RocksDB-like) database in 100% safe Rust. WAL journalling with background compaction and built-in LZ4 compression.
670
+
671
+ **When to use**:
672
+ - Multi-node P2P deployments with high write fanout
673
+ - Workloads where peers flood puts during resync
674
+ - You want maximum write throughput
675
+
676
+ **Trade-offs**:
677
+ - ✅ 3–4× faster writes than redb (journal append vs fsync per write)
678
+ - ✅ Built-in LZ4 compression (free, SSTable-level)
679
+ - ✅ WriteBatch — single journal entry for atomic multi-put
680
+ - ✅ 100% safe Rust, no unsafe blocks
681
+ - ❌ ~1.4× slower random reads than redb (multi-level LSM lookup vs B+tree)
682
+ - ❌ Not fsync'd per write — data is crash-safe (WAL) but a power loss may lose recent un-fsync'd writes
683
+ - ❌ Background compaction causes read latency variance
684
+
685
+ **Durability model**: fjall's default matches RocksDB — writes are crash-safe via WAL (survive process crash), but not fsync'd to disk until explicit `persist()`. For a P2P database where peers hold copies of the data, this is the correct trade-off: if one node loses its WAL on power failure, peers resync it. `Flush` triggers `persist(SyncAll)` for full durability.
686
+
687
+ **Benchmarks** (see [`bench/RESULTS.md`](bench/RESULTS.md)):
688
+
689
+ | Benchmark | redb | fjall |
690
+ |---|---|---|
691
+ | write_storm (sequential) | ~977 elem/s | ~3,000 elem/s |
692
+ | concurrent_write_storm (4 tasks) | ~1,195 elem/s | ~4,836 elem/s |
693
+ | read_storm (random) | ~610 elem/s | ~447 elem/s |
649
694
 
650
695
  ### Persy (Opt-In)
651
696
 
652
697
  **What**: Embedded segment-based store with per-transaction isolation and optional `background_ops` fsync offloading.
653
698
 
654
699
  **When to use**:
655
- - Multi-node meshes with high concurrent write fanout
656
700
  - Workloads where many writers hit disjoint keys simultaneously
657
701
  - You're benchmarking and Persy shows wins on your data
658
702
 
@@ -660,41 +704,82 @@ BEAM supports two persistent storage backends for the embedded database layer. B
660
704
  - ✅ Multiple writers proceed in parallel on disjoint keys
661
705
  - ✅ Optional `background_ops` for fsync offloading
662
706
  - ❌ Younger ecosystem, fewer Stack Overflow answers
663
- - ❌ Requires more careful substrate reading when debugging
707
+ - ❌ Author has acknowledged crash-safety issues; development has slowed
708
+ - ❌ No WASM path (native-only)
664
709
  - ❌ Performance characteristics need your own benchmarks
665
710
 
711
+ ### Comparison Summary
712
+
713
+ | | redb | fjall | Persy |
714
+ |---|---|---|---|
715
+ | **Structure** | B+tree | LSM-tree | Segment store |
716
+ | **Write path** | fsync per Put | WAL journal append | Per-tx isolation |
717
+ | **Durability** | Bulletproof (fsync) | Crash-safe (WAL), not power-safe | Per-tx |
718
+ | **Read speed** | Fastest (mmap) | Slower (multi-level) | Moderate |
719
+ | **Write speed** | Slowest (fsync) | Fastest (journal) | Moderate |
720
+ | **Concurrency** | Single-writer | Multi-writer | Multi-writer |
721
+ | **Compression** | None | LZ4 (free) | None |
722
+ | **WASM** | No | No | No |
723
+ | **Maturity** | Most mature | Active dev | Slowing dev |
724
+ | **Best for** | Single-node | Multi-node P2P | High-concurrency |
725
+
666
726
  ### Selection
667
727
 
668
- Persy is a **build-time** feature, not a runtime flag. Build with `--features persy` to enable Persy support in the migration subcommand:
728
+ Storage backends are **build-time** features, not runtime flags:
669
729
 
670
730
  ```bash
671
731
  # Default build — redb only
672
732
  cargo build --release --bin beam
673
733
 
674
- # With Persy support (enables migration subcommand)
734
+ # With fjall support
735
+ cargo build --release --bin beam --features fjall
736
+
737
+ # With Persy and/or fjall support (enables migration subcommand)
675
738
  cargo build --release --bin beam --features persy
739
+ cargo build --release --bin beam --features fjall
676
740
 
677
741
  # Run with redb (default)
678
- cargo run --release --bin beam -- --port 4944
742
+ cargo run --release --bin beam -- start --port 4944
679
743
 
680
744
  # In-memory only (no persistence)
681
- cargo run --release --bin beam -- --port 4944 --memory-storage true
745
+ cargo run --release --bin beam -- start --port 4944 --memory-storage true
746
+ ```
747
+
748
+ **Library usage** — use any backend programmatically (requires corresponding feature flags):
749
+
750
+ ```rust,ignore
751
+ use beam::adapters::{FjallStorage, RedbStorage, MemoryStorage};
752
+
753
+ // fjall (recommended for multi-node, requires --features fjall)
754
+ let storage = FjallStorage::new_with_config(Config::default(), "beam.fjall");
755
+
756
+ // redb (default, best for single-node)
757
+ let storage = RedbStorage::new_with_config(Config::default(), "beam.redb", None);
758
+
759
+ // in-memory (ephemeral)
760
+ let storage = MemoryStorage::new();
682
761
  ```
683
762
 
684
763
  ### Migration Between Backends
685
764
 
686
- The `beam migrate` subcommand converts between formats (requires `--features persy` for Persy support):
765
+ The `beam migrate` subcommand converts between all supported storage formats (requires `--features persy` and/or `--features fjall`):
687
766
 
688
767
  ```bash
689
768
  # Preview without writing
690
769
  beam migrate --from redb --to persy --source ./data.redb --target ./data.persy --dry-run
691
770
 
692
- # Execute migration
771
+ # redb ↔ persy
693
772
  beam migrate --from redb --to persy --source ./data.redb --target ./data.persy
694
-
695
- # Reverse direction
696
773
  beam migrate --from persy --to redb --source ./data.persy --target ./data.redb
697
774
 
775
+ # redb ↔ fjall (fjall uses a directory path, not a file)
776
+ beam migrate --from redb --to fjall --source ./data.redb --target ./data.fjall
777
+ beam migrate --from fjall --to redb --source ./data.fjall --target ./data.redb
778
+
779
+ # fjall ↔ persy
780
+ beam migrate --from fjall --to persy --source ./data.fjall --target ./data.persy
781
+ beam migrate --from persy --to fjall --source ./data.persy --target ./data.fjall
782
+
698
783
  # Overwrite existing target
699
784
  beam migrate --from redb --to persy --source ./data.redb --target ./data.persy --force
700
785
 
@@ -702,11 +787,11 @@ beam migrate --from redb --to persy --source ./data.redb --target ./data.persy -
702
787
  beam migrate --from redb --to persy --source ./data.redb --target ./data.persy --batch-size 5000
703
788
  ```
704
789
 
705
- Migration uses single-transaction-per-batch for safety and includes checksum verification. See `docs/migrations/migration-guide.md` for the full procedure including rollback.
790
+ Migration uses a reader/writer architecture with a canonical intermediate format — each backend has one reader and one writer. Adding a new storage backend requires only two functions, not N² pairwise paths. See `docs/migrations/migration-guide.md` for the full procedure including rollback.
706
791
 
707
792
  ### Mixed Meshes
708
793
 
709
- Nodes with different storage backends interoperate transparently. A redb node, a Persy node, and an in-memory node form a valid mesh. The wire protocol carries the data; storage is a local choice.
794
+ Nodes with different storage backends interoperate transparently. A redb node, a fjall node, a Persy node, and an in-memory node form a valid mesh. The wire protocol carries the data; storage is a local choice.
710
795
 
711
796
  **Cross-backend mesh verified** by `tests/cross_backend_mesh_e2e.rs`: 2 redb nodes + 1 Persy node converge correctly under the standard Put/Get protocol.
712
797
 
@@ -714,6 +799,7 @@ Nodes with different storage backends interoperate transparently. A redb node, a
714
799
 
715
800
  - The `beam_meta_v1` metadata table from redb (last-write timestamps) is not preserved when migrating redb → Persy. This metadata is not currently used by the actor framework, so the loss is cosmetic.
716
801
  - The migration tool is single-threaded per batch. For datasets larger than ~100k records, run during a maintenance window.
802
+ - fjall uses a directory path for storage (LSM-tree), while redb and Persy use single files. Migration involving fjall creates a directory at the target path.
717
803
 
718
804
  ---
719
805
 
@@ -786,10 +872,10 @@ BEAM uses Gun.js's JSON wire format. Messages are JSON objects with these fields
786
872
 
787
873
  | Flag | Required | Description |
788
874
  |------|----------|-------------|
789
- | `--from` | Yes | Source backend: `redb` or `persy` |
790
- | `--to` | Yes | Target backend: `redb` or `persy` |
791
- | `--source` | Yes | Path to source database file |
792
- | `--target` | Yes | Path to target database file (will be created) |
875
+ | `--from` | Yes | Source backend: `redb`, `persy`, or `fjall` |
876
+ | `--to` | Yes | Target backend: `redb`, `persy`, or `fjall` |
877
+ | `--source` | Yes | Path to source database (file for redb/persy, directory for fjall) |
878
+ | `--target` | Yes | Path to target database (file for redb/persy, directory for fjall) |
793
879
  | `--batch-size` | No | Records per batch (default: 1000) |
794
880
  | `--force` | No | Overwrite target if it already exists |
795
881
  | `--dry-run` | No | Preview without writing |
@@ -805,6 +891,8 @@ let config = Config {
805
891
  broadcast_buffer_size: 4096,
806
892
  ice_servers: vec!["stun:stun.l.google.com:19302".into()],
807
893
  dedup_capacity: 100_000,
894
+ mailbox_capacity: 65536,
895
+ child_mailbox_capacity: 256,
808
896
  };
809
897
  # }
810
898
  ```
@@ -872,6 +960,18 @@ cargo test
872
960
  # With WebRTC tests
873
961
  cargo test --features webrtc
874
962
 
963
+ # With fjall storage tests
964
+ cargo test --features fjall
965
+
966
+ # With Persy tests (includes redb↔persy migration tests)
967
+ cargo test --features persy
968
+
969
+ # With fjall tests (includes redb↔fjall migration tests)
970
+ cargo test --features fjall
971
+
972
+ # With both (includes all migration path tests)
973
+ cargo test --features fjall,persy
974
+
875
975
  # Lint (zero warnings required)
876
976
  cargo clippy -- -D warnings
877
977
 
@@ -903,6 +1003,7 @@ cargo test --test wire_live -- --ignored # Layer 3: live integration (needs Nod
903
1003
  | `redb_storage_persists` | Data survives restart with redb storage |
904
1004
  | `redb_storage_flush_returns_ok` | Flush ack protocol |
905
1005
  | `cross_backend_mesh_e2e` | 2 redb + 1 Persy nodes converge correctly |
1006
+ | `fjall_e2e` | 6 fjall storage tests: put-get, sequential, nested, LWW, flush, isolation (`--features fjall`) |
906
1007
  | `wire_tests` | 36 golden JSON fixtures — wire protocol spec as tests |
907
1008
  | `wire_live` | Live BEAM ↔ Gun.js bidirectional sync (4 scenarios) |
908
1009
 
@@ -1006,9 +1107,15 @@ cargo test --release --test relay_throughput_bench -- --ignored --nocapture
1006
1107
  # Micro-benchmarks (hot-path components)
1007
1108
  cargo bench --bench my_benchmark -- "wire_|dup_check|actor_mailbox"
1008
1109
 
1009
- # Storage benchmarks
1110
+ # Storage benchmarks (redb only by default)
1010
1111
  cargo bench --bench my_benchmark -- "write_storm|read_storm|mixed"
1011
1112
 
1113
+ # Storage benchmarks with fjall (head-to-head comparison)
1114
+ cargo bench --features fjall --bench my_benchmark -- "write_storm|read_storm|mixed"
1115
+
1116
+ # Storage benchmarks with persy
1117
+ cargo bench --features persy --bench my_benchmark -- "write_storm|read_storm|mixed"
1118
+
1012
1119
  # Live metrics endpoint (while relay is running)
1013
1120
  curl http://localhost:8080/metrics
1014
1121
  ```
@@ -1021,11 +1128,12 @@ methodology and analysis.
1021
1128
  | Feature | Default | Enables |
1022
1129
  |---------|---------|---------|
1023
1130
  | `webrtc` | No | `dep:str0m`, `dep:stun` — direct P2P connections via WebRTC data channels |
1131
+ | `fjall` | No | `dep:fjall` — LSM-tree storage backend (recommended for multi-node deployments) |
1024
1132
  | `persy` | No | `dep:persy` — Persy storage backend for high-concurrency workloads |
1025
1133
 
1026
- Without `webrtc`, the `stun` module and `WebRtcPeer` adapter are stubbed out (functions return `None`). Without `persy`, the `PersyStorage` adapter is not compiled in and migration to/from Persy is unavailable.
1134
+ Without `webrtc`, the `stun` module and `WebRtcPeer` adapter are stubbed out (functions return `None`). Without `persy`, the `PersyStorage` adapter is not compiled in and migration to/from Persy is unavailable. Without `fjall`, the `FjallStorage` adapter is not compiled in and migration to/from fjall is unavailable. Migration requires at least one of `persy` or `fjall` features.
1027
1135
 
1028
- **WASM**: When targeting `wasm32-unknown-unknown`, native-only modules (redb, Persy, tokio-tungstenite, multicast) are cfg-gated out. Browser adapters (`wasm_ws`, `wasm_idb`) are compiled in. Timer functions (`sleep`, `timeout`, `interval`) are provided by `tokio_with_wasm` via the `tokio_time` shim module instead of tokio's `time` feature (which panics on WASM). The `wasm.rs` module provides `#[wasm_bindgen]` JavaScript bindings. Build with `wasm-pack build --target web --release`.
1136
+ **WASM**: When targeting `wasm32-unknown-unknown`, native-only modules (redb, fjall, Persy, tokio-tungstenite, multicast) are cfg-gated out. Browser adapters (`wasm_ws`, `wasm_idb`) are compiled in. Timer functions (`sleep`, `timeout`, `interval`) are provided by `tokio_with_wasm` via the `tokio_time` shim module instead of tokio's `time` feature (which panics on WASM). The `wasm.rs` module provides `#[wasm_bindgen]` JavaScript bindings. Build with `wasm-pack build --target web --release`.
1029
1137
 
1030
1138
  ---
1031
1139
 
package/beam.d.ts CHANGED
@@ -49,6 +49,35 @@ export class Beam {
49
49
  * then flushed automatically.
50
50
  */
51
51
  static new_persistent(): Beam;
52
+ /**
53
+ * Creates a new BEAM node with OPFS (Origin Private File System) persistent storage.
54
+ *
55
+ * Data is stored as postcard-serialized files in the browser's OPFS
56
+ * and survives page reloads and browser restarts. Requires a secure
57
+ * context (HTTPS or localhost). OPFS is available in all modern browsers
58
+ * (Chrome 102+, Firefox 111+, Safari 15.2+).
59
+ *
60
+ * The OPFS directory opens asynchronously — writes are buffered until
61
+ * the directory is ready, then flushed automatically.
62
+ *
63
+ * ```js
64
+ * import init, { Beam } from "./beam.js";
65
+ * await init();
66
+ * const beam = Beam.new_with_opfs();
67
+ * beam.connect("ws://relay.example.com");
68
+ * beam.put("chat.001", "hello");
69
+ * // Reload page — data is still there.
70
+ * ```
71
+ */
72
+ static new_with_opfs(): Beam;
73
+ /**
74
+ * Creates a new BEAM node with OPFS storage at a custom directory name.
75
+ *
76
+ * ```js
77
+ * const beam = Beam.new_with_opfs_name("myapp_data");
78
+ * ```
79
+ */
80
+ static new_with_opfs_name(name: string): Beam;
52
81
  /**
53
82
  * Subscribes to child updates at the given path.
54
83
  *
@@ -108,6 +137,8 @@ export interface InitOutput {
108
137
  readonly beam_get: (a: number, b: number, c: number) => number;
109
138
  readonly beam_new: () => number;
110
139
  readonly beam_new_persistent: () => number;
140
+ readonly beam_new_with_opfs: () => number;
141
+ readonly beam_new_with_opfs_name: (a: number, b: number) => number;
111
142
  readonly beam_on: (a: number, b: number, c: number, d: number) => void;
112
143
  readonly beam_put: (a: number, b: number, c: number, d: number, e: number) => void;
113
144
  readonly beam_put_bool: (a: number, b: number, c: number, d: number) => void;
@@ -115,11 +146,11 @@ export interface InitOutput {
115
146
  readonly beam_put_num: (a: number, b: number, c: number, d: number) => void;
116
147
  readonly beam_stop: (a: number) => void;
117
148
  readonly task_worker_entry_point: (a: number, b: number) => void;
118
- readonly __wasm_bindgen_func_elem_1109: (a: number, b: number, c: number, d: number) => void;
119
- readonly __wasm_bindgen_func_elem_1123: (a: number, b: number, c: number, d: number) => void;
120
- readonly __wasm_bindgen_func_elem_232: (a: number, b: number, c: number) => void;
121
- readonly __wasm_bindgen_func_elem_232_2: (a: number, b: number, c: number) => void;
122
- readonly __wasm_bindgen_func_elem_232_3: (a: number, b: number, c: number) => void;
149
+ readonly __wasm_bindgen_func_elem_1218: (a: number, b: number, c: number, d: number) => void;
150
+ readonly __wasm_bindgen_func_elem_1232: (a: number, b: number, c: number, d: number) => void;
151
+ readonly __wasm_bindgen_func_elem_252: (a: number, b: number, c: number) => void;
152
+ readonly __wasm_bindgen_func_elem_252_2: (a: number, b: number, c: number) => void;
153
+ readonly __wasm_bindgen_func_elem_252_3: (a: number, b: number, c: number) => void;
123
154
  readonly __wbindgen_export: (a: number, b: number) => number;
124
155
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
125
156
  readonly __wbindgen_export3: (a: number) => void;
package/beam.js CHANGED
@@ -83,6 +83,46 @@ export class Beam {
83
83
  const ret = wasm.beam_new_persistent();
84
84
  return Beam.__wrap(ret);
85
85
  }
86
+ /**
87
+ * Creates a new BEAM node with OPFS (Origin Private File System) persistent storage.
88
+ *
89
+ * Data is stored as postcard-serialized files in the browser's OPFS
90
+ * and survives page reloads and browser restarts. Requires a secure
91
+ * context (HTTPS or localhost). OPFS is available in all modern browsers
92
+ * (Chrome 102+, Firefox 111+, Safari 15.2+).
93
+ *
94
+ * The OPFS directory opens asynchronously — writes are buffered until
95
+ * the directory is ready, then flushed automatically.
96
+ *
97
+ * ```js
98
+ * import init, { Beam } from "./beam.js";
99
+ * await init();
100
+ * const beam = Beam.new_with_opfs();
101
+ * beam.connect("ws://relay.example.com");
102
+ * beam.put("chat.001", "hello");
103
+ * // Reload page — data is still there.
104
+ * ```
105
+ * @returns {Beam}
106
+ */
107
+ static new_with_opfs() {
108
+ const ret = wasm.beam_new_with_opfs();
109
+ return Beam.__wrap(ret);
110
+ }
111
+ /**
112
+ * Creates a new BEAM node with OPFS storage at a custom directory name.
113
+ *
114
+ * ```js
115
+ * const beam = Beam.new_with_opfs_name("myapp_data");
116
+ * ```
117
+ * @param {string} name
118
+ * @returns {Beam}
119
+ */
120
+ static new_with_opfs_name(name) {
121
+ const ptr0 = passStringToWasm0(name, wasm.__wbindgen_export, wasm.__wbindgen_export2);
122
+ const len0 = WASM_VECTOR_LEN;
123
+ const ret = wasm.beam_new_with_opfs_name(ptr0, len0);
124
+ return Beam.__wrap(ret);
125
+ }
86
126
  /**
87
127
  * Subscribes to child updates at the given path.
88
128
  *
@@ -215,6 +255,10 @@ function __wbg_get_imports() {
215
255
  __wbg__wbg_cb_unref_be22cc64ae6946a0: function(arg0) {
216
256
  getObject(arg0)._wbg_cb_unref();
217
257
  },
258
+ __wbg_arrayBuffer_393639d72165e90f: function(arg0) {
259
+ const ret = getObject(arg0).arrayBuffer();
260
+ return addHeapObject(ret);
261
+ },
218
262
  __wbg_call_35dba3c747ad7521: function() { return handleError(function (arg0, arg1, arg2) {
219
263
  const ret = getObject(arg0).call(getObject(arg1), getObject(arg2));
220
264
  return addHeapObject(ret);
@@ -222,10 +266,18 @@ function __wbg_get_imports() {
222
266
  __wbg_close_b857478a8d4c1a16: function() { return handleError(function (arg0) {
223
267
  getObject(arg0).close();
224
268
  }, arguments); },
269
+ __wbg_close_bff888804a3b665e: function(arg0) {
270
+ const ret = getObject(arg0).close();
271
+ return addHeapObject(ret);
272
+ },
225
273
  __wbg_createObjectStore_d3884936b845900f: function() { return handleError(function (arg0, arg1, arg2) {
226
274
  const ret = getObject(arg0).createObjectStore(getStringFromWasm0(arg1, arg2));
227
275
  return addHeapObject(ret);
228
276
  }, arguments); },
277
+ __wbg_createWritable_376a22cc9862fab6: function(arg0) {
278
+ const ret = getObject(arg0).createWritable();
279
+ return addHeapObject(ret);
280
+ },
229
281
  __wbg_data_57d8ce4eb5f0a433: function(arg0) {
230
282
  const ret = getObject(arg0).data;
231
283
  return addHeapObject(ret);
@@ -247,6 +299,26 @@ function __wbg_get_imports() {
247
299
  __wbg_error_dd408a7b3cb542dd: function(arg0) {
248
300
  console.error(getObject(arg0));
249
301
  },
302
+ __wbg_getDirectoryHandle_3b6e6ec9cd618b74: function(arg0, arg1, arg2, arg3) {
303
+ const ret = getObject(arg0).getDirectoryHandle(getStringFromWasm0(arg1, arg2), getObject(arg3));
304
+ return addHeapObject(ret);
305
+ },
306
+ __wbg_getDirectory_2cc7169b6007ef84: function(arg0) {
307
+ const ret = getObject(arg0).getDirectory();
308
+ return addHeapObject(ret);
309
+ },
310
+ __wbg_getFileHandle_275e470e839818da: function(arg0, arg1, arg2) {
311
+ const ret = getObject(arg0).getFileHandle(getStringFromWasm0(arg1, arg2));
312
+ return addHeapObject(ret);
313
+ },
314
+ __wbg_getFileHandle_ba98be7045eab758: function(arg0, arg1, arg2, arg3) {
315
+ const ret = getObject(arg0).getFileHandle(getStringFromWasm0(arg1, arg2), getObject(arg3));
316
+ return addHeapObject(ret);
317
+ },
318
+ __wbg_getFile_f042da3b150a3230: function(arg0) {
319
+ const ret = getObject(arg0).getFile();
320
+ return addHeapObject(ret);
321
+ },
250
322
  __wbg_getRandomValues_a608c4436c19407a: function() { return handleError(function (arg0, arg1) {
251
323
  globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
252
324
  }, arguments); },
@@ -272,9 +344,17 @@ function __wbg_get_imports() {
272
344
  const ret = result;
273
345
  return ret;
274
346
  },
347
+ __wbg_length_36bd29c6848c2144: function(arg0) {
348
+ const ret = getObject(arg0).length;
349
+ return ret;
350
+ },
275
351
  __wbg_log_e6372b4fbfc9f81e: function(arg0) {
276
352
  console.log(getObject(arg0));
277
353
  },
354
+ __wbg_navigator_6cfdd5fa246d910f: function(arg0) {
355
+ const ret = getObject(arg0).navigator;
356
+ return addHeapObject(ret);
357
+ },
278
358
  __wbg_new_20a7c62e9b30cbf7: function() { return handleError(function (arg0, arg1) {
279
359
  const ret = new WebSocket(getStringFromWasm0(arg0, arg1));
280
360
  return addHeapObject(ret);
@@ -290,7 +370,7 @@ function __wbg_get_imports() {
290
370
  const a = state0.a;
291
371
  state0.a = 0;
292
372
  try {
293
- return __wasm_bindgen_func_elem_1123(a, state0.b, arg0, arg1);
373
+ return __wasm_bindgen_func_elem_1232(a, state0.b, arg0, arg1);
294
374
  } finally {
295
375
  state0.a = a;
296
376
  }
@@ -301,6 +381,14 @@ function __wbg_get_imports() {
301
381
  state0.a = 0;
302
382
  }
303
383
  },
384
+ __wbg_new_77cc4f4f472aeb81: function(arg0) {
385
+ const ret = new Uint8Array(getObject(arg0));
386
+ return addHeapObject(ret);
387
+ },
388
+ __wbg_new_ebe3e0f6837f0879: function() {
389
+ const ret = new Object();
390
+ return addHeapObject(ret);
391
+ },
304
392
  __wbg_new_typed_cceaf62d8d95e9f2: function(arg0, arg1) {
305
393
  try {
306
394
  var state0 = {a: arg0, b: arg1};
@@ -308,7 +396,7 @@ function __wbg_get_imports() {
308
396
  const a = state0.a;
309
397
  state0.a = 0;
310
398
  try {
311
- return __wasm_bindgen_func_elem_1123(a, state0.b, arg0, arg1);
399
+ return __wasm_bindgen_func_elem_1232(a, state0.b, arg0, arg1);
312
400
  } finally {
313
401
  state0.a = a;
314
402
  }
@@ -342,6 +430,9 @@ function __wbg_get_imports() {
342
430
  __wbg_postMessage_6dcc1574fef77104: function() { return handleError(function (arg0, arg1) {
343
431
  getObject(arg0).postMessage(getObject(arg1));
344
432
  }, arguments); },
433
+ __wbg_prototypesetcall_de8e0d9553586985: function(arg0, arg1, arg2) {
434
+ Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));
435
+ },
345
436
  __wbg_put_5e0ae8c80bb952a7: function() { return handleError(function (arg0, arg1, arg2) {
346
437
  const ret = getObject(arg0).put(getObject(arg1), getObject(arg2));
347
438
  return addHeapObject(ret);
@@ -353,10 +444,6 @@ function __wbg_get_imports() {
353
444
  const ret = getObject(arg0).queueMicrotask;
354
445
  return addHeapObject(ret);
355
446
  },
356
- __wbg_readyState_fe79161592fd15ce: function(arg0) {
357
- const ret = getObject(arg0).readyState;
358
- return ret;
359
- },
360
447
  __wbg_resolve_020f95d838c6ef25: function(arg0) {
361
448
  const ret = Promise.resolve(getObject(arg0));
362
449
  return addHeapObject(ret);
@@ -371,6 +458,12 @@ function __wbg_get_imports() {
371
458
  __wbg_setTimeout_593504220b42c5a5: function(arg0, arg1) {
372
459
  globalThis.setTimeout(getObject(arg0), arg1);
373
460
  },
461
+ __wbg_set_create_7df81b728041e33b: function(arg0, arg1) {
462
+ getObject(arg0).create = arg1 !== 0;
463
+ },
464
+ __wbg_set_create_c415df73c1fec0ca: function(arg0, arg1) {
465
+ getObject(arg0).create = arg1 !== 0;
466
+ },
374
467
  __wbg_set_onclose_cb71fea4ad9056fc: function(arg0, arg1) {
375
468
  getObject(arg0).onclose = getObject(arg1);
376
469
  },
@@ -415,6 +508,10 @@ function __wbg_get_imports() {
415
508
  const ret = typeof window === 'undefined' ? null : window;
416
509
  return isLikeNone(ret) ? 0 : addHeapObject(ret);
417
510
  },
511
+ __wbg_storage_9c75a59b053f787c: function(arg0) {
512
+ const ret = getObject(arg0).storage;
513
+ return addHeapObject(ret);
514
+ },
418
515
  __wbg_target_13424fe1cdc436ac: function(arg0) {
419
516
  const ret = getObject(arg0).target;
420
517
  return isLikeNone(ret) ? 0 : addHeapObject(ret);
@@ -431,24 +528,28 @@ function __wbg_get_imports() {
431
528
  const ret = getObject(arg0).transaction(getStringFromWasm0(arg1, arg2), __wbindgen_enum_IdbTransactionMode[arg3]);
432
529
  return addHeapObject(ret);
433
530
  }, arguments); },
531
+ __wbg_write_6c58d60c26aaeef5: function() { return handleError(function (arg0, arg1, arg2) {
532
+ const ret = getObject(arg0).write(getArrayU8FromWasm0(arg1, arg2));
533
+ return addHeapObject(ret);
534
+ }, arguments); },
434
535
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
435
536
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 15, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
436
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_232);
537
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_252);
437
538
  return addHeapObject(ret);
438
539
  },
439
540
  __wbindgen_cast_0000000000000002: function(arg0, arg1) {
440
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 272, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
441
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1109);
541
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 290, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
542
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_1218);
442
543
  return addHeapObject(ret);
443
544
  },
444
545
  __wbindgen_cast_0000000000000003: function(arg0, arg1) {
445
546
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 15, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
446
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_232_2);
547
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_252_2);
447
548
  return addHeapObject(ret);
448
549
  },
449
550
  __wbindgen_cast_0000000000000004: function(arg0, arg1) {
450
551
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 15, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
451
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_232_3);
552
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_252_3);
452
553
  return addHeapObject(ret);
453
554
  },
454
555
  __wbindgen_cast_0000000000000005: function(arg0) {
@@ -475,22 +576,22 @@ function __wbg_get_imports() {
475
576
  };
476
577
  }
477
578
 
478
- function __wasm_bindgen_func_elem_232(arg0, arg1, arg2) {
479
- wasm.__wasm_bindgen_func_elem_232(arg0, arg1, addHeapObject(arg2));
579
+ function __wasm_bindgen_func_elem_252(arg0, arg1, arg2) {
580
+ wasm.__wasm_bindgen_func_elem_252(arg0, arg1, addHeapObject(arg2));
480
581
  }
481
582
 
482
- function __wasm_bindgen_func_elem_232_2(arg0, arg1, arg2) {
483
- wasm.__wasm_bindgen_func_elem_232_2(arg0, arg1, addHeapObject(arg2));
583
+ function __wasm_bindgen_func_elem_252_2(arg0, arg1, arg2) {
584
+ wasm.__wasm_bindgen_func_elem_252_2(arg0, arg1, addHeapObject(arg2));
484
585
  }
485
586
 
486
- function __wasm_bindgen_func_elem_232_3(arg0, arg1, arg2) {
487
- wasm.__wasm_bindgen_func_elem_232_3(arg0, arg1, addHeapObject(arg2));
587
+ function __wasm_bindgen_func_elem_252_3(arg0, arg1, arg2) {
588
+ wasm.__wasm_bindgen_func_elem_252_3(arg0, arg1, addHeapObject(arg2));
488
589
  }
489
590
 
490
- function __wasm_bindgen_func_elem_1109(arg0, arg1, arg2) {
591
+ function __wasm_bindgen_func_elem_1218(arg0, arg1, arg2) {
491
592
  try {
492
593
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
493
- wasm.__wasm_bindgen_func_elem_1109(retptr, arg0, arg1, addHeapObject(arg2));
594
+ wasm.__wasm_bindgen_func_elem_1218(retptr, arg0, arg1, addHeapObject(arg2));
494
595
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
495
596
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
496
597
  if (r1) {
@@ -501,8 +602,8 @@ function __wasm_bindgen_func_elem_1109(arg0, arg1, arg2) {
501
602
  }
502
603
  }
503
604
 
504
- function __wasm_bindgen_func_elem_1123(arg0, arg1, arg2, arg3) {
505
- wasm.__wasm_bindgen_func_elem_1123(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
605
+ function __wasm_bindgen_func_elem_1232(arg0, arg1, arg2, arg3) {
606
+ wasm.__wasm_bindgen_func_elem_1232(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
506
607
  }
507
608
 
508
609
 
package/beam_bg.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "David Newman <david.r.newman@proton.me>"
7
7
  ],
8
8
  "description": "BEAM — distributed graph database syncing over WebSocket, WebRTC, and multicast. Successor to rod.",
9
- "version": "0.12.0",
9
+ "version": "0.17.0",
10
10
  "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",