@durable-streams/server-conformance-tests 0.2.3 → 0.3.0

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.
@@ -993,17 +993,23 @@ function runConformanceTests(options) {
993
993
  body: `historical`
994
994
  });
995
995
  const longPollPromise = fetch(`${getBaseUrl()}${streamPath}?offset=now&live=long-poll`, { method: `GET` });
996
- await new Promise((r) => setTimeout(r, 100));
997
- await fetch(`${getBaseUrl()}${streamPath}`, {
998
- method: `POST`,
999
- headers: { "Content-Type": `text/plain` },
1000
- body: `new data`
1001
- });
1002
- const response = await longPollPromise;
1003
- (0, vitest.expect)(response.status).toBe(200);
1004
- const text = await response.text();
1005
- (0, vitest.expect)(text).toBe(`new data`);
1006
- (0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
996
+ const interval = setInterval(() => {
997
+ fetch(`${getBaseUrl()}${streamPath}`, {
998
+ method: `POST`,
999
+ headers: { "Content-Type": `text/plain` },
1000
+ body: `new data`
1001
+ });
1002
+ }, 50);
1003
+ try {
1004
+ const response = await longPollPromise;
1005
+ (0, vitest.expect)(response.status).toBe(200);
1006
+ const text = await response.text();
1007
+ (0, vitest.expect)(text).toContain(`new data`);
1008
+ (0, vitest.expect)(text).not.toContain(`historical`);
1009
+ (0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
1010
+ } finally {
1011
+ clearInterval(interval);
1012
+ }
1007
1013
  });
1008
1014
  (0, vitest.test)(`should support offset=now with SSE mode`, async () => {
1009
1015
  const streamPath = `/v1/stream/offset-now-sse-test-${Date.now()}`;
@@ -1538,10 +1544,7 @@ function runConformanceTests(options) {
1538
1544
  });
1539
1545
  const response = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1540
1546
  const ttl = response.headers.get(`Stream-TTL`);
1541
- if (ttl) {
1542
- (0, vitest.expect)(parseInt(ttl)).toBeGreaterThan(0);
1543
- (0, vitest.expect)(parseInt(ttl)).toBeLessThanOrEqual(3600);
1544
- }
1547
+ (0, vitest.expect)(ttl).toBe(`3600`);
1545
1548
  });
