libp2r2p 0.10.18 → 0.10.20

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.
@@ -1,7 +1,8 @@
1
- import { getEventHash, isSerializableEvent, isValidEvent } from '../event/index.js'
1
+ import { isValidEvent } from '../event/index.js'
2
2
  import { ValidationError } from '../error/index.js'
3
3
  import { generateKeypair } from '../key/index.js'
4
4
  import * as privateChannel from '../private-channel/index.js'
5
+ import { normalizeRumor, deliveryInfo } from '../private-channel/helpers/rumor.js'
5
6
 
6
7
  export const ASK_KIND = 7329
7
8
  export const REPLY_KIND = 7330
@@ -11,10 +12,6 @@ const RESUBSCRIBE_GRACE_MS = 500
11
12
  const PRIVATE_MESSAGE_KINDS = [ASK_KIND, REPLY_KIND, TELL_KIND]
12
13
  const HEX_PUBKEY = /^[0-9a-f]{64}$/i
13
14
 
14
- const watchesByChannel = new Map()
15
- const subsByRelay = new Map()
16
- let nextWatchRevision = 1
17
-
18
15
  function nowSeconds () {
19
16
  return Math.floor(Date.now() / 1000)
20
17
  }
@@ -120,25 +117,15 @@ function cloneTags (tags) {
120
117
  async function makeOutgoingRumor ({ senderSigner, rumor }) {
121
118
  if (!senderSigner?.getPublicKey) throw new ValidationError('SENDER_SIGNER_REQUIRED')
122
119
  const senderPubkey = await senderSigner.getPublicKey()
123
- // This is what gets sent. Id and pubkey are added later by recipient.
124
- const wireEvent = {
125
- kind: rumor.kind,
126
- tags: cloneTags(rumor.tags),
127
- content: rumor.content,
128
- created_at: rumor.created_at !== undefined
129
- ? rumor.created_at
130
- : nowSeconds()
120
+ if (rumor.pubkey !== undefined && rumor.pubkey !== senderPubkey && rumor.created_at === undefined) {
121
+ throw new ValidationError('FORWARDED_RUMOR_TIMESTAMP_REQUIRED')
131
122
  }
132
- const event = normalizeRumor(wireEvent, senderPubkey)
123
+ const event = normalizeRumor({ ...rumor, tags: cloneTags(rumor.tags), created_at: rumor.created_at === undefined ? nowSeconds() : rumor.created_at }, senderPubkey)
124
+ const { id: _, pubkey, ...template } = event
125
+ const wireEvent = pubkey === senderPubkey ? template : { ...template, pubkey }
133
126
  return { event, wireEvent }
134
127
  }
135
128
 
136
- function normalizeRumor (event, pubkey) {
137
- const normalized = { ...event, pubkey }
138
- if (!isSerializableEvent(normalized)) throw new ValidationError('INVALID_RUMOR')
139
- return { ...normalized, id: getEventHash(normalized) }
140
- }
141
-
142
129
  function assertValidSignedEvent (event) {
143
130
  if (!isValidEvent(event)) {
144
131
  throw new ValidationError('INVALID_SIGNED_EVENT')
@@ -155,308 +142,364 @@ async function ownPrivateChannelPubkey (signer) {
155
142
  return signer.getPublicKey()
156
143
  }
157
144
 
158
- function assertWatching (channelPubkey) {
159
- if (!watchesByChannel.has(channelPubkey)) throw new Error('PRIVATE_MESSAGE_NOT_WATCHING')
160
- }
145
+ // A session owns its callbacks and relay reads. Other consumers, including
146
+ // the other side of the same shared channel, cannot overwrite or stop them.
147
+ export function createPrivateMessageSession () {
148
+ const watchesByChannel = new Map()
149
+ const subsByRelay = new Map()
150
+ let nextWatchRevision = 1
151
+ let requestSequence = 0
152
+ const watchRequests = new Map()
153
+ let identity
154
+ const receivedChunkScope = globalThis.crypto.randomUUID()
155
+ let subscribe = privateChannel.subscribe
156
+ function assertWatching (channelPubkey) {
157
+ if (!watchesByChannel.has(channelPubkey)) throw new Error('PRIVATE_MESSAGE_NOT_WATCHING')
158
+ }
161
159
 
162
- function watchCallbacks (channelPubkey) {
163
- return watchesByChannel.get(channelPubkey)?.callbacks || {}
164
- }
160
+ function watchCallbacks (channelPubkey) {
161
+ return watchesByChannel.get(channelPubkey)?.callbacks || {}
162
+ }
165
163
 
166
- function dispatchWatchedEvent (event, outer, meta) {
167
- const callbacks = watchCallbacks(meta.channelPubkey)
168
- const payload = parseRumorContent(event)
169
- const message = { event, outer, meta, payload }
164
+ async function dispatchWatchedEvent (event, outer, meta) {
165
+ const callbacks = watchCallbacks(meta.channelPubkey)
166
+ const payload = parseRumorContent(event)
167
+ const info = deliveryInfo(event, meta.senderPubkey ?? meta.router?.tags?.find(tag => tag[0] === 'f')?.[1])
168
+ const message = { event, outer, meta, payload, ...info }
169
+ if (info.provenance === 'hearsay' || (info.senderPubkey && info.senderPubkey !== event.pubkey)) {
170
+ await callbacks.onMessage?.(message)
171
+ return
172
+ }
170
173
 
171
- if (event.kind === ASK_KIND) {
172
- callbacks.onAsk?.({ ...message, question: event })
173
- } else if (event.kind === REPLY_KIND) {
174
- const questionId = readTag(event, 'q')
175
- callbacks.onReply?.({ ...message, questionId, reply: event })
176
- } else if (event.kind === TELL_KIND) {
177
- const receiverTag = readTag(event, 'r')
178
- if (receiverTag) callbacks.onTell?.({ ...message, tell: event })
179
- else callbacks.onYell?.({ ...message, yell: event })
180
- }
174
+ if (event.kind === ASK_KIND) {
175
+ await callbacks.onAsk?.({ ...message, question: event })
176
+ } else if (event.kind === REPLY_KIND) {
177
+ const questionId = readTag(event, 'q')
178
+ await callbacks.onReply?.({ ...message, questionId, reply: event })
179
+ } else if (event.kind === TELL_KIND) {
180
+ const receiverTag = readTag(event, 'r')
181
+ if (receiverTag) await callbacks.onTell?.({ ...message, tell: event })
182
+ else await callbacks.onYell?.({ ...message, yell: event })
183
+ }
181
184
 
182
- callbacks.onMessage?.(message)
183
- }
185
+ await callbacks.onMessage?.(message)
186
+ }
184
187
 
185
- function dispatchWatchedNymEvent (event, outer, meta) {
186
- const callbacks = watchCallbacks(meta.channelPubkey)
187
- callbacks.onNym?.({
188
- event,
189
- outer,
190
- meta,
191
- payload: parseRumorContent(event),
192
- nym: event
193
- })
194
- }
188
+ async function dispatchWatchedNymEvent (event, outer, meta) {
189
+ const callbacks = watchCallbacks(meta.channelPubkey)
190
+ await callbacks.onNym?.({
191
+ event,
192
+ outer,
193
+ meta,
194
+ payload: parseRumorContent(event),
195
+ ...deliveryInfo(event, meta.senderPubkey ?? meta.carrier?.pubkey ?? event.pubkey),
196
+ nym: event
197
+ })
198
+ }
195
199
 
196
- function dispatchSeedEvent (seed) {
197
- watchCallbacks(seed.channelPubkey).onSeed?.(seed)
198
- }
200
+ function dispatchSeedEvent (seed) {
201
+ return watchCallbacks(seed.channelPubkey).onSeed?.(seed)
202
+ }
199
203
 
200
- function dispatchContentKeyUsage (usage) {
201
- watchCallbacks(usage.channelPubkey).onContentKeyUsage?.(usage)
202
- }
204
+ function dispatchContentKeyUsage (usage) {
205
+ watchCallbacks(usage.channelPubkey).onContentKeyUsage?.(usage)
206
+ }
203
207
 
204
- function handleChunk (chunk) {
205
- watchCallbacks(chunk.channelPubkey).onChunk?.(chunk)
206
- }
208
+ function handleChunk (chunk) {
209
+ watchCallbacks(chunk.channelPubkey).onChunk?.(chunk)
210
+ }
207
211
 
208
- function desiredRelayState () {
209
- const relayToChannels = new Map()
210
- for (const [channelPubkey, watch] of watchesByChannel) {
211
- for (const relay of watch.relays) {
212
- if (!relayToChannels.has(relay)) relayToChannels.set(relay, new Set())
213
- relayToChannels.get(relay).add(channelPubkey)
212
+ function desiredRelayState () {
213
+ const relayToChannels = new Map()
214
+ for (const [channelPubkey, watch] of watchesByChannel) {
215
+ for (const relay of watch.relays) {
216
+ if (!relayToChannels.has(relay)) relayToChannels.set(relay, new Set())
217
+ relayToChannels.get(relay).add(channelPubkey)
218
+ }
214
219
  }
220
+ return relayToChannels
215
221
  }
216
- return relayToChannels
217
- }
218
222
 
219
- function signersForChannels (channels) {
220
- const out = {}
221
- for (const channel of channels) {
222
- const signer = watchesByChannel.get(channel)?.privateChannelSigner
223
- if (signer) out[channel] = signer
223
+ function signersForChannels (channels) {
224
+ const out = {}
225
+ for (const channel of channels) {
226
+ const signer = watchesByChannel.get(channel)?.privateChannelSigner
227
+ if (signer) out[channel] = signer
228
+ }
229
+ return out
224
230
  }
225
- return out
226
- }
227
231
 
228
- function readerSignersForChannels (channels) {
229
- const out = {}
230
- for (const channel of channels) {
231
- const signer = watchesByChannel.get(channel)?.privateChannelReaderSigner
232
- if (signer) out[channel] = signer
232
+ function readerSignersForChannels (channels) {
233
+ const out = {}
234
+ for (const channel of channels) {
235
+ const signer = watchesByChannel.get(channel)?.privateChannelReaderSigner
236
+ if (signer) out[channel] = signer
237
+ }
238
+ return out
233
239
  }
234
- return out
235
- }
236
240
 
237
- function readerPubkeysForChannels (channels) {
238
- const out = {}
239
- for (const channel of channels) {
240
- const pubkey = watchesByChannel.get(channel)?.privateChannelReaderPubkey
241
- if (pubkey) out[channel] = pubkey
241
+ function readerPubkeysForChannels (channels) {
242
+ const out = {}
243
+ for (const channel of channels) {
244
+ const pubkey = watchesByChannel.get(channel)?.privateChannelReaderPubkey
245
+ if (pubkey) out[channel] = pubkey
246
+ }
247
+ return out
242
248
  }
243
- return out
244
- }
245
249
 
246
- function modesForChannels (channels) {
247
- const out = {}
248
- for (const channel of channels) out[channel] = watchesByChannel.get(channel)?.mode || 'leecher'
249
- return out
250
- }
251
-
252
- function maxWatchNumber (channels, field) {
253
- const values = channels
254
- .map(channel => watchesByChannel.get(channel)?.[field])
255
- .filter(value => Number.isFinite(value))
256
- return values.length ? Math.max(...values) : undefined
257
- }
250
+ function modesForChannels (channels) {
251
+ const out = {}
252
+ for (const channel of channels) out[channel] = watchesByChannel.get(channel)?.mode || 'leecher'
253
+ return out
254
+ }
258
255
 
259
- function watchNumbersForChannels (channels, field) {
260
- const out = {}
261
- for (const channel of channels) {
262
- const value = watchesByChannel.get(channel)?.[field]
263
- if (Number.isFinite(value)) out[channel] = value
256
+ function maxWatchNumber (channels, field) {
257
+ const values = channels
258
+ .map(channel => watchesByChannel.get(channel)?.[field])
259
+ .filter(value => Number.isFinite(value))
260
+ return values.length ? Math.max(...values) : undefined
264
261
  }
265
- return out
266
- }
267
262
 
268
- function watchRevisionsForChannels (channels) {
269
- return Object.fromEntries(channels.map(channel => [channel, watchesByChannel.get(channel)?.revision || 0]))
270
- }
263
+ function watchNumbersForChannels (channels, field) {
264
+ const out = {}
265
+ for (const channel of channels) {
266
+ const value = watchesByChannel.get(channel)?.[field]
267
+ if (Number.isFinite(value)) out[channel] = value
268
+ }
269
+ return out
270
+ }
271
271
 
272
- function doesSubscriptionMatch (current, channels) {
273
- if (!current || !areSetsEqual(current.channels, channels)) return false
274
- for (const channel of channels) {
275
- if (current.revisions?.[channel] !== watchesByChannel.get(channel)?.revision) return false
272
+ function watchRevisionsForChannels (channels) {
273
+ return Object.fromEntries(channels.map(channel => [channel, watchesByChannel.get(channel)?.revision || 0]))
276
274
  }
277
- return true
278
- }
279
275
 
280
- function firstWatchValue (channels, field) {
281
- for (const channel of channels) {
282
- const value = watchesByChannel.get(channel)?.[field]
283
- if (value !== undefined) return value
276
+ function doesSubscriptionMatch (current, channels) {
277
+ if (!current || !areSetsEqual(current.channels, channels)) return false
278
+ for (const channel of channels) {
279
+ if (current.revisions?.[channel] !== watchesByChannel.get(channel)?.revision) return false
280
+ }
281
+ return true
284
282
  }
285
- return undefined
286
- }
287
283
 
288
- function closeSubscription (sub, gracefulClose) {
289
- if (gracefulClose) {
290
- setTimeout(() => Promise.resolve().then(() => sub.close()).catch(() => {}), RESUBSCRIBE_GRACE_MS)
291
- return null
284
+ function firstWatchValue (channels, field) {
285
+ for (const channel of channels) {
286
+ const value = watchesByChannel.get(channel)?.[field]
287
+ if (value !== undefined) return value
288
+ }
289
+ return undefined
292
290
  }
293
- try {
294
- return Promise.resolve(sub.close())
295
- } catch (err) {
296
- return Promise.reject(err)
291
+
292
+ function closeSubscription (sub, gracefulClose) {
293
+ if (gracefulClose) {
294
+ setTimeout(() => Promise.resolve().then(() => sub.close()).catch(() => {}), RESUBSCRIBE_GRACE_MS)
295
+ return null
296
+ }
297
+ try {
298
+ return Promise.resolve(sub.close())
299
+ } catch (err) {
300
+ return Promise.reject(err)
301
+ }
297
302
  }
298
- }
299
303
 
300
- function rebuildSubscriptions ({ _subscribe = privateChannel.subscribe, gracefulClose = true } = {}) {
301
- const desired = desiredRelayState()
302
- const closing = []
304
+ function rebuildSubscriptions ({ _subscribe = privateChannel.subscribe, gracefulClose = true } = {}) {
305
+ subscribe = _subscribe === privateChannel.subscribe ? subscribe : _subscribe
306
+ _subscribe = subscribe
307
+ const desired = desiredRelayState()
308
+ const closing = []
309
+
310
+ for (const [relay, current] of subsByRelay) {
311
+ const nextChannels = desired.get(relay)
312
+ if (nextChannels && doesSubscriptionMatch(current, nextChannels)) continue
313
+ if (!nextChannels) {
314
+ const close = closeSubscription(current.sub, gracefulClose)
315
+ if (close) closing.push(close)
316
+ subsByRelay.delete(relay)
317
+ }
318
+ }
303
319
 
304
- for (const [relay, current] of subsByRelay) {
305
- const nextChannels = desired.get(relay)
306
- if (nextChannels && doesSubscriptionMatch(current, nextChannels)) continue
307
- if (!nextChannels) {
308
- const close = closeSubscription(current.sub, gracefulClose)
309
- if (close) closing.push(close)
310
- subsByRelay.delete(relay)
320
+ for (const [relay, channels] of desired) {
321
+ const current = subsByRelay.get(relay)
322
+ if (doesSubscriptionMatch(current, channels)) continue
323
+
324
+ const channelList = [...channels]
325
+ const firstWatch = watchesByChannel.get(channelList[0])
326
+ const sub = _subscribe({
327
+ receiverSigner: firstWatch.receiverSigner,
328
+ iykcSigner: firstWatch.iykcSigner,
329
+ privateChannelSigner: firstWatch.privateChannelSigner,
330
+ privateChannelSignersByPubkey: signersForChannels(channelList),
331
+ privateChannelReaderSigner: firstWatch.privateChannelReaderSigner,
332
+ privateChannelReaderSignersByPubkey: readerSignersForChannels(channelList),
333
+ privateChannelReaderPubkey: firstWatch.privateChannelReaderPubkey,
334
+ privateChannelReaderPubkeysByPubkey: readerPubkeysForChannels(channelList),
335
+ privateChannelPubkeys: channelList,
336
+ receiverPubkey: firstWatch.receiverPubkey,
337
+ relays: [relay],
338
+ mode: firstWatch.mode,
339
+ modeByPubkey: modesForChannels(channelList),
340
+ receivedChunkScope,
341
+ receivedChunkTtlMs: maxWatchNumber(channelList, 'receivedChunkTtlMs'),
342
+ receivedChunkTtlMsByPubkey: watchNumbersForChannels(channelList, 'receivedChunkTtlMs'),
343
+ receivedChunkMaxBytes: maxWatchNumber(channelList, 'receivedChunkMaxBytes'),
344
+ receivedChunkIndexedDB: firstWatchValue(channelList, 'receivedChunkIndexedDB'),
345
+ ignoredGroupTtlMs: maxWatchNumber(channelList, 'ignoredGroupTtlMs'),
346
+ ignoredGroupMaxEntries: maxWatchNumber(channelList, 'ignoredGroupMaxEntries'),
347
+ limit: 0,
348
+ since: nowSeconds(),
349
+ liveOnly: true,
350
+ onChunk: handleChunk,
351
+ onEvent: (event, outer, meta) => {
352
+ return dispatchWatchedEvent(event, outer, meta)
353
+ },
354
+ onNymEvent: (event, outer, meta) => {
355
+ return dispatchWatchedNymEvent(event, outer, meta)
356
+ },
357
+ onSeedEvent: (seed) => {
358
+ return dispatchSeedEvent(seed)
359
+ },
360
+ onContentKeyUsage: dispatchContentKeyUsage,
361
+ onError: err => firstWatch.callbacks.onError?.(err)
362
+ })
363
+
364
+ subsByRelay.set(relay, {
365
+ channels: new Set(channels),
366
+ revisions: watchRevisionsForChannels(channelList),
367
+ sub
368
+ })
369
+ if (current) {
370
+ const close = closeSubscription(current.sub, gracefulClose)
371
+ if (close) closing.push(close)
372
+ }
311
373
  }
374
+ return Promise.allSettled(closing)
312
375
  }
313
376
 
314
- for (const [relay, channels] of desired) {
315
- const current = subsByRelay.get(relay)
316
- if (doesSubscriptionMatch(current, channels)) continue
317
-
318
- const channelList = [...channels]
319
- const firstWatch = watchesByChannel.get(channelList[0])
320
- const sub = _subscribe({
321
- receiverSigner: firstWatch.receiverSigner,
322
- iykcSigner: firstWatch.iykcSigner,
323
- privateChannelSigner: firstWatch.privateChannelSigner,
324
- privateChannelSignersByPubkey: signersForChannels(channelList),
325
- privateChannelReaderSigner: firstWatch.privateChannelReaderSigner,
326
- privateChannelReaderSignersByPubkey: readerSignersForChannels(channelList),
327
- privateChannelReaderPubkey: firstWatch.privateChannelReaderPubkey,
328
- privateChannelReaderPubkeysByPubkey: readerPubkeysForChannels(channelList),
329
- privateChannelPubkeys: channelList,
330
- receiverPubkey: firstWatch.receiverPubkey,
331
- relays: [relay],
332
- mode: firstWatch.mode,
333
- modeByPubkey: modesForChannels(channelList),
334
- receivedChunkTtlMs: maxWatchNumber(channelList, 'receivedChunkTtlMs'),
335
- receivedChunkTtlMsByPubkey: watchNumbersForChannels(channelList, 'receivedChunkTtlMs'),
336
- receivedChunkMaxBytes: maxWatchNumber(channelList, 'receivedChunkMaxBytes'),
337
- receivedChunkIndexedDB: firstWatchValue(channelList, 'receivedChunkIndexedDB'),
338
- ignoredGroupTtlMs: maxWatchNumber(channelList, 'ignoredGroupTtlMs'),
339
- ignoredGroupMaxEntries: maxWatchNumber(channelList, 'ignoredGroupMaxEntries'),
340
- limit: 0,
341
- since: nowSeconds(),
342
- liveOnly: true,
343
- onChunk: handleChunk,
344
- onEvent: (event, outer, meta) => {
345
- dispatchWatchedEvent(event, outer, meta)
346
- },
347
- onNymEvent: (event, outer, meta) => {
348
- dispatchWatchedNymEvent(event, outer, meta)
349
- },
350
- onSeedEvent: (seed) => {
351
- dispatchSeedEvent(seed)
352
- },
353
- onContentKeyUsage: dispatchContentKeyUsage,
354
- onError: err => firstWatch.callbacks.onError?.(err)
355
- })
377
+ async function watch ({
378
+ channels,
379
+ relays,
380
+ receiverSigner,
381
+ iykcSigner,
382
+ privateChannelSigner = receiverSigner,
383
+ privateChannelReaderSigner = privateChannelSigner,
384
+ privateChannelReaderPubkey,
385
+ receiverPubkey,
386
+ mode = 'leecher',
387
+ onAsk,
388
+ onReply,
389
+ onTell,
390
+ onYell,
391
+ onNym,
392
+ onMessage,
393
+ onSeed,
394
+ onChunk,
395
+ onContentKeyUsage,
396
+ onError,
397
+ receivedChunkTtlMs,
398
+ receivedChunkMaxBytes,
399
+ receivedChunkIndexedDB,
400
+ ignoredGroupTtlMs,
401
+ ignoredGroupMaxEntries,
402
+ since = nowSeconds(),
403
+ _subscribe = privateChannel.subscribe
404
+ }) {
405
+ if (!relays?.length) throw new ValidationError('NO_RELAYS')
406
+ const request = ++requestSequence
407
+ const channelList = uniq(channels?.length ? channels : [await ownPrivateChannelPubkey(privateChannelSigner)])
408
+ for (const channel of channelList) {
409
+ if ((watchRequests.get(channel) || 0) < request) watchRequests.set(channel, request)
410
+ }
411
+ const ownPubkey = receiverPubkey || await receiverSigner?.getPublicKey?.()
412
+ if (identity !== undefined && identity !== ownPubkey) throw new ValidationError('PRIVATE_MESSAGE_IDENTITY_MISMATCH')
413
+ identity = ownPubkey
414
+ const callbacks = { onAsk, onReply, onTell, onYell, onNym, onMessage, onSeed, onChunk, onContentKeyUsage, onError }
415
+
416
+ let changed = false
417
+ for (const channel of channelList) {
418
+ if (watchRequests.get(channel) !== request) continue
419
+ const next = {
420
+ request,
421
+ relays: uniq(relays),
422
+ receiverSigner,
423
+ iykcSigner,
424
+ privateChannelSigner,
425
+ privateChannelReaderSigner: privateChannelReaderSigner || privateChannelSigner,
426
+ privateChannelReaderPubkey,
427
+ receiverPubkey: ownPubkey,
428
+ mode,
429
+ receivedChunkTtlMs,
430
+ receivedChunkMaxBytes,
431
+ receivedChunkIndexedDB,
432
+ ignoredGroupTtlMs,
433
+ ignoredGroupMaxEntries,
434
+ callbacks,
435
+ since
436
+ }
437
+ const current = watchesByChannel.get(channel)
438
+ const areSettingsEqual = Boolean(
439
+ current &&
440
+ current.receiverSigner === next.receiverSigner &&
441
+ current.iykcSigner === next.iykcSigner &&
442
+ current.privateChannelSigner === next.privateChannelSigner &&
443
+ current.privateChannelReaderSigner === next.privateChannelReaderSigner &&
444
+ current.privateChannelReaderPubkey === next.privateChannelReaderPubkey &&
445
+ current.receiverPubkey === next.receiverPubkey &&
446
+ current.mode === next.mode &&
447
+ current.receivedChunkTtlMs === next.receivedChunkTtlMs &&
448
+ current.receivedChunkMaxBytes === next.receivedChunkMaxBytes &&
449
+ current.receivedChunkIndexedDB === next.receivedChunkIndexedDB &&
450
+ current.ignoredGroupTtlMs === next.ignoredGroupTtlMs &&
451
+ current.ignoredGroupMaxEntries === next.ignoredGroupMaxEntries
452
+ )
453
+ next.revision = areSettingsEqual ? current.revision : nextWatchRevision++
454
+ if (areSettingsEqual && areSetsEqual(new Set(current.relays), new Set(next.relays))) {
455
+ current.callbacks = callbacks
456
+ current.request = request
457
+ continue
458
+ }
459
+ watchesByChannel.set(channel, next)
460
+ changed = true
461
+ }
356
462
 
357
- subsByRelay.set(relay, {
358
- channels: new Set(channels),
359
- revisions: watchRevisionsForChannels(channelList),
360
- sub
361
- })
362
- if (current) {
363
- const close = closeSubscription(current.sub, gracefulClose)
364
- if (close) closing.push(close)
463
+ if (changed) await rebuildSubscriptions({ _subscribe })
464
+ return () => {
465
+ const owned = channelList.filter(channel => watchRequests.get(channel) === request)
466
+ return unwatch(owned)
365
467
  }
366
468
  }
367
- return Promise.allSettled(closing)
368
- }
369
469
 
370
- export async function watch ({
371
- channels,
372
- relays,
373
- receiverSigner,
374
- iykcSigner,
375
- privateChannelSigner = receiverSigner,
376
- privateChannelReaderSigner = privateChannelSigner,
377
- privateChannelReaderPubkey,
378
- receiverPubkey,
379
- mode = 'leecher',
380
- onAsk,
381
- onReply,
382
- onTell,
383
- onYell,
384
- onNym,
385
- onMessage,
386
- onSeed,
387
- onChunk,
388
- onContentKeyUsage,
389
- onError,
390
- receivedChunkTtlMs,
391
- receivedChunkMaxBytes,
392
- receivedChunkIndexedDB,
393
- ignoredGroupTtlMs,
394
- ignoredGroupMaxEntries,
395
- since = nowSeconds(),
396
- _subscribe = privateChannel.subscribe
397
- }) {
398
- if (!relays?.length) throw new ValidationError('NO_RELAYS')
399
- const channelList = uniq(channels?.length ? channels : [await ownPrivateChannelPubkey(privateChannelSigner)])
400
- const ownPubkey = receiverPubkey || await receiverSigner?.getPublicKey?.()
401
- const callbacks = { onAsk, onReply, onTell, onYell, onNym, onMessage, onSeed, onChunk, onContentKeyUsage, onError }
402
-
403
- let changed = false
404
- for (const channel of channelList) {
405
- const next = {
406
- relays: uniq(relays),
407
- receiverSigner,
408
- iykcSigner,
409
- privateChannelSigner,
410
- privateChannelReaderSigner: privateChannelReaderSigner || privateChannelSigner,
411
- privateChannelReaderPubkey,
412
- receiverPubkey: ownPubkey,
413
- mode,
414
- receivedChunkTtlMs,
415
- receivedChunkMaxBytes,
416
- receivedChunkIndexedDB,
417
- ignoredGroupTtlMs,
418
- ignoredGroupMaxEntries,
419
- callbacks,
420
- since
470
+ function unwatch (channels) {
471
+ const channelList = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...watchRequests.keys()]
472
+ for (const channel of channelList) {
473
+ watchRequests.set(channel, ++requestSequence)
474
+ watchesByChannel.delete(channel)
421
475
  }
422
- const current = watchesByChannel.get(channel)
423
- const areSettingsEqual = Boolean(
424
- current &&
425
- current.receiverSigner === next.receiverSigner &&
426
- current.iykcSigner === next.iykcSigner &&
427
- current.privateChannelSigner === next.privateChannelSigner &&
428
- current.privateChannelReaderSigner === next.privateChannelReaderSigner &&
429
- current.privateChannelReaderPubkey === next.privateChannelReaderPubkey &&
430
- current.receiverPubkey === next.receiverPubkey &&
431
- current.mode === next.mode &&
432
- current.receivedChunkTtlMs === next.receivedChunkTtlMs &&
433
- current.receivedChunkMaxBytes === next.receivedChunkMaxBytes &&
434
- current.receivedChunkIndexedDB === next.receivedChunkIndexedDB &&
435
- current.ignoredGroupTtlMs === next.ignoredGroupTtlMs &&
436
- current.ignoredGroupMaxEntries === next.ignoredGroupMaxEntries
437
- )
438
- next.revision = areSettingsEqual ? current.revision : nextWatchRevision++
439
- if (areSettingsEqual && areSetsEqual(new Set(current.relays), new Set(next.relays))) {
440
- current.callbacks = callbacks
441
- continue
442
- }
443
- watchesByChannel.set(channel, next)
444
- changed = true
476
+ return rebuildSubscriptions({ gracefulClose: false })
445
477
  }
446
478
 
447
- if (changed) await rebuildSubscriptions({ _subscribe })
448
- return () => unwatch(channelList)
449
- }
479
+ function clearChannelState (channelPubkey) {
480
+ if (watchesByChannel.has(channelPubkey)) return unwatch(channelPubkey)
481
+ return Promise.resolve([])
482
+ }
450
483
 
451
- export function unwatch (channels) {
452
- const channelList = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...watchesByChannel.keys()]
453
- for (const channel of channelList) watchesByChannel.delete(channel)
454
- return rebuildSubscriptions({ gracefulClose: false })
484
+ return {
485
+ watch, unwatch, clearChannelState,
486
+ ask: options => ask({ ...options, _assertWatching: assertWatching }),
487
+ reply, tell, yell, broadcastRumor, broadcastEvent, broadcastNymRumor, broadcastNymEvent
488
+ }
455
489
  }
456
490
 
457
- export function clearChannelState (channelPubkey) {
458
- if (watchesByChannel.has(channelPubkey)) return unwatch(channelPubkey)
459
- return Promise.resolve([])
491
+ const defaultSessions = new Map()
492
+ export async function watch (options) {
493
+ const identity = options.receiverPubkey || await options.receiverSigner?.getPublicKey?.() || ''
494
+ let session = defaultSessions.get(identity)
495
+ if (!session) defaultSessions.set(identity, (session = createPrivateMessageSession()))
496
+ return session.watch(options)
497
+ }
498
+ export async function unwatch (channels) {
499
+ await Promise.all([...defaultSessions.values()].map(session => session.unwatch(channels)))
500
+ }
501
+ export async function clearChannelState (channelPubkey) {
502
+ await Promise.all([...defaultSessions.values()].map(session => session.clearChannelState(channelPubkey)))
460
503
  }
461
504
 
462
505
  async function sendPrivateMessage ({
@@ -517,12 +560,19 @@ export async function ask ({
517
560
  deletionPubkey,
518
561
  deletionSeckey,
519
562
  autoDeletionCapability = true,
520
- _publish = privateChannel.publish
563
+ _publish = privateChannel.publish,
564
+ _assertWatching
521
565
  }) {
522
566
  if (!receiverPubkey) throw new ValidationError('RECEIVER_PUBKEY_REQUIRED')
523
567
  if (!privateChannelSigner?.getPublicKey) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
524
568
  const privateChannelPubkey = await ownPrivateChannelPubkey(privateChannelSigner)
525
- assertWatching(privateChannelPubkey)
569
+ if (_assertWatching) _assertWatching(privateChannelPubkey)
570
+ else {
571
+ const identity = await senderSigner?.getPublicKey?.()
572
+ const session = defaultSessions.get(identity)
573
+ if (!session) throw new Error('PRIVATE_MESSAGE_NOT_WATCHING')
574
+ return session.ask({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receiverPubkey, relays, relayToReceivers, recoveryRelays, message, code, payload, error, content, expirationSeconds, temporaryStorageArea, _getIykcProofs, deletionPubkey, deletionSeckey, autoDeletionCapability, _publish })
575
+ }
526
576
 
527
577
  const { event: question, wireEvent } = await makeOutgoingRumor({
528
578
  senderSigner,
@@ -716,6 +766,8 @@ export async function broadcastNymRumor ({
716
766
  _publish = privateChannel.publishNymEvent
717
767
  }) {
718
768
  if (!nymSigner?.getPublicKey) throw new ValidationError('NYM_SIGNER_REQUIRED')
769
+ if (!nymSigner?.getPublicKey) throw new ValidationError('NYM_SIGNER_REQUIRED')
770
+ if (rumor.pubkey !== undefined && rumor.pubkey !== await nymSigner.getPublicKey()) throw new ValidationError('NYM_RUMOR_AUTHOR_MISMATCH')
719
771
  const { event, wireEvent } = await makeOutgoingRumor({ senderSigner: nymSigner, rumor })
720
772
  const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
721
773
  const reports = await sendNymMessage({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish })