@durable-streams/state
Version:
State change event protocol for Durable Streams
255 lines (193 loc) • 6.68 kB
Markdown
---
name: state-schema
description: >
Defining typed state schemas for @durable-streams/state. createStateSchema()
with CollectionDefinition (schema, type, primaryKey), Standard Schema
validators (Zod, Valibot, ArkType), event helpers insert/update/delete/upsert,
ChangeEvent and ControlEvent types, State Protocol operations, transaction
IDs (txid) for write confirmation. Load when defining entity types, choosing
a schema validator, or creating typed change events.
type: core
library: durable-streams
library_version: "0.2.1"
sources:
- "durable-streams/durable-streams:packages/state/src/stream-db.ts"
- "durable-streams/durable-streams:packages/state/src/types.ts"
- "durable-streams/durable-streams:packages/state/STATE-PROTOCOL.md"
- "durable-streams/durable-streams:packages/state/README.md"
---
# Durable Streams — State Schema
Define typed entity collections over durable streams using Standard Schema
validators. Schemas route stream events to collections, validate data, and
provide typed helpers for creating change events.
## Setup
```typescript
import { createStateSchema } from "@durable-streams/state"
import { z } from "zod" // Use the correct import for your Zod version (e.g. "zod/v4" for Zod v4)
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
const messageSchema = z.object({
id: z.string(),
text: z.string(),
userId: z.string(),
timestamp: z.number(),
})
const schema = createStateSchema({
users: {
schema: userSchema,
type: "user", // Event type field — routes events to this collection
primaryKey: "id", // Field in value used as unique key
},
messages: {
schema: messageSchema,
type: "message",
primaryKey: "id",
},
})
```
## Core Patterns
### Creating typed change events
Schema collections provide typed helpers for building events:
```typescript
// Insert
const insertEvent = schema.users.insert({
value: { id: "1", name: "Kyle", email: "kyle@example.com" },
})
// Update
const updateEvent = schema.users.update({
value: { id: "1", name: "Kyle Mathews", email: "kyle@example.com" },
oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" },
})
// Delete
const deleteEvent = schema.users.delete({
key: "1",
oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" },
})
```
### Using transaction IDs for confirmation
```typescript
const txid = crypto.randomUUID()
const event = schema.users.insert({
value: { id: "1", name: "Kyle", email: "kyle@example.com" },
headers: { txid },
})
await stream.append(event)
// Then use db.utils.awaitTxId(txid) in StreamDB for confirmation
```
### Choosing a schema validator
Any library implementing [Standard Schema](https://standardschema.dev/) works:
```typescript
// Zod
import { z } from "zod"
const userSchema = z.object({ id: z.string(), name: z.string() })
// Valibot
import * as v from "valibot"
const userSchema = v.object({ id: v.string(), name: v.string() })
// Manual Standard Schema implementation
const userSchema = {
"~standard": {
version: 1,
vendor: "my-app",
validate: (value) => {
if (typeof value === "object" && value !== null && "id" in value) {
return { value }
}
return { issues: [{ message: "Invalid user" }] }
},
},
}
```
### Event types and type guards
```typescript
import { isChangeEvent, isControlEvent } from "@durable-streams/state"
import type {
StateEvent,
ChangeEvent,
ControlEvent,
} from "@durable-streams/state"
function handleEvent(event: StateEvent) {
if (isChangeEvent(event)) {
// event.type, event.key, event.value, event.headers.operation
console.log(`${event.headers.operation}: ${event.type}/${event.key}`)
}
if (isControlEvent(event)) {
// event.headers.control: "snapshot-start" | "snapshot-end" | "reset"
console.log(`Control: ${event.headers.control}`)
}
}
```
## Common Mistakes
### CRITICAL Using primitive values instead of objects in collections
Wrong:
```typescript
{ type: "count", key: "views", value: 42 }
```
Correct:
```typescript
{ type: "count", key: "views", value: { count: 42 } }
```
Collections require object values so the `primaryKey` field can be extracted. Primitive values throw during dispatch.
Source: packages/state/README.md best practices
### HIGH Using duplicate event types across collections
Wrong:
```typescript
createStateSchema({
users: { schema: userSchema, type: "entity", primaryKey: "id" },
posts: { schema: postSchema, type: "entity", primaryKey: "id" },
})
```
Correct:
```typescript
createStateSchema({
users: { schema: userSchema, type: "user", primaryKey: "id" },
posts: { schema: postSchema, type: "post", primaryKey: "id" },
})
```
`createStateSchema()` throws if two collections share the same `type` string. The `type` field routes events to collections — duplicates would be ambiguous.
Source: packages/state/src/stream-db.ts createStateSchema validation
### HIGH Forgetting to use a Standard Schema-compatible validator
Wrong:
```typescript
interface User {
id: string
name: string
}
createStateSchema({
users: { schema: User, type: "user", primaryKey: "id" }, // Not a validator!
})
```
Correct:
```typescript
import { z } from "zod"
const userSchema = z.object({ id: z.string(), name: z.string() })
createStateSchema({
users: { schema: userSchema, type: "user", primaryKey: "id" },
})
```
The `schema` field requires an object implementing the `~standard` interface. TypeScript interfaces and plain types are not validators.
Source: packages/state/README.md Standard Schema support section
### MEDIUM Using reserved collection names
Wrong:
```typescript
createStateSchema({
actions: { schema: actionSchema, type: "action", primaryKey: "id" },
})
```
Correct:
```typescript
createStateSchema({
userActions: { schema: actionSchema, type: "action", primaryKey: "id" },
})
```
Collection names `collections`, `preload`, `close`, `utils`, and `actions` are reserved — they collide with the StreamDB API surface.
Source: packages/state/src/stream-db.ts reserved name check
### HIGH Tension: Schema strictness vs. prototyping speed
This skill's patterns conflict with getting-started. The state package requires Standard Schema validators and typed collections, while quick prototyping favors raw JSON streams without schemas. Agents may jump to StreamDB for a simple demo when raw `stream()` with JSON mode would be faster.
See also: durable-streams/getting-started/SKILL.md
## See also
- [stream-db](../stream-db/SKILL.md) — Wire schemas into a reactive StreamDB
## Version
Targets @durable-streams/state v0.2.1.