@durable-streams/state 0.1.0
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/README.md +654 -0
- package/STATE-PROTOCOL.md +502 -0
- package/dist/index.cjs +558 -0
- package/dist/index.d.cts +284 -0
- package/dist/index.d.ts +284 -0
- package/dist/index.js +530 -0
- package/package.json +48 -0
- package/src/index.ts +33 -0
- package/src/materialized-state.ts +93 -0
- package/src/stream-db.ts +934 -0
- package/src/types.ts +80 -0
- package/state-protocol.schema.json +186 -0
package/README.md
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
# @durable-streams/state
|
|
2
|
+
|
|
3
|
+
Building blocks for transmitting structured state over Durable Streams. Use these primitives for any real-time protocol: AI token streams, presence updates, collaborative editing, or database sync.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @durable-streams/state
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This package provides flexible primitives for streaming structured state. You choose how much structure you need:
|
|
14
|
+
|
|
15
|
+
- **Simple state updates**: Stream JSON payloads and track current values
|
|
16
|
+
- **Typed collections**: Add schemas and primary keys for structured entities
|
|
17
|
+
- **Reactive queries**: Build on TanStack DB for subscriptions and optimistic updates
|
|
18
|
+
|
|
19
|
+
Stream whatever state your protocol requires.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Simple State
|
|
24
|
+
|
|
25
|
+
Stream structured JSON and query current values:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { MaterializedState } from "@durable-streams/state"
|
|
29
|
+
|
|
30
|
+
const state = new MaterializedState()
|
|
31
|
+
|
|
32
|
+
// Apply any structured change
|
|
33
|
+
state.apply({
|
|
34
|
+
type: "token",
|
|
35
|
+
key: "stream-1",
|
|
36
|
+
value: { content: "Hello", model: "claude-3" },
|
|
37
|
+
headers: { operation: "insert" },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Query current state
|
|
41
|
+
const token = state.get("token", "stream-1")
|
|
42
|
+
const allTokens = state.getType("token")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Typed Collections
|
|
46
|
+
|
|
47
|
+
Add schemas and validation for structured entities:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { createStateSchema, createStreamDB } from "@durable-streams/state"
|
|
51
|
+
|
|
52
|
+
// Define your schema
|
|
53
|
+
const schema = createStateSchema({
|
|
54
|
+
users: {
|
|
55
|
+
schema: userSchema, // Standard Schema validator
|
|
56
|
+
type: "user", // Event type field
|
|
57
|
+
primaryKey: "id", // Primary key field name
|
|
58
|
+
},
|
|
59
|
+
messages: {
|
|
60
|
+
schema: messageSchema,
|
|
61
|
+
type: "message",
|
|
62
|
+
primaryKey: "id",
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Create a stream-backed database
|
|
67
|
+
const db = createStreamDB({
|
|
68
|
+
streamOptions: {
|
|
69
|
+
url: "https://api.example.com/streams/my-stream",
|
|
70
|
+
contentType: "application/json",
|
|
71
|
+
},
|
|
72
|
+
state: schema,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Load initial data
|
|
76
|
+
await db.preload()
|
|
77
|
+
|
|
78
|
+
// Reactive queries with useLiveQuery
|
|
79
|
+
import { useLiveQuery } from "@tanstack/react-db" // or solid-db, vue-db
|
|
80
|
+
import { eq } from "@tanstack/db"
|
|
81
|
+
|
|
82
|
+
const userQuery = useLiveQuery((q) =>
|
|
83
|
+
q
|
|
84
|
+
.from({ users: db.collections.users })
|
|
85
|
+
.where(({ users }) => eq(users.id, "123"))
|
|
86
|
+
.findOne()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const allUsersQuery = useLiveQuery((q) =>
|
|
90
|
+
q.from({ users: db.collections.users })
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Core Concepts
|
|
95
|
+
|
|
96
|
+
### State Protocol
|
|
97
|
+
|
|
98
|
+
The Durable Streams State Protocol defines a standard format for state change events:
|
|
99
|
+
|
|
100
|
+
- **Change Events**: `insert`, `update`, `delete` operations on entities
|
|
101
|
+
- **Control Events**: `snapshot-start`, `snapshot-end`, `reset` signals
|
|
102
|
+
- **Entity Types**: Discriminator field that routes events to collections
|
|
103
|
+
- **Primary Keys**: Unique identifiers extracted from entity values
|
|
104
|
+
|
|
105
|
+
See [STATE-PROTOCOL.md](./STATE-PROTOCOL.md) for the full specification.
|
|
106
|
+
|
|
107
|
+
### MaterializedState
|
|
108
|
+
|
|
109
|
+
Simple in-memory state container for basic use cases:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { MaterializedState } from "@durable-streams/state"
|
|
113
|
+
|
|
114
|
+
const state = new MaterializedState()
|
|
115
|
+
|
|
116
|
+
// Apply change events
|
|
117
|
+
state.apply({
|
|
118
|
+
type: "user",
|
|
119
|
+
key: "1",
|
|
120
|
+
value: { name: "Kyle" },
|
|
121
|
+
headers: { operation: "insert" },
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Query state
|
|
125
|
+
const user = state.get("user", "1")
|
|
126
|
+
const allUsers = state.getType("user")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### StreamDB
|
|
130
|
+
|
|
131
|
+
Stream-backed database with TanStack DB collections. Provides reactive queries, subscriptions, and optimistic updates.
|
|
132
|
+
|
|
133
|
+
## Schema Definition
|
|
134
|
+
|
|
135
|
+
### createStateSchema()
|
|
136
|
+
|
|
137
|
+
Define your application state structure:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const schema = createStateSchema({
|
|
141
|
+
users: {
|
|
142
|
+
schema: userSchema, // Standard Schema validator
|
|
143
|
+
type: "user", // Event type for routing
|
|
144
|
+
primaryKey: "id", // Field to use as primary key
|
|
145
|
+
},
|
|
146
|
+
messages: {
|
|
147
|
+
schema: messageSchema,
|
|
148
|
+
type: "message",
|
|
149
|
+
primaryKey: "id",
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Standard Schema Support
|
|
155
|
+
|
|
156
|
+
Uses [Standard Schema](https://standardschema.dev/) for validation, supporting multiple libraries:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Zod
|
|
160
|
+
import { z } from "zod"
|
|
161
|
+
|
|
162
|
+
const userSchema = z.object({
|
|
163
|
+
id: z.string(),
|
|
164
|
+
name: z.string(),
|
|
165
|
+
email: z.string().email(),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Valibot
|
|
169
|
+
import * as v from "valibot"
|
|
170
|
+
|
|
171
|
+
const userSchema = v.object({
|
|
172
|
+
id: v.string(),
|
|
173
|
+
name: v.string(),
|
|
174
|
+
email: v.pipe(v.string(), v.email()),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Manual Standard Schema
|
|
178
|
+
const userSchema = {
|
|
179
|
+
"~standard": {
|
|
180
|
+
version: 1,
|
|
181
|
+
vendor: "my-app",
|
|
182
|
+
validate: (value) => {
|
|
183
|
+
// Your validation logic
|
|
184
|
+
if (isValid(value)) {
|
|
185
|
+
return { value }
|
|
186
|
+
}
|
|
187
|
+
return { issues: [{ message: "Invalid user" }] }
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Event Helpers
|
|
194
|
+
|
|
195
|
+
Schema provides typed event creation helpers:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Insert
|
|
199
|
+
const insertEvent = schema.users.insert({
|
|
200
|
+
value: { id: "1", name: "Kyle", email: "kyle@example.com" },
|
|
201
|
+
key: "1", // Optional, defaults to value[primaryKey]
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Update
|
|
205
|
+
const updateEvent = schema.users.update({
|
|
206
|
+
value: { id: "1", name: "Kyle Mathews", email: "kyle@example.com" },
|
|
207
|
+
oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" }, // Optional
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Delete
|
|
211
|
+
const deleteEvent = schema.users.delete({
|
|
212
|
+
key: "1",
|
|
213
|
+
oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" }, // Optional
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Custom headers
|
|
217
|
+
const eventWithTxId = schema.users.insert({
|
|
218
|
+
value: { id: "1", name: "Kyle" },
|
|
219
|
+
headers: {
|
|
220
|
+
txid: crypto.randomUUID(),
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## StreamDB
|
|
227
|
+
|
|
228
|
+
### Creating a Database
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const db = createStreamDB({
|
|
232
|
+
streamOptions: {
|
|
233
|
+
url: "https://api.example.com/streams/my-stream",
|
|
234
|
+
contentType: "application/json",
|
|
235
|
+
// All DurableStream options supported
|
|
236
|
+
headers: { Authorization: "Bearer token" },
|
|
237
|
+
batching: true,
|
|
238
|
+
},
|
|
239
|
+
state: schema,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// The stream is created lazily when preload() is called
|
|
243
|
+
await db.preload()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Reactive Queries with TanStack DB
|
|
247
|
+
|
|
248
|
+
StreamDB collections are TanStack DB collections. Use TanStack DB's query builder for filtering, sorting, aggregation, and joins with **differential dataflow** - dramatically faster than JavaScript filtering:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { useLiveQuery } from "@tanstack/[framework]-db" // react-db, solid-db, etc
|
|
252
|
+
import { eq, gt, and, count } from "@tanstack/db"
|
|
253
|
+
|
|
254
|
+
// Simple collection access
|
|
255
|
+
const query = useLiveQuery((q) => q.from({ users: db.collections.users }))
|
|
256
|
+
|
|
257
|
+
// Filtering with WHERE
|
|
258
|
+
const activeQuery = useLiveQuery((q) =>
|
|
259
|
+
q
|
|
260
|
+
.from({ users: db.collections.users })
|
|
261
|
+
.where(({ users }) => eq(users.active, true))
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
// Complex conditions
|
|
265
|
+
const query = useLiveQuery((q) =>
|
|
266
|
+
q
|
|
267
|
+
.from({ users: db.collections.users })
|
|
268
|
+
.where(({ users }) => and(eq(users.active, true), gt(users.age, 18)))
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Sorting and limiting
|
|
272
|
+
const topUsersQuery = useLiveQuery((q) =>
|
|
273
|
+
q
|
|
274
|
+
.from({ users: db.collections.users })
|
|
275
|
+
.orderBy(({ users }) => users.lastSeen, "desc")
|
|
276
|
+
.limit(10)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Aggregation with GROUP BY and ordering
|
|
280
|
+
const langStatsQuery = useLiveQuery((q) => {
|
|
281
|
+
const languageCounts = q
|
|
282
|
+
.from({ events: db.collections.events })
|
|
283
|
+
.groupBy(({ events }) => events.language)
|
|
284
|
+
.select(({ events }) => ({
|
|
285
|
+
language: events.language,
|
|
286
|
+
total: count(events.id),
|
|
287
|
+
}))
|
|
288
|
+
|
|
289
|
+
return q
|
|
290
|
+
.from({ stats: languageCounts })
|
|
291
|
+
.orderBy(({ stats }) => stats.total, "desc")
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// Joins across collections
|
|
295
|
+
const query = useLiveQuery((q) =>
|
|
296
|
+
q
|
|
297
|
+
.from({ messages: db.collections.messages })
|
|
298
|
+
.join({ users: db.collections.users }, ({ messages, users }) =>
|
|
299
|
+
eq(messages.userId, users.id)
|
|
300
|
+
)
|
|
301
|
+
.select(({ messages, users }) => ({
|
|
302
|
+
messageId: messages.id,
|
|
303
|
+
text: messages.text,
|
|
304
|
+
userName: users.name,
|
|
305
|
+
}))
|
|
306
|
+
)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Why use the query builder?**
|
|
310
|
+
|
|
311
|
+
- **Differential dataflow**: Incremental updates only recompute affected results
|
|
312
|
+
- **Dramatically faster**: Push filtering/sorting into the DB engine vs JavaScript
|
|
313
|
+
- **Reactive**: Queries automatically update when data changes
|
|
314
|
+
- **Type-safe**: Full TypeScript support with autocomplete
|
|
315
|
+
|
|
316
|
+
**Framework integration**: See [TanStack DB docs](https://tanstack.com/db) for framework-specific guides:
|
|
317
|
+
|
|
318
|
+
- [@tanstack/react-db](https://tanstack.com/db/latest/docs/framework/react/overview)
|
|
319
|
+
- [@tanstack/solid-db](https://tanstack.com/db/latest/docs/framework/solid/overview)
|
|
320
|
+
- [@tanstack/vue-db](https://tanstack.com/db/latest/docs/framework/vue/overview)
|
|
321
|
+
|
|
322
|
+
### Lifecycle Methods
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Load all data until up-to-date
|
|
326
|
+
await db.preload()
|
|
327
|
+
|
|
328
|
+
// Stop syncing and cleanup
|
|
329
|
+
db.close()
|
|
330
|
+
|
|
331
|
+
// Wait for a transaction to be confirmed
|
|
332
|
+
await db.utils.awaitTxId("txid-uuid", 5000) // 5 second timeout
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Optimistic Actions
|
|
336
|
+
|
|
337
|
+
Define actions with optimistic updates and server confirmation:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const db = createStreamDB({
|
|
341
|
+
streamOptions: { url: streamUrl, contentType: "application/json" },
|
|
342
|
+
state: schema,
|
|
343
|
+
actions: ({ db, stream }) => ({
|
|
344
|
+
addUser: {
|
|
345
|
+
// Optimistic update (runs immediately)
|
|
346
|
+
onMutate: (user) => {
|
|
347
|
+
db.collections.users.insert(user)
|
|
348
|
+
},
|
|
349
|
+
// Server mutation (runs async)
|
|
350
|
+
mutationFn: async (user) => {
|
|
351
|
+
const txid = crypto.randomUUID()
|
|
352
|
+
|
|
353
|
+
await stream.append(
|
|
354
|
+
schema.users.insert({
|
|
355
|
+
value: user,
|
|
356
|
+
headers: { txid },
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
// Wait for confirmation
|
|
361
|
+
await db.utils.awaitTxId(txid)
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
updateUser: {
|
|
366
|
+
onMutate: ({ id, updates }) => {
|
|
367
|
+
db.collections.users.update(id, (draft) => {
|
|
368
|
+
Object.assign(draft, updates)
|
|
369
|
+
})
|
|
370
|
+
},
|
|
371
|
+
mutationFn: async ({ id, updates }) => {
|
|
372
|
+
const txid = crypto.randomUUID()
|
|
373
|
+
const current = await db.collections.users.get(id)
|
|
374
|
+
|
|
375
|
+
await stream.append(
|
|
376
|
+
schema.users.update({
|
|
377
|
+
value: { ...current, ...updates },
|
|
378
|
+
oldValue: current,
|
|
379
|
+
headers: { txid },
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
await db.utils.awaitTxId(txid)
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// Call actions
|
|
390
|
+
await db.actions.addUser({ id: "1", name: "Kyle", email: "kyle@example.com" })
|
|
391
|
+
await db.actions.updateUser({ id: "1", updates: { name: "Kyle Mathews" } })
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Framework Integration
|
|
395
|
+
|
|
396
|
+
Use TanStack DB's framework adapters for reactive queries:
|
|
397
|
+
|
|
398
|
+
### React
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { useLiveQuery } from '@tanstack/react-db'
|
|
402
|
+
import { eq } from '@tanstack/db'
|
|
403
|
+
|
|
404
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
405
|
+
const userQuery = useLiveQuery((q) =>
|
|
406
|
+
q.from({ users: db.collections.users })
|
|
407
|
+
.where(({ users }) => eq(users.id, userId))
|
|
408
|
+
.findOne()
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if (userQuery.isLoading()) return <div>Loading...</div>
|
|
412
|
+
if (!userQuery.data) return <div>Not found</div>
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div>
|
|
416
|
+
<h1>{userQuery.data.name}</h1>
|
|
417
|
+
<p>{userQuery.data.email}</p>
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
See [@tanstack/react-db docs](https://tanstack.com/db/latest/docs/framework/react/overview) for more.
|
|
424
|
+
|
|
425
|
+
### Solid.js
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { useLiveQuery } from '@tanstack/solid-db'
|
|
429
|
+
import { eq } from '@tanstack/db'
|
|
430
|
+
|
|
431
|
+
function MessageList() {
|
|
432
|
+
const messagesQuery = useLiveQuery((q) =>
|
|
433
|
+
q.from({ messages: db.collections.messages })
|
|
434
|
+
.orderBy(({ messages }) => messages.timestamp, 'desc')
|
|
435
|
+
.limit(50)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<For each={messagesQuery.data}>
|
|
440
|
+
{(message) => <MessageCard message={message} />}
|
|
441
|
+
</For>
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
See [@tanstack/solid-db docs](https://tanstack.com/db/latest/docs/framework/solid/overview) for more.
|
|
447
|
+
|
|
448
|
+
## Common Patterns
|
|
449
|
+
|
|
450
|
+
### Key/Value Store
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
const schema = createStateSchema({
|
|
454
|
+
config: {
|
|
455
|
+
schema: configSchema,
|
|
456
|
+
type: "config",
|
|
457
|
+
primaryKey: "key",
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// Set value
|
|
462
|
+
await stream.append(
|
|
463
|
+
schema.config.insert({
|
|
464
|
+
value: { key: "theme", value: "dark" },
|
|
465
|
+
})
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
// Query value reactively
|
|
469
|
+
const themeQuery = useLiveQuery((q) =>
|
|
470
|
+
q
|
|
471
|
+
.from({ config: db.collections.config })
|
|
472
|
+
.where(({ config }) => eq(config.key, "theme"))
|
|
473
|
+
.findOne()
|
|
474
|
+
)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Presence Tracking
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
const schema = createStateSchema({
|
|
481
|
+
presence: {
|
|
482
|
+
schema: presenceSchema,
|
|
483
|
+
type: "presence",
|
|
484
|
+
primaryKey: "userId",
|
|
485
|
+
},
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
// Update presence
|
|
489
|
+
await stream.append(
|
|
490
|
+
schema.presence.update({
|
|
491
|
+
value: {
|
|
492
|
+
userId: "kyle",
|
|
493
|
+
status: "online",
|
|
494
|
+
lastSeen: Date.now(),
|
|
495
|
+
},
|
|
496
|
+
})
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
// Query presence with TanStack DB
|
|
500
|
+
const presenceQuery = useLiveQuery((q) =>
|
|
501
|
+
q
|
|
502
|
+
.from({ presence: db.collections.presence })
|
|
503
|
+
.where(({ presence }) => eq(presence.status, "online"))
|
|
504
|
+
)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Multi-Type Chat Room
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
const schema = createStateSchema({
|
|
511
|
+
users: { schema: userSchema, type: "user", primaryKey: "id" },
|
|
512
|
+
messages: { schema: messageSchema, type: "message", primaryKey: "id" },
|
|
513
|
+
reactions: { schema: reactionSchema, type: "reaction", primaryKey: "id" },
|
|
514
|
+
typing: { schema: typingSchema, type: "typing", primaryKey: "userId" },
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// Different types coexist in the same stream
|
|
518
|
+
await stream.append(schema.users.insert({ value: user }))
|
|
519
|
+
await stream.append(schema.messages.insert({ value: message }))
|
|
520
|
+
await stream.append(schema.reactions.insert({ value: reaction }))
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Best Practices
|
|
524
|
+
|
|
525
|
+
### 1. Use Object Values
|
|
526
|
+
|
|
527
|
+
StreamDB requires object values (not primitives) for the primary key pattern:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
// ❌ Won't work
|
|
531
|
+
{ type: 'count', key: 'views', value: 42 }
|
|
532
|
+
|
|
533
|
+
// ✅ Works
|
|
534
|
+
{ type: 'count', key: 'views', value: { count: 42 } }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### 2. Always Call close()
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
const db = createStreamDB({ streamOptions, state: schema })
|
|
542
|
+
|
|
543
|
+
return () => db.close() // Cleanup on unmount
|
|
544
|
+
}, [])
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### 3. Validate at Boundaries
|
|
548
|
+
|
|
549
|
+
Use Standard Schema to validate data at system boundaries:
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
const userSchema = z.object({
|
|
553
|
+
id: z.string().uuid(),
|
|
554
|
+
email: z.string().email(),
|
|
555
|
+
age: z.number().min(0).max(150),
|
|
556
|
+
})
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### 4. Use Transaction IDs
|
|
560
|
+
|
|
561
|
+
For critical operations, always use transaction IDs to ensure confirmation:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
const txid = crypto.randomUUID()
|
|
565
|
+
await stream.append(schema.users.insert({ value: user, headers: { txid } }))
|
|
566
|
+
await db.utils.awaitTxId(txid, 10000) // Wait up to 10 seconds
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### 5. Handle Errors Gracefully
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
try {
|
|
573
|
+
await db.actions.addUser(user)
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error.message.includes("Timeout")) {
|
|
576
|
+
// Handle timeout
|
|
577
|
+
} else {
|
|
578
|
+
// Handle other errors
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## API Reference
|
|
584
|
+
|
|
585
|
+
### Types
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
export type Operation = "insert" | "update" | "delete"
|
|
589
|
+
|
|
590
|
+
export interface ChangeEvent<T = unknown> {
|
|
591
|
+
type: string
|
|
592
|
+
key: string
|
|
593
|
+
value?: T
|
|
594
|
+
old_value?: T
|
|
595
|
+
headers: ChangeHeaders
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export interface ChangeHeaders {
|
|
599
|
+
operation: Operation
|
|
600
|
+
txid?: string
|
|
601
|
+
timestamp?: string
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface ControlEvent {
|
|
605
|
+
headers: {
|
|
606
|
+
control: "snapshot-start" | "snapshot-end" | "reset"
|
|
607
|
+
offset?: string
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export type StateEvent<T = unknown> = ChangeEvent<T> | ControlEvent
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Functions
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
// Create a state schema with typed collections and event helpers
|
|
618
|
+
export function createStateSchema<
|
|
619
|
+
T extends Record<string, CollectionDefinition>,
|
|
620
|
+
>(collections: T): StateSchema<T>
|
|
621
|
+
|
|
622
|
+
// Create a stream-backed database
|
|
623
|
+
export async function createStreamDB<
|
|
624
|
+
TDef extends StreamStateDefinition,
|
|
625
|
+
TActions extends Record<string, ActionDefinition<any>>,
|
|
626
|
+
>(
|
|
627
|
+
options: CreateStreamDBOptions<TDef, TActions>
|
|
628
|
+
): Promise<StreamDB<TDef> | StreamDBWithActions<TDef, TActions>>
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Classes
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
export class MaterializedState {
|
|
635
|
+
apply(event: ChangeEvent): void
|
|
636
|
+
applyBatch(events: ChangeEvent[]): void
|
|
637
|
+
get<T>(type: string, key: string): T | undefined
|
|
638
|
+
getType(type: string): Map<string, unknown>
|
|
639
|
+
clear(): void
|
|
640
|
+
readonly typeCount: number
|
|
641
|
+
readonly types: string[]
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
## License
|
|
646
|
+
|
|
647
|
+
Apache-2.0
|
|
648
|
+
|
|
649
|
+
## Learn More
|
|
650
|
+
|
|
651
|
+
- [STATE-PROTOCOL.md](./STATE-PROTOCOL.md) - Full protocol specification
|
|
652
|
+
- [Durable Streams Protocol](../../PROTOCOL.md) - Base protocol
|
|
653
|
+
- [Standard Schema](https://standardschema.dev/) - Schema validation
|
|
654
|
+
- [TanStack DB](https://tanstack.com/db) - Reactive collections
|