@playstack/core
Dependency-free errors, results, clocks, identifiers, and portable operation-context contracts.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/core is the dependency-free center of the package suite. It contains the contracts that capabilities use to agree on errors, results, time, identifiers, and operation metadata without becoming a general utility library.
Install
npm install @playstack/coreKeep it on the same fixed version as every other @playstack/* dependency.
Compose it at the application boundary
Create operation metadata where a request, job, or message enters the application. Pass it into domain commands explicitly; it is descriptive context, not authorization evidence.
import type { OperationContext } from '@playstack/core'
export function operationContextFromRequest(request: Request): OperationContext {
const correlationId = request.headers.get('x-correlation-id') ?? crypto.randomUUID()
const traceparent = request.headers.get('traceparent')
return {
correlationId,
actor: { type: 'user', id: authenticatedUserId(request) },
scope: { type: 'account', id: activeAccountId(request) },
...(traceparent ? { trace: { traceparent } } : {}),
}
}
const context = operationContextFromRequest(request)
await accounts.rename({ accountId, name, context })When one operation causes another, reuse the correlation ID and set the source event or command ID as causationId.
Configuration
Core reads no globals and has no configuration object. Supply implementations at the composition root:
| Contract | Required member | Application responsibility |
|---|---|---|
Clock | nowMs(): number | Select real or deterministic time. Use systemClock() in production. |
IdGenerator | next(): string | Generate IDs in the application’s required format. |
OperationContext | Optional metadata fields | Build it from trusted boundary state and pass it explicitly. |
Operation context properties
| Property | Type | Meaning |
|---|---|---|
correlationId | string? | Groups work that belongs to the same end-to-end operation. |
causationId | string? | Identifies the command or event that directly caused this work. |
actor | ActorReference? | Opaque apiKey, staff, system, or user reference. |
scope | ScopeReference? | Opaque account, accountMember, or user reference. |
trace | TraceContext? | W3C traceparent plus optional tracestate and baggage. |
Actor and scope references deliberately carry IDs, not hydrated account or user records.
Errors and results
Define package errors with a stable code, then expose expected outcomes as Result when the caller should branch without exception handling.
import {
PlaystackError,
createErrorCode,
err,
ok,
type Result,
} from '@playstack/core'
class CartEmptyError extends PlaystackError {
readonly code = createErrorCode('cart', 'cart_empty')
readonly retryable = false
}
function checkout(items: readonly Item[]): Result<Order, CartEmptyError> {
return items.length === 0 ? err(new CartEmptyError('Cart is empty')) : ok(createOrder(items))
}Error codes use playstack/<kebab-package>/<snake_case-code>. Every PlaystackError declares whether retrying the same operation may succeed.
Testing
Import FakeClock, SequenceIds, and assertPlaystackError from @playstack/core/testing to make time, identifiers, and error assertions deterministic.
The same entry owns the shared in-memory unit of work that every package's memory persistence adapter joins: MemoryTransactionHandle, runMemoryTransaction, and MemoryTransactionQueue. A transaction opened by one package's memory adapter can be passed to another package's adapter, and both commit or roll back together, exactly as they would on one database transaction. Every memory adapter exposes state(transaction?) for its working copy inside a transaction or its committed state without one.
import { MemoryAccountsPersistence } from '@playstack/accounts/testing'
import { MemoryAuthPersistence } from '@playstack/auth/testing'
const auth = new MemoryAuthPersistence()
const accounts = new MemoryAccountsPersistence()
await auth.transaction(async (transaction) => {
await auth.saveUser(user, transaction)
await accounts.saveAccount(account, transaction) // joins the same unit of work
})Memory adapters are single-writer test doubles: each serializes the transactions it opens, and composed flows inherit that ordering from the adapter that opened the transaction.
Boundary
Core contains no configuration loading, logging, persistence, cryptography, or framework lifecycle. Nearly every stateful package composes with these contracts, but the application remains responsible for creating and supplying them.
API entry points and requirements
Reference snapshot: @playstack/core@0.1.0-beta.1. Import only the entry point your runtime needs. Paths below are relative to the installed package; use Go to Definition in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.
| Public entry point | Declaration file |
|---|---|
@playstack/core | ./dist/index.d.ts |
@playstack/core/errors | ./dist/errors.d.ts |
@playstack/core/testing | ./dist/testing.d.ts |
@playstack/core/package.json | No TypeScript declaration (asset or metadata export). |
Node.js engine requirement: >=20. This is not a claim that every entry point works in browsers or Workers.
Peer dependencies
This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.
For a complete first program, start with Getting started. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.