@hashtree/worker 0.2.0 → 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 (259) hide show
  1. package/package.json +7 -3
  2. package/src/app-runtime.ts +393 -0
  3. package/src/capabilities/blossomBandwidthTracker.ts +74 -0
  4. package/src/capabilities/blossomTransport.ts +179 -0
  5. package/src/capabilities/connectivity.ts +54 -0
  6. package/src/capabilities/idbStorage.ts +94 -0
  7. package/src/capabilities/meshRouterStore.ts +426 -0
  8. package/src/capabilities/rootResolver.ts +497 -0
  9. package/src/client-id.ts +137 -0
  10. package/src/client.ts +501 -0
  11. package/{dist/entry.js → src/entry.ts} +1 -1
  12. package/src/htree-path.ts +53 -0
  13. package/src/htree-url.ts +156 -0
  14. package/src/index.ts +76 -0
  15. package/src/mediaStreaming.ts +64 -0
  16. package/src/p2p/boundedQueue.ts +168 -0
  17. package/src/p2p/errorMessage.ts +6 -0
  18. package/src/p2p/index.ts +48 -0
  19. package/src/p2p/lruCache.ts +78 -0
  20. package/src/p2p/meshQueryRouter.ts +361 -0
  21. package/src/p2p/protocol.ts +11 -0
  22. package/src/p2p/queryForwardingMachine.ts +197 -0
  23. package/src/p2p/signaling.ts +284 -0
  24. package/src/p2p/uploadRateLimiter.ts +85 -0
  25. package/src/p2p/webrtcController.ts +1168 -0
  26. package/src/p2p/webrtcProxy.ts +519 -0
  27. package/src/privacyGuards.ts +31 -0
  28. package/src/protocol.ts +124 -0
  29. package/src/relay/identity.ts +86 -0
  30. package/src/relay/mediaHandler.ts +1633 -0
  31. package/src/relay/ndk.ts +590 -0
  32. package/{dist/relay/nostr-wasm.js → src/relay/nostr-wasm.ts} +4 -1
  33. package/src/relay/nostr.ts +249 -0
  34. package/src/relay/protocol.ts +361 -0
  35. package/src/relay/publicAssetUrl.ts +25 -0
  36. package/src/relay/rootPathResolver.ts +50 -0
  37. package/src/relay/shims.d.ts +17 -0
  38. package/src/relay/signing.ts +332 -0
  39. package/src/relay/treeRootCache.ts +354 -0
  40. package/src/relay/treeRootSubscription.ts +577 -0
  41. package/src/relay/utils/constants.ts +139 -0
  42. package/src/relay/utils/errorMessage.ts +7 -0
  43. package/src/relay/utils/lruCache.ts +79 -0
  44. package/src/relay/webrtc.ts +5 -0
  45. package/src/relay/webrtcSignaling.ts +108 -0
  46. package/src/relay/worker.ts +1787 -0
  47. package/src/relay-client.ts +265 -0
  48. package/src/relay-entry.ts +1 -0
  49. package/src/runtime-network.ts +134 -0
  50. package/src/runtime.ts +153 -0
  51. package/{dist/transferableBytes.js → src/transferableBytes.ts} +2 -3
  52. package/src/tree-root.ts +851 -0
  53. package/src/types.ts +8 -0
  54. package/src/worker.ts +975 -0
  55. package/dist/app-runtime.d.ts +0 -60
  56. package/dist/app-runtime.d.ts.map +0 -1
  57. package/dist/app-runtime.js +0 -271
  58. package/dist/app-runtime.js.map +0 -1
  59. package/dist/capabilities/blossomBandwidthTracker.d.ts +0 -26
  60. package/dist/capabilities/blossomBandwidthTracker.d.ts.map +0 -1
  61. package/dist/capabilities/blossomBandwidthTracker.js +0 -53
  62. package/dist/capabilities/blossomBandwidthTracker.js.map +0 -1
  63. package/dist/capabilities/blossomTransport.d.ts +0 -22
  64. package/dist/capabilities/blossomTransport.d.ts.map +0 -1
  65. package/dist/capabilities/blossomTransport.js +0 -139
  66. package/dist/capabilities/blossomTransport.js.map +0 -1
  67. package/dist/capabilities/connectivity.d.ts +0 -3
  68. package/dist/capabilities/connectivity.d.ts.map +0 -1
  69. package/dist/capabilities/connectivity.js +0 -49
  70. package/dist/capabilities/connectivity.js.map +0 -1
  71. package/dist/capabilities/idbStorage.d.ts +0 -25
  72. package/dist/capabilities/idbStorage.d.ts.map +0 -1
  73. package/dist/capabilities/idbStorage.js +0 -73
  74. package/dist/capabilities/idbStorage.js.map +0 -1
  75. package/dist/capabilities/meshRouterStore.d.ts +0 -71
  76. package/dist/capabilities/meshRouterStore.d.ts.map +0 -1
  77. package/dist/capabilities/meshRouterStore.js +0 -316
  78. package/dist/capabilities/meshRouterStore.js.map +0 -1
  79. package/dist/capabilities/rootResolver.d.ts +0 -10
  80. package/dist/capabilities/rootResolver.d.ts.map +0 -1
  81. package/dist/capabilities/rootResolver.js +0 -392
  82. package/dist/capabilities/rootResolver.js.map +0 -1
  83. package/dist/client-id.d.ts +0 -18
  84. package/dist/client-id.d.ts.map +0 -1
  85. package/dist/client-id.js +0 -98
  86. package/dist/client-id.js.map +0 -1
  87. package/dist/client.d.ts +0 -61
  88. package/dist/client.d.ts.map +0 -1
  89. package/dist/client.js +0 -417
  90. package/dist/client.js.map +0 -1
  91. package/dist/entry.d.ts +0 -2
  92. package/dist/entry.d.ts.map +0 -1
  93. package/dist/entry.js.map +0 -1
  94. package/dist/htree-path.d.ts +0 -13
  95. package/dist/htree-path.d.ts.map +0 -1
  96. package/dist/htree-path.js +0 -38
  97. package/dist/htree-path.js.map +0 -1
  98. package/dist/htree-url.d.ts +0 -22
  99. package/dist/htree-url.d.ts.map +0 -1
  100. package/dist/htree-url.js +0 -118
  101. package/dist/htree-url.js.map +0 -1
  102. package/dist/index.d.ts +0 -17
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js +0 -8
  105. package/dist/index.js.map +0 -1
  106. package/dist/mediaStreaming.d.ts +0 -7
  107. package/dist/mediaStreaming.d.ts.map +0 -1
  108. package/dist/mediaStreaming.js +0 -48
  109. package/dist/mediaStreaming.js.map +0 -1
  110. package/dist/p2p/boundedQueue.d.ts +0 -79
  111. package/dist/p2p/boundedQueue.d.ts.map +0 -1
  112. package/dist/p2p/boundedQueue.js +0 -134
  113. package/dist/p2p/boundedQueue.js.map +0 -1
  114. package/dist/p2p/errorMessage.d.ts +0 -5
  115. package/dist/p2p/errorMessage.d.ts.map +0 -1
  116. package/dist/p2p/errorMessage.js +0 -7
  117. package/dist/p2p/errorMessage.js.map +0 -1
  118. package/dist/p2p/index.d.ts +0 -8
  119. package/dist/p2p/index.d.ts.map +0 -1
  120. package/dist/p2p/index.js +0 -6
  121. package/dist/p2p/index.js.map +0 -1
  122. package/dist/p2p/lruCache.d.ts +0 -26
  123. package/dist/p2p/lruCache.d.ts.map +0 -1
  124. package/dist/p2p/lruCache.js +0 -65
  125. package/dist/p2p/lruCache.js.map +0 -1
  126. package/dist/p2p/meshQueryRouter.d.ts +0 -57
  127. package/dist/p2p/meshQueryRouter.d.ts.map +0 -1
  128. package/dist/p2p/meshQueryRouter.js +0 -264
  129. package/dist/p2p/meshQueryRouter.js.map +0 -1
  130. package/dist/p2p/protocol.d.ts +0 -10
  131. package/dist/p2p/protocol.d.ts.map +0 -1
  132. package/dist/p2p/protocol.js +0 -2
  133. package/dist/p2p/protocol.js.map +0 -1
  134. package/dist/p2p/queryForwardingMachine.d.ts +0 -46
  135. package/dist/p2p/queryForwardingMachine.d.ts.map +0 -1
  136. package/dist/p2p/queryForwardingMachine.js +0 -144
  137. package/dist/p2p/queryForwardingMachine.js.map +0 -1
  138. package/dist/p2p/signaling.d.ts +0 -63
  139. package/dist/p2p/signaling.d.ts.map +0 -1
  140. package/dist/p2p/signaling.js +0 -185
  141. package/dist/p2p/signaling.js.map +0 -1
  142. package/dist/p2p/uploadRateLimiter.d.ts +0 -21
  143. package/dist/p2p/uploadRateLimiter.d.ts.map +0 -1
  144. package/dist/p2p/uploadRateLimiter.js +0 -62
  145. package/dist/p2p/uploadRateLimiter.js.map +0 -1
  146. package/dist/p2p/webrtcController.d.ts +0 -176
  147. package/dist/p2p/webrtcController.d.ts.map +0 -1
  148. package/dist/p2p/webrtcController.js +0 -938
  149. package/dist/p2p/webrtcController.js.map +0 -1
  150. package/dist/p2p/webrtcProxy.d.ts +0 -62
  151. package/dist/p2p/webrtcProxy.d.ts.map +0 -1
  152. package/dist/p2p/webrtcProxy.js +0 -447
  153. package/dist/p2p/webrtcProxy.js.map +0 -1
  154. package/dist/privacyGuards.d.ts +0 -14
  155. package/dist/privacyGuards.d.ts.map +0 -1
  156. package/dist/privacyGuards.js +0 -27
  157. package/dist/privacyGuards.js.map +0 -1
  158. package/dist/protocol.d.ts +0 -225
  159. package/dist/protocol.d.ts.map +0 -1
  160. package/dist/protocol.js +0 -2
  161. package/dist/protocol.js.map +0 -1
  162. package/dist/relay/identity.d.ts +0 -36
  163. package/dist/relay/identity.d.ts.map +0 -1
  164. package/dist/relay/identity.js +0 -78
  165. package/dist/relay/identity.js.map +0 -1
  166. package/dist/relay/mediaHandler.d.ts +0 -64
  167. package/dist/relay/mediaHandler.d.ts.map +0 -1
  168. package/dist/relay/mediaHandler.js +0 -1285
  169. package/dist/relay/mediaHandler.js.map +0 -1
  170. package/dist/relay/ndk.d.ts +0 -96
  171. package/dist/relay/ndk.d.ts.map +0 -1
  172. package/dist/relay/ndk.js +0 -502
  173. package/dist/relay/ndk.js.map +0 -1
  174. package/dist/relay/nostr-wasm.d.ts +0 -14
  175. package/dist/relay/nostr-wasm.d.ts.map +0 -1
  176. package/dist/relay/nostr-wasm.js.map +0 -1
  177. package/dist/relay/nostr.d.ts +0 -60
  178. package/dist/relay/nostr.d.ts.map +0 -1
  179. package/dist/relay/nostr.js +0 -207
  180. package/dist/relay/nostr.js.map +0 -1
  181. package/dist/relay/protocol.d.ts +0 -592
  182. package/dist/relay/protocol.d.ts.map +0 -1
  183. package/dist/relay/protocol.js +0 -16
  184. package/dist/relay/protocol.js.map +0 -1
  185. package/dist/relay/publicAssetUrl.d.ts +0 -6
  186. package/dist/relay/publicAssetUrl.d.ts.map +0 -1
  187. package/dist/relay/publicAssetUrl.js +0 -14
  188. package/dist/relay/publicAssetUrl.js.map +0 -1
  189. package/dist/relay/rootPathResolver.d.ts +0 -9
  190. package/dist/relay/rootPathResolver.d.ts.map +0 -1
  191. package/dist/relay/rootPathResolver.js +0 -32
  192. package/dist/relay/rootPathResolver.js.map +0 -1
  193. package/dist/relay/signing.d.ts +0 -50
  194. package/dist/relay/signing.d.ts.map +0 -1
  195. package/dist/relay/signing.js +0 -299
  196. package/dist/relay/signing.js.map +0 -1
  197. package/dist/relay/treeRootCache.d.ts +0 -86
  198. package/dist/relay/treeRootCache.d.ts.map +0 -1
  199. package/dist/relay/treeRootCache.js +0 -269
  200. package/dist/relay/treeRootCache.js.map +0 -1
  201. package/dist/relay/treeRootSubscription.d.ts +0 -55
  202. package/dist/relay/treeRootSubscription.d.ts.map +0 -1
  203. package/dist/relay/treeRootSubscription.js +0 -478
  204. package/dist/relay/treeRootSubscription.js.map +0 -1
  205. package/dist/relay/utils/constants.d.ts +0 -76
  206. package/dist/relay/utils/constants.d.ts.map +0 -1
  207. package/dist/relay/utils/constants.js +0 -113
  208. package/dist/relay/utils/constants.js.map +0 -1
  209. package/dist/relay/utils/errorMessage.d.ts +0 -5
  210. package/dist/relay/utils/errorMessage.d.ts.map +0 -1
  211. package/dist/relay/utils/errorMessage.js +0 -8
  212. package/dist/relay/utils/errorMessage.js.map +0 -1
  213. package/dist/relay/utils/lruCache.d.ts +0 -26
  214. package/dist/relay/utils/lruCache.d.ts.map +0 -1
  215. package/dist/relay/utils/lruCache.js +0 -66
  216. package/dist/relay/utils/lruCache.js.map +0 -1
  217. package/dist/relay/webrtc.d.ts +0 -2
  218. package/dist/relay/webrtc.d.ts.map +0 -1
  219. package/dist/relay/webrtc.js +0 -3
  220. package/dist/relay/webrtc.js.map +0 -1
  221. package/dist/relay/webrtcSignaling.d.ts +0 -37
  222. package/dist/relay/webrtcSignaling.d.ts.map +0 -1
  223. package/dist/relay/webrtcSignaling.js +0 -86
  224. package/dist/relay/webrtcSignaling.js.map +0 -1
  225. package/dist/relay/worker.d.ts +0 -12
  226. package/dist/relay/worker.d.ts.map +0 -1
  227. package/dist/relay/worker.js +0 -1540
  228. package/dist/relay/worker.js.map +0 -1
  229. package/dist/relay-client.d.ts +0 -31
  230. package/dist/relay-client.d.ts.map +0 -1
  231. package/dist/relay-client.js +0 -197
  232. package/dist/relay-client.js.map +0 -1
  233. package/dist/relay-entry.d.ts +0 -2
  234. package/dist/relay-entry.d.ts.map +0 -1
  235. package/dist/relay-entry.js +0 -2
  236. package/dist/relay-entry.js.map +0 -1
  237. package/dist/runtime-network.d.ts +0 -23
  238. package/dist/runtime-network.d.ts.map +0 -1
  239. package/dist/runtime-network.js +0 -105
  240. package/dist/runtime-network.js.map +0 -1
  241. package/dist/runtime.d.ts +0 -24
  242. package/dist/runtime.d.ts.map +0 -1
  243. package/dist/runtime.js +0 -126
  244. package/dist/runtime.js.map +0 -1
  245. package/dist/transferableBytes.d.ts +0 -2
  246. package/dist/transferableBytes.d.ts.map +0 -1
  247. package/dist/transferableBytes.js.map +0 -1
  248. package/dist/tree-root.d.ts +0 -201
  249. package/dist/tree-root.d.ts.map +0 -1
  250. package/dist/tree-root.js +0 -632
  251. package/dist/tree-root.js.map +0 -1
  252. package/dist/types.d.ts +0 -2
  253. package/dist/types.d.ts.map +0 -1
  254. package/dist/types.js +0 -2
  255. package/dist/types.js.map +0 -1
  256. package/dist/worker.d.ts +0 -9
  257. package/dist/worker.d.ts.map +0 -1
  258. package/dist/worker.js +0 -792
  259. package/dist/worker.js.map +0 -1
