@econ-v1/domain 6.7.0 → 6.10.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.
Files changed (286) hide show
  1. package/dist/errors/index.d.ts +96 -25
  2. package/dist/errors/index.d.ts.map +1 -1
  3. package/dist/events/index.d.ts +127 -20
  4. package/dist/events/index.d.ts.map +1 -1
  5. package/dist/index.d.ts +250 -47
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/models/agent.d.ts +25 -4
  8. package/dist/models/agent.d.ts.map +1 -1
  9. package/dist/models/app-data.d.ts +84 -0
  10. package/dist/models/app-data.d.ts.map +1 -0
  11. package/dist/models/conversation.d.ts +12 -2
  12. package/dist/models/conversation.d.ts.map +1 -1
  13. package/dist/models/delegation.d.ts +12 -3
  14. package/dist/models/delegation.d.ts.map +1 -1
  15. package/dist/models/device-boot-state.d.ts +12 -0
  16. package/dist/models/device-boot-state.d.ts.map +1 -0
  17. package/dist/models/device-identity.d.ts +11 -3
  18. package/dist/models/device-identity.d.ts.map +1 -1
  19. package/dist/models/graph.d.ts +26 -3
  20. package/dist/models/graph.d.ts.map +1 -1
  21. package/dist/models/index.d.ts +82 -14
  22. package/dist/models/index.d.ts.map +1 -1
  23. package/dist/models/json.d.ts +5 -5
  24. package/dist/models/json.d.ts.map +1 -1
  25. package/dist/models/l402.d.ts +4 -5
  26. package/dist/models/l402.d.ts.map +1 -1
  27. package/dist/models/live-query-definition.d.ts +15 -16
  28. package/dist/models/live-query-definition.d.ts.map +1 -1
  29. package/dist/models/node-app-memory.d.ts +107 -0
  30. package/dist/models/node-app-memory.d.ts.map +1 -0
  31. package/dist/models/outbox-intent.d.ts +3 -3
  32. package/dist/models/outbox-intent.d.ts.map +1 -1
  33. package/dist/models/outbox.d.ts +67 -15
  34. package/dist/models/outbox.d.ts.map +1 -1
  35. package/dist/models/owner-views.d.ts +28 -28
  36. package/dist/models/owner-views.d.ts.map +1 -1
  37. package/dist/models/payment.d.ts +28 -9
  38. package/dist/models/payment.d.ts.map +1 -1
  39. package/dist/models/query-diff.d.ts +2 -2
  40. package/dist/models/query-diff.d.ts.map +1 -1
  41. package/dist/models/release-manifest.d.ts +32 -47
  42. package/dist/models/release-manifest.d.ts.map +1 -1
  43. package/dist/models/repository-write.d.ts +3 -3
  44. package/dist/models/repository-write.d.ts.map +1 -1
  45. package/dist/models/revisioned-commands.d.ts +66 -13
  46. package/dist/models/revisioned-commands.d.ts.map +1 -1
  47. package/dist/models/revisioned.d.ts +50 -10
  48. package/dist/models/revisioned.d.ts.map +1 -1
  49. package/dist/models/settings.d.ts +2 -2
  50. package/dist/models/settings.d.ts.map +1 -1
  51. package/dist/models/sync.d.ts +18 -18
  52. package/dist/models/sync.d.ts.map +1 -1
  53. package/dist/models/wallet.d.ts +16 -16
  54. package/dist/models/wallet.d.ts.map +1 -1
  55. package/dist/queries/certificate-trust.d.ts +13 -0
  56. package/dist/queries/certificate-trust.d.ts.map +1 -0
  57. package/dist/queries/conversations.d.ts +38 -7
  58. package/dist/queries/conversations.d.ts.map +1 -1
  59. package/dist/queries/discovery.d.ts +10 -10
  60. package/dist/queries/discovery.d.ts.map +1 -1
  61. package/dist/queries/notifications.d.ts +24 -4
  62. package/dist/queries/notifications.d.ts.map +1 -1
  63. package/dist/queries/onboarding-flow.d.ts +25 -0
  64. package/dist/queries/onboarding-flow.d.ts.map +1 -0
  65. package/dist/queries/onboarding-lcd-screen.d.ts +56 -0
  66. package/dist/queries/onboarding-lcd-screen.d.ts.map +1 -0
  67. package/dist/queries/onboarding-status.d.ts +10 -10
  68. package/dist/queries/onboarding-status.d.ts.map +1 -1
  69. package/dist/queries/revisioned.d.ts +47 -13
  70. package/dist/queries/revisioned.d.ts.map +1 -1
  71. package/dist/queries/wallet.d.ts +40 -10
  72. package/dist/queries/wallet.d.ts.map +1 -1
  73. package/dist/services/abort-signal.d.ts +54 -0
  74. package/dist/services/abort-signal.d.ts.map +1 -0
  75. package/dist/services/certificate-trust-service.d.ts +42 -0
  76. package/dist/services/certificate-trust-service.d.ts.map +1 -0
  77. package/dist/services/conversation-service.d.ts +103 -15
  78. package/dist/services/conversation-service.d.ts.map +1 -1
  79. package/dist/services/conversation-sync.d.ts +9 -5
  80. package/dist/services/conversation-sync.d.ts.map +1 -1
  81. package/dist/services/device-boot-state-service.d.ts +17 -0
  82. package/dist/services/device-boot-state-service.d.ts.map +1 -0
  83. package/dist/services/device-cleanup-service.d.ts +47 -0
  84. package/dist/services/device-cleanup-service.d.ts.map +1 -0
  85. package/dist/services/discovery-probe-service.d.ts +37 -46
  86. package/dist/services/discovery-probe-service.d.ts.map +1 -1
  87. package/dist/services/gossip-service.d.ts +28 -13
  88. package/dist/services/gossip-service.d.ts.map +1 -1
  89. package/dist/services/invite-redemption-service.d.ts +43 -0
  90. package/dist/services/invite-redemption-service.d.ts.map +1 -0
  91. package/dist/services/lan-candidate-refresh-service.d.ts +22 -7
  92. package/dist/services/lan-candidate-refresh-service.d.ts.map +1 -1
  93. package/dist/services/lcd-decision-service.d.ts +58 -0
  94. package/dist/services/lcd-decision-service.d.ts.map +1 -0
  95. package/dist/services/onboarding-checkpoint-store.d.ts +54 -0
  96. package/dist/services/onboarding-checkpoint-store.d.ts.map +1 -0
  97. package/dist/services/onboarding-flow-service.d.ts +180 -0
  98. package/dist/services/onboarding-flow-service.d.ts.map +1 -0
  99. package/dist/services/onboarding-status-service.d.ts +21 -38
  100. package/dist/services/onboarding-status-service.d.ts.map +1 -1
  101. package/dist/services/outbox-service.d.ts +105 -29
  102. package/dist/services/outbox-service.d.ts.map +1 -1
  103. package/dist/services/owner-claim-service.d.ts +187 -0
  104. package/dist/services/owner-claim-service.d.ts.map +1 -0
  105. package/dist/services/owner-view-refresh-service.d.ts +40 -0
  106. package/dist/services/owner-view-refresh-service.d.ts.map +1 -0
  107. package/dist/services/owner-view-service.d.ts +49 -4
  108. package/dist/services/owner-view-service.d.ts.map +1 -1
  109. package/dist/services/peer-directory-service.d.ts +53 -17
  110. package/dist/services/peer-directory-service.d.ts.map +1 -1
  111. package/dist/services/release-manifest-verifier.d.ts +18 -20
  112. package/dist/services/release-manifest-verifier.d.ts.map +1 -1
  113. package/dist/services/revisioned-domain-service.d.ts +50 -17
  114. package/dist/services/revisioned-domain-service.d.ts.map +1 -1
  115. package/dist/services/runtime-globals.d.ts +39 -0
  116. package/dist/services/runtime-globals.d.ts.map +1 -0
  117. package/dist/services/session-service.d.ts +23 -3
  118. package/dist/services/session-service.d.ts.map +1 -1
  119. package/dist/services/sync/authoritative-mirror.d.ts +88 -15
  120. package/dist/services/sync/authoritative-mirror.d.ts.map +1 -1
  121. package/dist/services/sync/index.d.ts +17 -5
  122. package/dist/services/sync/index.d.ts.map +1 -1
  123. package/dist/services/sync/log-sync.d.ts +42 -8
  124. package/dist/services/sync/log-sync.d.ts.map +1 -1
  125. package/dist/services/sync/revision-merge.d.ts +80 -13
  126. package/dist/services/sync/revision-merge.d.ts.map +1 -1
  127. package/dist/services/sync/strategy.d.ts +11 -1
  128. package/dist/services/sync/strategy.d.ts.map +1 -1
  129. package/dist/services/transport-manager.d.ts +84 -29
  130. package/dist/services/transport-manager.d.ts.map +1 -1
  131. package/dist/services/wallet-mirror-service.d.ts +81 -18
  132. package/dist/services/wallet-mirror-service.d.ts.map +1 -1
  133. package/dist/services/wifi-provisioning-service.d.ts +146 -0
  134. package/dist/services/wifi-provisioning-service.d.ts.map +1 -0
  135. package/package.json +18 -12
  136. package/src/errors/index.js +126 -0
  137. package/src/events/index.js +124 -0
  138. package/src/index.js +566 -0
  139. package/src/models/agent.js +43 -0
  140. package/src/models/app-data.js +122 -0
  141. package/src/models/conversation.js +36 -0
  142. package/{dist → src}/models/delegation.js +18 -1
  143. package/src/models/device-boot-state.js +3 -0
  144. package/{dist → src}/models/device-identity.js +18 -2
  145. package/{dist → src}/models/graph.js +30 -3
  146. package/src/models/index.js +175 -0
  147. package/src/models/json.js +18 -0
  148. package/src/models/l402.js +9 -0
  149. package/src/models/live-query-definition.js +30 -0
  150. package/src/models/node-app-memory.js +130 -0
  151. package/src/models/outbox-intent.js +12 -0
  152. package/{dist → src}/models/outbox.js +102 -3
  153. package/src/models/owner-views.js +59 -0
  154. package/src/models/payment.js +24 -0
  155. package/src/models/query-diff.js +7 -0
  156. package/src/models/release-manifest.js +160 -0
  157. package/src/models/repository-write.js +10 -0
  158. package/src/models/revisioned-commands.js +92 -0
  159. package/src/models/revisioned.js +41 -0
  160. package/src/models/settings.js +16 -0
  161. package/src/models/sync.js +44 -0
  162. package/src/models/wallet.js +40 -0
  163. package/src/queries/certificate-trust.js +31 -0
  164. package/{dist → src}/queries/conversations.js +54 -3
  165. package/src/queries/discovery.js +34 -0
  166. package/{dist → src}/queries/notifications.js +25 -2
  167. package/src/queries/onboarding-flow.js +48 -0
  168. package/src/queries/onboarding-lcd-screen.js +100 -0
  169. package/src/queries/onboarding-status.js +34 -0
  170. package/src/queries/revisioned.js +128 -0
  171. package/{dist → src}/queries/wallet.js +35 -1
  172. package/src/services/abort-signal.js +69 -0
  173. package/src/services/certificate-trust-service.js +119 -0
  174. package/{dist → src}/services/conversation-service.js +108 -1
  175. package/src/services/conversation-sync.js +28 -0
  176. package/src/services/device-boot-state-service.js +45 -0
  177. package/src/services/device-cleanup-service.js +112 -0
  178. package/src/services/discovery-probe-service.js +196 -0
  179. package/{dist → src}/services/gossip-service.js +54 -4
  180. package/src/services/invite-redemption-service.js +64 -0
  181. package/src/services/lan-candidate-refresh-service.js +164 -0
  182. package/src/services/lcd-decision-service.js +93 -0
  183. package/src/services/onboarding-checkpoint-store.js +155 -0
  184. package/src/services/onboarding-flow-service.js +1096 -0
  185. package/src/services/onboarding-status-service.js +64 -0
  186. package/{dist → src}/services/outbox-service.js +132 -1
  187. package/src/services/owner-claim-service.js +347 -0
  188. package/src/services/owner-view-refresh-service.js +208 -0
  189. package/src/services/owner-view-service.js +87 -0
  190. package/{dist → src}/services/peer-directory-service.js +72 -3
  191. package/src/services/release-manifest-verifier.js +108 -0
  192. package/{dist → src}/services/revisioned-domain-service.js +189 -9
  193. package/src/services/runtime-globals.js +174 -0
  194. package/src/services/session-service.js +50 -0
  195. package/{dist → src}/services/sync/authoritative-mirror.js +90 -1
  196. package/src/services/sync/index.js +67 -0
  197. package/src/services/sync/log-sync.js +60 -0
  198. package/src/services/sync/revision-merge.js +119 -0
  199. package/src/services/sync/strategy.js +44 -0
  200. package/src/services/transport-manager.js +478 -0
  201. package/{dist → src}/services/wallet-mirror-service.js +226 -6
  202. package/src/services/wifi-provisioning-service.js +278 -0
  203. package/dist/errors/index.js +0 -45
  204. package/dist/errors/index.js.map +0 -1
  205. package/dist/events/index.js +0 -17
  206. package/dist/events/index.js.map +0 -1
  207. package/dist/index.js +0 -31
  208. package/dist/index.js.map +0 -1
  209. package/dist/models/agent.js +0 -17
  210. package/dist/models/agent.js.map +0 -1
  211. package/dist/models/conversation.js +0 -2
  212. package/dist/models/conversation.js.map +0 -1
  213. package/dist/models/delegation.js.map +0 -1
  214. package/dist/models/device-identity.js.map +0 -1
  215. package/dist/models/graph.js.map +0 -1
  216. package/dist/models/index.js +0 -8
  217. package/dist/models/index.js.map +0 -1
  218. package/dist/models/json.js +0 -2
  219. package/dist/models/json.js.map +0 -1
  220. package/dist/models/l402.js +0 -2
  221. package/dist/models/l402.js.map +0 -1
  222. package/dist/models/live-query-definition.js +0 -2
  223. package/dist/models/live-query-definition.js.map +0 -1
  224. package/dist/models/outbox-intent.js +0 -2
  225. package/dist/models/outbox-intent.js.map +0 -1
  226. package/dist/models/outbox.js.map +0 -1
  227. package/dist/models/owner-views.js +0 -7
  228. package/dist/models/owner-views.js.map +0 -1
  229. package/dist/models/payment.js +0 -3
  230. package/dist/models/payment.js.map +0 -1
  231. package/dist/models/query-diff.js +0 -2
  232. package/dist/models/query-diff.js.map +0 -1
  233. package/dist/models/release-manifest.js +0 -147
  234. package/dist/models/release-manifest.js.map +0 -1
  235. package/dist/models/repository-write.js +0 -2
  236. package/dist/models/repository-write.js.map +0 -1
  237. package/dist/models/revisioned-commands.js +0 -35
  238. package/dist/models/revisioned-commands.js.map +0 -1
  239. package/dist/models/revisioned.js +0 -2
  240. package/dist/models/revisioned.js.map +0 -1
  241. package/dist/models/settings.js +0 -2
  242. package/dist/models/settings.js.map +0 -1
  243. package/dist/models/sync.js +0 -2
  244. package/dist/models/sync.js.map +0 -1
  245. package/dist/models/wallet.js +0 -2
  246. package/dist/models/wallet.js.map +0 -1
  247. package/dist/queries/conversations.js.map +0 -1
  248. package/dist/queries/discovery.js +0 -27
  249. package/dist/queries/discovery.js.map +0 -1
  250. package/dist/queries/notifications.js.map +0 -1
  251. package/dist/queries/onboarding-status.js +0 -27
  252. package/dist/queries/onboarding-status.js.map +0 -1
  253. package/dist/queries/revisioned.js +0 -58
  254. package/dist/queries/revisioned.js.map +0 -1
  255. package/dist/queries/wallet.js.map +0 -1
  256. package/dist/services/conversation-service.js.map +0 -1
  257. package/dist/services/conversation-sync.js +0 -9
  258. package/dist/services/conversation-sync.js.map +0 -1
  259. package/dist/services/discovery-probe-service.js +0 -129
  260. package/dist/services/discovery-probe-service.js.map +0 -1
  261. package/dist/services/gossip-service.js.map +0 -1
  262. package/dist/services/lan-candidate-refresh-service.js +0 -79
  263. package/dist/services/lan-candidate-refresh-service.js.map +0 -1
  264. package/dist/services/onboarding-status-service.js +0 -32
  265. package/dist/services/onboarding-status-service.js.map +0 -1
  266. package/dist/services/outbox-service.js.map +0 -1
  267. package/dist/services/owner-view-service.js +0 -21
  268. package/dist/services/owner-view-service.js.map +0 -1
  269. package/dist/services/peer-directory-service.js.map +0 -1
  270. package/dist/services/release-manifest-verifier.js +0 -84
  271. package/dist/services/release-manifest-verifier.js.map +0 -1
  272. package/dist/services/revisioned-domain-service.js.map +0 -1
  273. package/dist/services/session-service.js +0 -20
  274. package/dist/services/session-service.js.map +0 -1
  275. package/dist/services/sync/authoritative-mirror.js.map +0 -1
  276. package/dist/services/sync/index.js +0 -4
  277. package/dist/services/sync/index.js.map +0 -1
  278. package/dist/services/sync/log-sync.js +0 -20
  279. package/dist/services/sync/log-sync.js.map +0 -1
  280. package/dist/services/sync/revision-merge.js +0 -46
  281. package/dist/services/sync/revision-merge.js.map +0 -1
  282. package/dist/services/sync/strategy.js +0 -2
  283. package/dist/services/sync/strategy.js.map +0 -1
  284. package/dist/services/transport-manager.js +0 -234
  285. package/dist/services/transport-manager.js.map +0 -1
  286. package/dist/services/wallet-mirror-service.js.map +0 -1
