Skip to content

EventManager

Ahmed Abbas edited this page Apr 23, 2025 · 3 revisions

Chapter 9: EventManager

Welcome back! In Chapter 8: ApiManager, we saw how the SDK acts like a messenger, talking to the Convert servers to fetch configuration data and send back tracking reports. This handles communication with the outside world.

But how do different parts inside the SDK talk to each other? For example, when the ApiManager successfully fetches the configuration data, how does it tell the rest of the SDK "Okay, the data is here, we're ready to go!"? How does the Core module know when to resolve the onReady() promise we used back in Chapter 1: ConvertSDK / Core?

The Problem: Internal Announcements

Imagine a large office building (the SDK). Different departments (the Managers like DataManager, ApiManager, etc.) are working on their tasks. When one department finishes something important, how do they notify other departments that might need to know?

  • The Mail Room (ApiManager) receives a big package (the project configuration). How does it announce this arrival?
  • The Decision Desk (BucketingManager) assigns someone to a specific project (buckets a visitor). How can the Logging Department know about this decision to record it?
  • The Front Desk (ConvertSDK / Core) needs to know when the entire office is officially open for business (SDK is initialized and ready).

Shouting across the office isn't efficient! They need a structured way to make announcements and for interested departments to listen to those announcements.

What is EventManager? The Office Intercom System

Meet the EventManager! Think of it as the internal intercom or announcement system for the Convert SDK. It allows different parts of the SDK to broadcast messages (events) and other parts (or even you, the user) to listen for specific messages.

It works on a simple Publish/Subscribe (or "Pub/Sub") model:

  1. Publishing (Firing Events): When something significant happens, a component can use the EventManager to fire an event. This is like making an announcement over the intercom. For example:

    • The Core fires the READY event when initialization is complete.
    • The Core fires CONFIG_UPDATED when new configuration data is fetched.
    • The Context fires BUCKETING when a visitor is assigned to a variation.
    • The Context fires CONVERSION when trackConversion is called.
  2. Subscribing (Listening with on): Other components (or your code) can tell the EventManager they are interested in specific announcements using the on method. This is like tuning a radio to a specific station. When that event is fired, the listener's callback function is executed.

This system allows different parts of the SDK to communicate and react to happenings without being tightly coupled or needing direct references to each other.

How it's Used

1. The onReady() Convenience Method:

The most common way you'll interact with the EventManager's work is indirectly, through the convert.onReady() method we saw in Chapter 1: ConvertSDK / Core.

import ConvertSDK from '@convertcom/js-sdk';

const convert = new ConvertSDK({ sdkKey: 'YOUR_SDK_KEY' });

console.log('Waiting for SDK to be ready...');

// This relies on the EventManager!
convert.onReady().then(() => {
  console.log('SDK is ready! The READY event was fired internally.');
  // Now you can safely use convert.createContext(...) etc.
}).catch(error => {
  console.error('SDK initialization failed:', error);
});
  • What happens? When you call new ConvertSDK(...), the SDK starts initializing. Once the initial configuration data is fetched and processed, the Core module uses the EventManager to fire(SystemEvents.READY).
  • The convert.onReady() method had previously set up a listener (using eventManager.on(SystemEvents.READY, ...)) for this specific event.
  • When the READY event is fired, the listener's callback runs, which in turn resolves the Promise returned by onReady(), allowing your .then() block to execute.

2. Listening to Other SDK Events (Optional):

While onReady() is the main one, you can also listen for other events fired by the SDK if you need to hook into specific internal actions. You do this using the convert.on() method (which is a direct wrapper around eventManager.on()).

import ConvertSDK from '@convertcom/js-sdk';
// Import the event names enum for clarity
import { SystemEvents } from '@convertcom/js-sdk-enums';

const convert = new ConvertSDK({ sdkKey: 'YOUR_SDK_KEY' });