1546
1549
  (0, vitest.test)(`should return Expires-At metadata if configured`, async () => {
1547
1550
  const streamPath = `/v1/stream/head-expires-metadata-test-${Date.now()}`;
@@ -1561,6 +1564,16 @@ function runConformanceTests(options) {
1561
1564
  (0, vitest.describe)(`TTL Expiration Behavior`, () => {
1562
1565
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1563
1566
  const uniquePath = (prefix) => `/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1567
+ const waitForDeletion = async (url, initialSleepMs, expectedStatuses = [404], timeoutMs = 5e3) => {
1568
+ await sleep(initialSleepMs);
1569
+ await vitest.vi.waitFor(async () => {
1570
+ const head = await fetch(url, { method: `HEAD` });
1571
+ (0, vitest.expect)(expectedStatuses).toContain(head.status);
1572
+ }, {
1573
+ timeout: timeoutMs,
1574
+ interval: 200
1575
+ });
1576
+ };
1564
1577
  vitest.test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
1565
1578
  const streamPath = uniquePath(`ttl-expire-head`);
1566
1579
  const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
@@ -1573,11 +1586,11 @@ function runConformanceTests(options) {
1573
1586
  (0, vitest.expect)(createResponse.status).toBe(201);
1574
1587
  const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1575
1588
  (0, vitest.expect)(headBefore.status).toBe(200);
1576
- await sleep(1500);
1577
- const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1578
- (0, vitest.expect)(headAfter.status).toBe(404);
1589
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 1e3);
1590
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1591
+ (0, vitest.expect)(getAfter.status).toBe(404);
1579
1592
  });
1580
- vitest.test.concurrent(`should return 404 on GET after TTL expires`, async () => {
1593
+ vitest.test.concurrent(`should return 404 on GET after TTL expires (idle)`, async () => {
1581
1594
  const streamPath = uniquePath(`ttl-expire-get`);
1582
1595
  const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1583
1596
  method: `PUT`,
@@ -1588,13 +1601,11 @@ function runConformanceTests(options) {
1588
1601
  body: `test data`
1589
1602
  });
1590
1603
  (0, vitest.expect)(createResponse.status).toBe(201);
1591
- const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1592
- (0, vitest.expect)(getBefore.status).toBe(200);
1593
- await sleep(1500);
1604
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 1e3);
1594
1605
  const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1595
1606
  (0, vitest.expect)(getAfter.status).toBe(404);
1596
1607
  });
1597
- vitest.test.concurrent(`should return 404 on POST append after TTL expires`, async () => {
1608
+ vitest.test.concurrent(`should return 404 on POST append after TTL expires (idle)`, async () => {
1598
1609
  const streamPath = uniquePath(`ttl-expire-post`);
1599
1610
  const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1600
1611
  method: `PUT`,
@@ -1604,13 +1615,7 @@ function runConformanceTests(options) {
1604
1615
  }
1605
1616
  });
1606
1617
  (0, vitest.expect)(createResponse.status).toBe(201);
1607
- const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1608
- method: `POST`,
1609
- headers: { "Content-Type": `text/plain` },
1610
- body: `appended data`
1611
- });
1612
- (0, vitest.expect)(postBefore.status).toBe(204);
1613
- await sleep(1500);
1618
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 1e3);
1614
1619
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1615
1620
  method: `POST`,
1616
1621
  headers: { "Content-Type": `text/plain` },
@@ -1620,7 +1625,7 @@ function runConformanceTests(options) {
1620
1625
  });
1621
1626
  vitest.test.concurrent(`should return 404 on HEAD after Expires-At passes`, async () => {
1622
1627
  const streamPath = uniquePath(`expires-at-head`);
1623
- const expiresAt = new Date(Date.now() + 1e3).toISOString();
1628
+ const expiresAt = new Date(Date.now() + 3e3).toISOString();
1624
1629
  const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1625
1630
  method: `PUT`,
1626
1631
  headers: {
@@ -1631,13 +1636,13 @@ function runConformanceTests(options) {
1631
1636
  (0, vitest.expect)(createResponse.status).toBe(201);
1632
1637
  const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1633
1638
  (0, vitest.expect)(headBefore.status).toBe(200);
1634
- await sleep(1500);
1635
- const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1636
- (0, vitest.expect)(headAfter.status).toBe(404);
1639
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 3e3);
1640
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1641
+ (0, vitest.expect)(getAfter.status).toBe(404);
1637
1642
  });
1638
1643
  vitest.test.concurrent(`should return 404 on GET after Expires-At passes`, async () => {
1639
1644
  const streamPath = uniquePath(`expires-at-get`);
1640
- const expiresAt = new Date(Date.now() + 1e3).toISOString();
1645
+ const expiresAt = new Date(Date.now() + 3e3).toISOString();
1641
1646
  const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1642
1647
  method: `PUT`,
1643
1648
  headers: {
@@ -1649,13 +1654,13 @@ function runConformanceTests(options) {
1649
1654
  (0, vitest.expect)(createResponse.status).toBe(201);
1650
1655
  const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1651
1656
  (0, vitest.expect)(getBefore.status).toBe(200);
1652
- await sleep(1500);
1657
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 3e3);
1653
1658
  const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1654
1659
  (0, vitest.expect)(getAfter.status).toBe(404);
1655
1660
  });
1656
1661
  vitest.test.concurrent(`should return 404 on POST append after Expires-At passes`, async () => {
1657
1662
  const streamPath = uniquePath(`expires-at-post`);
1658
- const expiresAt = new Date(Date.now() + 1e3).toISOString();
1663
+ const expiresAt = new Date(Date.now() + 3e3).toISOString();
1659
1664
  const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1660
1665
  method: `PUT`,
1661
1666
  headers: {
@@ -1670,7 +1675,7 @@ function runConformanceTests(options) {
1670
1675
  body: `appended data`
1671
1676
  });
1672
1677
  (0, vitest.expect)(postBefore.status).toBe(204);
1673
- await sleep(1500);
1678
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 3e3);
1674
1679
  const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1675
1680
  method: `POST`,
1676
1681
  headers: { "Content-Type": `text/plain` },
@@ -1689,7 +1694,7 @@ function runConformanceTests(options) {
1689
1694
  body: `original data`
1690
1695
  });
1691
1696
  (0, vitest.expect)(createResponse.status).toBe(201);
1692
- await sleep(1500);
1697
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 1e3);
1693
1698
  const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1694
1699
  method: `PUT`,
1695
1700
  headers: {
@@ -1704,6 +1709,81 @@ function runConformanceTests(options) {
1704
1709
  const body = await getResponse.text();
1705
1710
  (0, vitest.expect)(body).toContain(`new data`);
1706
1711
  });
1712
+ vitest.test.concurrent(`should extend TTL on write (sliding window)`, async () => {
1713
+ const streamPath = uniquePath(`ttl-renew-write`);
1714
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1715
+ method: `PUT`,
1716
+ headers: {
1717
+ "Content-Type": `text/plain`,
1718
+ "Stream-TTL": `2`
1719
+ }
1720
+ });
1721
+ (0, vitest.expect)(createResponse.status).toBe(201);
1722
+ await sleep(1500);
1723
+ const appendResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1724
+ method: `POST`,
1725
+ headers: { "Content-Type": `text/plain` },
1726
+ body: `keep alive`
1727
+ });
1728
+ (0, vitest.expect)(appendResponse.status).toBe(204);
1729
+ await sleep(1500);
1730
+ const headResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1731
+ (0, vitest.expect)(headResponse.status).toBe(200);
1732
+ });
1733
+ vitest.test.concurrent(`should extend TTL on read (sliding window)`, async () => {
1734
+ const streamPath = uniquePath(`ttl-renew-read`);
1735
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1736
+ method: `PUT`,
1737
+ headers: {
1738
+ "Content-Type": `text/plain`,
1739
+ "Stream-TTL": `2`
1740
+ },
1741
+ body: `test data`
1742
+ });
1743
+ (0, vitest.expect)(createResponse.status).toBe(201);
1744
+ await sleep(1500);
1745
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1746
+ (0, vitest.expect)(readResponse.status).toBe(200);
1747
+ await sleep(1500);
1748
+ const headResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1749
+ (0, vitest.expect)(headResponse.status).toBe(200);
1750
+ });
1751
+ vitest.test.concurrent(`should NOT extend TTL on HEAD`, async () => {
1752
+ const streamPath = uniquePath(`ttl-no-renew-head`);
1753
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1754
+ method: `PUT`,
1755
+ headers: {
1756
+ "Content-Type": `text/plain`,
1757
+ "Stream-TTL": `2`
1758
+ }
1759
+ });
1760
+ (0, vitest.expect)(createResponse.status).toBe(201);
1761
+ await sleep(1500);
1762
+ const headMid = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1763
+ (0, vitest.expect)(headMid.status).toBe(200);
1764
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 500);
1765
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1766
+ (0, vitest.expect)(getAfter.status).toBe(404);
1767
+ });
1768
+ vitest.test.concurrent(`should NOT extend Expires-At on read or write`, async () => {
1769
+ const streamPath = uniquePath(`expires-at-no-renew`);
1770
+ const expiresAt = new Date(Date.now() + 4e3).toISOString();
1771
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1772
+ method: `PUT`,
1773
+ headers: {
1774
+ "Content-Type": `text/plain`,
1775
+ "Stream-Expires-At": expiresAt
1776
+ },
1777
+ body: `test data`
1778
+ });
1779
+ (0, vitest.expect)(createResponse.status).toBe(201);
1780
+ await sleep(2e3);
1781
+ const readResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1782
+ (0, vitest.expect)(readResponse.status).toBe(200);
1783
+ await waitForDeletion(`${getBaseUrl()}${streamPath}`, 2e3);
1784
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1785
+ (0, vitest.expect)(getAfter.status).toBe(404);
1786
+ });
1707
1787
  });
1708
1788
  (0, vitest.describe)(`Caching and ETag`, () => {
1709
1789
  (0, vitest.test)(`should generate ETag on GET responses`, async () => {
@@ -2826,8 +2906,11 @@ function runConformanceTests(options) {
2826
2906
  const finalResult = await readEntireStream(streamPath);
2827
2907
  (0, vitest.expect)(finalResult).toEqual(expected);
2828
2908
  }
2829
- ), { numRuns: 20 });
2830
- });
2909
+ ), {
2910
+ numRuns: 20,
2911
+ interruptAfterTimeLimit: 1e4
2912
+ });
2913
+ }, 3e4);
2831
2914
  (0, vitest.test)(`single byte values cover full range (0-255) with concurrent readers during write`, async () => {
2832
2915
  await fast_check.assert(fast_check.asyncProperty(
2833
2916
  // Generate a byte value from 0-255
@@ -2860,8 +2943,11 @@ function runConformanceTests(options) {
2860
2943
  const finalResult = await readEntireStream(streamPath);
2861
2944
  (0, vitest.expect)(finalResult).toEqual(expected);
2862
2945
  }
2863
- ), { numRuns: 50 });
2864
- });
2946
+ ), {
2947
+ numRuns: 50,
2948
+ interruptAfterTimeLimit: 1e4
2949
+ });
2950
+ }, 3e4);
2865
2951
  });
2866
2952
  (0, vitest.describe)(`Operation Sequence Properties`, () => {
2867
2953
  (0, vitest.test)(`random operation sequences maintain stream invariants`, async () => {
@@ -2938,8 +3024,11 @@ function runConformanceTests(options) {
2938
3024
  }
2939
3025
  return true;
2940
3026
  }
2941
- ), { numRuns: 15 });
2942
- });
3027
+ ), {
3028
+ numRuns: 15,
3029
+ interruptAfterTimeLimit: 3e4
3030
+ });
3031
+ }, 6e4);
2943
3032
  (0, vitest.test)(`offsets are always monotonically increasing`, async () => {
2944
3033
  await fast_check.assert(fast_check.asyncProperty(
2945
3034
  // Generate multiple chunks to append
@@ -5022,6 +5111,1650 @@ function runConformanceTests(options) {
5022
5111
  });
5023
5112
  });
5024
5113
  });
5114
+ (0, vitest.describe)(`Fork - Creation`, () => {
5115
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
5116
+ const STREAM_FORK_OFFSET_HEADER = `Stream-Fork-Offset`;
5117
+ const STREAM_CLOSED_HEADER_FORK = `Stream-Closed`;
5118
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5119
+ (0, vitest.test)(`should fork at current head (default)`, async () => {
5120
+ const id = uniqueId();
5121
+ const sourcePath = `/v1/stream/fork-create-head-src-${id}`;
5122
+ const forkPath = `/v1/stream/fork-create-head-fork-${id}`;
5123
+ const createRes = await fetch(`${getBaseUrl()}${sourcePath}`, {
5124
+ method: `PUT`,
5125
+ headers: { "Content-Type": `text/plain` },
5126
+ body: `source data`
5127
+ });
5128
+ (0, vitest.expect)(createRes.status).toBe(201);
5129
+ const sourceOffset = createRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
5130
+ (0, vitest.expect)(sourceOffset).toBeDefined();
5131
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5132
+ method: `PUT`,
5133
+ headers: {
5134
+ "Content-Type": `text/plain`,
5135
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5136
+ }
5137
+ });
5138
+ (0, vitest.expect)(forkRes.status).toBe(201);
5139
+ });
5140
+ (0, vitest.test)(`should fork at a specific offset`, async () => {
5141
+ const id = uniqueId();
5142
+ const sourcePath = `/v1/stream/fork-create-offset-src-${id}`;
5143
+ const forkPath = `/v1/stream/fork-create-offset-fork-${id}`;
5144
+ const createRes = await fetch(`${getBaseUrl()}${sourcePath}`, {
5145
+ method: `PUT`,
5146
+ headers: { "Content-Type": `text/plain` },
5147
+ body: `first`
5148
+ });
5149
+ (0, vitest.expect)(createRes.status).toBe(201);
5150
+ const midOffset = createRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
5151
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5152
+ method: `POST`,
5153
+ headers: { "Content-Type": `text/plain` },
5154
+ body: `second`
5155
+ });
5156
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5157
+ method: `PUT`,
5158
+ headers: {
5159
+ "Content-Type": `text/plain`,
5160
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
5161
+ [STREAM_FORK_OFFSET_HEADER]: midOffset
5162
+ }
5163
+ });
5164
+ (0, vitest.expect)(forkRes.status).toBe(201);
5165
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5166
+ (0, vitest.expect)(readRes.status).toBe(200);
5167
+ const body = await readRes.text();
5168
+ (0, vitest.expect)(body).toBe(`first`);
5169
+ });
5170
+ (0, vitest.test)(`should fork at zero offset (empty inherited data)`, async () => {
5171
+ const id = uniqueId();
5172
+ const sourcePath = `/v1/stream/fork-create-zero-src-${id}`;
5173
+ const forkPath = `/v1/stream/fork-create-zero-fork-${id}`;
5174
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5175
+ method: `PUT`,
5176
+ headers: { "Content-Type": `text/plain` },
5177
+ body: `source data`
5178
+ });
5179
+ const zeroOffset = `0000000000000000_0000000000000000`;
5180
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5181
+ method: `PUT`,
5182
+ headers: {
5183
+ "Content-Type": `text/plain`,
5184
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
5185
+ [STREAM_FORK_OFFSET_HEADER]: zeroOffset
5186
+ }
5187
+ });
5188
+ (0, vitest.expect)(forkRes.status).toBe(201);
5189
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5190
+ (0, vitest.expect)(readRes.status).toBe(200);
5191
+ const body = await readRes.text();
5192
+ (0, vitest.expect)(body).toBe(``);
5193
+ (0, vitest.expect)(readRes.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
5194
+ });
5195
+ (0, vitest.test)(`should fork at head offset (all source data inherited)`, async () => {
5196
+ const id = uniqueId();
5197
+ const sourcePath = `/v1/stream/fork-create-all-src-${id}`;
5198
+ const forkPath = `/v1/stream/fork-create-all-fork-${id}`;
5199
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5200
+ method: `PUT`,
5201
+ headers: { "Content-Type": `text/plain` },
5202
+ body: `chunk1`
5203
+ });
5204
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5205
+ method: `POST`,
5206
+ headers: { "Content-Type": `text/plain` },
5207
+ body: `chunk2`
5208
+ });
5209
+ const headRes = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `HEAD` });
5210
+ const headOffset = headRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
5211
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5212
+ method: `PUT`,
5213
+ headers: {
5214
+ "Content-Type": `text/plain`,
5215
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
5216
+ [STREAM_FORK_OFFSET_HEADER]: headOffset
5217
+ }
5218
+ });
5219
+ (0, vitest.expect)(forkRes.status).toBe(201);
5220
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5221
+ (0, vitest.expect)(readRes.status).toBe(200);
5222
+ const body = await readRes.text();
5223
+ (0, vitest.expect)(body).toBe(`chunk1chunk2`);
5224
+ });
5225
+ (0, vitest.test)(`should return 404 when forking a nonexistent stream`, async () => {
5226
+ const id = uniqueId();
5227
+ const forkPath = `/v1/stream/fork-create-404-fork-${id}`;
5228
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5229
+ method: `PUT`,
5230
+ headers: {
5231
+ "Content-Type": `text/plain`,
5232
+ [STREAM_FORKED_FROM_HEADER]: `/v1/stream/nonexistent-${id}`
5233
+ }
5234
+ });
5235
+ (0, vitest.expect)(forkRes.status).toBe(404);
5236
+ });
5237
+ (0, vitest.test)(`should return 400 when forking at offset beyond stream length`, async () => {
5238
+ const id = uniqueId();
5239
+ const sourcePath = `/v1/stream/fork-create-beyond-src-${id}`;
5240
+ const forkPath = `/v1/stream/fork-create-beyond-fork-${id}`;
5241
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5242
+ method: `PUT`,
5243
+ headers: { "Content-Type": `text/plain` },
5244
+ body: `small data`
5245
+ });
5246
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5247
+ method: `PUT`,
5248
+ headers: {
5249
+ "Content-Type": `text/plain`,
5250
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
5251
+ [STREAM_FORK_OFFSET_HEADER]: `9999999999999999_9999999999999999`
5252
+ }
5253
+ });
5254
+ (0, vitest.expect)(forkRes.status).toBe(400);
5255
+ });
5256
+ (0, vitest.test)(`should return 409 when forking to path already in use with different config`, async () => {
5257
+ const id = uniqueId();
5258
+ const sourcePath = `/v1/stream/fork-create-conflict-src-${id}`;
5259
+ const forkPath = `/v1/stream/fork-create-conflict-fork-${id}`;
5260
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5261
+ method: `PUT`,
5262
+ headers: { "Content-Type": `text/plain` },
5263
+ body: `source`
5264
+ });
5265
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5266
+ method: `PUT`,
5267
+ headers: { "Content-Type": `application/json` }
5268
+ });
5269
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5270
+ method: `PUT`,
5271
+ headers: {
5272
+ "Content-Type": `text/plain`,
5273
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5274
+ }
5275
+ });
5276
+ (0, vitest.expect)(forkRes.status).toBe(409);
5277
+ });
5278
+ (0, vitest.test)(`should fork a closed stream — fork starts open`, async () => {
5279
+ const id = uniqueId();
5280
+ const sourcePath = `/v1/stream/fork-create-closed-src-${id}`;
5281
+ const forkPath = `/v1/stream/fork-create-closed-fork-${id}`;
5282
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5283
+ method: `PUT`,
5284
+ headers: { "Content-Type": `text/plain` },
5285
+ body: `closed data`
5286
+ });
5287
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5288
+ method: `POST`,
5289
+ headers: { [STREAM_CLOSED_HEADER_FORK]: `true` }
5290
+ });
5291
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5292
+ method: `PUT`,
5293
+ headers: {
5294
+ "Content-Type": `text/plain`,
5295
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5296
+ }
5297
+ });
5298
+ (0, vitest.expect)(forkRes.status).toBe(201);
5299
+ (0, vitest.expect)(forkRes.headers.get(STREAM_CLOSED_HEADER_FORK)).toBeNull();
5300
+ const appendRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5301
+ method: `POST`,
5302
+ headers: { "Content-Type": `text/plain` },
5303
+ body: ` fork data`
5304
+ });
5305
+ (0, vitest.expect)(appendRes.status).toBe(204);
5306
+ });
5307
+ (0, vitest.test)(`should fork an empty stream`, async () => {
5308
+ const id = uniqueId();
5309
+ const sourcePath = `/v1/stream/fork-create-empty-src-${id}`;
5310
+ const forkPath = `/v1/stream/fork-create-empty-fork-${id}`;
5311
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5312
+ method: `PUT`,
5313
+ headers: { "Content-Type": `text/plain` }
5314
+ });
5315
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5316
+ method: `PUT`,
5317
+ headers: {
5318
+ "Content-Type": `text/plain`,
5319
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5320
+ }
5321
+ });
5322
+ (0, vitest.expect)(forkRes.status).toBe(201);
5323
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5324
+ (0, vitest.expect)(readRes.status).toBe(200);
5325
+ const body = await readRes.text();
5326
+ (0, vitest.expect)(body).toBe(``);
5327
+ (0, vitest.expect)(readRes.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
5328
+ });
5329
+ (0, vitest.test)(`should fork preserving content-type when specified`, async () => {
5330
+ const id = uniqueId();
5331
+ const sourcePath = `/v1/stream/fork-create-ct-src-${id}`;
5332
+ const forkPath = `/v1/stream/fork-create-ct-fork-${id}`;
5333
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5334
+ method: `PUT`,
5335
+ headers: { "Content-Type": `application/json` },
5336
+ body: `[{"key":"value"}]`
5337
+ });
5338
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5339
+ method: `PUT`,
5340
+ headers: {
5341
+ "Content-Type": `application/json`,
5342
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5343
+ }
5344
+ });
5345
+ (0, vitest.expect)(forkRes.status).toBe(201);
5346
+ (0, vitest.expect)(forkRes.headers.get(`content-type`)).toBe(`application/json`);
5347
+ const headRes = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
5348
+ (0, vitest.expect)(headRes.headers.get(`content-type`)).toBe(`application/json`);
5349
+ });
5350
+ });
5351
+ (0, vitest.describe)(`Fork - Reading`, () => {
5352
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
5353
+ const STREAM_FORK_OFFSET_HEADER = `Stream-Fork-Offset`;
5354
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5355
+ (0, vitest.test)(`should read entire fork (source + fork data)`, async () => {
5356
+ const id = uniqueId();
5357
+ const sourcePath = `/v1/stream/fork-read-entire-src-${id}`;
5358
+ const forkPath = `/v1/stream/fork-read-entire-fork-${id}`;
5359
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5360
+ method: `PUT`,
5361
+ headers: { "Content-Type": `text/plain` },
5362
+ body: `source`
5363
+ });
5364
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5365
+ method: `PUT`,
5366
+ headers: {
5367
+ "Content-Type": `text/plain`,
5368
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5369
+ }
5370
+ });
5371
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5372
+ method: `POST`,
5373
+ headers: { "Content-Type": `text/plain` },
5374
+ body: ` fork`
5375
+ });
5376
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5377
+ (0, vitest.expect)(readRes.status).toBe(200);
5378
+ const body = await readRes.text();
5379
+ (0, vitest.expect)(body).toBe(`source fork`);
5380
+ });
5381
+ (0, vitest.test)(`should read only inherited portion`, async () => {
5382
+ const id = uniqueId();
5383
+ const sourcePath = `/v1/stream/fork-read-inherited-src-${id}`;
5384
+ const forkPath = `/v1/stream/fork-read-inherited-fork-${id}`;
5385
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5386
+ method: `PUT`,
5387
+ headers: { "Content-Type": `text/plain` },
5388
+ body: `inherited data`
5389
+ });
5390
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5391
+ method: `PUT`,
5392
+ headers: {
5393
+ "Content-Type": `text/plain`,
5394
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5395
+ }
5396
+ });
5397
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5398
+ (0, vitest.expect)(readRes.status).toBe(200);
5399
+ const body = await readRes.text();
5400
+ (0, vitest.expect)(body).toBe(`inherited data`);
5401
+ (0, vitest.expect)(readRes.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
5402
+ });
5403
+ (0, vitest.test)(`should read only fork's own data (starting past fork offset)`, async () => {
5404
+ const id = uniqueId();
5405
+ const sourcePath = `/v1/stream/fork-read-own-src-${id}`;
5406
+ const forkPath = `/v1/stream/fork-read-own-fork-${id}`;
5407
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5408
+ method: `PUT`,
5409
+ headers: { "Content-Type": `text/plain` },
5410
+ body: `source`
5411
+ });
5412
+ const sourceHead = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `HEAD` });
5413
+ const forkOffset = sourceHead.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
5414
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5415
+ method: `PUT`,
5416
+ headers: {
5417
+ "Content-Type": `text/plain`,
5418
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5419
+ }
5420
+ });
5421
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5422
+ method: `POST`,
5423
+ headers: { "Content-Type": `text/plain` },
5424
+ body: `fork only`
5425
+ });
5426
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=${forkOffset}`);
5427
+ (0, vitest.expect)(readRes.status).toBe(200);
5428
+ const body = await readRes.text();
5429
+ (0, vitest.expect)(body).toBe(`fork only`);
5430
+ });
5431
+ (0, vitest.test)(`should read across fork boundary`, async () => {
5432
+ const id = uniqueId();
5433
+ const sourcePath = `/v1/stream/fork-read-boundary-src-${id}`;
5434
+ const forkPath = `/v1/stream/fork-read-boundary-fork-${id}`;
5435
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5436
+ method: `PUT`,
5437
+ headers: { "Content-Type": `text/plain` },
5438
+ body: `A`
5439
+ });
5440
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5441
+ method: `POST`,
5442
+ headers: { "Content-Type": `text/plain` },
5443
+ body: `B`
5444
+ });
5445
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5446
+ method: `PUT`,
5447
+ headers: {
5448
+ "Content-Type": `text/plain`,
5449
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5450
+ }
5451
+ });
5452
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5453
+ method: `POST`,
5454
+ headers: { "Content-Type": `text/plain` },
5455
+ body: `C`
5456
+ });
5457
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5458
+ (0, vitest.expect)(readRes.status).toBe(200);
5459
+ const body = await readRes.text();
5460
+ (0, vitest.expect)(body).toBe(`ABC`);
5461
+ });
5462
+ (0, vitest.test)(`should not show source appends after fork`, async () => {
5463
+ const id = uniqueId();
5464
+ const sourcePath = `/v1/stream/fork-read-isolation-src-${id}`;
5465
+ const forkPath = `/v1/stream/fork-read-isolation-fork-${id}`;
5466
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5467
+ method: `PUT`,
5468
+ headers: { "Content-Type": `text/plain` },
5469
+ body: `before`
5470
+ });
5471
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5472
+ method: `PUT`,
5473
+ headers: {
5474
+ "Content-Type": `text/plain`,
5475
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5476
+ }
5477
+ });
5478
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5479
+ method: `POST`,
5480
+ headers: { "Content-Type": `text/plain` },
5481
+ body: ` after`
5482
+ });
5483
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5484
+ (0, vitest.expect)(readRes.status).toBe(200);
5485
+ const body = await readRes.text();
5486
+ (0, vitest.expect)(body).toBe(`before`);
5487
+ });
5488
+ (0, vitest.test)(`should NOT include fork headers on HEAD/GET/PUT responses (forks are transparent)`, async () => {
5489
+ const id = uniqueId();
5490
+ const sourcePath = `/v1/stream/fork-read-headers-src-${id}`;
5491
+ const forkPath = `/v1/stream/fork-read-headers-fork-${id}`;
5492
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5493
+ method: `PUT`,
5494
+ headers: { "Content-Type": `text/plain` },
5495
+ body: `data`
5496
+ });
5497
+ const putRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5498
+ method: `PUT`,
5499
+ headers: {
5500
+ "Content-Type": `text/plain`,
5501
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5502
+ }
5503
+ });
5504
+ (0, vitest.expect)(putRes.headers.get(STREAM_FORKED_FROM_HEADER)).toBeNull();
5505
+ (0, vitest.expect)(putRes.headers.get(STREAM_FORK_OFFSET_HEADER)).toBeNull();
5506
+ const headRes = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
5507
+ (0, vitest.expect)(headRes.headers.get(STREAM_FORKED_FROM_HEADER)).toBeNull();
5508
+ (0, vitest.expect)(headRes.headers.get(STREAM_FORK_OFFSET_HEADER)).toBeNull();
5509
+ const getRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5510
+ await getRes.text();
5511
+ (0, vitest.expect)(getRes.headers.get(STREAM_FORKED_FROM_HEADER)).toBeNull();
5512
+ (0, vitest.expect)(getRes.headers.get(STREAM_FORK_OFFSET_HEADER)).toBeNull();
5513
+ });
5514
+ });
5515
+ (0, vitest.describe)(`Fork - Appending`, () => {
5516
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
5517
+ const STREAM_CLOSED_HEADER_FORK = `Stream-Closed`;
5518
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5519
+ (0, vitest.test)(`should append to a fork`, async () => {
5520
+ const id = uniqueId();
5521
+ const sourcePath = `/v1/stream/fork-append-src-${id}`;
5522
+ const forkPath = `/v1/stream/fork-append-fork-${id}`;
5523
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5524
+ method: `PUT`,
5525
+ headers: { "Content-Type": `text/plain` },
5526
+ body: `source`
5527
+ });
5528
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5529
+ method: `PUT`,
5530
+ headers: {
5531
+ "Content-Type": `text/plain`,
5532
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5533
+ }
5534
+ });
5535
+ const appendRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5536
+ method: `POST`,
5537
+ headers: { "Content-Type": `text/plain` },
5538
+ body: ` appended`
5539
+ });
5540
+ (0, vitest.expect)(appendRes.status).toBe(204);
5541
+ (0, vitest.expect)(appendRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
5542
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5543
+ const body = await readRes.text();
5544
+ (0, vitest.expect)(body).toBe(`source appended`);
5545
+ });
5546
+ (0, vitest.test)(`should support idempotent producer on fork`, async () => {
5547
+ const id = uniqueId();
5548
+ const sourcePath = `/v1/stream/fork-append-idempotent-src-${id}`;
5549
+ const forkPath = `/v1/stream/fork-append-idempotent-fork-${id}`;
5550
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5551
+ method: `PUT`,
5552
+ headers: { "Content-Type": `text/plain` },
5553
+ body: `source`
5554
+ });
5555
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5556
+ method: `PUT`,
5557
+ headers: {
5558
+ "Content-Type": `text/plain`,
5559
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5560
+ }
5561
+ });
5562
+ const append1 = await fetch(`${getBaseUrl()}${forkPath}`, {
5563
+ method: `POST`,
5564
+ headers: {
5565
+ "Content-Type": `text/plain`,
5566
+ "Producer-Id": `fork-producer-${id}`,
5567
+ "Producer-Epoch": `0`,
5568
+ "Producer-Seq": `0`
5569
+ },
5570
+ body: `msg1`
5571
+ });
5572
+ (0, vitest.expect)(append1.status).toBe(200);
5573
+ const append1Retry = await fetch(`${getBaseUrl()}${forkPath}`, {
5574
+ method: `POST`,
5575
+ headers: {
5576
+ "Content-Type": `text/plain`,
5577
+ "Producer-Id": `fork-producer-${id}`,
5578
+ "Producer-Epoch": `0`,
5579
+ "Producer-Seq": `0`
5580
+ },
5581
+ body: `msg1`
5582
+ });
5583
+ (0, vitest.expect)(append1Retry.status).toBe(204);
5584
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5585
+ const body = await readRes.text();
5586
+ (0, vitest.expect)(body).toBe(`sourcemsg1`);
5587
+ });
5588
+ (0, vitest.test)(`should close forked stream independently`, async () => {
5589
+ const id = uniqueId();
5590
+ const sourcePath = `/v1/stream/fork-append-close-src-${id}`;
5591
+ const forkPath = `/v1/stream/fork-append-close-fork-${id}`;
5592
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5593
+ method: `PUT`,
5594
+ headers: { "Content-Type": `text/plain` },
5595
+ body: `source`
5596
+ });
5597
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5598
+ method: `PUT`,
5599
+ headers: {
5600
+ "Content-Type": `text/plain`,
5601
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5602
+ }
5603
+ });
5604
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5605
+ method: `POST`,
5606
+ headers: { "Content-Type": `text/plain` },
5607
+ body: ` final`
5608
+ });
5609
+ const closeRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5610
+ method: `POST`,
5611
+ headers: { [STREAM_CLOSED_HEADER_FORK]: `true` }
5612
+ });
5613
+ (0, vitest.expect)([200, 204]).toContain(closeRes.status);
5614
+ (0, vitest.expect)(closeRes.headers.get(STREAM_CLOSED_HEADER_FORK)).toBe(`true`);
5615
+ const sourceHead = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `HEAD` });
5616
+ (0, vitest.expect)(sourceHead.headers.get(STREAM_CLOSED_HEADER_FORK)).toBeNull();
5617
+ });
5618
+ (0, vitest.test)(`should not affect fork when source is closed`, async () => {
5619
+ const id = uniqueId();
5620
+ const sourcePath = `/v1/stream/fork-append-src-close-src-${id}`;
5621
+ const forkPath = `/v1/stream/fork-append-src-close-fork-${id}`;
5622
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5623
+ method: `PUT`,
5624
+ headers: { "Content-Type": `text/plain` },
5625
+ body: `source`
5626
+ });
5627
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5628
+ method: `PUT`,
5629
+ headers: {
5630
+ "Content-Type": `text/plain`,
5631
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5632
+ }
5633
+ });
5634
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5635
+ method: `POST`,
5636
+ headers: { [STREAM_CLOSED_HEADER_FORK]: `true` }
5637
+ });
5638
+ const appendRes = await fetch(`${getBaseUrl()}${forkPath}`, {
5639
+ method: `POST`,
5640
+ headers: { "Content-Type": `text/plain` },
5641
+ body: ` fork data`
5642
+ });
5643
+ (0, vitest.expect)(appendRes.status).toBe(204);
5644
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
5645
+ (0, vitest.expect)(forkHead.headers.get(STREAM_CLOSED_HEADER_FORK)).toBeNull();
5646
+ });
5647
+ (0, vitest.test)(`should append to source after fork — source independent`, async () => {
5648
+ const id = uniqueId();
5649
+ const sourcePath = `/v1/stream/fork-append-src-indep-src-${id}`;
5650
+ const forkPath = `/v1/stream/fork-append-src-indep-fork-${id}`;
5651
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5652
+ method: `PUT`,
5653
+ headers: { "Content-Type": `text/plain` },
5654
+ body: `initial`
5655
+ });
5656
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5657
+ method: `PUT`,
5658
+ headers: {
5659
+ "Content-Type": `text/plain`,
5660
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5661
+ }
5662
+ });
5663
+ const appendRes = await fetch(`${getBaseUrl()}${sourcePath}`, {
5664
+ method: `POST`,
5665
+ headers: { "Content-Type": `text/plain` },
5666
+ body: ` extra`
5667
+ });
5668
+ (0, vitest.expect)(appendRes.status).toBe(204);
5669
+ const sourceRead = await fetch(`${getBaseUrl()}${sourcePath}?offset=-1`);
5670
+ const sourceBody = await sourceRead.text();
5671
+ (0, vitest.expect)(sourceBody).toBe(`initial extra`);
5672
+ const forkRead = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
5673
+ const forkBody = await forkRead.text();
5674
+ (0, vitest.expect)(forkBody).toBe(`initial`);
5675
+ });
5676
+ });
5677
+ (0, vitest.describe)(`Fork - Recursive`, () => {
5678
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
5679
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5680
+ (0, vitest.test)(`should create a three-level fork chain`, async () => {
5681
+ const id = uniqueId();
5682
+ const level0 = `/v1/stream/fork-recursive-l0-${id}`;
5683
+ const level1 = `/v1/stream/fork-recursive-l1-${id}`;
5684
+ const level2 = `/v1/stream/fork-recursive-l2-${id}`;
5685
+ await fetch(`${getBaseUrl()}${level0}`, {
5686
+ method: `PUT`,
5687
+ headers: { "Content-Type": `text/plain` },
5688
+ body: `L0`
5689
+ });
5690
+ const fork1Res = await fetch(`${getBaseUrl()}${level1}`, {
5691
+ method: `PUT`,
5692
+ headers: {
5693
+ "Content-Type": `text/plain`,
5694
+ [STREAM_FORKED_FROM_HEADER]: level0
5695
+ }
5696
+ });
5697
+ (0, vitest.expect)(fork1Res.status).toBe(201);
5698
+ const fork2Res = await fetch(`${getBaseUrl()}${level2}`, {
5699
+ method: `PUT`,
5700
+ headers: {
5701
+ "Content-Type": `text/plain`,
5702
+ [STREAM_FORKED_FROM_HEADER]: level1
5703
+ }
5704
+ });
5705
+ (0, vitest.expect)(fork2Res.status).toBe(201);
5706
+ });
5707
+ (0, vitest.test)(`should fork at mid-point of inherited data`, async () => {
5708
+ const id = uniqueId();
5709
+ const level0 = `/v1/stream/fork-recursive-mid-l0-${id}`;
5710
+ const level1 = `/v1/stream/fork-recursive-mid-l1-${id}`;
5711
+ const level2 = `/v1/stream/fork-recursive-mid-l2-${id}`;
5712
+ await fetch(`${getBaseUrl()}${level0}`, {
5713
+ method: `PUT`,
5714
+ headers: { "Content-Type": `text/plain` },
5715
+ body: `A`
5716
+ });
5717
+ await fetch(`${getBaseUrl()}${level0}`, {
5718
+ method: `POST`,
5719
+ headers: { "Content-Type": `text/plain` },
5720
+ body: `B`
5721
+ });
5722
+ await fetch(`${getBaseUrl()}${level1}`, {
5723
+ method: `PUT`,
5724
+ headers: {
5725
+ "Content-Type": `text/plain`,
5726
+ [STREAM_FORKED_FROM_HEADER]: level0
5727
+ }
5728
+ });
5729
+ await fetch(`${getBaseUrl()}${level1}`, {
5730
+ method: `POST`,
5731
+ headers: { "Content-Type": `text/plain` },
5732
+ body: `C`
5733
+ });
5734
+ const l1Head = await fetch(`${getBaseUrl()}${level1}`, { method: `HEAD` });
5735
+ (0, vitest.expect)(l1Head.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER)).toBeDefined();
5736
+ const fork2Res = await fetch(`${getBaseUrl()}${level2}`, {
5737
+ method: `PUT`,
5738
+ headers: {
5739
+ "Content-Type": `text/plain`,
5740
+ [STREAM_FORKED_FROM_HEADER]: level1
5741
+ }
5742
+ });
5743
+ (0, vitest.expect)(fork2Res.status).toBe(201);
5744
+ const readRes = await fetch(`${getBaseUrl()}${level2}?offset=-1`);
5745
+ const body = await readRes.text();
5746
+ (0, vitest.expect)(body).toBe(`ABC`);
5747
+ });
5748
+ (0, vitest.test)(`should read correctly across three levels`, async () => {
5749
+ const id = uniqueId();
5750
+ const level0 = `/v1/stream/fork-recursive-read-l0-${id}`;
5751
+ const level1 = `/v1/stream/fork-recursive-read-l1-${id}`;
5752
+ const level2 = `/v1/stream/fork-recursive-read-l2-${id}`;
5753
+ await fetch(`${getBaseUrl()}${level0}`, {
5754
+ method: `PUT`,
5755
+ headers: { "Content-Type": `text/plain` },
5756
+ body: `A`
5757
+ });
5758
+ await fetch(`${getBaseUrl()}${level1}`, {
5759
+ method: `PUT`,
5760
+ headers: {
5761
+ "Content-Type": `text/plain`,
5762
+ [STREAM_FORKED_FROM_HEADER]: level0
5763
+ }
5764
+ });
5765
+ await fetch(`${getBaseUrl()}${level1}`, {
5766
+ method: `POST`,
5767
+ headers: { "Content-Type": `text/plain` },
5768
+ body: `B`
5769
+ });
5770
+ await fetch(`${getBaseUrl()}${level2}`, {
5771
+ method: `PUT`,
5772
+ headers: {
5773
+ "Content-Type": `text/plain`,
5774
+ [STREAM_FORKED_FROM_HEADER]: level1
5775
+ }
5776
+ });
5777
+ await fetch(`${getBaseUrl()}${level2}`, {
5778
+ method: `POST`,
5779
+ headers: { "Content-Type": `text/plain` },
5780
+ body: `C`
5781
+ });
5782
+ const r0 = await (await fetch(`${getBaseUrl()}${level0}?offset=-1`)).text();
5783
+ (0, vitest.expect)(r0).toBe(`A`);
5784
+ const r1 = await (await fetch(`${getBaseUrl()}${level1}?offset=-1`)).text();
5785
+ (0, vitest.expect)(r1).toBe(`AB`);
5786
+ const r2 = await (await fetch(`${getBaseUrl()}${level2}?offset=-1`)).text();
5787
+ (0, vitest.expect)(r2).toBe(`ABC`);
5788
+ });
5789
+ (0, vitest.test)(`should append at each level independently`, async () => {
5790
+ const id = uniqueId();
5791
+ const level0 = `/v1/stream/fork-recursive-indep-l0-${id}`;
5792
+ const level1 = `/v1/stream/fork-recursive-indep-l1-${id}`;
5793
+ const level2 = `/v1/stream/fork-recursive-indep-l2-${id}`;
5794
+ await fetch(`${getBaseUrl()}${level0}`, {
5795
+ method: `PUT`,
5796
+ headers: { "Content-Type": `text/plain` },
5797
+ body: `X`
5798
+ });
5799
+ await fetch(`${getBaseUrl()}${level1}`, {
5800
+ method: `PUT`,
5801
+ headers: {
5802
+ "Content-Type": `text/plain`,
5803
+ [STREAM_FORKED_FROM_HEADER]: level0
5804
+ }
5805
+ });
5806
+ await fetch(`${getBaseUrl()}${level1}`, {
5807
+ method: `POST`,
5808
+ headers: { "Content-Type": `text/plain` },
5809
+ body: `Y`
5810
+ });
5811
+ await fetch(`${getBaseUrl()}${level2}`, {
5812
+ method: `PUT`,
5813
+ headers: {
5814
+ "Content-Type": `text/plain`,
5815
+ [STREAM_FORKED_FROM_HEADER]: level1
5816
+ }
5817
+ });
5818
+ await fetch(`${getBaseUrl()}${level2}`, {
5819
+ method: `POST`,
5820
+ headers: { "Content-Type": `text/plain` },
5821
+ body: `Z`
5822
+ });
5823
+ await fetch(`${getBaseUrl()}${level0}`, {
5824
+ method: `POST`,
5825
+ headers: { "Content-Type": `text/plain` },
5826
+ body: `0`
5827
+ });
5828
+ await fetch(`${getBaseUrl()}${level1}`, {
5829
+ method: `POST`,
5830
+ headers: { "Content-Type": `text/plain` },
5831
+ body: `1`
5832
+ });
5833
+ const r0 = await (await fetch(`${getBaseUrl()}${level0}?offset=-1`)).text();
5834
+ (0, vitest.expect)(r0).toBe(`X0`);
5835
+ const r1 = await (await fetch(`${getBaseUrl()}${level1}?offset=-1`)).text();
5836
+ (0, vitest.expect)(r1).toBe(`XY1`);
5837
+ const r2 = await (await fetch(`${getBaseUrl()}${level2}?offset=-1`)).text();
5838
+ (0, vitest.expect)(r2).toBe(`XYZ`);
5839
+ });
5840
+ });
5841
+ (0, vitest.describe)(`Fork - Live Modes`, () => {
5842
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
5843
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5844
+ (0, vitest.test)(`should return inherited data immediately on long-poll`, async () => {
5845
+ const id = uniqueId();
5846
+ const sourcePath = `/v1/stream/fork-live-inherited-src-${id}`;
5847
+ const forkPath = `/v1/stream/fork-live-inherited-fork-${id}`;
5848
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5849
+ method: `PUT`,
5850
+ headers: { "Content-Type": `text/plain` },
5851
+ body: `inherited data`
5852
+ });
5853
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5854
+ method: `PUT`,
5855
+ headers: {
5856
+ "Content-Type": `text/plain`,
5857
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5858
+ }
5859
+ });
5860
+ const controller = new AbortController();
5861
+ const timeoutId = setTimeout(() => controller.abort(), 3e3);
5862
+ try {
5863
+ const response = await fetch(`${getBaseUrl()}${forkPath}?offset=-1&live=long-poll`, {
5864
+ method: `GET`,
5865
+ signal: controller.signal
5866
+ });
5867
+ clearTimeout(timeoutId);
5868
+ (0, vitest.expect)(response.status).toBe(200);
5869
+ const body = await response.text();
5870
+ (0, vitest.expect)(body).toBe(`inherited data`);
5871
+ (0, vitest.expect)(response.headers.get(__durable_streams_client.STREAM_UP_TO_DATE_HEADER)).toBe(`true`);
5872
+ } catch (e) {
5873
+ clearTimeout(timeoutId);
5874
+ if (!(e instanceof Error && e.name === `AbortError`)) throw e;
5875
+ (0, vitest.expect)(true).toBe(false);
5876
+ }
5877
+ });
5878
+ (0, vitest.test)(`should wait for fork appends, not source appends, on long-poll at tail`, async () => {
5879
+ const id = uniqueId();
5880
+ const sourcePath = `/v1/stream/fork-live-tail-src-${id}`;
5881
+ const forkPath = `/v1/stream/fork-live-tail-fork-${id}`;
5882
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5883
+ method: `PUT`,
5884
+ headers: { "Content-Type": `text/plain` },
5885
+ body: `source`
5886
+ });
5887
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5888
+ method: `PUT`,
5889
+ headers: {
5890
+ "Content-Type": `text/plain`,
5891
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5892
+ }
5893
+ });
5894
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `GET` });
5895
+ await forkHead.text();
5896
+ const forkOffset = forkHead.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
5897
+ const longPollPromise = fetch(`${getBaseUrl()}${forkPath}?offset=${forkOffset}&live=long-poll`, { method: `GET` });
5898
+ await new Promise((r) => setTimeout(r, 50));
5899
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5900
+ method: `POST`,
5901
+ headers: { "Content-Type": `text/plain` },
5902
+ body: ` source extra`
5903
+ });
5904
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5905
+ method: `POST`,
5906
+ headers: { "Content-Type": `text/plain` },
5907
+ body: ` fork new`
5908
+ });
5909
+ const response = await longPollPromise;
5910
+ (0, vitest.expect)(response.status).toBe(200);
5911
+ const body = await response.text();
5912
+ (0, vitest.expect)(body).toBe(` fork new`);
5913
+ }, getLongPollTestTimeoutMs());
5914
+ (0, vitest.test)(`should stream fork data via SSE`, async () => {
5915
+ const id = uniqueId();
5916
+ const sourcePath = `/v1/stream/fork-live-sse-src-${id}`;
5917
+ const forkPath = `/v1/stream/fork-live-sse-fork-${id}`;
5918
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5919
+ method: `PUT`,
5920
+ headers: { "Content-Type": `text/plain` },
5921
+ body: `inherited`
5922
+ });
5923
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5924
+ method: `PUT`,
5925
+ headers: {
5926
+ "Content-Type": `text/plain`,
5927
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5928
+ }
5929
+ });
5930
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5931
+ method: `POST`,
5932
+ headers: { "Content-Type": `text/plain` },
5933
+ body: ` forked`
5934
+ });
5935
+ const { response, received } = await fetchSSE(`${getBaseUrl()}${forkPath}?offset=-1&live=sse`, {
5936
+ untilContent: `forked`,
5937
+ timeoutMs: 5e3,
5938
+ maxChunks: 20
5939
+ });
5940
+ (0, vitest.expect)(response.status).toBe(200);
5941
+ (0, vitest.expect)(received).toContain(`inherited`);
5942
+ (0, vitest.expect)(received).toContain(`forked`);
5943
+ });
5944
+ (0, vitest.test)(`should handle long-poll handover at fork offset`, async () => {
5945
+ const id = uniqueId();
5946
+ const sourcePath = `/v1/stream/fork-live-handover-src-${id}`;
5947
+ const forkPath = `/v1/stream/fork-live-handover-fork-${id}`;
5948
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5949
+ method: `PUT`,
5950
+ headers: { "Content-Type": `text/plain` },
5951
+ body: `source data`
5952
+ });
5953
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5954
+ method: `PUT`,
5955
+ headers: {
5956
+ "Content-Type": `text/plain`,
5957
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
5958
+ }
5959
+ });
5960
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1&live=long-poll`);
5961
+ (0, vitest.expect)(readRes.status).toBe(200);
5962
+ const firstBody = await readRes.text();
5963
+ (0, vitest.expect)(firstBody).toBe(`source data`);
5964
+ const nextOffset = readRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
5965
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5966
+ method: `POST`,
5967
+ headers: { "Content-Type": `text/plain` },
5968
+ body: ` fork append`
5969
+ });
5970
+ const readRes2 = await fetch(`${getBaseUrl()}${forkPath}?offset=${nextOffset}`);
5971
+ (0, vitest.expect)(readRes2.status).toBe(200);
5972
+ const secondBody = await readRes2.text();
5973
+ (0, vitest.expect)(secondBody).toBe(` fork append`);
5974
+ });
5975
+ });
5976
+ (0, vitest.describe)(`Fork - Deletion and Lifecycle`, () => {
5977
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
5978
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5979
+ const waitForStatus = async (url, expectedStatus, timeoutMs = 5e3) => {
5980
+ await vitest.vi.waitFor(async () => {
5981
+ const res = await fetch(url, { method: `HEAD` });
5982
+ (0, vitest.expect)(res.status).toBe(expectedStatus);
5983
+ }, {
5984
+ timeout: timeoutMs,
5985
+ interval: 200
5986
+ });
5987
+ };
5988
+ (0, vitest.test)(`should delete fork without affecting source`, async () => {
5989
+ const id = uniqueId();
5990
+ const sourcePath = `/v1/stream/fork-del-src-unaffected-src-${id}`;
5991
+ const forkPath = `/v1/stream/fork-del-src-unaffected-fork-${id}`;
5992
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
5993
+ method: `PUT`,
5994
+ headers: { "Content-Type": `text/plain` },
5995
+ body: `source data`
5996
+ });
5997
+ await fetch(`${getBaseUrl()}${forkPath}`, {
5998
+ method: `PUT`,
5999
+ headers: {
6000
+ "Content-Type": `text/plain`,
6001
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6002
+ }
6003
+ });
6004
+ const deleteRes = await fetch(`${getBaseUrl()}${forkPath}`, { method: `DELETE` });
6005
+ (0, vitest.expect)(deleteRes.status).toBe(204);
6006
+ const forkRead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `GET` });
6007
+ (0, vitest.expect)(forkRead.status).toBe(404);
6008
+ const sourceRead = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `GET` });
6009
+ (0, vitest.expect)(sourceRead.status).toBe(200);
6010
+ const body = await sourceRead.text();
6011
+ (0, vitest.expect)(body).toBe(`source data`);
6012
+ });
6013
+ (0, vitest.test)(`should soft-delete source while fork exists — fork still reads`, async () => {
6014
+ const id = uniqueId();
6015
+ const sourcePath = `/v1/stream/fork-del-soft-src-${id}`;
6016
+ const forkPath = `/v1/stream/fork-del-soft-fork-${id}`;
6017
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6018
+ method: `PUT`,
6019
+ headers: { "Content-Type": `text/plain` },
6020
+ body: `preserved data`
6021
+ });
6022
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6023
+ method: `PUT`,
6024
+ headers: {
6025
+ "Content-Type": `text/plain`,
6026
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6027
+ }
6028
+ });
6029
+ const deleteRes = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6030
+ (0, vitest.expect)(deleteRes.status).toBe(204);
6031
+ const sourceHead = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `HEAD` });
6032
+ (0, vitest.expect)(sourceHead.status).toBe(410);
6033
+ const forkRead = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
6034
+ (0, vitest.expect)(forkRead.status).toBe(200);
6035
+ const body = await forkRead.text();
6036
+ (0, vitest.expect)(body).toBe(`preserved data`);
6037
+ });
6038
+ (0, vitest.test)(`should block re-creation of soft-deleted source (PUT returns 409)`, async () => {
6039
+ const id = uniqueId();
6040
+ const sourcePath = `/v1/stream/fork-del-block-recreate-src-${id}`;
6041
+ const forkPath = `/v1/stream/fork-del-block-recreate-fork-${id}`;
6042
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6043
+ method: `PUT`,
6044
+ headers: { "Content-Type": `text/plain` },
6045
+ body: `original`
6046
+ });
6047
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6048
+ method: `PUT`,
6049
+ headers: {
6050
+ "Content-Type": `text/plain`,
6051
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6052
+ }
6053
+ });
6054
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6055
+ const recreateRes = await fetch(`${getBaseUrl()}${sourcePath}`, {
6056
+ method: `PUT`,
6057
+ headers: { "Content-Type": `text/plain` }
6058
+ });
6059
+ (0, vitest.expect)(recreateRes.status).toBe(409);
6060
+ });
6061
+ (0, vitest.test)(`should return 410 for GET on soft-deleted source`, async () => {
6062
+ const id = uniqueId();
6063
+ const sourcePath = `/v1/stream/fork-del-soft-get-${id}`;
6064
+ const forkPath = `/v1/stream/fork-del-soft-get-fork-${id}`;
6065
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6066
+ method: `PUT`,
6067
+ headers: { "Content-Type": `text/plain` },
6068
+ body: `data`
6069
+ });
6070
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6071
+ method: `PUT`,
6072
+ headers: {
6073
+ "Content-Type": `text/plain`,
6074
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6075
+ }
6076
+ });
6077
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6078
+ const getRes = await fetch(`${getBaseUrl()}${sourcePath}?offset=-1`);
6079
+ (0, vitest.expect)(getRes.status).toBe(410);
6080
+ });
6081
+ (0, vitest.test)(`should return 410 for POST on soft-deleted source`, async () => {
6082
+ const id = uniqueId();
6083
+ const sourcePath = `/v1/stream/fork-del-soft-post-${id}`;
6084
+ const forkPath = `/v1/stream/fork-del-soft-post-fork-${id}`;
6085
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6086
+ method: `PUT`,
6087
+ headers: { "Content-Type": `text/plain` },
6088
+ body: `data`
6089
+ });
6090
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6091
+ method: `PUT`,
6092
+ headers: {
6093
+ "Content-Type": `text/plain`,
6094
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6095
+ }
6096
+ });
6097
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6098
+ const postRes = await fetch(`${getBaseUrl()}${sourcePath}`, {
6099
+ method: `POST`,
6100
+ headers: { "Content-Type": `text/plain` },
6101
+ body: `more data`
6102
+ });
6103
+ (0, vitest.expect)(postRes.status).toBe(410);
6104
+ });
6105
+ (0, vitest.test)(`should return 410 for DELETE on soft-deleted source`, async () => {
6106
+ const id = uniqueId();
6107
+ const sourcePath = `/v1/stream/fork-del-soft-del-${id}`;
6108
+ const forkPath = `/v1/stream/fork-del-soft-del-fork-${id}`;
6109
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6110
+ method: `PUT`,
6111
+ headers: { "Content-Type": `text/plain` },
6112
+ body: `data`
6113
+ });
6114
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6115
+ method: `PUT`,
6116
+ headers: {
6117
+ "Content-Type": `text/plain`,
6118
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6119
+ }
6120
+ });
6121
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6122
+ const deleteRes = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6123
+ (0, vitest.expect)(deleteRes.status).toBe(410);
6124
+ });
6125
+ (0, vitest.test)(`should return 409 for fork from soft-deleted source`, async () => {
6126
+ const id = uniqueId();
6127
+ const sourcePath = `/v1/stream/fork-del-soft-refork-${id}`;
6128
+ const forkPath = `/v1/stream/fork-del-soft-refork-fork1-${id}`;
6129
+ const fork2Path = `/v1/stream/fork-del-soft-refork-fork2-${id}`;
6130
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6131
+ method: `PUT`,
6132
+ headers: { "Content-Type": `text/plain` },
6133
+ body: `data`
6134
+ });
6135
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6136
+ method: `PUT`,
6137
+ headers: {
6138
+ "Content-Type": `text/plain`,
6139
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6140
+ }
6141
+ });
6142
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6143
+ const fork2Res = await fetch(`${getBaseUrl()}${fork2Path}`, {
6144
+ method: `PUT`,
6145
+ headers: {
6146
+ "Content-Type": `text/plain`,
6147
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6148
+ }
6149
+ });
6150
+ (0, vitest.expect)(fork2Res.status).toBe(409);
6151
+ });
6152
+ (0, vitest.test)(`should return 409 for fork with content-type mismatch`, async () => {
6153
+ const id = uniqueId();
6154
+ const sourcePath = `/v1/stream/fork-ct-mismatch-src-${id}`;
6155
+ const forkPath = `/v1/stream/fork-ct-mismatch-fork-${id}`;
6156
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6157
+ method: `PUT`,
6158
+ headers: { "Content-Type": `text/plain` },
6159
+ body: `data`
6160
+ });
6161
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6162
+ method: `PUT`,
6163
+ headers: {
6164
+ "Content-Type": `application/json`,
6165
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6166
+ }
6167
+ });
6168
+ (0, vitest.expect)(forkRes.status).toBe(409);
6169
+ });
6170
+ (0, vitest.test)(`should cascade GC when last fork is deleted`, async () => {
6171
+ const id = uniqueId();
6172
+ const sourcePath = `/v1/stream/fork-del-cascade-src-${id}`;
6173
+ const forkPath = `/v1/stream/fork-del-cascade-fork-${id}`;
6174
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6175
+ method: `PUT`,
6176
+ headers: { "Content-Type": `text/plain` },
6177
+ body: `cascade data`
6178
+ });
6179
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6180
+ method: `PUT`,
6181
+ headers: {
6182
+ "Content-Type": `text/plain`,
6183
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6184
+ }
6185
+ });
6186
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6187
+ const sourceHead1 = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `HEAD` });
6188
+ (0, vitest.expect)(sourceHead1.status).toBe(410);
6189
+ const deleteFork = await fetch(`${getBaseUrl()}${forkPath}`, { method: `DELETE` });
6190
+ (0, vitest.expect)(deleteFork.status).toBe(204);
6191
+ await waitForStatus(`${getBaseUrl()}${sourcePath}`, 404);
6192
+ });
6193
+ (0, vitest.test)(`should cascade GC through three levels`, async () => {
6194
+ const id = uniqueId();
6195
+ const level0 = `/v1/stream/fork-del-cascade3-l0-${id}`;
6196
+ const level1 = `/v1/stream/fork-del-cascade3-l1-${id}`;
6197
+ const level2 = `/v1/stream/fork-del-cascade3-l2-${id}`;
6198
+ await fetch(`${getBaseUrl()}${level0}`, {
6199
+ method: `PUT`,
6200
+ headers: { "Content-Type": `text/plain` },
6201
+ body: `root`
6202
+ });
6203
+ await fetch(`${getBaseUrl()}${level1}`, {
6204
+ method: `PUT`,
6205
+ headers: {
6206
+ "Content-Type": `text/plain`,
6207
+ [STREAM_FORKED_FROM_HEADER]: level0
6208
+ }
6209
+ });
6210
+ await fetch(`${getBaseUrl()}${level2}`, {
6211
+ method: `PUT`,
6212
+ headers: {
6213
+ "Content-Type": `text/plain`,
6214
+ [STREAM_FORKED_FROM_HEADER]: level1
6215
+ }
6216
+ });
6217
+ await fetch(`${getBaseUrl()}${level0}`, { method: `DELETE` });
6218
+ await fetch(`${getBaseUrl()}${level1}`, { method: `DELETE` });
6219
+ (0, vitest.expect)((await fetch(`${getBaseUrl()}${level0}`, { method: `HEAD` })).status).toBe(410);
6220
+ (0, vitest.expect)((await fetch(`${getBaseUrl()}${level1}`, { method: `HEAD` })).status).toBe(410);
6221
+ const deleteLevel2 = await fetch(`${getBaseUrl()}${level2}`, { method: `DELETE` });
6222
+ (0, vitest.expect)(deleteLevel2.status).toBe(204);
6223
+ (0, vitest.expect)((await fetch(`${getBaseUrl()}${level2}`, { method: `HEAD` })).status).toBe(404);
6224
+ await waitForStatus(`${getBaseUrl()}${level1}`, 404);
6225
+ await waitForStatus(`${getBaseUrl()}${level0}`, 404);
6226
+ });
6227
+ (0, vitest.test)(`should preserve data when deleting middle of chain`, async () => {
6228
+ const id = uniqueId();
6229
+ const level0 = `/v1/stream/fork-del-middle-l0-${id}`;
6230
+ const level1 = `/v1/stream/fork-del-middle-l1-${id}`;
6231
+ const level2 = `/v1/stream/fork-del-middle-l2-${id}`;
6232
+ await fetch(`${getBaseUrl()}${level0}`, {
6233
+ method: `PUT`,
6234
+ headers: { "Content-Type": `text/plain` },
6235
+ body: `A`
6236
+ });
6237
+ await fetch(`${getBaseUrl()}${level1}`, {
6238
+ method: `PUT`,
6239
+ headers: {
6240
+ "Content-Type": `text/plain`,
6241
+ [STREAM_FORKED_FROM_HEADER]: level0
6242
+ }
6243
+ });
6244
+ await fetch(`${getBaseUrl()}${level1}`, {
6245
+ method: `POST`,
6246
+ headers: { "Content-Type": `text/plain` },
6247
+ body: `B`
6248
+ });
6249
+ await fetch(`${getBaseUrl()}${level2}`, {
6250
+ method: `PUT`,
6251
+ headers: {
6252
+ "Content-Type": `text/plain`,
6253
+ [STREAM_FORKED_FROM_HEADER]: level1
6254
+ }
6255
+ });
6256
+ await fetch(`${getBaseUrl()}${level2}`, {
6257
+ method: `POST`,
6258
+ headers: { "Content-Type": `text/plain` },
6259
+ body: `C`
6260
+ });
6261
+ await fetch(`${getBaseUrl()}${level1}`, { method: `DELETE` });
6262
+ (0, vitest.expect)((await fetch(`${getBaseUrl()}${level1}`, { method: `HEAD` })).status).toBe(410);
6263
+ const readRes = await fetch(`${getBaseUrl()}${level2}?offset=-1`);
6264
+ (0, vitest.expect)(readRes.status).toBe(200);
6265
+ const body = await readRes.text();
6266
+ (0, vitest.expect)(body).toBe(`ABC`);
6267
+ const l0Read = await fetch(`${getBaseUrl()}${level0}?offset=-1`);
6268
+ (0, vitest.expect)(l0Read.status).toBe(200);
6269
+ (0, vitest.expect)(await l0Read.text()).toBe(`A`);
6270
+ });
6271
+ (0, vitest.test)(`should keep source alive when all forks are deleted`, async () => {
6272
+ const id = uniqueId();
6273
+ const sourcePath = `/v1/stream/fork-del-allgone-src-${id}`;
6274
+ const fork1Path = `/v1/stream/fork-del-allgone-f1-${id}`;
6275
+ const fork2Path = `/v1/stream/fork-del-allgone-f2-${id}`;
6276
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6277
+ method: `PUT`,
6278
+ headers: { "Content-Type": `text/plain` },
6279
+ body: `alive`
6280
+ });
6281
+ await fetch(`${getBaseUrl()}${fork1Path}`, {
6282
+ method: `PUT`,
6283
+ headers: {
6284
+ "Content-Type": `text/plain`,
6285
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6286
+ }
6287
+ });
6288
+ await fetch(`${getBaseUrl()}${fork2Path}`, {
6289
+ method: `PUT`,
6290
+ headers: {
6291
+ "Content-Type": `text/plain`,
6292
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6293
+ }
6294
+ });
6295
+ await fetch(`${getBaseUrl()}${fork1Path}`, { method: `DELETE` });
6296
+ await fetch(`${getBaseUrl()}${fork2Path}`, { method: `DELETE` });
6297
+ const sourceRead = await fetch(`${getBaseUrl()}${sourcePath}?offset=-1`);
6298
+ (0, vitest.expect)(sourceRead.status).toBe(200);
6299
+ const body = await sourceRead.text();
6300
+ (0, vitest.expect)(body).toBe(`alive`);
6301
+ });
6302
+ });
6303
+ (0, vitest.describe)(`Fork - TTL and Expiry`, () => {
6304
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
6305
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
6306
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6307
+ const waitForDeletion = async (url, initialSleepMs, expectedStatuses = [404], timeoutMs = 5e3) => {
6308
+ await sleep(initialSleepMs);
6309
+ await vitest.vi.waitFor(async () => {
6310
+ const head = await fetch(url, { method: `HEAD` });
6311
+ (0, vitest.expect)(expectedStatuses).toContain(head.status);
6312
+ }, {
6313
+ timeout: timeoutMs,
6314
+ interval: 200
6315
+ });
6316
+ };
6317
+ (0, vitest.test)(`should inherit source expiry when none specified`, async () => {
6318
+ const id = uniqueId();
6319
+ const sourcePath = `/v1/stream/fork-ttl-inherit-src-${id}`;
6320
+ const forkPath = `/v1/stream/fork-ttl-inherit-fork-${id}`;
6321
+ const expiresAt = new Date(Date.now() + 36e5).toISOString();
6322
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6323
+ method: `PUT`,
6324
+ headers: {
6325
+ "Content-Type": `text/plain`,
6326
+ "Stream-Expires-At": expiresAt
6327
+ },
6328
+ body: `data`
6329
+ });
6330
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6331
+ method: `PUT`,
6332
+ headers: {
6333
+ "Content-Type": `text/plain`,
6334
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6335
+ }
6336
+ });
6337
+ (0, vitest.expect)(forkRes.status).toBe(201);
6338
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6339
+ (0, vitest.expect)(forkHead.status).toBe(200);
6340
+ const forkExpires = forkHead.headers.get(`Stream-Expires-At`);
6341
+ if (forkExpires) (0, vitest.expect)(new Date(forkExpires).getTime()).toBeLessThanOrEqual(new Date(expiresAt).getTime());
6342
+ });
6343
+ (0, vitest.test)(`should allow fork with shorter TTL`, async () => {
6344
+ const id = uniqueId();
6345
+ const sourcePath = `/v1/stream/fork-ttl-shorter-src-${id}`;
6346
+ const forkPath = `/v1/stream/fork-ttl-shorter-fork-${id}`;
6347
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6348
+ method: `PUT`,
6349
+ headers: {
6350
+ "Content-Type": `text/plain`,
6351
+ "Stream-TTL": `3600`
6352
+ },
6353
+ body: `data`
6354
+ });
6355
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6356
+ method: `PUT`,
6357
+ headers: {
6358
+ "Content-Type": `text/plain`,
6359
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6360
+ "Stream-TTL": `1800`
6361
+ }
6362
+ });
6363
+ (0, vitest.expect)([200, 201]).toContain(forkRes.status);
6364
+ });
6365
+ vitest.test.concurrent(`should expire fork based on TTL (releases refcount)`, async () => {
6366
+ const id = uniqueId();
6367
+ const sourcePath = `/v1/stream/fork-ttl-expire-src-${id}`;
6368
+ const forkPath = `/v1/stream/fork-ttl-expire-fork-${id}`;
6369
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6370
+ method: `PUT`,
6371
+ headers: {
6372
+ "Content-Type": `text/plain`,
6373
+ "Stream-TTL": `60`
6374
+ },
6375
+ body: `data`
6376
+ });
6377
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6378
+ method: `PUT`,
6379
+ headers: {
6380
+ "Content-Type": `text/plain`,
6381
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6382
+ "Stream-TTL": `1`
6383
+ }
6384
+ });
6385
+ const forkHeadBefore = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6386
+ (0, vitest.expect)(forkHeadBefore.status).toBe(200);
6387
+ await waitForDeletion(`${getBaseUrl()}${forkPath}`, 1e3);
6388
+ const forkGetAfter = await fetch(`${getBaseUrl()}${forkPath}`, { method: `GET` });
6389
+ (0, vitest.expect)(forkGetAfter.status).toBe(404);
6390
+ });
6391
+ vitest.test.concurrent(`should expire source with living forks (source goes 410)`, async () => {
6392
+ const id = uniqueId();
6393
+ const sourcePath = `/v1/stream/fork-ttl-src-expire-src-${id}`;
6394
+ const forkPath = `/v1/stream/fork-ttl-src-expire-fork-${id}`;
6395
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6396
+ method: `PUT`,
6397
+ headers: {
6398
+ "Content-Type": `text/plain`,
6399
+ "Stream-TTL": `1`
6400
+ },
6401
+ body: `data`
6402
+ });
6403
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6404
+ method: `PUT`,
6405
+ headers: {
6406
+ "Content-Type": `text/plain`,
6407
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6408
+ }
6409
+ });
6410
+ await waitForDeletion(`${getBaseUrl()}${sourcePath}`, 1e3, [404, 410]);
6411
+ const sourceGet = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `GET` });
6412
+ (0, vitest.expect)([404, 410]).toContain(sourceGet.status);
6413
+ await waitForDeletion(`${getBaseUrl()}${forkPath}`, 0, [404, 410]);
6414
+ const forkGet = await fetch(`${getBaseUrl()}${forkPath}`, { method: `GET` });
6415
+ (0, vitest.expect)([404, 410]).toContain(forkGet.status);
6416
+ });
6417
+ (0, vitest.test)(`should inherit source TTL value when none specified`, async () => {
6418
+ const id = uniqueId();
6419
+ const sourcePath = `/v1/stream/fork-ttl-inherit-ttl-src-${id}`;
6420
+ const forkPath = `/v1/stream/fork-ttl-inherit-ttl-fork-${id}`;
6421
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6422
+ method: `PUT`,
6423
+ headers: {
6424
+ "Content-Type": `text/plain`,
6425
+ "Stream-TTL": `3600`
6426
+ },
6427
+ body: `data`
6428
+ });
6429
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6430
+ method: `PUT`,
6431
+ headers: {
6432
+ "Content-Type": `text/plain`,
6433
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6434
+ }
6435
+ });
6436
+ (0, vitest.expect)(forkRes.status).toBe(201);
6437
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6438
+ (0, vitest.expect)(forkHead.status).toBe(200);
6439
+ const forkTTL = forkHead.headers.get(`Stream-TTL`);
6440
+ (0, vitest.expect)(forkTTL).toBe(`3600`);
6441
+ });
6442
+ (0, vitest.test)(`should use fork's own TTL when specified`, async () => {
6443
+ const id = uniqueId();
6444
+ const sourcePath = `/v1/stream/fork-own-ttl-src-${id}`;
6445
+ const forkPath = `/v1/stream/fork-own-ttl-fork-${id}`;
6446
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6447
+ method: `PUT`,
6448
+ headers: {
6449
+ "Content-Type": `text/plain`,
6450
+ "Stream-TTL": `3600`
6451
+ },
6452
+ body: `data`
6453
+ });
6454
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6455
+ method: `PUT`,
6456
+ headers: {
6457
+ "Content-Type": `text/plain`,
6458
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6459
+ "Stream-TTL": `7200`
6460
+ }
6461
+ });
6462
+ (0, vitest.expect)(forkRes.status).toBe(201);
6463
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6464
+ (0, vitest.expect)(forkHead.status).toBe(200);
6465
+ const forkTTL = forkHead.headers.get(`Stream-TTL`);
6466
+ (0, vitest.expect)(forkTTL).toBe(`7200`);
6467
+ });
6468
+ vitest.test.concurrent(`should allow fork to outlive source via TTL renewal`, async () => {
6469
+ const id = uniqueId();
6470
+ const sourcePath = `/v1/stream/fork-outlive-src-${id}`;
6471
+ const forkPath = `/v1/stream/fork-outlive-fork-${id}`;
6472
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6473
+ method: `PUT`,
6474
+ headers: {
6475
+ "Content-Type": `text/plain`,
6476
+ "Stream-TTL": `2`
6477
+ },
6478
+ body: `source data`
6479
+ });
6480
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6481
+ method: `PUT`,
6482
+ headers: {
6483
+ "Content-Type": `text/plain`,
6484
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6485
+ "Stream-TTL": `2`
6486
+ }
6487
+ });
6488
+ (0, vitest.expect)(forkRes.status).toBe(201);
6489
+ await sleep(1500);
6490
+ const forkRead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `GET` });
6491
+ (0, vitest.expect)(forkRead.status).toBe(200);
6492
+ await waitForDeletion(`${getBaseUrl()}${sourcePath}`, 500, [404, 410]);
6493
+ const sourceGet = await fetch(`${getBaseUrl()}${sourcePath}`, { method: `GET` });
6494
+ (0, vitest.expect)([404, 410]).toContain(sourceGet.status);
6495
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6496
+ (0, vitest.expect)(forkHead.status).toBe(200);
6497
+ });
6498
+ (0, vitest.test)(`should allow fork Expires-At beyond source TTL expiry`, async () => {
6499
+ const id = uniqueId();
6500
+ const sourcePath = `/v1/stream/fork-expires-beyond-src-${id}`;
6501
+ const forkPath = `/v1/stream/fork-expires-beyond-fork-${id}`;
6502
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6503
+ method: `PUT`,
6504
+ headers: {
6505
+ "Content-Type": `text/plain`,
6506
+ "Stream-TTL": `10`
6507
+ },
6508
+ body: `data`
6509
+ });
6510
+ const farFuture = new Date(Date.now() + 36e5).toISOString();
6511
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6512
+ method: `PUT`,
6513
+ headers: {
6514
+ "Content-Type": `text/plain`,
6515
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6516
+ "Stream-Expires-At": farFuture
6517
+ }
6518
+ });
6519
+ (0, vitest.expect)(forkRes.status).toBe(201);
6520
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6521
+ (0, vitest.expect)(forkHead.status).toBe(200);
6522
+ const forkExpiresAt = forkHead.headers.get(`Stream-Expires-At`);
6523
+ if (forkExpiresAt) (0, vitest.expect)(new Date(forkExpiresAt).getTime()).toBeGreaterThan(Date.now() + 35e5);
6524
+ });
6525
+ (0, vitest.test)(`should allow fork TTL longer than source TTL (no capping)`, async () => {
6526
+ const id = uniqueId();
6527
+ const sourcePath = `/v1/stream/fork-ttl-nocap-src-${id}`;
6528
+ const forkPath = `/v1/stream/fork-ttl-nocap-fork-${id}`;
6529
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6530
+ method: `PUT`,
6531
+ headers: {
6532
+ "Content-Type": `text/plain`,
6533
+ "Stream-TTL": `10`
6534
+ },
6535
+ body: `data`
6536
+ });
6537
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6538
+ method: `PUT`,
6539
+ headers: {
6540
+ "Content-Type": `text/plain`,
6541
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6542
+ "Stream-TTL": `99999`
6543
+ }
6544
+ });
6545
+ (0, vitest.expect)([200, 201]).toContain(forkRes.status);
6546
+ const forkHead = await fetch(`${getBaseUrl()}${forkPath}`, { method: `HEAD` });
6547
+ (0, vitest.expect)(forkHead.status).toBe(200);
6548
+ const forkTTL = forkHead.headers.get(`Stream-TTL`);
6549
+ (0, vitest.expect)(forkTTL).toBe(`99999`);
6550
+ });
6551
+ });
6552
+ (0, vitest.describe)(`Fork - JSON Mode`, () => {
6553
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
6554
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
6555
+ (0, vitest.test)(`should fork a JSON stream`, async () => {
6556
+ const id = uniqueId();
6557
+ const sourcePath = `/v1/stream/fork-json-src-${id}`;
6558
+ const forkPath = `/v1/stream/fork-json-fork-${id}`;
6559
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6560
+ method: `PUT`,
6561
+ headers: { "Content-Type": `application/json` },
6562
+ body: `[{"event":"one"}]`
6563
+ });
6564
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6565
+ method: `PUT`,
6566
+ headers: {
6567
+ "Content-Type": `application/json`,
6568
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6569
+ }
6570
+ });
6571
+ (0, vitest.expect)(forkRes.status).toBe(201);
6572
+ (0, vitest.expect)(forkRes.headers.get(`content-type`)).toBe(`application/json`);
6573
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
6574
+ (0, vitest.expect)(readRes.status).toBe(200);
6575
+ (0, vitest.expect)(readRes.headers.get(`content-type`)).toBe(`application/json`);
6576
+ const body = JSON.parse(await readRes.text());
6577
+ (0, vitest.expect)(Array.isArray(body)).toBe(true);
6578
+ (0, vitest.expect)(body).toEqual([{ event: `one` }]);
6579
+ });
6580
+ (0, vitest.test)(`should read forked JSON across boundary`, async () => {
6581
+ const id = uniqueId();
6582
+ const sourcePath = `/v1/stream/fork-json-boundary-src-${id}`;
6583
+ const forkPath = `/v1/stream/fork-json-boundary-fork-${id}`;
6584
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6585
+ method: `PUT`,
6586
+ headers: { "Content-Type": `application/json` },
6587
+ body: `[{"from":"source"}]`
6588
+ });
6589
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6590
+ method: `PUT`,
6591
+ headers: {
6592
+ "Content-Type": `application/json`,
6593
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6594
+ }
6595
+ });
6596
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6597
+ method: `POST`,
6598
+ headers: { "Content-Type": `application/json` },
6599
+ body: `[{"from":"fork"}]`
6600
+ });
6601
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
6602
+ (0, vitest.expect)(readRes.status).toBe(200);
6603
+ const body = JSON.parse(await readRes.text());
6604
+ (0, vitest.expect)(Array.isArray(body)).toBe(true);
6605
+ (0, vitest.expect)(body).toEqual([{ from: `source` }, { from: `fork` }]);
6606
+ });
6607
+ });
6608
+ (0, vitest.describe)(`Fork - Edge Cases`, () => {
6609
+ const STREAM_FORKED_FROM_HEADER = `Stream-Forked-From`;
6610
+ const STREAM_FORK_OFFSET_HEADER = `Stream-Fork-Offset`;
6611
+ const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
6612
+ (0, vitest.test)(`should handle fork then immediately delete source`, async () => {
6613
+ const id = uniqueId();
6614
+ const sourcePath = `/v1/stream/fork-edge-imm-del-src-${id}`;
6615
+ const forkPath = `/v1/stream/fork-edge-imm-del-fork-${id}`;
6616
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6617
+ method: `PUT`,
6618
+ headers: { "Content-Type": `text/plain` },
6619
+ body: `ephemeral`
6620
+ });
6621
+ await fetch(`${getBaseUrl()}${forkPath}`, {
6622
+ method: `PUT`,
6623
+ headers: {
6624
+ "Content-Type": `text/plain`,
6625
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6626
+ }
6627
+ });
6628
+ await fetch(`${getBaseUrl()}${sourcePath}`, { method: `DELETE` });
6629
+ const readRes = await fetch(`${getBaseUrl()}${forkPath}?offset=-1`);
6630
+ (0, vitest.expect)(readRes.status).toBe(200);
6631
+ const body = await readRes.text();
6632
+ (0, vitest.expect)(body).toBe(`ephemeral`);
6633
+ });
6634
+ (0, vitest.test)(`should handle many forks of same stream (10 forks)`, async () => {
6635
+ const id = uniqueId();
6636
+ const sourcePath = `/v1/stream/fork-edge-many-src-${id}`;
6637
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6638
+ method: `PUT`,
6639
+ headers: { "Content-Type": `text/plain` },
6640
+ body: `shared data`
6641
+ });
6642
+ const forkPaths = [];
6643
+ for (let i = 0; i < 10; i++) {
6644
+ const forkPath = `/v1/stream/fork-edge-many-f${i}-${id}`;
6645
+ const forkRes = await fetch(`${getBaseUrl()}${forkPath}`, {
6646
+ method: `PUT`,
6647
+ headers: {
6648
+ "Content-Type": `text/plain`,
6649
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6650
+ }
6651
+ });
6652
+ (0, vitest.expect)(forkRes.status).toBe(201);
6653
+ forkPaths.push(forkPath);
6654
+ }
6655
+ for (const fp of forkPaths) {
6656
+ const readRes = await fetch(`${getBaseUrl()}${fp}?offset=-1`);
6657
+ (0, vitest.expect)(readRes.status).toBe(200);
6658
+ const body = await readRes.text();
6659
+ (0, vitest.expect)(body).toBe(`shared data`);
6660
+ }
6661
+ for (const fp of forkPaths) await fetch(`${getBaseUrl()}${fp}`, { method: `DELETE` });
6662
+ });
6663
+ (0, vitest.test)(`should fork at every offset position`, async () => {
6664
+ const id = uniqueId();
6665
+ const sourcePath = `/v1/stream/fork-edge-every-offset-src-${id}`;
6666
+ const createRes = await fetch(`${getBaseUrl()}${sourcePath}`, {
6667
+ method: `PUT`,
6668
+ headers: { "Content-Type": `text/plain` },
6669
+ body: `A`
6670
+ });
6671
+ const offset0 = `0000000000000000_0000000000000000`;
6672
+ const offset1 = createRes.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
6673
+ const append1 = await fetch(`${getBaseUrl()}${sourcePath}`, {
6674
+ method: `POST`,
6675
+ headers: { "Content-Type": `text/plain` },
6676
+ body: `B`
6677
+ });
6678
+ const offset2 = append1.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
6679
+ const append2 = await fetch(`${getBaseUrl()}${sourcePath}`, {
6680
+ method: `POST`,
6681
+ headers: { "Content-Type": `text/plain` },
6682
+ body: `C`
6683
+ });
6684
+ const offset3 = append2.headers.get(__durable_streams_client.STREAM_OFFSET_HEADER);
6685
+ const f0 = `/v1/stream/fork-edge-every-f0-${id}`;
6686
+ const f0Res = await fetch(`${getBaseUrl()}${f0}`, {
6687
+ method: `PUT`,
6688
+ headers: {
6689
+ "Content-Type": `text/plain`,
6690
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6691
+ [STREAM_FORK_OFFSET_HEADER]: offset0
6692
+ }
6693
+ });
6694
+ (0, vitest.expect)(f0Res.status).toBe(201);
6695
+ const f0Body = await (await fetch(`${getBaseUrl()}${f0}?offset=-1`)).text();
6696
+ (0, vitest.expect)(f0Body).toBe(``);
6697
+ const f1 = `/v1/stream/fork-edge-every-f1-${id}`;
6698
+ await fetch(`${getBaseUrl()}${f1}`, {
6699
+ method: `PUT`,
6700
+ headers: {
6701
+ "Content-Type": `text/plain`,
6702
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6703
+ [STREAM_FORK_OFFSET_HEADER]: offset1
6704
+ }
6705
+ });
6706
+ const f1Body = await (await fetch(`${getBaseUrl()}${f1}?offset=-1`)).text();
6707
+ (0, vitest.expect)(f1Body).toBe(`A`);
6708
+ const f2 = `/v1/stream/fork-edge-every-f2-${id}`;
6709
+ await fetch(`${getBaseUrl()}${f2}`, {
6710
+ method: `PUT`,
6711
+ headers: {
6712
+ "Content-Type": `text/plain`,
6713
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6714
+ [STREAM_FORK_OFFSET_HEADER]: offset2
6715
+ }
6716
+ });
6717
+ const f2Body = await (await fetch(`${getBaseUrl()}${f2}?offset=-1`)).text();
6718
+ (0, vitest.expect)(f2Body).toBe(`AB`);
6719
+ const f3 = `/v1/stream/fork-edge-every-f3-${id}`;
6720
+ await fetch(`${getBaseUrl()}${f3}`, {
6721
+ method: `PUT`,
6722
+ headers: {
6723
+ "Content-Type": `text/plain`,
6724
+ [STREAM_FORKED_FROM_HEADER]: sourcePath,
6725
+ [STREAM_FORK_OFFSET_HEADER]: offset3
6726
+ }
6727
+ });
6728
+ const f3Body = await (await fetch(`${getBaseUrl()}${f3}?offset=-1`)).text();
6729
+ (0, vitest.expect)(f3Body).toBe(`ABC`);
6730
+ });
6731
+ (0, vitest.test)(`should handle idempotent fork creation (PUT twice)`, async () => {
6732
+ const id = uniqueId();
6733
+ const sourcePath = `/v1/stream/fork-edge-idempotent-src-${id}`;
6734
+ const forkPath = `/v1/stream/fork-edge-idempotent-fork-${id}`;
6735
+ await fetch(`${getBaseUrl()}${sourcePath}`, {
6736
+ method: `PUT`,
6737
+ headers: { "Content-Type": `text/plain` },
6738
+ body: `data`
6739
+ });
6740
+ const fork1 = await fetch(`${getBaseUrl()}${forkPath}`, {
6741
+ method: `PUT`,
6742
+ headers: {
6743
+ "Content-Type": `text/plain`,
6744
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6745
+ }
6746
+ });
6747
+ (0, vitest.expect)(fork1.status).toBe(201);
6748
+ const fork2 = await fetch(`${getBaseUrl()}${forkPath}`, {
6749
+ method: `PUT`,
6750
+ headers: {
6751
+ "Content-Type": `text/plain`,
6752
+ [STREAM_FORKED_FROM_HEADER]: sourcePath
6753
+ }
6754
+ });
6755
+ (0, vitest.expect)(fork2.status).toBe(200);
6756
+ });
6757
+ });
5025
6758
  }
5026
6759
 
5027
6760
  //#endregion