query-events.go raw
1 //go:build js && wasm
2
3 package wasmdb
4
5 import (
6 "bytes"
7 "context"
8 "errors"
9 "sort"
10 "strconv"
11 "time"
12
13 "github.com/aperturerobotics/go-indexeddb/idb"
14 "next.orly.dev/pkg/lol/chk"
15 sha256 "github.com/minio/sha256-simd"
16
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/database"
24 "next.orly.dev/pkg/database/indexes/types"
25 "next.orly.dev/pkg/interfaces/store"
26 "next.orly.dev/pkg/utils"
27 )
28
29 // CheckExpiration checks if an event has expired based on its "expiration" tag
30 func CheckExpiration(ev *event.E) (expired bool) {
31 var err error
32 expTag := ev.Tags.GetFirst([]byte("expiration"))
33 if expTag != nil {
34 expTS := ints.New(0)
35 if _, err = expTS.Unmarshal(expTag.Value()); err == nil {
36 if int64(expTS.N) < time.Now().Unix() {
37 return true
38 }
39 }
40 }
41 return
42 }
43
44 // GetSerialsByRange retrieves serials from an index range using cursor iteration.
45 // The index keys must end with a 5-byte serial number.
46 func (w *W) GetSerialsByRange(idx database.Range) (sers types.Uint40s, err error) {
47 if len(idx.Start) < 3 {
48 return nil, errors.New("invalid range: start key too short")
49 }
50
51 // Extract the object store name from the 3-byte prefix
52 storeName := string(idx.Start[:3])
53
54 // Open a read transaction
55 tx, err := w.db.Transaction(idb.TransactionReadOnly, storeName)
56 if err != nil {
57 return nil, err
58 }
59
60 objStore, err := tx.ObjectStore(storeName)
61 if err != nil {
62 return nil, err
63 }
64
65 // Open cursor in reverse order (newest first like Badger)
66 cursorReq, err := objStore.OpenCursor(idb.CursorPrevious)
67 if err != nil {
68 return nil, err
69 }
70
71 // Pre-allocate slice
72 sers = make(types.Uint40s, 0, 100)
73
74 // Create end boundary with 0xff suffix for inclusive range
75 endBoundary := make([]byte, len(idx.End)+5)
76 copy(endBoundary, idx.End)
77 for i := len(idx.End); i < len(endBoundary); i++ {
78 endBoundary[i] = 0xff
79 }
80
81 err = cursorReq.Iter(w.ctx, func(cursor *idb.CursorWithValue) error {
82 keyVal, keyErr := cursor.Key()
83 if keyErr != nil {
84 return keyErr
85 }
86
87 key := safeValueToBytes(keyVal)
88 if len(key) < 8 { // minimum: 3 prefix + 5 serial
89 return cursor.Continue()
90 }
91
92 // Check if key is within range
93 keyWithoutSerial := key[:len(key)-5]
94
95 // Compare with start (lower bound)
96 cmp := bytes.Compare(keyWithoutSerial, idx.Start)
97 if cmp < 0 {
98 // Key is before range start, stop iteration
99 return errors.New("done")
100 }
101
102 // Compare with end boundary
103 if bytes.Compare(key, endBoundary) > 0 {
104 // Key is after range end, continue to find keys in range
105 return cursor.Continue()
106 }
107
108 // Extract serial from last 5 bytes
109 ser := new(types.Uint40)
110 if err := ser.UnmarshalRead(bytes.NewReader(key[len(key)-5:])); err == nil {
111 sers = append(sers, ser)
112 }
113
114 return cursor.Continue()
115 })
116
117 if err != nil && err.Error() != "done" {
118 return nil, err
119 }
120
121 // Sort by serial (ascending)
122 sort.Slice(sers, func(i, j int) bool {
123 return sers[i].Get() < sers[j].Get()
124 })
125
126 return sers, nil
127 }
128
129 // QueryForIds retrieves IdPkTs records based on a filter.
130 // Results are sorted by timestamp in reverse chronological order.
131 func (w *W) QueryForIds(c context.Context, f *filter.F) (idPkTs []*store.IdPkTs, err error) {
132 if f.Ids != nil && f.Ids.Len() > 0 {
133 err = errors.New("query for Ids is invalid for a filter with Ids")
134 return
135 }
136
137 var idxs []database.Range
138 if idxs, err = database.GetIndexesFromFilter(f); chk.E(err) {
139 return
140 }
141
142 var results []*store.IdPkTs
143 results = make([]*store.IdPkTs, 0, len(idxs)*100)
144
145 // Track match counts for search ranking
146 counts := make(map[uint64]int)
147
148 for _, idx := range idxs {
149 var founds types.Uint40s
150 if founds, err = w.GetSerialsByRange(idx); err != nil {
151 w.Logger.Warnf("QueryForIds: GetSerialsByRange error: %v", err)
152 continue
153 }
154
155 var tmp []*store.IdPkTs
156 if tmp, err = w.GetFullIdPubkeyBySerials(founds); err != nil {
157 w.Logger.Warnf("QueryForIds: GetFullIdPubkeyBySerials error: %v", err)
158 continue
159 }
160
161 // Track match counts for search queries
162 if len(f.Search) > 0 {
163 for _, v := range tmp {
164 counts[v.Ser]++
165 }
166 }
167 results = append(results, tmp...)
168 }
169
170 // Deduplicate results
171 seen := make(map[uint64]struct{}, len(results))
172 idPkTs = make([]*store.IdPkTs, 0, len(results))
173 for _, idpk := range results {
174 if _, ok := seen[idpk.Ser]; !ok {
175 seen[idpk.Ser] = struct{}{}
176 idPkTs = append(idPkTs, idpk)
177 }
178 }
179
180 // For search queries combined with other filters, verify matches
181 if len(f.Search) > 0 && ((f.Authors != nil && f.Authors.Len() > 0) ||
182 (f.Kinds != nil && f.Kinds.Len() > 0) ||
183 (f.Tags != nil && f.Tags.Len() > 0)) {
184 // Build serial list for fetching
185 serials := make([]*types.Uint40, 0, len(idPkTs))
186 for _, v := range idPkTs {
187 s := new(types.Uint40)
188 s.Set(v.Ser)
189 serials = append(serials, s)
190 }
191
192 var evs map[uint64]*event.E
193 if evs, err = w.FetchEventsBySerials(serials); chk.E(err) {
194 return
195 }
196
197 filtered := make([]*store.IdPkTs, 0, len(idPkTs))
198 for _, v := range idPkTs {
199 ev, ok := evs[v.Ser]
200 if !ok || ev == nil {
201 continue
202 }
203
204 matchesAll := true
205 if f.Authors != nil && f.Authors.Len() > 0 && !f.Authors.Contains(ev.Pubkey) {
206 matchesAll = false
207 }
208 if matchesAll && f.Kinds != nil && f.Kinds.Len() > 0 && !f.Kinds.Contains(ev.Kind) {
209 matchesAll = false
210 }
211 if matchesAll && f.Tags != nil && f.Tags.Len() > 0 {
212 tagOK := true
213 for _, t := range *f.Tags {
214 if t.Len() < 2 {
215 continue
216 }
217 key := t.Key()
218 values := t.T[1:]
219 if !ev.Tags.ContainsAny(key, values) {
220 tagOK = false
221 break
222 }
223 }
224 if !tagOK {
225 matchesAll = false
226 }
227 }
228 if matchesAll {
229 filtered = append(filtered, v)
230 }
231 }
232 idPkTs = filtered
233 }
234
235 // Sort by timestamp (newest first)
236 if len(f.Search) == 0 {
237 sort.Slice(idPkTs, func(i, j int) bool {
238 return idPkTs[i].Ts > idPkTs[j].Ts
239 })
240 } else {
241 // Search ranking: blend match count with recency
242 var maxCount int
243 var minTs, maxTs int64
244 if len(idPkTs) > 0 {
245 minTs, maxTs = idPkTs[0].Ts, idPkTs[0].Ts
246 }
247 for _, v := range idPkTs {
248 if c := counts[v.Ser]; c > maxCount {
249 maxCount = c
250 }
251 if v.Ts < minTs {
252 minTs = v.Ts
253 }
254 if v.Ts > maxTs {
255 maxTs = v.Ts
256 }
257 }
258 tsSpan := maxTs - minTs
259 if tsSpan <= 0 {
260 tsSpan = 1
261 }
262 if maxCount <= 0 {
263 maxCount = 1
264 }
265 sort.Slice(idPkTs, func(i, j int) bool {
266 ci := float64(counts[idPkTs[i].Ser]) / float64(maxCount)
267 cj := float64(counts[idPkTs[j].Ser]) / float64(maxCount)
268 ai := float64(idPkTs[i].Ts-minTs) / float64(tsSpan)
269 aj := float64(idPkTs[j].Ts-minTs) / float64(tsSpan)
270 si := 0.5*ci + 0.5*ai
271 sj := 0.5*cj + 0.5*aj
272 if si == sj {
273 return idPkTs[i].Ts > idPkTs[j].Ts
274 }
275 return si > sj
276 })
277 }
278
279 // Apply limit
280 if f.Limit != nil && len(idPkTs) > int(*f.Limit) {
281 idPkTs = idPkTs[:*f.Limit]
282 }
283
284 return
285 }
286
287 // QueryForSerials takes a filter and returns matching event serials
288 func (w *W) QueryForSerials(c context.Context, f *filter.F) (sers types.Uint40s, err error) {
289 var founds []*types.Uint40
290 var idPkTs []*store.IdPkTs
291
292 if f.Ids != nil && f.Ids.Len() > 0 {
293 // Use batch lookup for IDs
294 var serialMap map[string]*types.Uint40
295 if serialMap, err = w.GetSerialsByIds(f.Ids); chk.E(err) {
296 return
297 }
298 for _, ser := range serialMap {
299 founds = append(founds, ser)
300 }
301 var tmp []*store.IdPkTs
302 if tmp, err = w.GetFullIdPubkeyBySerials(founds); chk.E(err) {
303 return
304 }
305 idPkTs = append(idPkTs, tmp...)
306 } else {
307 if idPkTs, err = w.QueryForIds(c, f); chk.E(err) {
308 return
309 }
310 }
311
312 // Extract serials
313 for _, idpk := range idPkTs {
314 ser := new(types.Uint40)
315 if err = ser.Set(idpk.Ser); chk.E(err) {
316 continue
317 }
318 sers = append(sers, ser)
319 }
320 return
321 }
322
323 // QueryEvents queries events based on a filter
324 func (w *W) QueryEvents(c context.Context, f *filter.F) (evs event.S, err error) {
325 return w.QueryEventsWithOptions(c, f, true, false)
326 }
327
328 // QueryAllVersions queries events and returns all versions of replaceable events
329 func (w *W) QueryAllVersions(c context.Context, f *filter.F) (evs event.S, err error) {
330 return w.QueryEventsWithOptions(c, f, true, true)
331 }
332
333 // QueryEventsWithOptions queries events with additional options for deletion and versioning
334 func (w *W) QueryEventsWithOptions(c context.Context, f *filter.F, includeDeleteEvents bool, showAllVersions bool) (evs event.S, err error) {
335 wantMultipleVersions := showAllVersions || (f.Limit != nil && *f.Limit > 1)
336
337 var expDeletes types.Uint40s
338 var expEvs event.S
339
340 // Handle ID-based queries
341 if f.Ids != nil && f.Ids.Len() > 0 {
342 w.Logger.Debugf("QueryEvents: ids path, count=%d", f.Ids.Len())
343
344 serials, idErr := w.GetSerialsByIds(f.Ids)
345 if idErr != nil {
346 w.Logger.Warnf("QueryEvents: error looking up ids: %v", idErr)
347 }
348
349 // Convert to slice for batch fetch
350 var serialsSlice []*types.Uint40
351 idHexToSerial := make(map[uint64]string, len(serials))
352 for idHex, ser := range serials {
353 serialsSlice = append(serialsSlice, ser)
354 idHexToSerial[ser.Get()] = idHex
355 }
356
357 // Batch fetch events
358 var fetchedEvents map[uint64]*event.E
359 if fetchedEvents, err = w.FetchEventsBySerials(serialsSlice); err != nil {
360 w.Logger.Warnf("QueryEvents: batch fetch failed: %v", err)
361 return
362 }
363
364 // Process fetched events
365 for serialValue, ev := range fetchedEvents {
366 idHex := idHexToSerial[serialValue]
367
368 ser := new(types.Uint40)
369 if err = ser.Set(serialValue); err != nil {
370 continue
371 }
372
373 // Check expiration
374 if CheckExpiration(ev) {
375 w.Logger.Debugf("QueryEvents: id=%s filtered out due to expiration", idHex)
376 expDeletes = append(expDeletes, ser)
377 expEvs = append(expEvs, ev)
378 continue
379 }
380
381 // Check for deletion
382 if derr := w.CheckForDeleted(ev, nil); derr != nil {
383 w.Logger.Debugf("QueryEvents: id=%s filtered out due to deletion: %v", idHex, derr)
384 continue
385 }
386
387 evs = append(evs, ev)
388 }
389
390 // Sort and apply limit
391 sort.Slice(evs, func(i, j int) bool {
392 return evs[i].CreatedAt > evs[j].CreatedAt
393 })
394 if f.Limit != nil && len(evs) > int(*f.Limit) {
395 evs = evs[:*f.Limit]
396 }
397 } else {
398 // Non-IDs path
399 var idPkTs []*store.IdPkTs
400 if idPkTs, err = w.QueryForIds(c, f); chk.E(err) {
401 return
402 }
403
404 // Maps for replaceable event handling
405 replaceableEvents := make(map[string]*event.E)
406 replaceableEventVersions := make(map[string]event.S)
407 paramReplaceableEvents := make(map[string]map[string]*event.E)
408 paramReplaceableEventVersions := make(map[string]map[string]event.S)
409 var regularEvents event.S
410
411 // Deletion tracking maps
412 deletionsByKindPubkey := make(map[string]bool)
413 deletionsByKindPubkeyDTag := make(map[string]map[string]int64)
414 deletedEventIds := make(map[string]bool)
415
416 // Query for deletion events if we have authors
417 if f.Authors != nil && f.Authors.Len() > 0 {
418 deletionFilter := &filter.F{
419 Kinds: kind.NewS(kind.New(5)),
420 Authors: f.Authors,
421 }
422 var deletionIdPkTs []*store.IdPkTs
423 if deletionIdPkTs, err = w.QueryForIds(c, deletionFilter); err == nil {
424 idPkTs = append(idPkTs, deletionIdPkTs...)
425 }
426 }
427
428 // Prepare serials for batch fetch
429 var allSerials []*types.Uint40
430 serialToIdPk := make(map[uint64]*store.IdPkTs, len(idPkTs))
431 for _, idpk := range idPkTs {
432 ser := new(types.Uint40)
433 if err = ser.Set(idpk.Ser); err != nil {
434 continue
435 }
436 allSerials = append(allSerials, ser)
437 serialToIdPk[ser.Get()] = idpk
438 }
439
440 // Batch fetch all events
441 var allEvents map[uint64]*event.E
442 if allEvents, err = w.FetchEventsBySerials(allSerials); err != nil {
443 w.Logger.Warnf("QueryEvents: batch fetch failed in non-IDs path: %v", err)
444 return
445 }
446
447 // First pass: collect deletion events
448 for serialValue, ev := range allEvents {
449 ser := new(types.Uint40)
450 if err = ser.Set(serialValue); err != nil {
451 continue
452 }
453
454 if CheckExpiration(ev) {
455 expDeletes = append(expDeletes, ser)
456 expEvs = append(expEvs, ev)
457 continue
458 }
459
460 if ev.Kind == kind.Deletion.K {
461 // Process e-tags and a-tags for deletion tracking
462 aTags := ev.Tags.GetAll([]byte("a"))
463 for _, aTag := range aTags {
464 if aTag.Len() < 2 {
465 continue
466 }
467 split := bytes.Split(aTag.Value(), []byte{':'})
468 if len(split) < 2 {
469 continue
470 }
471 kindInt, parseErr := strconv.Atoi(string(split[0]))
472 if parseErr != nil {
473 continue
474 }
475 kk := kind.New(uint16(kindInt))
476 if !kind.IsReplaceable(kk.K) {
477 continue
478 }
479 var pk []byte
480 if pk, err = hex.DecAppend(nil, split[1]); err != nil {
481 continue
482 }
483 if !utils.FastEqual(pk, ev.Pubkey) {
484 continue
485 }
486 key := hex.Enc(pk) + ":" + strconv.Itoa(int(kk.K))
487
488 if kind.IsParameterizedReplaceable(kk.K) {
489 if len(split) < 3 {
490 continue
491 }
492 if _, exists := deletionsByKindPubkeyDTag[key]; !exists {
493 deletionsByKindPubkeyDTag[key] = make(map[string]int64)
494 }
495 dValue := string(split[2])
496 if ts, ok := deletionsByKindPubkeyDTag[key][dValue]; !ok || ev.CreatedAt > ts {
497 deletionsByKindPubkeyDTag[key][dValue] = ev.CreatedAt
498 }
499 } else {
500 deletionsByKindPubkey[key] = true
501 }
502 }
503
504 // Process e-tags for specific event deletions
505 eTags := ev.Tags.GetAll([]byte("e"))
506 for _, eTag := range eTags {
507 eTagHex := eTag.ValueHex()
508 if len(eTagHex) != 64 {
509 continue
510 }
511 evId := make([]byte, sha256.Size)
512 if _, hexErr := hex.DecBytes(evId, eTagHex); hexErr != nil {
513 continue
514 }
515
516 // Look for target in current batch
517 var targetEv *event.E
518 for _, candidateEv := range allEvents {
519 if utils.FastEqual(candidateEv.ID, evId) {
520 targetEv = candidateEv
521 break
522 }
523 }
524
525 // Try to fetch if not in batch
526 if targetEv == nil {
527 ser, serErr := w.GetSerialById(evId)
528 if serErr != nil || ser == nil {
529 continue
530 }
531 targetEv, serErr = w.FetchEventBySerial(ser)
532 if serErr != nil || targetEv == nil {
533 continue
534 }
535 }
536
537 if !utils.FastEqual(targetEv.Pubkey, ev.Pubkey) {
538 continue
539 }
540 deletedEventIds[hex.Enc(targetEv.ID)] = true
541 }
542 }
543 }
544
545 // Second pass: process all events, filtering deleted ones
546 for _, ev := range allEvents {
547 // Tag filter verification
548 if f.Tags != nil && f.Tags.Len() > 0 {
549 tagMatches := 0
550 for _, filterTag := range *f.Tags {
551 if filterTag.Len() >= 2 {
552 filterKey := filterTag.Key()
553 var actualKey []byte
554 if len(filterKey) == 2 && filterKey[0] == '#' {
555 actualKey = filterKey[1:]
556 } else {
557 actualKey = filterKey
558 }
559 eventHasTag := false
560 if ev.Tags != nil {
561 for _, eventTag := range *ev.Tags {
562 if eventTag.Len() >= 2 && bytes.Equal(eventTag.Key(), actualKey) {
563 for _, filterValue := range filterTag.T[1:] {
564 if database.TagValuesMatchUsingTagMethods(eventTag, filterValue) {
565 eventHasTag = true
566 break
567 }
568 }
569 if eventHasTag {
570 break
571 }
572 }
573 }
574 }
575 if eventHasTag {
576 tagMatches++
577 }
578 }
579 }
580 if tagMatches < f.Tags.Len() {
581 continue
582 }
583 }
584
585 // Skip deletion events unless explicitly requested
586 if ev.Kind == kind.Deletion.K {
587 kind5Requested := false
588 if f.Kinds != nil && f.Kinds.Len() > 0 {
589 for i := 0; i < f.Kinds.Len(); i++ {
590 if f.Kinds.K[i].K == kind.Deletion.K {
591 kind5Requested = true
592 break
593 }
594 }
595 }
596 if !kind5Requested {
597 continue
598 }
599 }
600
601 // Check if event ID is in filter
602 isIdInFilter := false
603 if f.Ids != nil && f.Ids.Len() > 0 {
604 for i := 0; i < f.Ids.Len(); i++ {
605 if utils.FastEqual(ev.ID, (*f.Ids).T[i]) {
606 isIdInFilter = true
607 break
608 }
609 }
610 }
611
612 // Check if specifically deleted
613 eventIdHex := hex.Enc(ev.ID)
614 if deletedEventIds[eventIdHex] {
615 continue
616 }
617
618 // Handle replaceable events
619 if kind.IsReplaceable(ev.Kind) {
620 key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind))
621 if deletionsByKindPubkey[key] && !isIdInFilter {
622 continue
623 } else if wantMultipleVersions {
624 replaceableEventVersions[key] = append(replaceableEventVersions[key], ev)
625 } else {
626 existing, exists := replaceableEvents[key]
627 if !exists || ev.CreatedAt > existing.CreatedAt {
628 replaceableEvents[key] = ev
629 }
630 }
631 } else if kind.IsParameterizedReplaceable(ev.Kind) {
632 key := hex.Enc(ev.Pubkey) + ":" + strconv.Itoa(int(ev.Kind))
633 dTag := ev.Tags.GetFirst([]byte("d"))
634 var dValue string
635 if dTag != nil && dTag.Len() > 1 {
636 dValue = string(dTag.Value())
637 }
638
639 if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists {
640 if delTs, ok := deletionMap[dValue]; ok && ev.CreatedAt < delTs && !isIdInFilter {
641 continue
642 }
643 }
644
645 if wantMultipleVersions {
646 if _, exists := paramReplaceableEventVersions[key]; !exists {
647 paramReplaceableEventVersions[key] = make(map[string]event.S)
648 }
649 paramReplaceableEventVersions[key][dValue] = append(paramReplaceableEventVersions[key][dValue], ev)
650 } else {
651 if _, exists := paramReplaceableEvents[key]; !exists {
652 paramReplaceableEvents[key] = make(map[string]*event.E)
653 }
654 existing, exists := paramReplaceableEvents[key][dValue]
655 if !exists || ev.CreatedAt > existing.CreatedAt {
656 paramReplaceableEvents[key][dValue] = ev
657 }
658 }
659 } else {
660 regularEvents = append(regularEvents, ev)
661 }
662 }
663
664 // Collect results
665 if wantMultipleVersions {
666 for _, versions := range replaceableEventVersions {
667 sort.Slice(versions, func(i, j int) bool {
668 return versions[i].CreatedAt > versions[j].CreatedAt
669 })
670 limit := len(versions)
671 if f.Limit != nil && int(*f.Limit) < limit {
672 limit = int(*f.Limit)
673 }
674 for i := 0; i < limit; i++ {
675 evs = append(evs, versions[i])
676 }
677 }
678 } else {
679 for _, ev := range replaceableEvents {
680 evs = append(evs, ev)
681 }
682 }
683
684 if wantMultipleVersions {
685 for _, dTagMap := range paramReplaceableEventVersions {
686 for _, versions := range dTagMap {
687 sort.Slice(versions, func(i, j int) bool {
688 return versions[i].CreatedAt > versions[j].CreatedAt
689 })
690 limit := len(versions)
691 if f.Limit != nil && int(*f.Limit) < limit {
692 limit = int(*f.Limit)
693 }
694 for i := 0; i < limit; i++ {
695 evs = append(evs, versions[i])
696 }
697 }
698 }
699 } else {
700 for _, innerMap := range paramReplaceableEvents {
701 for _, ev := range innerMap {
702 evs = append(evs, ev)
703 }
704 }
705 }
706
707 evs = append(evs, regularEvents...)
708
709 // Sort and limit
710 sort.Slice(evs, func(i, j int) bool {
711 return evs[i].CreatedAt > evs[j].CreatedAt
712 })
713 if f.Limit != nil && len(evs) > int(*f.Limit) {
714 evs = evs[:*f.Limit]
715 }
716
717 // Delete expired events in background
718 go func() {
719 for i, ser := range expDeletes {
720 w.DeleteEventBySerial(context.Background(), ser, expEvs[i])
721 }
722 }()
723 }
724
725 return
726 }
727
728 // QueryDeleteEventsByTargetId queries for delete events targeting a specific event ID
729 func (w *W) QueryDeleteEventsByTargetId(c context.Context, targetEventId []byte) (evs event.S, err error) {
730 f := &filter.F{
731 Kinds: kind.NewS(kind.Deletion),
732 Tags: tag.NewS(
733 tag.NewFromAny("#e", hex.Enc(targetEventId)),
734 ),
735 }
736 return w.QueryEventsWithOptions(c, f, true, false)
737 }
738
739 // CountEvents counts events matching a filter
740 func (w *W) CountEvents(c context.Context, f *filter.F) (count int, approx bool, err error) {
741 approx = false
742 if f == nil {
743 return 0, false, nil
744 }
745
746 // For ID-based queries, count resolved IDs
747 if f.Ids != nil && f.Ids.Len() > 0 {
748 serials, idErr := w.GetSerialsByIds(f.Ids)
749 if idErr != nil {
750 return 0, false, idErr
751 }
752 return len(serials), false, nil
753 }
754
755 // For other queries, get serials and count
756 var sers types.Uint40s
757 if sers, err = w.QueryForSerials(c, f); err != nil {
758 return 0, false, err
759 }
760
761 return len(sers), false, nil
762 }
763
764 // GetSerialsFromFilter is an alias for QueryForSerials for interface compatibility
765 func (w *W) GetSerialsFromFilter(f *filter.F) (serials types.Uint40s, err error) {
766 return w.QueryForSerials(w.ctx, f)
767 }
768