@vendit-dev/event-store-client
v1.1.3
Published
Client encapsulating GRPC interface to communicate with vendit event store server.
Keywords
Readme
@vendit-dev/event-store-client
Client encapsulating the gRPC interface to communicate with the vendit event store server.
Version lines
| Line | Branch | Transport | Status |
| --- | --- | --- | --- |
| 1.x | v1-sqs | AWS SQS | Active development |
| 0.x | master | BullMQ (Redis) | Maintenance for legacy projects |
The 1.x line replaces BullMQ with AWS SQS as the event delivery transport. Both lines
are maintained independently: fixes for legacy (BullMQ) consumers land on master and
release as 0.x, while SQS-based development happens on v1-sqs and releases as 1.x.
What changed in 1.0.0
- BullMQ removed. Events are now delivered through one SQS Standard queue per
(aggregate, context, service)subscription. - Queues are owned by the event store server. The client never creates queues; on
saga registration it calls the new
SubscribegRPC RPC and receives the queue URL to poll. The client only needssqs:ReceiveMessage,sqs:DeleteMessage,sqs:ChangeMessageVisibilityandsqs:GetQueueAttributespermissions. - Revision-ordered processing. SQS Standard queues deliver out of order, so the
client gates saga invocation by stream revision: an event whose revision is ahead of
currentRevision + 1is returned to the queue (with a short visibility timeout) and redelivered until the gap fills. AfteroutOfOrderMaxReceiveCountredeliveries the event is processed anyway so a permanently missing revision cannot wedge a stream. Events published withrevision: -1(side-effect events) bypass gating. - Redis stays for distributed locking only. The per-stream mutex that serializes
saga processing across replicas still uses Redis.
redisFeaturesEnabled: falsedisables both the Redis mutex and queue consumption, exactly as in0.x(useful for tests).
Everything else — the public API (useSagas, store, publish, created, updated,
deleted, rebuild, resyncAggregate, snapshots) — is unchanged from 0.x.
New constructor options (all optional)
const eventManager = new VenditEventManager({
// ...existing options (host, port, serviceName, apiKey, context, redis, ...)
sqsClient, // injected SQSClient instance (takes precedence)
sqsConfig, // SQSClientConfig used to build a client when sqsClient is not given
// default: { region: AWS_REGION ?? AWS_DEFAULT_REGION ?? 'ap-northeast-2' }
sqsWaitTimeSeconds: 20, // long-polling wait per receive
sqsBatchSize: 10, // messages per receive (1-10)
outOfOrderRetryDelaySeconds: 5, // visibility timeout applied to future-revision events
outOfOrderMaxReceiveCount: 30, // receives after which ordering is no longer enforced
});AWS credentials resolve through the SDK default provider chain (env vars, shared
config/SSO profile, IAM role). Pass your own sqsClient for custom credential handling.
Subscribe heartbeat & dead-queue ejection (1.1.2+)
The client re-calls the (idempotent) Subscribe RPC every
subscribeHeartbeatIntervalMs (default 5 min, 0 disables). This keeps the
server-side registration fresh and self-heals dead-queue ejection: the
event store unsubscribes a service whose queue shows a large backlog with zero
in-flight messages over several consecutive checks (i.e. nobody is consuming).
The queue itself is left intact (14-day retention). If a service was ejected
and later recovers, the heartbeat re-registers it automatically — but events
published while it was unsubscribed were never queued for it, so it should run
resyncAggregate to fill the gap.
Saga transactions (1.1.0+)
Sagas can opt into distributed transactions coordinated by the event store
server. See apps/event-store-svc/docs/SAGA_TRANSACTIONS.md in vpms-cluster
for the full protocol.
const sagas: EventSaga<Context> = {
Reservation: {
transaction: 1, // integer >= 1; lower groups run first
rollback: async (event, ctx, buildResult) => { /* compensate */ },
// or rollback: 'REBUILD' (requires prebuild) - mechanical compensation by
// replaying the stream from the latest snapshot.
revision: async (event, ctx) => ...,
build: async (event, ctx, processors) => ..., // reversible work only
},
};
// Publisher side: store + wait for the whole multi-service transaction.
const result = await eventManager.storeTransactional(
{ streamId, type: 'CREATED', data, aggregate: 'Reservation' },
{ timeoutMs: 30_000 },
);
// result.status: COMMITTED | ROLLED_BACK | ROLLED_BACK_PARTIAL | TIMED_OUT
// result.failedServices / result.rollbackFailures: per-participant detailSemantics summary:
- Sagas with the same
transactiongroup across services commit or roll back together per event; lower-numbered groups complete before higher ones; plain sagas run last and only for committed events. - A
buildthrow fails the transaction. Processors awaited bybuildpropagate; fire-and-forget processors are pure side effects (and escape rollback - choose the bucket by choosing whether toawait). - Failed events stay in the store marked
ROLLED_BACKand are skipped (but counted) by replays; subsequent events on the stream wait for the transaction to resolve. - A terminally failed rollback parks only that (service, stream) pair; resolve
via
eventManager.resolveTransaction(eventId, service, resolution)withRETRY_ROLLBACK|MARK_ROLLED_BACK|MARK_COMMITTED. eventManager.getTransactionTopology()shows which services participate in which groups per aggregate.- Test every transactional saga with
testCompensationSymmetry(exported) so compensation logic cannot rot unnoticed. - Function rollbacks must be idempotent and guard against unapplied events:
redelivery and race recovery can (rarely) deliver a rollback for an event
this service never built. Start every function rollback with a guard like
if (model.revision !== event.revision) return;— or userollback: 'REBUILD', which is immune by construction (it derives state from the stream instead of inverting operations).
Migrating a service from 0.x
- Bump the dependency to
^1.0.0. - Deploy against an event store server that supports the
SubscribeRPC (publishes to SQS instead of BullMQ). - Ensure the runtime has SQS consume permissions and
AWS_REGIONset (or passsqsClient/sqsConfig). - Drain or ignore the legacy
VSCSUBKEY_*BullMQ queues in Redis; they are no longer consumed.
Development
yarn install
npm run protoc # regenerate src/proto from src/eventStore.proto (requires protoc)
npm run build # tsc -> lib/build.sh post-processes the generated src/proto/eventStore.ts (protobufjs import
extension, SubscriptionPushExceptAck narrowing) — do not hand-edit the generated file.
