@durable-streams/client 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 +6 -0
- package/dist/index.cjs +364 -168
- package/dist/index.js +364 -168
- package/package.json +10 -3
- package/skills/getting-started/SKILL.md +223 -0
- package/skills/go-to-production/SKILL.md +243 -0
- package/skills/reading-streams/SKILL.md +247 -0
- package/skills/reading-streams/references/stream-response-methods.md +133 -0
- package/skills/server-deployment/SKILL.md +211 -0
- package/skills/writing-data/SKILL.md +311 -0
- package/src/response.ts +332 -303
- package/src/stream-response-state.ts +306 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-data
|
|
3
|
+
description: >
|
|
4
|
+
Writing data to durable streams. DurableStream.create() with contentType,
|
|
5
|
+
DurableStream.append() for simple writes, IdempotentProducer for
|
|
6
|
+
high-throughput exactly-once delivery with autoClaim, fire-and-forget
|
|
7
|
+
append(), flush(), close(), StaleEpochError handling, JSON mode vs byte
|
|
8
|
+
stream mode, stream closure. Load when writing, producing, or appending
|
|
9
|
+
data to a durable stream.
|
|
10
|
+
type: core
|
|
11
|
+
library: durable-streams
|
|
12
|
+
library_version: "0.2.1"
|
|
13
|
+
requires:
|
|
14
|
+
- getting-started
|
|
15
|
+
sources:
|
|
16
|
+
- "durable-streams/durable-streams:packages/client/src/stream.ts"
|
|
17
|
+
- "durable-streams/durable-streams:packages/client/src/idempotent-producer.ts"
|
|
18
|
+
- "durable-streams/durable-streams:packages/client/src/types.ts"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
This skill builds on durable-streams/getting-started. Read it first for setup and offset basics.
|
|
22
|
+
|
|
23
|
+
# Durable Streams — Writing Data
|
|
24
|
+
|
|
25
|
+
Two write APIs: `DurableStream.append()` for simple writes, `IdempotentProducer`
|
|
26
|
+
for sustained high-throughput writes with exactly-once delivery. Use the producer
|
|
27
|
+
for anything beyond a few one-off appends.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
DurableStream,
|
|
34
|
+
IdempotentProducer,
|
|
35
|
+
StaleEpochError,
|
|
36
|
+
} from "@durable-streams/client"
|
|
37
|
+
|
|
38
|
+
// Create a JSON-mode stream
|
|
39
|
+
const handle = await DurableStream.create({
|
|
40
|
+
url: "https://your-server.com/v1/stream/my-stream",
|
|
41
|
+
contentType: "application/json",
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Set up IdempotentProducer for reliable writes
|
|
45
|
+
const producer = new IdempotentProducer(handle, "my-service", {
|
|
46
|
+
autoClaim: true,
|
|
47
|
+
onError: (err) => {
|
|
48
|
+
if (err instanceof StaleEpochError) {
|
|
49
|
+
console.log("Another producer took over")
|
|
50
|
+
} else {
|
|
51
|
+
console.error("Write error:", err)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Fire-and-forget writes — automatically batched and deduplicated
|
|
57
|
+
for (const event of events) {
|
|
58
|
+
producer.append(JSON.stringify(event))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure all pending writes are delivered
|
|
62
|
+
await producer.flush()
|
|
63
|
+
await producer.close()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Core Patterns
|
|
67
|
+
|
|
68
|
+
### Simple writes with append()
|
|
69
|
+
|
|
70
|
+
For writing a few items and waiting for completion, `append()` is straightforward:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { DurableStream } from "@durable-streams/client"
|
|
74
|
+
|
|
75
|
+
const handle = await DurableStream.create({
|
|
76
|
+
url: "https://your-server.com/v1/stream/events",
|
|
77
|
+
contentType: "application/json",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Each append waits for server confirmation
|
|
81
|
+
await handle.append({ type: "order.created", orderId: "123" })
|
|
82
|
+
await handle.append({ type: "order.paid", orderId: "123" })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### High-throughput writes with IdempotentProducer
|
|
86
|
+
|
|
87
|
+
For sustained writes, the producer batches, pipelines, and deduplicates automatically:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { DurableStream, IdempotentProducer } from "@durable-streams/client"
|
|
91
|
+
|
|
92
|
+
const handle = await DurableStream.create({
|
|
93
|
+
url: "https://your-server.com/v1/stream/tokens",
|
|
94
|
+
contentType: "text/plain",
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const producer = new IdempotentProducer(handle, "llm-worker", {
|
|
98
|
+
autoClaim: true,
|
|
99
|
+
lingerMs: 10, // Batch window (default: 5ms)
|
|
100
|
+
maxBatchBytes: 65536, // Max batch size (default: 1MB)
|
|
101
|
+
maxInFlight: 4, // Concurrent HTTP requests (default: 5)
|
|
102
|
+
onError: (err) => console.error(err),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
for await (const token of llm.stream(prompt)) {
|
|
106
|
+
producer.append(token) // Fire-and-forget — don't await
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await producer.flush() // Wait for all batches to land
|
|
110
|
+
await producer.close() // Clean up
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Closing a stream
|
|
114
|
+
|
|
115
|
+
Mark a stream as permanently closed (no more writes accepted):
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Close with optional final message
|
|
119
|
+
await handle.close({ body: JSON.stringify({ type: "stream.complete" }) })
|
|
120
|
+
|
|
121
|
+
// Or close without a final message
|
|
122
|
+
await handle.close()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Byte stream mode with custom framing
|
|
126
|
+
|
|
127
|
+
For non-JSON streams, use your own framing (e.g., newline-delimited JSON):
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const handle = await DurableStream.create({
|
|
131
|
+
url: "https://your-server.com/v1/stream/logs",
|
|
132
|
+
contentType: "text/plain",
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const producer = new IdempotentProducer(handle, "logger", { autoClaim: true })
|
|
136
|
+
|
|
137
|
+
producer.append(JSON.stringify({ level: "info", msg: "started" }) + "\n")
|
|
138
|
+
producer.append(JSON.stringify({ level: "error", msg: "failed" }) + "\n")
|
|
139
|
+
|
|
140
|
+
await producer.flush()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Common Mistakes
|
|
144
|
+
|
|
145
|
+
### CRITICAL Using raw append() for sustained writes
|
|
146
|
+
|
|
147
|
+
Wrong:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const handle = await DurableStream.create({
|
|
151
|
+
url,
|
|
152
|
+
contentType: "application/json",
|
|
153
|
+
})
|
|
154
|
+
for (const event of events) {
|
|
155
|
+
await handle.append(JSON.stringify(event)) // No dedup, no batching, sequential
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Correct:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const handle = await DurableStream.create({
|
|
163
|
+
url,
|
|
164
|
+
contentType: "application/json",
|
|
165
|
+
})
|
|
166
|
+
const producer = new IdempotentProducer(handle, "my-service", {
|
|
167
|
+
autoClaim: true,
|
|
168
|
+
onError: (err) => console.error(err),
|
|
169
|
+
})
|
|
170
|
+
for (const event of events) {
|
|
171
|
+
producer.append(JSON.stringify(event)) // Fire-and-forget, batched, deduplicated
|
|
172
|
+
}
|
|
173
|
+
await producer.flush()
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Raw `append()` has no deduplication — on retry after network error, data may be duplicated. `IdempotentProducer` handles batching, pipelining, and exactly-once delivery.
|
|
177
|
+
|
|
178
|
+
Source: packages/client/src/idempotent-producer.ts
|
|
179
|
+
|
|
180
|
+
### CRITICAL Not calling flush() before shutdown
|
|
181
|
+
|
|
182
|
+
Wrong:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
for (const event of events) {
|
|
186
|
+
producer.append(event)
|
|
187
|
+
}
|
|
188
|
+
// Process exits — pending batch lost!
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Correct:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
for (const event of events) {
|
|
195
|
+
producer.append(event)
|
|
196
|
+
}
|
|
197
|
+
await producer.flush()
|
|
198
|
+
await producer.close()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`IdempotentProducer` batches writes. Without `flush()`, pending messages in the buffer are lost when the process exits.
|
|
202
|
+
|
|
203
|
+
Source: packages/client/src/idempotent-producer.ts
|
|
204
|
+
|
|
205
|
+
### HIGH Awaiting each producer.append() call
|
|
206
|
+
|
|
207
|
+
Wrong:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
for (const event of events) {
|
|
211
|
+
await producer.append(JSON.stringify(event)) // Defeats pipelining!
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Correct:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
for (const event of events) {
|
|
219
|
+
producer.append(JSON.stringify(event)) // Fire-and-forget
|
|
220
|
+
}
|
|
221
|
+
await producer.flush() // Wait for all to complete
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`append()` is fire-and-forget by design. Awaiting it serializes every write and defeats batching and pipelining. Errors go to the `onError` callback.
|
|
225
|
+
|
|
226
|
+
Source: packages/client/src/idempotent-producer.ts
|
|
227
|
+
|
|
228
|
+
### HIGH Passing objects to append instead of strings
|
|
229
|
+
|
|
230
|
+
Wrong:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
producer.append({ event: "user.created" }) // throws!
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Correct:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
producer.append(JSON.stringify({ event: "user.created" }))
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`IdempotentProducer.append()` accepts only `string` or `Uint8Array` — it does **not** auto-serialize objects, even for JSON-mode streams. Always call `JSON.stringify()` before appending.
|
|
243
|
+
|
|
244
|
+
Source: packages/client/src/idempotent-producer.ts
|
|
245
|
+
|
|
246
|
+
### HIGH Not handling StaleEpochError for multi-worker scenarios
|
|
247
|
+
|
|
248
|
+
Wrong:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const producer = new IdempotentProducer(handle, "worker", {
|
|
252
|
+
onError: (err) => console.error(err), // Logs but doesn't stop
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Correct:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
let fenced = false
|
|
260
|
+
const producer = new IdempotentProducer(handle, "worker", {
|
|
261
|
+
onError: (err) => {
|
|
262
|
+
if (err instanceof StaleEpochError) {
|
|
263
|
+
fenced = true // Stop producing
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
for await (const token of source) {
|
|
269
|
+
if (fenced) break
|
|
270
|
+
producer.append(token)
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
When another producer claims a higher epoch, the current producer gets fenced. Without checking, your write loop continues appending to a dead producer.
|
|
275
|
+
|
|
276
|
+
Source: packages/client/src/idempotent-producer.ts
|
|
277
|
+
|
|
278
|
+
### HIGH Manually managing epoch numbers
|
|
279
|
+
|
|
280
|
+
Wrong:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const producer = new IdempotentProducer(handle, "worker", { epoch: 2 })
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Correct:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const producer = new IdempotentProducer(handle, "worker", {
|
|
290
|
+
autoClaim: true, // Handles epoch transitions automatically
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Epoch and sequence management is an internal protocol detail. Use `autoClaim: true` and let the producer handle transitions automatically.
|
|
295
|
+
|
|
296
|
+
Source: maintainer guidance
|
|
297
|
+
|
|
298
|
+
### HIGH Tension: Simplicity vs. reliability in writes
|
|
299
|
+
|
|
300
|
+
This skill's patterns conflict with getting-started. Getting-started simplicity favors `append()` for quick demos, but production code should use `IdempotentProducer` for exactly-once guarantees. Agents optimizing for a quick setup may use `append()` and miss deduplication and batching.
|
|
301
|
+
|
|
302
|
+
See also: durable-streams/getting-started/SKILL.md § Common Mistakes
|
|
303
|
+
|
|
304
|
+
## See also
|
|
305
|
+
|
|
306
|
+
- [getting-started](../getting-started/SKILL.md) — Basic stream creation and reading
|
|
307
|
+
- [go-to-production](../go-to-production/SKILL.md) — TTL configuration and production concerns
|
|
308
|
+
|
|
309
|
+
## Version
|
|
310
|
+
|
|
311
|
+
Targets @durable-streams/client v0.2.1.
|