@durable-streams/client 0.2.0 → 0.2.1
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 +201 -8
- package/dist/index.cjs +331 -38
- package/dist/index.d.cts +139 -9
- package/dist/index.d.ts +139 -9
- package/dist/index.js +329 -39
- package/package.json +2 -2
- package/src/constants.ts +19 -2
- package/src/error.ts +20 -0
- package/src/idempotent-producer.ts +144 -5
- package/src/index.ts +7 -0
- package/src/response.ts +176 -17
- package/src/sse.ts +10 -1
- package/src/stream-api.ts +13 -0
- package/src/stream.ts +147 -26
- package/src/types.ts +73 -0
- package/src/utils.ts +10 -1
package/README.md
CHANGED
|
@@ -232,6 +232,7 @@ class DurableStream {
|
|
|
232
232
|
head(opts?: { signal?: AbortSignal }): Promise<HeadResult>
|
|
233
233
|
create(opts?: CreateOptions): Promise<this>
|
|
234
234
|
delete(opts?: { signal?: AbortSignal }): Promise<void>
|
|
235
|
+
close(opts?: CloseOptions): Promise<CloseResult> // Close stream (EOF)
|
|
235
236
|
append(
|
|
236
237
|
body: BodyInit | Uint8Array | string,
|
|
237
238
|
opts?: AppendOptions
|
|
@@ -264,6 +265,28 @@ const res = await stream({ url, live: "long-poll" })
|
|
|
264
265
|
const res = await stream({ url, live: "sse" })
|
|
265
266
|
```
|
|
266
267
|
|
|
268
|
+
### Binary Streams with SSE
|
|
269
|
+
|
|
270
|
+
For binary content types (e.g., `application/octet-stream`), SSE mode requires the `encoding` option:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
const stream = await DurableStream.create({
|
|
274
|
+
url: "https://streams.example.com/my-binary-stream",
|
|
275
|
+
contentType: "application/octet-stream",
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const response = await stream.read({
|
|
279
|
+
live: "sse",
|
|
280
|
+
encoding: "base64",
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
response.subscribe((chunk) => {
|
|
284
|
+
console.log(chunk.data) // Uint8Array - automatically decoded from base64
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The client automatically decodes base64 data events before returning them. This is required for any content type other than `text/*` or `application/json` when using SSE mode.
|
|
289
|
+
|
|
267
290
|
### Headers and Params
|
|
268
291
|
|
|
269
292
|
Headers and params support both static values and functions (sync or async) for dynamic values like authentication tokens.
|
|
@@ -610,6 +633,7 @@ res.startOffset // The starting offset passed to stream()
|
|
|
610
633
|
res.offset // Current offset (updates as data is consumed)
|
|
611
634
|
res.cursor // Cursor for collapsing (if provided by server)
|
|
612
635
|
res.upToDate // Whether we've caught up to the stream head
|
|
636
|
+
res.streamClosed // Whether the stream is permanently closed (EOF)
|
|
613
637
|
```
|
|
614
638
|
|
|
615
639
|
---
|
|
@@ -897,12 +921,35 @@ Send any pending batch immediately and wait for all in-flight batches to complet
|
|
|
897
921
|
await producer.flush()
|
|
898
922
|
```
|
|
899
923
|
|
|
900
|
-
#### `close(): Promise<
|
|
924
|
+
#### `close(finalMessage?): Promise<CloseResult>`
|
|
925
|
+
|
|
926
|
+
Flush pending messages and close the underlying **stream** (EOF). This is the typical way to end a producer session:
|
|
901
927
|
|
|
902
|
-
|
|
928
|
+
1. Flushes all pending messages
|
|
929
|
+
2. Optionally appends a final message atomically with close
|
|
930
|
+
3. Closes the stream (no further appends permitted by any producer)
|
|
931
|
+
|
|
932
|
+
**Idempotent**: Safe to retry on network failures - uses producer headers for deduplication.
|
|
903
933
|
|
|
904
934
|
```typescript
|
|
905
|
-
|
|
935
|
+
// Close stream (EOF)
|
|
936
|
+
const result = await producer.close()
|
|
937
|
+
console.log("Final offset:", result.finalOffset)
|
|
938
|
+
|
|
939
|
+
// Close with final message (atomic append + close)
|
|
940
|
+
const result = await producer.close('{"done": true}')
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
#### `detach(): Promise<void>`
|
|
944
|
+
|
|
945
|
+
Stop the producer without closing the underlying stream. Use this when:
|
|
946
|
+
|
|
947
|
+
- Handing off writing to another producer
|
|
948
|
+
- Keeping the stream open for future writes
|
|
949
|
+
- Stopping this producer but not signaling EOF to readers
|
|
950
|
+
|
|
951
|
+
```typescript
|
|
952
|
+
await producer.detach() // Stream remains open
|
|
906
953
|
```
|
|
907
954
|
|
|
908
955
|
#### `restart(): Promise<void>`
|
|
@@ -949,19 +996,165 @@ await producer.flush() // Wait for all batches to complete
|
|
|
949
996
|
|
|
950
997
|
---
|
|
951
998
|
|
|
999
|
+
## Stream Closure (EOF)
|
|
1000
|
+
|
|
1001
|
+
Durable Streams supports permanently closing streams to signal EOF (End of File). Once closed, no further appends are permitted, but data remains fully readable.
|
|
1002
|
+
|
|
1003
|
+
### Writer Side
|
|
1004
|
+
|
|
1005
|
+
#### Using DurableStream.close()
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
const stream = await DurableStream.connect({ url })
|
|
1009
|
+
|
|
1010
|
+
// Simple close (no final message)
|
|
1011
|
+
const result = await stream.close()
|
|
1012
|
+
console.log("Final offset:", result.finalOffset)
|
|
1013
|
+
|
|
1014
|
+
// Atomic append-and-close with final message
|
|
1015
|
+
const result = await stream.close({
|
|
1016
|
+
body: '{"status": "complete"}',
|
|
1017
|
+
})
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Options:**
|
|
1021
|
+
|
|
1022
|
+
```typescript
|
|
1023
|
+
interface CloseOptions {
|
|
1024
|
+
body?: Uint8Array | string // Optional final message
|
|
1025
|
+
contentType?: string // Content type (must match stream)
|
|
1026
|
+
signal?: AbortSignal // Cancellation
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
interface CloseResult {
|
|
1030
|
+
finalOffset: Offset // The offset after the last byte
|
|
1031
|
+
}
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
**Idempotency:**
|
|
1035
|
+
|
|
1036
|
+
- `close()` without body: Idempotent — safe to call multiple times
|
|
1037
|
+
- `close({ body })` with body: NOT idempotent — throws `StreamClosedError` if already closed. Use `IdempotentProducer.close(finalMessage)` for idempotent close-with-body.
|
|
1038
|
+
|
|
1039
|
+
#### Using IdempotentProducer.close()
|
|
1040
|
+
|
|
1041
|
+
For reliable close with final message (safe to retry):
|
|
1042
|
+
|
|
1043
|
+
```typescript
|
|
1044
|
+
const producer = new IdempotentProducer(stream, "producer-1", {
|
|
1045
|
+
autoClaim: true,
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
// Write some messages
|
|
1049
|
+
producer.append('{"event": "start"}')
|
|
1050
|
+
producer.append('{"event": "data"}')
|
|
1051
|
+
|
|
1052
|
+
// Close with final message (idempotent, safe to retry)
|
|
1053
|
+
const result = await producer.close('{"event": "end"}')
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
**Important:** `IdempotentProducer.close()` closes the **stream**, not just the producer. Use `detach()` to stop the producer without closing the stream.
|
|
1057
|
+
|
|
1058
|
+
#### Creating Closed Streams
|
|
1059
|
+
|
|
1060
|
+
Create a stream that's immediately closed (useful for cached responses, errors, single-shot data):
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
// Empty closed stream
|
|
1064
|
+
const stream = await DurableStream.create({
|
|
1065
|
+
url: "https://streams.example.com/cached-response",
|
|
1066
|
+
contentType: "application/json",
|
|
1067
|
+
closed: true,
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
// Closed stream with initial content
|
|
1071
|
+
const stream = await DurableStream.create({
|
|
1072
|
+
url: "https://streams.example.com/error-response",
|
|
1073
|
+
contentType: "application/json",
|
|
1074
|
+
body: '{"error": "Service unavailable"}',
|
|
1075
|
+
closed: true,
|
|
1076
|
+
})
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Reader Side
|
|
1080
|
+
|
|
1081
|
+
#### Detecting Closure
|
|
1082
|
+
|
|
1083
|
+
The `streamClosed` property indicates when a stream is permanently closed:
|
|
1084
|
+
|
|
1085
|
+
```typescript
|
|
1086
|
+
// StreamResponse properties
|
|
1087
|
+
const res = await stream({ url, live: true })
|
|
1088
|
+
console.log(res.streamClosed) // false initially
|
|
1089
|
+
|
|
1090
|
+
// In subscribers - batch/chunk metadata includes streamClosed
|
|
1091
|
+
res.subscribeJson((batch) => {
|
|
1092
|
+
console.log("Items:", batch.items)
|
|
1093
|
+
console.log("Stream closed:", batch.streamClosed) // true when EOF reached
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
// In HEAD requests
|
|
1097
|
+
const metadata = await stream.head()
|
|
1098
|
+
console.log("Stream closed:", metadata.streamClosed)
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
#### Live Mode Behavior
|
|
1102
|
+
|
|
1103
|
+
When a stream is closed:
|
|
1104
|
+
|
|
1105
|
+
- **Long-poll**: Returns immediately with `streamClosed: true` (no waiting)
|
|
1106
|
+
- **SSE**: Sends `streamClosed: true` in final control event, then closes connection
|
|
1107
|
+
- **Subscribers**: Receive final batch with `streamClosed: true`, then stop
|
|
1108
|
+
|
|
1109
|
+
```typescript
|
|
1110
|
+
const res = await stream({ url, live: true })
|
|
1111
|
+
|
|
1112
|
+
res.subscribeJson((batch) => {
|
|
1113
|
+
for (const item of batch.items) {
|
|
1114
|
+
process(item)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (batch.streamClosed) {
|
|
1118
|
+
console.log("Stream complete, no more data will arrive")
|
|
1119
|
+
// Connection will close automatically
|
|
1120
|
+
}
|
|
1121
|
+
})
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### Error Handling
|
|
1125
|
+
|
|
1126
|
+
Attempting to append to a closed stream throws `StreamClosedError`:
|
|
1127
|
+
|
|
1128
|
+
```typescript
|
|
1129
|
+
import { StreamClosedError } from "@durable-streams/client"
|
|
1130
|
+
|
|
1131
|
+
try {
|
|
1132
|
+
await stream.append("data")
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
if (error instanceof StreamClosedError) {
|
|
1135
|
+
console.log("Stream is closed at offset:", error.finalOffset)
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
---
|
|
1141
|
+
|
|
952
1142
|
## Types
|
|
953
1143
|
|
|
954
1144
|
Key types exported from the package:
|
|
955
1145
|
|
|
956
1146
|
- `Offset` - Opaque string for stream position
|
|
957
|
-
- `StreamResponse` - Response object from stream()
|
|
958
|
-
- `ByteChunk` - `{ data: Uint8Array, offset: Offset, upToDate: boolean, cursor?: string }`
|
|
959
|
-
- `JsonBatch<T>` - `{ items: T[], offset: Offset, upToDate: boolean, cursor?: string }`
|
|
960
|
-
- `TextChunk` - `{ text: string, offset: Offset, upToDate: boolean, cursor?: string }`
|
|
961
|
-
- `HeadResult` - Metadata from HEAD requests
|
|
1147
|
+
- `StreamResponse` - Response object from stream() (includes `streamClosed` property)
|
|
1148
|
+
- `ByteChunk` - `{ data: Uint8Array, offset: Offset, upToDate: boolean, streamClosed: boolean, cursor?: string }`
|
|
1149
|
+
- `JsonBatch<T>` - `{ items: T[], offset: Offset, upToDate: boolean, streamClosed: boolean, cursor?: string }`
|
|
1150
|
+
- `TextChunk` - `{ text: string, offset: Offset, upToDate: boolean, streamClosed: boolean, cursor?: string }`
|
|
1151
|
+
- `HeadResult` - Metadata from HEAD requests (includes `streamClosed` property)
|
|
1152
|
+
- `CloseOptions` - Options for closing a stream
|
|
1153
|
+
- `CloseResult` - Result from closing a stream (includes `finalOffset`)
|
|
962
1154
|
- `IdempotentProducer` - Exactly-once producer class
|
|
963
1155
|
- `StaleEpochError` - Thrown when producer epoch is stale (zombie fencing)
|
|
964
1156
|
- `SequenceGapError` - Thrown when sequence numbers are out of order
|
|
1157
|
+
- `StreamClosedError` - Thrown when attempting to append to a closed stream (includes `finalOffset`)
|
|
965
1158
|
- `DurableStreamError` - Protocol-level errors with codes
|
|
966
1159
|
- `FetchError` - Transport/network errors
|
|
967
1160
|
|