| name | forking |
| description | Creating and using forked streams. Fork a source stream at a specific offset using Stream-Forked-From and Stream-Fork-Offset headers via DurableStream.create(). Reads transparently stitch inherited and fork data. Covers fork creation, fresh handle pattern, TTL/expiry inheritance, content-type inheritance, and deletion lifecycle. Load when forking, branching, or creating a stream variant from an existing stream.
|
| type | core |
| library | durable-streams |
| library_version | 0.2.1 |
| requires | ["getting-started"] |
| sources | ["durable-streams/durable-streams:packages/client/src/stream.ts","durable-streams/durable-streams:PROTOCOL.md"] |
This skill builds on durable-streams/getting-started. Read it first for setup and offset basics.
Durable Streams — Forking
Fork creates a new stream that references the data of a source stream up to a
specified offset, without copying it. The fork is independent: it has its own
URL, TTL, closure state, and deletion lifecycle.
Setup
import { DurableStream } from "@durable-streams/client"
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
"Stream-Fork-Offset": "1024",
},
})
const fork = await DurableStream.connect({
url: "https://your-server.com/v1/stream/my-fork",
})
const res = await fork.stream({ json: true })
const items = await res.json()
await fork.append(JSON.stringify({ role: "user", text: "what if instead..." }))
Core Patterns
Create a fork at the source's current tail
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
},
})
Create a fork at a specific offset
Use a server-returned offset from a previous HEAD, GET, or POST response:
const source = await DurableStream.connect({
url: "https://your-server.com/v1/stream/my-source",
})
const head = await source.head()
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
"Stream-Fork-Offset": savedOffset,
},
})
Read a fork
Reading a fork is identical to reading any stream. The fork transparently
stitches inherited data from the source with the fork's own appends:
import { stream } from "@durable-streams/client"
const res = await stream({
url: "https://your-server.com/v1/stream/my-fork",
offset: "-1",
live: true,
})
res.subscribeJson(async (batch) => {
for (const item of batch.items) {
console.log(item)
}
})
Write to a fork
Appends work the same as any stream. Data goes only to the fork:
const fork = await DurableStream.connect({
url: "https://your-server.com/v1/stream/my-fork",
})
await fork.append(JSON.stringify({ event: "branched" }))
Delete a fork
await DurableStream.delete({
url: "https://your-server.com/v1/stream/my-fork",
})
Deleting a fork decrements the source's reference count. If the source was
soft-deleted and this was its last fork, the source is cleaned up too.
TTL and expiry
A fork has its own TTL and expiry. If the fork request provides Stream-TTL
or Stream-Expires-At, the fork uses those values. If omitted, the fork
inherits from the source: a source with a TTL passes its value on (the fork
runs its own sliding window), a source with Expires-At passes its hard
deadline on.
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
ttlSeconds: 3600,
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
},
})
Common Mistakes
CRITICAL Reusing the create handle for reads and writes
Wrong:
const fork = await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
"Stream-Fork-Offset": "1024",
},
})
await fork.append(JSON.stringify({ event: "data" }))
Correct:
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
"Stream-Fork-Offset": "1024",
},
})
const fork = await DurableStream.connect({
url: "https://your-server.com/v1/stream/my-fork",
})
await fork.append(JSON.stringify({ event: "data" }))
options.headers applies to every request on a handle. The fork headers are only meaningful on the initial PUT. Servers ignore them on reads and appends, but using a fresh handle keeps requests clean.
Source: packages/client/src/stream.ts
CRITICAL Fabricating offset values for Stream-Fork-Offset
Wrong:
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
"Stream-Fork-Offset": "100",
},
})
Correct:
const source = await DurableStream.connect({
url: "https://your-server.com/v1/stream/my-source",
})
const head = await source.head()
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
"Stream-Fork-Offset": head.offset!,
},
})
Offsets are opaque tokens. Fabricated values may return 400 Bad Request. Always use an offset from a previous HEAD, GET, or POST response.
Source: PROTOCOL.md section 6 (Offsets), section 4.2 (Stream forking)
HIGH Mismatched Content-Type on fork creation
Wrong:
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
contentType: "text/plain",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
},
})
Correct:
await DurableStream.create({
url: "https://your-server.com/v1/stream/my-fork",
headers: {
"Stream-Forked-From": "/v1/stream/my-source",
},
})
When forking, omit Content-Type to inherit it from the source. If provided, it must match the source's content type exactly or the server returns 409 Conflict.
Source: PROTOCOL.md section 4.2 (Stream forking)
MEDIUM Not handling 410 Gone for soft-deleted sources
Wrong:
try {
const res = await stream({ url: sourceUrl, offset: "-1", live: false })
const data = await res.json()
} catch (err) {
if (err.statusCode === 404) console.log("Not found")
}
Correct:
try {
const res = await stream({ url: sourceUrl, offset: "-1", live: false })
const data = await res.json()
} catch (err) {
if (err.statusCode === 404) console.log("Not found")
if (err.statusCode === 410) console.log("Soft-deleted — has active forks")
}
When a source stream with active forks is deleted, it returns 410 Gone for all client operations. The source's data is retained internally for fork reads, but the source URL is no longer directly accessible.
Source: PROTOCOL.md section 4.2 (Soft-delete and lifecycle)
See also
Version
Targets @durable-streams/client v0.2.1.