npm package: @singcl/ad-execute-manager
Description: A powerful and flexible ad execution management library for handling reward-based ads, interstitial ads, and other advertising formats in JavaScript applications.
Author: singcl
Latest version: 1.17.16

# AD Execute Manager

A powerful and flexible ad execution management library for handling reward-based ads, interstitial ads, and other advertising formats in JavaScript applications.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Example Code](#example-code)
- [API Reference](#api-reference)
- [Development Guide](#development-guide)
- [License](#license)

## Features

- **Unified Ad Execution Interface**: Single interface for managing different types of ads
- **Task Queue Management**: Handles multiple ad execution tasks in a queue
- **Flexible Control Flow**: Manual control over ad execution flow with `next` function
- **Comprehensive Error Handling**: Complete error handling and logging
- **State Persistence**: Built-in storage for ad state management
- **Analytics Integration**: Built-in analytics support
- **Middleware Pattern**: Uses middleware pattern for ad execution flow
- **Cancellation Support**: Ability to clear and cancel pending tasks

## Installation

```bash
npm install @singcl/ad-execute-manager
```

## Quick Start

### Basic Usage

```javascript
import { AdExecuteManager, RewardAdFather } from '@singcl/ad-execute-manager';

// Get the singleton instance
const adManager = AdExecuteManager.getInstance();

// Create an ad instance (extend RewardAdFather)
class MyRewardAd extends RewardAdFather {
  async ad(ctx, next) {
    // Your ad logic here
    console.log('Executing reward ad');
    
    // Call next when ready to proceed
    await next();
    
    return { success: true, message: 'Ad executed successfully' };
  }
}

// Create ad instance
const myAd = new MyRewardAd();

// Add task to execution queue
const result = await adManager.addTask(myAd, {
  options: { /* ad options */ },
  collection: { /* callback collection */ }
});
```

### Advanced Usage

```javascript
import { AdExecuteManager, RewardAdSceneTriggerManager } from '@singcl/ad-execute-manager';

// Initialize with logging enabled
const adManager = AdExecuteManager.getInstance({ log: true });

// Check if manager is running
if (adManager.isRunning()) {
  console.log('Ad manager is currently executing tasks');
}

// Get current task ID
const currentTaskId = adManager.getCurrentTaskId();

// Get total number of pending tasks
const taskCount = adManager.getTaskCount();

// Wait for all tasks to complete
await adManager.whenAllTasksComplete();

// Clear all pending tasks
adManager.clearTasks();
```

## Core Concepts

### Ad Execution Flow

AD Execute Manager uses a middleware pattern to handle ad execution flow. Each ad task goes through the following steps:

1. **Initialization**: Create ad instance and configure parameters
2. **Queue Management**: Ad task enters the execution queue
3. **Ad Execution**: Call the `ad` method of the ad instance
4. **Completion Callback**: Call callback functions after ad execution completes
5. **Task Cleanup**: Clean up task resources and execute the next task

### Ad Types

- **Reward Ads**: Inherit from `RewardAdFather`, used for scenarios where users need to complete viewing to receive rewards
- **Interstitial Ads**: Inherit from `InterstitialAdFather`, used for scenarios where ads are inserted into the application flow

### Scene Management

Manage ad trigger scenes through `RewardAdSceneTriggerManager`, which allows executing different ad logic based on different scenes.

### Frequency Control

Use `CountRecorder` to implement ad display frequency control, which can set daily display limits.

## Example Code

### 1. Launch Reward Ad

```javascript
import { CountRecorder, PubSub } from '@singcl/ad-execute-manager';
import CommonSettings from './CommonSettings';
import { SCENT_TEXT_OBJ } from './const';
import RewardAdNovelExb from './RewardAdNovelExb';

class RewardAdLaunch extends RewardAdNovelExb {
  _scene = SCENT_TEXT_OBJ.launch_ad; // Ad execution scene
  
  constructor(args) {
    super(args);
    this.launchSettings = RewardAdLaunchSettings.new();
  }

  adCloseLister(args) {
    this._clearAdTimeout();
    this._adCloseGlobalRecorder(args);
    this._adCloseSuccessAnalytics({ scene: this._scene, ad_is_completed: args.isEnded ? 1 : 0, ad_count: args.count });

    this.launchSettings.updateToday(); // Update today
    this.launchSettings.updateFrequency();
    const baseArgs = { ...args, scene: this._scene };
    const _end = (ctx) => {
      this.adDestroy();
      this._resolve?.(Object.assign({}, args, { scene: this._scene }, ctx));
      this._resolve = null;
      this._next?.(); // Execute the next task's callback function to continue the flow
      this._next = null;
    };

    const _circle = () => {
      if (this.launchSettings.remainFrequency() > 0) {
        this._adInner({ options: { scene: this._scene }, collection: { resolve: this._resolve } }, this._next);
      } else {
        _end({ frequency: this.launchSettings.getFrequency() });
      }
    };
    if (args.isEnded) {
      this.launchSettings.updateFinished();
      if (this.launchSettings.remainFrequency() == 0) {
        this.launchSettings.updateLastFinished(true);
      }
      this._outerFinishedCallback(baseArgs);
      this._onFinish?.(baseArgs);
    } else {
      this._outerHalfwayCallback(baseArgs);
      this._onHalfway?.(baseArgs);
    }
    this._outerCloseCallback();
    _circle();
  }

  static build(args) {
    if (!RewardAdLaunch.instance) {
      RewardAdLaunch.instance = new RewardAdLaunch(args);
    }
    return RewardAdLaunch.instance;
  }

  static getInstance() {
    if (!RewardAdLaunch.instance) {
      throw new Error('RewardAdLaunch instance is not init');
    }
    return RewardAdLaunch.instance;
  }

  static new(args) {
    return new RewardAdLaunch(args);
  }

  static ad = new Promise((resolve) => {
    RewardAdLaunch.adResolve = resolve;
  });

  static satisfy(options, callback) {
    const con = RewardAdLaunchSettings.new().condition();
    if (typeof callback !== 'function') {
      return Promise.resolve();
    }
    return callback(con ? { options } : null);
  }

  static eventEmitter = new PubSub();
}

export default RewardAdLaunch;

class RewardAdLaunchSettings {
  _fixedFrequency = null; // Fixed display frequency, if null, use configuration
  frequency = {
    total: 0,
    current: 0,
    finished: 0,
    lastFinished: false, // Whether the last one is completed
  };
  constructor() {
    this.commonSettings = CommonSettings.new();
    this.countRecorder = CountRecorder.new({
      local_sign: 'launch_count',
      total: this._adTimes(),
      userId: this.commonSettings.getUserId(),
    });
    this.frequency.total = this.timesPerFrequency();
  }

  _adTimes() {
    // Get launch ad configuration from system configuration
    const { inter_site_pop_ups, inter_site_pop_ups_num } = this.commonSettings.getSysConfig();
    // Return configuration value if it exists, otherwise return 0
    return Number(inter_site_pop_ups) > 0 && inter_site_pop_ups_num >= 1 ? Number(inter_site_pop_ups_num) : 0;
  }

  updateToday() {
    this.countRecorder.updateToday();
  }

  updateFrequency() {
    this.frequency.current += 1;
  }

  updateFinished() {
    this.frequency.finished += 1;
  }

  updateLastFinished(v = true) {
    this.frequency.lastFinished = v;
  }

  remainFrequency() {
    return this.frequency.total - this.frequency.current;
  }

  getFrequency() {
    return Object.assign({}, this.frequency);
  }

  timesPerFrequency() {
    const { inter_site_pop_ups } = this.commonSettings.getSysConfig();
    const ups = this._fixedFrequency ?? Number(inter_site_pop_ups);
    const count = Math.min(Math.max(0, Number(ups)), this._remain());
    return Math.max(0, count);
  }

  _remain() {
    return this.countRecorder.remain();
  }

  condition() {
    const remain = this._remain();
    return Number(remain) > 0 && this.timesPerFrequency() > 0;
  }

  static new(args) {
    return new RewardAdLaunchSettings(args);
  }
}
```

### 2. Interstitial Ad

```javascript
import AdExecuteManager, { CountRecorder, Logger } from '@singcl/ad-execute-manager';
import { matchErrorWithKeywords, getCurrentPageInterScene } from './_utils';
import { SCENT_TEXT_OBJ } from './const';
import InterstitialAdNovelExb from './InterstitialAdNovelExb';
import RewardAdGlobalRecorder from './RewardAdGlobalRecorder';
import CommonSettings from './CommonSettings';

class InterstitialAdNormal extends InterstitialAdNovelExb {
  _scene = SCENT_TEXT_OBJ.other;
  _launchGap = 30 * 1000;
  _adBetweenGap = 60 * 1000;
  _timer = null;
  _adClose = 20000;
  _backgroundRetryTime = 3000; // Retry interval when app is in background
  _foregroundRetryTime = 5000; // Retry interval when app is in foreground
  
  constructor(args) {
    super(args);
    this.logger = new Logger({ prefix: InterstitialAdNormal.name });
    this.commonSettings = CommonSettings.new();
    this.countRecorder = CountRecorder.new({
      local_sign: 'interstitial_show_count',
      total: this.commonSettings.getCpAdDetails().dayAd,
      userId: this.commonSettings.getUserId(),
    });
    this._launchGap = this.commonSettings.getCpAdDetails().firstAdGap * 1000;
    this._adBetweenGap = this.commonSettings.getCpAdDetails().secondAdGap * 1000;
    this._adClose = this.commonSettings.getCpAdDetails().adClose ?? 20000;
  }

  _adCloseSuccessAnalytics(_args) {
    // ExbAnalyticsJS.getInstance().track('incentive_ad_close', {
    //   scene: _args.scene,
    //   ad_is_completed: _args.ad_is_completed,
    // });
  }

  _onInnerAdShowSuccess() {
    this.countRecorder.updateToday(); // Update today's display count
    if (this._adClose) {
      setTimeout(() => {
        this.adCloseLister();
      }, this._adClose);
    }
  }

  async launch() {
    const recordIns = RewardAdGlobalRecorder.getInstance();
    const launchAdLastShowTime = recordIns.launchAdLastShowTime;
    const startLaunchTime = Math.max(this._launchGap - (new Date().getTime() - launchAdLastShowTime), 0);
    
    const circle = () => {
      return new Promise((resolve) => {
        const _fn = async () => {
          await AdExecuteManager.getInstance().whenAllTasksComplete();
          const nowTime = new Date().getTime();
          const rewardAdLastShowTime = recordIns.rewardAdLastShowTime;
          const interstitialAdLastShowTime = recordIns.interstitialAdLastShowTime;
          
          const remain = Math.max(
            this._adBetweenGap - (nowTime - Math.max(rewardAdLastShowTime, interstitialAdLastShowTime)),
            0
          );
          if (remain > 0) {
            setTimeout(_fn, remain);
          } else {
            resolve();
          }
        };
        _fn();
      });
    };
    
    const fn2 = async (t) => {
      this._timer = setTimeout(async () => {
        if (this.countRecorder.remain() <= 0) {
          clearTimeout(this._timer);
          this._timer = null;
          return;
        }
        await circle();
        let res = null;
        if (this.getAppDisplayStatus() === 'hide') {
          const msg = `app in background: pause ad show, interstitial ad, time retry: ${this._backgroundRetryTime}ms`;
          this.logger.log(msg);
          res = {
            apiError: {
              errMsg: msg,
              errorCode: 110000,
            },
          };
          this.record(res);
        } else {
          this.logger.log(`Ad entering queue, will play soon, GAP: ${this._adBetweenGap}ms`);
          res = await this.addExecuteManager({
            options: { retry: 0, scene: getCurrentPageInterScene() || this._scene },
          });
        }
        if (res && !res.apiError) {
          clearTimeout(this._timer);
          this._timer = null;
          fn2(this._adBetweenGap);
        } else {
          const e = res?.apiError;
          if (matchErrorWithKeywords(this._ttErrorMsgs, e?.errMsg) || this._ttErrorCodes.includes(e?.errorCode)) {
            clearTimeout(this._timer);
            this._timer = null;
            return;
          }
          setTimeout(
            () => {
              clearTimeout(this._timer);
              this._timer = null;
              fn2(0);
            },
            this.getAppDisplayStatus() === 'hide' ? this._backgroundRetryTime : this._foregroundRetryTime
          );
        }
      }, t);
    };

    fn2(startLaunchTime);
  }

  getAppDisplayStatus() {
    return RewardAdGlobalRecorder.getInstance().appDisplayStatus;
  }

  static build(args) {
    if (!InterstitialAdNormal.instance) {
      InterstitialAdNormal.instance = new InterstitialAdNormal(args);
    }
    return InterstitialAdNormal.instance;
  }

  static getInstance() {
    if (!InterstitialAdNormal.instance) {
      throw new Error('InterstitialAdNormal instance is not init');
    }
    return InterstitialAdNormal.instance;
  }

  static new(args) {
    return new InterstitialAdNormal(args);
  }
}

export default InterstitialAdNormal;
```

### 3. Ad Scene Trigger Manager

```javascript
import { Logger } from '@singcl/ad-execute-manager';
import { SCENT_TYPE_OBJ } from './const';

class RewardAdSceneTriggerManager {
  static instance = null;
  _initSign = ''; // Initialization sign
  _currScene = null; // Current scene
  
  constructor(args) {
    if (RewardAdSceneTriggerManager.instance) {
      return RewardAdSceneTriggerManager.instance;
    }
    this._initSign = args?.sign ?? ''; // Initialization sign
    this.logger = new Logger({ prefix: '' });
    RewardAdSceneTriggerManager.instance = this;
  }

  initialize(args) {
    // Initialization logic
  }

  addScene(value) {
    this.logger.log('-------------AD trigger scene:--------------', value);
    this._currScene = value;
    return this;
  }

  addSceneType(args) {
    this.addScene(SCENT_TYPE_OBJ[args.scene]);
    return this;
  }

  getCurrentScene() {
    return this._currScene;
  }

  placeholder() {
    return null;
  }

  static build(args) {
    if (!RewardAdSceneTriggerManager.instance) {
      RewardAdSceneTriggerManager.instance = new RewardAdSceneTriggerManager(args);
    }
    return RewardAdSceneTriggerManager.instance;
  }

  static getInstance() {
    if (!RewardAdSceneTriggerManager.instance) {
      throw new Error('RewardAdSceneTriggerManager instance is not init');
    }
    return RewardAdSceneTriggerManager.instance;
  }
}

export default RewardAdSceneTriggerManager;
```

## API Reference

### AdExecuteManager

The main class for managing ad execution flow.

#### Methods

- `getInstance(args)`: Get the singleton instance of AdExecuteManager
- `getSafeInstance()`: Get the instance, returns null if not initialized
- `addTask(adInstance, ctx)`: Add an ad task to the execution queue
- `clearTasks()`: Cancel all pending tasks
- `getTaskCount()`: Get the number of pending tasks
- `isRunning()`: Check if tasks are currently running
- `getCurrentTaskId()`: Get the ID of the current executing task
- `whenAllTasksComplete()`: Returns a Promise that resolves when all tasks are complete

### RewardAdFather

Base class for reward ad implementations.

### InterstitialAdFather

Base class for interstitial ad implementations.

### Other Exports

- `SerializableError`: Error class that can be serialized
- `Logger`: Logging utility
- `Storage`: Storage management
- `CountRecorder`: Ad execution counter
- `RewardAdGlobalRecorder`: Global ad recorder
- `RewardAdSceneTriggerManager`: Scene-based ad trigger manager
- `AdAnalyticsJS`: Analytics integration
- `RewardAdNovel`: Novel-specific reward ad implementation
- `InterstitialAdNovel`: Novel-specific interstitial ad implementation
- `PubSub`: Publish-subscribe pattern implementation

## Development Guide

### Development Scripts

- `npm run build`: Build the library for production
- `npm run dev`: Turn on watch mode
- `npm run lint`: Lint your code
- `npm run format`: Format your code
- `npm run test`: Run tests

### Project Structure

```
src/
  ad/             # Core ad-related code
  helper/         # Helper utilities
  typings/        # Type definitions
  utils/          # Common utilities
  index.js        # Entry file
example/         # Example code
  AdUnlock/       # Ad unlock related examples
  mini_program/   # Mini program examples
  typings/        # Example type definitions
  *.js            # Various ad implementation examples
```

## License

This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.