opentray 0.31.3 → 0.33.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 +34 -2
- package/dist/index.d.mts +404 -48
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +814 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
package/dist/index.mjs
CHANGED
|
@@ -484,6 +484,37 @@ const resolveAccessibleLibrary = async (path, target, packageName) => {
|
|
|
484
484
|
//#endregion
|
|
485
485
|
//#region src/client.ts
|
|
486
486
|
/**
|
|
487
|
+
* Default per-call deadline classes (W1, harden-transport-robustness;
|
|
488
|
+
* field-validated against the pnpm-pub silent-wedge incident): interactive
|
|
489
|
+
* calls settle or reject within 5 s, teardown-class frames within 2 s, and
|
|
490
|
+
* bootstrap-class round-trips (connection init and first bring-up) within
|
|
491
|
+
* 10 s. They live beside `OpenTrayTransport` because they are part of the
|
|
492
|
+
* generic transport call contract, not of one broker implementation.
|
|
493
|
+
*/
|
|
494
|
+
const INTERACTIVE_CALL_DEADLINE_MS = 5e3;
|
|
495
|
+
const TEARDOWN_CALL_DEADLINE_MS = 2e3;
|
|
496
|
+
const BOOTSTRAP_CALL_DEADLINE_MS = 1e4;
|
|
497
|
+
/**
|
|
498
|
+
* Typed per-call deadline rejection: the transport's budget for one
|
|
499
|
+
* round-trip expired without a correlated reply. This is the third
|
|
500
|
+
* rejection class, distinct from transport-lost (the plain
|
|
501
|
+
* `BROKER_CONNECTION_CLOSED_MESSAGE` sentinel family, or the typed
|
|
502
|
+
* `extension_transport_closed` for accepted deferred operations) and from
|
|
503
|
+
* broker-rejected (`BrokerServerError` / `ExtensionOperationError`). An
|
|
504
|
+
* expiry settles exactly one call; it never declares the transport dead —
|
|
505
|
+
* death stays a socket-close/error (and, later, heartbeat) verdict.
|
|
506
|
+
*/
|
|
507
|
+
var TransportTimeoutError = class extends Error {
|
|
508
|
+
requestId;
|
|
509
|
+
deadlineMs;
|
|
510
|
+
constructor(requestId, deadlineMs) {
|
|
511
|
+
super(`transport call deadline expired after ${deadlineMs}ms: ${requestId}`);
|
|
512
|
+
this.name = "TransportTimeoutError";
|
|
513
|
+
this.requestId = requestId;
|
|
514
|
+
this.deadlineMs = deadlineMs;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
/**
|
|
487
518
|
* Typed rejection for synchronous broker error frames: carries the wire
|
|
488
519
|
* `code`, the structured `details` payload when the error family freezes one,
|
|
489
520
|
* and the transport cause when relevant. Consumers match on `code`; the human
|
|
@@ -652,7 +683,7 @@ function createTrayHandle(transport, appId, trayId, nextRequestId = createReques
|
|
|
652
683
|
requestId,
|
|
653
684
|
appId,
|
|
654
685
|
trayId
|
|
655
|
-
});
|
|
686
|
+
}, { deadlineMs: TEARDOWN_CALL_DEADLINE_MS });
|
|
656
687
|
expectResponse(response, requestId, "ack");
|
|
657
688
|
}
|
|
658
689
|
};
|
|
@@ -665,10 +696,15 @@ const attachEventfulTrayHandle = (handle, source, appId, nextMountId, sessionId)
|
|
|
665
696
|
if (!isOwnedTrayEvent(frame.event, appId, handle.trayId)) return;
|
|
666
697
|
handler(frame.event);
|
|
667
698
|
});
|
|
668
|
-
const onConnectionDead = isConnectionDeadSource(source) ? (handler) => source.onConnectionDead(handler) : void 0;
|
|
699
|
+
const onConnectionDead = isConnectionDeadSource$1(source) ? (handler) => source.onConnectionDead(handler) : void 0;
|
|
700
|
+
const transportSupervision = isTransportSupervisionSource(source) ? {
|
|
701
|
+
onTransportStateChange: (handler) => source.onTransportStateChange(handler),
|
|
702
|
+
registerTransportRebuild: (rebuild) => source.registerTransportRebuild(rebuild)
|
|
703
|
+
} : void 0;
|
|
669
704
|
const eventful = {
|
|
670
705
|
...handle,
|
|
671
706
|
...onConnectionDead === void 0 ? {} : { onConnectionDead },
|
|
707
|
+
...transportSupervision === void 0 ? {} : transportSupervision,
|
|
672
708
|
listenExtension(ext, handler) {
|
|
673
709
|
return source.onEvent((frame) => {
|
|
674
710
|
if (frame.type !== "ext-event" || frame.ext !== ext) return;
|
|
@@ -708,7 +744,8 @@ const attachEventfulTrayHandle = (handle, source, appId, nextMountId, sessionId)
|
|
|
708
744
|
};
|
|
709
745
|
const isOwnedTrayEvent = (event, appId, trayId) => event.type !== "ready" && event.appId === appId && event.trayId === trayId;
|
|
710
746
|
const isOpenTrayEventSource = (transport) => "onEvent" in transport && typeof transport.onEvent === "function";
|
|
711
|
-
const isConnectionDeadSource = (transport) => "onConnectionDead" in transport && typeof transport.onConnectionDead === "function";
|
|
747
|
+
const isConnectionDeadSource$1 = (transport) => "onConnectionDead" in transport && typeof transport.onConnectionDead === "function";
|
|
748
|
+
const isTransportSupervisionSource = (transport) => "onTransportStateChange" in transport && typeof transport.onTransportStateChange === "function" && "registerTransportRebuild" in transport && typeof transport.registerTransportRebuild === "function";
|
|
712
749
|
const resolveDefaultAppRef = async (transport, nextRequestId) => {
|
|
713
750
|
const requestId = nextRequestId();
|
|
714
751
|
const response = await transport.request({
|
|
@@ -1559,6 +1596,12 @@ var ExtensionOperationError = class extends Error {
|
|
|
1559
1596
|
};
|
|
1560
1597
|
const extensionTransportClosedError = (cause) => new ExtensionOperationError(EXTENSION_TRANSPORT_CLOSED_CODE, "the broker transport closed while a deferred extension operation was pending", { cause });
|
|
1561
1598
|
/**
|
|
1599
|
+
* Correlation label the bootstrap deadline reports under: the init frame
|
|
1600
|
+
* carries no requestId on the wire, so the typed timeout names the call class
|
|
1601
|
+
* itself.
|
|
1602
|
+
*/
|
|
1603
|
+
const INIT_REQUEST_ID = "init";
|
|
1604
|
+
/**
|
|
1562
1605
|
* Typed handshake rejection for a Ready frame whose protocol version does not
|
|
1563
1606
|
* equal the version this client sent in its Init frame: a structurally valid
|
|
1564
1607
|
* Ready frame alone must never accept the session (add-ext-dialog batch A
|
|
@@ -1669,6 +1712,7 @@ var LocalBrokerConnection = class {
|
|
|
1669
1712
|
/** operationId -> requestId of accepted-but-unsettled deferred operations. */
|
|
1670
1713
|
deferredOperations = /* @__PURE__ */ new Map();
|
|
1671
1714
|
deadListeners = /* @__PURE__ */ new Set();
|
|
1715
|
+
deadlineExpiryListeners = /* @__PURE__ */ new Set();
|
|
1672
1716
|
deadError;
|
|
1673
1717
|
ready;
|
|
1674
1718
|
constructor(socket, endpoint, callerLabel) {
|
|
@@ -1695,6 +1739,12 @@ var LocalBrokerConnection = class {
|
|
|
1695
1739
|
this.deadListeners.delete(listener);
|
|
1696
1740
|
};
|
|
1697
1741
|
}
|
|
1742
|
+
onDeadlineExpiry(listener) {
|
|
1743
|
+
this.deadlineExpiryListeners.add(listener);
|
|
1744
|
+
return () => {
|
|
1745
|
+
this.deadlineExpiryListeners.delete(listener);
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1698
1748
|
async init(clientVersion, protocolVersion, expectedBrokerArtifactIdentity) {
|
|
1699
1749
|
const ready = new Promise((resolve, reject) => {
|
|
1700
1750
|
this.ready = {
|
|
@@ -1707,18 +1757,24 @@ var LocalBrokerConnection = class {
|
|
|
1707
1757
|
protocolVersion,
|
|
1708
1758
|
clientVersion
|
|
1709
1759
|
});
|
|
1710
|
-
const frame = await ready;
|
|
1760
|
+
const frame = await this.raceBootstrapDeadline(ready);
|
|
1711
1761
|
if (frame.protocolVersion !== protocolVersion) throw new BrokerProtocolVersionError(protocolVersion, frame.protocolVersion);
|
|
1712
1762
|
if (!brokerArtifactIdentityEquals(frame.brokerArtifactIdentity, expectedBrokerArtifactIdentity)) throw new Error(`broker artifact identity mismatch: expected=${JSON.stringify(expectedBrokerArtifactIdentity)} actual=${JSON.stringify(frame.brokerArtifactIdentity)}`);
|
|
1713
1763
|
this.sessionId = frame.sessionId;
|
|
1714
1764
|
}
|
|
1715
|
-
async request(frame) {
|
|
1765
|
+
async request(frame, options = {}) {
|
|
1716
1766
|
if (this.pending.has(frame.requestId)) throw new Error(`duplicate requestId: ${frame.requestId}`);
|
|
1717
1767
|
if (this.deadError !== void 0) throw new Error(this.deadError.message);
|
|
1768
|
+
const deadlineMs = options.deadlineMs ?? 5e3;
|
|
1718
1769
|
const response = new Promise((resolve, reject) => {
|
|
1770
|
+
const deadlineTimer = setTimeout(() => {
|
|
1771
|
+
this.expireRequest(frame.requestId, deadlineMs);
|
|
1772
|
+
}, deadlineMs);
|
|
1773
|
+
deadlineTimer.unref();
|
|
1719
1774
|
this.pending.set(frame.requestId, {
|
|
1720
1775
|
resolve,
|
|
1721
|
-
reject
|
|
1776
|
+
reject,
|
|
1777
|
+
deadlineTimer
|
|
1722
1778
|
});
|
|
1723
1779
|
});
|
|
1724
1780
|
this.write(frame);
|
|
@@ -1734,7 +1790,15 @@ var LocalBrokerConnection = class {
|
|
|
1734
1790
|
if (this.deadError !== void 0) return;
|
|
1735
1791
|
this.write({ type: "exit" });
|
|
1736
1792
|
await new Promise((resolve) => {
|
|
1737
|
-
|
|
1793
|
+
const degrade = setTimeout(() => {
|
|
1794
|
+
this.socket.destroy();
|
|
1795
|
+
resolve();
|
|
1796
|
+
}, TEARDOWN_CALL_DEADLINE_MS);
|
|
1797
|
+
degrade.unref();
|
|
1798
|
+
this.socket.end(() => {
|
|
1799
|
+
clearTimeout(degrade);
|
|
1800
|
+
resolve();
|
|
1801
|
+
});
|
|
1738
1802
|
});
|
|
1739
1803
|
}
|
|
1740
1804
|
consume(chunk) {
|
|
@@ -1764,8 +1828,7 @@ var LocalBrokerConnection = class {
|
|
|
1764
1828
|
if (frame.type === "error") {
|
|
1765
1829
|
const error = new BrokerServerError(frame.code, frame.message, { details: frame.details });
|
|
1766
1830
|
if (frame.requestId !== void 0) {
|
|
1767
|
-
this.
|
|
1768
|
-
this.pending.delete(frame.requestId);
|
|
1831
|
+
this.takePending(frame.requestId)?.reject(error);
|
|
1769
1832
|
return;
|
|
1770
1833
|
}
|
|
1771
1834
|
if (this.ready) {
|
|
@@ -1786,8 +1849,7 @@ var LocalBrokerConnection = class {
|
|
|
1786
1849
|
}
|
|
1787
1850
|
const requestId = responseRequestId(frame);
|
|
1788
1851
|
if (requestId !== void 0) {
|
|
1789
|
-
this.
|
|
1790
|
-
this.pending.delete(requestId);
|
|
1852
|
+
this.takePending(requestId)?.resolve(frame);
|
|
1791
1853
|
return;
|
|
1792
1854
|
}
|
|
1793
1855
|
if (frame.type === "event" || frame.type === "app-event" || frame.type === "ext-event") for (const listener of this.listeners) listener(frame);
|
|
@@ -1800,7 +1862,10 @@ var LocalBrokerConnection = class {
|
|
|
1800
1862
|
* settles the request. The broker's owner loop guarantees exactly one
|
|
1801
1863
|
* terminal per operation; a second acceptance for the same request or an
|
|
1802
1864
|
* already-bound operationId is defensively ignored, never a second
|
|
1803
|
-
* registration.
|
|
1865
|
+
* registration. Acceptance also stops the call's deadline: dispatch was the
|
|
1866
|
+
* bounded transport phase the client owns, and the accepted operation now
|
|
1867
|
+
* awaits a native terminal that can be legitimately user-paced (a held modal
|
|
1868
|
+
* dialog), bounded only by transport liveness.
|
|
1804
1869
|
*/
|
|
1805
1870
|
acceptDeferredOperation(requestId, operationId) {
|
|
1806
1871
|
const entry = this.pending.get(requestId);
|
|
@@ -1808,6 +1873,7 @@ var LocalBrokerConnection = class {
|
|
|
1808
1873
|
if (this.deferredOperations.has(operationId)) return;
|
|
1809
1874
|
entry.deferredOperation = operationId;
|
|
1810
1875
|
this.deferredOperations.set(operationId, requestId);
|
|
1876
|
+
this.clearDeadline(entry);
|
|
1811
1877
|
}
|
|
1812
1878
|
/**
|
|
1813
1879
|
* The single terminal settlement: resolves the still-pending request with
|
|
@@ -1815,15 +1881,17 @@ var LocalBrokerConnection = class {
|
|
|
1815
1881
|
* typed wire error (`"error"`). Terminals for unknown or already-settled
|
|
1816
1882
|
* operationIds are silently dropped - the broker side already settles
|
|
1817
1883
|
* through a one-shot CAS; the client mirrors that stateless drop instead of
|
|
1818
|
-
* guessing an owner.
|
|
1884
|
+
* guessing an owner. A terminal arriving after the call's deadline expired
|
|
1885
|
+
* is the same stateless drop (W1).
|
|
1819
1886
|
*/
|
|
1820
1887
|
settleDeferredOperation(operationId, payload) {
|
|
1821
1888
|
const requestId = this.deferredOperations.get(operationId);
|
|
1822
1889
|
if (requestId === void 0) return;
|
|
1823
|
-
const entry = this.
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1890
|
+
const entry = this.takePending(requestId);
|
|
1891
|
+
if (entry === void 0) {
|
|
1892
|
+
this.deferredOperations.delete(operationId);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1827
1895
|
if (payload.kind === "result") {
|
|
1828
1896
|
const terminal = {
|
|
1829
1897
|
type: "ext-operation-terminal",
|
|
@@ -1836,10 +1904,83 @@ var LocalBrokerConnection = class {
|
|
|
1836
1904
|
const wireError = payload.error;
|
|
1837
1905
|
entry.reject(new ExtensionOperationError(wireError.code, wireError.message, { details: wireError.details }));
|
|
1838
1906
|
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Removes and fully cleans one pending entry: clears its deadline timer and
|
|
1909
|
+
* the deferred-operation reverse mapping, so every settle path (response,
|
|
1910
|
+
* correlated error, deferred terminal, expiry) leaves no timer or mapping
|
|
1911
|
+
* behind.
|
|
1912
|
+
*/
|
|
1913
|
+
takePending(requestId) {
|
|
1914
|
+
const entry = this.pending.get(requestId);
|
|
1915
|
+
if (entry === void 0) return;
|
|
1916
|
+
this.pending.delete(requestId);
|
|
1917
|
+
this.clearDeadline(entry);
|
|
1918
|
+
if (entry.deferredOperation !== void 0) this.deferredOperations.delete(entry.deferredOperation);
|
|
1919
|
+
return entry;
|
|
1920
|
+
}
|
|
1921
|
+
clearDeadline(entry) {
|
|
1922
|
+
if (entry.deadlineTimer !== void 0) {
|
|
1923
|
+
clearTimeout(entry.deadlineTimer);
|
|
1924
|
+
entry.deadlineTimer = void 0;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Deadline expiry (W1): settles exactly this call with the typed timeout
|
|
1929
|
+
* rejection and removes its entries, so a late reply or terminal finds no
|
|
1930
|
+
* owner. Deliberately NOT `markDead` — an unanswered call is one liveness
|
|
1931
|
+
* signal, not a death verdict; the connection stays usable and the death
|
|
1932
|
+
* listeners stay silent. The expiry is reported through the internal seam
|
|
1933
|
+
* for the Phase C heartbeat's failure classification.
|
|
1934
|
+
*/
|
|
1935
|
+
expireRequest(requestId, deadlineMs) {
|
|
1936
|
+
const entry = this.pending.get(requestId);
|
|
1937
|
+
if (entry === void 0) return;
|
|
1938
|
+
this.takePending(requestId);
|
|
1939
|
+
this.notifyDeadlineExpiry({
|
|
1940
|
+
requestId,
|
|
1941
|
+
deadlineMs
|
|
1942
|
+
});
|
|
1943
|
+
entry.reject(new TransportTimeoutError(requestId, deadlineMs));
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Bootstrap-class budget (W1): the Ready frame must arrive within
|
|
1947
|
+
* `BOOTSTRAP_CALL_DEADLINE_MS` or init settles with the typed timeout
|
|
1948
|
+
* rejection. Like per-call expiry this never marks the transport dead —
|
|
1949
|
+
* `connectLocalBroker`'s error path performs the bounded close.
|
|
1950
|
+
*/
|
|
1951
|
+
raceBootstrapDeadline(ready) {
|
|
1952
|
+
return new Promise((resolve, reject) => {
|
|
1953
|
+
let bootstrapTimer = setTimeout(() => {
|
|
1954
|
+
bootstrapTimer = void 0;
|
|
1955
|
+
this.notifyDeadlineExpiry({
|
|
1956
|
+
requestId: INIT_REQUEST_ID,
|
|
1957
|
+
deadlineMs: BOOTSTRAP_CALL_DEADLINE_MS
|
|
1958
|
+
});
|
|
1959
|
+
reject(new TransportTimeoutError(INIT_REQUEST_ID, BOOTSTRAP_CALL_DEADLINE_MS));
|
|
1960
|
+
}, BOOTSTRAP_CALL_DEADLINE_MS);
|
|
1961
|
+
bootstrapTimer.unref();
|
|
1962
|
+
const settle = (settleFn) => {
|
|
1963
|
+
if (bootstrapTimer !== void 0) {
|
|
1964
|
+
clearTimeout(bootstrapTimer);
|
|
1965
|
+
bootstrapTimer = void 0;
|
|
1966
|
+
}
|
|
1967
|
+
settleFn();
|
|
1968
|
+
};
|
|
1969
|
+
ready.then((frame) => settle(() => resolve(frame)), (error) => settle(() => reject(error)));
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
notifyDeadlineExpiry(expiry) {
|
|
1973
|
+
for (const listener of [...this.deadlineExpiryListeners]) try {
|
|
1974
|
+
listener(expiry);
|
|
1975
|
+
} catch {}
|
|
1976
|
+
}
|
|
1839
1977
|
rejectAll(error) {
|
|
1840
1978
|
this.ready?.reject(error);
|
|
1841
1979
|
this.ready = void 0;
|
|
1842
|
-
for (const pending of this.pending.values())
|
|
1980
|
+
for (const pending of this.pending.values()) {
|
|
1981
|
+
this.clearDeadline(pending);
|
|
1982
|
+
pending.reject(error);
|
|
1983
|
+
}
|
|
1843
1984
|
this.pending.clear();
|
|
1844
1985
|
this.deferredOperations.clear();
|
|
1845
1986
|
}
|
|
@@ -1858,7 +1999,10 @@ var LocalBrokerConnection = class {
|
|
|
1858
1999
|
this.deadError = error;
|
|
1859
2000
|
this.ready?.reject(error);
|
|
1860
2001
|
this.ready = void 0;
|
|
1861
|
-
for (const pending of this.pending.values())
|
|
2002
|
+
for (const pending of this.pending.values()) {
|
|
2003
|
+
this.clearDeadline(pending);
|
|
2004
|
+
pending.reject(pending.deferredOperation === void 0 ? error : extensionTransportClosedError(error));
|
|
2005
|
+
}
|
|
1862
2006
|
this.pending.clear();
|
|
1863
2007
|
this.deferredOperations.clear();
|
|
1864
2008
|
for (const listener of [...this.deadListeners]) try {
|
|
@@ -1897,6 +2041,643 @@ const resolveCliEntrypoint = () => {
|
|
|
1897
2041
|
const suffix = fileURLToPath(import.meta.url).endsWith(".ts") ? "cli.ts" : "cli.mjs";
|
|
1898
2042
|
return fileURLToPath(new URL(`./${suffix}`, import.meta.url));
|
|
1899
2043
|
};
|
|
2044
|
+
/**
|
|
2045
|
+
* Typed fail-fast rejection for every call issued after supervision reached
|
|
2046
|
+
* its terminal `abandoned` state: the recovery budget is exhausted and the
|
|
2047
|
+
* runtime stays headless by design (the consumer process keeps running).
|
|
2048
|
+
* Consumers match on `code` (`transport_abandoned`); `details` carries the
|
|
2049
|
+
* budget facts `{ recoveries, windowMs }`.
|
|
2050
|
+
*/
|
|
2051
|
+
var TransportAbandonedError = class extends Error {
|
|
2052
|
+
code = "transport_abandoned";
|
|
2053
|
+
details;
|
|
2054
|
+
constructor(recoveries, windowMs) {
|
|
2055
|
+
super(`transport abandoned: ${recoveries} recovery attempts within ${windowMs}ms exhausted the budget; every later call fails fast`);
|
|
2056
|
+
this.name = "TransportAbandonedError";
|
|
2057
|
+
this.details = {
|
|
2058
|
+
recoveries,
|
|
2059
|
+
windowMs
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
const journalKeyOf = (entry) => {
|
|
2064
|
+
switch (entry.kind) {
|
|
2065
|
+
case "resolve-default-app": return "resolve-default-app";
|
|
2066
|
+
case "create-tray": return;
|
|
2067
|
+
case "set-tray-menu":
|
|
2068
|
+
case "set-tray-icon":
|
|
2069
|
+
case "set-tray-tooltip": return `${entry.kind}:${entry.appId}:${entry.trayId}`;
|
|
2070
|
+
case "set-app-name":
|
|
2071
|
+
case "set-app-icon":
|
|
2072
|
+
case "set-app-icon-variant": return `${entry.kind}:${entry.appId}`;
|
|
2073
|
+
case "load-ext": return `load-ext:${entry.appId}:${entry.mountId ?? entry.name}`;
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
const isDeadlineExpirySource = (connection) => "onDeadlineExpiry" in connection && typeof connection.onDeadlineExpiry === "function";
|
|
2077
|
+
const isConnectionDeadSource = (connection) => "onConnectionDead" in connection && typeof connection.onConnectionDead === "function";
|
|
2078
|
+
const sleepUnref = (ms) => new Promise((resolve) => {
|
|
2079
|
+
setTimeout(resolve, ms).unref();
|
|
2080
|
+
});
|
|
2081
|
+
/**
|
|
2082
|
+
* Supervised transport (W2/W4/W5/W6): a stable `OpenTrayConnection`-shaped
|
|
2083
|
+
* proxy for the layers above (createClient, tray handles) that owns one
|
|
2084
|
+
* broker connection per generation underneath. Consumer event subscriptions
|
|
2085
|
+
* and the supervision state machine survive generation changes;
|
|
2086
|
+
* per-connection surfaces (`onConnectionDead`) stay per-generation truth.
|
|
2087
|
+
*/
|
|
2088
|
+
var SupervisedLocalBrokerConnection = class {
|
|
2089
|
+
connectFactory;
|
|
2090
|
+
heartbeatIntervalMs;
|
|
2091
|
+
probeDeadlineMs;
|
|
2092
|
+
heartbeatFailureThreshold;
|
|
2093
|
+
recoveryEnabled;
|
|
2094
|
+
maxRestarts;
|
|
2095
|
+
recoveryWindowMs;
|
|
2096
|
+
cooldownMs;
|
|
2097
|
+
backoffFactor;
|
|
2098
|
+
backoffCapMs;
|
|
2099
|
+
restartApp;
|
|
2100
|
+
connection;
|
|
2101
|
+
generation = 0;
|
|
2102
|
+
state = "healthy";
|
|
2103
|
+
journal = [];
|
|
2104
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
2105
|
+
deadListeners = /* @__PURE__ */ new Set();
|
|
2106
|
+
stateListeners = /* @__PURE__ */ new Set();
|
|
2107
|
+
rebuildCallbacks = [];
|
|
2108
|
+
unwireGeneration = noop$1;
|
|
2109
|
+
heartbeatTimer;
|
|
2110
|
+
heartbeatOrdinal = 0;
|
|
2111
|
+
consecutiveHeartbeatFailures = 0;
|
|
2112
|
+
lastSettledAt = 0;
|
|
2113
|
+
restartTimestamps = [];
|
|
2114
|
+
callerInitiated = false;
|
|
2115
|
+
shutdownPromise;
|
|
2116
|
+
constructor(connectFactory, options) {
|
|
2117
|
+
this.connectFactory = connectFactory;
|
|
2118
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 3e4;
|
|
2119
|
+
this.probeDeadlineMs = options.probeDeadlineMs ?? 3e3;
|
|
2120
|
+
this.heartbeatFailureThreshold = options.heartbeatFailureThreshold ?? 3;
|
|
2121
|
+
const recovery = options.recovery ?? {};
|
|
2122
|
+
this.recoveryEnabled = recovery.enabled ?? true;
|
|
2123
|
+
this.maxRestarts = recovery.maxRestarts ?? 3;
|
|
2124
|
+
this.recoveryWindowMs = recovery.windowMs ?? 6e5;
|
|
2125
|
+
this.cooldownMs = recovery.cooldownMs ?? 1e3;
|
|
2126
|
+
this.backoffFactor = recovery.backoffFactor ?? 2;
|
|
2127
|
+
this.backoffCapMs = recovery.backoffCapMs ?? 3e4;
|
|
2128
|
+
this.restartApp = recovery.restartApp;
|
|
2129
|
+
}
|
|
2130
|
+
/** Current supervision state; edge events go to {@link onTransportStateChange}. */
|
|
2131
|
+
get transportState() {
|
|
2132
|
+
return this.state;
|
|
2133
|
+
}
|
|
2134
|
+
/** Monotonic generation counter; each respawn/replay increments it once. */
|
|
2135
|
+
get transportGeneration() {
|
|
2136
|
+
return this.generation;
|
|
2137
|
+
}
|
|
2138
|
+
/** Current generation's endpoint (proxy; absent before the first connect). */
|
|
2139
|
+
get endpoint() {
|
|
2140
|
+
return this.connection?.endpoint ?? "";
|
|
2141
|
+
}
|
|
2142
|
+
/** Current generation's caller label (proxy; absent before the first connect). */
|
|
2143
|
+
get callerLabel() {
|
|
2144
|
+
return this.connection?.callerLabel ?? "";
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Current generation's broker session id. Read dynamically so extension
|
|
2148
|
+
* contexts and rebuild callbacks can attribute session-owned state to the
|
|
2149
|
+
* live generation (the id changes on every respawn).
|
|
2150
|
+
*/
|
|
2151
|
+
get sessionId() {
|
|
2152
|
+
return this.connection?.sessionId ?? "";
|
|
2153
|
+
}
|
|
2154
|
+
/** True once the current generation's connection died (or none exists). */
|
|
2155
|
+
get connectionDead() {
|
|
2156
|
+
return this.connection === void 0 || this.connection.connectionDead;
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Establishes generation 0. A failure here propagates to the caller
|
|
2160
|
+
* unchanged — supervision (heartbeat, recovery) only guards a connection
|
|
2161
|
+
* that once existed; an app that never started is a plain startup failure.
|
|
2162
|
+
*/
|
|
2163
|
+
async connect() {
|
|
2164
|
+
const connection = await this.connectFactory();
|
|
2165
|
+
this.connection = connection;
|
|
2166
|
+
this.unwireGeneration = this.wireGeneration(connection);
|
|
2167
|
+
this.lastSettledAt = Date.now();
|
|
2168
|
+
this.consecutiveHeartbeatFailures = 0;
|
|
2169
|
+
this.startHeartbeat();
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Forwards one request to the current generation. After the terminal
|
|
2173
|
+
* `abandoned` state every call fails fast with {@link TransportAbandonedError}.
|
|
2174
|
+
* A teardown-budget call (`deadlineMs === TEARDOWN_CALL_DEADLINE_MS`) marks
|
|
2175
|
+
* the supervision caller-initiated: Phase A froze the budget class as the
|
|
2176
|
+
* caller-intent channel, so a socket close racing caller-initiated destroy
|
|
2177
|
+
* can never be misread as uninvited death.
|
|
2178
|
+
*/
|
|
2179
|
+
async request(frame, options = {}) {
|
|
2180
|
+
if (this.state === "abandoned") throw new TransportAbandonedError(this.restartTimestamps.length, this.recoveryWindowMs);
|
|
2181
|
+
if (options.deadlineMs === 2e3) this.callerInitiated = true;
|
|
2182
|
+
const connection = this.connection;
|
|
2183
|
+
if (connection === void 0) throw new Error(BROKER_CONNECTION_CLOSED_MESSAGE);
|
|
2184
|
+
const candidate = journalCandidateFrom(frame);
|
|
2185
|
+
const response = await connection.request(frame, options);
|
|
2186
|
+
this.noteSuccessfulSettlement();
|
|
2187
|
+
if (candidate !== void 0) this.commitJournalEntry(candidate, response);
|
|
2188
|
+
if (frame.type === "destroy-tray") this.evictJournalTray(frame.appId, frame.trayId);
|
|
2189
|
+
return response;
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Consumer event subscription that survives generation changes: each new
|
|
2193
|
+
* generation's event stream fans out to the same listeners.
|
|
2194
|
+
*/
|
|
2195
|
+
onEvent(listener) {
|
|
2196
|
+
this.eventListeners.add(listener);
|
|
2197
|
+
return () => {
|
|
2198
|
+
this.eventListeners.delete(listener);
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Per-connection death notification, forwarded for every generation (D3
|
|
2203
|
+
* semantics preserved: consumers using it for per-connection cleanup keep
|
|
2204
|
+
* working; the supervision state machine is the new higher-level truth).
|
|
2205
|
+
*/
|
|
2206
|
+
onConnectionDead(listener) {
|
|
2207
|
+
this.deadListeners.add(listener);
|
|
2208
|
+
return () => {
|
|
2209
|
+
this.deadListeners.delete(listener);
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Tier 1 health projection (W6): edge-triggered `healthy | recovering |
|
|
2214
|
+
* abandoned` transitions. The initial `healthy` state never fires —
|
|
2215
|
+
* consumers reading the current state use `transportState`.
|
|
2216
|
+
*/
|
|
2217
|
+
onTransportStateChange(listener) {
|
|
2218
|
+
this.stateListeners.add(listener);
|
|
2219
|
+
return () => {
|
|
2220
|
+
this.stateListeners.delete(listener);
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Registers one facade rebuild callback (W4). Callbacks run in
|
|
2225
|
+
* registration order after the core journal replay on every successful
|
|
2226
|
+
* recovery, receiving the fresh generation number and session id so
|
|
2227
|
+
* session-attributed native state can be re-created against the live
|
|
2228
|
+
* broker session.
|
|
2229
|
+
*/
|
|
2230
|
+
registerTransportRebuild(rebuild) {
|
|
2231
|
+
this.rebuildCallbacks.push(rebuild);
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Graceful teardown channel (W4/W3): idempotent, marks the supervision
|
|
2235
|
+
* caller-initiated synchronously (so the close-driven death event can
|
|
2236
|
+
* never trigger recovery), stops the heartbeat, then closes the current
|
|
2237
|
+
* generation within its bounded graceful-close budget.
|
|
2238
|
+
*/
|
|
2239
|
+
async shutdown() {
|
|
2240
|
+
this.callerInitiated = true;
|
|
2241
|
+
this.shutdownPromise ??= (async () => {
|
|
2242
|
+
this.stopHeartbeat();
|
|
2243
|
+
this.unwireGeneration();
|
|
2244
|
+
this.unwireGeneration = noop$1;
|
|
2245
|
+
const connection = this.connection;
|
|
2246
|
+
this.connection = void 0;
|
|
2247
|
+
if (connection !== void 0) await connection.close().catch(noopAsync);
|
|
2248
|
+
})();
|
|
2249
|
+
await this.shutdownPromise;
|
|
2250
|
+
}
|
|
2251
|
+
/** LocalBrokerClient structural compatibility: graceful close == shutdown. */
|
|
2252
|
+
close() {
|
|
2253
|
+
return this.shutdown();
|
|
2254
|
+
}
|
|
2255
|
+
startHeartbeat() {
|
|
2256
|
+
if (this.heartbeatTimer !== void 0) return;
|
|
2257
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2258
|
+
this.heartbeatTick();
|
|
2259
|
+
}, this.heartbeatIntervalMs);
|
|
2260
|
+
this.heartbeatTimer.unref();
|
|
2261
|
+
}
|
|
2262
|
+
stopHeartbeat() {
|
|
2263
|
+
if (this.heartbeatTimer !== void 0) {
|
|
2264
|
+
clearInterval(this.heartbeatTimer);
|
|
2265
|
+
this.heartbeatTimer = void 0;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Idle-gated heartbeat (W2): a transport that keeps settling requests
|
|
2270
|
+
* proves its own liveness, so the probe budget is spent only on silence.
|
|
2271
|
+
* A probe failure of any rejection class (typed timeout, transport-lost,
|
|
2272
|
+
* broker error frame) counts; a successful settlement resets the streak.
|
|
2273
|
+
*/
|
|
2274
|
+
async heartbeatTick() {
|
|
2275
|
+
const connection = this.connection;
|
|
2276
|
+
if (connection === void 0 || this.state !== "healthy" || this.callerInitiated) return;
|
|
2277
|
+
if (Date.now() - this.lastSettledAt < this.heartbeatIntervalMs) return;
|
|
2278
|
+
const requestId = `supervisor-heartbeat-${this.generation}-${this.heartbeatOrdinal += 1}`;
|
|
2279
|
+
try {
|
|
2280
|
+
await connection.request({
|
|
2281
|
+
type: "health",
|
|
2282
|
+
requestId
|
|
2283
|
+
}, { deadlineMs: this.probeDeadlineMs });
|
|
2284
|
+
this.consecutiveHeartbeatFailures = 0;
|
|
2285
|
+
this.noteSuccessfulSettlement();
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
this.consecutiveHeartbeatFailures += 1;
|
|
2288
|
+
if (this.consecutiveHeartbeatFailures < this.heartbeatFailureThreshold) return;
|
|
2289
|
+
const death = new Error(BROKER_CONNECTION_CLOSED_MESSAGE, { cause: `heartbeat declared death after ${this.consecutiveHeartbeatFailures} consecutive probe failures (${String(error)})` });
|
|
2290
|
+
this.handleGenerationDeath(death);
|
|
2291
|
+
connection.close().catch(noopAsync);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
noteSuccessfulSettlement() {
|
|
2295
|
+
this.lastSettledAt = Date.now();
|
|
2296
|
+
this.consecutiveHeartbeatFailures = 0;
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Subscribes this supervisor's stable listener sets to one generation.
|
|
2300
|
+
* Event fanout and the death/expiry seams are wired before the generation
|
|
2301
|
+
* becomes `this.connection` (recovery candidates are wired before replay)
|
|
2302
|
+
* so no observable event window opens between adoption steps.
|
|
2303
|
+
*/
|
|
2304
|
+
wireGeneration(connection) {
|
|
2305
|
+
const unwires = [];
|
|
2306
|
+
const unlistenEvents = connection.onEvent((frame) => {
|
|
2307
|
+
for (const listener of [...this.eventListeners]) try {
|
|
2308
|
+
listener(frame);
|
|
2309
|
+
} catch {}
|
|
2310
|
+
});
|
|
2311
|
+
unwires.push(unlistenEvents);
|
|
2312
|
+
if (isConnectionDeadSource(connection)) {
|
|
2313
|
+
const unlistenDead = connection.onConnectionDead((error) => {
|
|
2314
|
+
this.handleGenerationDeath(error);
|
|
2315
|
+
});
|
|
2316
|
+
unwires.push(unlistenDead);
|
|
2317
|
+
}
|
|
2318
|
+
if (isDeadlineExpirySource(connection)) {
|
|
2319
|
+
const unlistenExpiry = connection.onDeadlineExpiry((expiry) => {
|
|
2320
|
+
if (expiry.requestId.startsWith("supervisor-heartbeat-")) return;
|
|
2321
|
+
this.consecutiveHeartbeatFailures += 1;
|
|
2322
|
+
if (this.consecutiveHeartbeatFailures >= this.heartbeatFailureThreshold && this.state === "healthy" && !this.callerInitiated) {
|
|
2323
|
+
const connectionAtDeath = this.connection;
|
|
2324
|
+
this.handleGenerationDeath(new Error(BROKER_CONNECTION_CLOSED_MESSAGE, { cause: `heartbeat declared death after ${this.consecutiveHeartbeatFailures} consecutive deadline expiries` }));
|
|
2325
|
+
if (connectionAtDeath !== void 0) connectionAtDeath.close().catch(noopAsync);
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
unwires.push(unlistenExpiry);
|
|
2329
|
+
}
|
|
2330
|
+
return () => {
|
|
2331
|
+
for (const unwire of unwires.splice(0)) unwire();
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Death funnel: forwards the per-connection event first (D3 truth), stops
|
|
2336
|
+
* this generation's wiring, then decides the supervision reaction. The
|
|
2337
|
+
* state guard makes the reaction single-flight — deaths of candidates
|
|
2338
|
+
* during an ongoing recovery only forward, they never nest recoveries.
|
|
2339
|
+
*/
|
|
2340
|
+
handleGenerationDeath(error) {
|
|
2341
|
+
for (const listener of [...this.deadListeners]) try {
|
|
2342
|
+
listener(error);
|
|
2343
|
+
} catch {}
|
|
2344
|
+
if (this.callerInitiated || this.state !== "healthy") return;
|
|
2345
|
+
this.unwireGeneration();
|
|
2346
|
+
this.unwireGeneration = noop$1;
|
|
2347
|
+
if (this.restartApp !== void 0) {
|
|
2348
|
+
this.handOverToRestartApp();
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (!this.recoveryEnabled) return;
|
|
2352
|
+
this.runRecovery();
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Tier 2 hand-over: bounded teardown of the current generation, the app's
|
|
2356
|
+
* restart vector invoked exactly once, then terminal. No in-process
|
|
2357
|
+
* rebuild, no loop — restart ownership belongs to the callback.
|
|
2358
|
+
*/
|
|
2359
|
+
async handOverToRestartApp() {
|
|
2360
|
+
this.setState("recovering");
|
|
2361
|
+
this.callerInitiated = true;
|
|
2362
|
+
this.stopHeartbeat();
|
|
2363
|
+
const connection = this.connection;
|
|
2364
|
+
if (connection !== void 0 && !connection.connectionDead) await connection.close().catch(noopAsync);
|
|
2365
|
+
try {
|
|
2366
|
+
await this.restartApp?.();
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
console.error("OpenTray recovery.restartApp callback failed:", error);
|
|
2369
|
+
}
|
|
2370
|
+
this.setState("abandoned");
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* In-process recovery (W4/W5): cooldown backoff → reconnect through the
|
|
2374
|
+
* original connect options → journal replay → facade rebuilds → healthy.
|
|
2375
|
+
* Every attempt consumes budget up front; a failed attempt (connect,
|
|
2376
|
+
* replay, or rebuild — including artifact identity mismatches) retries
|
|
2377
|
+
* inside the remaining budget until exhaustion abandons the runtime.
|
|
2378
|
+
*/
|
|
2379
|
+
async runRecovery() {
|
|
2380
|
+
this.setState("recovering");
|
|
2381
|
+
let backoffOrdinal = 0;
|
|
2382
|
+
while (true) {
|
|
2383
|
+
if (this.callerInitiated) return;
|
|
2384
|
+
if (!this.consumeRestartBudget()) {
|
|
2385
|
+
this.setState("abandoned");
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
await sleepUnref(this.cooldownForBackoff(backoffOrdinal));
|
|
2389
|
+
if (this.callerInitiated) return;
|
|
2390
|
+
try {
|
|
2391
|
+
const candidate = await this.connectFactory();
|
|
2392
|
+
await this.adoptGeneration(candidate);
|
|
2393
|
+
this.setState("healthy");
|
|
2394
|
+
return;
|
|
2395
|
+
} catch {
|
|
2396
|
+
backoffOrdinal += 1;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
cooldownForBackoff(backoffOrdinal) {
|
|
2401
|
+
return Math.min(this.cooldownMs * this.backoffFactor ** backoffOrdinal, this.backoffCapMs);
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Sliding-window budget: one timestamp per respawn attempt (successful or
|
|
2405
|
+
* not — both are respawns the consumer experienced). In-memory per
|
|
2406
|
+
* supervisor by ruling; a fresh `createTray` starts a fresh budget.
|
|
2407
|
+
*/
|
|
2408
|
+
consumeRestartBudget() {
|
|
2409
|
+
const windowStart = Date.now() - this.recoveryWindowMs;
|
|
2410
|
+
this.restartTimestamps = this.restartTimestamps.filter((timestamp) => timestamp > windowStart);
|
|
2411
|
+
if (this.restartTimestamps.length >= this.maxRestarts) return false;
|
|
2412
|
+
this.restartTimestamps.push(Date.now());
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Wires the candidate generation, replays the journal, and runs the
|
|
2417
|
+
* facade rebuild callbacks. `this.connection` is committed only after the
|
|
2418
|
+
* full sequence succeeds: consumer calls during recovery keep failing
|
|
2419
|
+
* fast against the dead generation instead of racing a half-replayed
|
|
2420
|
+
* broker. A failing candidate is unwired and closed (bounded) before the
|
|
2421
|
+
* attempt is reported failed.
|
|
2422
|
+
*/
|
|
2423
|
+
async adoptGeneration(candidate) {
|
|
2424
|
+
this.generation += 1;
|
|
2425
|
+
this.unwireGeneration = this.wireGeneration(candidate);
|
|
2426
|
+
try {
|
|
2427
|
+
await this.replayJournal(candidate);
|
|
2428
|
+
for (const rebuild of [...this.rebuildCallbacks]) await rebuild({
|
|
2429
|
+
generation: this.generation,
|
|
2430
|
+
sessionId: candidate.sessionId.length > 0 ? candidate.sessionId : void 0
|
|
2431
|
+
});
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
this.unwireGeneration();
|
|
2434
|
+
this.unwireGeneration = noop$1;
|
|
2435
|
+
await candidate.close().catch(noopAsync);
|
|
2436
|
+
throw error;
|
|
2437
|
+
}
|
|
2438
|
+
this.connection = candidate;
|
|
2439
|
+
this.consecutiveHeartbeatFailures = 0;
|
|
2440
|
+
this.noteSuccessfulSettlement();
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Replays the declarative journal in record order with fresh request ids
|
|
2444
|
+
* under the bootstrap deadline class. The resolved default app of the new
|
|
2445
|
+
* generation becomes the appId authority: replayed frames are rewritten
|
|
2446
|
+
* onto it, so app-scoped state follows broker truth instead of a stale
|
|
2447
|
+
* generation's identity.
|
|
2448
|
+
*/
|
|
2449
|
+
async replayJournal(connection) {
|
|
2450
|
+
let ordinal = 0;
|
|
2451
|
+
const appIdRewrites = /* @__PURE__ */ new Map();
|
|
2452
|
+
const rewriteAppId = (appId) => appIdRewrites.get(appId) ?? appId;
|
|
2453
|
+
for (const entry of this.journal) {
|
|
2454
|
+
ordinal += 1;
|
|
2455
|
+
const requestId = `supervisor-replay-${this.generation}-${ordinal}`;
|
|
2456
|
+
let frame;
|
|
2457
|
+
switch (entry.kind) {
|
|
2458
|
+
case "resolve-default-app":
|
|
2459
|
+
frame = {
|
|
2460
|
+
type: "resolve-default-app",
|
|
2461
|
+
requestId
|
|
2462
|
+
};
|
|
2463
|
+
break;
|
|
2464
|
+
case "create-tray":
|
|
2465
|
+
frame = {
|
|
2466
|
+
type: "create-tray",
|
|
2467
|
+
requestId,
|
|
2468
|
+
app: { appId: rewriteAppId(entry.app.appId) },
|
|
2469
|
+
tray: {
|
|
2470
|
+
...entry.tray,
|
|
2471
|
+
id: entry.trayId
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
break;
|
|
2475
|
+
case "set-tray-menu":
|
|
2476
|
+
frame = {
|
|
2477
|
+
type: "set-tray-menu",
|
|
2478
|
+
requestId,
|
|
2479
|
+
appId: rewriteAppId(entry.appId),
|
|
2480
|
+
trayId: entry.trayId,
|
|
2481
|
+
menu: entry.menu
|
|
2482
|
+
};
|
|
2483
|
+
break;
|
|
2484
|
+
case "set-tray-icon":
|
|
2485
|
+
frame = {
|
|
2486
|
+
type: "set-tray-icon",
|
|
2487
|
+
requestId,
|
|
2488
|
+
appId: rewriteAppId(entry.appId),
|
|
2489
|
+
trayId: entry.trayId,
|
|
2490
|
+
icon: entry.icon
|
|
2491
|
+
};
|
|
2492
|
+
break;
|
|
2493
|
+
case "set-tray-tooltip":
|
|
2494
|
+
frame = {
|
|
2495
|
+
type: "set-tray-tooltip",
|
|
2496
|
+
requestId,
|
|
2497
|
+
appId: rewriteAppId(entry.appId),
|
|
2498
|
+
trayId: entry.trayId,
|
|
2499
|
+
tooltip: entry.tooltip
|
|
2500
|
+
};
|
|
2501
|
+
break;
|
|
2502
|
+
case "set-app-name":
|
|
2503
|
+
frame = {
|
|
2504
|
+
type: "set-app-name",
|
|
2505
|
+
requestId,
|
|
2506
|
+
appId: rewriteAppId(entry.appId),
|
|
2507
|
+
name: entry.name
|
|
2508
|
+
};
|
|
2509
|
+
break;
|
|
2510
|
+
case "set-app-icon":
|
|
2511
|
+
frame = {
|
|
2512
|
+
type: "set-app-icon",
|
|
2513
|
+
requestId,
|
|
2514
|
+
appId: rewriteAppId(entry.appId),
|
|
2515
|
+
appIcon: entry.appIcon
|
|
2516
|
+
};
|
|
2517
|
+
break;
|
|
2518
|
+
case "set-app-icon-variant":
|
|
2519
|
+
frame = {
|
|
2520
|
+
type: "set-app-icon-variant",
|
|
2521
|
+
requestId,
|
|
2522
|
+
appId: rewriteAppId(entry.appId),
|
|
2523
|
+
variant: entry.variant
|
|
2524
|
+
};
|
|
2525
|
+
break;
|
|
2526
|
+
case "load-ext":
|
|
2527
|
+
frame = {
|
|
2528
|
+
type: "load-ext",
|
|
2529
|
+
requestId,
|
|
2530
|
+
appId: rewriteAppId(entry.appId),
|
|
2531
|
+
name: entry.name,
|
|
2532
|
+
path: entry.path,
|
|
2533
|
+
expectedIdentity: entry.expectedIdentity,
|
|
2534
|
+
...entry.mountId === void 0 ? {} : { mountId: entry.mountId }
|
|
2535
|
+
};
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2538
|
+
const response = await connection.request(frame, { deadlineMs: BOOTSTRAP_CALL_DEADLINE_MS });
|
|
2539
|
+
if (response.type === "error") throw new BrokerServerError(response.code, response.message, { details: response.details });
|
|
2540
|
+
if (entry.kind === "resolve-default-app" && response.type === "default-app") appIdRewrites.set(entry.appId, response.app.appId);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
setState(state) {
|
|
2544
|
+
if (this.state === state) return;
|
|
2545
|
+
this.state = state;
|
|
2546
|
+
if (state === "abandoned") this.stopHeartbeat();
|
|
2547
|
+
for (const listener of [...this.stateListeners]) try {
|
|
2548
|
+
listener(state);
|
|
2549
|
+
} catch {}
|
|
2550
|
+
}
|
|
2551
|
+
commitJournalEntry(frame, response) {
|
|
2552
|
+
switch (frame.type) {
|
|
2553
|
+
case "resolve-default-app":
|
|
2554
|
+
if (response.type === "default-app") this.upsertJournal({
|
|
2555
|
+
kind: "resolve-default-app",
|
|
2556
|
+
appId: response.app.appId
|
|
2557
|
+
});
|
|
2558
|
+
return;
|
|
2559
|
+
case "create-tray": {
|
|
2560
|
+
const trayId = response.type === "tray-created" && typeof response.trayId === "string" ? response.trayId : frame.tray.id;
|
|
2561
|
+
this.journal.push({
|
|
2562
|
+
kind: "create-tray",
|
|
2563
|
+
app: frame.app,
|
|
2564
|
+
tray: frame.tray,
|
|
2565
|
+
trayId
|
|
2566
|
+
});
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
case "set-tray-menu":
|
|
2570
|
+
this.upsertJournal({
|
|
2571
|
+
kind: "set-tray-menu",
|
|
2572
|
+
appId: frame.appId,
|
|
2573
|
+
trayId: frame.trayId,
|
|
2574
|
+
menu: frame.menu
|
|
2575
|
+
});
|
|
2576
|
+
return;
|
|
2577
|
+
case "set-tray-icon":
|
|
2578
|
+
this.upsertJournal({
|
|
2579
|
+
kind: "set-tray-icon",
|
|
2580
|
+
appId: frame.appId,
|
|
2581
|
+
trayId: frame.trayId,
|
|
2582
|
+
icon: frame.icon
|
|
2583
|
+
});
|
|
2584
|
+
return;
|
|
2585
|
+
case "set-tray-tooltip":
|
|
2586
|
+
this.upsertJournal({
|
|
2587
|
+
kind: "set-tray-tooltip",
|
|
2588
|
+
appId: frame.appId,
|
|
2589
|
+
trayId: frame.trayId,
|
|
2590
|
+
tooltip: frame.tooltip
|
|
2591
|
+
});
|
|
2592
|
+
return;
|
|
2593
|
+
case "set-app-name":
|
|
2594
|
+
this.upsertJournal({
|
|
2595
|
+
kind: "set-app-name",
|
|
2596
|
+
appId: frame.appId,
|
|
2597
|
+
name: frame.name
|
|
2598
|
+
});
|
|
2599
|
+
return;
|
|
2600
|
+
case "set-app-icon":
|
|
2601
|
+
this.upsertJournal({
|
|
2602
|
+
kind: "set-app-icon",
|
|
2603
|
+
appId: frame.appId,
|
|
2604
|
+
appIcon: frame.appIcon
|
|
2605
|
+
});
|
|
2606
|
+
return;
|
|
2607
|
+
case "set-app-icon-variant":
|
|
2608
|
+
this.upsertJournal({
|
|
2609
|
+
kind: "set-app-icon-variant",
|
|
2610
|
+
appId: frame.appId,
|
|
2611
|
+
variant: frame.variant
|
|
2612
|
+
});
|
|
2613
|
+
return;
|
|
2614
|
+
case "load-ext":
|
|
2615
|
+
this.upsertJournal({
|
|
2616
|
+
kind: "load-ext",
|
|
2617
|
+
appId: frame.appId,
|
|
2618
|
+
name: frame.name,
|
|
2619
|
+
path: frame.path,
|
|
2620
|
+
expectedIdentity: frame.expectedIdentity,
|
|
2621
|
+
...frame.mountId === void 0 ? {} : { mountId: frame.mountId }
|
|
2622
|
+
});
|
|
2623
|
+
return;
|
|
2624
|
+
default: return;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Keyed entries replace in place at first-occurrence position (last-write-
|
|
2629
|
+
* wins value, stable ordering); first occurrences append. Tray creations
|
|
2630
|
+
* and mount declarations keep their creation order by construction.
|
|
2631
|
+
*/
|
|
2632
|
+
upsertJournal(entry) {
|
|
2633
|
+
const key = journalKeyOf(entry);
|
|
2634
|
+
if (key === void 0) {
|
|
2635
|
+
this.journal.push(entry);
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
const existing = this.journal.findIndex((candidate) => journalKeyOf(candidate) === key);
|
|
2639
|
+
if (existing < 0) {
|
|
2640
|
+
this.journal.push(entry);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
this.journal[existing] = entry;
|
|
2644
|
+
}
|
|
2645
|
+
evictJournalTray(appId, trayId) {
|
|
2646
|
+
this.journal = this.journal.filter((entry) => {
|
|
2647
|
+
switch (entry.kind) {
|
|
2648
|
+
case "create-tray": return !(entry.app.appId === appId && entry.trayId === trayId);
|
|
2649
|
+
case "set-tray-menu":
|
|
2650
|
+
case "set-tray-icon":
|
|
2651
|
+
case "set-tray-tooltip": return !(entry.appId === appId && entry.trayId === trayId);
|
|
2652
|
+
default: return true;
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
};
|
|
2657
|
+
/**
|
|
2658
|
+
* Snapshot candidate for the declarative journal: a defensive clone of every
|
|
2659
|
+
* journalable mutating frame, taken before the await so later caller
|
|
2660
|
+
* mutation of the frame object cannot corrupt the recorded state. The entry
|
|
2661
|
+
* is committed only after the frame settles successfully.
|
|
2662
|
+
*/
|
|
2663
|
+
const journalCandidateFrom = (frame) => {
|
|
2664
|
+
switch (frame.type) {
|
|
2665
|
+
case "resolve-default-app":
|
|
2666
|
+
case "create-tray":
|
|
2667
|
+
case "set-tray-menu":
|
|
2668
|
+
case "set-tray-icon":
|
|
2669
|
+
case "set-tray-tooltip":
|
|
2670
|
+
case "set-app-name":
|
|
2671
|
+
case "set-app-icon":
|
|
2672
|
+
case "set-app-icon-variant":
|
|
2673
|
+
case "load-ext": return structuredClone(frame);
|
|
2674
|
+
default: return;
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
/** Constructs a supervised transport around one connect factory. */
|
|
2678
|
+
const createTransportSupervisor = (options) => new SupervisedLocalBrokerConnection(options.connect, options);
|
|
2679
|
+
const noop$1 = () => {};
|
|
2680
|
+
const noopAsync = () => {};
|
|
1900
2681
|
//#endregion
|
|
1901
2682
|
//#region src/menu-input.ts
|
|
1902
2683
|
const normalizeCreateTrayMenu = (menu) => {
|
|
@@ -2054,23 +2835,28 @@ const createTray = async (options, runtimeOptions = {}) => {
|
|
|
2054
2835
|
const appIcon = runtimeOptions.appIcon === void 0 ? void 0 : normalizeAppIcon(runtimeOptions.appIcon);
|
|
2055
2836
|
const normalized = normalizeCreateTrayOptions(options);
|
|
2056
2837
|
const appLaunch = normalizeAppLaunch(runtimeOptions.appLaunch);
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2838
|
+
const { recovery, ...brokerOptions } = runtimeOptions;
|
|
2839
|
+
const supervisor = createTransportSupervisor({
|
|
2840
|
+
connect: () => connectLocalBroker({
|
|
2841
|
+
...brokerOptions,
|
|
2842
|
+
...appIcon === void 0 ? {} : { appIcon },
|
|
2843
|
+
appLaunch
|
|
2844
|
+
}),
|
|
2845
|
+
...recovery === void 0 ? {} : { recovery }
|
|
2061
2846
|
});
|
|
2062
2847
|
try {
|
|
2063
|
-
|
|
2848
|
+
await supervisor.connect();
|
|
2849
|
+
const tray = await createClient(supervisor, { appOptions: {
|
|
2064
2850
|
...runtimeOptions.appName === void 0 ? {} : { name: runtimeOptions.appName },
|
|
2065
2851
|
...appIcon === void 0 ? {} : { appIcon }
|
|
2066
2852
|
} }).createTray(normalized.options);
|
|
2067
2853
|
return wrapCreateTrayHandle(tray, {
|
|
2068
|
-
|
|
2854
|
+
shutdown: () => supervisor.shutdown(),
|
|
2069
2855
|
destroyPromise: void 0,
|
|
2070
2856
|
menuUnsubscribe: bindMenuClickHandlers(tray, normalized.menuHandlers)
|
|
2071
2857
|
});
|
|
2072
2858
|
} catch (error) {
|
|
2073
|
-
await
|
|
2859
|
+
await supervisor.shutdown().catch(noop);
|
|
2074
2860
|
throw error;
|
|
2075
2861
|
}
|
|
2076
2862
|
};
|
|
@@ -2117,7 +2903,7 @@ const wrapCreateTrayHandle = (tray, state) => {
|
|
|
2117
2903
|
if (!isTransportClosed(error)) throw error;
|
|
2118
2904
|
} finally {
|
|
2119
2905
|
try {
|
|
2120
|
-
await state.
|
|
2906
|
+
await state.shutdown();
|
|
2121
2907
|
} catch (error) {
|
|
2122
2908
|
if (!isTransportClosed(error)) throw error;
|
|
2123
2909
|
}
|
|
@@ -2135,6 +2921,6 @@ const bindMenuClickHandlers = (tray, handlers) => {
|
|
|
2135
2921
|
};
|
|
2136
2922
|
const noop = () => {};
|
|
2137
2923
|
//#endregion
|
|
2138
|
-
export { AppIconVariantNotFoundError, BROKER_CONNECTION_CLOSED_MESSAGE, BrokerProtocolVersionError, BrokerServerError, DEFAULT_APP_ICON_VARIANT, EMBEDDED_STAGING_MANIFEST_PATH, EXTENSION_EVENT_RECORD_MAX_BYTES, EXTENSION_TRANSPORT_CLOSED_CODE, ExtensionOperationError, InvalidAppIconError, NATIVE_EXTENSION_EMBEDDED_ERROR_CODES, NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX, NativeExtensionArtifactResolutionError, NativeExtensionEmbeddedArtifactError, PROTOCOL_VERSION, createBrokerEndpointIdentity, createClient, createInitFrame, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
|
|
2924
|
+
export { AppIconVariantNotFoundError, BOOTSTRAP_CALL_DEADLINE_MS, BROKER_CONNECTION_CLOSED_MESSAGE, BrokerProtocolVersionError, BrokerServerError, DEFAULT_APP_ICON_VARIANT, EMBEDDED_STAGING_MANIFEST_PATH, EXTENSION_EVENT_RECORD_MAX_BYTES, EXTENSION_TRANSPORT_CLOSED_CODE, ExtensionOperationError, INTERACTIVE_CALL_DEADLINE_MS, InvalidAppIconError, NATIVE_EXTENSION_EMBEDDED_ERROR_CODES, NATIVE_EXTENSION_EMBEDDED_TARGET_MATRIX, NativeExtensionArtifactResolutionError, NativeExtensionEmbeddedArtifactError, PROTOCOL_VERSION, TEARDOWN_CALL_DEADLINE_MS, TransportAbandonedError, TransportTimeoutError, createBrokerEndpointIdentity, createClient, createInitFrame, createTransportSupervisor, createTray, createTrayHandle, formatBrokerEndpointName, formatBrokerStateRoot, formatUnixSocketPath, formatWindowsPipeName, isSupportedProtocolVersion, resolveNativeExtensionArtifact, validateAppIcon };
|
|
2139
2925
|
|
|
2140
2926
|
//# sourceMappingURL=index.mjs.map
|