convert.onReady().then(() => {
  console.log('SDK Ready. Setting up listeners...');

  // Listen for when any visitor bucketing happens
  convert.on(SystemEvents.BUCKETING, (bucketingData, error) => {
    if (error) {
      console.error('Error during bucketing:', error);
      return;
    }
    console.log('A bucketing decision was made:', bucketingData);
    // Example bucketingData: { visitorId: 'user123', experienceKey: '...', variationKey: '...' }
  });

  // Listen for when conversion tracking happens
  convert.on(SystemEvents.CONVERSION, (conversionData, error) => {
    if (error) {
      console.error('Error during conversion:', error);
      return;
    }
    console.log('A conversion was tracked:', conversionData);
    // Example conversionData: { visitorId: 'user123', goalKey: 'signup', goalData: {...} }
  });

  // Listen for when the configuration is updated after the initial load
  convert.on(SystemEvents.CONFIG_UPDATED, (configData, error) => {
     console.log('SDK configuration was updated in the background.');
  });

  // Now, when you use the SDK, these listeners will be triggered:
  const visitorContext = convert.createContext('user123');
  // This will trigger the BUCKETING listener:
  visitorContext.runExperience('some-experience');
  // This will trigger the CONVERSION listener:
  visitorContext.trackConversion('signup');

});
  • convert.on(eventName, callbackFunction) lets you subscribe to specific internal events.
  • SystemEvents provides predefined names for common SDK events (READY, CONFIG_UPDATED, BUCKETING, CONVERSION, etc.).
  • The callbackFunction receives arguments related to the event (like details of the bucketing or conversion) and potentially an error object.

This allows for advanced use cases, like sending SDK event data to your own analytics system or debugging internal SDK behaviour.

Under the Hood: The Intercom Mechanism

How does the EventManager manage these announcements and listeners?

1. Keeping Lists (_listeners): The EventManager maintains an internal record (like a directory or a Javascript object/map called _listeners). The keys of this record are the event names (e.g., 'ready', 'bucketing'), and the value for each key is a list (an array) of all the callback functions that have subscribed to that event.

// Simplified internal state of _listeners
{
  'ready': [ function1_for_ready, function2_for_ready ],
  'bucketing': [ functionA_for_bucketing ],
  'conversion': [ functionX_for_conversion, functionY_for_conversion ]
}

