@naeemo/capnp 0.3.0 → 0.4.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/README.md +1 -0
- package/dist/index.cjs +2710 -776
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2674 -766
- package/dist/index.js.map +1 -1
- package/dist/rpc-connection-BKWQQ7f9.js +960 -0
- package/dist/rpc-connection-BKWQQ7f9.js.map +1 -0
- package/dist/rpc-connection-C2C1wyga.js +3 -0
- package/dist/rpc-connection-Dz3rYT1P.js +870 -0
- package/dist/rpc-connection-Dz3rYT1P.js.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_rpc_connection = require('./rpc-connection-BKWQQ7f9.js');
|
|
2
3
|
|
|
3
4
|
//#region src/core/pointer.ts
|
|
4
5
|
/**
|
|
@@ -1693,771 +1694,6 @@ var WebSocketTransport = class WebSocketTransport {
|
|
|
1693
1694
|
}
|
|
1694
1695
|
};
|
|
1695
1696
|
|
|
1696
|
-
//#endregion
|
|
1697
|
-
//#region src/rpc/four-tables.ts
|
|
1698
|
-
/** Manages the question table for outbound calls */
|
|
1699
|
-
var QuestionTable = class {
|
|
1700
|
-
questions = /* @__PURE__ */ new Map();
|
|
1701
|
-
nextId = 1;
|
|
1702
|
-
/** Create a new question entry */
|
|
1703
|
-
create() {
|
|
1704
|
-
const id = this.allocateId();
|
|
1705
|
-
let resolveCompletion;
|
|
1706
|
-
let rejectCompletion;
|
|
1707
|
-
const question = {
|
|
1708
|
-
id,
|
|
1709
|
-
isComplete: false,
|
|
1710
|
-
finishSent: false,
|
|
1711
|
-
completionPromise: new Promise((resolve, reject) => {
|
|
1712
|
-
resolveCompletion = resolve;
|
|
1713
|
-
rejectCompletion = reject;
|
|
1714
|
-
}),
|
|
1715
|
-
resolveCompletion,
|
|
1716
|
-
rejectCompletion
|
|
1717
|
-
};
|
|
1718
|
-
this.questions.set(id, question);
|
|
1719
|
-
return question;
|
|
1720
|
-
}
|
|
1721
|
-
/** Get a question by ID */
|
|
1722
|
-
get(id) {
|
|
1723
|
-
return this.questions.get(id);
|
|
1724
|
-
}
|
|
1725
|
-
/** Mark a question as complete */
|
|
1726
|
-
complete(id, result) {
|
|
1727
|
-
const question = this.questions.get(id);
|
|
1728
|
-
if (question && !question.isComplete) {
|
|
1729
|
-
question.isComplete = true;
|
|
1730
|
-
question.resolveCompletion(result);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
/** Mark a question as canceled */
|
|
1734
|
-
cancel(id, error) {
|
|
1735
|
-
const question = this.questions.get(id);
|
|
1736
|
-
if (question && !question.isComplete) {
|
|
1737
|
-
question.isComplete = true;
|
|
1738
|
-
question.rejectCompletion(error);
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
/** Mark that Finish has been sent for a question */
|
|
1742
|
-
markFinishSent(id) {
|
|
1743
|
-
const question = this.questions.get(id);
|
|
1744
|
-
if (question) question.finishSent = true;
|
|
1745
|
-
}
|
|
1746
|
-
/** Remove a question from the table (when both sides are done) */
|
|
1747
|
-
remove(id) {
|
|
1748
|
-
const question = this.questions.get(id);
|
|
1749
|
-
if (question?.isComplete && question.finishSent) this.questions.delete(id);
|
|
1750
|
-
}
|
|
1751
|
-
/** Clean up all questions (e.g., on disconnect) */
|
|
1752
|
-
clear() {
|
|
1753
|
-
for (const question of this.questions.values()) if (!question.isComplete) question.rejectCompletion(/* @__PURE__ */ new Error("Connection closed"));
|
|
1754
|
-
this.questions.clear();
|
|
1755
|
-
this.nextId = 1;
|
|
1756
|
-
}
|
|
1757
|
-
allocateId() {
|
|
1758
|
-
return this.nextId++;
|
|
1759
|
-
}
|
|
1760
|
-
};
|
|
1761
|
-
/** Manages the answer table for inbound calls */
|
|
1762
|
-
var AnswerTable = class {
|
|
1763
|
-
answers = /* @__PURE__ */ new Map();
|
|
1764
|
-
/** Create a new answer entry */
|
|
1765
|
-
create(id) {
|
|
1766
|
-
const answer = {
|
|
1767
|
-
id,
|
|
1768
|
-
isComplete: false,
|
|
1769
|
-
returnSent: false,
|
|
1770
|
-
finishReceived: false
|
|
1771
|
-
};
|
|
1772
|
-
this.answers.set(id, answer);
|
|
1773
|
-
return answer;
|
|
1774
|
-
}
|
|
1775
|
-
/** Get an answer by ID */
|
|
1776
|
-
get(id) {
|
|
1777
|
-
return this.answers.get(id);
|
|
1778
|
-
}
|
|
1779
|
-
/** Mark that Return has been sent */
|
|
1780
|
-
markReturnSent(id) {
|
|
1781
|
-
const answer = this.answers.get(id);
|
|
1782
|
-
if (answer) answer.returnSent = true;
|
|
1783
|
-
}
|
|
1784
|
-
/** Mark that Finish has been received */
|
|
1785
|
-
markFinishReceived(id) {
|
|
1786
|
-
const answer = this.answers.get(id);
|
|
1787
|
-
if (answer) answer.finishReceived = true;
|
|
1788
|
-
}
|
|
1789
|
-
/** Remove an answer from the table (when both sides are done) */
|
|
1790
|
-
remove(id) {
|
|
1791
|
-
const answer = this.answers.get(id);
|
|
1792
|
-
if (answer?.returnSent && answer.finishReceived) this.answers.delete(id);
|
|
1793
|
-
}
|
|
1794
|
-
/** Clean up all answers (e.g., on disconnect) */
|
|
1795
|
-
clear() {
|
|
1796
|
-
this.answers.clear();
|
|
1797
|
-
}
|
|
1798
|
-
};
|
|
1799
|
-
/** Manages the import table for capabilities received from remote */
|
|
1800
|
-
var ImportTable = class {
|
|
1801
|
-
imports = /* @__PURE__ */ new Map();
|
|
1802
|
-
/** Add a new import */
|
|
1803
|
-
add(id, isPromise) {
|
|
1804
|
-
const importEntry = {
|
|
1805
|
-
id,
|
|
1806
|
-
refCount: 1,
|
|
1807
|
-
isPromise
|
|
1808
|
-
};
|
|
1809
|
-
this.imports.set(id, importEntry);
|
|
1810
|
-
return importEntry;
|
|
1811
|
-
}
|
|
1812
|
-
/** Get an import by ID */
|
|
1813
|
-
get(id) {
|
|
1814
|
-
return this.imports.get(id);
|
|
1815
|
-
}
|
|
1816
|
-
/** Increment reference count */
|
|
1817
|
-
addRef(id) {
|
|
1818
|
-
const importEntry = this.imports.get(id);
|
|
1819
|
-
if (importEntry) importEntry.refCount++;
|
|
1820
|
-
}
|
|
1821
|
-
/** Decrement reference count, returns true if refCount reached 0 */
|
|
1822
|
-
release(id, count) {
|
|
1823
|
-
const importEntry = this.imports.get(id);
|
|
1824
|
-
if (importEntry) {
|
|
1825
|
-
importEntry.refCount -= count;
|
|
1826
|
-
if (importEntry.refCount <= 0) {
|
|
1827
|
-
this.imports.delete(id);
|
|
1828
|
-
return true;
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
return false;
|
|
1832
|
-
}
|
|
1833
|
-
/** Mark a promise as resolved */
|
|
1834
|
-
markResolved(id) {
|
|
1835
|
-
const importEntry = this.imports.get(id);
|
|
1836
|
-
if (importEntry) importEntry.isPromise = false;
|
|
1837
|
-
}
|
|
1838
|
-
/** Clean up all imports (e.g., on disconnect) */
|
|
1839
|
-
clear() {
|
|
1840
|
-
this.imports.clear();
|
|
1841
|
-
}
|
|
1842
|
-
};
|
|
1843
|
-
/** Manages the export table for capabilities sent to remote */
|
|
1844
|
-
var ExportTable = class {
|
|
1845
|
-
exports = /* @__PURE__ */ new Map();
|
|
1846
|
-
nextId = 1;
|
|
1847
|
-
/** Add a new export */
|
|
1848
|
-
add(capability, isPromise) {
|
|
1849
|
-
const id = this.allocateId();
|
|
1850
|
-
const exportEntry = {
|
|
1851
|
-
id,
|
|
1852
|
-
refCount: 1,
|
|
1853
|
-
isPromise,
|
|
1854
|
-
capability
|
|
1855
|
-
};
|
|
1856
|
-
this.exports.set(id, exportEntry);
|
|
1857
|
-
return exportEntry;
|
|
1858
|
-
}
|
|
1859
|
-
/** Get an export by ID */
|
|
1860
|
-
get(id) {
|
|
1861
|
-
return this.exports.get(id);
|
|
1862
|
-
}
|
|
1863
|
-
/** Increment reference count */
|
|
1864
|
-
addRef(id) {
|
|
1865
|
-
const exportEntry = this.exports.get(id);
|
|
1866
|
-
if (exportEntry) exportEntry.refCount++;
|
|
1867
|
-
}
|
|
1868
|
-
/** Decrement reference count, returns true if refCount reached 0 */
|
|
1869
|
-
release(id, count) {
|
|
1870
|
-
const exportEntry = this.exports.get(id);
|
|
1871
|
-
if (exportEntry) {
|
|
1872
|
-
exportEntry.refCount -= count;
|
|
1873
|
-
if (exportEntry.refCount <= 0) {
|
|
1874
|
-
this.exports.delete(id);
|
|
1875
|
-
return true;
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
return false;
|
|
1879
|
-
}
|
|
1880
|
-
/** Mark a promise as resolved */
|
|
1881
|
-
markResolved(id) {
|
|
1882
|
-
const exportEntry = this.exports.get(id);
|
|
1883
|
-
if (exportEntry) exportEntry.isPromise = false;
|
|
1884
|
-
}
|
|
1885
|
-
/** Clean up all exports (e.g., on disconnect) */
|
|
1886
|
-
clear() {
|
|
1887
|
-
this.exports.clear();
|
|
1888
|
-
this.nextId = 1;
|
|
1889
|
-
}
|
|
1890
|
-
allocateId() {
|
|
1891
|
-
return this.nextId++;
|
|
1892
|
-
}
|
|
1893
|
-
};
|
|
1894
|
-
|
|
1895
|
-
//#endregion
|
|
1896
|
-
//#region src/rpc/pipeline.ts
|
|
1897
|
-
/**
|
|
1898
|
-
* Tracks a chain of operations to apply to a promised answer.
|
|
1899
|
-
* This forms the "transform" field in PromisedAnswer.
|
|
1900
|
-
*/
|
|
1901
|
-
var PipelineOpTracker = class PipelineOpTracker {
|
|
1902
|
-
ops = [];
|
|
1903
|
-
/**
|
|
1904
|
-
* Add a no-op (use the result as-is)
|
|
1905
|
-
*/
|
|
1906
|
-
addNoop() {
|
|
1907
|
-
this.ops.push({ type: "noop" });
|
|
1908
|
-
}
|
|
1909
|
-
/**
|
|
1910
|
-
* Add a pointer field access operation
|
|
1911
|
-
*/
|
|
1912
|
-
addGetPointerField(fieldIndex) {
|
|
1913
|
-
this.ops.push({
|
|
1914
|
-
type: "getPointerField",
|
|
1915
|
-
fieldIndex
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
1918
|
-
/**
|
|
1919
|
-
* Get the current transform chain
|
|
1920
|
-
*/
|
|
1921
|
-
getTransform() {
|
|
1922
|
-
return [...this.ops];
|
|
1923
|
-
}
|
|
1924
|
-
/**
|
|
1925
|
-
* Clone this tracker (for creating derived pipelines)
|
|
1926
|
-
*/
|
|
1927
|
-
clone() {
|
|
1928
|
-
const cloned = new PipelineOpTracker();
|
|
1929
|
-
cloned.ops = [...this.ops];
|
|
1930
|
-
return cloned;
|
|
1931
|
-
}
|
|
1932
|
-
};
|
|
1933
|
-
/**
|
|
1934
|
-
* Symbol used to identify pipeline clients internally
|
|
1935
|
-
*/
|
|
1936
|
-
const PIPELINE_CLIENT_SYMBOL = Symbol("PipelineClient");
|
|
1937
|
-
/**
|
|
1938
|
-
* Creates a PipelineClient using JavaScript Proxy.
|
|
1939
|
-
* The proxy intercepts property accesses to build up the transform chain.
|
|
1940
|
-
*/
|
|
1941
|
-
function createPipelineClient(options) {
|
|
1942
|
-
const { connection, questionId, opTracker = new PipelineOpTracker() } = options;
|
|
1943
|
-
return {
|
|
1944
|
-
[PIPELINE_CLIENT_SYMBOL]: true,
|
|
1945
|
-
connection,
|
|
1946
|
-
questionId,
|
|
1947
|
-
opTracker,
|
|
1948
|
-
call(interfaceId, methodId, params) {
|
|
1949
|
-
return makePipelinedCall(connection, questionId, opTracker.getTransform(), interfaceId, methodId, params);
|
|
1950
|
-
},
|
|
1951
|
-
getPointerField(fieldIndex) {
|
|
1952
|
-
const newTracker = opTracker.clone();
|
|
1953
|
-
newTracker.addGetPointerField(fieldIndex);
|
|
1954
|
-
return createPipelineClient({
|
|
1955
|
-
connection,
|
|
1956
|
-
questionId,
|
|
1957
|
-
opTracker: newTracker
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
/**
|
|
1963
|
-
* Check if a value is a PipelineClient
|
|
1964
|
-
*/
|
|
1965
|
-
function isPipelineClient(value) {
|
|
1966
|
-
return typeof value === "object" && value !== null && PIPELINE_CLIENT_SYMBOL in value;
|
|
1967
|
-
}
|
|
1968
|
-
/**
|
|
1969
|
-
* Makes a call on a promised answer (pipeline call).
|
|
1970
|
-
* This sends a Call message with target.type = 'promisedAnswer'.
|
|
1971
|
-
*/
|
|
1972
|
-
async function makePipelinedCall(connection, questionId, transform, interfaceId, methodId, params) {
|
|
1973
|
-
const newQuestionId = connection.createQuestion();
|
|
1974
|
-
const call = {
|
|
1975
|
-
questionId: newQuestionId,
|
|
1976
|
-
target: {
|
|
1977
|
-
type: "promisedAnswer",
|
|
1978
|
-
promisedAnswer: {
|
|
1979
|
-
questionId,
|
|
1980
|
-
transform
|
|
1981
|
-
}
|
|
1982
|
-
},
|
|
1983
|
-
interfaceId,
|
|
1984
|
-
methodId,
|
|
1985
|
-
allowThirdPartyTailCall: false,
|
|
1986
|
-
noPromisePipelining: false,
|
|
1987
|
-
onlyPromisePipeline: false,
|
|
1988
|
-
params,
|
|
1989
|
-
sendResultsTo: { type: "caller" }
|
|
1990
|
-
};
|
|
1991
|
-
await connection.sendCall(call);
|
|
1992
|
-
return connection.waitForAnswer(newQuestionId);
|
|
1993
|
-
}
|
|
1994
|
-
/**
|
|
1995
|
-
* Manages calls that were made on a pipeline client before the answer arrived.
|
|
1996
|
-
* When the answer arrives, these calls are dispatched to the actual capability.
|
|
1997
|
-
*/
|
|
1998
|
-
var QueuedCallManager = class {
|
|
1999
|
-
queuedCalls = /* @__PURE__ */ new Map();
|
|
2000
|
-
/**
|
|
2001
|
-
* Queue a call for when the promise resolves
|
|
2002
|
-
*/
|
|
2003
|
-
queueCall(questionId, call) {
|
|
2004
|
-
const calls = this.queuedCalls.get(questionId) ?? [];
|
|
2005
|
-
calls.push(call);
|
|
2006
|
-
this.queuedCalls.set(questionId, calls);
|
|
2007
|
-
}
|
|
2008
|
-
/**
|
|
2009
|
-
* Get and clear all queued calls for a question
|
|
2010
|
-
*/
|
|
2011
|
-
dequeueCalls(questionId) {
|
|
2012
|
-
const calls = this.queuedCalls.get(questionId) ?? [];
|
|
2013
|
-
this.queuedCalls.delete(questionId);
|
|
2014
|
-
return calls;
|
|
2015
|
-
}
|
|
2016
|
-
/**
|
|
2017
|
-
* Check if there are queued calls for a question
|
|
2018
|
-
*/
|
|
2019
|
-
hasQueuedCalls(questionId) {
|
|
2020
|
-
return (this.queuedCalls.get(questionId)?.length ?? 0) > 0;
|
|
2021
|
-
}
|
|
2022
|
-
/**
|
|
2023
|
-
* Clear all queued calls (e.g., on disconnect)
|
|
2024
|
-
*/
|
|
2025
|
-
clear() {
|
|
2026
|
-
for (const calls of this.queuedCalls.values()) for (const call of calls) call.reject(/* @__PURE__ */ new Error("Connection closed"));
|
|
2027
|
-
this.queuedCalls.clear();
|
|
2028
|
-
}
|
|
2029
|
-
};
|
|
2030
|
-
/**
|
|
2031
|
-
* Tracks pending pipeline resolutions
|
|
2032
|
-
*/
|
|
2033
|
-
var PipelineResolutionTracker = class {
|
|
2034
|
-
pendingResolutions = /* @__PURE__ */ new Map();
|
|
2035
|
-
/**
|
|
2036
|
-
* Mark a question as resolved to a capability
|
|
2037
|
-
*/
|
|
2038
|
-
resolveToCapability(questionId, importId) {
|
|
2039
|
-
this.pendingResolutions.set(questionId, {
|
|
2040
|
-
type: "capability",
|
|
2041
|
-
importId
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
/**
|
|
2045
|
-
* Mark a question as resolved to an exception
|
|
2046
|
-
*/
|
|
2047
|
-
resolveToException(questionId, reason) {
|
|
2048
|
-
this.pendingResolutions.set(questionId, {
|
|
2049
|
-
type: "exception",
|
|
2050
|
-
reason
|
|
2051
|
-
});
|
|
2052
|
-
}
|
|
2053
|
-
/**
|
|
2054
|
-
* Get the resolution for a question (if available)
|
|
2055
|
-
*/
|
|
2056
|
-
getResolution(questionId) {
|
|
2057
|
-
return this.pendingResolutions.get(questionId);
|
|
2058
|
-
}
|
|
2059
|
-
/**
|
|
2060
|
-
* Check if a question has been resolved
|
|
2061
|
-
*/
|
|
2062
|
-
isResolved(questionId) {
|
|
2063
|
-
return this.pendingResolutions.has(questionId);
|
|
2064
|
-
}
|
|
2065
|
-
/**
|
|
2066
|
-
* Remove a resolution entry
|
|
2067
|
-
*/
|
|
2068
|
-
remove(questionId) {
|
|
2069
|
-
this.pendingResolutions.delete(questionId);
|
|
2070
|
-
}
|
|
2071
|
-
/**
|
|
2072
|
-
* Clear all resolutions
|
|
2073
|
-
*/
|
|
2074
|
-
clear() {
|
|
2075
|
-
this.pendingResolutions.clear();
|
|
2076
|
-
}
|
|
2077
|
-
};
|
|
2078
|
-
|
|
2079
|
-
//#endregion
|
|
2080
|
-
//#region src/rpc/rpc-connection.ts
|
|
2081
|
-
/**
|
|
2082
|
-
* RpcConnection
|
|
2083
|
-
*
|
|
2084
|
-
* Manages a single RPC connection, handling message routing and the Four Tables.
|
|
2085
|
-
* This is the core of the RPC implementation.
|
|
2086
|
-
*
|
|
2087
|
-
* Phase 2 Updates:
|
|
2088
|
-
* - Added Promise Pipelining support
|
|
2089
|
-
* - Added capability passing
|
|
2090
|
-
* - Added Resolve/Release/Disembargo message handling
|
|
2091
|
-
*/
|
|
2092
|
-
var RpcConnection = class {
|
|
2093
|
-
transport;
|
|
2094
|
-
options;
|
|
2095
|
-
questions = new QuestionTable();
|
|
2096
|
-
answers = new AnswerTable();
|
|
2097
|
-
imports = new ImportTable();
|
|
2098
|
-
exports = new ExportTable();
|
|
2099
|
-
queuedCalls = new QueuedCallManager();
|
|
2100
|
-
pipelineResolutions = new PipelineResolutionTracker();
|
|
2101
|
-
running = false;
|
|
2102
|
-
messageHandler;
|
|
2103
|
-
constructor(transport, options = {}) {
|
|
2104
|
-
this.transport = transport;
|
|
2105
|
-
this.options = options;
|
|
2106
|
-
this.transport.onClose = (reason) => {
|
|
2107
|
-
this.handleDisconnect(reason);
|
|
2108
|
-
};
|
|
2109
|
-
this.transport.onError = (error) => {
|
|
2110
|
-
this.handleError(error);
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
/** Start processing messages */
|
|
2114
|
-
async start() {
|
|
2115
|
-
if (this.running) return;
|
|
2116
|
-
this.running = true;
|
|
2117
|
-
this.messageHandler = this.messageLoop();
|
|
2118
|
-
}
|
|
2119
|
-
/** Stop the connection */
|
|
2120
|
-
async stop() {
|
|
2121
|
-
this.running = false;
|
|
2122
|
-
this.transport.close();
|
|
2123
|
-
if (this.messageHandler) try {
|
|
2124
|
-
await this.messageHandler;
|
|
2125
|
-
} catch {}
|
|
2126
|
-
}
|
|
2127
|
-
/** Send a bootstrap request and return the bootstrap capability */
|
|
2128
|
-
async bootstrap() {
|
|
2129
|
-
const question = this.questions.create();
|
|
2130
|
-
const bootstrapMsg = {
|
|
2131
|
-
type: "bootstrap",
|
|
2132
|
-
bootstrap: { questionId: question.id }
|
|
2133
|
-
};
|
|
2134
|
-
await this.transport.send(bootstrapMsg);
|
|
2135
|
-
await question.completionPromise;
|
|
2136
|
-
return {};
|
|
2137
|
-
}
|
|
2138
|
-
/** Make a call to a remote capability */
|
|
2139
|
-
async call(target, interfaceId, methodId, params) {
|
|
2140
|
-
if (isPipelineClient(target)) return target.call(interfaceId, methodId, params);
|
|
2141
|
-
const question = this.questions.create();
|
|
2142
|
-
const callMsg = {
|
|
2143
|
-
type: "call",
|
|
2144
|
-
call: {
|
|
2145
|
-
questionId: question.id,
|
|
2146
|
-
target: {
|
|
2147
|
-
type: "importedCap",
|
|
2148
|
-
importId: target
|
|
2149
|
-
},
|
|
2150
|
-
interfaceId,
|
|
2151
|
-
methodId,
|
|
2152
|
-
allowThirdPartyTailCall: false,
|
|
2153
|
-
noPromisePipelining: false,
|
|
2154
|
-
onlyPromisePipeline: false,
|
|
2155
|
-
params,
|
|
2156
|
-
sendResultsTo: { type: "caller" }
|
|
2157
|
-
}
|
|
2158
|
-
};
|
|
2159
|
-
await this.transport.send(callMsg);
|
|
2160
|
-
return question.completionPromise;
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* Make a call that returns a PipelineClient for promise pipelining.
|
|
2164
|
-
* This allows making calls on the result before it arrives.
|
|
2165
|
-
*/
|
|
2166
|
-
async callPipelined(target, interfaceId, methodId, params) {
|
|
2167
|
-
const question = this.questions.create();
|
|
2168
|
-
const callMsg = {
|
|
2169
|
-
type: "call",
|
|
2170
|
-
call: {
|
|
2171
|
-
questionId: question.id,
|
|
2172
|
-
target: {
|
|
2173
|
-
type: "importedCap",
|
|
2174
|
-
importId: target
|
|
2175
|
-
},
|
|
2176
|
-
interfaceId,
|
|
2177
|
-
methodId,
|
|
2178
|
-
allowThirdPartyTailCall: false,
|
|
2179
|
-
noPromisePipelining: false,
|
|
2180
|
-
onlyPromisePipeline: false,
|
|
2181
|
-
params,
|
|
2182
|
-
sendResultsTo: { type: "caller" }
|
|
2183
|
-
}
|
|
2184
|
-
};
|
|
2185
|
-
await this.transport.send(callMsg);
|
|
2186
|
-
return createPipelineClient({
|
|
2187
|
-
connection: this,
|
|
2188
|
-
questionId: question.id
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
/** Send a finish message to release a question */
|
|
2192
|
-
async finish(questionId, releaseResultCaps = true) {
|
|
2193
|
-
if (!this.questions.get(questionId)) return;
|
|
2194
|
-
const finishMsg = {
|
|
2195
|
-
type: "finish",
|
|
2196
|
-
finish: {
|
|
2197
|
-
questionId,
|
|
2198
|
-
releaseResultCaps,
|
|
2199
|
-
requireEarlyCancellationWorkaround: false
|
|
2200
|
-
}
|
|
2201
|
-
};
|
|
2202
|
-
await this.transport.send(finishMsg);
|
|
2203
|
-
this.questions.markFinishSent(questionId);
|
|
2204
|
-
this.questions.remove(questionId);
|
|
2205
|
-
}
|
|
2206
|
-
/** Send a release message for an imported capability */
|
|
2207
|
-
async release(importId, referenceCount = 1) {
|
|
2208
|
-
const releaseMsg = {
|
|
2209
|
-
type: "release",
|
|
2210
|
-
release: {
|
|
2211
|
-
id: importId,
|
|
2212
|
-
referenceCount
|
|
2213
|
-
}
|
|
2214
|
-
};
|
|
2215
|
-
await this.transport.send(releaseMsg);
|
|
2216
|
-
}
|
|
2217
|
-
/** Send a resolve message to indicate a promise has resolved */
|
|
2218
|
-
async resolve(promiseId, cap) {
|
|
2219
|
-
const resolveMsg = {
|
|
2220
|
-
type: "resolve",
|
|
2221
|
-
resolve: {
|
|
2222
|
-
promiseId,
|
|
2223
|
-
resolution: {
|
|
2224
|
-
type: "cap",
|
|
2225
|
-
cap
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
};
|
|
2229
|
-
await this.transport.send(resolveMsg);
|
|
2230
|
-
}
|
|
2231
|
-
/** Send a resolve message indicating a promise was broken */
|
|
2232
|
-
async resolveException(promiseId, reason) {
|
|
2233
|
-
const resolveMsg = {
|
|
2234
|
-
type: "resolve",
|
|
2235
|
-
resolve: {
|
|
2236
|
-
promiseId,
|
|
2237
|
-
resolution: {
|
|
2238
|
-
type: "exception",
|
|
2239
|
-
exception: {
|
|
2240
|
-
reason,
|
|
2241
|
-
type: "failed"
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
};
|
|
2246
|
-
await this.transport.send(resolveMsg);
|
|
2247
|
-
}
|
|
2248
|
-
/** Internal method: Create a new question (used by pipeline) */
|
|
2249
|
-
createQuestion() {
|
|
2250
|
-
return this.questions.create().id;
|
|
2251
|
-
}
|
|
2252
|
-
/** Internal method: Send a call message (used by pipeline) */
|
|
2253
|
-
async sendCall(call) {
|
|
2254
|
-
const callMsg = {
|
|
2255
|
-
type: "call",
|
|
2256
|
-
call
|
|
2257
|
-
};
|
|
2258
|
-
await this.transport.send(callMsg);
|
|
2259
|
-
}
|
|
2260
|
-
/** Internal method: Wait for an answer (used by pipeline) */
|
|
2261
|
-
async waitForAnswer(questionId) {
|
|
2262
|
-
const question = this.questions.get(questionId);
|
|
2263
|
-
if (!question) throw new Error(`Question ${questionId} not found`);
|
|
2264
|
-
return question.completionPromise;
|
|
2265
|
-
}
|
|
2266
|
-
/** Main message processing loop */
|
|
2267
|
-
async messageLoop() {
|
|
2268
|
-
while (this.running) try {
|
|
2269
|
-
const message = await this.transport.receive();
|
|
2270
|
-
if (message === null) break;
|
|
2271
|
-
await this.handleMessage(message);
|
|
2272
|
-
} catch (error) {
|
|
2273
|
-
if (this.running) this.handleError(error);
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
/** Handle incoming messages */
|
|
2277
|
-
async handleMessage(message) {
|
|
2278
|
-
switch (message.type) {
|
|
2279
|
-
case "bootstrap":
|
|
2280
|
-
await this.handleBootstrap(message.bootstrap);
|
|
2281
|
-
break;
|
|
2282
|
-
case "call":
|
|
2283
|
-
await this.handleCall(message.call);
|
|
2284
|
-
break;
|
|
2285
|
-
case "return":
|
|
2286
|
-
await this.handleReturn(message.return);
|
|
2287
|
-
break;
|
|
2288
|
-
case "finish":
|
|
2289
|
-
await this.handleFinish(message.finish);
|
|
2290
|
-
break;
|
|
2291
|
-
case "resolve":
|
|
2292
|
-
await this.handleResolve(message.resolve);
|
|
2293
|
-
break;
|
|
2294
|
-
case "release":
|
|
2295
|
-
await this.handleRelease(message.release);
|
|
2296
|
-
break;
|
|
2297
|
-
case "disembargo":
|
|
2298
|
-
await this.handleDisembargo(message.disembargo);
|
|
2299
|
-
break;
|
|
2300
|
-
case "abort":
|
|
2301
|
-
this.handleAbort(message.exception.reason);
|
|
2302
|
-
break;
|
|
2303
|
-
case "unimplemented": break;
|
|
2304
|
-
default: await this.sendUnimplemented(message);
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
/** Handle bootstrap request */
|
|
2308
|
-
async handleBootstrap(bootstrap) {
|
|
2309
|
-
this.answers.create(bootstrap.questionId);
|
|
2310
|
-
const returnMsg = {
|
|
2311
|
-
type: "return",
|
|
2312
|
-
return: {
|
|
2313
|
-
answerId: bootstrap.questionId,
|
|
2314
|
-
releaseParamCaps: true,
|
|
2315
|
-
noFinishNeeded: false,
|
|
2316
|
-
result: {
|
|
2317
|
-
type: "results",
|
|
2318
|
-
payload: {
|
|
2319
|
-
content: new Uint8Array(0),
|
|
2320
|
-
capTable: []
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
};
|
|
2325
|
-
await this.transport.send(returnMsg);
|
|
2326
|
-
this.answers.markReturnSent(bootstrap.questionId);
|
|
2327
|
-
}
|
|
2328
|
-
/** Handle incoming call */
|
|
2329
|
-
async handleCall(call) {
|
|
2330
|
-
this.answers.create(call.questionId);
|
|
2331
|
-
const returnMsg = {
|
|
2332
|
-
type: "return",
|
|
2333
|
-
return: {
|
|
2334
|
-
answerId: call.questionId,
|
|
2335
|
-
releaseParamCaps: true,
|
|
2336
|
-
noFinishNeeded: false,
|
|
2337
|
-
result: {
|
|
2338
|
-
type: "exception",
|
|
2339
|
-
exception: {
|
|
2340
|
-
reason: "Method not implemented",
|
|
2341
|
-
type: "unimplemented"
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
};
|
|
2346
|
-
await this.transport.send(returnMsg);
|
|
2347
|
-
this.answers.markReturnSent(call.questionId);
|
|
2348
|
-
}
|
|
2349
|
-
/** Handle return message */
|
|
2350
|
-
async handleReturn(ret) {
|
|
2351
|
-
if (!this.questions.get(ret.answerId)) return;
|
|
2352
|
-
if (ret.result.type === "results") {
|
|
2353
|
-
const capTable = ret.result.payload.capTable;
|
|
2354
|
-
if (capTable.length > 0) {
|
|
2355
|
-
const cap = capTable[0];
|
|
2356
|
-
if (cap.type === "receiverHosted") this.pipelineResolutions.resolveToCapability(ret.answerId, cap.importId);
|
|
2357
|
-
}
|
|
2358
|
-
} else if (ret.result.type === "exception") this.pipelineResolutions.resolveToException(ret.answerId, ret.result.exception.reason);
|
|
2359
|
-
switch (ret.result.type) {
|
|
2360
|
-
case "results":
|
|
2361
|
-
this.questions.complete(ret.answerId, ret.result.payload);
|
|
2362
|
-
break;
|
|
2363
|
-
case "exception":
|
|
2364
|
-
this.questions.cancel(ret.answerId, new Error(ret.result.exception.reason));
|
|
2365
|
-
break;
|
|
2366
|
-
case "canceled":
|
|
2367
|
-
this.questions.cancel(ret.answerId, /* @__PURE__ */ new Error("Call canceled"));
|
|
2368
|
-
break;
|
|
2369
|
-
default: this.questions.cancel(ret.answerId, /* @__PURE__ */ new Error("Unknown return type"));
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
/** Handle finish message */
|
|
2373
|
-
async handleFinish(finish) {
|
|
2374
|
-
this.answers.markFinishReceived(finish.questionId);
|
|
2375
|
-
this.answers.remove(finish.questionId);
|
|
2376
|
-
}
|
|
2377
|
-
/** Handle resolve message (Level 1) */
|
|
2378
|
-
async handleResolve(resolve) {
|
|
2379
|
-
const { promiseId, resolution } = resolve;
|
|
2380
|
-
switch (resolution.type) {
|
|
2381
|
-
case "cap":
|
|
2382
|
-
this.imports.markResolved(promiseId);
|
|
2383
|
-
break;
|
|
2384
|
-
case "exception":
|
|
2385
|
-
console.warn(`Promise ${promiseId} broken: ${resolution.exception.reason}`);
|
|
2386
|
-
break;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
/** Handle release message (Level 1) */
|
|
2390
|
-
async handleRelease(release) {
|
|
2391
|
-
const { id, referenceCount } = release;
|
|
2392
|
-
if (this.exports.release(id, referenceCount)) console.log(`Export ${id} fully released`);
|
|
2393
|
-
}
|
|
2394
|
-
/** Handle disembargo message (Level 1) */
|
|
2395
|
-
async handleDisembargo(disembargo) {
|
|
2396
|
-
const { target, context } = disembargo;
|
|
2397
|
-
if (context.type === "senderLoopback") {
|
|
2398
|
-
const echoMsg = {
|
|
2399
|
-
type: "disembargo",
|
|
2400
|
-
disembargo: {
|
|
2401
|
-
target,
|
|
2402
|
-
context: {
|
|
2403
|
-
type: "receiverLoopback",
|
|
2404
|
-
embargoId: context.embargoId
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
};
|
|
2408
|
-
await this.transport.send(echoMsg);
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
/** Handle abort message */
|
|
2412
|
-
handleAbort(_reason) {
|
|
2413
|
-
this.running = false;
|
|
2414
|
-
this.questions.clear();
|
|
2415
|
-
this.answers.clear();
|
|
2416
|
-
this.imports.clear();
|
|
2417
|
-
this.exports.clear();
|
|
2418
|
-
this.queuedCalls.clear();
|
|
2419
|
-
this.pipelineResolutions.clear();
|
|
2420
|
-
}
|
|
2421
|
-
/** Handle disconnect */
|
|
2422
|
-
handleDisconnect(_reason) {
|
|
2423
|
-
this.running = false;
|
|
2424
|
-
this.questions.clear();
|
|
2425
|
-
this.answers.clear();
|
|
2426
|
-
this.imports.clear();
|
|
2427
|
-
this.exports.clear();
|
|
2428
|
-
this.queuedCalls.clear();
|
|
2429
|
-
this.pipelineResolutions.clear();
|
|
2430
|
-
}
|
|
2431
|
-
/** Handle error */
|
|
2432
|
-
handleError(error) {
|
|
2433
|
-
console.error("RPC error:", error);
|
|
2434
|
-
}
|
|
2435
|
-
/** Send unimplemented response */
|
|
2436
|
-
async sendUnimplemented(originalMessage) {
|
|
2437
|
-
const msg = {
|
|
2438
|
-
type: "unimplemented",
|
|
2439
|
-
message: originalMessage
|
|
2440
|
-
};
|
|
2441
|
-
await this.transport.send(msg);
|
|
2442
|
-
}
|
|
2443
|
-
/** Import a capability from the remote peer */
|
|
2444
|
-
importCapability(importId, isPromise = false) {
|
|
2445
|
-
this.imports.add(importId, isPromise);
|
|
2446
|
-
}
|
|
2447
|
-
/** Export a capability to the remote peer */
|
|
2448
|
-
exportCapability(capability, isPromise = false) {
|
|
2449
|
-
return this.exports.add(capability, isPromise).id;
|
|
2450
|
-
}
|
|
2451
|
-
/** Get an imported capability */
|
|
2452
|
-
getImport(importId) {
|
|
2453
|
-
return this.imports.get(importId);
|
|
2454
|
-
}
|
|
2455
|
-
/** Get an exported capability */
|
|
2456
|
-
getExport(exportId) {
|
|
2457
|
-
return this.exports.get(exportId);
|
|
2458
|
-
}
|
|
2459
|
-
};
|
|
2460
|
-
|
|
2461
1697
|
//#endregion
|
|
2462
1698
|
//#region src/rpc/capability-client.ts
|
|
2463
1699
|
/** Base class for capability client implementations */
|
|
@@ -3000,11 +2236,2690 @@ function configureGlobalMemoryPool(options) {
|
|
|
3000
2236
|
}
|
|
3001
2237
|
|
|
3002
2238
|
//#endregion
|
|
3003
|
-
|
|
2239
|
+
//#region src/rpc/connection-manager.ts
|
|
2240
|
+
/**
|
|
2241
|
+
* ConnectionManager manages multiple RPC connections for Level 3 RPC.
|
|
2242
|
+
*
|
|
2243
|
+
* Key responsibilities:
|
|
2244
|
+
* 1. Maintain a pool of connections to other vats
|
|
2245
|
+
* 2. Handle automatic connection establishment for third-party capabilities
|
|
2246
|
+
* 3. Manage pending provisions (capabilities waiting to be picked up)
|
|
2247
|
+
* 4. Route messages to the appropriate connection
|
|
2248
|
+
* 5. Handle connection lifecycle (connect, disconnect, reconnect)
|
|
2249
|
+
*/
|
|
2250
|
+
var ConnectionManager = class {
|
|
2251
|
+
options;
|
|
2252
|
+
connections = /* @__PURE__ */ new Map();
|
|
2253
|
+
pendingProvisions = /* @__PURE__ */ new Map();
|
|
2254
|
+
connectionPromises = /* @__PURE__ */ new Map();
|
|
2255
|
+
constructor(options) {
|
|
2256
|
+
this.options = {
|
|
2257
|
+
maxConnections: 100,
|
|
2258
|
+
idleTimeoutMs: 3e5,
|
|
2259
|
+
autoConnect: true,
|
|
2260
|
+
...options
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Register an existing connection with the manager.
|
|
2265
|
+
* This is called when a connection is established (either inbound or outbound).
|
|
2266
|
+
*/
|
|
2267
|
+
registerConnection(vatId, connection) {
|
|
2268
|
+
const vatIdKey = this.vatIdToKey(vatId);
|
|
2269
|
+
const info = {
|
|
2270
|
+
connection,
|
|
2271
|
+
remoteVatId: vatId,
|
|
2272
|
+
establishedAt: /* @__PURE__ */ new Date(),
|
|
2273
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
2274
|
+
state: "connected"
|
|
2275
|
+
};
|
|
2276
|
+
this.connections.set(vatIdKey, info);
|
|
2277
|
+
return info;
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Get or establish a connection to a vat.
|
|
2281
|
+
* If autoConnect is enabled and no connection exists, a new one will be created.
|
|
2282
|
+
*/
|
|
2283
|
+
async getConnection(vatId) {
|
|
2284
|
+
const vatIdKey = this.vatIdToKey(vatId);
|
|
2285
|
+
const existing = this.connections.get(vatIdKey);
|
|
2286
|
+
if (existing && existing.state === "connected") {
|
|
2287
|
+
existing.lastActivity = /* @__PURE__ */ new Date();
|
|
2288
|
+
return existing.connection;
|
|
2289
|
+
}
|
|
2290
|
+
const pending = this.connectionPromises.get(vatIdKey);
|
|
2291
|
+
if (pending) return pending;
|
|
2292
|
+
if (this.options.autoConnect) return this.establishConnection(vatId);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Establish a new connection to a vat.
|
|
2296
|
+
*/
|
|
2297
|
+
async establishConnection(vatId, address) {
|
|
2298
|
+
const vatIdKey = this.vatIdToKey(vatId);
|
|
2299
|
+
if (this.connectionPromises.has(vatIdKey)) return this.connectionPromises.get(vatIdKey);
|
|
2300
|
+
const connectPromise = this.doEstablishConnection(vatId, address);
|
|
2301
|
+
this.connectionPromises.set(vatIdKey, connectPromise);
|
|
2302
|
+
try {
|
|
2303
|
+
return await connectPromise;
|
|
2304
|
+
} finally {
|
|
2305
|
+
this.connectionPromises.delete(vatIdKey);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async doEstablishConnection(vatId, address) {
|
|
2309
|
+
const { RpcConnection } = await Promise.resolve().then(() => require("./rpc-connection-BKWQQ7f9.js")).then((n) => n.rpc_connection_exports);
|
|
2310
|
+
const connection = new RpcConnection(await this.options.connectionFactory(vatId, address), this.options.connectionOptions);
|
|
2311
|
+
await connection.start();
|
|
2312
|
+
this.registerConnection(vatId, connection);
|
|
2313
|
+
return connection;
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Close a connection to a vat.
|
|
2317
|
+
*/
|
|
2318
|
+
async closeConnection(vatId) {
|
|
2319
|
+
const vatIdKey = this.vatIdToKey(vatId);
|
|
2320
|
+
const info = this.connections.get(vatIdKey);
|
|
2321
|
+
if (info) {
|
|
2322
|
+
info.state = "closing";
|
|
2323
|
+
await info.connection.stop();
|
|
2324
|
+
this.connections.delete(vatIdKey);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Close all connections.
|
|
2329
|
+
*/
|
|
2330
|
+
async closeAll() {
|
|
2331
|
+
const closePromises = [];
|
|
2332
|
+
for (const [_vatIdKey, info] of this.connections) {
|
|
2333
|
+
info.state = "closing";
|
|
2334
|
+
closePromises.push(info.connection.stop().catch(() => {}));
|
|
2335
|
+
}
|
|
2336
|
+
await Promise.all(closePromises);
|
|
2337
|
+
this.connections.clear();
|
|
2338
|
+
this.pendingProvisions.clear();
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Create a pending provision for a third-party capability.
|
|
2342
|
+
* Called when we receive a Provide message.
|
|
2343
|
+
*/
|
|
2344
|
+
createPendingProvision(provisionId, recipientId, targetExportId, questionId, embargoed) {
|
|
2345
|
+
const provisionKey = this.provisionIdToKey(provisionId);
|
|
2346
|
+
const provision = {
|
|
2347
|
+
provisionId,
|
|
2348
|
+
recipientId,
|
|
2349
|
+
targetExportId,
|
|
2350
|
+
questionId,
|
|
2351
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2352
|
+
embargoed
|
|
2353
|
+
};
|
|
2354
|
+
this.pendingProvisions.set(provisionKey, provision);
|
|
2355
|
+
return provision;
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Get a pending provision by ID.
|
|
2359
|
+
*/
|
|
2360
|
+
getPendingProvision(provisionId) {
|
|
2361
|
+
const provisionKey = this.provisionIdToKey(provisionId);
|
|
2362
|
+
return this.pendingProvisions.get(provisionKey);
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Remove a pending provision (when it's been accepted or expired).
|
|
2366
|
+
*/
|
|
2367
|
+
removePendingProvision(provisionId) {
|
|
2368
|
+
const provisionKey = this.provisionIdToKey(provisionId);
|
|
2369
|
+
return this.pendingProvisions.delete(provisionKey);
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Find provisions for a specific recipient.
|
|
2373
|
+
*/
|
|
2374
|
+
findProvisionsForRecipient(recipientId) {
|
|
2375
|
+
const recipientKey = this.vatIdToKey(recipientId);
|
|
2376
|
+
const result = [];
|
|
2377
|
+
for (const provision of this.pendingProvisions.values()) if (this.vatIdToKey(provision.recipientId) === recipientKey) result.push(provision);
|
|
2378
|
+
return result;
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Clean up expired provisions.
|
|
2382
|
+
*/
|
|
2383
|
+
cleanupExpiredProvisions(maxAgeMs = 3e5) {
|
|
2384
|
+
const now = Date.now();
|
|
2385
|
+
let removed = 0;
|
|
2386
|
+
for (const [key, provision] of this.pendingProvisions) if (now - provision.createdAt.getTime() > maxAgeMs) {
|
|
2387
|
+
this.pendingProvisions.delete(key);
|
|
2388
|
+
removed++;
|
|
2389
|
+
}
|
|
2390
|
+
return removed;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Resolve a third-party capability ID to a connection.
|
|
2394
|
+
* This is the core of Level 3 RPC - automatically establishing connections
|
|
2395
|
+
* to third parties when capabilities are passed between vats.
|
|
2396
|
+
*/
|
|
2397
|
+
async resolveThirdPartyCap(thirdPartyCapId) {
|
|
2398
|
+
const parsed = this.parseThirdPartyCapId(thirdPartyCapId);
|
|
2399
|
+
if (!parsed) return;
|
|
2400
|
+
const connection = await this.getConnection(parsed.vatId);
|
|
2401
|
+
if (!connection) return;
|
|
2402
|
+
return {
|
|
2403
|
+
connection,
|
|
2404
|
+
provisionId: parsed.provisionId
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Parse a ThirdPartyCapId to extract vat ID and provision ID.
|
|
2409
|
+
* The format is implementation-specific, but typically:
|
|
2410
|
+
* - First N bytes: vat ID
|
|
2411
|
+
* - Remaining bytes: provision ID
|
|
2412
|
+
*/
|
|
2413
|
+
parseThirdPartyCapId(thirdPartyCapId) {
|
|
2414
|
+
const data = thirdPartyCapId.id;
|
|
2415
|
+
if (data.length < 32) return;
|
|
2416
|
+
const vatIdBytes = data.slice(0, 32);
|
|
2417
|
+
const provisionIdBytes = data.slice(32);
|
|
2418
|
+
return {
|
|
2419
|
+
vatId: { id: vatIdBytes },
|
|
2420
|
+
provisionId: { id: provisionIdBytes }
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Get all active connections.
|
|
2425
|
+
*/
|
|
2426
|
+
getAllConnections() {
|
|
2427
|
+
return Array.from(this.connections.values());
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Get the number of active connections.
|
|
2431
|
+
*/
|
|
2432
|
+
getConnectionCount() {
|
|
2433
|
+
return this.connections.size;
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Get the number of pending provisions.
|
|
2437
|
+
*/
|
|
2438
|
+
getPendingProvisionCount() {
|
|
2439
|
+
return this.pendingProvisions.size;
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Check if a connection exists to a vat.
|
|
2443
|
+
*/
|
|
2444
|
+
hasConnection(vatId) {
|
|
2445
|
+
const vatIdKey = this.vatIdToKey(vatId);
|
|
2446
|
+
const info = this.connections.get(vatIdKey);
|
|
2447
|
+
return info !== void 0 && info.state === "connected";
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Update the last activity timestamp for a connection.
|
|
2451
|
+
*/
|
|
2452
|
+
touchConnection(vatId) {
|
|
2453
|
+
const vatIdKey = this.vatIdToKey(vatId);
|
|
2454
|
+
const info = this.connections.get(vatIdKey);
|
|
2455
|
+
if (info) info.lastActivity = /* @__PURE__ */ new Date();
|
|
2456
|
+
}
|
|
2457
|
+
vatIdToKey(vatId) {
|
|
2458
|
+
return Array.from(vatId.id).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2459
|
+
}
|
|
2460
|
+
provisionIdToKey(provisionId) {
|
|
2461
|
+
return Array.from(provisionId.id).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
/**
|
|
2465
|
+
* Create a ThirdPartyCapId from vat ID and provision ID.
|
|
2466
|
+
*/
|
|
2467
|
+
function createThirdPartyCapId(vatId, provisionId) {
|
|
2468
|
+
const combined = new Uint8Array(vatId.id.length + provisionId.id.length);
|
|
2469
|
+
combined.set(vatId.id, 0);
|
|
2470
|
+
combined.set(provisionId.id, vatId.id.length);
|
|
2471
|
+
return { id: combined };
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Create a RecipientId from a vat ID.
|
|
2475
|
+
*/
|
|
2476
|
+
function createRecipientId(vatId) {
|
|
2477
|
+
return { id: vatId.id };
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Create a ProvisionId from raw bytes.
|
|
2481
|
+
*/
|
|
2482
|
+
function createProvisionId(id) {
|
|
2483
|
+
return { id };
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Generate a random provision ID.
|
|
2487
|
+
*/
|
|
2488
|
+
function generateProvisionId() {
|
|
2489
|
+
const id = new Uint8Array(32);
|
|
2490
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(id);
|
|
2491
|
+
else {
|
|
2492
|
+
const { randomBytes } = require("node:crypto");
|
|
2493
|
+
randomBytes(32).copy(id);
|
|
2494
|
+
}
|
|
2495
|
+
return { id };
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Generate a random vat ID.
|
|
2499
|
+
*/
|
|
2500
|
+
function generateVatId() {
|
|
2501
|
+
const id = new Uint8Array(32);
|
|
2502
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(id);
|
|
2503
|
+
else {
|
|
2504
|
+
const { randomBytes } = require("node:crypto");
|
|
2505
|
+
randomBytes(32).copy(id);
|
|
2506
|
+
}
|
|
2507
|
+
return { id };
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
//#endregion
|
|
2511
|
+
//#region src/rpc/level3-handlers.ts
|
|
2512
|
+
/**
|
|
2513
|
+
* Manages Level 3 RPC message handling for a connection.
|
|
2514
|
+
*
|
|
2515
|
+
* This class handles:
|
|
2516
|
+
* 1. Provide messages - when someone wants to give us a capability to share with a third party
|
|
2517
|
+
* 2. Accept messages - when a third party wants to pick up a capability we provided
|
|
2518
|
+
* 3. Embargo handling - breaking cycles in introduction graphs
|
|
2519
|
+
*/
|
|
2520
|
+
var Level3Handlers = class {
|
|
2521
|
+
options;
|
|
2522
|
+
pendingAccepts = /* @__PURE__ */ new Map();
|
|
2523
|
+
embargoedCalls = /* @__PURE__ */ new Map();
|
|
2524
|
+
nextEmbargoId = 1;
|
|
2525
|
+
constructor(options) {
|
|
2526
|
+
this.options = options;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Handle an incoming Provide message.
|
|
2530
|
+
*
|
|
2531
|
+
* When we receive a Provide message, it means someone wants us to hold a capability
|
|
2532
|
+
* and make it available to a specific third party. We:
|
|
2533
|
+
* 1. Create a pending provision
|
|
2534
|
+
* 2. Return an answer acknowledging receipt
|
|
2535
|
+
* 3. Wait for the third party to Accept
|
|
2536
|
+
*/
|
|
2537
|
+
async handleProvide(provide) {
|
|
2538
|
+
const { questionId, target, recipient } = provide;
|
|
2539
|
+
const { connectionManager } = this.options;
|
|
2540
|
+
const provisionId = generateProvisionId();
|
|
2541
|
+
const targetExportId = this.extractTargetExportId(target);
|
|
2542
|
+
if (targetExportId === void 0) {
|
|
2543
|
+
await this.sendReturnException(questionId, "Invalid provide target: must be a hosted capability");
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
connectionManager.createPendingProvision(provisionId, this.recipientIdToVatId(recipient), targetExportId, questionId, false);
|
|
2547
|
+
if (this.options.onProvide) await this.options.onProvide(provide);
|
|
2548
|
+
await this.sendReturnResults(questionId, { provisionId: provisionId.id });
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Send a Provide message to offer a capability to a third party.
|
|
2552
|
+
*
|
|
2553
|
+
* This is called when we want to introduce a third party to a capability we hold.
|
|
2554
|
+
* For example, Alice calls this to offer Bob access to Carol's capability.
|
|
2555
|
+
*/
|
|
2556
|
+
async sendProvide(target, recipient) {
|
|
2557
|
+
const { connection } = this.options;
|
|
2558
|
+
const questionId = connection.createQuestion();
|
|
2559
|
+
const provideMsg = {
|
|
2560
|
+
type: "provide",
|
|
2561
|
+
provide: {
|
|
2562
|
+
questionId,
|
|
2563
|
+
target,
|
|
2564
|
+
recipient
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
await connection.sendCall(provideMsg.provide);
|
|
2568
|
+
await connection.waitForAnswer(questionId);
|
|
2569
|
+
return {
|
|
2570
|
+
questionId,
|
|
2571
|
+
provisionId: { id: new Uint8Array(0) }
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Handle an incoming Accept message.
|
|
2576
|
+
*
|
|
2577
|
+
* When we receive an Accept message, it means a third party wants to pick up
|
|
2578
|
+
* a capability we previously agreed to provide. We:
|
|
2579
|
+
* 1. Look up the pending provision
|
|
2580
|
+
* 2. Verify the recipient matches
|
|
2581
|
+
* 3. Return the capability
|
|
2582
|
+
*/
|
|
2583
|
+
async handleAccept(accept) {
|
|
2584
|
+
const { questionId, provision, embargo } = accept;
|
|
2585
|
+
const { connectionManager } = this.options;
|
|
2586
|
+
const pendingProvision = connectionManager.getPendingProvision(provision);
|
|
2587
|
+
if (!pendingProvision) {
|
|
2588
|
+
await this.sendReturnException(questionId, "Invalid provision ID: no pending provision found");
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
connectionManager.removePendingProvision(provision);
|
|
2592
|
+
if (embargo || pendingProvision.embargoed) {
|
|
2593
|
+
await this.handleEmbargoedAccept(questionId, pendingProvision);
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const capDescriptor = {
|
|
2597
|
+
type: "senderHosted",
|
|
2598
|
+
exportId: pendingProvision.targetExportId
|
|
2599
|
+
};
|
|
2600
|
+
await this.sendReturnCapability(questionId, capDescriptor);
|
|
2601
|
+
if (this.options.onAccept) await this.options.onAccept(accept);
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Send an Accept message to pick up a capability from a third party.
|
|
2605
|
+
*
|
|
2606
|
+
* This is called when we receive a third-party capability and want to
|
|
2607
|
+
* establish a direct connection to use it.
|
|
2608
|
+
*/
|
|
2609
|
+
async sendAccept(targetConnection, provision, embargo = false) {
|
|
2610
|
+
const questionId = targetConnection.createQuestion();
|
|
2611
|
+
const acceptMsg = {
|
|
2612
|
+
type: "accept",
|
|
2613
|
+
accept: {
|
|
2614
|
+
questionId,
|
|
2615
|
+
provision,
|
|
2616
|
+
embargo
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
await targetConnection.sendCall(acceptMsg.accept);
|
|
2620
|
+
await targetConnection.waitForAnswer(questionId);
|
|
2621
|
+
return 0;
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Handle an embargoed accept.
|
|
2625
|
+
*
|
|
2626
|
+
* Embargoes are used to break cycles in the introduction graph. For example,
|
|
2627
|
+
* if Alice introduces Bob to Carol and Carol to Bob simultaneously, both
|
|
2628
|
+
* introductions use embargo=true to prevent deadlock.
|
|
2629
|
+
*/
|
|
2630
|
+
async handleEmbargoedAccept(questionId, provision) {
|
|
2631
|
+
const pendingAccept = {
|
|
2632
|
+
questionId,
|
|
2633
|
+
provision,
|
|
2634
|
+
embargoId: this.nextEmbargoId++
|
|
2635
|
+
};
|
|
2636
|
+
this.pendingAccepts.set(questionId, pendingAccept);
|
|
2637
|
+
await this.sendReturnResultsSentElsewhere(questionId);
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Handle a Disembargo message.
|
|
2641
|
+
*
|
|
2642
|
+
* Disembargo messages are used to lift embargoes on capabilities.
|
|
2643
|
+
*/
|
|
2644
|
+
async handleDisembargo(disembargo) {
|
|
2645
|
+
const { target, context } = disembargo;
|
|
2646
|
+
switch (context.type) {
|
|
2647
|
+
case "senderLoopback":
|
|
2648
|
+
await this.sendDisembargoEcho(disembargo);
|
|
2649
|
+
break;
|
|
2650
|
+
case "receiverLoopback":
|
|
2651
|
+
await this.liftEmbargo(context.embargoId);
|
|
2652
|
+
break;
|
|
2653
|
+
case "accept":
|
|
2654
|
+
await this.liftAcceptEmbargo(target);
|
|
2655
|
+
break;
|
|
2656
|
+
case "provide":
|
|
2657
|
+
await this.liftProvideEmbargo(context.questionId);
|
|
2658
|
+
break;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Lift an embargo by ID.
|
|
2663
|
+
*/
|
|
2664
|
+
async liftEmbargo(embargoId) {
|
|
2665
|
+
const calls = this.embargoedCalls.get(embargoId);
|
|
2666
|
+
if (calls) {
|
|
2667
|
+
for (const call of calls) await this.resendEmbargoedCall(call);
|
|
2668
|
+
this.embargoedCalls.delete(embargoId);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Lift an embargo on an accept.
|
|
2673
|
+
*/
|
|
2674
|
+
async liftAcceptEmbargo(target) {
|
|
2675
|
+
for (const [questionId, pendingAccept] of this.pendingAccepts) if (this.matchesTarget(pendingAccept.provision.targetExportId, target)) {
|
|
2676
|
+
const capDescriptor = {
|
|
2677
|
+
type: "senderHosted",
|
|
2678
|
+
exportId: pendingAccept.provision.targetExportId
|
|
2679
|
+
};
|
|
2680
|
+
await this.sendReturnCapability(pendingAccept.questionId, capDescriptor);
|
|
2681
|
+
this.pendingAccepts.delete(questionId);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Lift an embargo on a provide.
|
|
2686
|
+
*/
|
|
2687
|
+
async liftProvideEmbargo(_questionId) {
|
|
2688
|
+
const { connectionManager } = this.options;
|
|
2689
|
+
for (const _provision of connectionManager.getAllConnections());
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Handle receiving a third-party capability.
|
|
2693
|
+
*
|
|
2694
|
+
* When we receive a CapDescriptor with type 'thirdPartyHosted', we need to:
|
|
2695
|
+
* 1. Establish a connection to the third party (if not already connected)
|
|
2696
|
+
* 2. Send an Accept message to pick up the capability
|
|
2697
|
+
* 3. Return a local import ID for the capability
|
|
2698
|
+
*/
|
|
2699
|
+
async handleThirdPartyCapability(thirdPartyCapId) {
|
|
2700
|
+
const { connectionManager } = this.options;
|
|
2701
|
+
const resolved = await connectionManager.resolveThirdPartyCap(thirdPartyCapId);
|
|
2702
|
+
if (!resolved) return;
|
|
2703
|
+
const { connection, provisionId } = resolved;
|
|
2704
|
+
return await this.sendAccept(connection, provisionId);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Create a third-party capability descriptor.
|
|
2708
|
+
*
|
|
2709
|
+
* This is called when we want to pass a capability to a peer, but the capability
|
|
2710
|
+
* is actually hosted by a third party. We create a ThirdPartyCapId that allows
|
|
2711
|
+
* the recipient to connect directly to the third party.
|
|
2712
|
+
*/
|
|
2713
|
+
createThirdPartyCapDescriptor(_hostedConnection, exportId, recipientVatId) {
|
|
2714
|
+
const { connectionManager, selfVatId } = this.options;
|
|
2715
|
+
const provisionId = generateProvisionId();
|
|
2716
|
+
const thirdPartyCapId = createThirdPartyCapId(selfVatId, provisionId);
|
|
2717
|
+
connectionManager.createPendingProvision(provisionId, recipientVatId, exportId, 0, false);
|
|
2718
|
+
return {
|
|
2719
|
+
type: "thirdPartyHosted",
|
|
2720
|
+
thirdPartyCapId
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
extractTargetExportId(target) {
|
|
2724
|
+
if (target.type === "importedCap") return target.importId;
|
|
2725
|
+
}
|
|
2726
|
+
recipientIdToVatId(recipient) {
|
|
2727
|
+
return { id: recipient.id };
|
|
2728
|
+
}
|
|
2729
|
+
matchesTarget(exportId, target) {
|
|
2730
|
+
if (target.type === "importedCap") return target.importId === exportId;
|
|
2731
|
+
return false;
|
|
2732
|
+
}
|
|
2733
|
+
async sendReturnResults(questionId, _results) {
|
|
2734
|
+
const { connection } = this.options;
|
|
2735
|
+
const returnMsg = {
|
|
2736
|
+
type: "return",
|
|
2737
|
+
return: {
|
|
2738
|
+
answerId: questionId,
|
|
2739
|
+
releaseParamCaps: true,
|
|
2740
|
+
noFinishNeeded: false,
|
|
2741
|
+
result: {
|
|
2742
|
+
type: "results",
|
|
2743
|
+
payload: {
|
|
2744
|
+
content: new Uint8Array(0),
|
|
2745
|
+
capTable: []
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
await connection.sendReturn(returnMsg.return);
|
|
2751
|
+
}
|
|
2752
|
+
async sendReturnCapability(questionId, cap) {
|
|
2753
|
+
const { connection } = this.options;
|
|
2754
|
+
const returnMsg = {
|
|
2755
|
+
type: "return",
|
|
2756
|
+
return: {
|
|
2757
|
+
answerId: questionId,
|
|
2758
|
+
releaseParamCaps: true,
|
|
2759
|
+
noFinishNeeded: false,
|
|
2760
|
+
result: {
|
|
2761
|
+
type: "results",
|
|
2762
|
+
payload: {
|
|
2763
|
+
content: new Uint8Array(0),
|
|
2764
|
+
capTable: [cap]
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
await connection.sendReturn(returnMsg.return);
|
|
2770
|
+
}
|
|
2771
|
+
async sendReturnException(questionId, reason) {
|
|
2772
|
+
const { connection } = this.options;
|
|
2773
|
+
const returnMsg = {
|
|
2774
|
+
type: "return",
|
|
2775
|
+
return: {
|
|
2776
|
+
answerId: questionId,
|
|
2777
|
+
releaseParamCaps: true,
|
|
2778
|
+
noFinishNeeded: false,
|
|
2779
|
+
result: {
|
|
2780
|
+
type: "exception",
|
|
2781
|
+
exception: {
|
|
2782
|
+
reason,
|
|
2783
|
+
type: "failed"
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
await connection.sendReturn(returnMsg.return);
|
|
2789
|
+
}
|
|
2790
|
+
async sendReturnResultsSentElsewhere(questionId) {
|
|
2791
|
+
const { connection } = this.options;
|
|
2792
|
+
const returnMsg = {
|
|
2793
|
+
type: "return",
|
|
2794
|
+
return: {
|
|
2795
|
+
answerId: questionId,
|
|
2796
|
+
releaseParamCaps: true,
|
|
2797
|
+
noFinishNeeded: false,
|
|
2798
|
+
result: { type: "resultsSentElsewhere" }
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
await connection.sendReturn(returnMsg.return);
|
|
2802
|
+
}
|
|
2803
|
+
async sendDisembargoEcho(disembargo) {
|
|
2804
|
+
const { connection } = this.options;
|
|
2805
|
+
if (disembargo.context.type !== "senderLoopback") return;
|
|
2806
|
+
const echoMsg = {
|
|
2807
|
+
type: "disembargo",
|
|
2808
|
+
disembargo: {
|
|
2809
|
+
target: disembargo.target,
|
|
2810
|
+
context: {
|
|
2811
|
+
type: "receiverLoopback",
|
|
2812
|
+
embargoId: disembargo.context.embargoId
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
await connection.sendDisembargo(echoMsg.disembargo);
|
|
2817
|
+
}
|
|
2818
|
+
async resendEmbargoedCall(_call) {}
|
|
2819
|
+
};
|
|
2820
|
+
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/rpc/level4-types.ts
|
|
2823
|
+
/**
|
|
2824
|
+
* Default join options.
|
|
2825
|
+
*/
|
|
2826
|
+
const DEFAULT_JOIN_OPTIONS = {
|
|
2827
|
+
timeoutMs: 3e4,
|
|
2828
|
+
requireCryptoVerification: true,
|
|
2829
|
+
cacheResult: true,
|
|
2830
|
+
cacheTtlMs: 3e5
|
|
2831
|
+
};
|
|
2832
|
+
/**
|
|
2833
|
+
* Default escrow configuration.
|
|
2834
|
+
*/
|
|
2835
|
+
const DEFAULT_ESCROW_CONFIG = {
|
|
2836
|
+
enabled: false,
|
|
2837
|
+
requiredParties: 2,
|
|
2838
|
+
timeoutMs: 6e4
|
|
2839
|
+
};
|
|
2840
|
+
/**
|
|
2841
|
+
* Default join security policy.
|
|
2842
|
+
*/
|
|
2843
|
+
const DEFAULT_JOIN_SECURITY_POLICY = {
|
|
2844
|
+
verifyIdentityHashes: true,
|
|
2845
|
+
checkRevocation: true,
|
|
2846
|
+
maxProxyDepth: 10,
|
|
2847
|
+
auditLog: true,
|
|
2848
|
+
allowedVats: []
|
|
2849
|
+
};
|
|
2850
|
+
|
|
2851
|
+
//#endregion
|
|
2852
|
+
//#region src/rpc/level4-handlers.ts
|
|
2853
|
+
/**
|
|
2854
|
+
* Manages Level 4 RPC message handling for reference equality verification.
|
|
2855
|
+
*
|
|
2856
|
+
* This class handles:
|
|
2857
|
+
* 1. Join messages - verifying that two capabilities point to the same object
|
|
2858
|
+
* 2. Object identity tracking and caching
|
|
2859
|
+
* 3. Escrow agent functionality for consensus verification
|
|
2860
|
+
* 4. Security verification (anti-spoofing)
|
|
2861
|
+
*
|
|
2862
|
+
* ## Usage Example
|
|
2863
|
+
*
|
|
2864
|
+
* ```typescript
|
|
2865
|
+
* const level4Handlers = new Level4Handlers({
|
|
2866
|
+
* connection,
|
|
2867
|
+
* connectionManager,
|
|
2868
|
+
* level3Handlers,
|
|
2869
|
+
* selfVatId,
|
|
2870
|
+
* });
|
|
2871
|
+
*
|
|
2872
|
+
* // Enable escrow mode for consensus verification
|
|
2873
|
+
* level4Handlers.setEscrowConfig({
|
|
2874
|
+
* enabled: true,
|
|
2875
|
+
* requiredParties: 2,
|
|
2876
|
+
* });
|
|
2877
|
+
*
|
|
2878
|
+
* // Send a Join request
|
|
2879
|
+
* const result = await level4Handlers.sendJoin(target1, target2);
|
|
2880
|
+
* if (result.equal) {
|
|
2881
|
+
* console.log('Capabilities point to the same object!');
|
|
2882
|
+
* }
|
|
2883
|
+
* ```
|
|
2884
|
+
*/
|
|
2885
|
+
var Level4Handlers = class {
|
|
2886
|
+
options;
|
|
2887
|
+
pendingJoins = /* @__PURE__ */ new Map();
|
|
2888
|
+
joinResultsCache = /* @__PURE__ */ new Map();
|
|
2889
|
+
objectIdentities = /* @__PURE__ */ new Map();
|
|
2890
|
+
nextJoinId = 1;
|
|
2891
|
+
escrowConfig;
|
|
2892
|
+
securityPolicy;
|
|
2893
|
+
joinOptions;
|
|
2894
|
+
escrowParties = /* @__PURE__ */ new Map();
|
|
2895
|
+
escrowConsensus;
|
|
2896
|
+
constructor(options) {
|
|
2897
|
+
this.options = options;
|
|
2898
|
+
this.escrowConfig = {
|
|
2899
|
+
...DEFAULT_ESCROW_CONFIG,
|
|
2900
|
+
...options.escrowConfig
|
|
2901
|
+
};
|
|
2902
|
+
this.securityPolicy = {
|
|
2903
|
+
...DEFAULT_JOIN_SECURITY_POLICY,
|
|
2904
|
+
...options.securityPolicy
|
|
2905
|
+
};
|
|
2906
|
+
this.joinOptions = {
|
|
2907
|
+
...DEFAULT_JOIN_OPTIONS,
|
|
2908
|
+
...options.joinOptions
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Handle an incoming Join message.
|
|
2913
|
+
*
|
|
2914
|
+
* When we receive a Join message, we need to verify whether the two
|
|
2915
|
+
* capability references point to the same underlying object.
|
|
2916
|
+
*
|
|
2917
|
+
* The verification process:
|
|
2918
|
+
* 1. Resolve both targets to their underlying objects
|
|
2919
|
+
* 2. Compare object identities (vat ID + object ID)
|
|
2920
|
+
* 3. Optionally verify identity hashes cryptographically
|
|
2921
|
+
* 4. Return the result
|
|
2922
|
+
*/
|
|
2923
|
+
async handleJoin(join) {
|
|
2924
|
+
const { questionId, target, otherCap, joinId } = join;
|
|
2925
|
+
try {
|
|
2926
|
+
const cacheKey = this.getCacheKey(target, otherCap);
|
|
2927
|
+
const cached = this.joinResultsCache.get(cacheKey);
|
|
2928
|
+
if (cached && Date.now() - cached.cachedAt < this.joinOptions.cacheTtlMs) {
|
|
2929
|
+
await this.sendJoinResult(questionId, cached.result);
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
const identity1 = await this.resolveTargetToIdentity(target);
|
|
2933
|
+
const identity2 = await this.resolveTargetToIdentity(otherCap);
|
|
2934
|
+
const result = this.compareIdentities(identity1, identity2, joinId);
|
|
2935
|
+
if (this.joinOptions.cacheResult) this.joinResultsCache.set(cacheKey, {
|
|
2936
|
+
result,
|
|
2937
|
+
cachedAt: Date.now(),
|
|
2938
|
+
targets: [this.hashTarget(target), this.hashTarget(otherCap)]
|
|
2939
|
+
});
|
|
2940
|
+
if (this.securityPolicy.auditLog) this.logJoinOperation(target, otherCap, result);
|
|
2941
|
+
await this.sendJoinResult(questionId, result);
|
|
2942
|
+
if (this.options.onJoin) await this.options.onJoin(join);
|
|
2943
|
+
} catch (error) {
|
|
2944
|
+
await this.sendJoinException(questionId, error instanceof Error ? error.message : "Join operation failed");
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Send a Join message to verify that two capabilities point to the same object.
|
|
2949
|
+
*
|
|
2950
|
+
* @param target1 First capability target
|
|
2951
|
+
* @param target2 Second capability target
|
|
2952
|
+
* @returns Promise resolving to the join result
|
|
2953
|
+
*/
|
|
2954
|
+
async sendJoin(target1, target2) {
|
|
2955
|
+
const { connection } = this.options;
|
|
2956
|
+
const joinId = this.nextJoinId++;
|
|
2957
|
+
const questionId = connection.createQuestion();
|
|
2958
|
+
const pendingJoin = {
|
|
2959
|
+
joinId,
|
|
2960
|
+
target1,
|
|
2961
|
+
target2,
|
|
2962
|
+
startedAt: Date.now(),
|
|
2963
|
+
resolve: () => {},
|
|
2964
|
+
reject: () => {}
|
|
2965
|
+
};
|
|
2966
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
2967
|
+
pendingJoin.resolve = resolve;
|
|
2968
|
+
pendingJoin.reject = reject;
|
|
2969
|
+
});
|
|
2970
|
+
this.pendingJoins.set(joinId, pendingJoin);
|
|
2971
|
+
const timeoutMs = this.joinOptions.timeoutMs;
|
|
2972
|
+
const timeoutId = setTimeout(() => {
|
|
2973
|
+
this.pendingJoins.delete(joinId);
|
|
2974
|
+
pendingJoin.reject(/* @__PURE__ */ new Error(`Join operation timed out after ${timeoutMs}ms`));
|
|
2975
|
+
}, timeoutMs);
|
|
2976
|
+
const originalResolve = pendingJoin.resolve;
|
|
2977
|
+
pendingJoin.resolve = (result) => {
|
|
2978
|
+
clearTimeout(timeoutId);
|
|
2979
|
+
this.pendingJoins.delete(joinId);
|
|
2980
|
+
originalResolve(result);
|
|
2981
|
+
};
|
|
2982
|
+
try {
|
|
2983
|
+
const joinMsg = {
|
|
2984
|
+
type: "join",
|
|
2985
|
+
join: {
|
|
2986
|
+
questionId,
|
|
2987
|
+
target: target1,
|
|
2988
|
+
otherCap: target2,
|
|
2989
|
+
joinId
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
await connection.sendCall(joinMsg.join);
|
|
2993
|
+
return await completionPromise;
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
clearTimeout(timeoutId);
|
|
2996
|
+
this.pendingJoins.delete(joinId);
|
|
2997
|
+
throw error;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* Complete a pending join operation with the result.
|
|
3002
|
+
* This is called when we receive a Return message for a Join.
|
|
3003
|
+
*/
|
|
3004
|
+
completeJoin(joinId, result) {
|
|
3005
|
+
const pending = this.pendingJoins.get(joinId);
|
|
3006
|
+
if (pending) {
|
|
3007
|
+
pending.resolve(result);
|
|
3008
|
+
this.pendingJoins.delete(joinId);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Resolve a MessageTarget to its ObjectIdentity.
|
|
3013
|
+
*
|
|
3014
|
+
* This may involve:
|
|
3015
|
+
* 1. Looking up local exports
|
|
3016
|
+
* 2. Resolving promises
|
|
3017
|
+
* 3. Following third-party capabilities
|
|
3018
|
+
* 4. Verifying proxy chains
|
|
3019
|
+
*/
|
|
3020
|
+
async resolveTargetToIdentity(target) {
|
|
3021
|
+
if (target.type === "importedCap") {
|
|
3022
|
+
const cached = this.objectIdentities.get(target.importId);
|
|
3023
|
+
if (cached) return cached;
|
|
3024
|
+
if (!this.options.connection.getImport(target.importId)) return null;
|
|
3025
|
+
return null;
|
|
3026
|
+
}
|
|
3027
|
+
if (target.type === "promisedAnswer") {
|
|
3028
|
+
const { questionId } = target.promisedAnswer;
|
|
3029
|
+
try {
|
|
3030
|
+
await this.options.connection.waitForAnswer(questionId);
|
|
3031
|
+
return null;
|
|
3032
|
+
} catch {
|
|
3033
|
+
return null;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Compare two object identities for equality.
|
|
3040
|
+
*/
|
|
3041
|
+
compareIdentities(identity1, identity2, joinId) {
|
|
3042
|
+
if (!identity1 && !identity2) return {
|
|
3043
|
+
equal: true,
|
|
3044
|
+
joinId
|
|
3045
|
+
};
|
|
3046
|
+
if (!identity1 || !identity2) return {
|
|
3047
|
+
equal: false,
|
|
3048
|
+
joinId,
|
|
3049
|
+
inequalityReason: "One capability is null, the other is not"
|
|
3050
|
+
};
|
|
3051
|
+
if (!this.arraysEqual(identity1.vatId, identity2.vatId)) return {
|
|
3052
|
+
equal: false,
|
|
3053
|
+
joinId,
|
|
3054
|
+
inequalityReason: "Capabilities hosted by different vats"
|
|
3055
|
+
};
|
|
3056
|
+
if (!this.arraysEqual(identity1.objectId, identity2.objectId)) return {
|
|
3057
|
+
equal: false,
|
|
3058
|
+
joinId,
|
|
3059
|
+
inequalityReason: "Different object IDs within the same vat"
|
|
3060
|
+
};
|
|
3061
|
+
if (this.securityPolicy.verifyIdentityHashes) {
|
|
3062
|
+
if (identity1.identityHash && identity2.identityHash) {
|
|
3063
|
+
if (!this.arraysEqual(identity1.identityHash, identity2.identityHash)) return {
|
|
3064
|
+
equal: false,
|
|
3065
|
+
joinId,
|
|
3066
|
+
inequalityReason: "Identity hash mismatch (possible spoofing attempt)"
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
return {
|
|
3071
|
+
equal: true,
|
|
3072
|
+
joinId,
|
|
3073
|
+
identity: identity1
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Set the escrow configuration.
|
|
3078
|
+
*/
|
|
3079
|
+
setEscrowConfig(config) {
|
|
3080
|
+
this.escrowConfig = {
|
|
3081
|
+
...this.escrowConfig,
|
|
3082
|
+
...config
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Register a party in an escrow consensus verification.
|
|
3087
|
+
*
|
|
3088
|
+
* This is used when multiple parties need to verify they are referring
|
|
3089
|
+
* to the same object (e.g., in a trade or agreement).
|
|
3090
|
+
*
|
|
3091
|
+
* @param partyId Unique identifier for the party
|
|
3092
|
+
* @param target The capability reference from this party
|
|
3093
|
+
* @returns Whether consensus has been reached
|
|
3094
|
+
*/
|
|
3095
|
+
async registerEscrowParty(partyId, target) {
|
|
3096
|
+
if (!this.escrowConfig.enabled) throw new Error("Escrow mode is not enabled");
|
|
3097
|
+
if (this.escrowParties.has(partyId)) throw new Error(`Party ${partyId} is already registered`);
|
|
3098
|
+
this.escrowParties.set(partyId, { target });
|
|
3099
|
+
if (this.escrowParties.size >= this.escrowConfig.requiredParties) {
|
|
3100
|
+
const consensus = await this.verifyEscrowConsensus();
|
|
3101
|
+
if (consensus.consensus) {
|
|
3102
|
+
this.escrowConsensus = {
|
|
3103
|
+
identity: consensus.identity,
|
|
3104
|
+
parties: Array.from(this.escrowParties.keys())
|
|
3105
|
+
};
|
|
3106
|
+
if (this.escrowConfig.onConsensus) this.escrowConfig.onConsensus(consensus.identity, Array.from(this.escrowParties.keys()));
|
|
3107
|
+
} else if (this.escrowConfig.onConsensusFailure) this.escrowConfig.onConsensusFailure(consensus.reason, Array.from(this.escrowParties.keys()));
|
|
3108
|
+
return {
|
|
3109
|
+
consensus: consensus.consensus,
|
|
3110
|
+
identity: consensus.identity
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
return { consensus: false };
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Verify that all registered escrow parties refer to the same object.
|
|
3117
|
+
*/
|
|
3118
|
+
async verifyEscrowConsensus() {
|
|
3119
|
+
const parties = Array.from(this.escrowParties.entries());
|
|
3120
|
+
if (parties.length < this.escrowConfig.requiredParties) return {
|
|
3121
|
+
consensus: false,
|
|
3122
|
+
reason: "Not enough parties registered"
|
|
3123
|
+
};
|
|
3124
|
+
const [firstPartyId, firstParty] = parties[0];
|
|
3125
|
+
const firstIdentity = await this.resolveTargetToIdentity(firstParty.target);
|
|
3126
|
+
if (!firstIdentity) return {
|
|
3127
|
+
consensus: false,
|
|
3128
|
+
reason: `Could not resolve identity for party ${firstPartyId}`
|
|
3129
|
+
};
|
|
3130
|
+
for (const [partyId, party] of parties.slice(1)) {
|
|
3131
|
+
const identity = await this.resolveTargetToIdentity(party.target);
|
|
3132
|
+
const comparison = this.compareIdentities(firstIdentity, identity, 0);
|
|
3133
|
+
if (!comparison.equal) return {
|
|
3134
|
+
consensus: false,
|
|
3135
|
+
reason: `Party ${partyId} refers to a different object: ${comparison.inequalityReason}`
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
return {
|
|
3139
|
+
consensus: true,
|
|
3140
|
+
identity: firstIdentity
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Clear all escrow state.
|
|
3145
|
+
*/
|
|
3146
|
+
clearEscrow() {
|
|
3147
|
+
this.escrowParties.clear();
|
|
3148
|
+
this.escrowConsensus = void 0;
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Get the current escrow consensus if reached.
|
|
3152
|
+
*/
|
|
3153
|
+
getEscrowConsensus() {
|
|
3154
|
+
return this.escrowConsensus;
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Set the security policy.
|
|
3158
|
+
*/
|
|
3159
|
+
setSecurityPolicy(policy) {
|
|
3160
|
+
this.securityPolicy = {
|
|
3161
|
+
...this.securityPolicy,
|
|
3162
|
+
...policy
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Verify that a vat is allowed to participate in join operations.
|
|
3167
|
+
*/
|
|
3168
|
+
isVatAllowed(vatId) {
|
|
3169
|
+
if (this.securityPolicy.allowedVats.length === 0) return true;
|
|
3170
|
+
return this.securityPolicy.allowedVats.some((allowed) => this.arraysEqual(allowed, vatId));
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Generate a cryptographic identity hash for an object.
|
|
3174
|
+
*
|
|
3175
|
+
* This creates a verifiable fingerprint of the object's identity
|
|
3176
|
+
* that can be used to detect spoofing attempts.
|
|
3177
|
+
*/
|
|
3178
|
+
async generateIdentityHash(vatId, objectId) {
|
|
3179
|
+
const combined = new Uint8Array(vatId.length + objectId.length);
|
|
3180
|
+
combined.set(vatId, 0);
|
|
3181
|
+
combined.set(objectId, vatId.length);
|
|
3182
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
3183
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", combined);
|
|
3184
|
+
return new Uint8Array(hashBuffer);
|
|
3185
|
+
}
|
|
3186
|
+
const { createHash } = require("node:crypto");
|
|
3187
|
+
const hash = createHash("sha256");
|
|
3188
|
+
hash.update(combined);
|
|
3189
|
+
return hash.digest();
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Clear the join results cache.
|
|
3193
|
+
*/
|
|
3194
|
+
clearCache() {
|
|
3195
|
+
this.joinResultsCache.clear();
|
|
3196
|
+
}
|
|
3197
|
+
/**
|
|
3198
|
+
* Clean up expired cache entries.
|
|
3199
|
+
*/
|
|
3200
|
+
cleanupExpiredCache() {
|
|
3201
|
+
const now = Date.now();
|
|
3202
|
+
let removed = 0;
|
|
3203
|
+
for (const [key, entry] of this.joinResultsCache) if (now - entry.cachedAt > this.joinOptions.cacheTtlMs) {
|
|
3204
|
+
this.joinResultsCache.delete(key);
|
|
3205
|
+
removed++;
|
|
3206
|
+
}
|
|
3207
|
+
return removed;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Get cache statistics.
|
|
3211
|
+
*/
|
|
3212
|
+
getCacheStats() {
|
|
3213
|
+
return {
|
|
3214
|
+
size: this.joinResultsCache.size,
|
|
3215
|
+
hitRate: 0
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
getCacheKey(target1, target2) {
|
|
3219
|
+
const sorted = [this.hashTarget(target1), this.hashTarget(target2)].sort();
|
|
3220
|
+
return `join:${sorted[0]}:${sorted[1]}`;
|
|
3221
|
+
}
|
|
3222
|
+
hashTarget(target) {
|
|
3223
|
+
if (target.type === "importedCap") return `import:${target.importId}`;
|
|
3224
|
+
if (target.type === "promisedAnswer") return `answer:${target.promisedAnswer.questionId}`;
|
|
3225
|
+
return "unknown";
|
|
3226
|
+
}
|
|
3227
|
+
arraysEqual(a, b) {
|
|
3228
|
+
if (a.length !== b.length) return false;
|
|
3229
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
async sendJoinResult(questionId, result) {
|
|
3233
|
+
const { connection } = this.options;
|
|
3234
|
+
const returnMsg = {
|
|
3235
|
+
type: "return",
|
|
3236
|
+
return: {
|
|
3237
|
+
answerId: questionId,
|
|
3238
|
+
releaseParamCaps: true,
|
|
3239
|
+
noFinishNeeded: false,
|
|
3240
|
+
result: {
|
|
3241
|
+
type: "results",
|
|
3242
|
+
payload: {
|
|
3243
|
+
content: this.serializeJoinResult(result),
|
|
3244
|
+
capTable: []
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
await connection.sendReturn(returnMsg.return);
|
|
3250
|
+
}
|
|
3251
|
+
async sendJoinException(questionId, reason) {
|
|
3252
|
+
const { connection } = this.options;
|
|
3253
|
+
const returnMsg = {
|
|
3254
|
+
type: "return",
|
|
3255
|
+
return: {
|
|
3256
|
+
answerId: questionId,
|
|
3257
|
+
releaseParamCaps: true,
|
|
3258
|
+
noFinishNeeded: false,
|
|
3259
|
+
result: {
|
|
3260
|
+
type: "exception",
|
|
3261
|
+
exception: {
|
|
3262
|
+
reason,
|
|
3263
|
+
type: "failed"
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
await connection.sendReturn(returnMsg.return);
|
|
3269
|
+
}
|
|
3270
|
+
serializeJoinResult(result) {
|
|
3271
|
+
const obj = {
|
|
3272
|
+
equal: result.equal,
|
|
3273
|
+
joinId: result.joinId,
|
|
3274
|
+
inequalityReason: result.inequalityReason
|
|
3275
|
+
};
|
|
3276
|
+
return new TextEncoder().encode(JSON.stringify(obj));
|
|
3277
|
+
}
|
|
3278
|
+
logJoinOperation(target1, target2, result) {
|
|
3279
|
+
console.log("[Level4] Join operation:", {
|
|
3280
|
+
target1: this.hashTarget(target1),
|
|
3281
|
+
target2: this.hashTarget(target2),
|
|
3282
|
+
equal: result.equal,
|
|
3283
|
+
joinId: result.joinId,
|
|
3284
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
|
|
3289
|
+
//#endregion
|
|
3290
|
+
//#region src/rpc/stream.ts
|
|
3291
|
+
/** Stream priority levels */
|
|
3292
|
+
let StreamPriority = /* @__PURE__ */ function(StreamPriority) {
|
|
3293
|
+
StreamPriority[StreamPriority["CRITICAL"] = 0] = "CRITICAL";
|
|
3294
|
+
StreamPriority[StreamPriority["HIGH"] = 1] = "HIGH";
|
|
3295
|
+
StreamPriority[StreamPriority["NORMAL"] = 2] = "NORMAL";
|
|
3296
|
+
StreamPriority[StreamPriority["LOW"] = 3] = "LOW";
|
|
3297
|
+
StreamPriority[StreamPriority["BACKGROUND"] = 4] = "BACKGROUND";
|
|
3298
|
+
return StreamPriority;
|
|
3299
|
+
}({});
|
|
3300
|
+
/** Default flow control configuration */
|
|
3301
|
+
const DEFAULT_FLOW_CONTROL = {
|
|
3302
|
+
initialWindowSize: 65536,
|
|
3303
|
+
maxWindowSize: 1048576,
|
|
3304
|
+
minWindowSize: 4096,
|
|
3305
|
+
windowUpdateThreshold: 16384,
|
|
3306
|
+
windowUpdateIncrement: 32768
|
|
3307
|
+
};
|
|
3308
|
+
/**
|
|
3309
|
+
* Stream abstraction for Cap'n Proto RPC
|
|
3310
|
+
*
|
|
3311
|
+
* Manages bidirectional streaming with flow control and backpressure.
|
|
3312
|
+
*/
|
|
3313
|
+
var Stream = class {
|
|
3314
|
+
options;
|
|
3315
|
+
handlers;
|
|
3316
|
+
state = "connecting";
|
|
3317
|
+
error;
|
|
3318
|
+
sendWindow;
|
|
3319
|
+
receiveWindow;
|
|
3320
|
+
flowControlConfig;
|
|
3321
|
+
sendBuffer = [];
|
|
3322
|
+
receiveBuffer = [];
|
|
3323
|
+
maxBufferSize = 1048576;
|
|
3324
|
+
bytesSent = 0;
|
|
3325
|
+
bytesReceived = 0;
|
|
3326
|
+
totalBytesExpected;
|
|
3327
|
+
lastProgressUpdate = 0;
|
|
3328
|
+
progressUpdateInterval;
|
|
3329
|
+
transferStartTime;
|
|
3330
|
+
nextSendSequence = 0;
|
|
3331
|
+
nextExpectedSequence = 0;
|
|
3332
|
+
openResolver;
|
|
3333
|
+
openRejector;
|
|
3334
|
+
closeResolver;
|
|
3335
|
+
constructor(options, handlers = {}) {
|
|
3336
|
+
this.options = options;
|
|
3337
|
+
this.handlers = handlers;
|
|
3338
|
+
this.flowControlConfig = {
|
|
3339
|
+
...DEFAULT_FLOW_CONTROL,
|
|
3340
|
+
...options.flowControl
|
|
3341
|
+
};
|
|
3342
|
+
this.sendWindow = {
|
|
3343
|
+
currentSize: this.flowControlConfig.initialWindowSize,
|
|
3344
|
+
maxSize: this.flowControlConfig.maxWindowSize,
|
|
3345
|
+
bytesInWindow: 0,
|
|
3346
|
+
backpressureActive: false
|
|
3347
|
+
};
|
|
3348
|
+
this.receiveWindow = {
|
|
3349
|
+
currentSize: this.flowControlConfig.initialWindowSize,
|
|
3350
|
+
maxSize: this.flowControlConfig.maxWindowSize,
|
|
3351
|
+
bytesInWindow: 0,
|
|
3352
|
+
backpressureActive: false
|
|
3353
|
+
};
|
|
3354
|
+
this.progressUpdateInterval = options.progressInterval ?? 65536;
|
|
3355
|
+
}
|
|
3356
|
+
/** Get stream ID */
|
|
3357
|
+
get id() {
|
|
3358
|
+
return this.options.streamId;
|
|
3359
|
+
}
|
|
3360
|
+
/** Get stream direction */
|
|
3361
|
+
get direction() {
|
|
3362
|
+
return this.options.direction;
|
|
3363
|
+
}
|
|
3364
|
+
/** Get stream priority */
|
|
3365
|
+
get priority() {
|
|
3366
|
+
return this.options.priority ?? StreamPriority.NORMAL;
|
|
3367
|
+
}
|
|
3368
|
+
/** Get current stream state */
|
|
3369
|
+
get currentState() {
|
|
3370
|
+
return this.state;
|
|
3371
|
+
}
|
|
3372
|
+
/** Get whether stream is open */
|
|
3373
|
+
get isOpen() {
|
|
3374
|
+
return this.state === "open";
|
|
3375
|
+
}
|
|
3376
|
+
/** Get whether backpressure is active for sending */
|
|
3377
|
+
get isBackpressureActive() {
|
|
3378
|
+
return this.sendWindow.backpressureActive;
|
|
3379
|
+
}
|
|
3380
|
+
/** Get bytes sent */
|
|
3381
|
+
get bytesSentCount() {
|
|
3382
|
+
return this.bytesSent;
|
|
3383
|
+
}
|
|
3384
|
+
/** Get bytes received */
|
|
3385
|
+
get bytesReceivedCount() {
|
|
3386
|
+
return this.bytesReceived;
|
|
3387
|
+
}
|
|
3388
|
+
/** Get metadata */
|
|
3389
|
+
get metadata() {
|
|
3390
|
+
return this.options.metadata;
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Open the stream
|
|
3394
|
+
*/
|
|
3395
|
+
async open() {
|
|
3396
|
+
if (this.state !== "connecting") throw new Error(`Cannot open stream in state: ${this.state}`);
|
|
3397
|
+
return new Promise((resolve, reject) => {
|
|
3398
|
+
this.openResolver = resolve;
|
|
3399
|
+
this.openRejector = reject;
|
|
3400
|
+
this.transitionState("open");
|
|
3401
|
+
this.transferStartTime = Date.now();
|
|
3402
|
+
this.handlers.onOpen?.();
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Send data through the stream
|
|
3407
|
+
*
|
|
3408
|
+
* Respects flow control and handles backpressure.
|
|
3409
|
+
*/
|
|
3410
|
+
async send(data, endOfStream = false) {
|
|
3411
|
+
if (this.state !== "open") throw new Error(`Cannot send in state: ${this.state}`);
|
|
3412
|
+
if (this.sendWindow.bytesInWindow + data.length > this.sendWindow.currentSize) await this.waitForWindowUpdate();
|
|
3413
|
+
const chunk = {
|
|
3414
|
+
data,
|
|
3415
|
+
endOfStream,
|
|
3416
|
+
sequenceNumber: this.nextSendSequence++,
|
|
3417
|
+
timestamp: Date.now()
|
|
3418
|
+
};
|
|
3419
|
+
this.sendWindow.bytesInWindow += data.length;
|
|
3420
|
+
this.bytesSent += data.length;
|
|
3421
|
+
this.checkBackpressure();
|
|
3422
|
+
await this.sendChunk(chunk);
|
|
3423
|
+
this.reportProgress();
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Send a chunk of data
|
|
3427
|
+
*
|
|
3428
|
+
* Override in subclasses to implement actual transport.
|
|
3429
|
+
*/
|
|
3430
|
+
async sendChunk(chunk) {
|
|
3431
|
+
this.sendBuffer.push(chunk);
|
|
3432
|
+
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Receive data from the stream
|
|
3435
|
+
*
|
|
3436
|
+
* Returns buffered data or waits for new data.
|
|
3437
|
+
*/
|
|
3438
|
+
async receive() {
|
|
3439
|
+
if (this.receiveBuffer.length > 0) return this.receiveBuffer.shift();
|
|
3440
|
+
if (this.state === "closed") return null;
|
|
3441
|
+
return new Promise((resolve, reject) => {
|
|
3442
|
+
const checkBuffer = () => {
|
|
3443
|
+
if (this.receiveBuffer.length > 0) resolve(this.receiveBuffer.shift());
|
|
3444
|
+
else if (this.state === "closed") resolve(null);
|
|
3445
|
+
else if (this.state === "error") reject(this.error ?? /* @__PURE__ */ new Error("Stream error"));
|
|
3446
|
+
else setTimeout(checkBuffer, 10);
|
|
3447
|
+
};
|
|
3448
|
+
checkBuffer();
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Handle incoming chunk from transport
|
|
3453
|
+
*/
|
|
3454
|
+
handleIncomingChunk(chunk) {
|
|
3455
|
+
if (this.state !== "open" && this.state !== "closing") return;
|
|
3456
|
+
this.receiveWindow.bytesInWindow += chunk.data.length;
|
|
3457
|
+
this.bytesReceived += chunk.data.length;
|
|
3458
|
+
this.checkReceiveWindow();
|
|
3459
|
+
if (this.handlers.onData) Promise.resolve(this.handlers.onData(chunk)).catch((err) => {
|
|
3460
|
+
this.handleError(err);
|
|
3461
|
+
});
|
|
3462
|
+
else this.receiveBuffer.push(chunk);
|
|
3463
|
+
this.reportProgress();
|
|
3464
|
+
if (chunk.endOfStream) this.transitionState("closing");
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Update the send window (called when receiving window update from peer)
|
|
3468
|
+
*/
|
|
3469
|
+
updateSendWindow(increment) {
|
|
3470
|
+
this.sendWindow.currentSize = Math.min(this.sendWindow.currentSize + increment, this.sendWindow.maxSize);
|
|
3471
|
+
if (this.sendWindow.backpressureActive) {
|
|
3472
|
+
if (this.sendWindow.currentSize - this.sendWindow.bytesInWindow >= this.flowControlConfig.minWindowSize) {
|
|
3473
|
+
this.sendWindow.backpressureActive = false;
|
|
3474
|
+
this.handlers.onBackpressure?.(false);
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Acknowledge received bytes (called to update peer's send window)
|
|
3480
|
+
*/
|
|
3481
|
+
acknowledgeBytes(bytes) {
|
|
3482
|
+
this.receiveWindow.bytesInWindow = Math.max(0, this.receiveWindow.bytesInWindow - bytes);
|
|
3483
|
+
if (this.receiveWindow.bytesInWindow < this.flowControlConfig.windowUpdateThreshold) this.sendWindowUpdate();
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Close the stream gracefully
|
|
3487
|
+
*/
|
|
3488
|
+
async close() {
|
|
3489
|
+
if (this.state === "closed" || this.state === "closing") return;
|
|
3490
|
+
this.transitionState("closing");
|
|
3491
|
+
await this.drainSendBuffer();
|
|
3492
|
+
await this.sendChunk({
|
|
3493
|
+
data: new Uint8Array(0),
|
|
3494
|
+
endOfStream: true
|
|
3495
|
+
});
|
|
3496
|
+
this.transitionState("closed");
|
|
3497
|
+
this.handlers.onClose?.();
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Abort the stream with an error
|
|
3501
|
+
*/
|
|
3502
|
+
abort(error) {
|
|
3503
|
+
this.error = error;
|
|
3504
|
+
this.transitionState("error");
|
|
3505
|
+
this.handlers.onError?.(error);
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Set total bytes expected (for progress calculation)
|
|
3509
|
+
*/
|
|
3510
|
+
setTotalBytesExpected(total) {
|
|
3511
|
+
this.totalBytesExpected = total;
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* Wait for the stream to be ready for sending
|
|
3515
|
+
*/
|
|
3516
|
+
async ready() {
|
|
3517
|
+
if (this.state === "open" && !this.sendWindow.backpressureActive) return;
|
|
3518
|
+
if (this.state !== "open") throw new Error(`Stream not open: ${this.state}`);
|
|
3519
|
+
return this.waitForWindowUpdate();
|
|
3520
|
+
}
|
|
3521
|
+
transitionState(newState) {
|
|
3522
|
+
this.state;
|
|
3523
|
+
this.state = newState;
|
|
3524
|
+
if (newState === "open" && this.openResolver) {
|
|
3525
|
+
this.openResolver();
|
|
3526
|
+
this.openResolver = void 0;
|
|
3527
|
+
this.openRejector = void 0;
|
|
3528
|
+
} else if (newState === "error" && this.openRejector) {
|
|
3529
|
+
this.openRejector(this.error ?? /* @__PURE__ */ new Error("Stream error"));
|
|
3530
|
+
this.openResolver = void 0;
|
|
3531
|
+
this.openRejector = void 0;
|
|
3532
|
+
}
|
|
3533
|
+
if (newState === "closed" && this.closeResolver) {
|
|
3534
|
+
this.closeResolver();
|
|
3535
|
+
this.closeResolver = void 0;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
checkBackpressure() {
|
|
3539
|
+
if (this.sendWindow.currentSize - this.sendWindow.bytesInWindow < this.flowControlConfig.minWindowSize && !this.sendWindow.backpressureActive) {
|
|
3540
|
+
this.sendWindow.backpressureActive = true;
|
|
3541
|
+
this.handlers.onBackpressure?.(true);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
checkReceiveWindow() {
|
|
3545
|
+
if (this.receiveWindow.bytesInWindow < this.flowControlConfig.windowUpdateThreshold) this.sendWindowUpdate();
|
|
3546
|
+
}
|
|
3547
|
+
async waitForWindowUpdate() {
|
|
3548
|
+
return new Promise((resolve, reject) => {
|
|
3549
|
+
const checkWindow = () => {
|
|
3550
|
+
const available = this.sendWindow.currentSize - this.sendWindow.bytesInWindow;
|
|
3551
|
+
if (this.state === "error") {
|
|
3552
|
+
reject(this.error ?? /* @__PURE__ */ new Error("Stream error"));
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
if (this.state !== "open") {
|
|
3556
|
+
reject(/* @__PURE__ */ new Error("Stream closed"));
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
if (available >= this.flowControlConfig.minWindowSize) {
|
|
3560
|
+
resolve();
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
setTimeout(checkWindow, 10);
|
|
3564
|
+
};
|
|
3565
|
+
checkWindow();
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
async drainSendBuffer() {
|
|
3569
|
+
this.sendBuffer.length = 0;
|
|
3570
|
+
}
|
|
3571
|
+
sendWindowUpdate() {}
|
|
3572
|
+
reportProgress() {
|
|
3573
|
+
if (!this.options.enableProgress || !this.handlers.onProgress) return;
|
|
3574
|
+
const now = Date.now();
|
|
3575
|
+
const bytesTransferred = Math.max(this.bytesSent, this.bytesReceived);
|
|
3576
|
+
if (bytesTransferred - this.lastProgressUpdate < this.progressUpdateInterval) return;
|
|
3577
|
+
this.lastProgressUpdate = bytesTransferred;
|
|
3578
|
+
let transferRate;
|
|
3579
|
+
if (this.transferStartTime) {
|
|
3580
|
+
const elapsed = (now - this.transferStartTime) / 1e3;
|
|
3581
|
+
if (elapsed > 0) transferRate = bytesTransferred / elapsed;
|
|
3582
|
+
}
|
|
3583
|
+
let percentage;
|
|
3584
|
+
let estimatedTimeRemaining;
|
|
3585
|
+
if (this.totalBytesExpected && this.totalBytesExpected > 0) {
|
|
3586
|
+
percentage = Math.min(100, bytesTransferred / this.totalBytesExpected * 100);
|
|
3587
|
+
if (transferRate && transferRate > 0) estimatedTimeRemaining = (this.totalBytesExpected - bytesTransferred) / transferRate * 1e3;
|
|
3588
|
+
}
|
|
3589
|
+
const progress = {
|
|
3590
|
+
streamId: this.id,
|
|
3591
|
+
bytesTransferred,
|
|
3592
|
+
totalBytes: this.totalBytesExpected,
|
|
3593
|
+
percentage,
|
|
3594
|
+
transferRate,
|
|
3595
|
+
estimatedTimeRemaining
|
|
3596
|
+
};
|
|
3597
|
+
this.handlers.onProgress(progress);
|
|
3598
|
+
}
|
|
3599
|
+
handleError(error) {
|
|
3600
|
+
this.error = error;
|
|
3601
|
+
this.transitionState("error");
|
|
3602
|
+
this.handlers.onError?.(error);
|
|
3603
|
+
}
|
|
3604
|
+
};
|
|
3605
|
+
/**
|
|
3606
|
+
* Create a new stream
|
|
3607
|
+
*/
|
|
3608
|
+
function createStream(options, handlers) {
|
|
3609
|
+
return new Stream(options, handlers);
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Check if an object is a Stream
|
|
3613
|
+
*/
|
|
3614
|
+
function isStream(obj) {
|
|
3615
|
+
return obj instanceof Stream;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
//#endregion
|
|
3619
|
+
//#region src/rpc/bulk.ts
|
|
3620
|
+
/** Default bulk transfer configuration */
|
|
3621
|
+
const DEFAULT_BULK_CONFIG = {
|
|
3622
|
+
chunkSize: 16384,
|
|
3623
|
+
enableProgress: true,
|
|
3624
|
+
progressInterval: 65536,
|
|
3625
|
+
maxConcurrentChunks: 8,
|
|
3626
|
+
chunkAckTimeoutMs: 3e4
|
|
3627
|
+
};
|
|
3628
|
+
/**
|
|
3629
|
+
* Bulk transfer manager
|
|
3630
|
+
*
|
|
3631
|
+
* Manages high-volume data transfers with flow control and backpressure.
|
|
3632
|
+
*/
|
|
3633
|
+
var BulkTransfer = class {
|
|
3634
|
+
stream;
|
|
3635
|
+
config;
|
|
3636
|
+
handlers;
|
|
3637
|
+
metadata;
|
|
3638
|
+
direction;
|
|
3639
|
+
state = "pending";
|
|
3640
|
+
error;
|
|
3641
|
+
chunksInFlight = 0;
|
|
3642
|
+
chunksAcknowledged = 0;
|
|
3643
|
+
totalChunks = 0;
|
|
3644
|
+
startTime;
|
|
3645
|
+
endTime;
|
|
3646
|
+
pendingChunks = /* @__PURE__ */ new Map();
|
|
3647
|
+
chunkAckCallbacks = /* @__PURE__ */ new Map();
|
|
3648
|
+
currentWindowSize;
|
|
3649
|
+
dataSource;
|
|
3650
|
+
dataSink;
|
|
3651
|
+
constructor(stream, direction, metadata, config = {}, handlers = {}) {
|
|
3652
|
+
this.stream = stream;
|
|
3653
|
+
this.direction = direction;
|
|
3654
|
+
this.metadata = metadata;
|
|
3655
|
+
this.config = {
|
|
3656
|
+
...DEFAULT_BULK_CONFIG,
|
|
3657
|
+
...config
|
|
3658
|
+
};
|
|
3659
|
+
this.handlers = handlers;
|
|
3660
|
+
this.currentWindowSize = this.config.flowControl?.initialWindowSize ?? DEFAULT_FLOW_CONTROL.initialWindowSize;
|
|
3661
|
+
this.setupStreamHandlers();
|
|
3662
|
+
}
|
|
3663
|
+
/** Get transfer ID */
|
|
3664
|
+
get id() {
|
|
3665
|
+
return this.metadata.id;
|
|
3666
|
+
}
|
|
3667
|
+
/** Get current state */
|
|
3668
|
+
get currentState() {
|
|
3669
|
+
return this.state;
|
|
3670
|
+
}
|
|
3671
|
+
/** Get transfer statistics */
|
|
3672
|
+
get stats() {
|
|
3673
|
+
const now = Date.now();
|
|
3674
|
+
const elapsed = this.startTime ? (this.endTime ?? now) - this.startTime : 0;
|
|
3675
|
+
const bytesTransferred = this.stream.bytesSentCount + this.stream.bytesReceivedCount;
|
|
3676
|
+
let transferRate = 0;
|
|
3677
|
+
if (elapsed > 0) transferRate = bytesTransferred / elapsed * 1e3;
|
|
3678
|
+
let estimatedTimeRemaining;
|
|
3679
|
+
if (this.metadata.totalSize && transferRate > 0) estimatedTimeRemaining = (this.metadata.totalSize - bytesTransferred) / transferRate * 1e3;
|
|
3680
|
+
return {
|
|
3681
|
+
bytesTransferred,
|
|
3682
|
+
totalBytes: this.metadata.totalSize,
|
|
3683
|
+
transferRate,
|
|
3684
|
+
elapsedTime: elapsed,
|
|
3685
|
+
estimatedTimeRemaining,
|
|
3686
|
+
chunksTransferred: this.chunksAcknowledged + this.pendingChunks.size,
|
|
3687
|
+
chunksAcknowledged: this.chunksAcknowledged,
|
|
3688
|
+
currentWindowSize: this.currentWindowSize,
|
|
3689
|
+
backpressureActive: this.stream.isBackpressureActive
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Set the data source for upload
|
|
3694
|
+
*/
|
|
3695
|
+
setDataSource(source) {
|
|
3696
|
+
if (this.direction !== "upload") throw new Error("Data source only valid for uploads");
|
|
3697
|
+
this.dataSource = source;
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3700
|
+
* Set the data sink for download
|
|
3701
|
+
*/
|
|
3702
|
+
setDataSink(sink) {
|
|
3703
|
+
if (this.direction !== "download") throw new Error("Data sink only valid for downloads");
|
|
3704
|
+
this.dataSink = sink;
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Start the bulk transfer
|
|
3708
|
+
*/
|
|
3709
|
+
async start() {
|
|
3710
|
+
if (this.state !== "pending") throw new Error(`Cannot start transfer in state: ${this.state}`);
|
|
3711
|
+
this.state = "transferring";
|
|
3712
|
+
this.startTime = Date.now();
|
|
3713
|
+
this.handlers.onStart?.();
|
|
3714
|
+
if (this.metadata.totalSize) this.stream.setTotalBytesExpected(this.metadata.totalSize);
|
|
3715
|
+
try {
|
|
3716
|
+
if (this.direction === "upload") await this.performUpload();
|
|
3717
|
+
else await this.performDownload();
|
|
3718
|
+
if (this.state === "transferring") {
|
|
3719
|
+
this.state = "completed";
|
|
3720
|
+
this.endTime = Date.now();
|
|
3721
|
+
this.handlers.onComplete?.();
|
|
3722
|
+
}
|
|
3723
|
+
} catch (err) {
|
|
3724
|
+
this.handleError(err);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* Pause the transfer
|
|
3729
|
+
*/
|
|
3730
|
+
pause() {
|
|
3731
|
+
if (this.state === "transferring") this.state = "paused";
|
|
3732
|
+
}
|
|
3733
|
+
/**
|
|
3734
|
+
* Resume the transfer
|
|
3735
|
+
*/
|
|
3736
|
+
resume() {
|
|
3737
|
+
if (this.state === "paused") this.state = "transferring";
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Cancel the transfer
|
|
3741
|
+
*/
|
|
3742
|
+
cancel() {
|
|
3743
|
+
if (this.state === "completed" || this.state === "error" || this.state === "cancelled") return;
|
|
3744
|
+
this.state = "cancelled";
|
|
3745
|
+
this.endTime = Date.now();
|
|
3746
|
+
for (const { timeout } of this.pendingChunks.values()) clearTimeout(timeout);
|
|
3747
|
+
this.pendingChunks.clear();
|
|
3748
|
+
this.handlers.onCancel?.();
|
|
3749
|
+
}
|
|
3750
|
+
/**
|
|
3751
|
+
* Handle chunk acknowledgment from peer
|
|
3752
|
+
*/
|
|
3753
|
+
handleChunkAck(ack) {
|
|
3754
|
+
const pending = this.pendingChunks.get(ack.sequenceNumber);
|
|
3755
|
+
if (pending) {
|
|
3756
|
+
clearTimeout(pending.timeout);
|
|
3757
|
+
this.pendingChunks.delete(ack.sequenceNumber);
|
|
3758
|
+
this.chunksAcknowledged++;
|
|
3759
|
+
this.chunksInFlight--;
|
|
3760
|
+
this.currentWindowSize += ack.bytesAcknowledged;
|
|
3761
|
+
const callback = this.chunkAckCallbacks.get(ack.sequenceNumber);
|
|
3762
|
+
if (callback) {
|
|
3763
|
+
callback();
|
|
3764
|
+
this.chunkAckCallbacks.delete(ack.sequenceNumber);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Update flow control window
|
|
3770
|
+
*/
|
|
3771
|
+
updateWindow(newWindowSize) {
|
|
3772
|
+
this.currentWindowSize = newWindowSize;
|
|
3773
|
+
this.stream.updateSendWindow(newWindowSize);
|
|
3774
|
+
}
|
|
3775
|
+
setupStreamHandlers() {}
|
|
3776
|
+
async performUpload() {
|
|
3777
|
+
if (!this.dataSource) throw new Error("No data source set for upload");
|
|
3778
|
+
if (Symbol.asyncIterator in this.dataSource) for await (const data of this.dataSource) {
|
|
3779
|
+
if (this.state !== "transferring") break;
|
|
3780
|
+
await this.sendChunkWithFlowControl(data);
|
|
3781
|
+
}
|
|
3782
|
+
else {
|
|
3783
|
+
const sourceFn = this.dataSource;
|
|
3784
|
+
while (this.state === "transferring") {
|
|
3785
|
+
const data = await sourceFn();
|
|
3786
|
+
if (!data || data.length === 0) break;
|
|
3787
|
+
await this.sendChunkWithFlowControl(data);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
await this.waitForAllAcks();
|
|
3791
|
+
}
|
|
3792
|
+
async performDownload() {
|
|
3793
|
+
if (!this.dataSink) throw new Error("No data sink set for download");
|
|
3794
|
+
while (this.state === "transferring") await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3795
|
+
}
|
|
3796
|
+
async sendChunkWithFlowControl(data) {
|
|
3797
|
+
while (this.chunksInFlight >= (this.config.maxConcurrentChunks ?? 8)) {
|
|
3798
|
+
if (this.state !== "transferring") return;
|
|
3799
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
3800
|
+
}
|
|
3801
|
+
await this.stream.ready();
|
|
3802
|
+
const sequenceNumber = this.totalChunks++;
|
|
3803
|
+
await this.stream.send(data, false);
|
|
3804
|
+
this.chunksInFlight++;
|
|
3805
|
+
const timeout = setTimeout(() => {
|
|
3806
|
+
this.handleChunkTimeout(sequenceNumber);
|
|
3807
|
+
}, this.config.chunkAckTimeoutMs ?? 3e4);
|
|
3808
|
+
this.pendingChunks.set(sequenceNumber, {
|
|
3809
|
+
chunk: {
|
|
3810
|
+
data,
|
|
3811
|
+
sequenceNumber,
|
|
3812
|
+
timestamp: Date.now()
|
|
3813
|
+
},
|
|
3814
|
+
timeout
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
async sendChunkAck(_sequenceNumber, _bytes) {}
|
|
3818
|
+
handleChunkTimeout(sequenceNumber) {
|
|
3819
|
+
if (this.pendingChunks.get(sequenceNumber)) {
|
|
3820
|
+
this.pendingChunks.delete(sequenceNumber);
|
|
3821
|
+
this.chunksInFlight--;
|
|
3822
|
+
this.handleError(/* @__PURE__ */ new Error(`Chunk ${sequenceNumber} acknowledgment timeout`));
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
async waitForAllAcks() {
|
|
3826
|
+
for (const [sequenceNumber, { timeout }] of this.pendingChunks) {
|
|
3827
|
+
clearTimeout(timeout);
|
|
3828
|
+
this.handleChunkAck({
|
|
3829
|
+
sequenceNumber,
|
|
3830
|
+
bytesAcknowledged: 0
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
this.pendingChunks.clear();
|
|
3834
|
+
}
|
|
3835
|
+
handleError(error) {
|
|
3836
|
+
if (this.state === "completed" || this.state === "error") return;
|
|
3837
|
+
this.error = error;
|
|
3838
|
+
this.state = "error";
|
|
3839
|
+
this.endTime = Date.now();
|
|
3840
|
+
for (const { timeout } of this.pendingChunks.values()) clearTimeout(timeout);
|
|
3841
|
+
this.handlers.onError?.(error);
|
|
3842
|
+
}
|
|
3843
|
+
};
|
|
3844
|
+
/**
|
|
3845
|
+
* Bulk transfer manager
|
|
3846
|
+
*
|
|
3847
|
+
* Manages multiple concurrent bulk transfers.
|
|
3848
|
+
*/
|
|
3849
|
+
var BulkTransferManager = class {
|
|
3850
|
+
transfers = /* @__PURE__ */ new Map();
|
|
3851
|
+
streams = /* @__PURE__ */ new Map();
|
|
3852
|
+
nextStreamId = 1;
|
|
3853
|
+
/**
|
|
3854
|
+
* Create a new bulk transfer
|
|
3855
|
+
*/
|
|
3856
|
+
createTransfer(direction, metadata, config, handlers) {
|
|
3857
|
+
const streamId = this.nextStreamId++;
|
|
3858
|
+
const stream = new Stream({
|
|
3859
|
+
streamId,
|
|
3860
|
+
direction: direction === "upload" ? "outbound" : "inbound",
|
|
3861
|
+
priority: StreamPriority.NORMAL,
|
|
3862
|
+
enableProgress: config?.enableProgress ?? true,
|
|
3863
|
+
progressInterval: config?.progressInterval,
|
|
3864
|
+
flowControl: config?.flowControl
|
|
3865
|
+
}, {
|
|
3866
|
+
onProgress: handlers?.onProgress,
|
|
3867
|
+
onBackpressure: handlers?.onBackpressure,
|
|
3868
|
+
onError: handlers?.onError
|
|
3869
|
+
});
|
|
3870
|
+
this.streams.set(streamId, stream);
|
|
3871
|
+
const transfer = new BulkTransfer(stream, direction, metadata, config, handlers);
|
|
3872
|
+
this.transfers.set(metadata.id, transfer);
|
|
3873
|
+
return transfer;
|
|
3874
|
+
}
|
|
3875
|
+
/**
|
|
3876
|
+
* Get a transfer by ID
|
|
3877
|
+
*/
|
|
3878
|
+
getTransfer(id) {
|
|
3879
|
+
return this.transfers.get(id);
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Get a stream by ID
|
|
3883
|
+
*/
|
|
3884
|
+
getStream(id) {
|
|
3885
|
+
return this.streams.get(id);
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Remove a transfer
|
|
3889
|
+
*/
|
|
3890
|
+
removeTransfer(id) {
|
|
3891
|
+
const transfer = this.transfers.get(id);
|
|
3892
|
+
if (transfer) {
|
|
3893
|
+
transfer.cancel();
|
|
3894
|
+
this.transfers.delete(id);
|
|
3895
|
+
return true;
|
|
3896
|
+
}
|
|
3897
|
+
return false;
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Get all active transfers
|
|
3901
|
+
*/
|
|
3902
|
+
getActiveTransfers() {
|
|
3903
|
+
return Array.from(this.transfers.values()).filter((t) => t.currentState === "pending" || t.currentState === "transferring" || t.currentState === "paused");
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Get transfer statistics summary
|
|
3907
|
+
*/
|
|
3908
|
+
getStatsSummary() {
|
|
3909
|
+
const transfers = Array.from(this.transfers.values());
|
|
3910
|
+
const active = transfers.filter((t) => t.currentState === "transferring");
|
|
3911
|
+
const completed = transfers.filter((t) => t.currentState === "completed");
|
|
3912
|
+
const totalBytes = transfers.reduce((sum, t) => sum + t.stats.bytesTransferred, 0);
|
|
3913
|
+
return {
|
|
3914
|
+
totalTransfers: transfers.length,
|
|
3915
|
+
activeTransfers: active.length,
|
|
3916
|
+
completedTransfers: completed.length,
|
|
3917
|
+
totalBytesTransferred: totalBytes
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Close all transfers
|
|
3922
|
+
*/
|
|
3923
|
+
async closeAll() {
|
|
3924
|
+
for (const transfer of this.transfers.values()) transfer.cancel();
|
|
3925
|
+
this.transfers.clear();
|
|
3926
|
+
this.streams.clear();
|
|
3927
|
+
}
|
|
3928
|
+
};
|
|
3929
|
+
/**
|
|
3930
|
+
* Create a bulk transfer manager
|
|
3931
|
+
*/
|
|
3932
|
+
function createBulkTransferManager() {
|
|
3933
|
+
return new BulkTransferManager();
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
//#endregion
|
|
3937
|
+
//#region src/rpc/realtime.ts
|
|
3938
|
+
/**
|
|
3939
|
+
* Realtime API - Real-time communication with prioritization
|
|
3940
|
+
*
|
|
3941
|
+
* Phase 5: Flow Control and Realtime Communication
|
|
3942
|
+
*
|
|
3943
|
+
* Features:
|
|
3944
|
+
* - Message priority queues
|
|
3945
|
+
* - Message drop policies for latency-sensitive scenarios
|
|
3946
|
+
* - Bandwidth adaptation
|
|
3947
|
+
* - Jitter buffer management
|
|
3948
|
+
*/
|
|
3949
|
+
/** Message drop policy for latency-sensitive scenarios */
|
|
3950
|
+
let DropPolicy = /* @__PURE__ */ function(DropPolicy) {
|
|
3951
|
+
/** Never drop messages */
|
|
3952
|
+
DropPolicy["NEVER"] = "never";
|
|
3953
|
+
/** Drop oldest messages when queue is full */
|
|
3954
|
+
DropPolicy["DROP_OLDEST"] = "drop_oldest";
|
|
3955
|
+
/** Drop newest messages when queue is full */
|
|
3956
|
+
DropPolicy["DROP_NEWEST"] = "drop_newest";
|
|
3957
|
+
/** Drop low priority messages first */
|
|
3958
|
+
DropPolicy["DROP_LOW_PRIORITY"] = "drop_low_priority";
|
|
3959
|
+
/** Drop messages that exceed max latency */
|
|
3960
|
+
DropPolicy["DROP_STALE"] = "drop_stale";
|
|
3961
|
+
return DropPolicy;
|
|
3962
|
+
}({});
|
|
3963
|
+
/** Default realtime configuration */
|
|
3964
|
+
const DEFAULT_REALTIME_CONFIG = {
|
|
3965
|
+
targetLatencyMs: 50,
|
|
3966
|
+
maxLatencyMs: 200,
|
|
3967
|
+
jitterBufferMs: 30,
|
|
3968
|
+
maxQueueSize: 1e3,
|
|
3969
|
+
dropPolicy: DropPolicy.DROP_STALE,
|
|
3970
|
+
adaptiveBitrate: true,
|
|
3971
|
+
minBitrate: 16e3,
|
|
3972
|
+
maxBitrate: 10485760,
|
|
3973
|
+
bandwidthWindowMs: 1e3
|
|
3974
|
+
};
|
|
3975
|
+
/**
|
|
3976
|
+
* Priority queue for realtime messages
|
|
3977
|
+
*/
|
|
3978
|
+
var PriorityMessageQueue = class {
|
|
3979
|
+
queues = /* @__PURE__ */ new Map();
|
|
3980
|
+
totalSize = 0;
|
|
3981
|
+
maxSize;
|
|
3982
|
+
dropPolicy;
|
|
3983
|
+
maxLatencyMs;
|
|
3984
|
+
constructor(maxSize, dropPolicy, maxLatencyMs) {
|
|
3985
|
+
this.maxSize = maxSize;
|
|
3986
|
+
this.dropPolicy = dropPolicy;
|
|
3987
|
+
this.maxLatencyMs = maxLatencyMs;
|
|
3988
|
+
for (let i = 0; i <= 4; i++) this.queues.set(i, []);
|
|
3989
|
+
}
|
|
3990
|
+
/** Get total queue size */
|
|
3991
|
+
get size() {
|
|
3992
|
+
return this.totalSize;
|
|
3993
|
+
}
|
|
3994
|
+
/** Check if queue is empty */
|
|
3995
|
+
get isEmpty() {
|
|
3996
|
+
return this.totalSize === 0;
|
|
3997
|
+
}
|
|
3998
|
+
/** Enqueue a message */
|
|
3999
|
+
enqueue(message) {
|
|
4000
|
+
if (this.dropPolicy === DropPolicy.DROP_STALE) {
|
|
4001
|
+
if (Date.now() - message.timestamp > this.maxLatencyMs && !message.critical) return false;
|
|
4002
|
+
}
|
|
4003
|
+
if (this.totalSize >= this.maxSize) {
|
|
4004
|
+
if (!this.handleQueueFull(message)) return false;
|
|
4005
|
+
}
|
|
4006
|
+
this.queues.get(message.priority).push(message);
|
|
4007
|
+
this.totalSize++;
|
|
4008
|
+
return true;
|
|
4009
|
+
}
|
|
4010
|
+
/** Dequeue the highest priority message */
|
|
4011
|
+
dequeue() {
|
|
4012
|
+
for (let priority = 0; priority <= 4; priority++) {
|
|
4013
|
+
const queue = this.queues.get(priority);
|
|
4014
|
+
if (queue.length > 0) {
|
|
4015
|
+
this.totalSize--;
|
|
4016
|
+
return queue.shift();
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
/** Peek at the highest priority message without removing */
|
|
4021
|
+
peek() {
|
|
4022
|
+
for (let priority = 0; priority <= 4; priority++) {
|
|
4023
|
+
const queue = this.queues.get(priority);
|
|
4024
|
+
if (queue.length > 0) return queue[0];
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
/** Remove stale messages */
|
|
4028
|
+
removeStale() {
|
|
4029
|
+
const now = Date.now();
|
|
4030
|
+
const removed = [];
|
|
4031
|
+
for (const [priority, queue] of this.queues) {
|
|
4032
|
+
const remaining = [];
|
|
4033
|
+
for (const msg of queue) if (now - msg.timestamp <= this.maxLatencyMs || msg.critical) remaining.push(msg);
|
|
4034
|
+
else {
|
|
4035
|
+
removed.push(msg);
|
|
4036
|
+
this.totalSize--;
|
|
4037
|
+
}
|
|
4038
|
+
this.queues.set(priority, remaining);
|
|
4039
|
+
}
|
|
4040
|
+
return removed;
|
|
4041
|
+
}
|
|
4042
|
+
/** Clear all messages */
|
|
4043
|
+
clear() {
|
|
4044
|
+
const all = [];
|
|
4045
|
+
for (const queue of this.queues.values()) all.push(...queue);
|
|
4046
|
+
for (const queue of this.queues.values()) queue.length = 0;
|
|
4047
|
+
this.totalSize = 0;
|
|
4048
|
+
return all;
|
|
4049
|
+
}
|
|
4050
|
+
handleQueueFull(newMessage) {
|
|
4051
|
+
switch (this.dropPolicy) {
|
|
4052
|
+
case DropPolicy.NEVER: return false;
|
|
4053
|
+
case DropPolicy.DROP_OLDEST:
|
|
4054
|
+
for (let priority = 4; priority >= 0; priority--) {
|
|
4055
|
+
const queue = this.queues.get(priority);
|
|
4056
|
+
if (queue.length > 0 && priority >= newMessage.priority) {
|
|
4057
|
+
queue.shift();
|
|
4058
|
+
this.totalSize--;
|
|
4059
|
+
return true;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
return false;
|
|
4063
|
+
case DropPolicy.DROP_NEWEST:
|
|
4064
|
+
if (newMessage.priority >= StreamPriority.NORMAL) return false;
|
|
4065
|
+
return true;
|
|
4066
|
+
case DropPolicy.DROP_LOW_PRIORITY:
|
|
4067
|
+
for (let priority = 4; priority > newMessage.priority; priority--) {
|
|
4068
|
+
const queue = this.queues.get(priority);
|
|
4069
|
+
if (queue.length > 0) {
|
|
4070
|
+
queue.shift();
|
|
4071
|
+
this.totalSize--;
|
|
4072
|
+
return true;
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
return false;
|
|
4076
|
+
case DropPolicy.DROP_STALE:
|
|
4077
|
+
if (this.removeStale().length > 0) return true;
|
|
4078
|
+
for (let priority = 4; priority >= 0; priority--) {
|
|
4079
|
+
const queue = this.queues.get(priority);
|
|
4080
|
+
if (queue.length > 0 && priority >= newMessage.priority) {
|
|
4081
|
+
queue.shift();
|
|
4082
|
+
this.totalSize--;
|
|
4083
|
+
return true;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
return false;
|
|
4087
|
+
default: return false;
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
/**
|
|
4092
|
+
* Realtime stream for low-latency communication
|
|
4093
|
+
*
|
|
4094
|
+
* Manages message prioritization, jitter buffering, and bandwidth adaptation.
|
|
4095
|
+
*/
|
|
4096
|
+
var RealtimeStream = class {
|
|
4097
|
+
stream;
|
|
4098
|
+
config;
|
|
4099
|
+
handlers;
|
|
4100
|
+
sendQueue;
|
|
4101
|
+
receiveQueue;
|
|
4102
|
+
jitterBuffer = [];
|
|
4103
|
+
jitterBufferTargetSize;
|
|
4104
|
+
bandwidthStats = {
|
|
4105
|
+
currentBitrate: 0,
|
|
4106
|
+
measuredBandwidth: 0,
|
|
4107
|
+
packetLossRate: 0,
|
|
4108
|
+
averageLatencyMs: 0,
|
|
4109
|
+
jitterMs: 0,
|
|
4110
|
+
congestionLevel: 0
|
|
4111
|
+
};
|
|
4112
|
+
bitrateHistory = [];
|
|
4113
|
+
latencyHistory = [];
|
|
4114
|
+
lastBandwidthUpdate = 0;
|
|
4115
|
+
nextSendSequence = 0;
|
|
4116
|
+
nextExpectedSequence = 0;
|
|
4117
|
+
receivedSequences = /* @__PURE__ */ new Set();
|
|
4118
|
+
isRunning = false;
|
|
4119
|
+
sendInterval;
|
|
4120
|
+
jitterInterval;
|
|
4121
|
+
bandwidthInterval;
|
|
4122
|
+
constructor(stream, config = {}, handlers = {}) {
|
|
4123
|
+
this.stream = stream;
|
|
4124
|
+
this.config = {
|
|
4125
|
+
...DEFAULT_REALTIME_CONFIG,
|
|
4126
|
+
...config
|
|
4127
|
+
};
|
|
4128
|
+
this.handlers = handlers;
|
|
4129
|
+
this.sendQueue = new PriorityMessageQueue(this.config.maxQueueSize, this.config.dropPolicy, this.config.maxLatencyMs);
|
|
4130
|
+
this.receiveQueue = new PriorityMessageQueue(this.config.maxQueueSize, this.config.dropPolicy, this.config.maxLatencyMs);
|
|
4131
|
+
this.jitterBufferTargetSize = Math.ceil(this.config.jitterBufferMs / this.config.targetLatencyMs);
|
|
4132
|
+
this.setupStreamHandlers();
|
|
4133
|
+
}
|
|
4134
|
+
/** Get current bandwidth statistics */
|
|
4135
|
+
get stats() {
|
|
4136
|
+
return { ...this.bandwidthStats };
|
|
4137
|
+
}
|
|
4138
|
+
/** Get current send queue size */
|
|
4139
|
+
get sendQueueSize() {
|
|
4140
|
+
return this.sendQueue.size;
|
|
4141
|
+
}
|
|
4142
|
+
/** Get current receive queue size */
|
|
4143
|
+
get receiveQueueSize() {
|
|
4144
|
+
return this.receiveQueue.size;
|
|
4145
|
+
}
|
|
4146
|
+
/** Get jitter buffer size */
|
|
4147
|
+
get jitterBufferSize() {
|
|
4148
|
+
return this.jitterBuffer.length;
|
|
4149
|
+
}
|
|
4150
|
+
/**
|
|
4151
|
+
* Start the realtime stream
|
|
4152
|
+
*/
|
|
4153
|
+
start() {
|
|
4154
|
+
if (this.isRunning) return;
|
|
4155
|
+
this.isRunning = true;
|
|
4156
|
+
this.sendInterval = setInterval(() => {
|
|
4157
|
+
this.processSendQueue();
|
|
4158
|
+
}, this.config.targetLatencyMs / 2);
|
|
4159
|
+
this.jitterInterval = setInterval(() => {
|
|
4160
|
+
this.processJitterBuffer();
|
|
4161
|
+
}, this.config.targetLatencyMs / 4);
|
|
4162
|
+
if (this.config.adaptiveBitrate) this.bandwidthInterval = setInterval(() => {
|
|
4163
|
+
this.updateBandwidthStats();
|
|
4164
|
+
}, this.config.bandwidthWindowMs);
|
|
4165
|
+
this.handlers.onReady?.();
|
|
4166
|
+
}
|
|
4167
|
+
/**
|
|
4168
|
+
* Stop the realtime stream
|
|
4169
|
+
*/
|
|
4170
|
+
stop() {
|
|
4171
|
+
this.isRunning = false;
|
|
4172
|
+
if (this.sendInterval) {
|
|
4173
|
+
clearInterval(this.sendInterval);
|
|
4174
|
+
this.sendInterval = void 0;
|
|
4175
|
+
}
|
|
4176
|
+
if (this.jitterInterval) {
|
|
4177
|
+
clearInterval(this.jitterInterval);
|
|
4178
|
+
this.jitterInterval = void 0;
|
|
4179
|
+
}
|
|
4180
|
+
if (this.bandwidthInterval) {
|
|
4181
|
+
clearInterval(this.bandwidthInterval);
|
|
4182
|
+
this.bandwidthInterval = void 0;
|
|
4183
|
+
}
|
|
4184
|
+
const dropped = this.sendQueue.clear();
|
|
4185
|
+
if (dropped.length > 0) this.handlers.onDrop?.(dropped, "stream stopped");
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* Send a realtime message
|
|
4189
|
+
*/
|
|
4190
|
+
sendMessage(data, priority = StreamPriority.NORMAL, options = {}) {
|
|
4191
|
+
if (!this.isRunning) return false;
|
|
4192
|
+
const message = {
|
|
4193
|
+
id: this.generateMessageId(),
|
|
4194
|
+
priority,
|
|
4195
|
+
timestamp: Date.now(),
|
|
4196
|
+
data,
|
|
4197
|
+
type: options.type,
|
|
4198
|
+
sequenceNumber: this.nextSendSequence++,
|
|
4199
|
+
critical: options.critical
|
|
4200
|
+
};
|
|
4201
|
+
if (!this.sendQueue.enqueue(message)) {
|
|
4202
|
+
this.handlers.onDrop?.([message], "queue full");
|
|
4203
|
+
return false;
|
|
4204
|
+
}
|
|
4205
|
+
return true;
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Receive the next message (blocking)
|
|
4209
|
+
*/
|
|
4210
|
+
async receiveMessage() {
|
|
4211
|
+
return new Promise((resolve) => {
|
|
4212
|
+
const checkQueue = () => {
|
|
4213
|
+
const message = this.receiveQueue.dequeue();
|
|
4214
|
+
if (message) resolve(message);
|
|
4215
|
+
else if (!this.isRunning) resolve(void 0);
|
|
4216
|
+
else setTimeout(checkQueue, 5);
|
|
4217
|
+
};
|
|
4218
|
+
checkQueue();
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
/**
|
|
4222
|
+
* Set target bitrate (for manual bitrate control)
|
|
4223
|
+
*/
|
|
4224
|
+
setTargetBitrate(bitrate) {
|
|
4225
|
+
this.bandwidthStats.currentBitrate = Math.max(this.config.minBitrate, Math.min(this.config.maxBitrate, bitrate));
|
|
4226
|
+
}
|
|
4227
|
+
setupStreamHandlers() {
|
|
4228
|
+
const originalOnData = this.stream.handlers?.onData;
|
|
4229
|
+
this.stream.handlers = {
|
|
4230
|
+
...this.stream.handlers,
|
|
4231
|
+
onData: (chunk) => {
|
|
4232
|
+
this.handleIncomingData(chunk);
|
|
4233
|
+
originalOnData?.(chunk);
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
handleIncomingData(chunk) {
|
|
4238
|
+
try {
|
|
4239
|
+
const message = this.deserializeMessage(chunk.data);
|
|
4240
|
+
if (message.sequenceNumber > this.nextExpectedSequence) {
|
|
4241
|
+
const lost = message.sequenceNumber - this.nextExpectedSequence;
|
|
4242
|
+
this.bandwidthStats.packetLossRate = this.bandwidthStats.packetLossRate * .9 + lost * .1;
|
|
4243
|
+
}
|
|
4244
|
+
this.nextExpectedSequence = message.sequenceNumber + 1;
|
|
4245
|
+
const latency = Date.now() - message.timestamp;
|
|
4246
|
+
this.latencyHistory.push(latency);
|
|
4247
|
+
if (this.latencyHistory.length > 100) this.latencyHistory.shift();
|
|
4248
|
+
const playoutTime = Date.now() + this.config.jitterBufferMs;
|
|
4249
|
+
this.jitterBuffer.push({
|
|
4250
|
+
message,
|
|
4251
|
+
receivedAt: Date.now(),
|
|
4252
|
+
playoutTime
|
|
4253
|
+
});
|
|
4254
|
+
this.jitterBuffer.sort((a, b) => a.message.sequenceNumber - b.message.sequenceNumber);
|
|
4255
|
+
} catch (error) {
|
|
4256
|
+
this.handlers.onError?.(error);
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
processSendQueue() {
|
|
4260
|
+
if (!this.isRunning || this.sendQueue.isEmpty) return;
|
|
4261
|
+
if (this.config.adaptiveBitrate) {
|
|
4262
|
+
const maxBytesPerInterval = this.bandwidthStats.currentBitrate * this.config.targetLatencyMs / 1e3 / 2;
|
|
4263
|
+
let bytesSent = 0;
|
|
4264
|
+
while (bytesSent < maxBytesPerInterval) {
|
|
4265
|
+
const message = this.sendQueue.dequeue();
|
|
4266
|
+
if (!message) break;
|
|
4267
|
+
this.sendMessageToStream(message);
|
|
4268
|
+
bytesSent += message.data.length;
|
|
4269
|
+
}
|
|
4270
|
+
} else {
|
|
4271
|
+
const message = this.sendQueue.dequeue();
|
|
4272
|
+
if (message) this.sendMessageToStream(message);
|
|
4273
|
+
}
|
|
4274
|
+
if (this.config.dropPolicy === DropPolicy.DROP_STALE) {
|
|
4275
|
+
const stale = this.sendQueue.removeStale();
|
|
4276
|
+
if (stale.length > 0) this.handlers.onDrop?.(stale, "stale");
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
sendMessageToStream(message) {
|
|
4280
|
+
try {
|
|
4281
|
+
const data = this.serializeMessage(message);
|
|
4282
|
+
this.stream.send(data).catch((err) => {
|
|
4283
|
+
this.handlers.onError?.(err);
|
|
4284
|
+
});
|
|
4285
|
+
this.bitrateHistory.push(data.length);
|
|
4286
|
+
if (this.bitrateHistory.length > 100) this.bitrateHistory.shift();
|
|
4287
|
+
} catch (error) {
|
|
4288
|
+
this.handlers.onError?.(error);
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
processJitterBuffer() {
|
|
4292
|
+
if (!this.isRunning || this.jitterBuffer.length === 0) return;
|
|
4293
|
+
const now = Date.now();
|
|
4294
|
+
while (this.jitterBuffer.length > 0) {
|
|
4295
|
+
const entry = this.jitterBuffer[0];
|
|
4296
|
+
if (this.jitterBuffer.length < this.jitterBufferTargetSize && entry.playoutTime > now) break;
|
|
4297
|
+
if (entry.playoutTime <= now) {
|
|
4298
|
+
this.jitterBuffer.shift();
|
|
4299
|
+
this.receiveQueue.enqueue(entry.message);
|
|
4300
|
+
this.handlers.onMessage?.(entry.message);
|
|
4301
|
+
} else break;
|
|
4302
|
+
}
|
|
4303
|
+
const maxAge = this.config.maxLatencyMs * 2;
|
|
4304
|
+
const stale = [];
|
|
4305
|
+
this.jitterBuffer = this.jitterBuffer.filter((entry) => {
|
|
4306
|
+
if (now - entry.receivedAt > maxAge && !entry.message.critical) {
|
|
4307
|
+
stale.push(entry.message);
|
|
4308
|
+
return false;
|
|
4309
|
+
}
|
|
4310
|
+
return true;
|
|
4311
|
+
});
|
|
4312
|
+
if (stale.length > 0) this.handlers.onDrop?.(stale, "jitter buffer stale");
|
|
4313
|
+
}
|
|
4314
|
+
updateBandwidthStats() {
|
|
4315
|
+
const now = Date.now();
|
|
4316
|
+
const windowMs = this.config.bandwidthWindowMs;
|
|
4317
|
+
if (this.bitrateHistory.length >= 2) {
|
|
4318
|
+
const totalBytes = this.bitrateHistory.reduce((a, b) => a + b, 0);
|
|
4319
|
+
this.bandwidthStats.measuredBandwidth = totalBytes / windowMs * 1e3;
|
|
4320
|
+
}
|
|
4321
|
+
if (this.latencyHistory.length > 0) {
|
|
4322
|
+
const avgLatency = this.latencyHistory.reduce((a, b) => a + b, 0) / this.latencyHistory.length;
|
|
4323
|
+
this.bandwidthStats.averageLatencyMs = avgLatency;
|
|
4324
|
+
const variance = this.latencyHistory.reduce((sum, lat) => sum + (lat - avgLatency) ** 2, 0) / this.latencyHistory.length;
|
|
4325
|
+
this.bandwidthStats.jitterMs = Math.sqrt(variance);
|
|
4326
|
+
this.handlers.onLatencyChange?.(avgLatency);
|
|
4327
|
+
}
|
|
4328
|
+
const queueUtilization = this.sendQueue.size / this.config.maxQueueSize;
|
|
4329
|
+
const latencyRatio = this.bandwidthStats.averageLatencyMs / this.config.targetLatencyMs;
|
|
4330
|
+
this.bandwidthStats.congestionLevel = Math.min(1, (queueUtilization + latencyRatio) / 2);
|
|
4331
|
+
if (this.config.adaptiveBitrate) this.adaptBitrate();
|
|
4332
|
+
this.lastBandwidthUpdate = now;
|
|
4333
|
+
}
|
|
4334
|
+
adaptBitrate() {
|
|
4335
|
+
const { congestionLevel, packetLossRate } = this.bandwidthStats;
|
|
4336
|
+
let newBitrate = this.bandwidthStats.currentBitrate;
|
|
4337
|
+
if (congestionLevel > .7 || packetLossRate > .05) newBitrate = newBitrate * .8;
|
|
4338
|
+
else if (congestionLevel < .3 && packetLossRate < .01) newBitrate = newBitrate * 1.05;
|
|
4339
|
+
newBitrate = Math.max(this.config.minBitrate, Math.min(this.config.maxBitrate, newBitrate));
|
|
4340
|
+
if (newBitrate !== this.bandwidthStats.currentBitrate) {
|
|
4341
|
+
this.bandwidthStats.currentBitrate = newBitrate;
|
|
4342
|
+
this.handlers.onBandwidthAdapt?.(newBitrate, this.bandwidthStats);
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
serializeMessage(message) {
|
|
4346
|
+
const header = JSON.stringify({
|
|
4347
|
+
id: message.id,
|
|
4348
|
+
priority: message.priority,
|
|
4349
|
+
timestamp: message.timestamp,
|
|
4350
|
+
type: message.type,
|
|
4351
|
+
sequenceNumber: message.sequenceNumber,
|
|
4352
|
+
critical: message.critical,
|
|
4353
|
+
dataLength: message.data.length
|
|
4354
|
+
});
|
|
4355
|
+
const headerBytes = new TextEncoder().encode(header);
|
|
4356
|
+
const headerLength = new Uint8Array(4);
|
|
4357
|
+
new DataView(headerLength.buffer).setUint32(0, headerBytes.length, true);
|
|
4358
|
+
const result = new Uint8Array(4 + headerBytes.length + message.data.length);
|
|
4359
|
+
result.set(headerLength, 0);
|
|
4360
|
+
result.set(headerBytes, 4);
|
|
4361
|
+
result.set(message.data, 4 + headerBytes.length);
|
|
4362
|
+
return result;
|
|
4363
|
+
}
|
|
4364
|
+
deserializeMessage(data) {
|
|
4365
|
+
const headerLength = new DataView(data.buffer, data.byteOffset, 4).getUint32(0, true);
|
|
4366
|
+
const headerBytes = data.slice(4, 4 + headerLength);
|
|
4367
|
+
const header = JSON.parse(new TextDecoder().decode(headerBytes));
|
|
4368
|
+
return {
|
|
4369
|
+
id: header.id,
|
|
4370
|
+
priority: header.priority,
|
|
4371
|
+
timestamp: header.timestamp,
|
|
4372
|
+
type: header.type,
|
|
4373
|
+
sequenceNumber: header.sequenceNumber,
|
|
4374
|
+
critical: header.critical,
|
|
4375
|
+
data: data.slice(4 + headerLength, 4 + headerLength + header.dataLength)
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
generateMessageId() {
|
|
4379
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4382
|
+
/**
|
|
4383
|
+
* Realtime stream manager
|
|
4384
|
+
*/
|
|
4385
|
+
var RealtimeStreamManager = class {
|
|
4386
|
+
streams = /* @__PURE__ */ new Map();
|
|
4387
|
+
nextStreamId = 1;
|
|
4388
|
+
/**
|
|
4389
|
+
* Create a new realtime stream
|
|
4390
|
+
*/
|
|
4391
|
+
createStream(baseStream, config, handlers) {
|
|
4392
|
+
const streamId = this.nextStreamId++;
|
|
4393
|
+
const stream = new RealtimeStream(baseStream, config, handlers);
|
|
4394
|
+
this.streams.set(streamId, stream);
|
|
4395
|
+
return stream;
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Get a stream by ID
|
|
4399
|
+
*/
|
|
4400
|
+
getStream(id) {
|
|
4401
|
+
return this.streams.get(id);
|
|
4402
|
+
}
|
|
4403
|
+
/**
|
|
4404
|
+
* Remove a stream
|
|
4405
|
+
*/
|
|
4406
|
+
removeStream(id) {
|
|
4407
|
+
const stream = this.streams.get(id);
|
|
4408
|
+
if (stream) {
|
|
4409
|
+
stream.stop();
|
|
4410
|
+
this.streams.delete(id);
|
|
4411
|
+
return true;
|
|
4412
|
+
}
|
|
4413
|
+
return false;
|
|
4414
|
+
}
|
|
4415
|
+
/**
|
|
4416
|
+
* Get all active streams
|
|
4417
|
+
*/
|
|
4418
|
+
getActiveStreams() {
|
|
4419
|
+
return Array.from(this.streams.values());
|
|
4420
|
+
}
|
|
4421
|
+
/**
|
|
4422
|
+
* Stop all streams
|
|
4423
|
+
*/
|
|
4424
|
+
stopAll() {
|
|
4425
|
+
for (const stream of this.streams.values()) stream.stop();
|
|
4426
|
+
this.streams.clear();
|
|
4427
|
+
}
|
|
4428
|
+
};
|
|
4429
|
+
/**
|
|
4430
|
+
* Create a realtime stream manager
|
|
4431
|
+
*/
|
|
4432
|
+
function createRealtimeStreamManager() {
|
|
4433
|
+
return new RealtimeStreamManager();
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
//#endregion
|
|
4437
|
+
//#region src/rpc/stream-manager.ts
|
|
4438
|
+
/**
|
|
4439
|
+
* Stream Management - Unified stream lifecycle management
|
|
4440
|
+
*
|
|
4441
|
+
* Phase 5: Flow Control and Realtime Communication
|
|
4442
|
+
*
|
|
4443
|
+
* Manages:
|
|
4444
|
+
* - Stream creation and registration
|
|
4445
|
+
* - Bidirectional stream support
|
|
4446
|
+
* - Stream lifecycle (open, close, error handling)
|
|
4447
|
+
* - Stream multiplexing over a single connection
|
|
4448
|
+
*/
|
|
4449
|
+
/** Stream type */
|
|
4450
|
+
let StreamType = /* @__PURE__ */ function(StreamType) {
|
|
4451
|
+
/** Standard stream */
|
|
4452
|
+
StreamType["STANDARD"] = "standard";
|
|
4453
|
+
/** Bulk transfer stream */
|
|
4454
|
+
StreamType["BULK"] = "bulk";
|
|
4455
|
+
/** Realtime stream */
|
|
4456
|
+
StreamType["REALTIME"] = "realtime";
|
|
4457
|
+
return StreamType;
|
|
4458
|
+
}({});
|
|
4459
|
+
/** Default stream manager configuration */
|
|
4460
|
+
const DEFAULT_STREAM_MANAGER_CONFIG = {
|
|
4461
|
+
maxStreams: 100,
|
|
4462
|
+
defaultPriority: StreamPriority.NORMAL,
|
|
4463
|
+
enableMultiplexing: true,
|
|
4464
|
+
idleTimeoutMs: 3e5
|
|
4465
|
+
};
|
|
4466
|
+
/**
|
|
4467
|
+
* Stream manager
|
|
4468
|
+
*
|
|
4469
|
+
* Manages all streams for an RPC connection.
|
|
4470
|
+
*/
|
|
4471
|
+
var StreamManager = class {
|
|
4472
|
+
config;
|
|
4473
|
+
handlers;
|
|
4474
|
+
connection;
|
|
4475
|
+
transport;
|
|
4476
|
+
streams = /* @__PURE__ */ new Map();
|
|
4477
|
+
streamTypes = /* @__PURE__ */ new Map();
|
|
4478
|
+
streamInfos = /* @__PURE__ */ new Map();
|
|
4479
|
+
nextStreamId = 1;
|
|
4480
|
+
bulkManager;
|
|
4481
|
+
realtimeManager;
|
|
4482
|
+
idleTimeout;
|
|
4483
|
+
isRunning = false;
|
|
4484
|
+
constructor(config = {}, handlers = {}) {
|
|
4485
|
+
this.config = {
|
|
4486
|
+
...DEFAULT_STREAM_MANAGER_CONFIG,
|
|
4487
|
+
...config
|
|
4488
|
+
};
|
|
4489
|
+
this.handlers = handlers;
|
|
4490
|
+
this.bulkManager = new BulkTransferManager();
|
|
4491
|
+
this.realtimeManager = new RealtimeStreamManager();
|
|
4492
|
+
}
|
|
4493
|
+
/** Get the number of active streams */
|
|
4494
|
+
get streamCount() {
|
|
4495
|
+
return this.streams.size;
|
|
4496
|
+
}
|
|
4497
|
+
/** Get the maximum number of streams */
|
|
4498
|
+
get maxStreams() {
|
|
4499
|
+
return this.config.maxStreams;
|
|
4500
|
+
}
|
|
4501
|
+
/** Get all stream infos */
|
|
4502
|
+
get allStreamInfos() {
|
|
4503
|
+
return Array.from(this.streamInfos.values());
|
|
4504
|
+
}
|
|
4505
|
+
/**
|
|
4506
|
+
* Attach to an RPC connection
|
|
4507
|
+
*/
|
|
4508
|
+
attach(connection, transport) {
|
|
4509
|
+
this.connection = connection;
|
|
4510
|
+
this.transport = transport;
|
|
4511
|
+
this.isRunning = true;
|
|
4512
|
+
this.resetIdleTimeout();
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Detach from connection
|
|
4516
|
+
*/
|
|
4517
|
+
detach() {
|
|
4518
|
+
this.closeAllStreams();
|
|
4519
|
+
this.isRunning = false;
|
|
4520
|
+
this.connection = void 0;
|
|
4521
|
+
this.transport = void 0;
|
|
4522
|
+
if (this.idleTimeout) {
|
|
4523
|
+
clearTimeout(this.idleTimeout);
|
|
4524
|
+
this.idleTimeout = void 0;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
/**
|
|
4528
|
+
* Create a new stream
|
|
4529
|
+
*/
|
|
4530
|
+
createStream(options = {}) {
|
|
4531
|
+
if (this.streams.size >= this.config.maxStreams) throw new Error(`Maximum number of streams (${this.config.maxStreams}) reached`);
|
|
4532
|
+
const streamId = this.nextStreamId++;
|
|
4533
|
+
const type = options.type ?? StreamType.STANDARD;
|
|
4534
|
+
const streamOptions = {
|
|
4535
|
+
streamId,
|
|
4536
|
+
direction: options.direction ?? "bidirectional",
|
|
4537
|
+
priority: options.priority ?? this.config.defaultPriority ?? StreamPriority.NORMAL,
|
|
4538
|
+
metadata: options.metadata,
|
|
4539
|
+
flowControl: options.flowControl
|
|
4540
|
+
};
|
|
4541
|
+
const stream = new Stream(streamOptions, {
|
|
4542
|
+
onOpen: () => {
|
|
4543
|
+
this.updateStreamState(streamId, "open");
|
|
4544
|
+
this.handlers.onStreamOpen?.(this.getStreamInfo(streamId));
|
|
4545
|
+
},
|
|
4546
|
+
onClose: () => {
|
|
4547
|
+
this.updateStreamState(streamId, "closed");
|
|
4548
|
+
this.handlers.onStreamClose?.(this.getStreamInfo(streamId));
|
|
4549
|
+
this.removeStream(streamId);
|
|
4550
|
+
},
|
|
4551
|
+
onError: (error) => {
|
|
4552
|
+
this.updateStreamState(streamId, "error");
|
|
4553
|
+
const info = this.getStreamInfo(streamId);
|
|
4554
|
+
if (info) this.handlers.onStreamError?.(info, error);
|
|
4555
|
+
}
|
|
4556
|
+
});
|
|
4557
|
+
this.streams.set(streamId, stream);
|
|
4558
|
+
this.streamTypes.set(streamId, type);
|
|
4559
|
+
const info = {
|
|
4560
|
+
id: streamId,
|
|
4561
|
+
type,
|
|
4562
|
+
direction: streamOptions.direction,
|
|
4563
|
+
priority: streamOptions.priority ?? StreamPriority.NORMAL,
|
|
4564
|
+
state: "connecting",
|
|
4565
|
+
createdAt: Date.now(),
|
|
4566
|
+
bytesTransferred: 0,
|
|
4567
|
+
metadata: options.metadata
|
|
4568
|
+
};
|
|
4569
|
+
this.streamInfos.set(streamId, info);
|
|
4570
|
+
this.handlers.onStreamCreate?.(info);
|
|
4571
|
+
this.handlers.onStreamCountChange?.(this.streams.size);
|
|
4572
|
+
this.resetIdleTimeout();
|
|
4573
|
+
return stream;
|
|
4574
|
+
}
|
|
4575
|
+
/**
|
|
4576
|
+
* Create a bulk transfer stream
|
|
4577
|
+
*/
|
|
4578
|
+
createBulkStream(direction, metadata, config, handlers) {
|
|
4579
|
+
this.createStream({
|
|
4580
|
+
type: StreamType.BULK,
|
|
4581
|
+
direction: direction === "upload" ? "outbound" : "inbound",
|
|
4582
|
+
priority: StreamPriority.NORMAL,
|
|
4583
|
+
metadata: metadata.custom
|
|
4584
|
+
});
|
|
4585
|
+
return this.bulkManager.createTransfer(direction, metadata, config, handlers);
|
|
4586
|
+
}
|
|
4587
|
+
/**
|
|
4588
|
+
* Create a realtime stream
|
|
4589
|
+
*/
|
|
4590
|
+
createRealtimeStream(config, handlers) {
|
|
4591
|
+
const stream = this.createStream({
|
|
4592
|
+
type: StreamType.REALTIME,
|
|
4593
|
+
direction: "bidirectional",
|
|
4594
|
+
priority: StreamPriority.HIGH
|
|
4595
|
+
});
|
|
4596
|
+
return this.realtimeManager.createStream(stream, config, handlers);
|
|
4597
|
+
}
|
|
4598
|
+
/**
|
|
4599
|
+
* Get a stream by ID
|
|
4600
|
+
*/
|
|
4601
|
+
getStream(id) {
|
|
4602
|
+
return this.streams.get(id);
|
|
4603
|
+
}
|
|
4604
|
+
/**
|
|
4605
|
+
* Get stream info by ID
|
|
4606
|
+
*/
|
|
4607
|
+
getStreamInfo(id) {
|
|
4608
|
+
return this.streamInfos.get(id);
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
4611
|
+
* Get stream type by ID
|
|
4612
|
+
*/
|
|
4613
|
+
getStreamType(id) {
|
|
4614
|
+
return this.streamTypes.get(id);
|
|
4615
|
+
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Get bulk transfer by ID
|
|
4618
|
+
*/
|
|
4619
|
+
getBulkTransfer(id) {
|
|
4620
|
+
return this.bulkManager.getTransfer(id);
|
|
4621
|
+
}
|
|
4622
|
+
/**
|
|
4623
|
+
* Get realtime stream by ID
|
|
4624
|
+
*/
|
|
4625
|
+
getRealtimeStream(id) {
|
|
4626
|
+
return this.realtimeManager.getStream(id);
|
|
4627
|
+
}
|
|
4628
|
+
/**
|
|
4629
|
+
* Close a specific stream
|
|
4630
|
+
*/
|
|
4631
|
+
async closeStream(id) {
|
|
4632
|
+
const stream = this.streams.get(id);
|
|
4633
|
+
if (!stream) return false;
|
|
4634
|
+
await stream.close();
|
|
4635
|
+
this.removeStream(id);
|
|
4636
|
+
return true;
|
|
4637
|
+
}
|
|
4638
|
+
/**
|
|
4639
|
+
* Close all streams
|
|
4640
|
+
*/
|
|
4641
|
+
async closeAllStreams() {
|
|
4642
|
+
const closePromises = [];
|
|
4643
|
+
for (const [_id, stream] of this.streams) closePromises.push(stream.close().catch(() => {}));
|
|
4644
|
+
await Promise.all(closePromises);
|
|
4645
|
+
this.streams.clear();
|
|
4646
|
+
this.streamTypes.clear();
|
|
4647
|
+
this.streamInfos.clear();
|
|
4648
|
+
this.bulkManager.closeAll();
|
|
4649
|
+
this.realtimeManager.stopAll();
|
|
4650
|
+
this.handlers.onStreamCountChange?.(0);
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* Get streams by type
|
|
4654
|
+
*/
|
|
4655
|
+
getStreamsByType(type) {
|
|
4656
|
+
return this.allStreamInfos.filter((info) => info.type === type);
|
|
4657
|
+
}
|
|
4658
|
+
/**
|
|
4659
|
+
* Get streams by state
|
|
4660
|
+
*/
|
|
4661
|
+
getStreamsByState(state) {
|
|
4662
|
+
return this.allStreamInfos.filter((info) => info.state === state);
|
|
4663
|
+
}
|
|
4664
|
+
/**
|
|
4665
|
+
* Get statistics
|
|
4666
|
+
*/
|
|
4667
|
+
getStatistics() {
|
|
4668
|
+
const infos = this.allStreamInfos;
|
|
4669
|
+
const active = infos.filter((i) => i.state === "open");
|
|
4670
|
+
const byType = {
|
|
4671
|
+
[StreamType.STANDARD]: 0,
|
|
4672
|
+
[StreamType.BULK]: 0,
|
|
4673
|
+
[StreamType.REALTIME]: 0
|
|
4674
|
+
};
|
|
4675
|
+
const byState = {
|
|
4676
|
+
connecting: 0,
|
|
4677
|
+
open: 0,
|
|
4678
|
+
closing: 0,
|
|
4679
|
+
closed: 0,
|
|
4680
|
+
error: 0
|
|
4681
|
+
};
|
|
4682
|
+
let totalBytes = 0;
|
|
4683
|
+
for (const info of infos) {
|
|
4684
|
+
byType[info.type]++;
|
|
4685
|
+
byState[info.state]++;
|
|
4686
|
+
totalBytes += info.bytesTransferred;
|
|
4687
|
+
}
|
|
4688
|
+
return {
|
|
4689
|
+
totalStreams: infos.length,
|
|
4690
|
+
activeStreams: active.length,
|
|
4691
|
+
streamsByType: byType,
|
|
4692
|
+
streamsByState: byState,
|
|
4693
|
+
totalBytesTransferred: totalBytes
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
/**
|
|
4697
|
+
* Update stream priority
|
|
4698
|
+
*/
|
|
4699
|
+
updatePriority(id, priority) {
|
|
4700
|
+
const info = this.streamInfos.get(id);
|
|
4701
|
+
if (!info) return false;
|
|
4702
|
+
info.priority = priority;
|
|
4703
|
+
return true;
|
|
4704
|
+
}
|
|
4705
|
+
/**
|
|
4706
|
+
* Pause all streams (backpressure)
|
|
4707
|
+
*/
|
|
4708
|
+
pauseAll() {
|
|
4709
|
+
for (const _stream of this.streams.values());
|
|
4710
|
+
}
|
|
4711
|
+
/**
|
|
4712
|
+
* Resume all streams
|
|
4713
|
+
*/
|
|
4714
|
+
resumeAll() {
|
|
4715
|
+
for (const _stream of this.streams.values());
|
|
4716
|
+
}
|
|
4717
|
+
removeStream(id) {
|
|
4718
|
+
this.streams.delete(id);
|
|
4719
|
+
this.streamTypes.delete(id);
|
|
4720
|
+
this.streamInfos.delete(id);
|
|
4721
|
+
this.handlers.onStreamCountChange?.(this.streams.size);
|
|
4722
|
+
this.resetIdleTimeout();
|
|
4723
|
+
}
|
|
4724
|
+
updateStreamState(id, state) {
|
|
4725
|
+
const info = this.streamInfos.get(id);
|
|
4726
|
+
if (info) info.state = state;
|
|
4727
|
+
}
|
|
4728
|
+
resetIdleTimeout() {
|
|
4729
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
4730
|
+
if (!this.isRunning || this.streams.size > 0) return;
|
|
4731
|
+
this.idleTimeout = setTimeout(() => {
|
|
4732
|
+
if (this.streams.size === 0) {}
|
|
4733
|
+
}, this.config.idleTimeoutMs);
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
/**
|
|
4737
|
+
* Create a stream manager
|
|
4738
|
+
*/
|
|
4739
|
+
function createStreamManager(config, handlers) {
|
|
4740
|
+
return new StreamManager(config, handlers);
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
//#endregion
|
|
4744
|
+
//#region src/rpc/streaming-connection.ts
|
|
4745
|
+
/** Default streaming capabilities */
|
|
4746
|
+
const DEFAULT_STREAMING_CAPABILITIES = {
|
|
4747
|
+
standardStreams: true,
|
|
4748
|
+
bulkTransfer: true,
|
|
4749
|
+
realtimeStreams: true,
|
|
4750
|
+
maxConcurrentStreams: 100,
|
|
4751
|
+
maxWindowSize: 1048576,
|
|
4752
|
+
flowControlAlgorithms: ["sliding-window", "rate-based"]
|
|
4753
|
+
};
|
|
4754
|
+
/**
|
|
4755
|
+
* Extended RPC connection with streaming support
|
|
4756
|
+
*
|
|
4757
|
+
* This class wraps RpcConnection and adds stream management capabilities.
|
|
4758
|
+
* It can be used as a drop-in replacement for RpcConnection.
|
|
4759
|
+
*/
|
|
4760
|
+
var StreamingRpcConnection = class extends require_rpc_connection.RpcConnection {
|
|
4761
|
+
streamManager;
|
|
4762
|
+
localCapabilities;
|
|
4763
|
+
remoteCapabilities;
|
|
4764
|
+
streamingEnabled;
|
|
4765
|
+
constructor(transport, options = {}) {
|
|
4766
|
+
super(transport, options);
|
|
4767
|
+
this.streamingEnabled = options.enableStreaming ?? true;
|
|
4768
|
+
this.localCapabilities = {
|
|
4769
|
+
...DEFAULT_STREAMING_CAPABILITIES,
|
|
4770
|
+
...options.localCapabilities
|
|
4771
|
+
};
|
|
4772
|
+
this.streamManager = new StreamManager(options.streamManagerConfig, options.streamManagerHandlers);
|
|
4773
|
+
this.streamManager.attach(this, transport);
|
|
4774
|
+
if (this.streamingEnabled) this.negotiateCapabilities();
|
|
4775
|
+
}
|
|
4776
|
+
/** Get the stream manager */
|
|
4777
|
+
get streams() {
|
|
4778
|
+
return this.streamManager;
|
|
4779
|
+
}
|
|
4780
|
+
/** Get local streaming capabilities */
|
|
4781
|
+
get capabilities() {
|
|
4782
|
+
return this.localCapabilities;
|
|
4783
|
+
}
|
|
4784
|
+
/** Get remote streaming capabilities (if negotiated) */
|
|
4785
|
+
get remoteStreamingCapabilities() {
|
|
4786
|
+
return this.remoteCapabilities;
|
|
4787
|
+
}
|
|
4788
|
+
/** Check if streaming is enabled */
|
|
4789
|
+
get isStreamingEnabled() {
|
|
4790
|
+
return this.streamingEnabled;
|
|
4791
|
+
}
|
|
4792
|
+
/**
|
|
4793
|
+
* Create a new standard stream
|
|
4794
|
+
*/
|
|
4795
|
+
createStream(options) {
|
|
4796
|
+
this.ensureStreamingEnabled();
|
|
4797
|
+
return this.streamManager.createStream(options);
|
|
4798
|
+
}
|
|
4799
|
+
/**
|
|
4800
|
+
* Create a bulk transfer stream
|
|
4801
|
+
*/
|
|
4802
|
+
createBulkTransfer(direction, metadata, config, handlers) {
|
|
4803
|
+
this.ensureStreamingEnabled();
|
|
4804
|
+
this.ensureCapability("bulkTransfer");
|
|
4805
|
+
return this.streamManager.createBulkStream(direction, metadata, config, handlers);
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Create a realtime stream
|
|
4809
|
+
*/
|
|
4810
|
+
createRealtimeStream(config, handlers) {
|
|
4811
|
+
this.ensureStreamingEnabled();
|
|
4812
|
+
this.ensureCapability("realtimeStreams");
|
|
4813
|
+
return this.streamManager.createRealtimeStream(config, handlers);
|
|
4814
|
+
}
|
|
4815
|
+
/**
|
|
4816
|
+
* Get stream statistics
|
|
4817
|
+
*/
|
|
4818
|
+
getStreamStatistics() {
|
|
4819
|
+
return this.streamManager.getStatistics();
|
|
4820
|
+
}
|
|
4821
|
+
/**
|
|
4822
|
+
* Close all streams gracefully
|
|
4823
|
+
*/
|
|
4824
|
+
async closeAllStreams() {
|
|
4825
|
+
await this.streamManager.closeAllStreams();
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* Override stop to properly clean up streams
|
|
4829
|
+
*/
|
|
4830
|
+
async stop() {
|
|
4831
|
+
await this.closeAllStreams();
|
|
4832
|
+
this.streamManager.detach();
|
|
4833
|
+
await super.stop();
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Negotiate streaming capabilities with remote peer
|
|
4837
|
+
*
|
|
4838
|
+
* This would typically be done during bootstrap or connection setup.
|
|
4839
|
+
* For now, we assume the remote has the same capabilities.
|
|
4840
|
+
*/
|
|
4841
|
+
async negotiateCapabilities() {
|
|
4842
|
+
this.remoteCapabilities = { ...this.localCapabilities };
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* Update remote capabilities (called when received from peer)
|
|
4846
|
+
*/
|
|
4847
|
+
setRemoteCapabilities(capabilities) {
|
|
4848
|
+
this.remoteCapabilities = capabilities;
|
|
4849
|
+
}
|
|
4850
|
+
/**
|
|
4851
|
+
* Check if a specific capability is supported by both peers
|
|
4852
|
+
*/
|
|
4853
|
+
isCapabilitySupported(capability) {
|
|
4854
|
+
const localValue = this.localCapabilities[capability];
|
|
4855
|
+
const remoteValue = this.remoteCapabilities?.[capability];
|
|
4856
|
+
if (typeof localValue === "boolean" && typeof remoteValue === "boolean") return localValue && remoteValue;
|
|
4857
|
+
if (typeof localValue === "number" && typeof remoteValue === "number") return localValue > 0 && remoteValue > 0;
|
|
4858
|
+
return false;
|
|
4859
|
+
}
|
|
4860
|
+
/**
|
|
4861
|
+
* Create a stream for pipeline results
|
|
4862
|
+
*
|
|
4863
|
+
* This allows large pipeline results to be streamed instead of buffered.
|
|
4864
|
+
*/
|
|
4865
|
+
createPipelineStream(questionId, options) {
|
|
4866
|
+
this.ensureStreamingEnabled();
|
|
4867
|
+
return this.createStream({
|
|
4868
|
+
...options,
|
|
4869
|
+
type: StreamType.STANDARD,
|
|
4870
|
+
metadata: {
|
|
4871
|
+
...options?.metadata,
|
|
4872
|
+
pipelineQuestionId: questionId.toString()
|
|
4873
|
+
}
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* Associate a stream with a capability
|
|
4878
|
+
*
|
|
4879
|
+
* This enables streaming data to/from a capability.
|
|
4880
|
+
*/
|
|
4881
|
+
associateStreamWithCapability(streamId, _importId) {
|
|
4882
|
+
if (!this.streamManager.getStream(streamId)) throw new Error(`Stream ${streamId} not found`);
|
|
4883
|
+
}
|
|
4884
|
+
ensureStreamingEnabled() {
|
|
4885
|
+
if (!this.streamingEnabled) throw new Error("Streaming is not enabled on this connection");
|
|
4886
|
+
}
|
|
4887
|
+
ensureCapability(capability) {
|
|
4888
|
+
if (!this.isCapabilitySupported(capability)) throw new Error(`Capability '${capability}' is not supported by both peers`);
|
|
4889
|
+
}
|
|
4890
|
+
};
|
|
4891
|
+
/**
|
|
4892
|
+
* Create a streaming RPC connection
|
|
4893
|
+
*/
|
|
4894
|
+
function createStreamingConnection(transport, options) {
|
|
4895
|
+
return new StreamingRpcConnection(transport, options);
|
|
4896
|
+
}
|
|
4897
|
+
/**
|
|
4898
|
+
* Check if a connection supports streaming
|
|
4899
|
+
*/
|
|
4900
|
+
function supportsStreaming(connection) {
|
|
4901
|
+
return connection instanceof StreamingRpcConnection;
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
//#endregion
|
|
4905
|
+
exports.AnswerTable = require_rpc_connection.AnswerTable;
|
|
3004
4906
|
exports.BaseCapabilityClient = BaseCapabilityClient;
|
|
4907
|
+
exports.BulkTransfer = BulkTransfer;
|
|
4908
|
+
exports.BulkTransferManager = BulkTransferManager;
|
|
4909
|
+
exports.ConnectionManager = ConnectionManager;
|
|
4910
|
+
exports.DEFAULT_BULK_CONFIG = DEFAULT_BULK_CONFIG;
|
|
4911
|
+
exports.DEFAULT_ESCROW_CONFIG = DEFAULT_ESCROW_CONFIG;
|
|
4912
|
+
exports.DEFAULT_FLOW_CONTROL = DEFAULT_FLOW_CONTROL;
|
|
4913
|
+
exports.DEFAULT_JOIN_OPTIONS = DEFAULT_JOIN_OPTIONS;
|
|
4914
|
+
exports.DEFAULT_JOIN_SECURITY_POLICY = DEFAULT_JOIN_SECURITY_POLICY;
|
|
4915
|
+
exports.DEFAULT_REALTIME_CONFIG = DEFAULT_REALTIME_CONFIG;
|
|
4916
|
+
exports.DEFAULT_STREAMING_CAPABILITIES = DEFAULT_STREAMING_CAPABILITIES;
|
|
4917
|
+
exports.DropPolicy = DropPolicy;
|
|
3005
4918
|
exports.ElementSize = ElementSize;
|
|
3006
|
-
exports.ExportTable = ExportTable;
|
|
3007
|
-
exports.ImportTable = ImportTable;
|
|
4919
|
+
exports.ExportTable = require_rpc_connection.ExportTable;
|
|
4920
|
+
exports.ImportTable = require_rpc_connection.ImportTable;
|
|
4921
|
+
exports.Level3Handlers = Level3Handlers;
|
|
4922
|
+
exports.Level4Handlers = Level4Handlers;
|
|
3008
4923
|
exports.ListBuilder = ListBuilder;
|
|
3009
4924
|
exports.ListReader = ListReader;
|
|
3010
4925
|
exports.MemoryPool = MemoryPool;
|
|
@@ -3012,15 +4927,22 @@ exports.MessageBuilder = MessageBuilder;
|
|
|
3012
4927
|
exports.MessageReader = MessageReader;
|
|
3013
4928
|
exports.MultiSegmentMessageBuilder = MultiSegmentMessageBuilder;
|
|
3014
4929
|
exports.OptimizedRpcMessageBuilder = OptimizedRpcMessageBuilder;
|
|
3015
|
-
exports.PIPELINE_CLIENT_SYMBOL = PIPELINE_CLIENT_SYMBOL;
|
|
3016
|
-
exports.PipelineOpTracker = PipelineOpTracker;
|
|
3017
|
-
exports.PipelineResolutionTracker = PipelineResolutionTracker;
|
|
4930
|
+
exports.PIPELINE_CLIENT_SYMBOL = require_rpc_connection.PIPELINE_CLIENT_SYMBOL;
|
|
4931
|
+
exports.PipelineOpTracker = require_rpc_connection.PipelineOpTracker;
|
|
4932
|
+
exports.PipelineResolutionTracker = require_rpc_connection.PipelineResolutionTracker;
|
|
3018
4933
|
exports.PointerTag = PointerTag;
|
|
3019
|
-
exports.QuestionTable = QuestionTable;
|
|
3020
|
-
exports.QueuedCallManager = QueuedCallManager;
|
|
4934
|
+
exports.QuestionTable = require_rpc_connection.QuestionTable;
|
|
4935
|
+
exports.QueuedCallManager = require_rpc_connection.QueuedCallManager;
|
|
4936
|
+
exports.RealtimeStream = RealtimeStream;
|
|
4937
|
+
exports.RealtimeStreamManager = RealtimeStreamManager;
|
|
3021
4938
|
exports.RestoreHandler = RestoreHandler;
|
|
3022
|
-
exports.RpcConnection = RpcConnection;
|
|
4939
|
+
exports.RpcConnection = require_rpc_connection.RpcConnection;
|
|
3023
4940
|
exports.Segment = Segment;
|
|
4941
|
+
exports.Stream = Stream;
|
|
4942
|
+
exports.StreamManager = StreamManager;
|
|
4943
|
+
exports.StreamPriority = StreamPriority;
|
|
4944
|
+
exports.StreamType = StreamType;
|
|
4945
|
+
exports.StreamingRpcConnection = StreamingRpcConnection;
|
|
3024
4946
|
exports.StructBuilder = StructBuilder;
|
|
3025
4947
|
exports.StructReader = StructReader;
|
|
3026
4948
|
exports.SturdyRefManager = SturdyRefManager;
|
|
@@ -3029,8 +4951,16 @@ exports.UnionReader = UnionReader;
|
|
|
3029
4951
|
exports.WORD_SIZE = WORD_SIZE;
|
|
3030
4952
|
exports.WebSocketTransport = WebSocketTransport;
|
|
3031
4953
|
exports.configureGlobalMemoryPool = configureGlobalMemoryPool;
|
|
3032
|
-
exports.
|
|
4954
|
+
exports.createBulkTransferManager = createBulkTransferManager;
|
|
4955
|
+
exports.createPipelineClient = require_rpc_connection.createPipelineClient;
|
|
4956
|
+
exports.createProvisionId = createProvisionId;
|
|
4957
|
+
exports.createRealtimeStreamManager = createRealtimeStreamManager;
|
|
4958
|
+
exports.createRecipientId = createRecipientId;
|
|
4959
|
+
exports.createStream = createStream;
|
|
4960
|
+
exports.createStreamManager = createStreamManager;
|
|
4961
|
+
exports.createStreamingConnection = createStreamingConnection;
|
|
3033
4962
|
exports.createSturdyRef = createSturdyRef;
|
|
4963
|
+
exports.createThirdPartyCapId = createThirdPartyCapId;
|
|
3034
4964
|
exports.createUnionBuilder = createUnionBuilder;
|
|
3035
4965
|
exports.createUnionReader = createUnionReader;
|
|
3036
4966
|
exports.createZeroCopyView = createZeroCopyView;
|
|
@@ -3040,10 +4970,14 @@ exports.deserializeSturdyRef = deserializeSturdyRef;
|
|
|
3040
4970
|
exports.encodeListPointer = encodeListPointer;
|
|
3041
4971
|
exports.encodeStructPointer = encodeStructPointer;
|
|
3042
4972
|
exports.fastCopy = fastCopy;
|
|
4973
|
+
exports.generateProvisionId = generateProvisionId;
|
|
4974
|
+
exports.generateVatId = generateVatId;
|
|
3043
4975
|
exports.getGlobalMemoryPool = getGlobalMemoryPool;
|
|
3044
|
-
exports.isPipelineClient = isPipelineClient;
|
|
4976
|
+
exports.isPipelineClient = require_rpc_connection.isPipelineClient;
|
|
3045
4977
|
exports.isSameBuffer = isSameBuffer;
|
|
4978
|
+
exports.isStream = isStream;
|
|
3046
4979
|
exports.isSturdyRefValid = isSturdyRefValid;
|
|
3047
4980
|
exports.serializeRpcMessage = serializeRpcMessage;
|
|
3048
4981
|
exports.serializeSturdyRef = serializeSturdyRef;
|
|
4982
|
+
exports.supportsStreaming = supportsStreaming;
|
|
3049
4983
|
//# sourceMappingURL=index.cjs.map
|