@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.
@@ -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.