@hashtree/worker 0.1.26 → 0.2.1

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 (258) hide show
  1. package/README.md +16 -16
  2. package/package.json +23 -19
  3. package/src/app-runtime.ts +393 -0
  4. package/src/capabilities/blossomBandwidthTracker.ts +74 -0
  5. package/src/capabilities/blossomTransport.ts +179 -0
  6. package/src/capabilities/connectivity.ts +54 -0
  7. package/src/capabilities/idbStorage.ts +94 -0
  8. package/src/capabilities/meshRouterStore.ts +426 -0
  9. package/src/capabilities/rootResolver.ts +497 -0
  10. package/src/client-id.ts +137 -0
  11. package/src/client.ts +501 -0
  12. package/{dist/entry.js → src/entry.ts} +1 -1
  13. package/src/htree-path.ts +53 -0
  14. package/src/htree-url.ts +156 -0
  15. package/src/index.ts +76 -0
  16. package/src/mediaStreaming.ts +64 -0
  17. package/src/p2p/boundedQueue.ts +168 -0
  18. package/src/p2p/errorMessage.ts +6 -0
  19. package/src/p2p/index.ts +48 -0
  20. package/src/p2p/lruCache.ts +78 -0
  21. package/src/p2p/meshQueryRouter.ts +361 -0
  22. package/src/p2p/protocol.ts +11 -0
  23. package/src/p2p/queryForwardingMachine.ts +197 -0
  24. package/src/p2p/signaling.ts +284 -0
  25. package/src/p2p/uploadRateLimiter.ts +85 -0
  26. package/src/p2p/webrtcController.ts +1168 -0
  27. package/src/p2p/webrtcProxy.ts +519 -0
  28. package/src/privacyGuards.ts +31 -0
  29. package/src/protocol.ts +124 -0
  30. package/src/relay/identity.ts +86 -0
  31. package/src/relay/mediaHandler.ts +1633 -0
  32. package/src/relay/ndk.ts +590 -0
  33. package/{dist/iris/nostr-wasm.js → src/relay/nostr-wasm.ts} +4 -1
  34. package/src/relay/nostr.ts +249 -0
  35. package/src/relay/protocol.ts +361 -0
  36. package/src/relay/publicAssetUrl.ts +25 -0
  37. package/src/relay/rootPathResolver.ts +50 -0
  38. package/src/relay/shims.d.ts +17 -0
  39. package/src/relay/signing.ts +332 -0
  40. package/src/relay/treeRootCache.ts +354 -0
  41. package/src/relay/treeRootSubscription.ts +577 -0
  42. package/src/relay/utils/constants.ts +139 -0
  43. package/src/relay/utils/errorMessage.ts +7 -0
  44. package/src/relay/utils/lruCache.ts +79 -0
  45. package/src/relay/webrtc.ts +5 -0
  46. package/src/relay/webrtcSignaling.ts +108 -0
  47. package/src/relay/worker.ts +1787 -0
  48. package/src/relay-client.ts +265 -0
  49. package/src/relay-entry.ts +1 -0
  50. package/src/runtime-network.ts +134 -0
  51. package/src/runtime.ts +153 -0
  52. package/src/transferableBytes.ts +5 -0
  53. package/src/tree-root.ts +851 -0
  54. package/src/types.ts +8 -0
  55. package/src/worker.ts +975 -0
  56. package/LICENSE +0 -21
  57. package/dist/app-runtime.d.ts +0 -60
  58. package/dist/app-runtime.d.ts.map +0 -1
  59. package/dist/app-runtime.js +0 -271
  60. package/dist/app-runtime.js.map +0 -1
  61. package/dist/capabilities/blossomBandwidthTracker.d.ts +0 -26
  62. package/dist/capabilities/blossomBandwidthTracker.d.ts.map +0 -1
  63. package/dist/capabilities/blossomBandwidthTracker.js +0 -53
  64. package/dist/capabilities/blossomBandwidthTracker.js.map +0 -1
  65. package/dist/capabilities/blossomTransport.d.ts +0 -22
  66. package/dist/capabilities/blossomTransport.d.ts.map +0 -1
  67. package/dist/capabilities/blossomTransport.js +0 -144
  68. package/dist/capabilities/blossomTransport.js.map +0 -1
  69. package/dist/capabilities/connectivity.d.ts +0 -3
  70. package/dist/capabilities/connectivity.d.ts.map +0 -1
  71. package/dist/capabilities/connectivity.js +0 -49
  72. package/dist/capabilities/connectivity.js.map +0 -1
  73. package/dist/capabilities/idbStorage.d.ts +0 -25
  74. package/dist/capabilities/idbStorage.d.ts.map +0 -1
  75. package/dist/capabilities/idbStorage.js +0 -73
  76. package/dist/capabilities/idbStorage.js.map +0 -1
  77. package/dist/capabilities/meshRouterStore.d.ts +0 -71
  78. package/dist/capabilities/meshRouterStore.d.ts.map +0 -1
  79. package/dist/capabilities/meshRouterStore.js +0 -316
  80. package/dist/capabilities/meshRouterStore.js.map +0 -1
  81. package/dist/capabilities/rootResolver.d.ts +0 -10
  82. package/dist/capabilities/rootResolver.d.ts.map +0 -1
  83. package/dist/capabilities/rootResolver.js +0 -393
  84. package/dist/capabilities/rootResolver.js.map +0 -1
  85. package/dist/client-id.d.ts +0 -18
  86. package/dist/client-id.d.ts.map +0 -1
  87. package/dist/client-id.js +0 -98
  88. package/dist/client-id.js.map +0 -1
  89. package/dist/client.d.ts +0 -61
  90. package/dist/client.d.ts.map +0 -1
  91. package/dist/client.js +0 -417
  92. package/dist/client.js.map +0 -1
  93. package/dist/entry.d.ts +0 -2
  94. package/dist/entry.d.ts.map +0 -1
  95. package/dist/entry.js.map +0 -1
  96. package/dist/htree-path.d.ts +0 -13
  97. package/dist/htree-path.d.ts.map +0 -1
  98. package/dist/htree-path.js +0 -38
  99. package/dist/htree-path.js.map +0 -1
  100. package/dist/htree-url.d.ts +0 -22
  101. package/dist/htree-url.d.ts.map +0 -1
  102. package/dist/htree-url.js +0 -118
  103. package/dist/htree-url.js.map +0 -1
  104. package/dist/index.d.ts +0 -17
  105. package/dist/index.d.ts.map +0 -1
  106. package/dist/index.js +0 -8
  107. package/dist/index.js.map +0 -1
  108. package/dist/iris/identity.d.ts +0 -36
  109. package/dist/iris/identity.d.ts.map +0 -1
  110. package/dist/iris/identity.js +0 -78
  111. package/dist/iris/identity.js.map +0 -1
  112. package/dist/iris/mediaHandler.d.ts +0 -64
  113. package/dist/iris/mediaHandler.d.ts.map +0 -1
  114. package/dist/iris/mediaHandler.js +0 -1285
  115. package/dist/iris/mediaHandler.js.map +0 -1
  116. package/dist/iris/ndk.d.ts +0 -96
  117. package/dist/iris/ndk.d.ts.map +0 -1
  118. package/dist/iris/ndk.js +0 -502
  119. package/dist/iris/ndk.js.map +0 -1
  120. package/dist/iris/nostr-wasm.d.ts +0 -14
  121. package/dist/iris/nostr-wasm.d.ts.map +0 -1
  122. package/dist/iris/nostr-wasm.js.map +0 -1
  123. package/dist/iris/nostr.d.ts +0 -60
  124. package/dist/iris/nostr.d.ts.map +0 -1
  125. package/dist/iris/nostr.js +0 -207
  126. package/dist/iris/nostr.js.map +0 -1
  127. package/dist/iris/protocol.d.ts +0 -583
  128. package/dist/iris/protocol.d.ts.map +0 -1
  129. package/dist/iris/protocol.js +0 -16
  130. package/dist/iris/protocol.js.map +0 -1
  131. package/dist/iris/publicAssetUrl.d.ts +0 -6
  132. package/dist/iris/publicAssetUrl.d.ts.map +0 -1
  133. package/dist/iris/publicAssetUrl.js +0 -14
  134. package/dist/iris/publicAssetUrl.js.map +0 -1
  135. package/dist/iris/rootPathResolver.d.ts +0 -9
  136. package/dist/iris/rootPathResolver.d.ts.map +0 -1
  137. package/dist/iris/rootPathResolver.js +0 -32
  138. package/dist/iris/rootPathResolver.js.map +0 -1
  139. package/dist/iris/signing.d.ts +0 -50
  140. package/dist/iris/signing.d.ts.map +0 -1
  141. package/dist/iris/signing.js +0 -299
  142. package/dist/iris/signing.js.map +0 -1
  143. package/dist/iris/treeRootCache.d.ts +0 -86
  144. package/dist/iris/treeRootCache.d.ts.map +0 -1
  145. package/dist/iris/treeRootCache.js +0 -269
  146. package/dist/iris/treeRootCache.js.map +0 -1
  147. package/dist/iris/treeRootSubscription.d.ts +0 -55
  148. package/dist/iris/treeRootSubscription.d.ts.map +0 -1
  149. package/dist/iris/treeRootSubscription.js +0 -479
  150. package/dist/iris/treeRootSubscription.js.map +0 -1
  151. package/dist/iris/utils/constants.d.ts +0 -76
  152. package/dist/iris/utils/constants.d.ts.map +0 -1
  153. package/dist/iris/utils/constants.js +0 -113
  154. package/dist/iris/utils/constants.js.map +0 -1
  155. package/dist/iris/utils/errorMessage.d.ts +0 -5
  156. package/dist/iris/utils/errorMessage.d.ts.map +0 -1
  157. package/dist/iris/utils/errorMessage.js +0 -8
  158. package/dist/iris/utils/errorMessage.js.map +0 -1
  159. package/dist/iris/utils/lruCache.d.ts +0 -26
  160. package/dist/iris/utils/lruCache.d.ts.map +0 -1
  161. package/dist/iris/utils/lruCache.js +0 -66
  162. package/dist/iris/utils/lruCache.js.map +0 -1
  163. package/dist/iris/webrtc.d.ts +0 -2
  164. package/dist/iris/webrtc.d.ts.map +0 -1
  165. package/dist/iris/webrtc.js +0 -3
  166. package/dist/iris/webrtc.js.map +0 -1
  167. package/dist/iris/webrtcSignaling.d.ts +0 -37
  168. package/dist/iris/webrtcSignaling.d.ts.map +0 -1
  169. package/dist/iris/webrtcSignaling.js +0 -86
  170. package/dist/iris/webrtcSignaling.js.map +0 -1
  171. package/dist/iris/worker.d.ts +0 -12
  172. package/dist/iris/worker.d.ts.map +0 -1
  173. package/dist/iris/worker.js +0 -1529
  174. package/dist/iris/worker.js.map +0 -1
  175. package/dist/iris-client.d.ts +0 -31
  176. package/dist/iris-client.d.ts.map +0 -1
  177. package/dist/iris-client.js +0 -197
  178. package/dist/iris-client.js.map +0 -1
  179. package/dist/iris-entry.d.ts +0 -2
  180. package/dist/iris-entry.d.ts.map +0 -1
  181. package/dist/iris-entry.js +0 -2
  182. package/dist/iris-entry.js.map +0 -1
  183. package/dist/mediaStreaming.d.ts +0 -7
  184. package/dist/mediaStreaming.d.ts.map +0 -1
  185. package/dist/mediaStreaming.js +0 -48
  186. package/dist/mediaStreaming.js.map +0 -1
  187. package/dist/p2p/boundedQueue.d.ts +0 -79
  188. package/dist/p2p/boundedQueue.d.ts.map +0 -1
  189. package/dist/p2p/boundedQueue.js +0 -134
  190. package/dist/p2p/boundedQueue.js.map +0 -1
  191. package/dist/p2p/errorMessage.d.ts +0 -5
  192. package/dist/p2p/errorMessage.d.ts.map +0 -1
  193. package/dist/p2p/errorMessage.js +0 -7
  194. package/dist/p2p/errorMessage.js.map +0 -1
  195. package/dist/p2p/index.d.ts +0 -8
  196. package/dist/p2p/index.d.ts.map +0 -1
  197. package/dist/p2p/index.js +0 -6
  198. package/dist/p2p/index.js.map +0 -1
  199. package/dist/p2p/lruCache.d.ts +0 -26
  200. package/dist/p2p/lruCache.d.ts.map +0 -1
  201. package/dist/p2p/lruCache.js +0 -65
  202. package/dist/p2p/lruCache.js.map +0 -1
  203. package/dist/p2p/meshQueryRouter.d.ts +0 -44
  204. package/dist/p2p/meshQueryRouter.d.ts.map +0 -1
  205. package/dist/p2p/meshQueryRouter.js +0 -228
  206. package/dist/p2p/meshQueryRouter.js.map +0 -1
  207. package/dist/p2p/protocol.d.ts +0 -10
  208. package/dist/p2p/protocol.d.ts.map +0 -1
  209. package/dist/p2p/protocol.js +0 -2
  210. package/dist/p2p/protocol.js.map +0 -1
  211. package/dist/p2p/queryForwardingMachine.d.ts +0 -46
  212. package/dist/p2p/queryForwardingMachine.d.ts.map +0 -1
  213. package/dist/p2p/queryForwardingMachine.js +0 -144
  214. package/dist/p2p/queryForwardingMachine.js.map +0 -1
  215. package/dist/p2p/signaling.d.ts +0 -63
  216. package/dist/p2p/signaling.d.ts.map +0 -1
  217. package/dist/p2p/signaling.js +0 -185
  218. package/dist/p2p/signaling.js.map +0 -1
  219. package/dist/p2p/uploadRateLimiter.d.ts +0 -21
  220. package/dist/p2p/uploadRateLimiter.d.ts.map +0 -1
  221. package/dist/p2p/uploadRateLimiter.js +0 -62
  222. package/dist/p2p/uploadRateLimiter.js.map +0 -1
  223. package/dist/p2p/webrtcController.d.ts +0 -168
  224. package/dist/p2p/webrtcController.d.ts.map +0 -1
  225. package/dist/p2p/webrtcController.js +0 -902
  226. package/dist/p2p/webrtcController.js.map +0 -1
  227. package/dist/p2p/webrtcProxy.d.ts +0 -62
  228. package/dist/p2p/webrtcProxy.d.ts.map +0 -1
  229. package/dist/p2p/webrtcProxy.js +0 -447
  230. package/dist/p2p/webrtcProxy.js.map +0 -1
  231. package/dist/privacyGuards.d.ts +0 -14
  232. package/dist/privacyGuards.d.ts.map +0 -1
  233. package/dist/privacyGuards.js +0 -27
  234. package/dist/privacyGuards.js.map +0 -1
  235. package/dist/protocol.d.ts +0 -225
  236. package/dist/protocol.d.ts.map +0 -1
  237. package/dist/protocol.js +0 -2
  238. package/dist/protocol.js.map +0 -1
  239. package/dist/runtime-network.d.ts +0 -23
  240. package/dist/runtime-network.d.ts.map +0 -1
  241. package/dist/runtime-network.js +0 -105
  242. package/dist/runtime-network.js.map +0 -1
  243. package/dist/runtime.d.ts +0 -23
  244. package/dist/runtime.d.ts.map +0 -1
  245. package/dist/runtime.js +0 -122
  246. package/dist/runtime.js.map +0 -1
  247. package/dist/tree-root.d.ts +0 -201
  248. package/dist/tree-root.d.ts.map +0 -1
  249. package/dist/tree-root.js +0 -632
  250. package/dist/tree-root.js.map +0 -1
  251. package/dist/types.d.ts +0 -2
  252. package/dist/types.d.ts.map +0 -1
  253. package/dist/types.js +0 -2
  254. package/dist/types.js.map +0 -1
  255. package/dist/worker.d.ts +0 -9
  256. package/dist/worker.d.ts.map +0 -1
  257. package/dist/worker.js +0 -797
  258. package/dist/worker.js.map +0 -1
