@durable-streams/state 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/intent.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // Auto-generated by @tanstack/intent setup
3
+ // Exposes the intent end-user CLI for consumers of this library.
4
+ // Commit this file, then add to your package.json:
5
+ // "bin": { "intent": "./bin/intent.js" }
6
+ await import("@tanstack/intent/intent-library")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/state",
3
3
  "description": "State change event protocol for Durable Streams",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "author": "Durable Stream contributors",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -36,23 +36,30 @@
36
36
  "./package.json": "./package.json"
37
37
  },
38
38
  "sideEffects": false,
39
+ "bin": {
40
+ "intent": "./bin/intent.js"
41
+ },
39
42
  "files": [
40
43
  "dist",
41
44
  "src",
42
45
  "state-protocol.schema.json",
43
- "STATE-PROTOCOL.md"
46
+ "STATE-PROTOCOL.md",
47
+ "skills",
48
+ "bin",
49
+ "!skills/_artifacts"
44
50
  ],
45
51
  "dependencies": {
46
52
  "@standard-schema/spec": "^1.0.0",
47
- "@durable-streams/client": "0.2.1"
53
+ "@durable-streams/client": "0.2.2"
48
54
  },
49
55
  "peerDependencies": {
50
56
  "@tanstack/db": ">=0.5.0"
51
57
  },
52
58
  "devDependencies": {
53
59
  "@tanstack/db": "latest",
60
+ "@tanstack/intent": "latest",
54
61
  "tsdown": "^0.9.0",
55
- "@durable-streams/server": "0.2.1"
62
+ "@durable-streams/server": "0.2.2"
56
63
  },