@@ -0,0 +1,284 @@
1
+ import type { SignalingMessage } from '@hashtree/nostr';
2
+
3
+ type DirectedSignalingMessage = Exclude<SignalingMessage, { type: 'hello' }>;
4
+
5
+ export const SIGNALING_KIND = 25050;
6
+ export const HELLO_TAG = 'hello';
7
+ export const MAX_EVENT_AGE_SEC = 30;
8
+ const HELLO_EXPIRATION_SEC = 5 * 60;
9
+
10
+ export interface SignalingEventLike {
11
+ pubkey: string;
12
+ created_at?: number;
13
+ tags: string[][];
14
+ content: string;
15
+ }
16
+
17
+ export interface GiftSeal {
18
+ pubkey: string;
19
+ kind: number;
20
+ content: string;
21
+ tags: string[][];
22
+ }
23
+
24
+ export interface SignalingTemplate {
25
+ kind: number;
26
+ created_at: number;
27
+ tags: string[][];
28
+ content: string;
29
+ }
30
+
31
+ export interface SignalingInnerEvent {
32
+ kind: number;
33
+ content: string;
34
+ tags: string[][];
35
+ }
36
+
37
+ export interface SignalingFilters {
38
+ since: number;
39
+ helloFilter: {
40
+ kinds: number[];
41
+ '#l': string[];
42
+ since: number;
43
+ };
44
+ directedFilter: {
45
+ kinds: number[];
46
+ '#p': string[];
47
+ since: number;
48
+ };
49
+ }
50
+
51
+ interface SendSignalingMessageOptions<TEvent extends SignalingEventLike> {
52
+ msg: SignalingMessage;
53
+ recipientPubkey?: string;
54
+ signEvent: (template: SignalingTemplate) => Promise<TEvent>;
55
+ giftWrap: (innerEvent: SignalingInnerEvent, recipientPubkey: string) => Promise<TEvent>;
56
+ publish: (event: TEvent) => Promise<void>;
57
+ nowMs?: () => number;
58
+ }
59
+
60
+ interface DecodeSignalingEventOptions<TEvent extends SignalingEventLike> {
61
+ event: TEvent;
62
+ giftUnwrap: (event: TEvent) => Promise<GiftSeal | null>;
63
+ nowMs?: () => number;
64
+ maxEventAgeSec?: number;
65
+ }
66
+
67
+ export interface DecodedSignalingEvent {
68
+ senderPubkey: string;
69
+ message: SignalingMessage;
70
+ }
71
+
72
+ function getSince(nowMs: number, maxEventAgeSec: number): number {
73
+ return Math.floor((nowMs - maxEventAgeSec * 1000) / 1000);
74
+ }
75
+
76
+ function isExpired(event: SignalingEventLike, nowSec: number, maxEventAgeSec: number): boolean {
77
+ const createdAt = event.created_at ?? 0;
78
+ if (nowSec - createdAt > maxEventAgeSec) {
79
+ return true;
80
+ }
81
+
82
+ const expirationTag = event.tags.find((tag) => tag[0] === 'expiration');
83
+ if (expirationTag?.[1]) {
84
+ const expiration = Number.parseInt(expirationTag[1], 10);
85
+ if (Number.isFinite(expiration) && expiration < nowSec) {
86
+ return true;
87
+ }
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ function normalizePeerEndpoint(value: string, senderPubkey: string): string {
94
+ const trimmed = value.trim();
95
+ if (!trimmed) return senderPubkey;
96
+ return trimmed.includes(':') ? senderPubkey : trimmed;
97
+ }
98
+
99
+ function normalizeSignalingMessage(raw: unknown, senderPubkey: string): SignalingMessage | null {
100
+ if (!raw || typeof raw !== 'object' || !('type' in raw)) return null;
101
+ const msg = raw as Record<string, unknown>;
102
+ if (typeof msg.type !== 'string') return null;
103
+
104
+ if (typeof msg.peerId === 'string' && msg.type === 'hello') {
105
+ return {
106
+ ...(msg as unknown as Extract<SignalingMessage, { type: 'hello' }>),
107
+ peerId: senderPubkey,
108
+ };
109
+ }
110
+
111
+ if (
112
+ 'targetPeerId' in msg &&
113
+ typeof msg.targetPeerId === 'string' &&
114
+ typeof msg.peerId === 'string'
115
+ ) {
116
+ return {
117
+ ...(msg as unknown as DirectedSignalingMessage),
118
+ peerId: senderPubkey,
119
+ targetPeerId: normalizePeerEndpoint(msg.targetPeerId, senderPubkey),
120
+ };
121
+ }
122
+
123
+ if (!('recipient' in msg) || typeof msg.recipient !== 'string' || typeof msg.peerId !== 'string') {
124
+ return null;
125
+ }
126
+
127
+ const senderPeerId = senderPubkey;
128
+ const targetPeerId = normalizePeerEndpoint(msg.recipient, senderPubkey);
129
+
130
+ switch (msg.type) {
131
+ case 'offer': {
132
+ const offer = msg.offer as { sdp?: string } | string | undefined;
133
+ const sdp = typeof offer === 'string' ? offer : offer?.sdp;
134
+ return sdp ? { type: 'offer', peerId: senderPeerId, targetPeerId, sdp } : null;
135
+ }
136
+ case 'answer': {
137
+ const answer = msg.answer as { sdp?: string } | string | undefined;
138
+ const sdp = typeof answer === 'string' ? answer : answer?.sdp;
139
+ return sdp ? { type: 'answer', peerId: senderPeerId, targetPeerId, sdp } : null;
140
+ }
141
+ case 'candidate': {
142
+ const candidateObj = msg.candidate as { candidate?: string; sdpMLineIndex?: number; sdpMid?: string } | string | undefined;
143
+ const candidate = typeof candidateObj === 'string' ? candidateObj : candidateObj?.candidate;
144
+ return candidate
145
+ ? {
146
+ type: 'candidate',
147
+ peerId: senderPeerId,
148
+ targetPeerId,
149
+ candidate,
150
+ sdpMLineIndex: typeof candidateObj === 'object' ? candidateObj?.sdpMLineIndex : undefined,
151
+ sdpMid: typeof candidateObj === 'object' ? candidateObj?.sdpMid : undefined,
152
+ }
153
+ : null;
154
+ }
155
+ case 'candidates': {
156
+ const candidates = Array.isArray(msg.candidates)
157
+ ? msg.candidates
158
+ .map((entry) => {
159
+ if (typeof entry === 'string') {
160
+ return { candidate: entry };
161
+ }
162
+ if (entry && typeof entry === 'object') {
163
+ const candidateEntry = entry as { candidate?: string; sdpMLineIndex?: number; sdpMid?: string };
164
+ if (typeof candidateEntry.candidate === 'string') {
165
+ return {
166
+ candidate: candidateEntry.candidate,
167
+ sdpMLineIndex: candidateEntry.sdpMLineIndex,
168
+ sdpMid: candidateEntry.sdpMid,
169
+ };
170
+ }
171
+ }
172
+ return null;
173
+ })
174
+ .filter((entry): entry is { candidate: string; sdpMLineIndex?: number; sdpMid?: string } => !!entry)
175
+ : [];
176
+
177
+ return { type: 'candidates', peerId: senderPeerId, targetPeerId, candidates };
178
+ }
179
+ default:
180
+ return null;
181
+ }
182
+ }
183
+
184
+ export function createSignalingFilters(
185
+ myPubkey: string,
186
+ nowMs = Date.now(),
187
+ maxEventAgeSec = MAX_EVENT_AGE_SEC
188
+ ): SignalingFilters {
189
+ const since = getSince(nowMs, maxEventAgeSec);
190
+ return {
191
+ since,
192
+ helloFilter: {
193
+ kinds: [SIGNALING_KIND],
194
+ '#l': [HELLO_TAG],
195
+ since,
196
+ },
197
+ directedFilter: {
198
+ kinds: [SIGNALING_KIND],
199
+ '#p': [myPubkey],
200
+ since,
201
+ },
202
+ };
203
+ }
204
+
205
+ export async function sendSignalingMessage<TEvent extends SignalingEventLike>({
206
+ msg,
207
+ recipientPubkey,
208
+ signEvent,
209
+ giftWrap,
210
+ publish,
211
+ nowMs = () => Date.now(),
212
+ }: SendSignalingMessageOptions<TEvent>): Promise<void> {
213
+ if (recipientPubkey) {
214
+ const wrappedEvent = await giftWrap(
215
+ {
216
+ kind: SIGNALING_KIND,
217
+ content: JSON.stringify(msg),
218
+ tags: [],
219
+ },
220
+ recipientPubkey
221
+ );
222
+ await publish(wrappedEvent);
223
+ return;
224
+ }
225
+
226
+ const createdAt = Math.floor(nowMs() / 1000);
227
+ const event = await signEvent({
228
+ kind: SIGNALING_KIND,
229
+ created_at: createdAt,
230
+ tags: [
231
+ ['l', HELLO_TAG],
232
+ ['peerId', msg.peerId],
233
+ ['expiration', String(createdAt + HELLO_EXPIRATION_SEC)],
234
+ ],
235
+ content: '',
236
+ });
237
+ await publish(event);
238
+ }
239
+
240
+ export async function decodeSignalingEvent<TEvent extends SignalingEventLike>({
241
+ event,
242
+ giftUnwrap,
243
+ nowMs = () => Date.now(),
244
+ maxEventAgeSec = MAX_EVENT_AGE_SEC,
245
+ }: DecodeSignalingEventOptions<TEvent>): Promise<DecodedSignalingEvent | null> {
246
+ const nowSec = nowMs() / 1000;
247
+ if (isExpired(event, nowSec, maxEventAgeSec)) {
248
+ return null;
249
+ }
250
+
251
+ const isHello = event.tags.some((tag) => tag[0] === 'l' && tag[1] === HELLO_TAG);
252
+ if (isHello) {
253
+ const peerIdTag = event.tags.find((tag) => tag[0] === 'peerId');
254
+ if (!peerIdTag?.[1]) return null;
255
+ const senderPeerId = normalizePeerEndpoint(event.pubkey, event.pubkey);
256
+ if (normalizePeerEndpoint(peerIdTag[1], event.pubkey) !== senderPeerId) {
257
+ return null;
258
+ }
259
+ return {
260
+ senderPubkey: event.pubkey,
261
+ message: {
262
+ type: 'hello',
263
+ peerId: senderPeerId,
264
+ },
265
+ };
266
+ }
267
+
268
+ const seal = await giftUnwrap(event);
269
+ if (!seal?.content) {
270
+ return null;
271
+ }
272
+
273
+ try {
274
+ const raw = JSON.parse(seal.content);
275
+ const message = normalizeSignalingMessage(raw, seal.pubkey);
276
+ if (!message) return null;
277
+ return {
278
+ senderPubkey: seal.pubkey,
279
+ message,
280
+ };
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
@@ -0,0 +1,85 @@
1
+ type UploadRateLimiterConfig = {
2
+ bytesPerSecond?: number | null;
3
+ now?: () => number;
4
+ };
5
+
6
+ type UploadReservation = {
7
+ allowed: boolean;
8
+ delayMs: number;
9
+ };
10
+
11
+ function normalizeBytesPerSecond(value?: number | null): number | null {
12
+ if (!Number.isFinite(value) || !value || value <= 0) {
13
+ return null;
14
+ }
15
+ return Math.floor(value);
16
+ }
17
+
18
+ export class UploadRateLimiter {
19
+ private bytesPerSecond: number | null;
20
+ private availableBytes: number;
21
+ private lastRefillMs: number;
22
+ private readonly now: () => number;
23
+
24
+ constructor(config: UploadRateLimiterConfig = {}) {
25
+ this.now = config.now ?? (() => performance.now());
26
+ this.bytesPerSecond = normalizeBytesPerSecond(config.bytesPerSecond);
27
+ this.availableBytes = this.bytesPerSecond ?? Number.POSITIVE_INFINITY;
28
+ this.lastRefillMs = this.now();
29
+ }
30
+
31
+ setBytesPerSecond(bytesPerSecond?: number | null): void {
32
+ const nowMs = this.now();
33
+ this.refill(nowMs);
34
+ this.bytesPerSecond = normalizeBytesPerSecond(bytesPerSecond);
35
+ this.availableBytes = this.bytesPerSecond
36
+ ? Math.min(this.availableBytes, this.bytesPerSecond)
37
+ : Number.POSITIVE_INFINITY;
38
+ this.lastRefillMs = nowMs;
39
+ }
40
+
41
+ getBytesPerSecond(): number | null {
42
+ return this.bytesPerSecond;
43
+ }
44
+
45
+ reserve(byteLength: number): UploadReservation {
46
+ if (byteLength <= 0) {
47
+ return { allowed: true, delayMs: 0 };
48
+ }
49
+
50
+ const limit = this.bytesPerSecond;
51
+ if (!limit) {
52
+ return { allowed: true, delayMs: 0 };
53
+ }
54
+
55
+ const nowMs = this.now();
56
+ this.refill(nowMs);
57
+
58
+ if (this.availableBytes >= byteLength) {
59
+ this.availableBytes = Math.max(0, this.availableBytes - byteLength);
60
+ return { allowed: true, delayMs: 0 };
61
+ }
62
+
63
+ const missingBytes = byteLength - this.availableBytes;
64
+ return {
65
+ allowed: false,
66
+ delayMs: Math.max(4, Math.ceil((missingBytes / limit) * 1000)),
67
+ };
68
+ }
69
+
70
+ private refill(nowMs: number): void {
71
+ const limit = this.bytesPerSecond;
72
+ if (!limit) {
73
+ this.availableBytes = Number.POSITIVE_INFINITY;
74
+ this.lastRefillMs = nowMs;
75
+ return;
76
+ }
77
+
78
+ const elapsedMs = Math.max(0, nowMs - this.lastRefillMs);
79
+ this.lastRefillMs = nowMs;
80
+ this.availableBytes = Math.min(
81
+ limit,
82
+ this.availableBytes + (elapsedMs * limit) / 1000,
83
+ );
84
+ }
85
+ }