Object storage overview
此内容尚不支持你的语言。
The framework’s object-storage backend is an S3-compatible persistence layer. Two implementations ship:
| Backend | Use |
|---|---|
FilesystemObjectStorageBackend | Local files; dev + tests. |
S3ObjectStorageBackend | Anything S3-compatible (AWS S3, MinIO, R2, B2). |
Used by:
ObjectStorageDurableStateStore— durable state in cloud storage.ObjectStorageSnapshotStore— snapshots in cloud storage.
Built on a small interface (PUT / GET / DELETE / LIST + CAS support); both backends speak the same surface.
When to use it
Section titled “When to use it”| You should use object storage when… |
|---|
| You’re cloud-native and S3 (or similar) is your storage platform. |
| You want shared persistence across cluster nodes without running Cassandra. |
| You want server-side encryption via cloud KMS. |
| You want infinite scaling without managing storage capacity. |
For single-node deployments, SQLite is simpler. For high-throughput multi-node persistence, Cassandra is faster. Object storage sits in between — cloud-friendly, decent performance, lots of features.
A minimal example
Section titled “A minimal example”import { DurableStateOptions, ObjectStorageDurableStateStore, ObjectStorageDurableStateStoreOptions, S3ObjectStorageBackend, S3ObjectStorageOptions,} from 'actor-ts/persistence';
const s3ObjectStorageOptions = S3ObjectStorageOptions.create() .withRegion('eu-west-1') .withBucket('my-app-state');const backend = new S3ObjectStorageBackend(s3ObjectStorageOptions);
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create().withBackend(backend);const stateStore = new ObjectStorageDurableStateStore( objectStorageDurableStateStoreOptions,);
const durableStateOptions = DurableStateOptions.create<State>() .withPersistenceId(`cart-${userId}`) .withStore(stateStore) .withEmptyState(() => ({ items: [] }));const cart = system.spawnAnonymous(() => new Cart(durableStateOptions));Cart’s state lives in S3 under cart-<userId> as the object
key. Reads and writes go through the S3 API.
What gets stored
Section titled “What gets stored”Object keys follow a predictable layout:
state/ cart-user-42 # one object per persistenceId cart-user-43
snapshots/ account-42/ seq-100 # snapshots indexed by seqNr seq-200 seq-300The framework manages this layout; you don’t construct keys manually.
The interface
Section titled “The interface”interface ObjectStorageBackend { put(key: string, body: Uint8Array, options?: PutOptions): Promise<{ etag: string }>; get(key: string): Promise<Option<ObjectFetched>>; delete(key: string): Promise<void>; list(options: { prefix: string; limit?: number }): Promise<ObjectInfo[]>;}Small surface — fits AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, etc. Most S3-compatible APIs match exactly.
CAS for optimistic concurrency
Section titled “CAS for optimistic concurrency”await backend.put('state/cart-42', body, { ifMatch: 'previous-etag',});ifMatch lets callers do compare-and-set writes — if the
current ETag differs, the put fails with
ObjectStorageConcurrencyError. Used by
ObjectStorageDurableStateStore to detect concurrent writers
without separate coordination.
ifNoneMatch: '*' is create-only — succeed only if the key
doesn’t exist yet.
Some older S3-compatible stores don’t honor these headers properly. The framework’s backend implementations throw clearly in that case rather than silently ignoring; check your provider’s CAS support before relying on it.
Backends
Section titled “Backends”Filesystem
Section titled “Filesystem”import { FilesystemObjectStorageBackend, FilesystemObjectStorageOptions } from 'actor-ts/persistence';
const filesystemObjectStorageOptions = FilesystemObjectStorageOptions.create().withDir('/var/lib/actor-ts');const backend = new FilesystemObjectStorageBackend( filesystemObjectStorageOptions,);Stores objects as files under rootDir. No network, no
S3 cost. Right for:
- Tests — same code path as production, with local files.
- Local dev — no Minio container required.
- Small single-node deployments — if you specifically want the object-storage interface without S3.
The root directory is the whole of this backend’s world. A key
carrying .., an absolute prefix or a NUL byte is rejected
outright, and every operation additionally canonicalises the path
it is about to touch — so a symlink sitting inside the root and
pointing out of it is refused rather than followed. The root
itself may be a symlink: each operation resolves it and measures
containment against the result, so /var/lib/app -> /mnt/data
works normally.
“The path it is about to touch” means the object names: the key
itself, the directories above it, and its .meta.json sidecar.
The key and its parents are checked for different reasons, and
neither check covers the other — rename never follows a link at
the final component, so a linked parent is what moves where a
body lands, while get and put’s compare-and-swap both open the
key with readFile, which does follow one.
Three limits are worth knowing, because the alternative is
assuming more than the check delivers. It reads the filesystem
and then acts on it, so it refuses a link that is already in place
but cannot win a race against one planted in between — portable
O_NOFOLLOW is not reachable through node:fs. It confines the
backend’s own operations; it is not a sandbox around a directory
other local processes may write to. If untrusted users can create
entries inside the storage root, that is already the larger
problem.
And it does not cover the per-key lock file. Taking the lock is
an exclusive create, which refuses a link rather than following
it — but the stale-lock recovery that runs once the acquisition
timeout is exhausted stats that name, and stat does follow
one. So a link planted at <key>.lock lets a file outside the
root decide whether the lock is treated as abandoned, and a
dangling link there keeps put and delete on that one key
retrying instead of returning.
import { S3ObjectStorageBackend, S3ObjectStorageOptions } from 'actor-ts/persistence';
const s3ObjectStorageOptions = S3ObjectStorageOptions.create() .withRegion('eu-west-1') .withBucket('my-app-state') .withEndpoint('https://s3.eu-west-1.amazonaws.com') // optional override .withCredentials({ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, });const backend = new S3ObjectStorageBackend(s3ObjectStorageOptions);Works with any S3-compatible service. For non-AWS:
// MinIO:const s3ObjectStorageOptions = S3ObjectStorageOptions.create() .withRegion('us-east-1') .withBucket('my-bucket') .withEndpoint('http://minio:9000') .withForcePathStyle(true);new S3ObjectStorageBackend(s3ObjectStorageOptions);
// Cloudflare R2:const s3ObjectStorage2Options = S3ObjectStorageOptions.create() .withRegion('auto') .withBucket('my-bucket') .withEndpoint('https://<account-id>.r2.cloudflarestorage.com');new S3ObjectStorageBackend(s3ObjectStorage2Options);
// Backblaze B2:const s3ObjectStorage3Options = S3ObjectStorageOptions.create() .withRegion('us-west-002') .withBucket('my-bucket') .withEndpoint('https://s3.us-west-002.backblazeb2.com');new S3ObjectStorageBackend(s3ObjectStorage3Options);Optional features
Section titled “Optional features”| Feature | Page |
|---|---|
| Compression (gzip / zstd) | Compression |
| Encryption at rest (AES-GCM) | Encryption |
| Body integrity (HMAC-SHA256) | Body integrity |
| Storage-key binding + rollback protection | Binding a body to its key |
| Master-key rotation | Key rotation |
| Per-actor compression / encryption policies | Per-actor policies |
| Snapshot store backend | Snapshot store backend |
All optional — start without; layer on as needed.
Performance
Section titled “Performance”Rough numbers for S3:
- Put (small object): 20-50 ms.
- Get: 10-30 ms.
- Delete: 30-50 ms.
Filesystem backend: sub-millisecond.
A LIST on the filesystem backend reads only the directory its
prefix names — everything up to the prefix’s last / — so it
costs the matched subtree, not the whole storage root. That is
what keeps a per-persistenceId prefix such as
snapshots/<pid>/ sub-millisecond however many other entities
share the root. A prefix with no / in it (snapshots) has no
directory to narrow to and still walks everything, so prefer
keys with a per-entity directory segment.
Object-storage is slower than SQLite / Cassandra for single operations. Compensate with:
- Snapshot policies that bound recovery.
- CachedSnapshotStore decorator for read-through caching.
- Batching wherever the framework allows.
Where to next
Section titled “Where to next”- Compression — gzip / zstd at-rest compression.
- Encryption — AES-GCM encryption at rest.
- Snapshot store backend — snapshots in object storage.
- Durable state — the main consumer.
- Persistence overview — the bigger picture.
