Layers and storage
Provide stream services with a storage Layer, choose a backend, and commit application rows with stream writes.
A Streamsy program is an Effect that needs stream services. A Layer provides them and decides where the data is kept.
import { ManagedRuntime } from "effect";
import { MemoryStorage } from "@streamsy/core";
import { shop } from "./streams.ts";
import { write } from "./write.ts";
const runtime = ManagedRuntime.make(MemoryStorage.layer(shop));
try {
await runtime.runPromise(write);
} finally {
await runtime.dispose();
}| Program shape | Runtime |
|---|---|
| Script or test | ManagedRuntime, as above |
| Server | Serve.layer under Layer.launch and the runtime's runMain. See Bun and Host contract |
Layers
| Layer | Persists | Package | Page |
|---|---|---|---|
MemoryStorage.layer(group, options) | No | @streamsy/core | Memory |
BunStorage.layer(group, options) | SQLite file | @streamsy/storage/bun | Bun |
DurableObjectStorage.layer(group, options) | Durable Object SQLite | @streamsy/storage/durable-object | Cloudflare |
RemoteStreams.layer(group, options) | Remote | @streamsy/core/remote | Remote client |
Streamsy.Host<typeof group> names the stream services, so one type fits any host Layer. Each host module names its options Options, for example BunStorage.Options.
Memory
MemoryStorage.layer(group, options) keeps streams in the process. The data is lost when the Layer is disposed.
| Option | Effect |
|---|---|
constrained | Copies fork prefixes, polls for wakes, and has no expiry index |
pollIntervalMs | The poll interval in constrained mode. Default 25 ms |
readLimit | The largest catch-up page. Default 1000 messages |
longPollTimeoutMs | The long-poll wait. Default 30 s |
Each mutation copies the whole store, so an append takes linear time. Use memory for development and tests. Use constrained: true in tests to check that a program does not depend on the default capabilities.
Several backends
Merge one Layer for each backend:
import { Layer } from "effect";
import { MemoryStorage, Streamsy } from "@streamsy/core";
import { BunStorage } from "@streamsy/storage/bun";
import { Drafts, Journal } from "./streams.ts";
export const layer = Layer.merge(
MemoryStorage.layer(Streamsy.group(Drafts)),
BunStorage.layer(Streamsy.group(Journal), { client: { filename: "./journal.sqlite" } }),
);
A missing stream service is a type error.
Rules:
- A group checks overlap only among its own streams. Put all served streams in one group.
- A fork stays inside one storage Layer.
- A remote Layer has its own
capabilities. Commit.transactionkeeps SQL, output appends, and the checkpoint on one SQL owner.Commit.producercan write to existing outputs on other owners. See Several backends.
Commit with your own tables
Every storage Layer gives CommitBoundary. On the SQLite Layers, withTransaction runs application SQL and a stream mutation in one transaction.
import { Effect } from "effect";
import { CommitBoundary, Storage, type Mutation } from "@streamsy/core";
import * as SqlClient from "effect/sql/SqlClient";
declare const mutation: Mutation;
const program = Effect.gen(function* () {
const boundary = yield* CommitBoundary;
const storage = yield* Storage;
const sql = yield* SqlClient.SqlClient;
return yield* boundary
.withTransaction(
Effect.gen(function* () {
yield* sql.unsafe("INSERT INTO application_rows(id) VALUES (?)", ["example"]);
return yield* storage.mutate(mutation);
}),
)
.pipe(Effect.catchTag("MutationRejected", Effect.succeed));
});A MutationRejected that leaves the boundary rolls back both writes. Catch it outside the boundary. Nested calls join the outer transaction and make no savepoints.
Testing
Application tests provide MemoryStorage.layer(group) and run the same program.
| Tool | Module | Use |
|---|---|---|
layerTest, StreamsTest | @streamsy/core/testing | Protocol tests with visible subscriptions |
StorageContract.run | @streamsy/core/testing | Storage adapter contract tests |
Bind streams to a test graph with Protocol.bind(layerTest(), group).
Protocol tier
| Name | Module | Gives |
|---|---|---|
Protocol.layer | @streamsy/core/protocol | StreamsReader and StreamsWriter over Storage |
Protocol.bind(graph, group) | @streamsy/core/protocol | Stream services over a protocol graph |
layerProtocol(options) | Each host module | The generic pair, with no streams |
MemoryStorage.layerProtocol() | @streamsy/core | The generic pair over memory |
Storage adapters, protocol tests, and HTTP internals use these.