2. Subscribing (on): When you call eventManager.on('eventName', callbackFn), it simply finds the list for 'eventName' in the _listeners record (or creates it if it doesn't exist) and adds callbackFn to that list.

3. Firing (fire): When some code calls eventManager.fire('eventName', eventArgs, error), the EventManager:

  1. Looks up 'eventName' in its _listeners record.
  2. If it finds a list of listeners for that event:
    • It iterates through each callback function in the list.
    • It calls each function, passing the eventArgs and error object to it.
  3. If no listeners are found for 'eventName', it does nothing.

4. Deferred Events (like READY): What if you call convert.onReady() after the SDK has already finished initializing and fired the READY event? You still want your .then() block to run! The EventManager has a mechanism for this using the deferred flag in the fire method.

  • When fire('ready', ..., true) is called, besides notifying current listeners, it also stores the fact that the 'ready' event has happened (in another internal record, _deferred).
  • Later, when eventManager.on('ready', callbackFn) is called, the on method checks the _deferred record. If it sees that 'ready' has already fired, it immediately executes the callbackFn with the stored arguments.

This ensures that listeners added after a deferred event has fired still get notified.

Sequence Diagram: onReady Flow

sequenceDiagram
    participant UserCode as Your Code
    participant Core as ConvertSDK / Core
    participant EventMgr as EventManager
    participant ApiMgr as ApiManager

    UserCode->>Core: new ConvertSDK()
    UserCode->>Core: onReady()
    Core->>+EventMgr: on('ready', callbackForPromise)
    EventMgr-->>-Core: Listener registered
    Core->>+ApiMgr: getConfig()
    Note right of Core: SDK Initialization starts...
    ApiMgr-->>Core: Returns Config Data
    Core->>Core: Process data, setup complete.
    Core->>+EventMgr: fire('ready', null, null, true)  // Fire READY event (deferred)
    Note right of EventMgr: Found listener 'callbackForPromise'
    EventMgr->>Core: Execute callbackForPromise()
    Core->>UserCode: Resolve the onReady() Promise
    EventMgr-->>-Core: 
    UserCode-->>UserCode: .then() block runs
Loading

Code Dive: EventManager Implementation

Let's look at simplified snippets from packages/event/src/event-manager.ts.

1. The Constructor

Initializes the storage for listeners and deferred events.

// File: packages/event/src/event-manager.ts (Simplified)

import {SystemEvents} from '@convertcom/js-sdk-enums';
import {LogManagerInterface} from '@convertcom/js-sdk-logger';

export class EventManager implements EventManagerInterface {
  private _loggerManager: LogManagerInterface | null;

  // Stores lists of callbacks for each event name
  _listeners: Record<string, Array<any>>;
  // Stores events that have already fired (for deferred handling)
  _deferred: Record<string, { args: any, err: any }>;

  // Optional function to transform event arguments before sending to listeners
  private _mapper: (...args: any) => any;

  constructor(config?: Config, { loggerManager }: { loggerManager?: LogManagerInterface } = {}) {
    // Initialize empty records
    this._listeners = {};
    this._deferred = {};

    this._loggerManager = loggerManager;
    // Use a default mapper if none provided
    this._mapper = config?.mapper || ((value: any) => value);
  }
  // ... methods ...
}
  • Sets up _listeners and _deferred as empty objects.
  • Stores the optional loggerManager.
  • Sets up a _mapper function (usually just returns the value as-is).

2. Subscribing to Events (on)

Adds a function to the listener list for a specific event.

// File: packages/event/src/event-manager.ts (Simplified)

// Inside EventManager class:
  /**
   * Add listener for event
   */
  on(event: SystemEvents | string, fn: (args: any, err: any) => void): void {
    // Find the list for 'event', or create an empty list if it's the first listener
    const listenersForEvent = (this._listeners[event] = this._listeners[event] || []);
    // Add the new callback function 'fn' to the list
    listenersForEvent.push(fn);

    this._loggerManager?.trace?.('EventManager.on()', {event: event});

    // === Handle Deferred Events ===
    // Check if this event has already fired and was marked as deferred
    if (Object.hasOwnProperty.call(this._deferred, event)) {
      // If yes, fire it again immediately just for this new listener
      // using the stored arguments/error from the first time it fired.
      const deferredData = this._deferred[event];
      this.fire(event, deferredData.args, deferredData.err); // Fire non-deferred this time
    }
  }
  • It ensures there's an array for the given event key in _listeners.
  • It pushes the provided callback function fn onto that array.
  • Crucially, it checks the _deferred record. If the event was previously fired as deferred, it immediately calls fire again (non-deferred) to notify this newly added listener.

3. Firing Events (fire)

Notifies all registered listeners for a specific event.

// File: packages/event/src/event-manager.ts (Simplified)

// Inside EventManager class:
  /**
   * Fire event with provided arguments and/or errors
   */
  fire(
    event: SystemEvents | string, // e.g., 'ready', 'bucketing'
    args: Record<string, unknown> | unknown = null, // Data associated with the event
    err: Error | any = null, // Optional error object
    deferred = false // Should this event be remembered if fired before listeners exist?
  ): void {
    this._loggerManager?.debug?.('EventManager.fire()', { event, args, err, deferred });

    // Get the list of listeners for this specific event (or an empty list if none)
    const listenersForEvent = this._listeners[event] || [];

    // Loop through all registered callback functions for this event
    for (const fn of listenersForEvent) {
      if (typeof fn === 'function') {
        try {
          // === Call the Listener ===
          // Apply the optional mapper to the arguments first
          const mappedArgs = this._mapper(args);
          // Execute the listener's callback function
          fn.apply(null, [mappedArgs, err]);
        } catch (error) {
          // Log errors within listener functions so they don't crash the SDK
          this._loggerManager?.error?.('EventManager.fire() listener error', error);
        }
      }
    } // End loop through listeners

    // === Handle Deferred Firing ===
    // If the 'deferred' flag is true AND we haven't already stored this event
    if (deferred && !Object.hasOwnProperty.call(this._deferred, event)) {
      // Store the arguments and error in the _deferred record
      this._deferred[event] = { args, err };
      this._loggerManager?.trace?.('Stored deferred event', {event});
    }
  }
  • It retrieves the list of listener functions for the given event.
  • It loops through the list and calls each function (fn), passing the (potentially mapped) args and err.
  • It includes error handling (try...catch) around the listener call.
  • If the deferred flag is set to true, it stores the event arguments in the _deferred record so that listeners added later via on can be notified immediately.

Conclusion

The EventManager is the SDK's internal communication backbone, implementing a simple publish/subscribe pattern. It allows different components to signal important occurrences (fire) and others to react to them (on) without direct dependencies.

You've learned:

  1. Why an internal event system is needed for communication between SDK modules.
  2. The "intercom" or "pub/sub" analogy for EventManager.
  3. How components fire events (like READY, BUCKETING, CONVERSION).
  4. How components (or your code) can listen using on.
  5. How the convert.onReady() method relies on the READY event fired by EventManager.
  6. How deferred events ensure listeners added late are still notified (important for READY).

We've now explored all the major managers and core components of the Convert SDK! We understand how they work together, from initialization and context creation to rule checking, bucketing, API communication, and internal event handling.

To wrap things up, let's take a closer look at the configuration options you can pass when creating the ConvertSDK instance and the common data types used throughout the SDK.

Ready to configure? Let's dive into the final details in Chapter 10: Config / Types!

Clone this wiki locally