@crvouga/mockingbird-service-postgres 3.11.0 → 3.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog — @crvouga/mockingbird-service-postgres
2
2
 
3
+ ## 3.12.0 (2026-10-24)
4
+
5
+ ### Features
6
+
7
+ - add buffered SQL durability ([288bdcc](https://github.com/crvouga/mockingbird/commit/288bdcca84582787a54c4fdf9e803db9aac0b8d7))
8
+ - add efficient browser durability ([a8be579](https://github.com/crvouga/mockingbird/commit/a8be5799a883ddf6f731208e06f399410de4661a))
9
+
3
10
  ## 3.11.0 (2026-10-21)
4
11
 
5
12
  ### Features
package/README.md CHANGED
@@ -553,7 +553,95 @@ without changing an application database. Accepted statements remain semanticall
553
553
  see [schema preflight and row-version metadata](COMPATIBILITY.md#schema-preflight-and-row-version-metadata)
554
554
  for limits, the report contract and `xmin` support.
555
555
 
556
- Stable runtime exports of the main entry:
556
+ Browser durability is opt-in through `openDurableDatabase({ key, backend?, databaseOptions?, pageSize? })`.
557
+ It returns a `Durable<Database>` handle: `.value` is the existing synchronous database,
558
+ `.flush()` saves a batch, `.write(db => ...)` serializes a synchronous batch and saves it, and
559
+ `.close()` saves before closing. Await a save before considering writes durable. Wrap related
560
+ SQL operations in `db.transaction()` when they need rollback as well as persistence.
561
+
562
+ ```ts
563
+ import { openDurableDatabase, IndexedDBDurabilityBackend } from "@crvouga/mockingbird-service-postgres";
564
+
565
+ const backend = new IndexedDBDurabilityBackend({ name: "my-app" });
566
+ const durable = await openDurableDatabase({ key: "main", backend });
567
+ const db = durable.value;
568
+ // Use db.exec / db.prepare / db.transaction as before.
569
+ await durable.flush(); // acknowledges an atomic storage commit
570
+ await durable.close();
571
+ await backend.close(); // release the connection when its last user finishes
572
+ ```
573
+
574
+ `IndexedDBDurabilityBackend` is the default and stores binary pages with atomic revision checks;
575
+ it sends only changed pages in one transaction. `durability: "strict"` is the default;
576
+ `"relaxed"` is available for explicitly weaker power-loss guarantees. Both settings resolve
577
+ only on the IndexedDB transaction's `complete` event.
578
+ `LocalStorageDurabilityBackend({ storage?, prefix?, locks? })` is a small-image fallback with
579
+ whole-image base64 rewrites and synchronous IO. It uses Web Locks when available; without
580
+ Web Locks, use a single writer per key. `MemoryDurabilityBackend` is a reference adapter.
581
+ A stale save throws `DurabilityConflictError`; reopen to reconcile rather than overwriting
582
+ another tab. Quota/IO failures reject the save, preserve the previously committed image and
583
+ can be retried. Saved data remains subject to the browser's storage quota and eviction policy.
584
+
585
+ `openDurable({ backend, key, codec, pageSize? })` works with any emulator using an injected
586
+ `DurabilityCodec<T>`. The `DurabilityBackend` port exposes `load` and atomic `commit` of a
587
+ `DurablePatch` against an expected revision. `DurableImage` carries the binary image, revision
588
+ and page size. The port and adapters are shared with `@crvouga/mockingbird-core`.
589
+ `withDurability(api, durable)` serializes Fetch requests and flushes before returning each
590
+ response. For other Mockingbird services, inject `durable.value` through their existing
591
+ `sqlite` option, then send requests through this wrapper. Route all writes through the same
592
+ handle/wrapper to preserve serialization; do not mutate the database during an outstanding save.
593
+
594
+ The immediate durability opener encodes one full SQLM/PGMM snapshot per batch and compares fixed-size pages (default
595
+ 16 KiB). This targets small to moderate databases, not unbounded datasets. It avoids per-row
596
+ storage IO, but encoding is still O(database size). It is not an SQLite file or OPFS VFS.
597
+ See [the browser benchmark](https://github.com/crvouga/mockingbird/blob/main/packages/service/sqlite/benchmarks/DURABILITY.md) for measurements and limits.
598
+
599
+ For higher write throughput, use `openBufferedDurableDatabase({ key, backend?, ... })`.
600
+ The returned `BufferedDurable<Database>` keeps the synchronous SQL API and schedules a save
601
+ 1,000 ms after the first unsaved change. This is a background deadline, not a debounce;
602
+ suspended tabs and storage failures can delay it. The default owned IndexedDB adapter uses
603
+ `durability: "relaxed"`. Use one writer per database key. Recent unsaved changes can be lost.
604
+
605
+ ```ts
606
+ import { openBufferedDurableDatabase } from "@crvouga/mockingbird-service-postgres";
607
+
608
+ const durable = await openBufferedDurableDatabase({
609
+ key: "main",
610
+ flushIntervalMs: 1000,
611
+ onError: (error) => console.error("Background save failed", error),
612
+ });
613
+ durable.value.exec("UPDATE items SET value = value + 1 WHERE id = 1");
614
+ // SQL returns immediately; persistence runs in the background.
615
+ await durable.flush(); // use when an acknowledgement is required
616
+ await durable.close(); // flush and release the handle before leaving
617
+ ```
618
+
619
+ Ordinary saves coalesce changed rows and append physical values plus runtime metadata;
620
+ they do not encode or scan every stored row, and recovery does not replay SQL/functions.
621
+ `pending` reports unacknowledged changes/queued work; `lastError` records background failures.
622
+ IO/quota failures retain pending work and retry with bounded backoff; a revision conflict
623
+ stops background persistence. Explicit `write`, `flush`, and `close` reject on failure and
624
+ await storage acknowledgement. Close through the handle, not `durable.value.close()`.
625
+ Do not leave SQL transactions open across a flush. Page-hide persistence is best effort.
626
+
627
+ Checkpoints compact after 256 delta batches or journal growth of at least
628
+ `max(4 MiB, checkpoint size)` by default (`compactAfterBatches`, `compactAfterBytes`).
629
+ Schema changes, unsupported incremental shapes, public raw state/native container access
630
+ and borrowed binary results/parameters conservatively select full checkpoints. Both openers read old
631
+ SQLM/PGMM images and the versioned checkpoint/journal envelope; opting in upgrades the image
632
+ on save. `Snapshot.encode/decode` formats and the existing immediate opener's defaults stay
633
+ unchanged. LocalStorage uses the same port but still rewrites the whole image.
634
+
635
+ `openBufferedDurable` is the reusable controller for other emulators, via an injected
636
+ `IncrementalDurabilityCodec`, `DurabilityScheduler`, `DurabilityLifecycle` and the same
637
+ `DurabilityBackend`. `browserDurabilityLifecycle` supplies optional page-hide notifications.
638
+ `encodeDurableCheckpoint`, `readDurableJournal`, `packDurableRecord`, and
639
+ `unpackDurableRecord` provide the shared versioned envelope/record framing.
640
+
641
+ Stable runtime exports of the main entry include `openDurableDatabase`, `openDurable`,
642
+ `withDurability`, `IndexedDBDurabilityBackend`, `LocalStorageDurabilityBackend`,
643
+ `MemoryDurabilityBackend`, `DurabilityConflictError`, and the existing exports:
644
+
557
645
 
558
646
  | Export | Description |
559
647
  | --- | --- |