Node and TypeScript
@factstr/factstr-node is the current published Node and TypeScript package for FACTSTR.
It provides Node.js bindings and TypeScript types for FACTSTR. The package exposes the Memory, SQLite, and PostgreSQL stores from the Rust implementation without reimplementing FACTSTR semantics in TypeScript.
Install
npm install @factstr/factstr-node
Current Stores
FactstrMemoryStoreFactstrSqliteStoreFactstrPostgresStore
Current API
appendqueryappendIfstreamAllstreamTostreamAllDurablestreamToDurableDurableStreamEventStreamSubscription
Append And Query
import {
type EventQuery,
type NewEvent,
FactstrMemoryStore,
FactstrPostgresStore,
FactstrSqliteStore,
} from '@factstr/factstr-node';
const memoryStore = new FactstrMemoryStore();
const sqliteStore = new FactstrSqliteStore('./factstr.sqlite');
const postgresStore = new FactstrPostgresStore(process.env.DATABASE_URL!);
const event: NewEvent = {
event_type: 'item-added',
payload: { sku: 'ABC-123', quantity: 1 },
};
memoryStore.append([event]);
sqliteStore.append([event]);
postgresStore.append([event]);
const query: EventQuery = {
filters: [
{
event_types: ['item-added'],
},
],
};
const result = sqliteStore.query(query);
console.log(result.event_records[0]?.occurred_at);
console.log(result.event_records[0]?.payload);
console.log(result.last_returned_sequence_number);
console.log(result.current_context_version);
This example keeps the current public package shape explicit:
- events use
event_typeandpayload query(...)returnsevent_records- each
EventRecordincludesoccurred_at last_returned_sequence_numberandcurrent_context_versionstay distinct
Conditional Append Example
import {
type AppendIfResult,
type EventQuery,
type NewEvent,
FactstrMemoryStore,
} from '@factstr/factstr-node';
const store = new FactstrMemoryStore();
const contextQuery: EventQuery = {
filters: [
{
event_types: ['item-added'],
},
],
};
const context = store.query(contextQuery);
const nextEvent: NewEvent = {
event_type: 'item-added',
payload: { sku: 'ABC-123', quantity: 1 },
};
const outcome: AppendIfResult = store.appendIf(
[nextEvent],
contextQuery,
context.current_context_version,
);
if (outcome.conflict) {
console.log('conditional append conflict', outcome.conflict);
} else {
console.log('append succeeded', outcome.append_result);
}
appendIf(...) checks whether the relevant command context changed before the new facts are committed.
Live Streams
import {
FactstrSqliteStore,
type EventQuery,
} from '@factstr/factstr-node';
const store = new FactstrSqliteStore('./factstr.sqlite');
const reservationFacts: EventQuery = {
filters: [
{
event_types: ['item-reserved', 'reservation-cancelled'],
},
],
};
const allSubscription = store.streamAll((events) => {
console.log('all committed batch', events);
});
const filteredSubscription = store.streamTo(reservationFacts, (events) => {
console.log('reservation batch', events);
});
allSubscription.unsubscribe();
filteredSubscription.unsubscribe();
streamAll(...) observes future committed batches. streamTo(...) observes future committed facts that match the given query.
Node stream callbacks may return void, boolean, Promise<void>, or
Promise<boolean>.
Live callback failure still does not roll back a successful append.
Live vs Durable Stream Registration
streamAll(...) and streamTo(...) register live streams. They observe only future committed batches after registration becomes active. Registration is synchronous because no replay happens during registration.
async function updateProjection(events: EventRecord[]): Promise<void> {
console.log('persist projection updates', events);
}
const subscription = store.streamAll(async (events) => {
await updateProjection(events);
});
The callback may return a Promise, but append success is not rolled back if the callback fails.
streamAllDurable(...) and streamToDurable(...) register durable streams. They may replay existing committed batches before returning the subscription. Registration is asynchronous because FACTSTR waits for replay callback success before advancing the durable cursor.
const subscription = await store.streamAllDurable(
{ name: 'inventory-projector' },
async (events) => {
await updateProjection(events);
},
);
For durable streams, the cursor advances only after the callback succeeds. If the callback throws, returns false, rejects, or resolves to false, the cursor does not advance.
API summary:
streamAll(handle): EventStreamSubscription
streamTo(query, handle): EventStreamSubscription
streamAllDurable(name, handle): Promise<EventStreamSubscription>
streamToDurable(name, query, handle): Promise<EventStreamSubscription>
Durable Streams
import {
type DurableStream,
FactstrSqliteStore,
} from '@factstr/factstr-node';
const store = new FactstrSqliteStore('./factstr.sqlite');
const durableStream: DurableStream = { name: 'inventory-projector' };
const subscription = await store.streamAllDurable(
durableStream,
async (events) => {
console.log('replayed or live batch', events);
},
);
subscription.unsubscribe();
streamAllDurable(...) and streamToDurable(...) replay committed facts
strictly after the stored durable cursor and then continue with future
committed delivery.
Durable registration must be awaited. The registration Promise resolves only after replay has completed successfully.
Durable cursor advancement waits for callback success. Rejected Promises,
resolved false, and synchronous throws prevent cursor advancement for that
delivered batch.
If a durable callback returns a Promise that never settles, FACTSTR keeps
waiting for that in-flight delivery. unsubscribe() stops future deliveries,
but it does not cancel a callback that is already in flight. The durable
cursor advances only if that in-flight callback eventually succeeds.
BigInt
Sequence and context values use bigint so Rust u64 meanings stay lossless in TypeScript.
occurred_at is exposed as an RFC 3339 string on each returned event record.
Store Durability Boundary
FactstrMemoryStorekeeps durable stream state only for the lifetime of one store instance.FactstrSqliteStorepersists facts and durable stream state in SQLite, so the same durable stream name can resume across reopening the same database path.FactstrPostgresStorepersists facts and durable stream state in PostgreSQL, so the same durable stream name can resume as long as the database state is retained.
Not Included Yet
- transport behavior