@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
@@ -1,1285 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * Media Streaming Handler for Hashtree Worker
4
- *
5
- * Handles media requests from the service worker via MessagePort.
6
- * Supports both direct CID-based requests and path-based requests with live streaming.
7
- */
8
- import { getCachedRoot, onCachedRootUpdate } from './treeRootCache';
9
- import { resolveTreeRootNow, subscribeToTreeRoots } from './treeRootSubscription';
10
- import { getErrorMessage } from './utils/errorMessage';
11
- import { nhashDecode, toHex } from '@hashtree/core';
12
- import { readTreeEventSnapshot, resolveSnapshotRootCid } from '@hashtree/nostr';
13
- import { nip19 } from 'nostr-tools';
14
- import { LRUCache } from './utils/lruCache';
15
- // Thumbnail filename patterns to look for (in priority order)
16
- const THUMBNAIL_PATTERNS = ['thumbnail.jpg', 'thumbnail.webp', 'thumbnail.png', 'thumbnail.jpeg'];
17
- const PLAYABLE_MEDIA_EXTENSION_SET = new Set([
18
- '.mp4',
19
- '.webm',
20
- '.mkv',
21
- '.mov',
22
- '.avi',
23
- '.m4v',
24
- '.ogv',
25
- '.3gp',
26
- '.mp3',
27
- '.wav',
28
- '.flac',
29
- '.m4a',
30
- '.aac',
31
- '.ogg',
32
- '.oga',
33
- '.opus',
34
- ]);
35
- const MIME_TYPES = {
36
- mp4: 'video/mp4',
37
- m4v: 'video/mp4',
38
- webm: 'video/webm',
39
- ogg: 'video/ogg',
40
- ogv: 'video/ogg',
41
- mov: 'video/quicktime',
42
- avi: 'video/x-msvideo',
43
- mkv: 'video/x-matroska',
44
- mp3: 'audio/mpeg',
45
- wav: 'audio/wav',
46
- flac: 'audio/flac',
47
- m4a: 'audio/mp4',
48
- aac: 'audio/aac',
49
- oga: 'audio/ogg',
50
- opus: 'audio/ogg',
51
- jpg: 'image/jpeg',
52
- jpeg: 'image/jpeg',
53
- png: 'image/png',
54
- webp: 'image/webp',
55
- };
56
- // Timeout for considering a stream "done" (no updates)
57
- const LIVE_STREAM_TIMEOUT = 10000; // 10 seconds
58
- const ROOT_WAIT_TIMEOUT_MS = 15000;
59
- const DIRECTORY_PROBE_TIMEOUT_MS = 1000;
60
- const IMMUTABLE_LOOKUP_CACHE_HIT_TTL_MS = 5 * 60 * 1000;
61
- const IMMUTABLE_LOOKUP_CACHE_MISS_TTL_MS = 1000;
62
- const DIRECTORY_CACHE_SIZE = 1024;
63
- const RESOLVED_ENTRY_CACHE_SIZE = 2048;
64
- const MUTABLE_RESOLVED_ENTRY_CACHE_SIZE = 2048;
65
- const FILE_SIZE_CACHE_SIZE = 1024;
66
- const THUMBNAIL_PATH_CACHE_SIZE = 1024;
67
- // Chunk size for streaming to media port
68
- const MEDIA_CHUNK_SIZE = 256 * 1024; // 256KB chunks - matches videoChunker's firstChunkSize
69
- const activeMediaStreams = new Map();
70
- const inflightRootWaits = new Map();
71
- const directoryLookupCache = createAsyncLookupCache(DIRECTORY_CACHE_SIZE);
72
- const resolvedEntryLookupCache = createAsyncLookupCache(RESOLVED_ENTRY_CACHE_SIZE);
73
- const mutableResolvedEntryLookupCache = createAsyncLookupCache(MUTABLE_RESOLVED_ENTRY_CACHE_SIZE);
74
- const fileSizeLookupCache = createAsyncLookupCache(FILE_SIZE_CACHE_SIZE);
75
- const thumbnailPathLookupCache = createAsyncLookupCache(THUMBNAIL_PATH_CACHE_SIZE);
76
- let mediaPort = null;
77
- let tree = null;
78
- let mediaDebugEnabled = false;
79
- function logMediaDebug(event, data) {
80
- if (!mediaDebugEnabled)
81
- return;
82
- if (data) {
83
- console.log(`[WorkerMedia] ${event}`, data);
84
- }
85
- else {
86
- console.log(`[WorkerMedia] ${event}`);
87
- }
88
- }
89
- function createAsyncLookupCache(maxEntries) {
90
- return {
91
- values: new LRUCache(maxEntries),
92
- inflight: new Map(),
93
- };
94
- }
95
- function clearAsyncLookupCache(cache) {
96
- cache.values.clear();
97
- cache.inflight.clear();
98
- }
99
- function clearMediaLookupCaches() {
100
- inflightRootWaits.clear();
101
- clearAsyncLookupCache(directoryLookupCache);
102
- clearAsyncLookupCache(resolvedEntryLookupCache);
103
- clearAsyncLookupCache(mutableResolvedEntryLookupCache);
104
- clearAsyncLookupCache(fileSizeLookupCache);
105
- clearAsyncLookupCache(thumbnailPathLookupCache);
106
- }
107
- function getLookupTtlMs(value) {
108
- return value == null ? IMMUTABLE_LOOKUP_CACHE_MISS_TTL_MS : IMMUTABLE_LOOKUP_CACHE_HIT_TTL_MS;
109
- }
110
- async function loadCachedLookup(cache, key, loader, ttlMsForValue) {
111
- const now = Date.now();
112
- const cached = cache.values.get(key);
113
- if (cached && cached.expiresAt > now) {
114
- return cached.value;
115
- }
116
- if (cached) {
117
- cache.values.delete(key);
118
- }
119
- const inflight = cache.inflight.get(key);
120
- if (inflight) {
121
- return inflight;
122
- }
123
- const pending = loader()
124
- .then((value) => {
125
- const ttlMs = ttlMsForValue(value);
126
- if (ttlMs > 0) {
127
- cache.values.set(key, {
128
- value,
129
- expiresAt: Date.now() + ttlMs,
130
- });
131
- }
132
- return value;
133
- })
134
- .finally(() => {
135
- cache.inflight.delete(key);
136
- });
137
- cache.inflight.set(key, pending);
138
- return pending;
139
- }
140
- function cidCacheKey(cid) {
141
- return cid.key ? `${toHex(cid.hash)}?k=${toHex(cid.key)}` : toHex(cid.hash);
142
- }
143
- function sameCid(a, b) {
144
- if (!a && !b)
145
- return true;
146
- if (!a || !b)
147
- return false;
148
- return cidCacheKey(a) === cidCacheKey(b);
149
- }
150
- async function resolveSnapshotRoot(nhash, linkKey) {
151
- if (!tree)
152
- return null;
153
- const snapshotCid = nhashDecode(nhash);
154
- const snapshot = await readTreeEventSnapshot(tree, nip19, snapshotCid);
155
- if (!snapshot) {
156
- return null;
157
- }
158
- return resolveSnapshotRootCid(snapshot, linkKey);
159
- }
160
- /**
161
- * Initialize the media handler with the HashTree instance
162
- */
163
- export function initMediaHandler(hashTree) {
164
- tree = hashTree;
165
- clearMediaLookupCaches();
166
- }
167
- /**
168
- * Register a MessagePort from the service worker for media streaming
169
- */
170
- export function registerMediaPort(port, debug) {
171
- mediaPort = port;
172
- mediaDebugEnabled = !!debug;
173
- port.start?.();
174
- port.onmessage = async (e) => {
175
- const req = e.data;
176
- if (req.type === 'hashtree-file') {
177
- // SW file request format (direct from service worker)
178
- await handleSwFileRequest(req);
179
- }
180
- else if (req.type === 'media') {
181
- await handleMediaRequestByCid(req);
182
- }
183
- else if (req.type === 'mediaByPath') {
184
- await handleMediaRequestByPath(req);
185
- }
186
- else if (req.type === 'cancelMedia') {
187
- // Cancel an active stream
188
- const stream = activeMediaStreams.get(req.requestId);
189
- if (stream) {
190
- stream.cancelled = true;
191
- activeMediaStreams.delete(req.requestId);
192
- }
193
- }
194
- };
195
- console.log('[Worker] Media port registered');
196
- logMediaDebug('port:registered', { debug: mediaDebugEnabled });
197
- }
198
- /**
199
- * Handle direct CID-based media request
200
- */
201
- async function handleMediaRequestByCid(req) {
202
- if (!tree || !mediaPort)
203
- return;
204
- const { requestId, cid: cidHex, start, end, mimeType } = req;
205
- try {
206
- // Convert hex CID to proper CID object
207
- const hash = new Uint8Array(cidHex.length / 2);
208
- for (let i = 0; i < hash.length; i++) {
209
- hash[i] = parseInt(cidHex.substr(i * 2, 2), 16);
210
- }
211
- const cid = { hash };
212
- // Get file size first
213
- const totalSize = await tree.getSize(hash);
214
- // Send headers
215
- mediaPort.postMessage({
216
- type: 'headers',
217
- requestId,
218
- totalSize,
219
- mimeType: mimeType || 'application/octet-stream',
220
- isLive: false,
221
- });
222
- // Read range and stream chunks
223
- const data = await tree.readFileRange(cid, start, end);
224
- if (data) {
225
- await streamChunksToPort(requestId, data);
226
- }
227
- else {
228
- mediaPort.postMessage({
229
- type: 'error',
230
- requestId,
231
- message: 'File not found',
232
- });
233
- }
234
- }
235
- catch (err) {
236
- mediaPort.postMessage({
237
- type: 'error',
238
- requestId,
239
- message: getErrorMessage(err),
240
- });
241
- }
242
- }
243
- /**
244
- * Handle npub/path-based media request (supports live streaming)
245
- */
246
- async function handleMediaRequestByPath(req) {
247
- if (!tree || !mediaPort)
248
- return;
249
- const { requestId, npub, path, start, mimeType } = req;
250
- try {
251
- // Parse path to get tree name
252
- const pathParts = path.split('/').filter(Boolean);
253
- const treeName = pathParts[0] || 'public';
254
- const filePath = pathParts.slice(1).join('/');
255
- // Resolve npub to current CID
256
- let cid = await waitForCachedRoot(npub, treeName);
257
- if (!cid) {
258
- mediaPort.postMessage({
259
- type: 'error',
260
- requestId,
261
- message: `Tree root not found for ${npub}/${treeName}`,
262
- });
263
- return;
264
- }
265
- // Navigate to file within tree if path specified
266
- if (filePath) {
267
- const resolved = await resolveMutableTreeEntry(npub, treeName, filePath, {
268
- allowSingleSegmentRootFallback: false,
269
- expectedMimeType: mimeType,
270
- });
271
- if (!resolved) {
272
- mediaPort.postMessage({
273
- type: 'error',
274
- requestId,
275
- message: `File not found: ${filePath}`,
276
- });
277
- return;
278
- }
279
- cid = resolved.cid;
280
- }
281
- // Get file size
282
- const totalSize = await tree.getSize(cid.hash);
283
- // Send headers (isLive will be determined by watching for updates)
284
- mediaPort.postMessage({
285
- type: 'headers',
286
- requestId,
287
- totalSize,
288
- mimeType: mimeType || 'application/octet-stream',
289
- isLive: false, // Will update if we detect changes
290
- });
291
- // Stream initial content
292
- const data = await tree.readFileRange(cid, start);
293
- let offset = start;
294
- if (data) {
295
- await streamChunksToPort(requestId, data, false); // Don't close yet
296
- offset += data.length;
297
- }
298
- // Register for live updates
299
- const streamInfo = {
300
- requestId,
301
- npub,
302
- path,
303
- offset,
304
- cancelled: false,
305
- };
306
- activeMediaStreams.set(requestId, streamInfo);
307
- // Set up tree root watcher for this npub
308
- // When root changes, we'll check if this file has new data
309
- watchTreeRootForStream(npub, treeName, filePath, streamInfo);
310
- }
311
- catch (err) {
312
- mediaPort.postMessage({
313
- type: 'error',
314
- requestId,
315
- message: getErrorMessage(err),
316
- });
317
- }
318
- }
319
- /**
320
- * Stream data chunks to media port
321
- */
322
- async function streamChunksToPort(requestId, data, sendDone = true) {
323
- if (!mediaPort)
324
- return;
325
- for (let offset = 0; offset < data.length; offset += MEDIA_CHUNK_SIZE) {
326
- const chunk = data.slice(offset, offset + MEDIA_CHUNK_SIZE);
327
- mediaPort.postMessage({ type: 'chunk', requestId, data: chunk }, [chunk.buffer]);
328
- }
329
- if (sendDone) {
330
- mediaPort.postMessage({ type: 'done', requestId });
331
- }
332
- }
333
- /**
334
- * Watch for tree root updates and push new data to stream
335
- */
336
- function watchTreeRootForStream(npub, treeName, filePath, streamInfo) {
337
- let lastActivity = Date.now();
338
- let timeoutId = null;
339
- const checkForUpdates = async () => {
340
- if (streamInfo.cancelled || !tree || !mediaPort) {
341
- cleanup();
342
- return;
343
- }
344
- // Check if stream timed out
345
- if (Date.now() - lastActivity > LIVE_STREAM_TIMEOUT) {
346
- // No updates for a while, close the stream
347
- mediaPort.postMessage({
348
- type: 'done',
349
- requestId: streamInfo.requestId,
350
- });
351
- cleanup();
352
- return;
353
- }
354
- try {
355
- // Get current root
356
- const cid = await getCachedRoot(npub, treeName);
357
- if (!cid) {
358
- scheduleNext();
359
- return;
360
- }
361
- // Navigate to file
362
- let fileCid = cid;
363
- if (filePath) {
364
- const resolved = await resolvePathFromDirectoryListings(cid, filePath);
365
- if (!resolved) {
366
- scheduleNext();
367
- return;
368
- }
369
- fileCid = resolved.cid;
370
- }
371
- // Check for new data
372
- const totalSize = await tree.getSize(fileCid.hash);
373
- if (totalSize > streamInfo.offset) {
374
- // New data available!
375
- lastActivity = Date.now();
376
- const newData = await tree.readFileRange(fileCid, streamInfo.offset);
377
- if (newData && newData.length > 0) {
378
- await streamChunksToPort(streamInfo.requestId, newData, false);
379
- streamInfo.offset += newData.length;
380
- }
381
- }
382
- }
383
- catch {
384
- // Ignore errors, just try again
385
- }
386
- scheduleNext();
387
- };
388
- const scheduleNext = () => {
389
- if (!streamInfo.cancelled) {
390
- timeoutId = setTimeout(checkForUpdates, 1000); // Check every second
391
- }
392
- };
393
- const cleanup = () => {
394
- if (timeoutId)
395
- clearTimeout(timeoutId);
396
- activeMediaStreams.delete(streamInfo.requestId);
397
- };
398
- // Start watching
399
- scheduleNext();
400
- }
401
- /**
402
- * Handle file request from service worker (hashtree-file format)
403
- * This is the main entry point for direct SW → Worker communication
404
- */
405
- async function handleSwFileRequest(req) {
406
- if (!tree || !mediaPort)
407
- return;
408
- const { requestId, npub, nhash, snapshot, linkKey, treeName, path, start, end, rangeHeader, mimeType, download } = req;
409
- logMediaDebug('sw:request', {
410
- requestId,
411
- npub: npub ?? null,
412
- nhash: nhash ?? null,
413
- snapshot: !!snapshot,
414
- hasLinkKey: !!linkKey,
415
- treeName: treeName ?? null,
416
- path,
417
- start,
418
- end: end ?? null,
419
- rangeHeader: rangeHeader ?? null,
420
- mimeType,
421
- download: !!download,
422
- });
423
- try {
424
- let resolvedEntry = null;
425
- if (nhash) {
426
- // Direct nhash request - decode to CID or resolve signed snapshot permalink.
427
- const rootCid = snapshot
428
- ? await resolveSnapshotRoot(nhash, linkKey)
429
- : nhashDecode(nhash);
430
- if (!rootCid) {
431
- sendSwError(requestId, snapshot ? 403 : 404, snapshot ? 'Tree snapshot could not be resolved' : `File not found: ${path}`);
432
- return;
433
- }
434
- resolvedEntry = await resolveEntryWithinRoot(rootCid, path || '', {
435
- allowSingleSegmentRootFallback: true,
436
- expectedMimeType: mimeType,
437
- });
438
- if (!resolvedEntry) {
439
- sendSwError(requestId, 404, `File not found: ${path}`);
440
- return;
441
- }
442
- }
443
- else if (npub && treeName) {
444
- // Npub-based request - resolve through mutable root history when needed
445
- resolvedEntry = await resolveMutableTreeEntry(npub, treeName, path || '', {
446
- allowSingleSegmentRootFallback: false,
447
- expectedMimeType: mimeType,
448
- });
449
- if (!resolvedEntry) {
450
- sendSwError(requestId, 404, 'File not found');
451
- return;
452
- }
453
- }
454
- if (!resolvedEntry?.cid) {
455
- sendSwError(requestId, 400, 'Invalid request');
456
- return;
457
- }
458
- // Get file size
459
- // Directory listings may report 0 for non-empty files when the actual byte size
460
- // is not embedded in the tree node metadata. Treat that as unknown and resolve
461
- // the real size from the file itself before building HTTP headers.
462
- const knownSize = typeof resolvedEntry.size === 'number' && resolvedEntry.size > 0
463
- ? resolvedEntry.size
464
- : null;
465
- const effectivePath = resolvedEntry.path ?? path;
466
- const effectiveMimeType = (mimeType === 'application/octet-stream'
467
- || isThumbnailAliasPath(path)
468
- || isVideoAliasPath(path))
469
- ? guessMimeTypeFromPath(effectivePath)
470
- : mimeType;
471
- const totalSize = knownSize ?? await getFileSize(resolvedEntry.cid);
472
- if (totalSize === null) {
473
- const canBufferWholeFile = !rangeHeader && start === 0 && end === undefined;
474
- if (!canBufferWholeFile || typeof tree.readFile !== 'function') {
475
- sendSwError(requestId, 404, 'File data not found');
476
- return;
477
- }
478
- const fullData = await tree.readFile(resolvedEntry.cid);
479
- if (!fullData) {
480
- sendSwError(requestId, 404, 'File data not found');
481
- return;
482
- }
483
- sendBufferedSwResponse(requestId, fullData, {
484
- npub,
485
- path: effectivePath,
486
- mimeType: effectiveMimeType,
487
- download,
488
- });
489
- return;
490
- }
491
- // Stream the content
492
- await streamSwResponse(requestId, resolvedEntry.cid, totalSize, {
493
- npub,
494
- path: effectivePath,
495
- start,
496
- end,
497
- rangeHeader,
498
- mimeType: effectiveMimeType,
499
- download,
500
- });
501
- }
502
- catch (err) {
503
- sendSwError(requestId, 500, getErrorMessage(err));
504
- }
505
- }
506
- async function waitForCachedRoot(npub, treeName) {
507
- const cached = await getCachedRoot(npub, treeName);
508
- if (cached)
509
- return cached;
510
- const cacheKey = `${npub}/${treeName}`;
511
- const inflight = inflightRootWaits.get(cacheKey);
512
- if (inflight) {
513
- return inflight;
514
- }
515
- const pubkey = decodeNpubToPubkey(npub);
516
- if (pubkey) {
517
- subscribeToTreeRoots(pubkey);
518
- }
519
- const pending = new Promise((resolve) => {
520
- let settled = false;
521
- const finish = (cid) => {
522
- if (settled)
523
- return;
524
- settled = true;
525
- clearTimeout(timeout);
526
- unsubscribe();
527
- resolve(cid);
528
- };
529
- const unsubscribe = onCachedRootUpdate((updatedNpub, updatedTreeName, cid) => {
530
- if (updatedNpub === npub && updatedTreeName === treeName && cid) {
531
- finish(cid);
532
- }
533
- });
534
- const timeout = setTimeout(() => {
535
- logMediaDebug('root:timeout', { npub, treeName });
536
- finish(null);
537
- }, ROOT_WAIT_TIMEOUT_MS);
538
- void getCachedRoot(npub, treeName).then((current) => {
539
- if (current) {
540
- finish(current);
541
- }
542
- });
543
- void resolveTreeRootNow(npub, treeName, ROOT_WAIT_TIMEOUT_MS).then((resolved) => {
544
- if (resolved) {
545
- finish(resolved);
546
- }
547
- }).catch(() => { });
548
- }).finally(() => {
549
- inflightRootWaits.delete(cacheKey);
550
- });
551
- inflightRootWaits.set(cacheKey, pending);
552
- return pending;
553
- }
554
- async function resolveMutableTreeEntry(npub, treeName, path, options) {
555
- const currentRoot = await waitForCachedRoot(npub, treeName);
556
- const cacheKey = [
557
- npub,
558
- treeName,
559
- currentRoot ? cidCacheKey(currentRoot) : 'none',
560
- options?.allowSingleSegmentRootFallback ? 'root-fallback' : 'strict',
561
- options?.expectedMimeType ?? '',
562
- path,
563
- ].join('|');
564
- return loadCachedLookup(mutableResolvedEntryLookupCache, cacheKey, async () => {
565
- if (currentRoot) {
566
- const currentEntry = await resolveEntryWithinRoot(currentRoot, path, options);
567
- if (currentEntry) {
568
- return currentEntry;
569
- }
570
- }
571
- return null;
572
- }, (value) => getLookupTtlMs(value));
573
- }
574
- function decodeNpubToPubkey(npub) {
575
- if (!npub.startsWith('npub1'))
576
- return null;
577
- try {
578
- const decoded = nip19.decode(npub);
579
- if (decoded.type !== 'npub')
580
- return null;
581
- return decoded.data;
582
- }
583
- catch {
584
- return null;
585
- }
586
- }
587
- async function resolveEntryWithinRoot(rootCid, path, options) {
588
- if (!tree)
589
- return null;
590
- const cacheKey = [
591
- cidCacheKey(rootCid),
592
- options?.allowSingleSegmentRootFallback ? 'root-fallback' : 'strict',
593
- path,
594
- ].join('|');
595
- return loadCachedLookup(resolvedEntryLookupCache, cacheKey, async () => {
596
- if (!path) {
597
- return { cid: rootCid };
598
- }
599
- const resolvedThumbnail = await resolveThumbnailAliasEntry(rootCid, path);
600
- if (resolvedThumbnail) {
601
- return resolvedThumbnail;
602
- }
603
- if (isThumbnailAliasPath(path)) {
604
- return null;
605
- }
606
- const resolvedPlayable = await resolvePlayableAliasEntry(rootCid, path);
607
- if (resolvedPlayable) {
608
- return resolvedPlayable;
609
- }
610
- if (isVideoAliasPath(path)) {
611
- return null;
612
- }
613
- if (options?.allowSingleSegmentRootFallback &&
614
- await canFallbackToRootBlob(rootCid, path, path, options?.expectedMimeType)) {
615
- const isDirectory = await canListDirectory(rootCid);
616
- if (!isDirectory) {
617
- return { cid: rootCid };
618
- }
619
- }
620
- if (options?.allowSingleSegmentRootFallback &&
621
- !path.includes('/') &&
622
- isExactThumbnailFilenamePath(path) &&
623
- !(await canListDirectory(rootCid))) {
624
- return null;
625
- }
626
- const entry = await resolvePathFromDirectoryListings(rootCid, path);
627
- if (entry) {
628
- return entry;
629
- }
630
- if (options?.allowSingleSegmentRootFallback &&
631
- await canFallbackToRootBlob(rootCid, path, path, options?.expectedMimeType)) {
632
- return { cid: rootCid };
633
- }
634
- return null;
635
- }, (value) => getLookupTtlMs(value));
636
- }
637
- async function resolveCidWithinRoot(rootCid, path, options) {
638
- return (await resolveEntryWithinRoot(rootCid, path, options))?.cid ?? null;
639
- }
640
- async function resolvePathFromDirectoryListings(rootCid, path) {
641
- if (!tree)
642
- return null;
643
- const parts = path.split('/').filter(Boolean);
644
- if (parts.length === 0) {
645
- return { cid: rootCid };
646
- }
647
- let currentCid = rootCid;
648
- for (let i = 0; i < parts.length; i += 1) {
649
- const entries = await loadDirectoryListing(currentCid);
650
- if (!entries) {
651
- return null;
652
- }
653
- const entry = entries.find((candidate) => candidate.name === parts[i]);
654
- if (!entry?.cid) {
655
- return null;
656
- }
657
- if (i === parts.length - 1) {
658
- return { cid: entry.cid, size: entry.size, path: parts.slice(0, i + 1).join('/') };
659
- }
660
- currentCid = entry.cid;
661
- }
662
- return null;
663
- }
664
- function hasImageBlobSignature(blob) {
665
- if (blob.length >= 3 && blob[0] === 0xff && blob[1] === 0xd8 && blob[2] === 0xff) {
666
- return true;
667
- }
668
- if (blob.length >= 8
669
- && blob[0] === 0x89
670
- && blob[1] === 0x50
671
- && blob[2] === 0x4e
672
- && blob[3] === 0x47
673
- && blob[4] === 0x0d
674
- && blob[5] === 0x0a
675
- && blob[6] === 0x1a
676
- && blob[7] === 0x0a) {
677
- return true;
678
- }
679
- if (blob.length >= 12
680
- && blob[0] === 0x52
681
- && blob[1] === 0x49
682
- && blob[2] === 0x46
683
- && blob[3] === 0x46
684
- && blob[8] === 0x57
685
- && blob[9] === 0x45
686
- && blob[10] === 0x42
687
- && blob[11] === 0x50) {
688
- return true;
689
- }
690
- if (blob.length >= 6
691
- && blob[0] === 0x47
692
- && blob[1] === 0x49
693
- && blob[2] === 0x46
694
- && blob[3] === 0x38
695
- && (blob[4] === 0x37 || blob[4] === 0x39)
696
- && blob[5] === 0x61) {
697
- return true;
698
- }
699
- return false;
700
- }
701
- function readAscii(blob, start, end) {
702
- return String.fromCharCode(...blob.slice(start, end));
703
- }
704
- function sniffPlayableMediaExtension(blob) {
705
- if (!blob.length)
706
- return null;
707
- if (blob.length >= 12 && readAscii(blob, 4, 8) === 'ftyp') {
708
- const brand = readAscii(blob, 8, 12).toLowerCase();
709
- if (brand.startsWith('m4a'))
710
- return '.m4a';
711
- if (brand.startsWith('qt'))
712
- return '.mov';
713
- return '.mp4';
714
- }
715
- if (blob.length >= 4
716
- && blob[0] === 0x1a
717
- && blob[1] === 0x45
718
- && blob[2] === 0xdf
719
- && blob[3] === 0xa3) {
720
- const lowerHeader = readAscii(blob, 0, Math.min(blob.length, 64)).toLowerCase();
721
- return lowerHeader.includes('webm') ? '.webm' : '.mkv';
722
- }
723
- if (blob.length >= 4 && readAscii(blob, 0, 4) === 'OggS') {
724
- return '.ogg';
725
- }
726
- if (blob.length >= 4 && readAscii(blob, 0, 4) === 'fLaC') {
727
- return '.flac';
728
- }
729
- if (blob.length >= 12
730
- && readAscii(blob, 0, 4) === 'RIFF'
731
- && readAscii(blob, 8, 12) === 'WAVE') {
732
- return '.wav';
733
- }
734
- if (blob.length >= 3 && readAscii(blob, 0, 3) === 'ID3') {
735
- return '.mp3';
736
- }
737
- if (blob.length >= 2 && blob[0] === 0xff && (blob[1] & 0xf6) === 0xf0) {
738
- return '.aac';
739
- }
740
- if (blob.length >= 2 && blob[0] === 0xff && (blob[1] & 0xe0) === 0xe0) {
741
- return '.mp3';
742
- }
743
- return null;
744
- }
745
- function guessMimeTypeFromPath(path) {
746
- const ext = path?.split('.').pop()?.toLowerCase() ?? '';
747
- return MIME_TYPES[ext] || 'application/octet-stream';
748
- }
749
- async function canFallbackToRootBlob(rootCid, resolvedPath, originalPath, expectedMimeType) {
750
- if (resolvedPath !== originalPath)
751
- return false;
752
- if (resolvedPath.includes('/'))
753
- return false;
754
- if (isExactThumbnailFilenamePath(resolvedPath)) {
755
- if (!expectedMimeType?.startsWith('image/')) {
756
- return false;
757
- }
758
- if (!tree) {
759
- return false;
760
- }
761
- try {
762
- if (typeof tree.readFileRange === 'function') {
763
- const header = await tree.readFileRange(rootCid, 0, 64);
764
- return !!header && hasImageBlobSignature(header);
765
- }
766
- if (typeof tree.getBlob === 'function') {
767
- const blob = await tree.getBlob(rootCid.hash);
768
- return !!blob && hasImageBlobSignature(blob);
769
- }
770
- return false;
771
- }
772
- catch {
773
- return false;
774
- }
775
- }
776
- return /\.[A-Za-z0-9]{1,16}$/.test(resolvedPath);
777
- }
778
- function isThumbnailAliasPath(path) {
779
- return path === 'thumbnail' || path.endsWith('/thumbnail');
780
- }
781
- function isVideoAliasPath(path) {
782
- return path === 'video' || path.endsWith('/video');
783
- }
784
- function isExactThumbnailFilenamePath(path) {
785
- const fileName = path.split('/').filter(Boolean).at(-1)?.toLowerCase() ?? '';
786
- return fileName.startsWith('thumbnail.');
787
- }
788
- function isImageFileName(fileName) {
789
- const normalized = fileName.trim().toLowerCase();
790
- return normalized.endsWith('.jpg')
791
- || normalized.endsWith('.jpeg')
792
- || normalized.endsWith('.png')
793
- || normalized.endsWith('.webp');
794
- }
795
- function findThumbnailFileEntry(entries) {
796
- for (const pattern of THUMBNAIL_PATTERNS) {
797
- const directMatch = entries.find((entry) => entry.name === pattern && entry.cid);
798
- if (directMatch?.cid) {
799
- return directMatch;
800
- }
801
- }
802
- return entries.find((entry) => isImageFileName(entry.name) && entry.cid) ?? null;
803
- }
804
- function resolveEmbeddedThumbnailLookup(value, entries, dirPath) {
805
- if (typeof value !== 'string') {
806
- return null;
807
- }
808
- const trimmed = value.trim();
809
- if (!trimmed) {
810
- return null;
811
- }
812
- if (trimmed.startsWith('nhash1')) {
813
- try {
814
- return {
815
- path: dirPath ? `${dirPath}/thumbnail` : 'thumbnail',
816
- cid: nhashDecode(trimmed),
817
- };
818
- }
819
- catch {
820
- return null;
821
- }
822
- }
823
- const normalized = trimmed
824
- .split('?')[0]
825
- ?.split('#')[0]
826
- ?.split('/')
827
- .filter(Boolean)
828
- .at(-1);
829
- if (!normalized) {
830
- return null;
831
- }
832
- const entry = entries.find((candidate) => candidate.name === normalized && candidate.cid);
833
- if (!entry?.cid) {
834
- return null;
835
- }
836
- return {
837
- path: dirPath ? `${dirPath}/${entry.name}` : entry.name,
838
- cid: entry.cid,
839
- size: entry.size,
840
- };
841
- }
842
- async function resolveThumbnailLookupFromMetadata(entries, dirPath) {
843
- if (!tree) {
844
- return null;
845
- }
846
- const playableEntry = entries.find((entry) => isPlayableMediaFileName(entry.name));
847
- const playableThumbnail = playableEntry?.meta && typeof playableEntry.meta.thumbnail === 'string'
848
- ? playableEntry.meta.thumbnail
849
- : null;
850
- const embeddedPlayableThumbnail = resolveEmbeddedThumbnailLookup(playableThumbnail, entries, dirPath);
851
- if (embeddedPlayableThumbnail) {
852
- return embeddedPlayableThumbnail;
853
- }
854
- for (const metadataName of ['metadata.json', 'info.json']) {
855
- const metadataEntry = entries.find((entry) => entry.name === metadataName && entry.cid);
856
- if (!metadataEntry?.cid) {
857
- continue;
858
- }
859
- try {
860
- const metadataData = await tree.readFile(metadataEntry.cid);
861
- if (!metadataData) {
862
- continue;
863
- }
864
- const parsed = JSON.parse(new TextDecoder().decode(metadataData));
865
- const embeddedThumbnail = resolveEmbeddedThumbnailLookup(parsed?.thumbnail, entries, dirPath);
866
- if (embeddedThumbnail) {
867
- return embeddedThumbnail;
868
- }
869
- }
870
- catch {
871
- continue;
872
- }
873
- }
874
- return null;
875
- }
876
- async function findThumbnailLookupInEntries(entries, dirPath) {
877
- const directThumbnail = findThumbnailFileEntry(entries);
878
- if (directThumbnail?.cid) {
879
- return {
880
- path: dirPath ? `${dirPath}/${directThumbnail.name}` : directThumbnail.name,
881
- cid: directThumbnail.cid,
882
- size: directThumbnail.size,
883
- };
884
- }
885
- return await resolveThumbnailLookupFromMetadata(entries, dirPath);
886
- }
887
- async function detectDirectPlayableLookup(cid, dirPath) {
888
- if (!tree || typeof tree.readFileRange !== 'function') {
889
- return null;
890
- }
891
- try {
892
- const header = await tree.readFileRange(cid, 0, 64);
893
- if (!(header instanceof Uint8Array) || header.length === 0) {
894
- return null;
895
- }
896
- const extension = sniffPlayableMediaExtension(header);
897
- if (!extension) {
898
- return null;
899
- }
900
- const fileName = `video${extension}`;
901
- return {
902
- cid,
903
- path: dirPath ? `${dirPath}/${fileName}` : fileName,
904
- };
905
- }
906
- catch {
907
- return null;
908
- }
909
- }
910
- function findPlayableLookupInEntries(entries, dirPath) {
911
- const playableEntry = entries.find((entry) => isPlayableMediaFileName(entry.name) && entry.cid);
912
- if (!playableEntry?.cid) {
913
- return null;
914
- }
915
- return {
916
- cid: playableEntry.cid,
917
- size: playableEntry.size,
918
- path: dirPath ? `${dirPath}/${playableEntry.name}` : playableEntry.name,
919
- };
920
- }
921
- async function normalizeAliasPath(rootCid, path) {
922
- if (!path)
923
- return '';
924
- const resolvedThumbnail = await resolveThumbnailAliasEntry(rootCid, path);
925
- if (resolvedThumbnail) {
926
- return resolvedThumbnail.path;
927
- }
928
- const resolvedPlayable = await resolvePlayableAliasEntry(rootCid, path);
929
- if (resolvedPlayable?.path) {
930
- return resolvedPlayable.path;
931
- }
932
- return path;
933
- }
934
- async function canListDirectory(rootCid) {
935
- if (!tree)
936
- return false;
937
- try {
938
- if ('isDirectory' in tree && typeof tree.isDirectory === 'function') {
939
- return await tree.isDirectory(rootCid);
940
- }
941
- const entries = await listDirectoryWithProbeTimeout(rootCid);
942
- return Array.isArray(entries);
943
- }
944
- catch {
945
- return false;
946
- }
947
- }
948
- export const __test__ = {
949
- handleSwFileRequest,
950
- resolveCidWithinRoot,
951
- resolveMutableTreeEntry,
952
- normalizeAliasPath,
953
- canListDirectory,
954
- waitForCachedRoot,
955
- };
956
- async function resolveThumbnailAliasEntry(rootCid, path) {
957
- if (!isThumbnailAliasPath(path)) {
958
- return null;
959
- }
960
- const dirPath = path.endsWith('/thumbnail')
961
- ? path.slice(0, -'/thumbnail'.length)
962
- : '';
963
- return findThumbnailLookupInDir(rootCid, dirPath);
964
- }
965
- async function resolvePlayableAliasEntry(rootCid, path) {
966
- if (!isVideoAliasPath(path)) {
967
- return null;
968
- }
969
- const dirPath = path.endsWith('/video')
970
- ? path.slice(0, -'/video'.length)
971
- : '';
972
- return findPlayableLookupInDir(rootCid, dirPath);
973
- }
974
- async function loadDirectoryListing(cid) {
975
- if (!tree)
976
- return null;
977
- return loadCachedLookup(directoryLookupCache, cidCacheKey(cid), async () => tree.listDirectory(cid), (value) => getLookupTtlMs(value));
978
- }
979
- async function listDirectoryWithProbeTimeout(cid) {
980
- if (!tree)
981
- return null;
982
- return Promise.race([
983
- loadDirectoryListing(cid),
984
- new Promise((resolve) => setTimeout(() => resolve(null), DIRECTORY_PROBE_TIMEOUT_MS)),
985
- ]);
986
- }
987
- /**
988
- * Send error response to SW
989
- */
990
- function sendSwError(requestId, status, message) {
991
- if (!mediaPort)
992
- return;
993
- logMediaDebug('sw:error', { requestId, status, message });
994
- mediaPort.postMessage({
995
- type: 'error',
996
- requestId,
997
- status,
998
- message,
999
- });
1000
- }
1001
- /**
1002
- * Get file size from CID (handles both chunked and single blob files)
1003
- */
1004
- async function getFileSize(cid) {
1005
- if (!tree)
1006
- return null;
1007
- return loadCachedLookup(fileSizeLookupCache, cidCacheKey(cid), async () => {
1008
- const treeNode = await tree.getTreeNode(cid);
1009
- if (treeNode) {
1010
- // Chunked file - sum link sizes from decrypted tree node
1011
- return treeNode.links.reduce((sum, l) => sum + l.size, 0);
1012
- }
1013
- // Single blob - fetch to check existence and get size
1014
- const blob = await tree.getBlob(cid.hash);
1015
- if (!blob)
1016
- return null;
1017
- // For encrypted blobs, decrypted size = encrypted size - 16 (nonce overhead)
1018
- return cid.key ? Math.max(0, blob.length - 16) : blob.length;
1019
- }, (value) => getLookupTtlMs(value));
1020
- }
1021
- /**
1022
- * Find actual thumbnail file in a directory
1023
- */
1024
- async function findThumbnailLookupInDir(rootCid, dirPath) {
1025
- if (!tree)
1026
- return null;
1027
- return loadCachedLookup(thumbnailPathLookupCache, `${cidCacheKey(rootCid)}|${dirPath}`, async () => {
1028
- try {
1029
- const dirEntry = dirPath
1030
- ? await resolvePathFromDirectoryListings(rootCid, dirPath)
1031
- : { cid: rootCid };
1032
- if (!dirEntry)
1033
- return null;
1034
- const entries = await listDirectoryWithProbeTimeout(dirEntry.cid);
1035
- if (!entries)
1036
- return null;
1037
- const rootThumbnail = await findThumbnailLookupInEntries(entries, dirPath);
1038
- if (rootThumbnail) {
1039
- return rootThumbnail;
1040
- }
1041
- const hasPlayableMediaFile = entries.some((entry) => isPlayableMediaFileName(entry.name));
1042
- if (!hasPlayableMediaFile && entries.length > 0) {
1043
- const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name));
1044
- for (const entry of sortedEntries.slice(0, 3)) {
1045
- if (entry.name.endsWith('.json') || entry.name.endsWith('.txt')) {
1046
- continue;
1047
- }
1048
- try {
1049
- const subEntries = await listDirectoryWithProbeTimeout(entry.cid);
1050
- if (!subEntries) {
1051
- continue;
1052
- }
1053
- const prefix = dirPath ? `${dirPath}/${entry.name}` : entry.name;
1054
- const nestedThumbnail = await findThumbnailLookupInEntries(subEntries, prefix);
1055
- if (nestedThumbnail) {
1056
- return nestedThumbnail;
1057
- }
1058
- }
1059
- catch {
1060
- continue;
1061
- }
1062
- }
1063
- }
1064
- return null;
1065
- }
1066
- catch {
1067
- return null;
1068
- }
1069
- }, (value) => getLookupTtlMs(value));
1070
- }
1071
- async function findPlayableLookupInDir(rootCid, dirPath) {
1072
- if (!tree)
1073
- return null;
1074
- return loadCachedLookup(resolvedEntryLookupCache, `${cidCacheKey(rootCid)}|video|${dirPath}`, async () => {
1075
- try {
1076
- const dirEntry = dirPath
1077
- ? await resolvePathFromDirectoryListings(rootCid, dirPath)
1078
- : { cid: rootCid, path: '' };
1079
- if (!dirEntry)
1080
- return null;
1081
- const entries = await listDirectoryWithProbeTimeout(dirEntry.cid);
1082
- if (entries && entries.length > 0) {
1083
- const directPlayable = findPlayableLookupInEntries(entries, dirPath);
1084
- if (directPlayable) {
1085
- return directPlayable;
1086
- }
1087
- const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name));
1088
- for (const entry of sortedEntries.slice(0, 3)) {
1089
- if (entry.name.endsWith('.json') || entry.name.endsWith('.txt') || !entry.cid) {
1090
- continue;
1091
- }
1092
- const prefix = dirPath ? `${dirPath}/${entry.name}` : entry.name;
1093
- const subEntries = await listDirectoryWithProbeTimeout(entry.cid);
1094
- if (!subEntries || subEntries.length === 0) {
1095
- const directPlayableChild = await detectDirectPlayableLookup(entry.cid, prefix);
1096
- if (directPlayableChild) {
1097
- return directPlayableChild;
1098
- }
1099
- continue;
1100
- }
1101
- const nestedPlayable = findPlayableLookupInEntries(subEntries, prefix);
1102
- if (nestedPlayable) {
1103
- return nestedPlayable;
1104
- }
1105
- }
1106
- }
1107
- return await detectDirectPlayableLookup(dirEntry.cid, dirPath);
1108
- }
1109
- catch {
1110
- return null;
1111
- }
1112
- }, (value) => getLookupTtlMs(value));
1113
- }
1114
- /**
1115
- * Stream response to SW with proper HTTP headers
1116
- */
1117
- async function streamSwResponse(requestId, cid, totalSize, options) {
1118
- if (!tree || !mediaPort)
1119
- return;
1120
- const { npub, path, start = 0, end, rangeHeader, mimeType = 'application/octet-stream', download } = options;
1121
- let rangeStart = start;
1122
- let rangeEnd = end !== undefined ? Math.min(end, totalSize - 1) : totalSize - 1;
1123
- if (rangeHeader) {
1124
- const parsedRange = parseHttpByteRange(rangeHeader, totalSize);
1125
- if (parsedRange.kind === 'range') {
1126
- rangeStart = parsedRange.range.start;
1127
- rangeEnd = parsedRange.range.endInclusive;
1128
- }
1129
- else if (parsedRange.kind === 'unsatisfiable') {
1130
- sendSwError(requestId, 416, `Range not satisfiable for ${totalSize} byte file`);
1131
- return;
1132
- }
1133
- }
1134
- const contentLength = rangeEnd - rangeStart + 1;
1135
- // Build cache control header
1136
- const isNpubRequest = !!npub;
1137
- const isImage = mimeType.startsWith('image/');
1138
- let cacheControl;
1139
- if (!isNpubRequest) {
1140
- cacheControl = 'public, max-age=31536000, immutable'; // nhash: immutable
1141
- }
1142
- else if (isImage) {
1143
- cacheControl = 'public, max-age=60, stale-while-revalidate=86400';
1144
- }
1145
- else {
1146
- cacheControl = 'no-cache, no-store, must-revalidate';
1147
- }
1148
- // Build headers
1149
- const headers = {
1150
- 'Content-Type': mimeType,
1151
- 'Accept-Ranges': 'bytes',
1152
- 'Cache-Control': cacheControl,
1153
- 'Content-Length': String(contentLength),
1154
- };
1155
- if (download) {
1156
- const filename = path || 'file';
1157
- headers['Content-Disposition'] = `attachment; filename="${filename}"`;
1158
- }
1159
- // Determine status (206 for range requests)
1160
- const isRangeRequest = !!rangeHeader || end !== undefined || start > 0;
1161
- const status = isRangeRequest ? 206 : 200;
1162
- if (isRangeRequest) {
1163
- headers['Content-Range'] = `bytes ${rangeStart}-${rangeEnd}/${totalSize}`;
1164
- }
1165
- logMediaDebug('sw:response', {
1166
- requestId,
1167
- status,
1168
- totalSize,
1169
- rangeStart,
1170
- rangeEnd,
1171
- });
1172
- // Send headers
1173
- mediaPort.postMessage({
1174
- type: 'headers',
1175
- requestId,
1176
- status,
1177
- headers,
1178
- totalSize,
1179
- });
1180
- // Stream chunks
1181
- let offset = rangeStart;
1182
- while (offset <= rangeEnd) {
1183
- const chunkEnd = Math.min(offset + MEDIA_CHUNK_SIZE - 1, rangeEnd);
1184
- const chunk = await tree.readFileRange(cid, offset, chunkEnd + 1);
1185
- if (!chunk)
1186
- break;
1187
- mediaPort.postMessage({ type: 'chunk', requestId, data: chunk }, [chunk.buffer]);
1188
- offset = chunkEnd + 1;
1189
- }
1190
- // Signal done
1191
- mediaPort.postMessage({ type: 'done', requestId });
1192
- }
1193
- function sendBufferedSwResponse(requestId, data, options) {
1194
- if (!mediaPort)
1195
- return;
1196
- const { npub, path, mimeType = 'application/octet-stream', download } = options;
1197
- const isNpubRequest = !!npub;
1198
- const isImage = mimeType.startsWith('image/');
1199
- let cacheControl;
1200
- if (!isNpubRequest) {
1201
- cacheControl = 'public, max-age=31536000, immutable';
1202
- }
1203
- else if (isImage) {
1204
- cacheControl = 'public, max-age=60, stale-while-revalidate=86400';
1205
- }
1206
- else {
1207
- cacheControl = 'no-cache, no-store, must-revalidate';
1208
- }
1209
- const headers = {
1210
- 'Content-Type': mimeType,
1211
- 'Content-Length': String(data.length),
1212
- 'Cache-Control': cacheControl,
1213
- };
1214
- if (download) {
1215
- headers['Content-Disposition'] = `attachment; filename="${path || 'file'}"`;
1216
- }
1217
- mediaPort.postMessage({
1218
- type: 'headers',
1219
- requestId,
1220
- status: 200,
1221
- headers,
1222
- totalSize: data.length,
1223
- });
1224
- mediaPort.postMessage({ type: 'chunk', requestId, data }, [data.buffer]);
1225
- mediaPort.postMessage({ type: 'done', requestId });
1226
- }
1227
- function parseHttpByteRange(rangeHeader, totalSize) {
1228
- if (!rangeHeader)
1229
- return { kind: 'unsupported' };
1230
- const bytesRange = rangeHeader.startsWith('bytes=')
1231
- ? rangeHeader.slice('bytes='.length)
1232
- : null;
1233
- if (!bytesRange || bytesRange.includes(','))
1234
- return { kind: 'unsupported' };
1235
- if (totalSize <= 0)
1236
- return { kind: 'unsatisfiable' };
1237
- const parts = bytesRange.split('-', 2);
1238
- if (parts.length !== 2)
1239
- return { kind: 'unsupported' };
1240
- const [startPart, endPart] = parts;
1241
- if (!startPart) {
1242
- const suffixLength = Number.parseInt(endPart, 10);
1243
- if (!Number.isFinite(suffixLength) || suffixLength <= 0) {
1244
- return { kind: 'unsatisfiable' };
1245
- }
1246
- const clampedSuffix = Math.min(suffixLength, totalSize);
1247
- return {
1248
- kind: 'range',
1249
- range: {
1250
- start: totalSize - clampedSuffix,
1251
- endInclusive: totalSize - 1,
1252
- },
1253
- };
1254
- }
1255
- const start = Number.parseInt(startPart, 10);
1256
- if (!Number.isFinite(start) || start < 0 || start >= totalSize) {
1257
- return { kind: 'unsatisfiable' };
1258
- }
1259
- const endInclusive = endPart ? Number.parseInt(endPart, 10) : totalSize - 1;
1260
- if (!Number.isFinite(endInclusive) || endInclusive < start) {
1261
- return { kind: 'unsatisfiable' };
1262
- }
1263
- return {
1264
- kind: 'range',
1265
- range: {
1266
- start,
1267
- endInclusive: Math.min(endInclusive, totalSize - 1),
1268
- },
1269
- };
1270
- }
1271
- function isPlayableMediaFileName(name) {
1272
- const normalized = name.trim().toLowerCase();
1273
- if (!normalized || normalized.endsWith('/')) {
1274
- return false;
1275
- }
1276
- if (normalized.startsWith('video.')) {
1277
- return true;
1278
- }
1279
- const lastDot = normalized.lastIndexOf('.');
1280
- if (lastDot === -1) {
1281
- return false;
1282
- }
1283
- return PLAYABLE_MEDIA_EXTENSION_SET.has(normalized.slice(lastDot));
1284
- }
1285
- //# sourceMappingURL=mediaHandler.js.map