@@ -0,0 +1,265 @@
1
+ import type { WorkerFactory } from './client.js';
2
+ import type {
3
+ TreeRootInfo,
4
+ WorkerConfig as RelayWorkerConfig,
5
+ WorkerRequest as RelayWorkerRequest,
6
+ WorkerResponse as RelayWorkerResponse,
7
+ } from './relay/protocol.js';
8
+
9
+ const REQUEST_TIMEOUT_MS = 30_000;
10
+
11
+ type PendingRequest = {
12
+ resolve: (message: RelayWorkerResponse) => void;
13
+ reject: (error: Error) => void;
14
+ timeoutId: ReturnType<typeof setTimeout>;
15
+ };
16
+
17
+ type RelayWorkerRequestPayload = RelayWorkerRequest extends infer T
18
+ ? T extends { id: string }
19
+ ? Omit<T, 'id'>
20
+ : never
21
+ : never;
22
+
23
+ export interface TreeRootUpdate extends TreeRootInfo {
24
+ npub: string;
25
+ treeName: string;
26
+ }
27
+
28
+ export type {
29
+ TreeRootInfo,
30
+ RelayWorkerConfig,
31
+ RelayWorkerRequest,
32
+ RelayWorkerResponse,
33
+ };
34
+
35
+ export class RelayWorkerClient {
36
+ private readonly workerFactory: WorkerFactory;
37
+ private readonly config: RelayWorkerConfig;
38
+ private worker: Worker | null = null;
39
+ private initPromise: Promise<void> | null = null;
40
+ private initPending:
41
+ | {
42
+ resolve: () => void;
43
+ reject: (error: Error) => void;
44
+ timeoutId: ReturnType<typeof setTimeout>;
45
+ }
46
+ | null = null;
47
+ private pendingRequests = new Map<string, PendingRequest>();
48
+ private treeRootListeners = new Set<(update: TreeRootUpdate) => void>();
49
+
50
+ constructor(workerFactory: WorkerFactory, config: RelayWorkerConfig) {
51
+ this.workerFactory = workerFactory;
52
+ this.config = config;
53
+ }
54
+
55
+ async init(): Promise<void> {
56
+ if (this.initPromise) return this.initPromise;
57
+
58
+ try {
59
+ this.spawnWorker();
60
+ } catch (err) {
61
+ throw err instanceof Error ? err : new Error(String(err));
62
+ }
63
+
64
+ this.initPromise = new Promise<void>((resolve, reject) => {
65
+ if (!this.worker) {
66
+ reject(new Error('Failed to create worker'));
67
+ return;
68
+ }
69
+
70
+ const timeoutId = setTimeout(() => {
71
+ this.initPending = null;
72
+ this.initPromise = null;
73
+ reject(new Error('Worker init timed out'));
74
+ }, REQUEST_TIMEOUT_MS);
75
+
76
+ this.initPending = {
77
+ resolve,
78
+ reject,
79
+ timeoutId,
80
+ };
81
+
82
+ this.worker.postMessage({
83
+ type: 'init',
84
+ id: this.nextRequestId('worker_init'),
85
+ config: this.config,
86
+ } as RelayWorkerRequest);
87
+ });
88
+
89
+ return this.initPromise;
90
+ }
91
+
92
+ private spawnWorker(): void {
93
+ if (this.workerFactory instanceof URL) {
94
+ this.worker = new Worker(this.workerFactory, { type: 'module' });
95
+ } else if (typeof this.workerFactory === 'string') {
96
+ this.worker = new Worker(this.workerFactory, { type: 'module' });
97
+ } else {
98
+ this.worker = new this.workerFactory();
99
+ }
100
+
101
+ this.worker.onmessage = (event: MessageEvent<RelayWorkerResponse>) => {
102
+ const message = event.data;
103
+
104
+ if (message.type === 'ready') {
105
+ if (this.initPending) {
106
+ clearTimeout(this.initPending.timeoutId);
107
+ this.initPending.resolve();
108
+ this.initPending = null;
109
+ }
110
+ return;
111
+ }
112
+
113
+ if (message.type === 'treeRootUpdate') {
114
+ for (const listener of this.treeRootListeners) {
115
+ const { type: _type, ...update } = message;
116
+ listener(update);
117
+ }
118
+ return;
119
+ }
120
+
121
+ if (message.type === 'error' && message.id) {
122
+ const errorMessage = typeof message.error === 'string' ? message.error : 'Worker error';
123
+ this.rejectPending(message.id, new Error(errorMessage));
124
+ return;
125
+ }
126
+
127
+ if ('id' in message && typeof message.id === 'string') {
128
+ this.resolvePending(message.id, message);
129
+ }
130
+ };
131
+
132
+ this.worker.onerror = (event) => {
133
+ const errorMessage = event instanceof ErrorEvent ? event.message : 'Worker error';
134
+ this.rejectAllPending(new Error(errorMessage));
135
+ };
136
+ }
137
+
138
+ private nextRequestId(prefix: string): string {
139
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
140
+ return `${prefix}_${crypto.randomUUID()}`;
141
+ }
142
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
143
+ }
144
+
145
+ private resolvePending(id: string, message: RelayWorkerResponse): void {
146
+ const pending = this.pendingRequests.get(id);
147
+ if (!pending) return;
148
+ clearTimeout(pending.timeoutId);
149
+ pending.resolve(message);
150
+ this.pendingRequests.delete(id);
151
+ }
152
+
153
+ private rejectPending(id: string, error: Error): void {
154
+ const pending = this.pendingRequests.get(id);
155
+ if (!pending) return;
156
+ clearTimeout(pending.timeoutId);
157
+ pending.reject(error);
158
+ this.pendingRequests.delete(id);
159
+ }
160
+
161
+ private rejectAllPending(error: Error): void {
162
+ for (const [id, pending] of this.pendingRequests.entries()) {
163
+ clearTimeout(pending.timeoutId);
164
+ pending.reject(error);
165
+ this.pendingRequests.delete(id);
166
+ }
167
+
168
+ if (this.initPending) {
169
+ clearTimeout(this.initPending.timeoutId);
170
+ this.initPending.reject(error);
171
+ this.initPending = null;
172
+ }
173
+
174
+ this.initPromise = null;
175
+ }
176
+
177
+ private async request(
178
+ payload: RelayWorkerRequestPayload,
179
+ timeoutMs = REQUEST_TIMEOUT_MS,
180
+ transfer: Transferable[] = [],
181
+ ): Promise<RelayWorkerResponse> {
182
+ await this.init();
183
+ if (!this.worker) {
184
+ throw new Error('Worker not initialized');
185
+ }
186
+
187
+ const id = this.nextRequestId(payload.type);
188
+ const message = { ...payload, id } as RelayWorkerRequest;
189
+
190
+ return new Promise<RelayWorkerResponse>((resolve, reject) => {
191
+ const timeoutId = setTimeout(() => {
192
+ this.pendingRequests.delete(id);
193
+ reject(new Error(`Worker request timed out: ${payload.type}`));
194
+ }, timeoutMs);
195
+
196
+ this.pendingRequests.set(id, { resolve, reject, timeoutId });
197
+ this.worker?.postMessage(message, transfer);
198
+ });
199
+ }
200
+
201
+ async registerMediaPort(port: MessagePort, debug?: boolean): Promise<void> {
202
+ await this.init();
203
+ if (!this.worker) {
204
+ throw new Error('Worker not initialized');
205
+ }
206
+
207
+ this.worker.postMessage({ type: 'registerMediaPort', port, debug } as RelayWorkerRequest, [port]);
208
+ }
209
+
210
+ async getTreeRootInfo(npub: string, treeName: string): Promise<TreeRootInfo | null> {
211
+ const res = await this.request({ type: 'getTreeRootInfo', npub, treeName });
212
+ if (res.type !== 'treeRootInfo') {
213
+ throw new Error('Unexpected tree root response');
214
+ }
215
+ if (res.error) {
216
+ throw new Error(res.error);
217
+ }
218
+ return res.record ?? null;
219
+ }
220
+
221
+ async subscribeTreeRoots(pubkey: string): Promise<void> {
222
+ const res = await this.request({ type: 'subscribeTreeRoots', pubkey });
223
+ if (res.type !== 'void') {
224
+ throw new Error('Unexpected tree root subscribe response');
225
+ }
226
+ if (res.error) {
227
+ throw new Error(res.error);
228
+ }
229
+ }
230
+
231
+ async unsubscribeTreeRoots(pubkey: string): Promise<void> {
232
+ const res = await this.request({ type: 'unsubscribeTreeRoots', pubkey });
233
+ if (res.type !== 'void') {
234
+ throw new Error('Unexpected tree root unsubscribe response');
235
+ }
236
+ if (res.error) {
237
+ throw new Error(res.error);
238
+ }
239
+ }
240
+
241
+ onTreeRootUpdate(listener: (update: TreeRootUpdate) => void): () => void {
242
+ this.treeRootListeners.add(listener);
243
+ return () => {
244
+ this.treeRootListeners.delete(listener);
245
+ };
246
+ }
247
+
248
+ async close(): Promise<void> {
249
+ try {
250
+ const res = await this.request({ type: 'close' });
251
+ if (res.type !== 'void' && res.type !== 'error') {
252
+ throw new Error('Unexpected response for close');
253
+ }
254
+ } catch {
255
+ // Ignore close errors and always terminate locally.
256
+ }
257
+
258
+ this.treeRootListeners.clear();
259
+ this.worker?.terminate();
260
+ this.worker = null;
261
+ this.initPromise = null;
262
+ this.initPending = null;
263
+ this.rejectAllPending(new Error('Worker closed'));
264
+ }
265
+ }
@@ -0,0 +1 @@
1
+ import './relay/worker.js';
@@ -0,0 +1,134 @@
1
+ import type { BlossomServerConfig } from './protocol.js';
2
+ import { getInjectedHtreeServerUrl, type HtreeRuntimeWindowLike } from './runtime.js';
3
+
4
+ export interface RuntimeNetworkOptions {
5
+ windowLike?: HtreeRuntimeWindowLike;
6
+ }
7
+
8
+ export interface ResolveRuntimeEndpointsOptions extends RuntimeNetworkOptions {
9
+ relays?: readonly string[];
10
+ blossomServers?: readonly BlossomServerConfig[];
11
+ }
12
+
13
+ export interface RuntimeEndpoints {
14
+ htreeServerUrl: string | null;
15
+ nostrRelays: string[];
16
+ blossomServers: BlossomServerConfig[];
17
+ }
18
+
19
+ export function normalizeRuntimeServerUrl(url: string): string {
20
+ return url.trim().replace(/\/+$/, '');
21
+ }
22
+
23
+ export function normalizeRuntimeRelayUrl(url: string): string {
24
+ return url.trim().replace(/\/+$/, '');
25
+ }
26
+
27
+ function normalizeBlossomServer(server: BlossomServerConfig): BlossomServerConfig | null {
28
+ const url = normalizeRuntimeServerUrl(server.url);
29
+ if (!url) return null;
30
+ return {
31
+ url,
32
+ read: server.read ?? true,
33
+ write: server.write ?? false,
34
+ };
35
+ }
36
+
37
+ function uniqueRelayUrls(relays: readonly string[]): string[] {
38
+ const seen = new Set<string>();
39
+ const normalized: string[] = [];
40
+ for (const relay of relays) {
41
+ const url = normalizeRuntimeRelayUrl(relay);
42
+ if (!url || seen.has(url)) continue;
43
+ seen.add(url);
44
+ normalized.push(url);
45
+ }
46
+ return normalized;
47
+ }
48
+
49
+ export function getRuntimeHtreeServerUrl(windowLike?: HtreeRuntimeWindowLike): string | null {
50
+ const serverUrl = getInjectedHtreeServerUrl(windowLike);
51
+ if (!serverUrl) return null;
52
+ return normalizeRuntimeServerUrl(serverUrl);
53
+ }
54
+
55
+ export function getRuntimeNostrRelayUrl(windowLike?: HtreeRuntimeWindowLike): string | null {
56
+ const serverUrl = getRuntimeHtreeServerUrl(windowLike);
57
+ if (!serverUrl) return null;
58
+
59
+ try {
60
+ const url = new URL(serverUrl);
61
+ if (url.protocol === 'http:') {
62
+ url.protocol = 'ws:';
63
+ } else if (url.protocol === 'https:') {
64
+ url.protocol = 'wss:';
65
+ } else {
66
+ return null;
67
+ }
68
+ url.pathname = '/ws';
69
+ url.search = '';
70
+ url.hash = '';
71
+ return normalizeRuntimeRelayUrl(url.toString());
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ export function getRuntimeBlossomServer(windowLike?: HtreeRuntimeWindowLike): BlossomServerConfig | null {
78
+ const serverUrl = getRuntimeHtreeServerUrl(windowLike);
79
+ if (!serverUrl) return null;
80
+ return {
81
+ url: serverUrl,
82
+ read: true,
83
+ write: true,
84
+ };
85
+ }
86
+
87
+ export function getRuntimeNostrRelays(
88
+ relays: readonly string[],
89
+ options: RuntimeNetworkOptions = {},
90
+ ): string[] {
91
+ const runtimeRelayUrl = getRuntimeNostrRelayUrl(options.windowLike);
92
+ if (runtimeRelayUrl) {
93
+ return [runtimeRelayUrl];
94
+ }
95
+ return uniqueRelayUrls(relays);
96
+ }
97
+
98
+ export function getRuntimeBlossomServers(
99
+ servers: readonly BlossomServerConfig[],
100
+ options: RuntimeNetworkOptions = {},
101
+ ): BlossomServerConfig[] {
102
+ const merged = new Map<string, BlossomServerConfig>();
103
+ const runtimeServer = getRuntimeBlossomServer(options.windowLike);
104
+ const candidates = runtimeServer ? [runtimeServer, ...servers] : [...servers];
105
+
106
+ for (const candidate of candidates) {
107
+ const normalized = normalizeBlossomServer(candidate);
108
+ if (!normalized) continue;
109
+
110
+ const existing = merged.get(normalized.url);
111
+ if (existing) {
112
+ merged.set(normalized.url, {
113
+ url: normalized.url,
114
+ read: existing.read || normalized.read,
115
+ write: existing.write || normalized.write,
116
+ });
117
+ continue;
118
+ }
119
+
120
+ merged.set(normalized.url, normalized);
121
+ }
122
+
123
+ return Array.from(merged.values());
124
+ }
125
+
126
+ export function resolveRuntimeEndpoints(
127
+ options: ResolveRuntimeEndpointsOptions = {},
128
+ ): RuntimeEndpoints {
129
+ return {
130
+ htreeServerUrl: getRuntimeHtreeServerUrl(options.windowLike),
131
+ nostrRelays: getRuntimeNostrRelays(options.relays ?? [], options),
132
+ blossomServers: getRuntimeBlossomServers(options.blossomServers ?? [], options),
133
+ };
134
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,153 @@
1
+ export interface HtreeRuntimeLocationLike {
2
+ protocol?: string;
3
+ hostname?: string;
4
+ search?: string;
5
+ }
6
+
7
+ export interface HtreeRuntimeWindowLike {
8
+ location?: HtreeRuntimeLocationLike;
9
+ __HTREE_SERVER_URL__?: string;
10
+ __HTREE_CANONICAL_URL__?: string | null;
11
+ htree?: {
12
+ htreeBaseUrl?: string;
13
+ };
14
+ }
15
+
16
+ export interface ResolveRuntimeHtreeBaseUrlOptions {
17
+ windowLike?: HtreeRuntimeWindowLike;
18
+ fallbackBaseUrl?: string | null;
19
+ }
20
+
21
+ function getWindowLike(windowLike?: HtreeRuntimeWindowLike): HtreeRuntimeWindowLike | null {
22
+ if (windowLike) return windowLike;
23
+ if (typeof window === 'undefined') return null;
24
+ return window as HtreeRuntimeWindowLike;
25
+ }
26
+
27
+ function normalizeBaseUrl(url: string | null | undefined): string {
28
+ return (url ?? '').trim().replace(/\/$/, '');
29
+ }
30
+
31
+ function getQueryParam(name: string, windowLike?: HtreeRuntimeWindowLike): string | null {
32
+ const runtimeWindow = getWindowLike(windowLike);
33
+ if (!runtimeWindow) return null;
34
+ try {
35
+ const value = new URLSearchParams(runtimeWindow.location?.search ?? '').get(name);
36
+ return typeof value === 'string' ? value.trim() || null : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function getPageProtocol(windowLike?: HtreeRuntimeWindowLike): string | null {
43
+ const runtimeWindow = getWindowLike(windowLike);
44
+ const protocol = runtimeWindow?.location?.protocol;
45
+ return typeof protocol === 'string' ? protocol.toLowerCase() : null;
46
+ }
47
+
48
+ function getPageHostname(windowLike?: HtreeRuntimeWindowLike): string | null {
49
+ const runtimeWindow = getWindowLike(windowLike);
50
+ const hostname = runtimeWindow?.location?.hostname;
51
+ return typeof hostname === 'string' ? hostname.toLowerCase() : null;
52
+ }
53
+
54
+ function hasCanonicalHtreeIdentity(windowLike?: HtreeRuntimeWindowLike): boolean {
55
+ const runtimeWindow = getWindowLike(windowLike);
56
+ const injectedCanonical = runtimeWindow?.__HTREE_CANONICAL_URL__;
57
+ const canonical = typeof injectedCanonical === 'string' && injectedCanonical.trim()
58
+ ? injectedCanonical.trim()
59
+ : getQueryParam('htree_canonical', windowLike);
60
+ return typeof canonical === 'string' && canonical.toLowerCase().startsWith('htree://');
61
+ }
62
+
63
+ function isLoopbackChildRuntime(windowLike?: HtreeRuntimeWindowLike): boolean {
64
+ if (getPageProtocol(windowLike) !== 'http:') return false;
65
+ const hostname = getPageHostname(windowLike);
66
+ if (!hostname) return false;
67
+ return hostname === '127.0.0.1'
68
+ || hostname === 'localhost'
69
+ || hostname.endsWith('.htree.localhost');
70
+ }
71
+
72
+ function getServerProtocol(serverUrl: string): string | null {
73
+ try {
74
+ return new URL(serverUrl).protocol.toLowerCase();
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function isLocalHttpServerUrl(serverUrl: string): boolean {
81
+ try {
82
+ const parsed = new URL(serverUrl);
83
+ const hostname = parsed.hostname.toLowerCase();
84
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:')
85
+ && (hostname === '127.0.0.1' || hostname === 'localhost' || hostname.endsWith('.htree.localhost'));
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ function getWindowHtreeBaseUrl(windowLike?: HtreeRuntimeWindowLike): string {
92
+ const runtimeWindow = getWindowLike(windowLike);
93
+ return normalizeBaseUrl(runtimeWindow?.htree?.htreeBaseUrl);
94
+ }
95
+
96
+ export function getInjectedHtreeServerUrl(windowLike?: HtreeRuntimeWindowLike): string | null {
97
+ const runtimeWindow = getWindowLike(windowLike);
98
+ if (!runtimeWindow) return null;
99
+ const override = runtimeWindow.__HTREE_SERVER_URL__;
100
+ const fallback = getQueryParam('htree_server', runtimeWindow);
101
+ const candidate = typeof override === 'string' && override.trim() ? override : fallback;
102
+ const normalized = normalizeBaseUrl(candidate);
103
+ return normalized || null;
104
+ }
105
+
106
+ export function shouldEagerLoadMediaInNativeChildRuntime(windowLike?: HtreeRuntimeWindowLike): boolean {
107
+ return isLoopbackChildRuntime(windowLike) && hasCanonicalHtreeIdentity(windowLike);
108
+ }
109
+
110
+ export function shouldPreferSameOriginHtreeRoutes(windowLike?: HtreeRuntimeWindowLike): boolean {
111
+ const serverUrl = getInjectedHtreeServerUrl(windowLike);
112
+ if (!serverUrl) return false;
113
+ const serverProtocol = getServerProtocol(serverUrl);
114
+ if (serverProtocol !== 'http:') return false;
115
+
116
+ const pageProtocol = getPageProtocol(windowLike);
117
+ if (pageProtocol === 'https:') return true;
118
+ if (pageProtocol === 'htree:') {
119
+ const hostname = getPageHostname(windowLike);
120
+ return hostname?.startsWith('npub1') === true || hostname === 'self';
121
+ }
122
+ if (hasCanonicalHtreeIdentity(windowLike) && !isLoopbackChildRuntime(windowLike)) return true;
123
+ return false;
124
+ }
125
+
126
+ export function canUseInjectedHtreeServerUrl(windowLike?: HtreeRuntimeWindowLike): boolean {
127
+ const serverUrl = getInjectedHtreeServerUrl(windowLike);
128
+ return !!serverUrl && !shouldPreferSameOriginHtreeRoutes(windowLike);
129
+ }
130
+
131
+ export function canUseSameOriginHtreeProtocolStreaming(windowLike?: HtreeRuntimeWindowLike): boolean {
132
+ return getPageProtocol(windowLike) === 'htree:';
133
+ }
134
+
135
+ export function resolveRuntimeHtreeBaseUrl(
136
+ options: ResolveRuntimeHtreeBaseUrlOptions = {},
137
+ ): string {
138
+ const { windowLike, fallbackBaseUrl } = options;
139
+ const injectedServerUrl = getInjectedHtreeServerUrl(windowLike);
140
+
141
+ if (injectedServerUrl && canUseInjectedHtreeServerUrl(windowLike)) {
142
+ return injectedServerUrl;
143
+ }
144
+
145
+ const windowBaseUrl = getWindowHtreeBaseUrl(windowLike);
146
+ if (windowBaseUrl) {
147
+ if (!isLocalHttpServerUrl(windowBaseUrl) || canUseInjectedHtreeServerUrl(windowLike)) {
148
+ return windowBaseUrl;
149
+ }
150
+ }
151
+
152
+ return normalizeBaseUrl(fallbackBaseUrl);
153
+ }
@@ -0,0 +1,5 @@
1
+ // Worker replies may reuse bytes from caches or stores. Copy them before
2
+ // transfer so postMessage ownership changes do not detach the source buffer.
3
+ export function cloneTransferableBytes(bytes: Uint8Array): Uint8Array {
4
+ return bytes.slice();
5
+ }