File size: 3,637 Bytes
68d7816
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import { IInstantiationService } from '#/_base/di/instantiation';
import type { InstantiationService } from '#/_base/di/instantiationService';
import type { LedgerEntryInfo } from '#/_base/lifecycle/ledger';
import { IEventService } from '#/app/event/event';
import { IEventBus } from '#/app/event/eventBus';
import { walkScopeContainers } from '#/debug/scopeTree';

import {
  IDebugEventsService,
  type DebugEventBusSnapshot,
  type DebugEventSubscription,
  type DebugEventSubscriptions,
} from './debugEvents';

interface UnitBookOwner {
  readonly unitBook: { entries(): LedgerEntryInfo[] };
}

interface BusCountSource {
  listenerCounts(): {
    all: number;
    perType: Record<string, number>;
    perAgent: Record<string, number>;
  };
}

interface GlobalCountSource {
  readonly listenerCount: number;
}

export class DebugEventsService implements IDebugEventsService {
  declare readonly _serviceBrand: undefined;

  private readonly root: InstantiationService;

  constructor(@IInstantiationService instantiation: IInstantiationService) {
    this.root = instantiation as InstantiationService;
  }

  subscriptions(): DebugEventSubscriptions {
    const subscriptions: DebugEventSubscription[] = [];
    const buses: DebugEventBusSnapshot[] = [];
    const seenUnits = new Set<object>();
    const seenBuses = new Set<object>();
    for (const info of walkScopeContainers(this.root)) {
      for (const registration of info.container.servicesSnapshot()) {
        const id = info.container.findIdentifier(registration.token);
        if (id === undefined) {
          continue;
        }
        const instance: unknown = info.container.fiberHost.materializedInstance(id);
        if (typeof instance !== 'object' || instance === null || seenUnits.has(instance)) {
          continue;
        }
        seenUnits.add(instance);
        if ('unitBook' in instance) {
          collectEventEntries((instance as UnitBookOwner).unitBook.entries(), subscriptions, {
            scopePath: info.path,
            unit: registration.token,
            uid: registration.uid,
          });
        }
      }
      const bus: unknown = info.container.fiberHost.materializedInstance(IEventBus);
      if (isBusCountSource(bus) && !seenBuses.has(bus)) {
        seenBuses.add(bus);
        buses.push({ scopePath: info.path, ...bus.listenerCounts() });
      }
    }
    const globalEvents: unknown = this.root.fiberHost.materializedInstance(IEventService);
    const globalListeners = isGlobalCountSource(globalEvents)
      ? globalEvents.listenerCount
      : undefined;
    return { subscriptions, buses, globalListeners };
  }
}

function collectEventEntries(
  entries: readonly LedgerEntryInfo[],
  out: DebugEventSubscription[],
  base: { scopePath: string; unit: string; uid?: number },
): void {
  for (const entry of entries) {
    if (isEventSubscriptionLabel(entry.label)) {
      out.push({ ...base, label: entry.label, kind: entry.kind });
    }
    if (entry.children !== undefined) {
      collectEventEntries(entry.children, out, base);
    }
  }
}

function isEventSubscriptionLabel(label: string): boolean {
  return label.startsWith('on:') || label === 'disposable:EventSubscription';
}

function isBusCountSource(value: unknown): value is BusCountSource {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as BusCountSource).listenerCounts === 'function'
  );
}

function isGlobalCountSource(value: unknown): value is GlobalCountSource {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as GlobalCountSource).listenerCount === 'number'
  );
}