@durable-streams/state 0.2.0 → 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 +6 -0
- package/package.json +11 -4
- package/skills/state-schema/SKILL.md +254 -0
- package/skills/stream-db/SKILL.md +264 -0
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.
|
|
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.
|
|
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.
|
|
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.
|