57
64
  "engines": {
58
65
  "node": ">=18.0.0"
@@ -0,0 +1,254 @@
1
+ ---
2
+ name: state-schema
3
+ description: >
4
+ Defining typed state schemas for @durable-streams/state. createStateSchema()
5
+ with CollectionDefinition (schema, type, primaryKey), Standard Schema
6
+ validators (Zod, Valibot, ArkType), event helpers insert/update/delete/upsert,
7
+ ChangeEvent and ControlEvent types, State Protocol operations, transaction
8
+ IDs (txid) for write confirmation. Load when defining entity types, choosing
9
+ a schema validator, or creating typed change events.
10
+ type: core
11
+ library: durable-streams
12
+ library_version: "0.2.1"
13
+ sources:
14
+ - "durable-streams/durable-streams:packages/state/src/stream-db.ts"
15
+ - "durable-streams/durable-streams:packages/state/src/types.ts"
16
+ - "durable-streams/durable-streams:packages/state/STATE-PROTOCOL.md"
17
+ - "durable-streams/durable-streams:packages/state/README.md"
18
+ ---
19
+
20
+ # Durable Streams — State Schema
21
+
22
+ Define typed entity collections over durable streams using Standard Schema
23
+ validators. Schemas route stream events to collections, validate data, and
24
+ provide typed helpers for creating change events.
25
+
26
+ ## Setup
27
+
28
+ ```typescript
29
+ import { createStateSchema } from "@durable-streams/state"
30
+ import { z } from "zod" // Use the correct import for your Zod version (e.g. "zod/v4" for Zod v4)
31
+
32
+ const userSchema = z.object({
33
+ id: z.string(),
34
+ name: z.string(),
35
+ email: z.string().email(),
36
+ })
37
+
38
+ const messageSchema = z.object({
39
+ id: z.string(),
40
+ text: z.string(),
41
+ userId: z.string(),
42
+ timestamp: z.number(),
43
+ })
44
+
45
+ const schema = createStateSchema({
46
+ users: {
47
+ schema: userSchema,
48
+ type: "user", // Event type field — routes events to this collection
49
+ primaryKey: "id", // Field in value used as unique key
50
+ },
51
+ messages: {
52
+ schema: messageSchema,
53
+ type: "message",
54
+ primaryKey: "id",
55
+ },
56
+ })
57
+ ```
58
+
59
+ ## Core Patterns
60
+
61
+ ### Creating typed change events
62
+
63
+ Schema collections provide typed helpers for building events:
64
+
65
+ ```typescript
66
+ // Insert
67
+ const insertEvent = schema.users.insert({
68
+ value: { id: "1", name: "Kyle", email: "kyle@example.com" },
69
+ })
70
+
71
+ // Update
72
+ const updateEvent = schema.users.update({
73
+ value: { id: "1", name: "Kyle Mathews", email: "kyle@example.com" },
74
+ oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" },
75
+ })
76
+
77
+ // Delete
78
+ const deleteEvent = schema.users.delete({
79
+ key: "1",
80
+ oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" },
81
+ })
82
+ ```
83
+
84
+ ### Using transaction IDs for confirmation
85
+
86
+ ```typescript
87
+ const txid = crypto.randomUUID()
88
+
89
+ const event = schema.users.insert({
90
+ value: { id: "1", name: "Kyle", email: "kyle@example.com" },
91
+ headers: { txid },
92
+ })
93
+
94
+ await stream.append(event)
95
+ // Then use db.utils.awaitTxId(txid) in StreamDB for confirmation
96
+ ```
97
+
98
+ ### Choosing a schema validator
99
+
100
+ Any library implementing [Standard Schema](https://standardschema.dev/) works:
101
+
102
+ ```typescript
103
+ // Zod
104
+ import { z } from "zod"
105
+ const userSchema = z.object({ id: z.string(), name: z.string() })
106
+
107
+ // Valibot
108
+ import * as v from "valibot"
109
+ const userSchema = v.object({ id: v.string(), name: v.string() })
110
+
111
+ // Manual Standard Schema implementation
112
+ const userSchema = {
113
+ "~standard": {
114
+ version: 1,
115
+ vendor: "my-app",
116
+ validate: (value) => {
117
+ if (typeof value === "object" && value !== null && "id" in value) {
118
+ return { value }
119
+ }
120
+ return { issues: [{ message: "Invalid user" }] }
121
+ },
122
+ },
123
+ }
124
+ ```
125
+
126
+ ### Event types and type guards
127
+
128
+ ```typescript
129
+ import { isChangeEvent, isControlEvent } from "@durable-streams/state"
130
+ import type {
131
+ StateEvent,
132
+ ChangeEvent,
133
+ ControlEvent,
134
+ } from "@durable-streams/state"
135
+
136
+ function handleEvent(event: StateEvent) {
137
+ if (isChangeEvent(event)) {
138
+ // event.type, event.key, event.value, event.headers.operation
139
+ console.log(`${event.headers.operation}: ${event.type}/${event.key}`)
140
+ }
141
+ if (isControlEvent(event)) {
142
+ // event.headers.control: "snapshot-start" | "snapshot-end" | "reset"
143
+ console.log(`Control: ${event.headers.control}`)
144
+ }
145
+ }
146
+ ```
147
+
148
+ ## Common Mistakes
149
+
150
+ ### CRITICAL Using primitive values instead of objects in collections
151
+
152
+ Wrong:
153
+
154
+ ```typescript
155
+ { type: "count", key: "views", value: 42 }
156
+ ```
157
+
158
+ Correct:
159
+
160
+ ```typescript
161
+ { type: "count", key: "views", value: { count: 42 } }
162
+ ```
163
+
164
+ Collections require object values so the `primaryKey` field can be extracted. Primitive values throw during dispatch.
165
+
166
+ Source: packages/state/README.md best practices
167
+
168
+ ### HIGH Using duplicate event types across collections
169
+
170
+ Wrong:
171
+
172
+ ```typescript
173
+ createStateSchema({
174
+ users: { schema: userSchema, type: "entity", primaryKey: "id" },
175
+ posts: { schema: postSchema, type: "entity", primaryKey: "id" },
176
+ })
177
+ ```
178
+
179
+ Correct:
180
+
181
+ ```typescript
182
+ createStateSchema({
183
+ users: { schema: userSchema, type: "user", primaryKey: "id" },
184
+ posts: { schema: postSchema, type: "post", primaryKey: "id" },
185
+ })
186
+ ```
187
+
188
+ `createStateSchema()` throws if two collections share the same `type` string. The `type` field routes events to collections — duplicates would be ambiguous.
189
+
190
+ Source: packages/state/src/stream-db.ts createStateSchema validation
191
+
192
+ ### HIGH Forgetting to use a Standard Schema-compatible validator
193
+
194
+ Wrong:
195
+
196
+ ```typescript
197
+ interface User {
198
+ id: string
199
+ name: string
200
+ }
201
+ createStateSchema({
202
+ users: { schema: User, type: "user", primaryKey: "id" }, // Not a validator!
203
+ })
204
+ ```
205
+
206
+ Correct:
207
+
208
+ ```typescript
209
+ import { z } from "zod"
210
+ const userSchema = z.object({ id: z.string(), name: z.string() })
211
+ createStateSchema({
212
+ users: { schema: userSchema, type: "user", primaryKey: "id" },
213
+ })
214
+ ```
215
+
216
+ The `schema` field requires an object implementing the `~standard` interface. TypeScript interfaces and plain types are not validators.
217
+
218
+ Source: packages/state/README.md Standard Schema support section
219
+
220
+ ### MEDIUM Using reserved collection names
221
+
222
+ Wrong:
223
+
224
+ ```typescript
225
+ createStateSchema({
226
+ actions: { schema: actionSchema, type: "action", primaryKey: "id" },
227
+ })
228
+ ```
229
+
230
+ Correct:
231
+
232
+ ```typescript
233
+ createStateSchema({
234
+ userActions: { schema: actionSchema, type: "action", primaryKey: "id" },
235
+ })
236
+ ```
237
+
238
+ Collection names `collections`, `preload`, `close`, `utils`, and `actions` are reserved — they collide with the StreamDB API surface.
239
+
240
+ Source: packages/state/src/stream-db.ts reserved name check
241
+
242
+ ### HIGH Tension: Schema strictness vs. prototyping speed
243
+
244
+ 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.
245
+
246
+ See also: durable-streams/getting-started/SKILL.md
247
+
248
+ ## See also
249
+
250
+ - [stream-db](../stream-db/SKILL.md) — Wire schemas into a reactive StreamDB
251
+
252
+ ## Version
253
+
254
+ Targets @durable-streams/state v0.2.1.
@@ -0,0 +1,264 @@
1
+ ---
2
+ name: stream-db
3
+ description: >
4
+ Stream-backed reactive database with @durable-streams/state. createStreamDB()
5
+ with schema and stream options, db.preload() lazy initialization,
6
+ db.collections for TanStack DB collections, optimistic actions with onMutate
7
+ and mutationFn, db.utils.awaitTxId() for transaction confirmation, control
8
+ events (snapshot-start, snapshot-end, reset), db.close() cleanup, re-exported
9
+ TanStack DB operators (eq, gt, and, or, count, sum, avg, min, max).
10
+ type: core
11
+ library: durable-streams
12
+ library_version: "0.2.1"
13
+ requires:
14
+ - state-schema
15
+ sources:
16
+ - "durable-streams/durable-streams:packages/state/src/stream-db.ts"
17
+ - "durable-streams/durable-streams:packages/state/README.md"
18
+ ---
19
+
20
+ This skill builds on durable-streams/state-schema. Read it first for schema definition and event types.
21
+
22
+ # Durable Streams — StreamDB
23
+
24
+ Create a stream-backed reactive database that syncs structured state from a
25
+ durable stream into TanStack DB collections. Provides reactive queries,
26
+ optimistic actions, and transaction confirmation.
27
+
28
+ ## Setup
29
+
30
+ ```typescript
31
+ import { createStreamDB, createStateSchema } from "@durable-streams/state"
32
+ import { DurableStream } from "@durable-streams/client"
33
+ import { z } from "zod"
34
+
35
+ const schema = createStateSchema({
36
+ users: {
37
+ schema: z.object({ id: z.string(), name: z.string(), email: z.string() }),
38
+ type: "user",
39
+ primaryKey: "id",
40
+ },
41
+ messages: {
42
+ schema: z.object({ id: z.string(), text: z.string(), userId: z.string() }),
43
+ type: "message",
44
+ primaryKey: "id",
45
+ },
46
+ })
47
+
48
+ const db = createStreamDB({
49
+ streamOptions: {
50
+ url: "https://your-server.com/v1/stream/my-app",
51
+ contentType: "application/json",
52
+ },
53
+ state: schema,
54
+ })
55
+
56
+ // The stream must already exist on the server before preload().
57
+ // Use DurableStream.connect() to attach to an existing stream,
58
+ // or create it first if it doesn't exist yet:
59
+ try {
60
+ await DurableStream.create({
61
+ url: "https://your-server.com/v1/stream/my-app",
62
+ contentType: "application/json",
63
+ })
64
+ } catch (e) {
65
+ if (e.code !== "CONFLICT_EXISTS") throw e // Already exists is fine
66
+ }
67
+
68
+ // Connect and load initial data
69
+ await db.preload()
70
+
71
+ // Access TanStack DB collections
72
+ const users = db.collections.users
73
+ const messages = db.collections.messages
74
+ ```
75
+
76
+ ## Core Patterns
77
+
78
+ ### Reactive queries with TanStack DB
79
+
80
+ StreamDB collections are TanStack DB collections. Use framework adapters for reactive queries:
81
+
82
+ ```typescript
83
+ import { useLiveQuery } from "@tanstack/react-db"
84
+ import { eq } from "@durable-streams/state"
85
+
86
+ function UserProfile({ userId }: { userId: string }) {
87
+ const userQuery = useLiveQuery((q) =>
88
+ q
89
+ .from({ users: db.collections.users })
90
+ .where(({ users }) => eq(users.id, userId))
91
+ .findOne()
92
+ )
93
+
94
+ if (!userQuery.data) return null
95
+ return <div>{userQuery.data.name}</div>
96
+ }
97
+ ```
98
+
99
+ ### Optimistic actions with server confirmation
100
+
101
+ ```typescript
102
+ import { createStreamDB, createStateSchema } from "@durable-streams/state"
103
+ import { z } from "zod"
104
+
105
+ const schema = createStateSchema({
106
+ users: {
107
+ schema: z.object({ id: z.string(), name: z.string() }),
108
+ type: "user",
109
+ primaryKey: "id",
110
+ },
111
+ })
112
+
113
+ const db = createStreamDB({
114
+ streamOptions: {
115
+ url: "https://your-server.com/v1/stream/my-app",
116
+ contentType: "application/json",
117
+ },
118
+ state: schema,
119
+ actions: ({ db, stream }) => ({
120
+ addUser: {
121
+ onMutate: (user) => {
122
+ db.collections.users.insert(user) // Optimistic — shows immediately
123
+ },
124
+ mutationFn: async (user) => {
125
+ const txid = crypto.randomUUID()
126
+ await stream.append(
127
+ JSON.stringify(
128
+ schema.users.insert({ value: user, headers: { txid } })
129
+ )
130
+ )
131
+ await db.utils.awaitTxId(txid, 10000) // Wait for confirmation
132
+ },
133
+ },
134
+ }),
135
+ })
136
+
137
+ await db.preload()
138
+ await db.actions.addUser({ id: "1", name: "Kyle" })
139
+ ```
140
+
141
+ ### Cleanup on unmount
142
+
143
+ ```typescript
144
+ import { useEffect, useState } from "react"
145
+
146
+ function App() {
147
+ const [db, setDb] = useState(null)
148
+
149
+ useEffect(() => {
150
+ const database = createStreamDB({ streamOptions, state: schema })
151
+ database.preload().then(() => setDb(database))
152
+ return () => database.close() // Clean up connections and timers
153
+ }, [])
154
+
155
+ if (!db) return <div>Loading...</div>
156
+ return <Dashboard db={db} />
157
+ }
158
+ ```
159
+
160
+ ### SSR: StreamDB is client-only
161
+
162
+ StreamDB holds open HTTP connections and relies on browser/Node.js runtime features. In meta-frameworks (TanStack Start, Next.js, Remix), ensure StreamDB only runs on the client:
163
+
164
+ ```typescript
165
+ // TanStack Start / React Router — mark the route as client-only
166
+ export const Route = createFileRoute("/dashboard")({
167
+ ssr: false,
168
+ component: Dashboard,
169
+ })
170
+ ```
171
+
172
+ Without `ssr: false`, the server-side render will attempt to create StreamDB and fail or produce `instanceof` mismatches between server and client bundles.
173
+
174
+ ## Common Mistakes
175
+
176
+ ### CRITICAL Forgetting to call preload() before accessing data
177
+
178
+ Wrong:
179
+
180
+ ```typescript
181
+ const db = createStreamDB({ streamOptions, state: schema })
182
+ const users = db.collections.users // Collections are empty!
183
+ ```
184
+
185
+ Correct:
186
+
187
+ ```typescript
188
+ const db = createStreamDB({ streamOptions, state: schema })
189
+ await db.preload() // Connect and load initial data
190
+ const users = db.collections.users
191
+ ```
192
+
193
+ StreamDB creates the stream lazily. Without `preload()`, no connection is established and collections remain empty.
194
+
195
+ Source: packages/state/src/stream-db.ts
196
+
197
+ ### HIGH Not calling close() on unmount/cleanup
198
+
199
+ Wrong:
200
+
201
+ ```typescript
202
+ useEffect(() => {
203
+ const db = createStreamDB({ streamOptions, state: schema })
204
+ db.preload()
205
+ setDb(db)
206
+ }, [])
207
+ ```
208
+
209
+ Correct:
210
+
211
+ ```typescript
212
+ useEffect(() => {
213
+ const db = createStreamDB({ streamOptions, state: schema })
214
+ db.preload()
215
+ setDb(db)
216
+ return () => db.close()
217
+ }, [])
218
+ ```
219
+
220
+ StreamDB holds open HTTP connections and a 15-second health check interval. Forgetting `close()` leaks connections and timers.
221
+
222
+ Source: packages/state/README.md best practices
223
+
224
+ ### HIGH Not using awaitTxId for critical writes
225
+
226
+ Wrong:
227
+
228
+ ```typescript
229
+ mutationFn: async (user) => {
230
+ await stream.append(JSON.stringify(schema.users.insert({ value: user })))
231
+ // No confirmation — optimistic state may diverge from server
232
+ }
233
+ ```
234
+
235
+ Correct:
236
+
237
+ ```typescript
238
+ mutationFn: async (user) => {
239
+ const txid = crypto.randomUUID()
240
+ await stream.append(
241
+ JSON.stringify(schema.users.insert({ value: user, headers: { txid } }))
242
+ )
243
+ await db.utils.awaitTxId(txid, 10000) // Wait up to 10 seconds
244
+ }
245
+ ```
246
+
247
+ Without `awaitTxId`, the client has no confirmation that the write was persisted. Optimistic state may diverge if the write fails silently.
248
+
249
+ Source: packages/state/README.md transaction IDs section
250
+
251
+ ### HIGH Tension: Catch-up completeness vs. live latency
252
+
253
+ This skill's patterns conflict with reading-streams. `preload()` waits for all existing data before resolving, which may take time for large streams. Agents may forget that after `preload()`, the StreamDB is already in live-tailing mode — no additional subscription setup is needed.
254
+
255
+ See also: durable-streams/reading-streams/SKILL.md
256
+
257
+ ## See also
258
+
259
+ - [state-schema](../state-schema/SKILL.md) — Define schemas before creating a StreamDB
260
+ - [reading-streams](../../../client/skills/reading-streams/SKILL.md) — Understanding live modes and offset management
261
+
262
+ ## Version
263
+
264
+ Targets @durable-streams/state v0.2.1.