nappup 1.5.2 → 1.5.3

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "1.5.2",
9
+ "version": "1.5.3",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
package/src/index.js CHANGED
@@ -165,13 +165,19 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, dTag
165
165
 
166
166
  async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, log, pause = 0, mimeType, shouldReupload = false }) {
167
167
  const writeRelays = (await signer.getRelays()).write
168
+ const relays = [...new Set([...writeRelays, ...nappRelays])]
168
169
  let chunkIndex = 0
169
170
  for await (const chunk of nmmr.getChunks()) {
170
171
  const dTag = chunk.x
171
172
  const currentCtag = `${chunk.rootX}:${chunk.index}`
172
- const { otherCtags, hasCurrentCtag } = await getPreviousCtags(dTag, currentCtag, writeRelays, signer)
173
+ const { otherCtags, hasCurrentCtag, foundEvent, missingRelays } = await getPreviousCtags(dTag, currentCtag, relays, signer)
173
174
  if (!shouldReupload && hasCurrentCtag) {
174
- log(`${filename}: Skipping chunk ${++chunkIndex} of ${chunkLength} (already uploaded)`)
175
+ if (missingRelays.length === 0) {
176
+ log(`${filename}: Skipping chunk ${++chunkIndex} of ${chunkLength} (already uploaded)`)
177
+ continue
178
+ }
179
+ log(`${filename}: Re-uploading chunk ${++chunkIndex} of ${chunkLength} to ${missingRelays.length} missing relays (out of ${relays.length})`)
180
+ ;({ pause } = (await throttledSendEvent(foundEvent, missingRelays, { pause, log, trailingPause: true })))
175
181
  continue
176
182
  }
177
183
  const binaryDataChunk = {
@@ -188,7 +194,6 @@ async function uploadBinaryDataChunks ({ nmmr, signer, filename, chunkLength, lo
188
194
  }
189
195
 
190
196
  const event = await signer.signEvent(binaryDataChunk)
191
- const relays = [...new Set([...writeRelays, ...nappRelays])]
192
197
  const fallbackRelayCount = relays.length - writeRelays.length
193
198
  log(`${filename}: Uploading file part ${++chunkIndex} of ${chunkLength} to ${writeRelays.length} relays${fallbackRelayCount > 0 ? ` (+${fallbackRelayCount} fallback)` : ''}`)
194
199
  ;({ pause } = (await throttledSendEvent(event, relays, { pause, log, trailingPause: true })))
@@ -241,20 +246,24 @@ async function throttledSendEvent (event, relays, {
241
246
  })
242
247
  }
243
248
 
244
- async function getPreviousCtags (dTagValue, currentCtagValue, writeRelays, signer) {
249
+ async function getPreviousCtags (dTagValue, currentCtagValue, relays, signer) {
250
+ const targetRelays = [...new Set([...relays, ...nappRelays])]
245
251
  const storedEvents = (await nostrRelays.getEvents({
246
252
  kinds: [34600],
247
253
  authors: [await signer.getPublicKey()],
248
254
  '#d': [dTagValue],
249
255
  limit: 1
250
- }, [...new Set([...writeRelays, ...nappRelays])])).result
256
+ }, targetRelays)).result
251
257
 
252
258
  let hasCurrentCtag = false
253
259
  const hasEvent = storedEvents.length > 0
254
260
  if (!hasEvent) return { otherCtags: [], hasEvent, hasCurrentCtag }
255
261
 
256
262
  const cTagValues = { [currentCtagValue]: true }
257
- const prevTags = storedEvents.sort((a, b) => b.created_at - a.created_at)[0].tags
263
+ storedEvents.sort((a, b) => b.created_at - a.created_at)
264
+ const bestEvent = storedEvents[0]
265
+ const prevTags = bestEvent.tags
266
+
258
267
  if (!Array.isArray(prevTags)) return { otherCtags: [], hasEvent, hasCurrentCtag }
259
268
 
260
269
  hasCurrentCtag = prevTags.some(tag =>
@@ -277,7 +286,11 @@ async function getPreviousCtags (dTagValue, currentCtagValue, writeRelays, signe
277
286
  return isCTag && isntDuplicate
278
287
  })
279
288
 
280
- return { otherCtags, hasEvent, hasCurrentCtag }
289
+ const matchingEvents = storedEvents.filter(e => e.id === bestEvent.id)
290
+ const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
291
+ const missingRelays = targetRelays.filter(r => !coveredRelays.has(r))
292
+
293
+ return { otherCtags, hasEvent, hasCurrentCtag, foundEvent: bestEvent, missingRelays }
281
294
  }
282
295
 
283
296
  async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, shouldReupload = false, log = () => {} }) {
@@ -299,7 +312,8 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
299
312
  const events = (await nostrRelays.getEvents({
300
313
  kinds: [kind],
301
314
  authors: [await signer.getPublicKey()],
302
- '#d': [dTag]
315
+ '#d': [dTag],
316
+ limit: 1
303
317
  }, writeRelays)).result
304
318
 
305
319
  if (events.length > 0) {
@@ -319,13 +333,18 @@ async function uploadBundle ({ dTag, channel, fileMetadata, signer, pause = 0, s
319
333
  })
320
334
 
321
335
  if (isSame) {
322
- log(`Bundle based on ${fileTags.length} files is up to date (id: ${mostRecentEvent.id} - created_at: ${new Date(mostRecentEvent.created_at * 1000).toISOString()})`)
323
- if (events.length === writeRelays.length && events.every(e => e.id === mostRecentEvent.id)) return mostRecentEvent
336
+ log(`Bundle based on ${fileTags.length} files is up-to-date (id: ${mostRecentEvent.id} - created_at: ${new Date(mostRecentEvent.created_at * 1000).toISOString()})`)
337
+
338
+ const matchingEvents = events.filter(e => e.id === mostRecentEvent.id)
339
+ const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
340
+ const missingRelays = writeRelays.filter(r => !coveredRelays.has(r))
341
+
342
+ if (missingRelays.length === 0) return mostRecentEvent
324
343
 
325
344
  // nostrRelays.getEvents currently doesn't tell us which event came from which relay,
326
345
  // so we re-upload to all relays to ensure consistency
327
- log(`Re-uploading existing bundle event to all ${writeRelays.length} relays`)
328
- await throttledSendEvent(mostRecentEvent, writeRelays, { pause, trailingPause: true, log })
346
+ log(`Re-uploading existing bundle event to ${missingRelays.length} missing relays (out of ${writeRelays.length})`)
347
+ await throttledSendEvent(mostRecentEvent, missingRelays, { pause, trailingPause: true, log })
329
348
  return mostRecentEvent
330
349
  }
331
350
  }
@@ -369,12 +388,14 @@ async function maybeUploadStall ({
369
388
  const hasMetadata = Boolean(trimmedName) || Boolean(trimmedSummary) || Boolean(iconRootHash) ||
370
389
  Boolean(self) || (countries && countries.length > 0) || (categories && categories.length > 0) || (hashtags && hashtags.length > 0)
371
390
 
372
- const previous = await getPreviousStall(dTag, writeRelays, signer, channel)
391
+ const relays = [...new Set([...writeRelays, ...nappRelays])]
392
+
393
+ const previousResult = await getPreviousStall(dTag, relays, signer, channel)
394
+ const previous = previousResult?.previous
373
395
  if (!previous && !hasMetadata) return { pause }
374
396
 
375
397
  const publishStall = async (event) => {
376
398
  const signedEvent = await signer.signEvent(event)
377
- const relays = [...new Set([...writeRelays, ...nappRelays])]
378
399
  return await throttledSendEvent(signedEvent, relays, { pause, log, trailingPause: true })
379
400
  }
380
401
 
@@ -600,7 +621,18 @@ async function maybeUploadStall ({
600
621
  }
601
622
  }
602
623
 
603
- if (!changed) return { pause }
624
+ if (!changed) {
625
+ const { storedEvents } = previousResult
626
+
627
+ const matchingEvents = storedEvents.filter(e => e.id === previous.id)
628
+ const coveredRelays = new Set(matchingEvents.map(e => e.meta?.relay).filter(Boolean))
629
+ const missingRelays = relays.filter(r => !coveredRelays.has(r))
630
+
631
+ if (missingRelays.length === 0) return { pause }
632
+
633
+ log(`Re-uploading existing stall event to ${missingRelays.length} missing relays (out of ${relays.length})`)
634
+ return await throttledSendEvent(previous, missingRelays, { pause, log, trailingPause: true })
635
+ }
604
636
 
605
637
  return await publishStall({
606
638
  kind,
@@ -610,7 +642,7 @@ async function maybeUploadStall ({
610
642
  })
611
643
  }
612
644
 
613
- async function getPreviousStall (dTagValue, writeRelays, signer, channel) {
645
+ async function getPreviousStall (dTagValue, relays, signer, channel) {
614
646
  const kind = {
615
647
  main: 37348,
616
648
  next: 37349,
@@ -622,8 +654,15 @@ async function getPreviousStall (dTagValue, writeRelays, signer, channel) {
622
654
  authors: [await signer.getPublicKey()],
623
655
  '#d': [dTagValue],
624
656
  limit: 1
625
- }, [...new Set([...writeRelays, ...nappRelays])])).result
657
+ }, relays)).result
626
658
 
627
659
  if (storedEvents.length === 0) return null
628
- return storedEvents.sort((a, b) => b.created_at - a.created_at)[0]
660
+
661
+ storedEvents.sort((a, b) => b.created_at - a.created_at)
662
+
663
+ return {
664
+ previous: storedEvents[0],
665
+ storedEvents,
666
+ targetRelayCount: relays.length
667
+ }
629
668
  }
@@ -80,6 +80,7 @@ export class NostrRelays {
80
80
  const relay = await this.#getRelay(url)
81
81
  sub = relay.subscribe([filter], {
82
82
  onevent: (event) => {
83
+ event.meta = { relay: url }
83
84
  events.push(event)
84
85
  },
85
86
  onclose: err => {
@@ -115,6 +116,9 @@ export class NostrRelays {
115
116
 
116
117
  // Send an event to a list of relays
117
118
  async sendEvent (event, relays, timeout = 3000) {
119
+ const eventToSend = event.meta ? { ...event } : event
120
+ if (eventToSend.meta) delete eventToSend.meta
121
+
118
122
  const promises = relays.map(async (url) => {
119
123
  let timer
120
124
  const p = Promise.withResolvers()
@@ -124,7 +128,7 @@ export class NostrRelays {
124
128
  }, timeout))
125
129
 
126
130
  const relay = await this.#getRelay(url)
127
- await relay.publish(event)
131
+ await relay.publish(eventToSend)
128
132
  p.resolve()
129
133
  } catch (err) {
130
134
  if (err.message?.startsWith('duplicate:')) return p.resolve()