Store
@rotorsoft/act-root / act/src / Store
Interface: Store
Defined in: libs/act/src/types/ports.ts:360
Interface for event store implementations.
The Store interface defines the contract for persistence adapters in Act. Implementations must provide event storage, querying, and distributed processing capabilities through leasing and watermark tracking.
Act includes two built-in implementations:
- InMemoryStore: For development and testing
- PostgresStore: For production use with PostgreSQL
Custom stores can be implemented for other databases or event log systems.
Exampleโ
import { store } from "@rotorsoft/act";
import { PostgresStore } from "@rotorsoft/act-pg";
// Replace the default in-memory store
store(new PostgresStore({
host: "localhost",
port: 5432,
database: "myapp",
user: "postgres",
password: "secret"
}));
const app = act()
.withState(Counter)
.build();
Seeโ
- InMemoryStore for the default implementation
- PostgresStore for the PostgreSQL implementation
Extendsโ
Propertiesโ
ackโ
ack: (
leases) =>Promise<Lease[]>
Defined in: libs/act/src/types/ports.ts:558
Acknowledges successful processing of leased streams.
Updates the watermark to indicate events have been processed successfully. Releases the lease so other workers can process subsequent events.
Parametersโ
leasesโ
Lease[]
Leases to acknowledge with updated watermarks
Returnsโ
Promise<Lease[]>
Acknowledged leases
Exampleโ
const leased = await store().claim(5, 5, randomUUID(), 10000);
// Process events up to ID 150
await store().ack(leased.map(l => ({ ...l, at: 150 })));
Seeโ
claim for acquiring leases
blockโ
block: (
leases) =>Promise<BlockedLease[]>
Defined in: libs/act/src/types/ports.ts:591
Blocks streams after persistent processing failures.
Blocked streams won't be returned by claim until manually unblocked. This prevents poison messages from repeatedly failing and consuming resources.
Streams are typically blocked when:
- Max retries reached
blockOnErroroption is true- Handler throws an error
Parametersโ
leasesโ
Leases to block with error messages
Returnsโ
Promise<BlockedLease[]>
Blocked leases
Exampleโ
try {
await processEvents(lease);
await store().ack([lease]);
} catch (error) {
if (lease.retry >= 3) {
await store().block([{
...lease,
error: error.message
}]);
}
}
Seeโ
claim for lease management
claimโ
claim: (
lagging,leading,by,millis,lane?) =>Promise<Lease[]>
Defined in: libs/act/src/types/ports.ts:491
Atomically discovers and leases streams for reaction processing.
Atomically discovers a stream and acquires a lease in one round-trip, eliminating the race that exists when discovery and locking are separate calls (a competing worker can grab the stream between the two).
PostgresStore uses FOR UPDATE SKIP LOCKED for zero-contention competing
consumer semantics โ workers never block each other, each grabbing different
streams atomically. InMemoryStore fuses its poll+lease logic equivalently.
Used by Act.drain() as the primary stream acquisition method.
Parametersโ
laggingโ
number
Max streams from the lagging frontier (ascending watermark)
leadingโ
number
Max streams from the leading frontier (descending watermark)
byโ
string
Unique lease holder identifier (UUID)
millisโ
number
Lease duration in milliseconds
lane?โ
string
Optional lane filter (ACT-1103)
Returnsโ
Promise<Lease[]>
Array of successfully leased streams with metadata
Exampleโ
const leased = await store().claim(5, 5, randomUUID(), 10000);
leased.forEach(({ stream, at, lagging }) => {
console.log(`Leased ${stream} at ${at} (lagging: ${lagging})`);
});
Seeโ
- subscribe for registering new streams (used by correlate)
- ack for acknowledging completion
- block for blocking failed streams
commitโ
commit: <
E>(stream,msgs,meta,expectedVersion?) =>Promise<Committed<E, keyofE>[]>
Defined in: libs/act/src/types/ports.ts:421
Commits one or more events to a stream atomically.
This is the core method for persisting events. It must:
- Assign global sequence IDs to events
- Increment the stream version
- Check optimistic concurrency if expectedVersion is provided
- Store events atomically (all or nothing)
- Attach metadata (id, stream, version, created timestamp)
Type Parametersโ
Eโ
E extends Schemas
Event schemas
Parametersโ
streamโ
string
The stream ID to commit to
msgsโ
Message<E, keyof E>[]
Array of messages (events) to commit
metaโ
Event metadata (correlation, causation)
expectedVersion?โ
number
Expected current version for optimistic concurrency
Returnsโ
Promise<Committed<E, keyof E>[]>
Array of committed events with full metadata
Throwsโ
If expectedVersion doesn't match current version
Exampleโ
const events = await store().commit(
"user-123",
[{ name: "UserCreated", data: { email: "user@example.com" } }],
{ correlation: "req-456", causation: { action: {...} } },
0 // Expect version 0 (new stream)
);
disposeโ
dispose:
Disposer
Defined in: libs/act/src/types/ports.ts:26
Inherited fromโ
Disposable.dispose
dropโ
drop: () =>
Promise<void>
Defined in: libs/act/src/types/ports.ts:390
Drops all data from the store.
Dangerous operation that deletes all events and state. Use with extreme caution, primarily for testing or development environments.
Returnsโ
Promise<void>
Exampleโ
// Clean up after tests
afterAll(async () => {
await store().drop();
});
notify?โ
optionalnotify?: (handler) =>NotifyDisposer|Promise<NotifyDisposer>
Defined in: libs/act/src/types/ports.ts:999
Optional cross-process commit notifications.
When implemented, the Act orchestrator subscribes once at build
time and routes notifications to wake settle() automatically โ so a
remote worker's commit triggers reactions on this process without
waiting for the debounce/poll cycle. Subscribers also receive each
notification on the "notified" lifecycle event for fan-out
(SSE pushes, audit logs, dashboards).
Self-filtering contract: implementations must skip their own
commits. The handler fires only for commits originating from a
different process writing to the same backing store. This keeps
the local fast path (do() already arms drain) free of duplicate
wake-ups and gives "notified" a clean cross-process semantic.
Hint, not a contract: the orchestrator never depends on notify
for correctness. If absent, dropped, or the store omits it, the
existing debounce/poll path still drains correctly โ notify only
lowers cross-process p99 reaction latency.
Adapter status (Act 0.x):
- PostgresStore: implemented via
LISTEN/NOTIFYon theact_commitchannel - InMemoryStore: not implemented (single-process โ no remote writers exist)
SqliteStore: not implemented (single-node by design)
Parametersโ
handlerโ
(notification) => void
Callback invoked once per remote commit
Returnsโ
NotifyDisposer | Promise<NotifyDisposer>
Disposer releasing the underlying listener
prioritizeโ
prioritize: (
filter,priority) =>Promise<number>
Defined in: libs/act/src/types/ports.ts:723
Bulk-update the scheduling priority of streams matching a filter.
Used by Act.prioritize for operator runtime control over
lagging-frontier claim() ordering. Unlike subscribe,
which keeps the per-stream priority at the max() of all
registered reactions targeting that stream, prioritize sets the
priority directly to priority for matching rows โ letting
operators override the build-time scheduling policy.
Filter semantics mirror query_streams: stream/source
are regex by default, exact with the *_exact flags. blocked
restricts to blocked or unblocked rows. Omitted fields don't
filter. An empty filter ({}) updates every registered
stream โ useful for "reset all priorities to N" but a footgun
otherwise.
Parametersโ
filterโ
PrioritizeFilter selecting which streams
to update. Required (use {} to target all).
priorityโ
number
New priority value. Set as-is โ no max(),
no clamp.
Returnsโ
Promise<number>
Count of streams whose priority was changed.
Examplesโ
await store().prioritize(
{ stream: "^projection-orders$", stream_exact: false },
10
);
await store().prioritize({ source: "^audit-" }, -5);
Seeโ
- Act.prioritize for the orchestrator-level wrapper
- claim for how priority biases stream scheduling
queryโ
query: <
E>(callback,query?) =>Promise<number>
Defined in: libs/act/src/types/ports.ts:454
Queries events from the store with optional filtering.
Calls the callback for each matching event. The callback approach allows processing large result sets without loading everything into memory.
Type Parametersโ
Eโ
E extends Schemas
Event schemas
Parametersโ
callbackโ
(event) => void
Function invoked for each matching event
query?โ
Readonly<{ after?: number; backward?: boolean; before?: number; correlation?: string; created_after?: Date; created_before?: Date; limit?: number; names?: string[]; stream?: string; stream_exact?: boolean; with_snaps?: boolean; }>
Optional filter criteria โ see Query for fields
(stream, name, after, before, created_after, created_before,
limit, with_snaps, stream_exact).
Returnsโ
Promise<number>
Total number of events processed
Exampleโ
let count = 0;
await store().query(
(event) => {
console.log(event.name, event.data);
count++;
},
{ stream: "user-123" }
);
console.log(`Found ${count} events`);
query_statsโ
query_stats: <
E>(input,options?) =>Promise<Map<string,StreamStats<E>>>
Defined in: libs/act/src/types/ports.ts:963
Per-stream aggregated stats โ single round trip per adapter.
Returns the latest event (head) plus opt-in extras (tail, count,
names) for each stream selected by input. Streams with no
qualifying events are absent from the result map.
Cost model. With no opt-in flags, the call uses an index-backed
head lookup per stream โ O(K) where K is the number of matched
streams. tail alone stays in the cheap tier. Setting count and/or
names triggers a full event scan over the matched streams (O(N)
where N is total events); both stats share that scan, so requesting
one or both is the same cost.
input. Either an explicit string[] of stream names, or a
narrow event-stream selector { stream?, stream_exact? } for
pattern-based or exact-name matching. Subscription-level filters
(source, blocked) are intentionally not accepted here โ they
describe subscriptions, not events, and conflating the two would
silently exclude unsubscribed event streams. For
"stats for all blocked subscriptions" compose explicitly:
query_streams({blocked: true}) โ collect names โ query_stats(names).
head vs tail naming. Follows the git-log convention: head
is the latest event (highest id), tail is the earliest (lowest id).
This is the opposite of the Unix head/tail commands.
Framework markers. Snapshots (__snapshot__) and tombstones
(__tombstone__) are real events and are included by default โ
intentional, so schema-evolution tooling can count them. To exclude
them, pass them in options.exclude (typed against EventName)
so typos are compile-time errors.
Snapshot counts come from names. When names: true and snapshots
are not in exclude, result.names["__snapshot__"] is the snapshot
count for that stream โ no separate field needed. Validates snapshot
policy at scale: names["__snapshot__"] / count should match the
configured snap predicate's expected ratio.
Time travel. options.before narrows to events with id < before,
answering "what did this stream look like at event N?" without
special call shape.
Type Parametersโ
Eโ
E extends Schemas
Event schemas. Narrow at the call site to type-check
exclude against your event names (typos fail at compile time).
Parametersโ
inputโ
string[] | Pick<StreamFilter, "stream" | "stream_exact">
Stream names or a filter selecting the streams to stat.
options?โ
Opt-in stat fields, event-name exclusions, and time-travel cutoff. See QueryStatsOptions.
Returnsโ
Promise<Map<string, StreamStats<E>>>
Map keyed by stream name. Streams with no qualifying events
(after exclude and before are applied) are absent.
Examplesโ
const stats = await store().query_stats(streams, {
exclude: [TOMBSTONE_EVENT],
});
for (const [stream, { head }] of stats) {
// head.id, head.version, head.name
}
const stats = await store().query_stats<MyEvents>(
{ stream: "^orders-" },
{ count: true, tail: true, names: true,
exclude: [TOMBSTONE_EVENT] }
);
for (const [stream, s] of stats) {
const snaps = s.names?.[SNAP_EVENT] ?? 0;
const domain = (s.count ?? 0) - snaps;
console.log(stream, { snaps, domain, tail: s.tail?.created });
}
const stats = await store().query_stats<TicketEvents>(
{ stream: "^ticket-" },
{ names: true }
);
for (const [stream, { names = {} }] of stats) {
if ((names["TicketOpened"] ?? 0) > 0) {
console.log(`${stream}: ${names["TicketOpened"]} legacy events`);
}
}
const stats = await store().query_stats(["order-42"], {
before: 100_000, // events up to (not including) id 100000
tail: true,
});
const { head, tail } = stats.get("order-42") ?? {};
// head = latest event with id < 100_000; tail = earliest in range
Seeโ
- QueryStatsOptions for the cost-aware option surface
- StreamStats for the per-stream result shape
- EventName for the typed exclude entries
query_streamsโ
query_streams: (
callback,query?) =>Promise<QueryStreamsResult>
Defined in: libs/act/src/types/ports.ts:855
Streams registered subscription positions to a callback, plus the highest event id in the store.
Read-only introspection for operational dashboards (Store / Subscriptions tab, projection lag, blocked subscriptions). Avoids forcing apps to open a second connection and run raw SQL against adapter-specific schemas.
Mirrors the Store.query callback pattern: the callback is
invoked once per matching position, allowing large result sets to be
processed without buffering. Results are ordered by stream name; use
query.after (last seen stream name) for keyset pagination on big
tables (dynamic reactions can produce one subscription per aggregate).
Parametersโ
callbackโ
(position) => void
Invoked once per matching StreamPosition.
query?โ
Optional QueryStreams filter (default limit: 100).
Returnsโ
Promise<QueryStreamsResult>
maxEventId and the count of positions emitted.
Examplesโ
const { maxEventId } = await store().query_streams(
(s) => console.log(`${s.stream}: lag=${maxEventId - s.at} ${s.error}`),
{ blocked: true, limit: 50 }
);
let after: string | undefined;
for (;;) {
const page: StreamPosition[] = [];
const { count } = await store().query_streams(
(s) => page.push(s),
{ after, limit: 100 }
);
if (!count) break;
// ... use page ...
after = page.at(-1)?.stream;
}
resetโ
reset: (
input) =>Promise<number>
Defined in: libs/act/src/types/ports.ts:628
Resets watermarks for the given streams to -1, making them eligible for replay from the beginning. Also clears retry, blocked, error, and lease state so the streams can be claimed immediately.
Prefer Act.reset() over calling this directly. This primitive
only resets the store; it does not raise the orchestrator's internal
"needs drain" flag, so a settled Act instance will short-circuit and
skip the replay. Act.reset() wraps this and arms the flag.
Accepts either an explicit list of stream names or a
StreamFilter for bulk operations (e.g., "rebuild every
blocked stream"). The filter form is the same shape used by
prioritize and query_streams. An empty filter
({}) matches every registered stream โ typically a footgun for
reset; prefer narrower filters like { blocked: true }.
Parametersโ
inputโ
string[] | StreamFilter
Stream names or a StreamFilter
Returnsโ
Promise<number>
Count of streams that were actually reset
Exampleโ
// By name
await app.reset(["my-projection"]);
// By filter โ rebuild every blocked stream in a projection family
await app.reset({ stream: "^proj-orders-", blocked: true });
// Low-level (does NOT trigger replay on settled apps)
await store().reset(["my-projection"]);
Seeโ
Act.reset for the high-level rebuild API that wraps this primitive and arms the orchestrator's drain flag
restore?โ
optionalrestore?: (driver) =>Promise<void>
Defined in: libs/act/src/types/ports.ts:807
Atomically wipe the store and commit a fresh sequence of events inside a single transaction.
Capability-gated. Adapters may or may not implement restore. Third-party stores that can't atomically wipe-and-rebuild in one transaction can omit it.
Driver pattern. The adapter is a thin transactional wrapper:
open the transaction (PG BEGIN, SQLite BEGIN IMMEDIATE, an
in-process snapshot for InMemoryStore), truncate the
events + streams/subscriptions tables, hand the orchestrator a
per-event insert callback by invoking driver(callback), then
commit or roll back. Any throw inside driver rolls back the
transaction โ the store ends byte-for-byte unchanged from the
pre-call state.
The framework's scan loop (in internal/event-sourcing.ts) is
what calls callback repeatedly: it iterates the source,
validates each event, applies drop_snapshots, fires
on_progress, rewrites meta.causation.event.id through the
per-call old โ new map, and counts kept/dropped. Adapters
never see that logic โ their job is the transaction lifecycle
plus the adapter-specific callback body.
Lossless created. The callback receives the event's
original timestamp; adapters write it through verbatim. This is
the property that makes restore a viable backup/migration
primitive โ distinct from commit, which always stamps
now().
Renumbered id. Adapters reseed ids densely (1..N on
PG/SQLite, 0..N-1 on InMemory). The source's original ids are
used by the orchestrator as causation lookup keys but never
written through.
No subscription preservation. Both the events and the
streams/subscriptions tables are wiped. Reactions re-subscribe
via the orchestrator's normal correlate() path on the next
settle cycle.
Cache. Restore does not touch the Cache port โ
callers must cache().clear() after restore to avoid serving
stale snapshots. Documented; not enforced.
Parametersโ
driverโ
(callback) => Promise<void>
Orchestrator-supplied iteration callback. The
adapter calls driver(callback) exactly once, from inside its
transaction. The callback argument is the adapter's per-event
insert hook โ it receives the event with meta.causation
already rewritten to the new id space and returns the new id
the adapter assigned. The driver is purely transactional from
the adapter's perspective โ kept/dropped counts and timing live
in Act.restore.
Returnsโ
Promise<void>
Seeโ
- Act.restore for the public entry point.
- truncate for the single-stream snapshot/tombstone primitive (different operation โ restore wipes the whole store)
seedโ
seed: () =>
Promise<void>
Defined in: libs/act/src/types/ports.ts:375
Initializes or resets the store.
Used primarily for testing to ensure a clean state between tests. For production stores, this might create necessary tables or indexes.
Returnsโ
Promise<void>
Exampleโ
// Reset store between tests
beforeEach(async () => {
await store().seed();
});
subscribeโ
subscribe: (
streams) =>Promise<{subscribed:number;watermark:number; }>
Defined in: libs/act/src/types/ports.ts:522
Registers streams for event processing.
Upserts stream entries so they become visible to claim. Used by
correlate() to register dynamically discovered reaction target streams.
Also returns the current maximum watermark across all subscribed streams, used internally for correlation checkpoint initialization on cold start.
Parametersโ
streamsโ
object[]
Streams to register with optional source hint
Returnsโ
Promise<{ subscribed: number; watermark: number; }>
subscribed count of newly registered streams, watermark max at across all streams
Exampleโ
const { subscribed, watermark } = await store().subscribe([
{ stream: "stats-user-1", source: "user-1" },
{ stream: "stats-user-2", source: "user-2", priority: 10 },
]);
Seeโ
- claim for discovering and leasing registered streams
- prioritize for changing priority after subscription
truncateโ
truncate: (
targets) =>Promise<TruncateResult>
Defined in: libs/act/src/types/ports.ts:741
Atomically truncates streams and seeds each with a final event.
For each target, in a single transaction:
- Deletes all events for the stream
- Removes the stream's entry from the streams table
- Inserts a
__snapshot__(whensnapshotis provided) or__tombstone__event as the sole event on the stream
Parametersโ
targetsโ
object[]
Streams to truncate with optional snapshot state and meta
Returnsโ
Promise<TruncateResult>
Map keyed by stream name, each entry with deleted count and committed event
Seeโ
Act.close for the high-level close-the-books API that orchestrates safety checks, archive callbacks, and atomic truncate+seed
unblockโ
unblock: (
input) =>Promise<number>
Defined in: libs/act/src/types/ports.ts:682
Clears the blocked flag on streams without replaying their history.
Sets blocked = false, retry_count = 0, error = null, and
clears any lease bookkeeping. The at watermark stays where it
was โ the stream resumes from the next event after the last
successful ack, not from zero.
The distinction from reset matters: reset() is for
projection rebuilds (replay from event 0); unblock() is for
recovering from a poison message after the operator fixes the
underlying issue. Use unblock() when you don't want to re-process
history.
Prefer Act.unblock() over calling this directly. Like
reset(), this primitive doesn't raise the orchestrator's internal
"needs drain" flag โ a settled Act instance will short-circuit and
skip the resume. Act.unblock() wraps this and arms the flag.
Only streams that were actually blocked at call time count toward
the return value; already-unblocked streams and unknown stream
names are silently skipped. The atomic single-statement update
makes the call safe to issue concurrently with claim() โ workers
holding a FOR UPDATE SKIP LOCKED lock won't see partial state.
Accepts either an explicit list of stream names or a
StreamFilter for bulk recovery (e.g., "unblock every
blocked order projection"). The blocked = true predicate is
always applied โ passing blocked: false in the filter matches
nothing. An empty filter ({}) means "unblock everything that's
blocked," which is a sane post-incident bulk recovery.
Parametersโ
inputโ
string[] | StreamFilter
Stream names or a StreamFilter
Returnsโ
Promise<number>
Count of streams that were actually flipped (were blocked)
Exampleโ
// By name (single targeted recovery)
await app.unblock(["webhooks-out-customer-42"]);
// By filter โ unblock every blocked stream in a family
await app.unblock({ stream: "^webhooks-out-" });
// Post-incident: unblock everything that's blocked
await app.unblock({});
// Low-level (does NOT trigger resume on settled apps)
await store().unblock(["webhooks-out-customer-42"]);
Seeโ
- Act.unblock for the high-level recovery API
- reset for the rebuild-from-zero alternative