Skip to main content

blossom_cache 0.5.1 copy "blossom_cache: ^0.5.1" to clipboard
blossom_cache: ^0.5.1 copied to clipboard

A network-free local Blossom blob store for Dart. Works on web, native, and server.

blossom_cache #

A network-free local Blossom blob store for Dart. Same code runs on web, native, and server.

Blobs are addressed by their sha256 hash. The cache computes the hash from the bytes by default; callers that already have one (e.g. from a Blossom server response, or from a faster platform digest like crypto.subtle.digest on web) can pass it via the sha256: parameter to skip the computation. Supplied hashes are trusted as-is and not verified.

Usage #

Pick the IdbFactory for your target, then open the cache:

import 'dart:typed_data';
import 'package:blossom_cache/blossom_cache.dart';

// Web
import 'package:idb_shim/idb_browser.dart';
final cache = await IdbBlossomCache.open(factory: idbFactoryBrowser);

// Native / server, small caches (sembast, loaded in memory on open).
// For large caches, see FileBlossomCache below.
import 'package:idb_shim/idb_io.dart';
final cache = await IdbBlossomCache.open(factory: idbFactorySembastIo);

// Tests / ephemeral
import 'package:idb_shim/idb_client_memory.dart';
final cache = await IdbBlossomCache.open(factory: newIdbFactoryMemory());

Then:

// Hash computed by the cache:
final descriptor = await cache.put(bytes, type: 'image/png');
final sha = descriptor.sha256;

// Or, when you already have it:
await cache.put(bytes, sha256: sha, type: 'image/png');

final read = await cache.get(sha);    // Uint8List?, updates lastAccessedAt
final meta = await cache.head(sha);   // BlobDescriptor?, metadata only
final all  = await cache.list();      // List<BlobDescriptor>
await cache.delete(sha);              // bool, manual delete

await cache.pin(sha);                 // protect from future auto-eviction
await cache.unpin(sha);
await cache.pin(sha, by: 'chat');     // pin on behalf of a holder
await cache.unpinAll('chat');         // release every pin that holder has

await cache.clearAllLocalData();      // wipe everything, pinned included

clearAllLocalData leaves the cache open and usable, so it fits a "clear cache" button or a logout flow:

Future<void> logout() async {
  await cache.clearAllLocalData();
  // ... then the rest of your session cleanup
}

Large caches on native (FileBlossomCache) #

idbFactorySembastIo keeps the whole database in memory, so it suits caches up to about 100 MB (sembast's own recommendation). For anything bigger, such as an image library of several GB, use FileBlossomCache from package:blossom_cache/blossom_cache_io.dart. It stores each blob as its own file and keeps only the metadata in sembast, so memory use stays flat whatever the size of the cache.

import 'package:blossom_cache/blossom_cache_io.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

final support = await getApplicationSupportDirectory();
final cache = await FileBlossomCache.open(
  path: p.join(support.path, 'blossom_cache'),
);

Give it a directory of its own. Prefer Application Support over Caches: the system may empty Caches to free space, pinned blobs included. Keys become file names, so put only accepts a lowercase hex sha256, which is what Blossom uses anyway.

FileBlossomCache needs dart:io and is not available on web. It lives in its own library so that package:blossom_cache/blossom_cache.dart keeps working everywhere.

Bounded cache (LRU eviction) #

Wrap any BlossomCache in a BoundedBlossomCache to cap total size. When a put would exceed the limit, the decorator evicts blobs by lastAccessedAt ascending (LRU), skipping pinned blobs:

final cache = BoundedBlossomCache(
  inner: await IdbBlossomCache.open(factory: idbFactoryBrowser),
  maxSize: 500 * 1024 * 1024, // 500 MB
);

If the cache cannot make enough room (a single blob is bigger than maxSize, or every remaining blob is pinned), put throws BlossomCacheOverflowException and the cache is left unchanged.

Pinning #

A blob can be pinned by several holders. BoundedBlossomCache will not auto-evict a blob while at least one holder pins it. Manual delete ignores pins and always removes the blob.

A holder is any string you choose: a feature name, an account pubkey, etc. pin and unpin use BlossomCache.defaultHolder when none is given, which is enough for a single consumer.

// Avatar, evictable
await cache.put(avatarBytes, type: 'image/png');

// Important file, never auto-evicted while 'chat' holds it
await cache.put(fileBytes, type: 'application/pdf', pinBy: 'chat');

// Another feature needs it too
await cache.pin(fileSha, by: 'drafts');

// 'chat' lets go, the blob stays pinned by 'drafts'
await cache.unpin(fileSha, by: 'chat');

Holders make per-account logout possible without touching other accounts:

Future<void> logout(String pubkey) async {
  await cache.unpinAll(pubkey); // blobs no one else pins become evictable
}

Re-putting a blob keeps its existing holders. BlobDescriptor.pinnedBy lists them and BlobDescriptor.pinned is true when that set is not empty.

Custom backends #

BlossomCache is an abstract class. Implement it against any storage you like (OPFS, S3, ...). The interface is intentionally small: put, get, head, delete, pin, unpin, unpinAll, list, clearAllLocalData.

1
likes
160
points
532
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A network-free local Blossom blob store for Dart. Works on web, native, and server.

Repository (GitHub)
View/report issues

Topics

#blossom #nostr #cache #storage #blob

License

MIT (license)

Dependencies

crypto, idb_shim, path, sembast

More

Packages that depend on blossom_cache