cricinfo-cli-go 0.1.1 → 0.1.4

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.
@@ -131,6 +131,15 @@ func (r *Resolver) Search(ctx context.Context, kind EntityKind, query string, op
131
131
  PreferredLeagueID: strings.TrimSpace(opts.LeagueID),
132
132
  PreferredMatchID: strings.TrimSpace(opts.MatchID),
133
133
  })
134
+ if kind == EntityMatch && strings.TrimSpace(query) != "" && len(entities) == 0 {
135
+ if err := r.seedFromEventsFresh(ctx); err != nil {
136
+ warnings = append(warnings, err.Error())
137
+ }
138
+ entities = r.index.Search(kind, query, limit, SearchContext{
139
+ PreferredLeagueID: strings.TrimSpace(opts.LeagueID),
140
+ PreferredMatchID: strings.TrimSpace(opts.MatchID),
141
+ })
142
+ }
134
143
 
135
144
  _ = r.index.Persist()
136
145
 
@@ -166,8 +175,16 @@ func (r *Resolver) seedContext(ctx context.Context, opts ResolveOptions) error {
166
175
  }
167
176
 
168
177
  func (r *Resolver) seedFromEvents(ctx context.Context) error {
178
+ return r.seedFromEventsWithTTL(ctx, true)
179
+ }
180
+
181
+ func (r *Resolver) seedFromEventsFresh(ctx context.Context) error {
182
+ return r.seedFromEventsWithTTL(ctx, false)
183
+ }
184
+
185
+ func (r *Resolver) seedFromEventsWithTTL(ctx context.Context, respectTTL bool) error {
169
186
  last := r.index.LastEventsSeedAt()
170
- if !last.IsZero() && r.now().Sub(last) < r.eventSeedTTL {
187
+ if respectTTL && !last.IsZero() && r.now().Sub(last) < r.eventSeedTTL {
171
188
  return nil
172
189
  }
173
190
 
@@ -188,7 +205,11 @@ func (r *Resolver) seedFromEvents(ctx context.Context) error {
188
205
  successCount := 0
189
206
  seedErrors := make([]string, 0)
190
207
  for i := 0; i < limit; i++ {
191
- if err := r.seedEventRef(ctx, page.Items[i].URL); err != nil {
208
+ seedFn := r.seedEventRef
209
+ if !respectTTL {
210
+ seedFn = r.seedEventRefFresh
211
+ }
212
+ if err := seedFn(ctx, page.Items[i].URL); err != nil {
192
213
  seedErrors = append(seedErrors, fmt.Sprintf("%s (%v)", page.Items[i].URL, err))
193
214
  continue
194
215
  }
@@ -266,11 +287,19 @@ func (r *Resolver) seedNumericID(ctx context.Context, kind EntityKind, id string
266
287
  }
267
288
 
268
289
  func (r *Resolver) seedEventRef(ctx context.Context, ref string) error {
290
+ return r.seedEventRefWithHydration(ctx, ref, true)
291
+ }
292
+
293
+ func (r *Resolver) seedEventRefFresh(ctx context.Context, ref string) error {
294
+ return r.seedEventRefWithHydration(ctx, ref, false)
295
+ }
296
+
297
+ func (r *Resolver) seedEventRefWithHydration(ctx context.Context, ref string, checkHydrated bool) error {
269
298
  ref = r.absoluteRef(ref)
270
299
  if ref == "" {
271
300
  return fmt.Errorf("empty event ref")
272
301
  }
273
- if r.isHydrated(ref) {
302
+ if checkHydrated && r.isHydrated(ref) {
274
303
  return nil
275
304
  }
276
305
 
@@ -317,6 +346,17 @@ func (r *Resolver) seedEventRef(ctx context.Context, ref string) error {
317
346
 
318
347
  competitions := mapSliceField(payload, "competitions")
319
348
  for _, comp := range competitions {
349
+ compRef := stringField(comp, "$ref")
350
+ if compRef != "" && len(mapSliceField(comp, "competitors")) == 0 {
351
+ seedCompetitionFn := r.seedCompetitionRef
352
+ if !checkHydrated {
353
+ seedCompetitionFn = r.seedCompetitionRefFresh
354
+ }
355
+ if err := seedCompetitionFn(ctx, compRef); err != nil {
356
+ return err
357
+ }
358
+ continue
359
+ }
320
360
  if err := r.seedCompetitionMap(ctx, comp, leagueID, eventID, eventName); err != nil {
321
361
  return err
322
362
  }
@@ -329,11 +369,19 @@ func (r *Resolver) seedEventRef(ctx context.Context, ref string) error {
329
369
  }
330
370
 
331
371
  func (r *Resolver) seedCompetitionRef(ctx context.Context, ref string) error {
372
+ return r.seedCompetitionRefWithHydration(ctx, ref, true)
373
+ }
374
+
375
+ func (r *Resolver) seedCompetitionRefFresh(ctx context.Context, ref string) error {
376
+ return r.seedCompetitionRefWithHydration(ctx, ref, false)
377
+ }
378
+
379
+ func (r *Resolver) seedCompetitionRefWithHydration(ctx context.Context, ref string, checkHydrated bool) error {
332
380
  ref = r.absoluteRef(ref)
333
381
  if ref == "" {
334
382
  return fmt.Errorf("empty competition ref")
335
383
  }
336
- if r.isHydrated(ref) {
384
+ if checkHydrated && r.isHydrated(ref) {
337
385
  return nil
338
386
  }
339
387
 
@@ -375,6 +423,15 @@ func (r *Resolver) seedCompetitionMap(ctx context.Context, comp map[string]any,
375
423
  matchName := nonEmpty(stringField(comp, "shortDescription"), stringField(comp, "description"), stringField(comp, "note"), eventName, stringField(comp, "date"))
376
424
  matchShort := nonEmpty(stringField(comp, "shortDescription"), eventName)
377
425
 
426
+ competitors := mapSliceField(comp, "competitors")
427
+ teamAliases := make([]string, 0)
428
+ for _, competitor := range competitors {
429
+ if err := r.seedCompetitorMap(ctx, competitor, leagueID, eventID, competitionID); err != nil {
430
+ return err
431
+ }
432
+ teamAliases = append(teamAliases, r.matchTeamAliasesFromCompetitor(ctx, competitor, leagueID, competitionID)...)
433
+ }
434
+
378
435
  if err := r.index.Upsert(IndexedEntity{
379
436
  Kind: EntityMatch,
380
437
  ID: competitionID,
@@ -384,32 +441,96 @@ func (r *Resolver) seedCompetitionMap(ctx context.Context, comp map[string]any,
384
441
  LeagueID: leagueID,
385
442
  EventID: eventID,
386
443
  MatchID: competitionID,
387
- Aliases: []string{
444
+ Aliases: append([]string{
388
445
  stringField(comp, "description"),
389
446
  stringField(comp, "shortDescription"),
390
447
  stringField(comp, "note"),
391
448
  eventName,
392
449
  competitionID,
393
450
  eventID,
394
- },
451
+ }, teamAliases...),
395
452
  UpdatedAt: r.now(),
396
453
  }); err != nil {
397
454
  return err
398
455
  }
399
456
 
400
- competitors := mapSliceField(comp, "competitors")
401
- for _, competitor := range competitors {
402
- if err := r.seedCompetitorMap(ctx, competitor, leagueID, eventID, competitionID); err != nil {
403
- return err
404
- }
405
- }
406
-
407
457
  if compRef != "" {
408
458
  r.markHydrated(r.absoluteRef(compRef))
409
459
  }
410
460
  return nil
411
461
  }
412
462
 
463
+ func (r *Resolver) matchTeamAliasesFromCompetitor(ctx context.Context, competitor map[string]any, leagueID, matchID string) []string {
464
+ team := mapField(competitor, "team")
465
+ aliases := []string{
466
+ stringField(team, "displayName"),
467
+ stringField(team, "name"),
468
+ stringField(team, "shortDisplayName"),
469
+ stringField(team, "shortName"),
470
+ stringField(team, "abbreviation"),
471
+ }
472
+
473
+ teamRef := refFromField(competitor, "team")
474
+ teamID := nonEmpty(
475
+ refIDs(teamRef)["teamId"],
476
+ stringField(team, "id"),
477
+ stringField(competitor, "id"),
478
+ )
479
+ if teamID == "" {
480
+ return expandKnownTeamAliases(aliases)
481
+ }
482
+
483
+ if existing, ok := r.index.FindByID(EntityTeam, teamID); ok {
484
+ aliases = append(aliases, existing.Name, existing.ShortName)
485
+ }
486
+
487
+ _ = r.seedTeamByID(ctx, teamID, leagueID, matchID)
488
+ if existing, ok := r.index.FindByID(EntityTeam, teamID); ok {
489
+ aliases = append(aliases, existing.Name, existing.ShortName)
490
+ }
491
+ return expandKnownTeamAliases(aliases)
492
+ }
493
+
494
+ func hasNonEmptyAlias(values ...string) bool {
495
+ for _, value := range values {
496
+ if strings.TrimSpace(value) != "" {
497
+ return true
498
+ }
499
+ }
500
+ return false
501
+ }
502
+
503
+ func expandKnownTeamAliases(values []string) []string {
504
+ expanded := append([]string{}, values...)
505
+ seen := map[string]struct{}{}
506
+ for _, value := range values {
507
+ normalized := normalizeAlias(value)
508
+ if normalized == "" {
509
+ continue
510
+ }
511
+ seen[normalized] = struct{}{}
512
+ }
513
+
514
+ add := func(value string) {
515
+ normalized := normalizeAlias(value)
516
+ if normalized == "" {
517
+ return
518
+ }
519
+ if _, ok := seen[normalized]; ok {
520
+ return
521
+ }
522
+ seen[normalized] = struct{}{}
523
+ expanded = append(expanded, value)
524
+ }
525
+
526
+ for _, value := range values {
527
+ for _, alias := range knownIPLTeamAliases[normalizeAlias(value)] {
528
+ add(alias)
529
+ }
530
+ }
531
+ return expanded
532
+ }
533
+
413
534
  func (r *Resolver) seedCompetitorMap(ctx context.Context, competitor map[string]any, leagueID, eventID, matchID string) error {
414
535
  competitorRef := stringField(competitor, "$ref")
415
536
  teamRef := refFromField(competitor, "team")
@@ -4,8 +4,13 @@ import (
4
4
  "context"
5
5
  "fmt"
6
6
  "strings"
7
+ "sync"
8
+ "time"
7
9
  )
8
10
 
11
+ const teamRosterNameHydrationConcurrency = 8
12
+ const teamRosterNameHydrationTimeout = 2500 * time.Millisecond
13
+
9
14
  // TeamServiceConfig configures team discovery and match-scoped team commands.
10
15
  type TeamServiceConfig struct {
11
16
  Client *Client
@@ -125,7 +130,7 @@ func (s *TeamService) Roster(ctx context.Context, teamQuery string, opts TeamLoo
125
130
  return NormalizedResult{}, fmt.Errorf("normalize team roster %q: %w", resolved.CanonicalRef, err)
126
131
  }
127
132
 
128
- s.enrichRosterEntries(entries)
133
+ warnings = append(warnings, s.enrichRosterEntries(ctx, entries)...)
129
134
 
130
135
  items := make([]any, 0, len(entries))
131
136
  for _, entry := range entries {
@@ -441,26 +446,124 @@ func (s *TeamService) fetchGlobalTeam(ctx context.Context, entity IndexedEntity)
441
446
  return *team, resolved, ""
442
447
  }
443
448
 
444
- func (s *TeamService) enrichRosterEntries(entries []TeamRosterEntry) {
449
+ func (s *TeamService) enrichRosterEntries(ctx context.Context, entries []TeamRosterEntry) []string {
445
450
  if s.resolver == nil || s.resolver.index == nil {
446
- return
451
+ return nil
447
452
  }
448
453
 
454
+ missing := make([]int, 0)
449
455
  for i := range entries {
450
- if strings.TrimSpace(entries[i].PlayerID) == "" {
456
+ playerID := strings.TrimSpace(entries[i].PlayerID)
457
+ if playerID == "" {
458
+ playerID = strings.TrimSpace(refIDs(entries[i].PlayerRef)["athleteId"])
459
+ entries[i].PlayerID = playerID
460
+ }
461
+ if playerID == "" {
451
462
  continue
452
463
  }
453
- player, ok := s.resolver.index.FindByID(EntityPlayer, entries[i].PlayerID)
464
+ player, ok := s.resolver.index.FindByID(EntityPlayer, playerID)
454
465
  if !ok {
466
+ missing = append(missing, i)
455
467
  continue
456
468
  }
457
469
  if strings.TrimSpace(entries[i].DisplayName) == "" {
458
- entries[i].DisplayName = nonEmpty(player.Name, player.ShortName)
470
+ entries[i].DisplayName = nonEmpty(player.Name, player.ShortName, player.ID)
459
471
  }
460
472
  if strings.TrimSpace(entries[i].PlayerRef) == "" {
461
473
  entries[i].PlayerRef = strings.TrimSpace(player.Ref)
462
474
  }
463
475
  }
476
+
477
+ if len(missing) == 0 {
478
+ return nil
479
+ }
480
+
481
+ type hydrateResult struct {
482
+ index int
483
+ displayName string
484
+ playerRef string
485
+ warning string
486
+ }
487
+
488
+ results := make([]hydrateResult, len(missing))
489
+ sem := make(chan struct{}, teamRosterNameHydrationConcurrency)
490
+ var wg sync.WaitGroup
491
+
492
+ for resultIndex, entryIndex := range missing {
493
+ wg.Add(1)
494
+ go func(resultIndex, entryIndex int) {
495
+ defer wg.Done()
496
+ sem <- struct{}{}
497
+ defer func() { <-sem }()
498
+
499
+ entry := entries[entryIndex]
500
+ playerID := strings.TrimSpace(entry.PlayerID)
501
+ playerRef := strings.TrimSpace(entry.PlayerRef)
502
+ if playerID == "" {
503
+ playerID = strings.TrimSpace(refIDs(playerRef)["athleteId"])
504
+ }
505
+ if playerID == "" && playerRef == "" {
506
+ results[resultIndex] = hydrateResult{
507
+ index: entryIndex,
508
+ warning: "roster entry missing player id/ref",
509
+ }
510
+ return
511
+ }
512
+
513
+ reqCtx, cancel := context.WithTimeout(ctx, teamRosterNameHydrationTimeout)
514
+ var seedErr error
515
+ if playerID != "" {
516
+ seedErr = s.resolver.seedPlayerByID(reqCtx, playerID, "", strings.TrimSpace(entry.MatchID))
517
+ } else {
518
+ seedErr = s.resolver.seedPlayerRef(reqCtx, playerRef, "", strings.TrimSpace(entry.MatchID))
519
+ }
520
+ cancel()
521
+
522
+ if playerID == "" {
523
+ playerID = strings.TrimSpace(refIDs(playerRef)["athleteId"])
524
+ }
525
+
526
+ player := IndexedEntity{}
527
+ ok := false
528
+ if playerID != "" {
529
+ player, ok = s.resolver.index.FindByID(EntityPlayer, playerID)
530
+ }
531
+ if !ok && playerRef != "" {
532
+ player, ok = s.resolver.index.FindByRef(EntityPlayer, playerRef)
533
+ }
534
+
535
+ result := hydrateResult{index: entryIndex}
536
+ if ok {
537
+ result.displayName = nonEmpty(strings.TrimSpace(player.Name), strings.TrimSpace(player.ShortName), strings.TrimSpace(player.ID))
538
+ result.playerRef = strings.TrimSpace(player.Ref)
539
+ }
540
+ if seedErr != nil && result.displayName == "" {
541
+ label := nonEmpty(playerID, playerRef, fmt.Sprintf("entry-%d", entryIndex+1))
542
+ result.warning = fmt.Sprintf("player %s: %v", label, seedErr)
543
+ }
544
+ results[resultIndex] = result
545
+ }(resultIndex, entryIndex)
546
+ }
547
+
548
+ wg.Wait()
549
+
550
+ warnings := make([]string, 0)
551
+ for _, result := range results {
552
+ if result.index < 0 || result.index >= len(entries) {
553
+ continue
554
+ }
555
+ if strings.TrimSpace(entries[result.index].DisplayName) == "" && strings.TrimSpace(result.displayName) != "" {
556
+ entries[result.index].DisplayName = result.displayName
557
+ }
558
+ if strings.TrimSpace(entries[result.index].PlayerRef) == "" && strings.TrimSpace(result.playerRef) != "" {
559
+ entries[result.index].PlayerRef = result.playerRef
560
+ }
561
+ if result.warning != "" {
562
+ warnings = append(warnings, result.warning)
563
+ }
564
+ }
565
+
566
+ return compactWarnings(warnings)
464
567
  }
465
568
 
466
569
  func (s *TeamService) enrichTeamLeaders(ctx context.Context, leaders *TeamLeaders) {