nappup 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +84 -12
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -9,15 +9,66 @@ import { NAPP_CATEGORIES } from '#config/napp-categories.js'
|
|
|
9
9
|
import { getBlossomServers, healthCheckServers, uploadFilesToBlossom } from '#services/blossom-upload.js'
|
|
10
10
|
import { uploadBinaryDataChunks, throttledSendEvent } from '#services/irfs-upload.js'
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// TL;DR
|
|
13
|
+
// import publishApp from 'nappup'
|
|
14
|
+
// await publishApp(
|
|
15
|
+
// fileList,
|
|
16
|
+
// window.nostr,
|
|
17
|
+
// { dTagRaw: 'My app identifier unique to this nsec', onEvent: ({ progress }) => console.log(progress) }
|
|
18
|
+
// )
|
|
19
|
+
//
|
|
20
|
+
// Simple usage -> onEvent: ({ progress, error }) => { if (error) { throw error } else { progressBar.style.width = `${progress}%` } }
|
|
21
|
+
// Geek usage ->
|
|
22
|
+
// onEvent: (event) => {
|
|
23
|
+
// if (event.type === 'file-uploaded') console.log(`Uploaded ${event.filename} via ${event.service}`)
|
|
24
|
+
// if (event.type === 'complete') console.log(`Done! Access at https://44billion.net/${event.naddr}`)
|
|
25
|
+
// if (event.type === 'error') console.error('Error during publishing:', event.error)
|
|
26
|
+
// }
|
|
27
|
+
//
|
|
28
|
+
export default async function (fileList, nostrSigner, opts = {}) {
|
|
29
|
+
const onEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null
|
|
30
|
+
let lastProgress = 0
|
|
13
31
|
try {
|
|
14
|
-
return await toApp(
|
|
32
|
+
return await toApp(fileList, nostrSigner, onEvent
|
|
33
|
+
? {
|
|
34
|
+
...opts,
|
|
35
|
+
onEvent (event) {
|
|
36
|
+
lastProgress = event.progress ?? lastProgress
|
|
37
|
+
onEvent(event)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
: opts)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (onEvent) try { onEvent({ type: 'error', error: err, progress: lastProgress }) } catch (_) {}
|
|
43
|
+
throw err
|
|
15
44
|
} finally {
|
|
16
45
|
await nostrRelays.disconnectAll()
|
|
17
46
|
}
|
|
18
47
|
}
|
|
19
48
|
|
|
20
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Publishes an app to Nostr relays and/or blossom servers.
|
|
51
|
+
*
|
|
52
|
+
* The optional `onEvent` callback receives structured progress events.
|
|
53
|
+
* Every event has `type` (string) and `progress` (0–100 integer).
|
|
54
|
+
*
|
|
55
|
+
* Event types:
|
|
56
|
+
* 'init' — { totalFiles, totalSteps, dTag, relayCount, blossomCount }
|
|
57
|
+
* 'icon-uploaded' — { service: 'blossom'|'irfs'|null }
|
|
58
|
+
* 'file-uploaded' — { filename, service: 'blossom'|'irfs' }
|
|
59
|
+
* 'stall-published' — listing metadata published
|
|
60
|
+
* 'bundle-published' — app bundle published
|
|
61
|
+
* 'complete' — { napp } (terminal, progress === 100)
|
|
62
|
+
* 'error' — { error } (terminal, error is rethrown)
|
|
63
|
+
*
|
|
64
|
+
* Terminal events ('complete' or 'error') signal that no more events will follow.
|
|
65
|
+
* The 'error' event is only emitted when using the default export wrapper.
|
|
66
|
+
* Direct `toApp` callers receive the thrown error via normal async/await.
|
|
67
|
+
*/
|
|
68
|
+
export async function toApp (fileList, nostrSigner, { log = () => {}, onEvent = () => {}, dTag, dTagRaw, channel = 'main', shouldReupload = false } = {}) {
|
|
69
|
+
let _steps = 0
|
|
70
|
+
let _totalSteps = 1
|
|
71
|
+
const emit = (event) => { try { onEvent({ ...event, progress: event.type === 'complete' ? 100 : Math.round((_steps / _totalSteps) * 100) }) } catch (_) {} }
|
|
21
72
|
if (!nostrSigner && typeof window !== 'undefined') nostrSigner = window.nostr
|
|
22
73
|
if (!nostrSigner) throw new Error('No Nostr signer found')
|
|
23
74
|
if (typeof window !== 'undefined' && nostrSigner === window.nostr) {
|
|
@@ -82,6 +133,10 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
82
133
|
|
|
83
134
|
const useBlossom = healthyBlossomServers.length > 0
|
|
84
135
|
|
|
136
|
+
const hasIconUpload = Boolean(nappJson.stallIcon?.[0]?.[0])
|
|
137
|
+
_totalSteps = fileList.length + (hasIconUpload ? 1 : 0) + 2
|
|
138
|
+
emit({ type: 'init', totalFiles: fileList.length, totalSteps: _totalSteps, dTag, relayCount: writeRelays.length, blossomCount: healthyBlossomServers.length })
|
|
139
|
+
|
|
85
140
|
// Upload icon from napp.json if present
|
|
86
141
|
if (nappJson.stallIcon?.[0]?.[0]) {
|
|
87
142
|
try {
|
|
@@ -106,7 +161,7 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
106
161
|
iconMetadata = {
|
|
107
162
|
rootHash: uploadedFiles[0].sha256,
|
|
108
163
|
mimeType,
|
|
109
|
-
service: 'blossom
|
|
164
|
+
service: 'b' // blossom
|
|
110
165
|
}
|
|
111
166
|
} else if (failedFiles.length > 0) {
|
|
112
167
|
log('Blossom icon upload failed, falling back to relay upload')
|
|
@@ -127,7 +182,7 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
127
182
|
iconMetadata = {
|
|
128
183
|
rootHash: nmmr.getRoot(),
|
|
129
184
|
mimeType,
|
|
130
|
-
service: 'irfs
|
|
185
|
+
service: 'i' // relay (irfs)
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
188
|
}
|
|
@@ -136,6 +191,11 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
136
191
|
}
|
|
137
192
|
}
|
|
138
193
|
|
|
194
|
+
if (hasIconUpload) {
|
|
195
|
+
_steps++
|
|
196
|
+
emit({ type: 'icon-uploaded', service: iconMetadata?.service === 'b' ? 'blossom' : iconMetadata ? 'irfs' : null })
|
|
197
|
+
}
|
|
198
|
+
|
|
139
199
|
log(`Processing ${fileList.length} files`)
|
|
140
200
|
|
|
141
201
|
// Files to upload via relay (irfs) — either all files or blossom failures
|
|
@@ -155,16 +215,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
155
215
|
rootHash: uploaded.sha256,
|
|
156
216
|
filename: uploaded.filename,
|
|
157
217
|
mimeType: uploaded.mimeType,
|
|
158
|
-
service: '
|
|
218
|
+
service: 'b'
|
|
159
219
|
})
|
|
160
220
|
|
|
161
221
|
if (faviconFile && uploaded.file === faviconFile) {
|
|
162
222
|
iconMetadata = {
|
|
163
223
|
rootHash: uploaded.sha256,
|
|
164
224
|
mimeType: uploaded.mimeType,
|
|
165
|
-
service: '
|
|
225
|
+
service: 'b'
|
|
166
226
|
}
|
|
167
227
|
}
|
|
228
|
+
|
|
229
|
+
_steps++
|
|
230
|
+
emit({ type: 'file-uploaded', filename: uploaded.filename, service: 'blossom' })
|
|
168
231
|
}
|
|
169
232
|
|
|
170
233
|
if (failedFiles.length > 0) {
|
|
@@ -193,16 +256,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
193
256
|
rootHash: nmmr.getRoot(),
|
|
194
257
|
filename,
|
|
195
258
|
mimeType: file.type || 'application/octet-stream',
|
|
196
|
-
service: '
|
|
259
|
+
service: 'i'
|
|
197
260
|
})
|
|
198
261
|
|
|
199
262
|
if (faviconFile && file === faviconFile) {
|
|
200
263
|
iconMetadata = {
|
|
201
264
|
rootHash: nmmr.getRoot(),
|
|
202
265
|
mimeType: file.type || 'application/octet-stream',
|
|
203
|
-
service: '
|
|
266
|
+
service: 'i'
|
|
204
267
|
}
|
|
205
268
|
}
|
|
269
|
+
|
|
270
|
+
_steps++
|
|
271
|
+
emit({ type: 'file-uploaded', filename, service: 'irfs' })
|
|
206
272
|
}
|
|
207
273
|
}
|
|
208
274
|
|
|
@@ -228,6 +294,8 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
228
294
|
categories: nappJson.category,
|
|
229
295
|
hashtags: nappJson.hashtag
|
|
230
296
|
})))
|
|
297
|
+
_steps++
|
|
298
|
+
emit({ type: 'stall-published' })
|
|
231
299
|
|
|
232
300
|
log(`Uploading bundle ${dTag}`)
|
|
233
301
|
const bundle = await uploadBundle({ dTag, channel, fileMetadata, signer: nostrSigner, pause, shouldReupload, log })
|
|
@@ -238,7 +306,11 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
|
|
|
238
306
|
relays: [],
|
|
239
307
|
kind: bundle.kind
|
|
240
308
|
})
|
|
309
|
+
_steps++
|
|
310
|
+
emit({ type: 'bundle-published' })
|
|
311
|
+
|
|
241
312
|
log(`Visit at https://44billion.net/${appEntity}`)
|
|
313
|
+
emit({ type: 'complete', napp: appEntity })
|
|
242
314
|
}
|
|
243
315
|
|
|
244
316
|
async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, shouldReupload = false, log = () => {} }) {
|
|
@@ -248,7 +320,7 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
|
|
|
248
320
|
draft: 37450 // vibe coded preview
|
|
249
321
|
}[channel] ?? 37448
|
|
250
322
|
|
|
251
|
-
const fileTags = fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType, v.service || '
|
|
323
|
+
const fileTags = fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType, v.service || 'i'])
|
|
252
324
|
const tags = [
|
|
253
325
|
['d', dTag],
|
|
254
326
|
...fileTags
|
|
@@ -285,7 +357,7 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
|
|
|
285
357
|
|
|
286
358
|
const isSame = currentFileTags.length === recentFileTags.length && currentFileTags.every((t, i) => {
|
|
287
359
|
const rt = recentFileTags[i]
|
|
288
|
-
return rt.length >= 4 && rt[1] === t[1] && rt[2] === t[2] && rt[3] === t[3] && (rt[4] || '
|
|
360
|
+
return rt.length >= 4 && rt[1] === t[1] && rt[2] === t[2] && rt[3] === t[3] && (rt[4] || 'i') === (t[4] || 'i')
|
|
289
361
|
})
|
|
290
362
|
|
|
291
363
|
if (isSame) {
|
|
@@ -346,7 +418,7 @@ async function maybeUploadStall ({
|
|
|
346
418
|
const trimmedSummary = typeof summary === 'string' ? summary.trim() : ''
|
|
347
419
|
const iconRootHash = icon?.rootHash
|
|
348
420
|
const iconMimeType = icon?.mimeType
|
|
349
|
-
const iconService = icon?.service || '
|
|
421
|
+
const iconService = icon?.service || 'i'
|
|
350
422
|
const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash) ||
|
|
351
423
|
Boolean(self) || (countries && countries.length > 0) || (categories && categories.length > 0) || (hashtags && hashtags.length > 0)
|
|
352
424
|
|