Persistence Resolution
Slingshot’s adapter model lets you write business logic once and run it against any supported database. Here’s how the dispatch works.
The shape
Section titled “The shape”Every persistence concern is expressed as RepoFactories<T>:
type RepoFactories<T> = Record<StoreType, (infra: StoreInfra) => T>;
type StoreType = 'memory' | 'redis' | 'sqlite' | 'postgres' | 'mongo';A RepoFactories<T> maps every supported store type to a factory that produces a fresh adapter. Business logic receives the adapter as T and never sees the store type.
resolveRepo()
Section titled “resolveRepo()”At startup, resolveRepo() reads the configured store type, calls the matching factory, and returns a fresh adapter:
import { resolveRepo } from '@lastshotlabs/slingshot-core';
declare const factories: import('@lastshotlabs/slingshot-core').RepoFactories<unknown>;declare const storeType: import('@lastshotlabs/slingshot-core').StoreType;declare const infra: import('@lastshotlabs/slingshot-core').StoreInfra;
const adapter = resolveRepo(factories, storeType, infra);// Returns a fresh T — no shared state, no singletonsinfra provides connection handles: getRedis(), getMongo(), getSqliteDb(), getPostgres(). Factories use them to create adapters against real connections in production and in-memory state in tests.
StoreInfra is intentionally scoped to persistence concerns only. Other bootstrap-time framework
data, such as the opaque root ws config draft and the cloned ctx.wsEndpoints map that plugins
can patch during setupPost, travels alongside infra on frameworkConfig / SlingshotContext
instead of being injected through repository factories.
buildContext() is the boundary where that app-level runtime draft is copied into context; adapter
factories never receive it through StoreInfra.
That same boundary now owns registry-backed events too. buildContext() attaches the canonical
ctx.events publisher next to ctx.bus, and ctx.destroy() emits the framework app:shutdown
event before teardown begins. Persistence factories still stay transport-agnostic; shutdown and
external delivery policy live at the context layer, not in repository resolution.
Writing a set of factories
Section titled “Writing a set of factories”import type { RepoFactories } from '@lastshotlabs/slingshot-core';
interface PostAdapter { readonly kind: 'post-adapter';}declare function createMemoryPostAdapter(): PostAdapter;declare function createSqlitePostAdapter( db: ReturnType<import('@lastshotlabs/slingshot-core').StoreInfra['getSqliteDb']>,): PostAdapter;declare function createPostgresPostAdapter( db: ReturnType<import('@lastshotlabs/slingshot-core').StoreInfra['getPostgres']>,): PostAdapter;declare function createMongoPostAdapter( db: ReturnType<import('@lastshotlabs/slingshot-core').StoreInfra['getMongo']>,): PostAdapter;
export const postAdapterFactories: RepoFactories<PostAdapter> = { memory: _infra => createMemoryPostAdapter(), sqlite: infra => createSqlitePostAdapter(infra.getSqliteDb()), postgres: infra => createPostgresPostAdapter(infra.getPostgres()), mongo: infra => createMongoPostAdapter(infra.getMongo()), redis: _infra => { throw new Error('Redis is not supported for post storage'); },};If a store type doesn’t make sense for a given concern (Redis for relational data, for instance), throw at startup. Don’t silently fall back.
Using inside a package or createEntityPlugin()
Section titled “Using inside a package or createEntityPlugin()”The canonical way to wire a repo is from inside a definePackage(...) module — the package
compiler hands the resolved adapter to your entity hooks. When you drop down to the lower-level
createEntityPlugin() escape hatch directly, call resolveRepo() inside the buildAdapter
callback:
createEntityPlugin({ name: 'blog', entities: [ { config: Post, operations: postOperations.operations, buildAdapter: (storeType, infra) => resolveRepo(createEntityFactories(Post, postOperations.operations), storeType, infra), }, ],});createEntityFactories() generates a RepoFactories<EntityAdapter> from an entity definition and its operations. The adapters are pure TypeScript — no ORM required. (MongoDB and Postgres adapters use Mongoose and Drizzle internally.)
No shared state between adapter instances
Section titled “No shared state between adapter instances”Each resolveRepo() call returns a fresh adapter. Two concurrent requests don’t share state. Two parallel test suites don’t share state. That’s the factory pattern’s guarantee.
Test isolation
Section titled “Test isolation”Use ctx.clear() to reset in-memory adapters between test cases:
afterEach(() => { ctx.clear(); // wipes all in-memory stores, resets event subscriptions});ctx.destroy() is the full app cleanup path. It runs plugin teardown first, shuts down the event bus and any WS transport, then closes DB connections. Call it in afterAll or process teardown handlers.
See also
Section titled “See also”- Context and Registrar — how
StoreInfrais provided - Reflect Symbol DI — how entity registration flows through infra