nappup 2.3.5 → 2.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -137,7 +137,7 @@ await publishApp(fileList, signer, {
137
137
 
138
138
  - **`fileList`** — a `FileList` or array of `File` objects (each needs `webkitRelativePath`).
139
139
  - **`signer`** — a [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md)-compatible signer. In the browser, `window.nostr` is used automatically if omitted.
140
- - **`onEvent`** — optional callback that receives progress events with a `type` (`'init'`, `'file-uploaded'`, `'complete'`, `'error'`, …) and `progress` (0–100).
140
+ - **`onEvent`** — optional callback that receives progress events with a `type` (`'services-checking'`, `'init'`, `'file-uploaded'`, `'complete'`, `'error'`, …) and `progress` (0–100).
141
141
 
142
142
  Rejected uploads use `NappupError`, with a stable code from
143
143
  `NAPPUP_ERROR_CODES`. The original error is retained as `cause`, and some
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "2.3.5",
9
+ "version": "2.3.6",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
package/src/index.js CHANGED
@@ -43,6 +43,7 @@ export default async function (fileList, nostrSigner, opts = {}) {
43
43
  * Every event has `type` (string) and `progress` (0–100 integer).
44
44
  *
45
45
  * Event types:
46
+ * 'services-checking' — Blossom preferences and server health are being checked
46
47
  * 'init' — { totalFiles, totalSteps, dTag, relayCount, blossomCount }
47
48
  * 'media-uploaded' — { mediaType: 'icon'|'key_art'|'screenshot', service: 'blossom'|'irfs'|null }
48
49
  * 'file-uploaded' — { filename, service: 'blossom'|'irfs' }
@@ -182,6 +183,7 @@ async function publishApp (fileList, nostrSigner, {
182
183
  let isExplicitIcon = false
183
184
  let pause = 1000
184
185
 
186
+ emit({ type: 'services-checking' })
185
187
  log('Checking for blossom servers...')
186
188
  let blossomServerUrls = []
187
189
  try {
@@ -3,6 +3,27 @@ import nostrRelays from '#services/nostr-relays.js'
3
3
  import { bytesToBase16 } from '#helpers/base16.js'
4
4
  import { normalizeBlossomServerUrl } from 'libp2r2p/url'
5
5
 
6
+ const DEFAULT_HEALTH_CHECK_TIMEOUT_MS = 5000
7
+ const DEFAULT_EXISTENCE_CHECK_TIMEOUT_MS = 5000
8
+
9
+ // Bounds browser fetches whose native network timeout can take minutes.
10
+ async function fetchWithTimeout (url, options, timeoutMs) {
11
+ const controller = new AbortController()
12
+ let timedOut = false
13
+ const timeoutId = setTimeout(() => {
14
+ timedOut = true
15
+ controller.abort()
16
+ }, timeoutMs)
17
+ try {
18
+ return await fetch(url, { ...options, signal: controller.signal })
19
+ } catch (error) {
20
+ if (timedOut) throw new Error(`request timed out after ${timeoutMs}ms`)
21
+ throw error
22
+ } finally {
23
+ clearTimeout(timeoutId)
24
+ }
25
+ }
26
+
6
27
  async function createAuthHeader (signer, modify) {
7
28
  const now = Math.floor(Date.now() / 1000)
8
29
  const event = {
@@ -43,13 +64,16 @@ export async function getBlossomServers (signer, writeRelays) {
43
64
  /**
44
65
  * Health-checks blossom servers with a simple HEAD request.
45
66
  * A server is considered healthy if fetch resolves (any HTTP status).
46
- * Only network-level errors mark a server as unreachable.
67
+ * Network errors and timeouts mark a server as unreachable.
47
68
  */
48
- export async function healthCheckServers (servers, signer, { log = () => {} } = {}) {
69
+ export async function healthCheckServers (servers, signer, {
70
+ log = () => {},
71
+ timeoutMs = DEFAULT_HEALTH_CHECK_TIMEOUT_MS
72
+ } = {}) {
49
73
  const results = await Promise.allSettled(
50
74
  servers.map(async (serverUrl) => {
51
75
  const normalized = normalizeBlossomServerUrl(serverUrl)
52
- await fetch(normalized, { method: 'HEAD' })
76
+ await fetchWithTimeout(normalized, { method: 'HEAD', mode: 'no-cors' }, timeoutMs)
53
77
  return normalized
54
78
  })
55
79
  )
@@ -86,9 +110,17 @@ export async function computeFileHash (file) {
86
110
  async function uploadFileToServer (serverUrl, signer, file, fileHash, mimeType, { shouldReupload, log, maxRetries = 5 }) {
87
111
  // Check if already uploaded
88
112
  if (!shouldReupload) {
89
- const checkResponse = await fetch(`${serverUrl}/${fileHash}`, { method: 'HEAD' })
90
- if (checkResponse.ok) {
91
- return { success: true, alreadyExists: true }
113
+ try {
114
+ const checkResponse = await fetchWithTimeout(
115
+ `${serverUrl}/${fileHash}`,
116
+ { method: 'HEAD' },
117
+ DEFAULT_EXISTENCE_CHECK_TIMEOUT_MS
118
+ )
119
+ if (checkResponse.ok) {
120
+ return { success: true, alreadyExists: true }
121
+ }
122
+ } catch (error) {
123
+ log(`Could not check whether ${fileHash} exists on ${serverUrl}; uploading it anyway: ${error?.message ?? error}`)
92
124
  }
93
125
  }
94
126
 
@@ -184,7 +216,8 @@ export async function uploadFilesToBlossom ({
184
216
  }
185
217
  })
186
218
 
187
- await Promise.allSettled(serverTasks)
219
+ // Unexpected task errors indicate a programming failure and must reach the caller.
220
+ await Promise.all(serverTasks)
188
221
 
189
222
  const uploadedFiles = []
190
223
  const failedFiles = []