 x.length;
    return y;
}

//Space Remove
function spaceRemove(x:String): number {
    let y : number = 0;
    y = x.replace(/\s+/,"").length;
    return y;
}<|endoftext|>import { RawMap } from '@/store/types';
import { testMapViewState3 } from '../../../../resources/testMapViewState3';
import Map from '@/Map/Map.ts';
import Spot from '@/Spot/Spot.ts';
import { createMapInstance, createSpotInstance, initMap } from '@/store/modules/MapViewModule/MapViewState';

describe('初期化に利用する関数のテスト', () => {
    it('createMapInstanceがMap型を返す', () => {
        const testRawMap = testMapViewState3.maps;
        const testRawMapData = testRawMap[1];
        const testMapInstance = createMapInstance(testRawMapData);
        const expectedMapProperties = {
            id: testRawMapData.id,
            name: testRawMapData.name,
            bounds: testRawMapData.bounds,
            floorName: testRawMapData.floorName,
        };
        const actualMapProperties = {
            id: (testMapInstance as any).id,
            name: (testMapInstance as any).name,
            bounds: (testMapInstance as any).bounds,
            floorName: (testMapInstance as any).floorName,
        };
        expect(actualMapProperties).toStrictEqual(expectedMapProperties);
    });
    it('createSpotInstanceがSpot型を返す', () => {
        const testRawMap = testMapViewState3.maps;
        const testRawMapData = testRawMap[0];
        const testSpotInstance = createSpotInstance(testRawMapData.spots[0]);
        const expectedSpotProperties = {
            id: testRawMapData.spots[0].id,
            name: testRawMapData.spots[0].name,
            coordinate: testRawMapData.spots[0].coordinate,
        };
        const actualSpotProperties = {
            id: (testSpotInstance as any).id,
            name: (testSpotInstance as any).name,
            coordinate: (testSpotInstance as any).coordinate,
        };
        expect(actualSpotProperties).toStrictEqual(expectedSpotProperties);
    });
});

describe('地図データ初期化のテスト', () => {
    /**
     * テストデータの構造は以下の通り。
     * rootMap(id:0) -> rootSpot(id:0) -> detailMap(id:1) -> detailSpot(id:0)
     * Mapインスタンス、Spotインスタンスそれぞれについてプロパティを検証する。
     */
    const testMapData: RawMap[] = testMapViewState3.maps;
    // rootMap
    const actualRootMap: Map = initMap(testMapData);
    const expectedRootMapData = testMapData[0];
    // rootSpot
    const actualRootSpot = (actualRootMap as any).spots[0];
    const expectedRootSpotData = expectedRootMapData.spots[0];
    // detailMap
    const actualDetailMap = (actualRootMap as any).spots[0].detailMaps[0];
    const expectedDetailMapData = testMapData[1];
    // detailSpot
    const actualDetailSpot = actualDetailMap.spots[0];
    const expectedDetailSpotData = expectedDetailMapData.spots[0];

    it('rootMapのプロパティ検証', () => {
        const actualRootMapProperties = {
            id: (actualRootMap as any).id,
            name: (actualRootMap as any).name,
            bounds: (actualRootMap as any).bounds,
            floorName: (actualRootMap as any).floorName,
            parentSpot: (actualRootMap as any).parentSpot,
            spotsNum: (actualRootMap as any).spots.length,
        };
        const expectedRootMapProperties = {
            id: expectedRootMapData.id,
            name: expectedRootMapData.name,
            bounds: expectedRootMapData.bounds,
            floorName: expectedRootMapData.floorName,
            parentSpot: undefined,
            spotsNum: 1,
        };
        expect(actualRootMapProperties).toStrictEqual(expectedRootMapProperties);
    });

    it('rootMapが持つSpotのプロパティ検証', () => {
        const actualRootSpotProperties = {
            id: actualRootSpot.id,
            name: actualRootSpot.name,
            coordinate: actualRootSpot.coordinate,
            floorName: actualRootSpot.floorName,
            parentMapName: actualRootSpot.parentMap.name,
            detailMapNum: actualRootSpot.detailMaps.length,
        };
        const expectedRootSpotDataProperties = {
            id: expectedRootSpotData.id,
            name: expectedRootSpotData.name,
            coordinate: expectedRootSpotData.coordinate,
            floorName: undefined,
            parentMapName: 'Kyudai',
            detailMapNum: 1,
        };
        expect(actualRootSpotProperties).toStrictEqual(expectedRootSpotDataProperties);
    });

    it('detailMapのプロパティ検証', () => {
        const actualDetailMapProperties = {
            id: actualDetailMap.id,
            name: actualDetailMap.name,
            bounds: actualDetailMap.bounds,
            floorName: actualDetailMap.floorName,
            parentSpotName: actualDetailMap.parentSpot.name,
            spotsNum: actualDetailMap.spots.length,
        };
        const expectedDetailMapProperties = {
            id: expectedDetailMapData.id,
            name: expectedDetailMapData.name,
            bounds: expectedDetailMapData.bounds,
            floorName: expectedDetailMapData.floorName,
            parentSpotName: 'SougouGakusyuPlaza',
            spotsNum: 1,
        };
        expect(actualDetailMapProperties).toStrictEqual(expectedDetailMapProperties);
    });

    it('detailMapが持つSpotのプロパティ検証', () => {
        const actualDetailSpotProperties = {
            id: actualDetailSpot.id,
            name: actualDetailSpot.name,
            coordinate: actualDetailSpot.coordinate,
            floorName: actualDetailSpot.floorName,
            parentMapName: actualDetailSpot.parentMap.name,
            detailMapNum: actualDetailSpot.detailMaps.length,
        };
        const expectedDetailSpotDataProperties = {
            id: expectedDetailSpotData.id,
            name: expectedDetailSpotData.name,
            coordinate: expectedDetailSpotData.coordinate,
            floorName: '1F',
            parentMapName: 'SougouGakusyuPlaza_1F',
            detailMapNum: 0,
        };
        expect(actualDetailSpotProperties).toStrictEqual(expectedDetailSpotDataProperties);
    });
});<|endoftext|>import EventBus, { CanvasEvent } from '../eventBus';
import { CollisionRect } from './Collision';

