@durable-streams/client 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.
@@ -0,0 +1,247 @@
1
+ ---
2
+ name: reading-streams
3
+ description: >
4
+ All stream reading patterns for @durable-streams/client. stream() function,
5
+ DurableStream.stream(), LiveMode (false, true, "long-poll", "sse"),
6
+ StreamResponse state machine, .json(), .text(), .jsonStream(), .textStream(),
7
+ .subscribeJson(), .subscribeBytes(), .subscribeText(), SSE resilience with
8
+ auto-fallback to long-poll, visibility-based pause, binary SSE base64
9
+ auto-decode, dynamic headers for auth token refresh, backoff config,
10
+ StreamErrorHandler onError for error recovery.
11
+ type: core
12
+ library: durable-streams
13
+ library_version: "0.2.1"
14
+ requires:
15
+ - getting-started
16
+ sources:
17
+ - "durable-streams/durable-streams:packages/client/src/stream-api.ts"
18
+ - "durable-streams/durable-streams:packages/client/src/response.ts"
19
+ - "durable-streams/durable-streams:packages/client/src/types.ts"
20
+ - "durable-streams/durable-streams:packages/client/src/fetch.ts"
21
+ ---
22
+
23
+ This skill builds on durable-streams/getting-started. Read it first for setup and offset basics.
24
+
25
+ # Durable Streams — Reading Streams
26
+
27
+ Use `stream()` for read-only access (fetch-like API). Use `DurableStream.stream()`
28
+ when you already have a `DurableStream` handle for read/write operations. Both
29
+ return a `StreamResponse` with identical consumption methods.
30
+
31
+ ## Setup
32
+
33
+ ```typescript
34
+ import { stream } from "@durable-streams/client"
35
+
36
+ // Catch-up read (returns all existing data, then stops)
37
+ const res = await stream<{ event: string; userId: string }>({
38
+ url: "https://your-server.com/v1/stream/my-stream",
39
+ offset: "-1",
40
+ live: false,
41
+ })
42
+ const items = await res.json()
43
+ ```
44
+
45
+ ## Core Patterns
46
+
47
+ ### Live modes
48
+
49
+ ```typescript
50
+ import { stream } from "@durable-streams/client"
51
+
52
+ // Catch-up only — stop at end of existing data
53
+ const catchUp = await stream({ url, offset: "-1", live: false })
54
+
55
+ // Auto-select best transport (SSE for JSON, long-poll for binary)
56
+ const auto = await stream({ url, offset: "-1", live: true })
57
+
58
+ // Explicit long-poll
59
+ const longPoll = await stream({ url, offset: "-1", live: "long-poll" })
60
+
61
+ // Explicit SSE
62
+ const sse = await stream({ url, offset: "-1", live: "sse" })
63
+ ```
64
+
65
+ ### Dynamic headers for auth token refresh
66
+
67
+ Header functions are called **per-request**, allowing token refresh during long-lived live streams:
68
+
69
+ ```typescript
70
+ import { stream } from "@durable-streams/client"
71
+
72
+ const res = await stream({
73
+ url: "https://your-server.com/v1/stream/my-stream",
74
+ offset: "-1",
75
+ live: true,
76
+ headers: {
77
+ Authorization: async () => `Bearer ${await getAccessToken()}`,
78
+ },
79
+ })
80
+ ```
81
+
82
+ ### Error recovery with onError
83
+
84
+ ```typescript
85
+ import { stream } from "@durable-streams/client"
86
+
87
+ const res = await stream({
88
+ url: "https://your-server.com/v1/stream/my-stream",
89
+ offset: "-1",
90
+ live: true,
91
+ onError: (error) => {
92
+ if (error.status === 401) {
93
+ // Refresh auth and retry with new headers
94
+ return { headers: { Authorization: `Bearer ${newToken}` } }
95
+ }
96
+ if (error.status === 404) {
97
+ return // Stop retrying (void = propagate error)
98
+ }
99
+ return {} // Retry with same params
100
+ },
101
+ })
102
+ ```
103
+
104
+ ### SSE resilience with auto-fallback
105
+
106
+ ```typescript
107
+ import { stream } from "@durable-streams/client"
108
+
109
+ const res = await stream({
110
+ url: "https://your-server.com/v1/stream/my-stream",
111
+ offset: "-1",
112
+ live: "sse",
113
+ sseResilience: {
114
+ minConnectionDuration: 1000, // Connections under 1s are "short"
115
+ maxShortConnections: 3, // Fall back after 3 short connections
116
+ },
117
+ })
118
+ ```
119
+
120
+ ## Common Mistakes
121
+
122
+ ### CRITICAL Not saving offset for resumption
123
+
124
+ Wrong:
125
+
126
+ ```typescript
127
+ res.subscribeJson((batch) => {
128
+ processItems(batch.items)
129
+ // offset not saved!
130
+ })
131
+ ```
132
+
133
+ Correct:
134
+
135
+ ```typescript
136
+ res.subscribeJson((batch) => {
137
+ processItems(batch.items)
138
+ saveCheckpoint(batch.offset)
139
+ })
140
+ ```
141
+
142
+ The whole point of durable streams is resumability. Without persisting the offset, you lose the ability to resume after disconnect.
143
+
144
+ Source: README.md resume from offset section
145
+
146
+ ### HIGH Using .json() on non-JSON content type streams
147
+
148
+ Wrong:
149
+
150
+ ```typescript
151
+ // Stream created with contentType: "text/plain"
152
+ const res = await stream({ url, offset: "-1", live: false })
153
+ const data = await res.json() // throws DurableStreamError!
154
+ ```
155
+
156
+ Correct:
157
+
158
+ ```typescript
159
+ const res = await stream({ url, offset: "-1", live: false })
160
+ const text = await res.text()
161
+ ```
162
+
163
+ `.json()`, `.jsonStream()`, and `.subscribeJson()` only work on JSON-mode streams (`contentType: "application/json"`). Use `.text()` or `.body()` for other content types.
164
+
165
+ Source: packages/client/src/response.ts
166
+
167
+ ### HIGH Ignoring onError handler for live streams
168
+
169
+ Wrong:
170
+
171
+ ```typescript
172
+ const res = await stream({ url, offset: "-1", live: true })
173
+ // No onError — auth failures retry forever with exponential backoff
174
+ ```
175
+
176
+ Correct:
177
+
178
+ ```typescript
179
+ const res = await stream({
180
+ url,
181
+ offset: "-1",
182
+ live: true,
183
+ onError: (error) => {
184
+ if (error.status === 401) return // Stop retrying
185
+ return {} // Retry for transient errors
186
+ },
187
+ })
188
+ ```
189
+
190
+ Without `onError`, permanent errors (401, 403) silently retry forever with exponential backoff.
191
+
192
+ Source: packages/client/src/types.ts StreamErrorHandler
193
+
194
+ ### HIGH Returning void from onError to retry
195
+
196
+ Wrong:
197
+
198
+ ```typescript
199
+ onError: (error) => {
200
+ console.log("retrying...")
201
+ // Returns undefined — error propagates instead of retrying!
202
+ }
203
+ ```
204
+
205
+ Correct:
206
+
207
+ ```typescript
208
+ onError: (error) => {
209
+ console.log("retrying...")
210
+ return {} // Return an object to signal retry
211
+ }
212
+ ```
213
+
214
+ The `onError` handler must return an object (`{}` or `{ headers, params }`) to signal retry. Returning `void`/`undefined` propagates the error.
215
+
216
+ Source: packages/client/src/types.ts RetryOpts
217
+
218
+ ### MEDIUM Using HTTP instead of HTTPS in browser
219
+
220
+ Wrong:
221
+
222
+ ```typescript
223
+ const res = await stream({ url: "http://api.example.com/v1/stream/my-stream" })
224
+ ```
225
+
226
+ Correct:
227
+
228
+ ```typescript
229
+ const res = await stream({ url: "https://api.example.com/v1/stream/my-stream" })
230
+ ```
231
+
232
+ HTTP/1.1 in browsers limits to ~6 concurrent connections per origin. With multiple live streams, this can freeze the app.
233
+
234
+ Source: packages/client/src/utils.ts warnIfUsingHttpInBrowser
235
+
236
+ ## References
237
+
238
+ - [StreamResponse consumption methods](references/stream-response-methods.md)
239
+
240
+ ## See also
241
+
242
+ - [getting-started](../getting-started/SKILL.md) — Basic setup and offset concepts
243
+ - [stream-db](../../../state/skills/stream-db/SKILL.md) — StreamDB uses stream reading internally
244
+
245
+ ## Version
246
+
247
+ Targets @durable-streams/client v0.2.1.
@@ -0,0 +1,133 @@
1
+ # StreamResponse Consumption Methods
2
+
3
+ A `StreamResponse<TJson>` provides three families of consumption methods.
4
+ Choose one per response — calling a second method throws `ALREADY_CONSUMED`.
5
+
6
+ ## Promise-Based (await for complete result)
7
+
8
+ Use for catch-up reads (`live: false`) or when you want all data at once.
9
+
10
+ ### `.json(): Promise<TJson[]>`
11
+
12
+ Returns all items as a parsed JSON array. JSON-mode streams only.
13
+
14
+ ```typescript
15
+ const res = await stream<{ id: string }>({ url, offset: "-1", live: false })
16
+ const items = await res.json()
17
+ // items: Array<{ id: string }>
18
+ ```
19
+
20
+ ### `.text(): Promise<string>`
21
+
22
+ Returns the complete body as a string. Works with any content type.
23
+
24
+ ```typescript
25
+ const res = await stream({ url, offset: "-1", live: false })
26
+ const text = await res.text()
27
+ ```
28
+
29
+ ### `.body(): Promise<Uint8Array>`
30
+
31
+ Returns the complete body as raw bytes.
32
+
33
+ ```typescript
34
+ const res = await stream({ url, offset: "-1", live: false })
35
+ const bytes = await res.body()
36
+ ```
37
+
38
+ ## ReadableStream-Based (async iteration)
39
+
40
+ Use for processing items one at a time as they arrive.
41
+
42
+ ### `.jsonStream(): ReadableStream<TJson>`
43
+
44
+ Yields individual JSON items. JSON-mode streams only.
45
+
46
+ ```typescript
47
+ const res = await stream<{ event: string }>({ url, offset: "-1", live: true })
48
+ for await (const item of res.jsonStream()) {
49
+ console.log(item.event)
50
+ }
51
+ ```
52
+
53
+ ### `.textStream(): ReadableStream<string>`
54
+
55
+ Yields text chunks as they arrive.
56
+
57
+ ```typescript
58
+ const res = await stream({ url, offset: "-1", live: true })
59
+ for await (const chunk of res.textStream()) {
60
+ process.stdout.write(chunk)
61
+ }
62
+ ```
63
+
64
+ ### `.bodyStream(): ReadableStream<Uint8Array>`
65
+
66
+ Yields raw byte chunks.
67
+
68
+ ```typescript
69
+ const res = await stream({ url, offset: "-1", live: true })
70
+ for await (const chunk of res.bodyStream()) {
71
+ processBytes(chunk)
72
+ }
73
+ ```
74
+
75
+ ## Subscriber-Based (callback with offset tracking)
76
+
77
+ Use for live subscriptions where you need the offset for checkpointing.
78
+
79
+ ### `.subscribeJson(callback): () => void`
80
+
81
+ Calls back with batches of JSON items and offset.
82
+
83
+ ```typescript
84
+ const res = await stream<{ event: string }>({ url, offset: "-1", live: true })
85
+ res.subscribeJson(async (batch) => {
86
+ for (const item of batch.items) {
87
+ console.log(item.event)
88
+ }
89
+ saveCheckpoint(batch.offset)
90
+ })
91
+ ```
92
+
93
+ ### `.subscribeText(callback): () => void`
94
+
95
+ Calls back with text data and offset.
96
+
97
+ ```typescript
98
+ const res = await stream({ url, offset: "-1", live: true })
99
+ res.subscribeText(async (chunk) => {
100
+ appendToUI(chunk.text)
101
+ saveCheckpoint(chunk.offset)
102
+ })
103
+ ```
104
+
105
+ ### `.subscribeBytes(callback): () => void`
106
+
107
+ Calls back with byte data and offset.
108
+
109
+ ```typescript
110
+ const res = await stream({ url, offset: "-1", live: true })
111
+ res.subscribeBytes(async (chunk) => {
112
+ processBytes(chunk.data)
113
+ saveCheckpoint(chunk.offset)
114
+ })
115
+ ```
116
+
117
+ ## Properties
118
+
119
+ | Property | Type | Description |
120
+ | -------------- | --------------------- | ----------------------------------------- |
121
+ | `offset` | `string` | Current offset (updates after each chunk) |
122
+ | `contentType` | `string \| undefined` | Content type from stream creation |
123
+ | `live` | `LiveMode` | The live mode for this session |
124
+ | `startOffset` | `string` | The offset this session started from |
125
+ | `upToDate` | `boolean` | Whether caught up with all existing data |
126
+ | `streamClosed` | `boolean` | Whether the stream is permanently closed |
127
+ | `closed` | `Promise<void>` | Resolves when the stream session ends |
128
+
129
+ ## Methods
130
+
131
+ | Method | Description |
132
+ | ---------- | ------------------------- |
133
+ | `cancel()` | Cancel the stream session |
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: server-deployment
3
+ description: >
4
+ Running durable stream servers. DurableStreamTestServer for development
5
+ (Node.js, @durable-streams/server, not for production), Caddy plugin for
6
+ production with Caddyfile configuration, data_dir for file-backed persistence,
7
+ max_file_handles tuning, long_poll_timeout, server binary downloads for macOS
8
+ Linux Windows, @durable-streams/cli tool setup, conformance test runner.
9
+ type: lifecycle
10
+ library: durable-streams
11
+ library_version: "0.2.1"
12
+ sources:
13
+ - "durable-streams/durable-streams:packages/caddy-plugin/README.md"
14
+ - "durable-streams/durable-streams:packages/server/README.md"
15
+ - "durable-streams/durable-streams:packages/cli/README.md"
16
+ ---
17
+
18
+ # Durable Streams — Server Deployment
19
+
20
+ Two server options: a Node.js development server for prototyping and a
21
+ Caddy-based production server with file persistence and CDN support.
22
+
23
+ ## Setup
24
+
25
+ ### Development server (Node.js)
26
+
27
+ ```typescript
28
+ import { DurableStreamTestServer } from "@durable-streams/server"
29
+
30
+ const server = new DurableStreamTestServer({
31
+ port: 4437,
32
+ host: "127.0.0.1",
33
+ })
34
+
35
+ await server.start()
36
+ console.log(`Dev server running on ${server.url}`)
37
+ ```
38
+
39
+ ### Production server (Caddy binary)
40
+
41
+ Download the binary from [GitHub releases](https://github.com/durable-streams/durable-streams/releases) for your platform.
42
+
43
+ ```bash
44
+ # Start the server
45
+ ./durable-streams-server run --config Caddyfile
46
+ ```
47
+
48
+ Caddyfile configuration:
49
+
50
+ ```
51
+ :8787 {
52
+ route /v1/stream/* {
53
+ durable_streams {
54
+ data_dir ./data
55
+ max_file_handles 200
56
+ long_poll_timeout 60s
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Core Patterns
63
+
64
+ ### CLI for testing
65
+
66
+ ```bash
67
+ # Install CLI
68
+ npm install -g @durable-streams/cli
69
+
70
+ # Set server URL
71
+ export STREAM_URL=http://localhost:4437
72
+
73
+ # Create, write, read
74
+ durable-stream create my-stream
75
+ durable-stream write my-stream "Hello, world!"
76
+ durable-stream read my-stream
77
+ ```
78
+
79
+ ### Running conformance tests
80
+
81
+ ```bash
82
+ # Against your server
83
+ npx @durable-streams/server-conformance-tests --run http://localhost:4437
84
+
85
+ # Watch mode for development
86
+ npx @durable-streams/server-conformance-tests --watch src http://localhost:4437
87
+ ```
88
+
89
+ ### Programmatic dev server with stream creation
90
+
91
+ ```typescript
92
+ import { DurableStreamTestServer } from "@durable-streams/server"
93
+ import { DurableStream } from "@durable-streams/client"
94
+
95
+ const server = new DurableStreamTestServer({ port: 0 }) // Random port
96
+ await server.start()
97
+
98
+ // Streams must be created before clients can connect
99
+ await DurableStream.create({
100
+ url: `${server.url}/v1/stream/my-app`,
101
+ contentType: "application/json",
102
+ })
103
+
104
+ console.log(`Dev server running on ${server.url}`)
105
+ ```
106
+
107
+ ### Programmatic dev server in tests
108
+
109
+ ```typescript
110
+ import { DurableStreamTestServer } from "@durable-streams/server"
111
+
112
+ const server = new DurableStreamTestServer({ port: 0 }) // Random port
113
+ await server.start()
114
+
115
+ // Run your tests against server.url
116
+
117
+ await server.stop()
118
+ ```
119
+
120
+ ### Caddy configuration options
121
+
122
+ | Option | Default | Description |
123
+ | ------------------- | ------------------ | -------------------------------------- |
124
+ | `data_dir` | (none — in-memory) | Directory for file-backed persistence |
125
+ | `max_file_handles` | 100 | Max concurrent open file handles |
126
+ | `long_poll_timeout` | 60s | How long to hold long-poll connections |
127
+
128
+ ## Common Mistakes
129
+
130
+ ### CRITICAL Using the Node.js dev server in production
131
+
132
+ Wrong:
133
+
134
+ ```typescript
135
+ // production deployment
136
+ import { DurableStreamTestServer } from "@durable-streams/server"
137
+ const server = new DurableStreamTestServer({ port: 4437 })
138
+ ```
139
+
140
+ Correct:
141
+
142
+ ```bash
143
+ # Use the Caddy plugin binary
144
+ ./durable-streams-server run --config Caddyfile
145
+ ```
146
+
147
+ `DurableStreamTestServer` is explicitly not for production. It uses in-memory storage, has no CDN integration, and is single-process only.
148
+
149
+ Source: packages/server/README.md
150
+
151
+ ### CRITICAL Not configuring data_dir for persistence
152
+
153
+ Wrong:
154
+
155
+ ```
156
+ :8787 {
157
+ route /v1/stream/* {
158
+ durable_streams
159
+ }
160
+ }
161
+ ```
162
+
163
+ Correct:
164
+
165
+ ```
166
+ :8787 {
167
+ route /v1/stream/* {
168
+ durable_streams {
169
+ data_dir ./data
170
+ max_file_handles 200
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ Without `data_dir`, the Caddy plugin uses in-memory storage. Server restarts lose all data.
177
+
178
+ Source: packages/caddy-plugin/README.md
179
+
180
+ ### MEDIUM Setting max_file_handles too low for production
181
+
182
+ Wrong:
183
+
184
+ ```
185
+ durable_streams {
186
+ data_dir ./data
187
+ # default 100 handles — fine for dev, low for production
188
+ }
189
+ ```
190
+
191
+ Correct:
192
+
193
+ ```
194
+ durable_streams {
195
+ data_dir ./data
196
+ max_file_handles 500 # Tune based on active stream count
197
+ }
198
+ ```
199
+
200
+ Default is 100 file handles. High-throughput deployments with many concurrent streams can exhaust the pool, causing latency spikes.
201
+
202
+ Source: packages/caddy-plugin/store/filepool.go
203
+
204
+ ## See also
205
+
206
+ - [getting-started](../getting-started/SKILL.md) — Connect a client to your server
207
+ - [go-to-production](../go-to-production/SKILL.md) — CDN caching, TTL, and HTTPS for production
208
+
209
+ ## Version
210
+
211
+ Targets durable-streams-server v0.2.1.