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