export interface Point {
  x: number;
  y: number;
}

export default abstract class ClickAble {

  abstract get rect(): CollisionRect;

  public addEventListener(type: string, handle: (evt: CanvasEvent) => void) {
    EventBus.addEventListener(type, (evt: CanvasEvent) => {
      if (!this.shouldDispatch(evt)) {
        return;
      }
      handle(evt);
    });
  }

  public removeEventListener(type: string, handle: (evt: CanvasEvent) => void) {
    EventBus.removeEventListener(type, handle);
  }

  public pointInRect(point: Point) {
    const { rect: { x, y, width, height } } = this;
    return point.x >= x && point.x <= x + width && point.y >= y && point.y <= y + height;
  }

  public shouldDispatch(e: CanvasEvent) {
    const { cx, cy } = e;
    return this.pointInRect({
      x: cx,
      y: cy
    });
  }

}<|endoftext|>import type { Adapter } from 'oidc-provider';
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
import {
  PassthroughAdapter,
  PassthroughAdapterFactory,
} from '../../../../src/identity/storage/PassthroughAdapterFactory';

describe('A PassthroughAdapterFactory', (): void => {
  let sourceFactory: jest.Mocked<AdapterFactory>;
  let factory: PassthroughAdapterFactory;

  beforeEach(async(): Promise<void> => {
    sourceFactory = {
      createStorageAdapter: jest.fn(),
    };

    factory = new PassthroughAdapterFactory(sourceFactory);
  });

  it('calls the source createStorageAdapter function.', async(): Promise<void> => {
    expect(factory.createStorageAdapter('Client')).toBeUndefined();
    expect(sourceFactory.createStorageAdapter).toHaveBeenCalledTimes(1);
    expect(sourceFactory.createStorageAdapter).toHaveBeenLastCalledWith('Client');
  });

  describe('A PassthroughAdapter', (): void => {
    let sourceAdapter: jest.Mocked<Adapter>;
    let adapter: PassthroughAdapter;

    beforeEach(async(): Promise<void> => {
      sourceAdapter = {
        upsert: jest.fn(),
        find: jest.fn(),
        findByUserCode: jest.fn(),
        findByUid: jest.fn(),
        consume: jest.fn(),
        destroy: jest.fn(),
        revokeByGrantId: jest.fn(),
      };

      adapter = new PassthroughAdapter('Name', sourceAdapter);
    });

    it('passes the call to the source for upsert.', async(): Promise<void> => {
      await expect(adapter.upsert('id', 'payload' as any, 5)).resolves.toBeUndefined();
      expect(sourceAdapter.upsert).toHaveBeenCalledTimes(1);
      expect(sourceAdapter.upsert).toHaveBeenLastCalledWith('id', 'payload' as any, 5);
    });

    it('passes the call to the source for find.', async(): Promise<void> => {
      await expect(adapter.find('id')).


opts };
	const delim = settings.format === cssFormat.css4 ? CSS4_DELIM : CSS2_DELIM;

	let unit = '';
	let digits = Math.min(Math.max(settings.decimals, 0), CSS_MAX_DECIMALS);

	profile.compress(v, rgb64);

	if (settings.percent) {
		multiplyAssignScalar(v, UNIT_TO_PCT);

		unit = CSS_PCT;

		if (settings.precision === cssPrecision.uint8 && digits > 0) {
			align(v, v, UINT8_TO_PCT, 0.5 - 1e-10);

			digits = 1;
		}

		clampScalar(v, v, 0.0, UNIT_TO_PCT);
	}
	else {
		multiplyAssignScalar(v, UNIT_TO_UINT8);

		if (settings.precision === cssPrecision.uint8) {
			round(v, v);

			digits = 0;
		}

		clampScalar(v, v, 0.0, UNIT_TO_UINT8);
	}

	return `rgb(${
		toFixed(v.x, digits) }${ unit }${ delim }${
		toFixed(v.y, digits) }${ unit }${ delim }${
		toFixed(v.z, digits) }${ unit })`;
}<|endoftext|>import { expensiveOperation } from "./_utils";
const cache = {};

// #region 7... 🧛 Function Memoization

// make a salad
// -> EXPENSIVE to do
const makeSalad = async (food1, food2) => {
  console.time("👉 salad took ");

  await expensiveOperation();

  console.timeEnd("👉 salad took ");
  console.log(food1 + " with " + food2 + " ...yummy yummy");
};

// memoize makeSalad
const memoMakeSalad = async (food1, food2) => {
  console.time("👉 salad took ");

  if (cache[food1 + food2]) {
    // short circuit
    console.timeEnd("👉 salad took ");
    console.log(food1 + " with " + food2 + " ...yummy yummy");
  } else {
    // execute the actual operation
    await expensiveOperation();

    console.timeEnd("👉 salad took ");
    console.log(food1 + " with " + food2 + " ...yummy yummy");

    // store the result in cache
    cache[food1 + food2] = food1 + " with " + food2 + " ...yummy yummy";
  }
};

// #endregion

// #region 8... 🦄 Selector Memoization_

// composed selectors ('calculated data')
// -> memoized
// const getFood = createSelector(
//   getFruit,
//   getMeals,
//   (fruits, meals) => ({ ...fruits, ...meals })
// );

// #endregion

// #region blerg
export { makeSalad, memoMakeSalad };
// #endregion<|endoftext|>/** @module base/plugin */

import Events from '../events';
import Core from '../../components/core';
import Video from '../video';

/**
 * UI를 가지지 않는 플러그인의 베이스 클래스
 *
 * @extends Events
 */
export default abstract class Plugin extends Events {
  /**
   * 비디오 플레이어 코어
   */
  private _core: Core;

  /**
   * 플러그인의 활성화 여부
   */
  private _enabled: boolean;

  /**
   * 모든 플러그인은 내부에서 core를 갖는다.
   */
  get core(): Core {
    return this._core;
  }

  /**
   * core를 통해서 video에 접근할 수 있다.
   */
  get video(): Video {
    return this._core.video;
  }

  /**
   * 비디오 플레이어 활성화 여부
   */
  get enabled(): boolean {
    return this._enabled;
  }

  /**
   * core를 설정하고 이벤트 리스너를 등록한다.
   */
  constructor(core: Core) {
    super();
    this._core = core;
    this._enabled = true;
    this.addEventListeners();
  }

  /**
   * Core에 이벤트 리스너를 등록한다.
   * NOTE: disable/enable을 제대로 작동시키기 위해서 반드시 this.listenTo 메소드를 이용해 이벤트 리스너를 등록해야한다.
   */
  protected abstract addEventListeners(): void;

  /**
   * 플러그인을 작동시킨다.
   * 이 함수는 disable 이후에 다시 플러그인을 작동시킬 때만 사용해야 한다.
   */
  public enable(): void {
    if (this._enabled) return;
    this.addEventListeners();
    this._enabled = true;
  }

  /**
   * 플러그인을 작동하지 않도록 한다.
   */
  public disable(): void {
    this.stopListening();
    this._enabled = false;
  }

  /**
   * 플러그인을 파괴하여 등록된 이벤트 리스너를 전부 제거한다.
   */
  public destroy(): Plugin {
    this.off();
    this.stopListening();
    return this;
  }
}<|endoftext|>import Web3 from "web3";

let web3: Web3 | null = null;
let isEnabling = false;
let done = false;

const waitUntilEnabled = () => {
    const promize = async (res: any, rej: any) => {
        await waitFor(1000);
        if (!isEnabling && done) {
            return res("");
        }
        if (isEnabling && !done) {
            promize(res, rej);
            // check again after few seconds
        }
    };

    return new Promise(promize);
};

export const getWeb3 = async () => {
    console.log("called");
    //@ts-ignore
    if (!window.ethereum) {
        window.alert("Please install MetaMask first.");
        return;
    }

    if (!web3) {
        try {
            // Request account access if needed

            if (isEnabling) {
                await waitUntilEnabled();
            }

            if (!isEnabling && !done) {
                console.log("enabling");
                isEnabling = true;
                //@ts-ignore

                await window.ethereum.enable();
            }

            // We don't know window.web3 version, so we use our own instance of Web3
            // with the injected provider given by MetaMask
            //@ts-ignore
            web3 = new Web3(window.ethereum);
        } catch (error) {
            window.alert("You need to allow MetaMask.");
            return;
        }
    }
    return web3;
};

export const waitFor = (time: number) => {
    return new Promise((res) => {
        setTimeout(res, time);
    });
};
export function uuidv4() {
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
        /[xy]/g,
        function (c) {
            var r = (Math.random() * 16) | 0,
                v = c === "x" ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        }
    );
}

export const getQueryParam = (name: string) => {
    if (typeof window === "undefined") {
        return "";
    }
    const search = window.location.search;
    const queryString = decodeURIComponent(search.slice(1, search.length));
    const queryObj: any = {};
    queryString.split("&").forEach((item) => {
        const items = item.split("=");
        queryObj[items[0]] = items[1];
    });
    console.log(queryObj);
    return queryObj[name];
};

export const tuplify = <T extends any[]>(...args: T) => {
    return args;
};

function roundRect(
    ctx: CanvasRenderingContext2D,
    x: number,
    y: number,
    width: number,
    height: number,
    radius: any,
    color1: string,
    color2: string
) {
    if (typeof radius === "undefined") {
        radius = 5;
    }

    radius = { tl: radius, tr: radius, br: radius, bl: radius };

    ctx.beginPath();
    ctx.moveTo(x + radius.tl, y);
    ctx.lineTo(x + width - radius.tr, y);
    ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
    ctx.lineTo(x + width, y + height - radius.br);
    ctx.quadraticCurveTo(
        x + width,
        y + height,
        x + width - radius.br,
        y + height
    );
    ctx.lineTo(x + radius.bl, y + height);
    ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
    ctx.lineTo(x, y + radius.tl);
    ctx.quadraticCurveTo(x, y, x + radius.tl, y);
    ctx.closePath();
    const grd = ctx.createLinearGradient(0, 0, 80, 0);
    grd.addColorStop(0, color1);
    grd.addColorStop(1, color2);
    ctx.fillStyle = grd;

    ctx.fill();
}

export const generateGradientImage = (color1: string, color2: string) => {
    if (typeof window === "undefined") {
        console.error("Only works in Client");
        return "";
    }
    const canvas = document.createElement("canvas");
    canvas.style.display = "none";
    canvas.width = 80;
    canvas.height = 48


.Map>();
    private _browserLocationPromise: Promise<{ latitude: number, longitude: number }>;

    constructor(private loader: LazyGoogleMapsApiLoader) {
        // check browser location
        this.getBrowserLocation()
            .then(noop);
        // preload map immediately
        this.loader
            .load()
            .then(noop);
    }

    onApiLoad(): Promise<void> {
        return this
            .loader
            .load();
    }

    createMarker(options?: google.maps.MarkerOptions): Promise<google.maps.Marker> {
        return this
            .loader
            .load()
            .then(() => new google.maps.Marker(options));
    }

    createDirections(options?: google.maps.DirectionsRendererOptions): Promise<google.maps.DirectionsRenderer> {
        return this.loader
            .load()
            .then(() => {
                return new google.maps.DirectionsRenderer(options);
            });
    }

    getDirections(origin: LongLat,
        destination: LongLat): Promise<google.maps.DirectionsResult> {
        return this
            .loader
            .load()
            .then(() => {
                let svc = new google.maps.DirectionsService();
                return new Promise<google.maps.DirectionsResult>((resolve, reject) => {
                    let request = {
                        origin: new google.maps.LatLng(
                            (<google.maps.LatLngLiteral>origin).lat || (<Coordinates>origin).latitude,
                            (<google.maps.LatLngLiteral>origin).lng || (<Coordinates>origin).longitude
                        ),
                        destination: new google.maps.LatLng(
                            (<google.maps.LatLngLiteral>destination).lat || (<Coordinates>destination).latitude,
                            (<google.maps.LatLngLiteral>destination).lng || (<Coordinates>destination).longitude
                        ),
                        travelMode: google.maps.TravelMode.DRIVING
                    };
                    svc.route(request, (result: google.maps.DirectionsResult, status: google.maps.DirectionsStatus) => {
                        if (status === google.maps.DirectionsStatus.OK) {
                            resolve(result);
                        } else {
                            console.error({ message: 'fail to get directions', status, result });
                            reject({ status, result });
                        }
                    });
                });
            });
    }

    createMap(el: HTMLElement, options?: google.maps.MapOptions): Promise<google.maps.Map> {
        return this.loader
            .load()
            .then(() => {
                return this.getBrowserLocation()
                    .then((coords: Coordinates) => {
                        options = Object.assign({}, options, DEFAULT_MAP_OPTIONS, {
                            center: new google.maps.LatLng(coords.latitude, coords.longitude),
                            zoom: 8
                        });
                        return new google.maps.Map(el, options);
                    });

            });
    }

    getMap(name: string): Promise<google.maps.Map> {
        return this.loader
            .load()
            .then(() => <google.maps.Map>this._maps.get(name));
    }

    addMap(name: string, map: google.maps.Map): void {
        this._maps.set(name, map);
    }

    removeMap(name: string): boolean {
        return this._maps.delete(name);
    }

    createAutocomplete(input: ElementRef,
        options: google.maps.places.AutocompleteOptions): Promise<google.maps.places.Autocomplete> {
        return this.loader.load().then(() => {
            return new google.maps.places.Autocomplete(input.nativeElement, options);
        });
    }

    getBrowserLocation(): Promise<Coordinates | { longitude: number, latitude: number }> {
        if (this._browserLocationPromise) {
            return (this._browserLocationPromise);
        }

        return this._browserLocationPromise = new Promise<{ latitude: number, longitude: number }>((resolve) => {
            if (location.protocol === 'https' && navigator.geolocation) {
                navigator.geolocation.getCurrentPosition(
                    (success: { coords: Coordinates, timestamp: number }) => {
                        resolve(success.coords);
                    }, (error) => {
                        console.error(error);
                        if (error.code !== PositionError.PERMISSION_DENIED) {
                            console.warn(`Permission is accepted but error encounter with message: ${error.message}`);
                        }
                        // if user didn't answer return default
                        resolve(DefaultCoords);
                    });
            } else {
                // if browser do not support location API return default (NYC)
                resolve(DefaultCoords);
            }
        });

    }

    calculateMapBounds(markers: Array<Coordinates> = []) {
        return this.loader.load().then(() => {
            return new Promise<google.maps.LatLngBounds>((resolve, reject) => {

                if (markers && markers.length > 1) {
                    let latLng = new google.maps.LatLng(markers[0].latitude, markers[0].longitude);
                    let bounds: google.maps.LatLngBounds = new google.maps.LatLngBounds(latLng);
                    for (let i = 1; i < markers.length; i++) {
                        let marker = markers[i];
                        bounds.extend(new google.maps.LatLng(marker.latitude, marker.longitude));
                    }
                    resolve(bounds);
                    return;
                }
                reject({ error: 'There is no markers in markers array', centerTo: DefaultCoords });
                return;

            });
        });
    }
}<|endoftext|>import { TileType } from '../../game/models/tile';
import Node from './node';
import Tokenizer from '../tokenizer';

export default class Type extends Node {
    private tileType: TileType;

    constructor(tokenizer: Tokenizer) {
        super();

        const type = tokenizer.pop();
        switch (type) {
            case 'SOLID':
                this.tileType = TileType.SOLID;
                break;
            case 'EMPTY':
                this.tileType = TileType.EMPTY;
                break;
            case 'SPIKE':
                this.tileType = TileType.SPIKE;
                break;
            default:
                throw new Error(`Unknown tile type "${type}".`);
        }
    }

    public validate(): void {
        // Do nothing.
    }

    public evaluate(): TileType {
        return this.tileType;
    }
}<|endoftext|>/**
 * 判断value是否是array-like的长度
 * 参数
 * value
 * 返回
 * boolean 返回true是false不是
 */
const MAX_SAFE_INTEGER: number = 9007199254740991;
 function isLength(value: any): boolean {
    return typeof value == 'number' &&
    value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER
 }

 export default isLength;<|endoftext|>/**
 * @file
 * dumpObject.ts
 *
 * @author Josh Parkinson <joshparkinson1991@gmail.com>
 */

import * as util from 'util';

/**
 * Dumps an object in it's entirety to a string.
 *
 * @param {object} object
 *     The object to dump
 * @param {boolean} showHidden
 *     Should hidden elements be shown
 *
 * @returns {string}
 *     The dumped object
 */
export default function dumpObject(object: object, showHidden: boolean = false): string {
    return util.inspect(object, {
        showHidden,
        depth: null,
        colors: true
    });
}<|endoftext|>import { StreamMessage } from '@streamr/protocol'
import { EthereumAddress, Logger, wait } from '@streamr/utils'
import sample from 'lodash/sample'
import { Gap, OrderedMessageChain } from './OrderedMessageChain'

const logger = new Logger(module)

export type GapFillStrategy = 'light' | 'full'

interface GapFillerOptions {
    chain: OrderedMessageChain
    resend: (gap: Gap, storageNodeAddress: EthereumAddress, abortSignal: AbortSignal) => AsyncGenerator<StreamMessage>
    getStorageNodeAddresses: () => Promise<EthereumAddress[]>
    strategy: GapFillStrategy
    initialWaitTime: number
    retryWaitTime: number
    maxRequestsPerGap: number
    abortSignal: AbortSignal
}

const runAbortableTask = async (
    run: () => Promise<void>
): Promise<void> => {
    try {
        await run()
    } catch (e) {
        if (e.code !== 'AbortError') {
            throw e
        }
    }
}

export class GapFiller {

    private currentTask: { gap: Gap, abortController: AbortController } | undefined = undefined
    private readonly chain: OrderedMessageChain
    private readonly resend: (gap: Gap, storageNodeAddress: EthereumAddress, abortSignal: AbortSignal) => AsyncGenerator<StreamMessage>
    private readonly getStorageNodeAddresses: () => Promise<EthereumAddress[]>
    private readonly strategy: GapFillStrategy
    private readonly initialWaitTime: number
    private readonly retryWaitTime: number
    private readonly maxRequestsPerGap: number

    constructor(opts: GapFillerOptions) {
        this.chain = opts.chain
        this.resend = opts.resend
        this.getStorageNodeAddresses = opts.getStorageNodeAddresses
        this.strategy = opts.strategy
        this.initialWaitTime = opts.initialWaitTime
        this.retryWaitTime = opts.retryWaitTime
        this.maxRequestsPerGap = opts.maxRequestsPerGap
        opts.abortSignal.addEventListener('abort', () => {
            this.currentTask?.abortController.abort()
        })
    }

    start(): void {
        this.chain.on('gapFound', (gap: Gap) => this.onGapFound(gap))
        this.chain.on('gapResolved', () => this.onGapResolved())
    }

    private async onGapFound(gap: Gap) {
        const abortController = new AbortController()
        this.currentTask = {
            gap,
            abortController
        }
        try {
            await runAbortableTask(async () => {
                await wait(this.initialWaitTime, abortController.signal)
                if (this.maxRequestsPerGap > 0) {
                    await this.fetchFromStorageNode(gap, abortController.signal)
                }
                /*
                 * The "