1 //go:build !(js && wasm)
2 3 package database
4 5 import (
6 "bytes"
7 "context"
8 "sort"
9 "strconv"
10 "time"
11 12 "next.orly.dev/pkg/lol/chk"
13 "next.orly.dev/pkg/lol/log"
14 "github.com/minio/sha256-simd"
15 "next.orly.dev/pkg/database/indexes/types"
16 "next.orly.dev/pkg/mode"
17 "next.orly.dev/pkg/nostr/encoders/event"
18 "next.orly.dev/pkg/nostr/encoders/filter"
19 "next.orly.dev/pkg/nostr/encoders/hex"
20 "next.orly.dev/pkg/nostr/encoders/ints"
21 "next.orly.dev/pkg/nostr/encoders/kind"
22 "next.orly.dev/pkg/nostr/encoders/tag"
23 "next.orly.dev/pkg/interfaces/store"
24 "next.orly.dev/pkg/utils"
25 )
26 27 func CheckExpiration(ev *event.E) (expired bool) {
28 var err error
29 expTag := ev.Tags.GetFirst([]byte("expiration"))
30 if expTag != nil {
31 expTS := ints.New(0)
32 if _, err = expTS.Unmarshal(expTag.Value()); !chk.E(err) {
33 if int64(expTS.N) < time.Now().Unix() {
34 return true
35 }
36 }
37 }
38 return
39 }
40 41 func (d *D) QueryEvents(c context.Context, f *filter.F) (
42 evs event.S, err error,
43 ) {
44 return d.QueryEventsWithOptions(c, f, true, false)
45 }
46 47 // QueryAllVersions queries events and returns all versions of replaceable events
48 func (d *D) QueryAllVersions(c context.Context, f *filter.F) (
49 evs event.S, err error,
50 ) {
51 return d.QueryEventsWithOptions(c, f, true, true)
52 }
53 54 func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDeleteEvents bool, showAllVersions bool) (
55 evs event.S, err error,
56 ) {
57 // Acquire query slot to limit concurrent queries and prevent memory exhaustion
58 if !d.AcquireQuerySlot(c) {
59 err = c.Err()
60 return
61 }
62 defer d.ReleaseQuerySlot()
63 64 // Fast path for NIP-33 addressable event queries (kind + author + d-tag)
65 // This provides O(1) direct lookup instead of index scanning
66 if !showAllVersions && IsAddressableEventQuery(f) {
67 var serial *types.Uint40
68 if serial, err = d.QueryForAddressableEvent(f); err != nil {
69 log.W.F("QueryEvents: addressable event fast path error: %v", err)
70 // Fall through to regular query path
71 err = nil
72 } else if serial != nil {
73 // Found via fast path - fetch the event
74 ev, fetchErr := d.FetchEventBySerial(serial)
75 if fetchErr == nil && ev != nil {
76 // Apply time filters if present
77 if f.Since != nil && ev.CreatedAt < f.Since.V {
78 log.T.F("QueryEvents: addressable event fast path - event before 'since' filter")
79 return // Return empty result
80 }
81 if f.Until != nil && ev.CreatedAt > f.Until.V {
82 log.T.F("QueryEvents: addressable event fast path - event after 'until' filter")
83 return // Return empty result
84 }
85 // Check deletion status (skip in open relay mode)
86 if !mode.IsOpen() {
87 if derr := d.CheckForDeleted(ev, nil); derr != nil {
88 log.T.F("QueryEvents: addressable event fast path - event deleted: %v", derr)
89 return // Return empty result
90 }
91 }
92 // Check expiration
93 if !mode.IsOpen() && CheckExpiration(ev) {
94 log.T.F("QueryEvents: addressable event fast path - event expired")
95 return // Return empty result
96 }
97 log.T.F("QueryEvents: addressable event fast path SUCCESS - kind=%d d=%s",
98 ev.Kind, string(ev.Tags.GetFirst([]byte("d")).Value()))
99 evs = append(evs, ev)
100 return
101 }
102 // Fetch failed - fall through to regular path
103 log.T.F("QueryEvents: addressable event fast path - fetch failed, falling back")
104 }
105 // Not found via fast path - fall through to regular query path
106 // This handles the case where the index doesn't exist yet (migration)
107 }
108 109 // Determine if we should return multiple versions of replaceable events
110 // based on the limit parameter
111 wantMultipleVersions := showAllVersions || (f.Limit != nil && *f.Limit > 1)
112 113 // if there is Ids in the query, this overrides anything else
114 var expDeletes types.Uint40s
115 var expEvs event.S
116 if f.Ids != nil && f.Ids.Len() > 0 {
117 // Get all serials for the requested IDs in a single batch operation
118 log.T.F("QueryEvents: ids path, count=%d", f.Ids.Len())
119 120 // Use GetSerialsByIds to batch process all IDs at once
121 serials, idErr := d.GetSerialsByIds(f.Ids)
122 if idErr != nil {
123 log.E.F("QueryEvents: error looking up ids: %v", idErr)
124 // Continue with whatever IDs we found
125 }
126 127 // Convert serials map to slice for batch fetch
128 var serialsSlice []*types.Uint40
129 serialsSlice = make([]*types.Uint40, 0, len(serials))
130 idHexToSerial := make(map[uint64]string, len(serials)) // Map serial value back to original ID hex
131 for idHex, ser := range serials {
132 serialsSlice = append(serialsSlice, ser)
133 idHexToSerial[ser.Get()] = idHex
134 }
135 136 // Fetch all events in a single batch operation
137 var fetchedEvents map[uint64]*event.E
138 if fetchedEvents, err = d.FetchEventsBySerials(serialsSlice); err != nil {
139 log.E.F("QueryEvents: batch fetch failed: %v", err)
140 return
141 }
142 143 // Process each successfully fetched event and apply filters
144 for serialValue, ev := range fetchedEvents {
145 idHex := idHexToSerial[serialValue]
146 147 // Convert serial value back to Uint40 for expiration handling
148 ser := new(types.Uint40)
149 if err = ser.Set(serialValue); err != nil {
150 log.T.F(
151 "QueryEvents: error converting serial %d: %v", serialValue,
152 err,
153 )
154 continue
155 }
156 157 // check for an expiration tag and delete after returning the result
158 // Skip expiration check when ACL is "none" (open relay mode)
159 if !mode.IsOpen() && CheckExpiration(ev) {
160 log.T.F(
161 "QueryEvents: id=%s filtered out due to expiration", idHex,
162 )
163 expDeletes = append(expDeletes, ser)
164 expEvs = append(expEvs, ev)
165 continue
166 }
167 168 // skip events that have been deleted by a proper deletion event
169 // Skip deletion check when ACL is "none" (open relay mode)
170 if !mode.IsOpen() {
171 if derr := d.CheckForDeleted(ev, nil); derr != nil {
172 log.T.F("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr)
173 continue
174 }
175 }
176 177 // Add the event to the results
178 evs = append(evs, ev)
179 // log.T.F("QueryEvents: id=%s SUCCESSFULLY FOUND, adding to results", idHex)
180 }
181 182 // sort the events by timestamp
183 sort.Slice(
184 evs, func(i, j int) bool {
185 return evs[i].CreatedAt > evs[j].CreatedAt
186 },
187 )
188 // Apply limit after processing
189 if f.Limit != nil && len(evs) > int(*f.Limit) {
190 evs = evs[:*f.Limit]
191 }
192 } else {
193 // non-IDs path
194 var idPkTs []*store.IdPkTs
195 // if f.Authors != nil && f.Authors.Len() > 0 && f.Kinds != nil && f.Kinds.Len() > 0 {
196 // log.T.F("QueryEvents: authors+kinds path, authors=%d kinds=%d", f.Authors.Len(), f.Kinds.Len())
197 // }
198 if idPkTs, err = d.QueryForIds(c, f); chk.E(err) {
199 return
200 }
201 // log.T.F("QueryEvents: QueryForIds returned %d candidates", len(idPkTs))
202 // Create a map to store versions of replaceable events
203 // If wantMultipleVersions is true, we keep multiple versions (sorted by timestamp)
204 // Otherwise, we keep only the latest
205 replaceableEvents := make(map[string]*event.E)
206 replaceableEventVersions := make(map[string]event.S) // For multiple versions
207 // Create a map to store the latest version of parameterized replaceable
208 // events
209 paramReplaceableEvents := make(map[string]map[string]*event.E)
210 paramReplaceableEventVersions := make(map[string]map[string]event.S) // For multiple versions
211 // Regular events that are not replaceable
212 var regularEvents event.S
213 // Map to track deletion events by kind and pubkey (for replaceable
214 // events)
215 deletionsByKindPubkey := make(map[string]bool)
216 // Map to track deletion events by kind, pubkey, and d-tag (for
217 // parameterized replaceable events). We store the newest delete timestamp per d-tag.
218 deletionsByKindPubkeyDTag := make(map[string]map[string]int64)
219 // Map to track specific event IDs that have been deleted
220 deletedEventIds := make(map[string]bool)
221 // Query for deletion events separately if we have authors in the filter
222 // We always need to fetch deletion events to build deletion maps, even if
223 // they're not explicitly requested in the kind filter
224 if f.Authors != nil && f.Authors.Len() > 0 {
225 // Create a filter for deletion events with the same authors
226 deletionFilter := &filter.F{
227 Kinds: kind.NewS(kind.New(5)), // Kind 5 is deletion
228 Authors: f.Authors,
229 }
230 231 var deletionIdPkTs []*store.IdPkTs
232 if deletionIdPkTs, err = d.QueryForIds(
233 c, deletionFilter,
234 ); chk.E(err) {
235 return
236 }
237 238 // Add deletion events to the list of events to process
239 idPkTs = append(idPkTs, deletionIdPkTs...)
240 }
241 // Prepare serials for batch fetch
242 var allSerials []*types.Uint40
243 allSerials = make([]*types.Uint40, 0, len(idPkTs))
244 serialToIdPk := make(map[uint64]*store.IdPkTs, len(idPkTs))
245 for _, idpk := range idPkTs {
246 ser := new(types.Uint40)
247 if err = ser.Set(idpk.Ser); err != nil {
248 continue
249 }
250 allSerials = append(allSerials, ser)
251 serialToIdPk[ser.Get()] = idpk
252 }
253 254 // Fetch all events in batch
255 var allEvents map[uint64]*event.E
256 if allEvents, err = d.FetchEventsBySerials(allSerials); err != nil {
257 log.E.F("QueryEvents: batch fetch failed in non-IDs path: %v", err)
258 return
259 }
260 261 // First pass: collect all deletion events
262 for serialValue, ev := range allEvents {
263 // Convert serial value back to Uint40 for expiration handling
264 ser := new(types.Uint40)
265 if err = ser.Set(serialValue); err != nil {
266 continue
267 }
268 269 // check for an expiration tag and delete after returning the result
270 // Skip expiration check when ACL is "none" (open relay mode)
271 if !mode.IsOpen() && CheckExpiration(ev) {
272 expDeletes = append(expDeletes, ser)
273 expEvs = append(expEvs, ev)
274 continue
275 }
276 // Process deletion events to build our deletion maps
277 // Skip deletion processing when ACL is "none" (open relay mode)
278 if !mode.IsOpen() && ev.Kind == kind.Deletion.K {
279 // Check for 'e' tags that directly reference event IDs
280 eTags := ev.Tags.GetAll([]byte("e"))
281 for _, eTag := range eTags {
282 if eTag.Len() < 2 {
283 continue
284 }
285 // We don't need to do anything with direct event ID
286 // references as we will filter those out in the second pass
287 }
288 // Check for 'a' tags that reference replaceable events
289 aTags := ev.Tags.GetAll([]byte("a"))
290 for _, aTag := range aTags {
291 if aTag.Len() < 2 {
292 continue
293 }
294 // Parse the 'a' tag value: kind:pubkey:d-tag (for parameterized) or kind:pubkey (for regular)
295 split := bytes.Split(aTag.Value(), []byte{':'})
296 if len(split) < 2 {
297 continue
298 }
299 // Parse the kind
300 kindStr := string(split[0])
301 kindInt, err := strconv.Atoi(kindStr)
302 if err != nil {
303 continue
304 }
305 kk := kind.New(uint16(kindInt))
306 // Process both regular and parameterized replaceable events
307 if !kind.IsReplaceable(kk.K) {
308 continue
309 }
310 // Parse the pubkey
311 var pk []byte
312 if pk, err = hex.DecAppend(nil, split[1]); err != nil {
313 continue
314 }
315 // Only allow users to delete their own events
316 if !utils.FastEqual(pk, ev.Pubkey) {
317 continue
318 }
319 // Create the key for the deletion map using hex
320 // representation of pubkey
321 key := hex.Enc(pk) + ":" + strconv.Itoa(int(kk.K))
322 323 if kind.IsParameterizedReplaceable(kk.K) {
324 // For parameterized replaceable events, use d-tag specific deletion
325 if len(split) < 3 {
326 continue
327 }
328 // Initialize the inner map if it doesn't exist
329 if _, exists := deletionsByKindPubkeyDTag[key]; !exists {
330 deletionsByKindPubkeyDTag[key] = make(map[string]int64)
331 }
332 // Record the newest delete timestamp for this d-tag
333 dValue := string(split[2])
334 if ts, ok := deletionsByKindPubkeyDTag[key][dValue]; !ok || ev.CreatedAt > ts {
335 deletionsByKindPubkeyDTag[key][dValue] = ev.CreatedAt
336 }
337 } else {
338 // For regular replaceable events, mark as deleted by kind/pubkey
339 deletionsByKindPubkey[key] = true
340 }
341 }
342 // For replaceable events, we need to check if there are any
343 // e-tags that reference events with the same kind and pubkey
344 for _, eTag := range eTags {
345 // Use ValueHex() to handle both binary and hex storage formats
346 eTagHex := eTag.ValueHex()
347 if len(eTagHex) != 64 {
348 continue
349 }
350 // Get the event ID from the e-tag
351 evId := make([]byte, sha256.Size)
352 if _, err = hex.DecBytes(evId, eTagHex); err != nil {
353 continue
354 }
355 356 // Look for the target event in our current batch instead of querying
357 var targetEv *event.E
358 for _, candidateEv := range allEvents {
359 if utils.FastEqual(candidateEv.ID, evId) {
360 targetEv = candidateEv
361 break
362 }
363 }
364 365 // DISABLED: Fetching target events not in current batch causes memory
366 // exhaustion under high concurrent load. Each GetSerialById call creates
367 // a Badger iterator that consumes significant memory. With many connections
368 // processing deletion events, this explodes memory usage (~300MB/connection).
369 // The deletion will still work when the target event is directly queried.
370 if targetEv == nil {
371 continue
372 }
373 374 // Only allow users to delete their own events
375 if !utils.FastEqual(targetEv.Pubkey, ev.Pubkey) {
376 continue
377 }
378 // Mark the specific event ID as deleted
379 deletedEventIds[hex.Enc(targetEv.ID)] = true
380 // Note: For e-tag deletions, we only mark the specific event as deleted,
381 // not all events of the same kind/pubkey
382 }
383 }
384 }
385 // Second pass: process all events, filtering out deleted ones
386 for _, ev := range allEvents {
387 // Add logging for tag filter debugging
388 if f.Tags != nil && f.Tags.Len() > 0 {
389 // var eventTags []string
390 // if ev.Tags != nil && ev.Tags.Len() > 0 {
391 // for _, t := range *ev.Tags {
392 // if t.Len() >= 2 {
393 // eventTags = append(
394 // eventTags,
395 // string(t.Key())+"="+string(t.Value()),
396 // )
397 // }
398 // }
399 // }
400 // log.T.F(
401 // "QueryEvents: processing event ID=%s kind=%d tags=%v",
402 // hex.Enc(ev.ID), ev.Kind, eventTags,
403 // )
404 // Check if this event matches ALL required tags in the filter
405 tagMatches := 0
406 for _, filterTag := range *f.Tags {
407 if filterTag.Len() >= 2 {
408 filterKey := filterTag.Key()
409 // Handle filter keys that start with # (remove the prefix for comparison)
410 var actualKey []byte
411 if len(filterKey) == 2 && filterKey[0] == '#' {
412 actualKey = filterKey[1:]
413 } else {
414 actualKey = filterKey
415 }
416 // Check if event has this tag key with any of the filter's values
417 eventHasTag := false
418 if ev.Tags != nil {
419 for _, eventTag := range *ev.Tags {
420 if eventTag.Len() >= 2 && bytes.Equal(
421 eventTag.Key(), actualKey,
422 ) {
423 // Check if the event's tag value matches any of the filter's values
424 // Using TagValuesMatchUsingTagMethods handles binary/hex conversion
425 // for e/p tags automatically
426 for _, filterValue := range filterTag.T[1:] {
427 if TagValuesMatchUsingTagMethods(eventTag, filterValue) {
428 eventHasTag = true
429 break
430 }
431 }
432 if eventHasTag {
433 break
434 }
435 }
436 }
437 }
438 if eventHasTag {
439 tagMatches++
440 }
441 // log.T.F(
442 // "QueryEvents: tag filter %s (actual key: %s) matches: %v (total matches: %d/%d)",
443 // string(filterKey), string(actualKey), eventHasTag,
444 // tagMatches, f.Tags.Len(),
445 // )
446 }
447 }
448 449 // If not all tags match, skip this event
450 if tagMatches < f.Tags.Len() {
451 // log.T.F(
452 // "QueryEvents: event ID=%s SKIPPED - only matches %d/%d required tags",
453 // hex.Enc(ev.ID), tagMatches, f.Tags.Len(),
454 // )
455 continue
456 }
457 // log.T.F(
458 // "QueryEvents: event ID=%s PASSES all tag filters",
459 // hex.Enc(ev.ID),
460 // )
461 }
462 463 // Skip events with kind 5 (Deletion) unless explicitly requested in the filter
464 if ev.Kind == kind.Deletion.K {
465 // Check if kind 5 (deletion) is explicitly requested in the filter
466 kind5Requested := false
467 if f.Kinds != nil && f.Kinds.Len() > 0 {
468 for i := 0; i < f.Kinds.Len(); i++ {
469 if f.Kinds.K[i].K == kind.Deletion.K {
470 kind5Requested = true
471 break
472 }
473 }
474 }
475 if !kind5Requested {
476 continue
477 }
478 }
479 // Check if this event's ID is in the filter
480 isIdInFilter := false
481 if f.Ids != nil && f.Ids.Len() > 0 {
482 for i := 0; i < f.Ids.Len(); i++ {
483 if utils.FastEqual(ev.ID, (*f.Ids).T[i]) {
484 isIdInFilter = true
485 break
486 }
487 }
488 }
489 // Check if this specific event has been deleted
490 // Skip deletion checks when ACL is "none" (open relay mode)
491 aclActive := !mode.IsOpen()
492 eventIdHex := hex.Enc(ev.ID)
493 if aclActive && deletedEventIds[eventIdHex] {
494 // Skip this event if it has been specifically deleted
495 continue
496 }
497 if kind.IsReplaceable(ev.Kind) {
498 // For replaceable events, we only keep the latest version for
499 // each pubkey and kind, and only if it hasn't been deleted
500 key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind))
501 // For replaceable events, we need to be more careful with
502 // deletion Only skip this event if it has been deleted by
503 // kind/pubkey and is not in the filter AND there isn't a newer
504 // event with the same kind/pubkey
505 if aclActive && deletionsByKindPubkey[key] && !isIdInFilter {
506 // This replaceable event has been deleted, skip it
507 continue
508 } else if wantMultipleVersions {
509 // If wantMultipleVersions is true, collect all versions
510 replaceableEventVersions[key] = append(replaceableEventVersions[key], ev)
511 } else {
512 // Normal replaceable event handling - keep only the newest
513 existing, exists := replaceableEvents[key]
514 if !exists || ev.CreatedAt > existing.CreatedAt {
515 replaceableEvents[key] = ev
516 }
517 }
518 } else if kind.IsParameterizedReplaceable(ev.Kind) {
519 // For parameterized replaceable events, we need to consider the
520 // 'd' tag
521 key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind))
522 523 // Get the 'd' tag value
524 dTag := ev.Tags.GetFirst([]byte("d"))
525 var dValue string
526 if dTag != nil && dTag.Len() > 1 {
527 dValue = string(dTag.Value())
528 } else {
529 // If no 'd' tag, use empty string
530 dValue = ""
531 }
532 533 // Check if this event has been deleted via an a-tag
534 // Skip deletion check when ACL is "none" (open relay mode)
535 if aclActive {
536 if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists {
537 // If there is a deletion timestamp and this event is older than the deletion,
538 // and this event is not specifically requested by ID, skip it
539 if delTs, ok := deletionMap[dValue]; ok && ev.CreatedAt < delTs && !isIdInFilter {
540 continue
541 }
542 }
543 }
544 545 if wantMultipleVersions {
546 // If wantMultipleVersions is true, collect all versions
547 if _, exists := paramReplaceableEventVersions[key]; !exists {
548 paramReplaceableEventVersions[key] = make(map[string]event.S)
549 }
550 paramReplaceableEventVersions[key][dValue] = append(paramReplaceableEventVersions[key][dValue], ev)
551 } else {
552 // Initialize the inner map if it doesn't exist
553 if _, exists := paramReplaceableEvents[key]; !exists {
554 paramReplaceableEvents[key] = make(map[string]*event.E)
555 }
556 557 // Check if we already have an event with this 'd' tag value
558 existing, exists := paramReplaceableEvents[key][dValue]
559 // Only keep the newer event, regardless of processing order
560 if !exists {
561 // No existing event, add this one
562 paramReplaceableEvents[key][dValue] = ev
563 } else if ev.CreatedAt > existing.CreatedAt {
564 // This event is newer than the existing one, replace it
565 paramReplaceableEvents[key][dValue] = ev
566 }
567 }
568 // If this event is older than the existing one, ignore it
569 } else {
570 // Regular events
571 regularEvents = append(regularEvents, ev)
572 }
573 }
574 // Add all the latest replaceable events to the result
575 if wantMultipleVersions {
576 // Add all versions (sorted by timestamp, newest first)
577 for key, versions := range replaceableEventVersions {
578 // Sort versions by timestamp (newest first)
579 sort.Slice(versions, func(i, j int) bool {
580 return versions[i].CreatedAt > versions[j].CreatedAt
581 })
582 // Add versions up to the limit
583 limit := len(versions)
584 if f.Limit != nil && int(*f.Limit) < limit {
585 limit = int(*f.Limit)
586 }
587 for i := 0; i < limit && i < len(versions); i++ {
588 evs = append(evs, versions[i])
589 }
590 _ = key // Use key to avoid unused variable warning
591 }
592 } else {
593 // Add only the newest version of each replaceable event
594 for _, ev := range replaceableEvents {
595 evs = append(evs, ev)
596 }
597 }
598 599 // Add all the latest parameterized replaceable events to the result
600 if wantMultipleVersions {
601 // Add all versions (sorted by timestamp, newest first)
602 for key, dTagMap := range paramReplaceableEventVersions {
603 for dTag, versions := range dTagMap {
604 // Sort versions by timestamp (newest first)
605 sort.Slice(versions, func(i, j int) bool {
606 return versions[i].CreatedAt > versions[j].CreatedAt
607 })
608 // Add versions up to the limit
609 limit := len(versions)
610 if f.Limit != nil && int(*f.Limit) < limit {
611 limit = int(*f.Limit)
612 }
613 for i := 0; i < limit && i < len(versions); i++ {
614 evs = append(evs, versions[i])
615 }
616 _ = key // Use key to avoid unused variable warning
617 _ = dTag // Use dTag to avoid unused variable warning
618 }
619 }
620 } else {
621 // Add only the newest version of each parameterized replaceable event
622 for _, innerMap := range paramReplaceableEvents {
623 for _, ev := range innerMap {
624 evs = append(evs, ev)
625 }
626 }
627 }
628 // Add all regular events to the result
629 evs = append(evs, regularEvents...)
630 // Sort all events by timestamp (newest first)
631 sort.Slice(
632 evs, func(i, j int) bool {
633 return evs[i].CreatedAt > evs[j].CreatedAt
634 },
635 )
636 // Apply limit after processing replaceable/addressable events
637 if f.Limit != nil && len(evs) > int(*f.Limit) {
638 evs = evs[:*f.Limit]
639 }
640 // TODO: DISABLED - inline deletion of expired events causes Badger race conditions
641 // under high concurrent load ("assignment to entry in nil map" panic).
642 // Expired events should be cleaned up by a separate, rate-limited background
643 // worker instead of being deleted inline during query processing.
644 // See: pkg/storage/gc.go TODOs for proper batch deletion implementation.
645 if len(expDeletes) > 0 {
646 log.D.F("QueryEvents: found %d expired events (deletion disabled)", len(expDeletes))
647 }
648 }
649 650 return
651 }
652 653 // QueryDeleteEventsByTargetId queries for delete events that target a specific event ID
654 func (d *D) QueryDeleteEventsByTargetId(c context.Context, targetEventId []byte) (
655 evs event.S, err error,
656 ) {
657 // Create a filter for deletion events with the target event ID in e-tags
658 f := &filter.F{
659 Kinds: kind.NewS(kind.Deletion),
660 Tags: tag.NewS(
661 tag.NewFromAny("#e", hex.Enc(targetEventId)),
662 ),
663 }
664 665 // Query for the delete events
666 if evs, err = d.QueryEventsWithOptions(c, f, true, false); chk.E(err) {
667 return
668 }
669 670 return
671 }
672