@@ -0,0 +1,478 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {import("@econ-v1/ports").DelayPort} DelayPort */
4
+ /** @typedef {import("@econ-v1/ports").DirectHomeNodeCandidate} DirectHomeNodeCandidate */
5
+ /** @typedef {import("@econ-v1/ports").DirectTransportPath} DirectTransportPath */
6
+ /** @typedef {import("@econ-v1/ports").HomeNodeClient} HomeNodeClient */
7
+ /** @typedef {import("@econ-v1/ports").TransportOfflineReason} TransportOfflineReason */
8
+ /** @typedef {import("@econ-v1/ports").TransportState} TransportState */
9
+ /** @typedef {import("./abort-signal.js").AbortSignalPort} AbortSignalPort */
10
+ /** @typedef {import("./runtime-globals.js").RuntimeUrl} RuntimeUrl */
11
+
12
+ import {
13
+ createRuntimeAbortController,
14
+ parseRuntimeUrl,
15
+ } from "./runtime-globals.js";
16
+
17
+ /** @type {readonly DirectTransportPath[]} */
18
+ /** @type {readonly DirectTransportPath[]} */
19
+ const DIRECT_TIER_ORDER = ["lan-direct", "lan-webtransport", "wan-direct"];
20
+
21
+ /** @typedef {{ readonly candidates: readonly DirectHomeNodeCandidate[], readonly dial: (candidate: DirectHomeNodeCandidate, signal: AbortSignalPort) => Promise<HomeNodeClient>, readonly delay?: DelayPort, readonly priorityGraceMs?: number, readonly candidateTimeoutMs?: number, readonly now?: () => string, readonly onState?: (state: TransportState) => void }} TransportManagerOptions */
22
+ /** @typedef {{ readonly higherPriorityThan?: DirectTransportPath, readonly path?: DirectTransportPath }} TransportConnectOptions */
23
+ /** @typedef {HomeNodeClient & { close?: () => void | Promise<void> }} ClosableHomeNodeClient */
24
+ /** @typedef {{ abort(): void, readonly signal: AbortSignalPort }} AbortControllerPort */
25
+ /** @typedef {{ readonly candidate: DirectHomeNodeCandidate, readonly controller: AbortControllerPort, readonly promise: Promise<HomeNodeClient> }} DialAttempt */
26
+ /** @typedef {{ readonly attempt: DialAttempt, readonly client: HomeNodeClient, readonly success: true }} DialSuccess */
27
+ /** @typedef {{ readonly attempt: DialAttempt, readonly success: false }} DialFailure */
28
+
29
+ export class TransportManager {
30
+ /** @type {readonly DirectHomeNodeCandidate[]} */
31
+ #candidates;
32
+ /** @readonly @type {TransportManagerOptions["dial"]} */
33
+ #dial;
34
+ /** @readonly @type {DelayPort | undefined} */
35
+ #delay;
36
+ /** @readonly @type {number} */
37
+ #candidateTimeoutMs;
38
+ /** @readonly @type {() => string} */
39
+ #now;
40
+ /** @readonly @type {(state: TransportState) => void} */
41
+ #onState;
42
+ /** @type {Promise<HomeNodeClient> | undefined} */
43
+ #run;
44
+ /** @type {AbortControllerPort | undefined} */
45
+ #runAbort;
46
+ /** @type {DirectHomeNodeCandidate | undefined} */
47
+ #selectedCandidate;
48
+ /** @readonly @type {Set<AbortControllerPort>} */
49
+ #controllers = new Set();
50
+ /** @readonly @type {WeakSet<object>} */
51
+ #closedClients = new WeakSet();
52
+ /** @readonly @type {Set<(state: TransportState) => void>} */
53
+ #listeners = new Set();
54
+ /** @type {TransportState} */
55
+ #state = {
56
+ changedAt: "1970-01-01T00:00:00.000Z",
57
+ path: "offline",
58
+ status: "offline",
59
+ };
60
+
61
+ /** @param {TransportManagerOptions} options */
62
+ constructor(options) {
63
+ this.#candidates = normalizeDirectCandidates(options.candidates);
64
+ this.#dial = options.dial;
65
+ this.#delay = options.delay;
66
+ this.#candidateTimeoutMs = Math.max(
67
+ 1,
68
+ options.candidateTimeoutMs ?? 15_000,
69
+ );
70
+ this.#now = options.now ?? (() => new Date().toISOString());
71
+ this.#onState = options.onState ?? (() => undefined);
72
+ }
73
+
74
+ /** @returns {TransportState} */
75
+ get state() {
76
+ return this.#state;
77
+ }
78
+
79
+ /** @returns {DirectHomeNodeCandidate | undefined} */
80
+ get selectedCandidate() {
81
+ return this.#selectedCandidate === undefined
82
+ ? undefined
83
+ : { ...this.#selectedCandidate };
84
+ }
85
+
86
+ /**
87
+ * @param {(state: TransportState) => void} listener
88
+ * @returns {() => void}
89
+ */
90
+ subscribe(listener) {
91
+ this.#listeners.add(listener);
92
+ return () => this.#listeners.delete(listener);
93
+ }
94
+
95
+ /**
96
+ * @param {readonly DirectHomeNodeCandidate[]} candidates
97
+ * @returns {void}
98
+ */
99
+ replaceCandidates(candidates) {
100
+ this.#candidates = normalizeDirectCandidates(candidates);
101
+ }
102
+
103
+ /**
104
+ * @param {TransportConnectOptions} [options]
105
+ * @returns {Promise<HomeNodeClient>}
106
+ */
107
+ connect(options = {}) {
108
+ if (
109
+ options.higherPriorityThan !== undefined &&
110
+ options.path !== undefined
111
+ ) {
112
+ throw new TypeError(
113
+ "Transport selection accepts either a path or a priority bound",
114
+ );
115
+ }
116
+ /** @type {readonly DirectHomeNodeCandidate[]} */
117
+ let candidates;
118
+ if (options.path !== undefined) {
119
+ candidates = this.#candidates.filter(
120
+ (candidate) => candidate.path === options.path,
121
+ );
122
+ } else if (options.higherPriorityThan !== undefined) {
123
+ const higherPriorityThan = options.higherPriorityThan;
124
+ candidates = this.#candidates.filter(
125
+ (candidate) => priority(candidate.path) < priority(higherPriorityThan),
126
+ );
127
+ } else {
128
+ candidates = this.#candidates;
129
+ }
130
+ return this.#shareRun(candidates, options.higherPriorityThan === undefined);
131
+ }
132
+
133
+ /**
134
+ * @param {DirectTransportPath} path
135
+ * @returns {void}
136
+ */
137
+ markConnected(path) {
138
+ this.#setState({ changedAt: this.#now(), path, status: "connected" });
139
+ }
140
+
141
+ /** @param {number} attempt @returns {void} */
142
+ markConnecting(attempt) {
143
+ this.#setState({
144
+ attempt: wholeAttempt(attempt),
145
+ changedAt: this.#now(),
146
+ path: "offline",
147
+ status: "connecting",
148
+ });
149
+ }
150
+
151
+ /** @param {number} attempt @param {string} nextRetryAt @returns {void} */
152
+ markReconnecting(attempt, nextRetryAt) {
153
+ this.#setState({
154
+ attempt: wholeAttempt(attempt),
155
+ changedAt: this.#now(),
156
+ nextRetryAt,
157
+ path: "offline",
158
+ status: "reconnecting",
159
+ });
160
+ }
161
+
162
+ /** @param {TransportOfflineReason} [reason] @returns {void} */
163
+ markOffline(reason) {
164
+ this.#setState({
165
+ changedAt: this.#now(),
166
+ path: "offline",
167
+ status: "offline",
168
+ ...(reason === undefined ? {} : { reason }),
169
+ });
170
+ }
171
+
172
+ /** @returns {void} */
173
+ abort() {
174
+ this.#runAbort?.abort();
175
+ for (const controller of this.#controllers) controller.abort();
176
+ this.markOffline();
177
+ }
178
+
179
+ /**
180
+ * @param {readonly DirectHomeNodeCandidate[]} candidates
181
+ * @param {boolean} publishState
182
+ * @returns {Promise<HomeNodeClient>}
183
+ */
184
+ #shareRun(candidates, publishState) {
185
+ if (this.#run !== undefined) return this.#run;
186
+ const abort = createRuntimeAbortController();
187
+ const connection = this.#connect(candidates, publishState, abort.signal);
188
+ /** @type {Promise<HomeNodeClient>} */
189
+ /** @type {Promise<HomeNodeClient>} */
190
+ const run = connection.finally(() => {
191
+ if (this.#run === run) this.#run = undefined;
192
+ if (this.#runAbort === abort) this.#runAbort = undefined;
193
+ });
194
+ this.#run = run;
195
+ this.#runAbort = abort;
196
+ return run;
197
+ }
198
+
199
+ /**
200
+ * @param {readonly DirectHomeNodeCandidate[]} candidates
201
+ * @param {boolean} publishState
202
+ * @param {AbortSignalPort} signal
203
+ * @returns {Promise<HomeNodeClient>}
204
+ */
205
+ async #connect(candidates, publishState, signal) {
206
+ /** @type {string[]} */
207
+ /** @type {string[]} */
208
+ const failures = [];
209
+ for (const path of DIRECT_TIER_ORDER) {
210
+ throwIfAborted(signal);
211
+ const tier = candidates.filter((candidate) => candidate.path === path);
212
+ if (tier.length === 0) continue;
213
+ const attempts = tier.map((candidate) =>
214
+ this.#startAttempt(candidate, signal),
215
+ );
216
+ /** @type {(DialSuccess | DialFailure)[]} */
217
+ const outcomes = await Promise.all(
218
+ attempts.map(async (attempt) => {
219
+ try {
220
+ return { attempt, client: await attempt.promise, success: true };
221
+ } catch (error) {
222
+ failures.push(`${attempt.candidate.path}:${messageFor(error)}`);
223
+ return { attempt, success: false };
224
+ } finally {
225
+ this.#controllers.delete(attempt.controller);
226
+ }
227
+ }),
228
+ );
229
+ throwIfAborted(signal);
230
+ const successes = outcomes.filter((outcome) => outcome.success);
231
+ const winner = successes[0];
232
+ if (winner === undefined) {
233
+ for (const attempt of attempts) attempt.controller.abort();
234
+ continue;
235
+ }
236
+ await Promise.all(
237
+ successes.slice(1).map(async (outcome) => {
238
+ outcome.attempt.controller.abort();
239
+ await this.#closeClient(outcome.client);
240
+ }),
241
+ );
242
+ this.#selectedCandidate = { ...winner.attempt.candidate };
243
+ if (publishState) this.markConnected(winner.attempt.candidate.path);
244
+ return winner.client;
245
+ }
246
+ if (publishState) this.markOffline();
247
+ throw new Error(
248
+ `No home-node transport available (${failures.join(", ") || "no candidates"})`,
249
+ );
250
+ }
251
+
252
+ /**
253
+ * @param {DirectHomeNodeCandidate} candidate
254
+ * @param {AbortSignalPort} runSignal
255
+ * @returns {DialAttempt}
256
+ */
257
+ #startAttempt(candidate, runSignal) {
258
+ const controller = createRuntimeAbortController();
259
+ this.#controllers.add(controller);
260
+ return {
261
+ candidate,
262
+ controller,
263
+ promise: this.#dialWithTimeout(candidate, controller, runSignal),
264
+ };
265
+ }
266
+
267
+ /**
268
+ * @param {DirectHomeNodeCandidate} candidate
269
+ * @param {AbortControllerPort} controller
270
+ * @param {AbortSignalPort} runSignal
271
+ * @returns {Promise<HomeNodeClient>}
272
+ */
273
+ async #dialWithTimeout(candidate, controller, runSignal) {
274
+ const dial = this.#dial(candidate, controller.signal);
275
+ void dial
276
+ .then(async (client) => {
277
+ if (controller.signal.aborted || runSignal.aborted)
278
+ await this.#closeClient(client);
279
+ })
280
+ .catch(() => undefined);
281
+ const cancellation = Promise.race([
282
+ rejectedOnAbort(controller.signal),
283
+ rejectedOnAbort(runSignal),
284
+ ]);
285
+ if (this.#delay === undefined)
286
+ return await Promise.race([dial, cancellation]);
287
+ const timeout = this.#delay.wait(this.#candidateTimeoutMs).then(() => {
288
+ controller.abort();
289
+ throw new Error("candidate dial timed out");
290
+ });
291
+ return await Promise.race([dial, timeout, cancellation]);
292
+ }
293
+
294
+ /**
295
+ * @param {ClosableHomeNodeClient} client
296
+ * @returns {Promise<void>}
297
+ */
298
+ async #closeClient(client) {
299
+ if (this.#closedClients.has(client)) return;
300
+ this.#closedClients.add(client);
301
+ await closeClient(client);
302
+ }
303
+
304
+ /**
305
+ * @param {TransportState} next
306
+ * @returns {void}
307
+ */
308
+ #setState(next) {
309
+ if (!transportStateChanged(this.#state, next)) return;
310
+ this.#state = next;
311
+ this.#onState(next);
312
+ for (const listener of [...this.#listeners]) listener(next);
313
+ }
314
+ }
315
+
316
+ /** @param {TransportState} current @param {TransportState} next @returns {boolean} */
317
+ export function transportStateChanged(current, next) {
318
+ if (current.status !== next.status || current.path !== next.path) return true;
319
+ if (current.status === "connecting" && next.status === "connecting")
320
+ return current.attempt !== next.attempt;
321
+ if (current.status === "reconnecting" && next.status === "reconnecting")
322
+ return (
323
+ current.attempt !== next.attempt ||
324
+ current.nextRetryAt !== next.nextRetryAt
325
+ );
326
+ if (current.status === "offline" && next.status === "offline")
327
+ return current.reason !== next.reason;
328
+ return false;
329
+ }
330
+
331
+ /** @param {number} attempt @returns {number} */
332
+ function wholeAttempt(attempt) {
333
+ return Number.isFinite(attempt) ? Math.max(1, Math.trunc(attempt)) : 1;
334
+ }
335
+
336
+ /**
337
+ * @param {readonly DirectHomeNodeCandidate[]} candidates
338
+ * @returns {readonly DirectHomeNodeCandidate[]}
339
+ */
340
+ function normalizeDirectCandidates(candidates) {
341
+ /** @type {Record<DirectTransportPath, DirectHomeNodeCandidate[]>} */
342
+ /** @type {Record<DirectTransportPath, DirectHomeNodeCandidate[]>} */
343
+ const tiers = {
344
+ "lan-direct": [],
345
+ "lan-webtransport": [],
346
+ "wan-direct": [],
347
+ };
348
+ const seen = new Set();
349
+ const trustedNodeIds = new Set();
350
+ for (const candidate of candidates) {
351
+ if (
352
+ !isDirectCandidate(candidate) ||
353
+ candidate.address.trim() === "" ||
354
+ candidate.id.trim() === ""
355
+ ) {
356
+ throw new TypeError(
357
+ "Direct transport candidates must have a path, address, and id",
358
+ );
359
+ }
360
+ const address = canonicalHttpCandidateOrigin(candidate.address);
361
+ const identityBinding =
362
+ candidate.path === "lan-webtransport"
363
+ ? undefined
364
+ : candidate.identityBinding;
365
+ if (identityBinding !== "bootstrap") trustedNodeIds.add(candidate.id);
366
+ const key = JSON.stringify([candidate.path, address, candidate.id]);
367
+ if (seen.has(key)) continue;
368
+ seen.add(key);
369
+ tiers[candidate.path].push({ ...candidate, address });
370
+ }
371
+ if (trustedNodeIds.size > 1) {
372
+ throw new TypeError(
373
+ "Direct transport candidates must share one trusted home-node identity",
374
+ );
375
+ }
376
+ return DIRECT_TIER_ORDER.flatMap((path) => tiers[path]);
377
+ }
378
+
379
+ /**
380
+ * @param {string} value
381
+ * @returns {string}
382
+ */
383
+ export function canonicalHttpCandidateOrigin(value) {
384
+ /** @type {RuntimeUrl} */
385
+ let url;
386
+ try {
387
+ url = parseRuntimeUrl(value);
388
+ } catch {
389
+ throw new TypeError(DIRECT_ORIGIN_MESSAGE);
390
+ }
391
+ if (
392
+ !isPotentiallyTrustworthyOrigin(url) ||
393
+ url.username !== "" ||
394
+ url.password !== "" ||
395
+ url.hostname === "" ||
396
+ url.origin !== value
397
+ ) {
398
+ throw new TypeError(DIRECT_ORIGIN_MESSAGE);
399
+ }
400
+ return url.origin;
401
+ }
402
+
403
+ const DIRECT_ORIGIN_MESSAGE =
404
+ "Direct transport candidates must use an exact HTTPS origin (or a loopback HTTP origin)";
405
+
406
+ /** @param {RuntimeUrl} url @returns {boolean} */
407
+ export function isPotentiallyTrustworthyOrigin(url) {
408
+ if (url.protocol === "https:") return true;
409
+ if (url.protocol !== "http:") return false;
410
+ const host = url.hostname.toLowerCase();
411
+ return (
412
+ host === "localhost" ||
413
+ host.endsWith(".localhost") ||
414
+ host === "[::1]" ||
415
+ /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/u.test(host)
416
+ );
417
+ }
418
+
419
+ /**
420
+ * @param {DirectHomeNodeCandidate} candidate
421
+ * @returns {boolean}
422
+ */
423
+ function isDirectCandidate(candidate) {
424
+ return (
425
+ candidate.path === "lan-direct" ||
426
+ candidate.path === "lan-webtransport" ||
427
+ candidate.path === "wan-direct"
428
+ );
429
+ }
430
+
431
+ /**
432
+ * @param {DirectTransportPath} path
433
+ * @returns {number}
434
+ */
435
+ function priority(path) {
436
+ return DIRECT_TIER_ORDER.indexOf(path);
437
+ }
438
+
439
+ /**
440
+ * @param {unknown} error
441
+ * @returns {string}
442
+ */
443
+ function messageFor(error) {
444
+ return error instanceof Error ? error.message : "unavailable";
445
+ }
446
+
447
+ /**
448
+ * @param {ClosableHomeNodeClient} client
449
+ * @returns {Promise<void>}
450
+ */
451
+ async function closeClient(client) {
452
+ if (client.close === undefined) return;
453
+ await Promise.resolve(client.close()).catch(() => undefined);
454
+ }
455
+
456
+ /**
457
+ * @param {AbortSignalPort} signal
458
+ * @returns {Promise<never>}
459
+ */
460
+ function rejectedOnAbort(signal) {
461
+ if (signal.aborted)
462
+ return Promise.reject(new Error("transport selection aborted"));
463
+ return new Promise((_resolve, reject) => {
464
+ signal.addEventListener(
465
+ "abort",
466
+ () => reject(new Error("transport selection aborted")),
467
+ { once: true },
468
+ );
469
+ });
470
+ }
471
+
472
+ /**
473
+ * @param {AbortSignalPort} signal
474
+ * @returns {void}
475
+ */
476
+ function throwIfAborted(signal) {
477
+ if (signal.aborted) throw new Error("transport selection aborted");
478
+ }