placeLabels(): Map<Series, PlacedLabel[]> {
        const series: Series[] = [];
        const data: (readonly PointLabelDatum[])[] = [];
        this.nodeData.forEach((d, s) => {
            if (s.visible && s.label.enabled) {
                series.push(s);
                data.push(s.getLabelData());
            }
        });
        const { seriesRect } = this;
        const labels: PlacedLabel[][] = seriesRect
            ? placeLabels(data, { x: 0, y: 0, width: seriesRect.width, height: seriesRect.height })
            : [];
        return new Map(labels.map((l, i) => [series[i], l]));
    }


private updateLegend() {
        const legendData: LegendDatum[] = [];

        this.series.filter(s => s.showInLegend).forEach(series => series.listSeriesItems(legendData));

        const { formatter } = this.legend.item.label;
        if (formatter) {
            legendData.forEach(datum => datum.label.text = formatter({
                id: datum.id,
                itemId: datum.itemId,
                value: datum.label.text
            }));
        }

        this.legend.data = legendData;
    }


abstract performLayout(): void;


update() {
        this.updatePending = false;
        this.series.forEach(series => {
            if (series.updatePending) {
                series.update();
            }
        });
    }


protected positionCaptions() {
        const { title, subtitle } = this;

        let titleVisible = false;
        let subtitleVisible = false;

        const spacing = 10;
        let paddingTop = spacing;

        if (title && title.enabled) {
            title.node.x = this.width / 2;
            title.node.y = paddingTop;
            titleVisible = true;
            const titleBBox = title.node.computeBBox(); // make sure to set node's x/y, then computeBBox
            if (titleBBox) {
                paddingTop = titleBBox.y + titleBBox.height;
            }

            if (subtitle && subtitle.enabled) {
                subtitle.node.x = this.width / 2;
                subtitle.node.y = paddingTop + spacing;
                subtitleVisible = true;
                const subtitleBBox = subtitle.node.computeBBox();
                if (subtitleBBox) {
                    paddingTop = subtitleBBox.y + subtitleBBox.height;
                }
            }
        }

        if (title) {
            title.node.visible = titleVisible;
        }
        if (subtitle) {
            subtitle.node.visible = subtitleVisible;
        }

        this.captionAutoPadding = Math.floor(paddingTop);
    }


protected positionLegend() {
        if (!this.legend.enabled || !this.legend.data.length) {
            return;
        }

        const { legend, captionAutoPadding, legendAutoPadding } = this;
        const width = this.width;
        const height = this.height - captionAutoPadding;
        const legendGroup = legend.group;
        const legendSpacing = legend.spacing;

        let translationX = 0;
        let translationY = 0;

        let legendBBox: BBox;
        switch (legend.position) {
            case 'bottom':
                legend.performLayout(width - legendSpacing * 2, 0);
                legendBBox = legendGroup.computeBBox();

                translationX = (width - legendBBox.width) / 2 - legendBBox.x;
                translationY = captionAutoPadding + height - legendBBox.height - legendBBox.y - legendSpacing;

                legendAutoPadding.bottom = legendBBox.height;
                break;

            case 'top':
                legend.performLayout(width - legendSpacing * 2, 0);
                legendBBox = legendGroup.computeBBox();

                translationX = (width - legendBBox.width) / 2 - legendBBox.x;
                translationY = captionAutoPadding + legendSpacing - legendBBox.y;

                legendAutoPadding.top = legendBBox.height;
                break;

            case 'left':
                legend.performLayout(0, height - legendSpacing * 2);
                legendBBox = legendGroup.computeBBox();

                translationX = legendSpacing - legendBBox.x;
                translationY = captionAutoPadding + (height - legendBBox.height) / 2 - legendBBox.y;

                legendAutoPadding.left = legendBBox.width;
                break;

            default: // case 'right':
                legend.performLayout(0, height - legendSpacing * 2);
                legendBBox = legendGroup.computeBBox();

                translationX = width - legendBBox.width - legendBBox.x - legendSpacing;
                translationY = captionAutoPadding + (height - legendBBox.height) / 2 - legendBBox.y;

                legendAutoPadding.right = legendBBox.width;
                break;
        }

        // Round off for pixel grid alignment to work properly.
        legendGroup.translationX = Math.floor(translationX + legendGroup.translationX);
        legendGroup.translationY = Math.floor(translationY + legendGroup.translationY);

        this.legendBBox = legendGroup.computeBBox();
    }


protected setupDomListeners(chartElement: HTMLCanvasElement) {
        chartElement.addEventListener('mousedown', this._onMouseDown);
        chartElement.addEventListener('mousemove', this._onMouseMove);
        chartElement.addEventListener('mouseup', this._onMouseUp);
        chartElement.addEventListener('mouseout', this._onMouseOut);
        chartElement.addEventListener('click', this._onClick);
    }


protected cleanupDomListeners(chartElement: HTMLCanvasElement) {
        chartElement.removeEventListener('mousedown', this._onMouseDown);
        chartElement.removeEventListener('mousemove', this._onMouseMove);
        chartElement.removeEventListener('mouseup', this._onMouseUp);
        chartElement.removeEventListener('mouseout', this._onMouseOut);
        chartElement.removeEventListener('click', this._onClick);
    }


getSeriesRect(): Readonly<BBox | undefined> {
        return this.seriesRect;
    }


// x/y are local canvas coordinates in CSS pixels, not actual pixels
private pickSeriesNode(x: number, y: number): {
        series: Series,
        node: Node
    } | undefined {
        if (!(this.seriesRect && this.seriesRect.containsPoint(x, y))) {
            return undefined;
        }

        const allSeries = this.series;
        let node: Node | undefined = undefined;
        for (let i = allSeries.length - 1; i >= 0; i--) {
            const series = allSeries[i];
            if (!series.visible) {
                continue;
            }
            node = series.pickGroup.pickNode(x, y);
            if (node) {
                return {
                    series,
                    node
                };
            }
        }
    }


// Provided x/y are in canvas coordinates.
private pickClosestSeriesNodeDatum(x: number, y: number): SeriesNodeDatum | undefined {
        if (!this.seriesRect || !this.seriesRect.containsPoint(x, y)) {
            return undefined;
        }

        const allSeries = this.series;

        type Point = { x: number, y: number};

        function getDistance(p1: Point, p2: Point): number {
            return Math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2);
        }

        let minDistance = Infinity;
        let closestDatum: SeriesNodeDatum | undefined;

        for (let i = allSeries.length - 1; i >= 0; i--) {
            const series = allSeries[i];
            if (!series.visible) {
                continue;
            }
            const hitPoint = series.group.transformPoint(x, y);
            series.getNodeData().forEach(datum => {
                if (!datum.point) {
                    return;
                }
                const distance = getDistance(hitPoint, datum.point);
                if (distance < minDistance) {
                    minDistance = distance;
                    closestDatum = datum;
                }
            });
        }

        return closestDatum;
    }


protected onMouseMove(event: MouseEvent): void {
        this.handleLegendMouseMove(event);

        if (this.tooltip.enabled) {
            if (this.tooltip.delay > 0) {
                this.tooltip.toggle(false);
            }
            this.handleTooltip(event);
        }
    }


protected handleTooltip(event: MouseEvent) {
        const { lastPick, tooltip: { tracking: tooltipTracking } } = this;
        const { offsetX, offsetY } = event;
        const pick = this.pickSeriesNode(offsetX, offsetY);
        let nodeDatum: SeriesNodeDatum | undefined;

        if (pick && pick.node instanceof Shape) {
            const { node } = pick;
            nodeDatum = node.datum as SeriesNodeDatum;
            if (lastPick && lastPick.datum === nodeDatum) {
                lastPick.node = node;
                lastPick.event = event;
            }
            // Marker nodes will have the `point` info in their datums.
            // Highlight if not a marker node or, if not in the tracking mode, highlight markers too.
            if ((!node.datum.point || !tooltipTracking)) {
                if (!lastPick // cursor moved from empty space to a node
                    || lastPick.node !== node) { // cursor moved from one node to another
                    this.onSeriesDatumPick(event, node.datum, node, event);
                } else if (pick.series.tooltip.enabled) { // cursor moved within the same node
                    this.tooltip.show(event);
                }
                // A non-marker node (takes precedence over marker nodes) was highlighted.
                // Or we are not in the tracking mode.
                // Either way, we are done at this point.
                return;
            }
        }

        let hideTooltip = false;
        // In tracking mode a tooltip is shown for the closest rendered datum.
        // This makes it easier to show tooltips when markers are small and/or plentiful
        // and also gives the ability to show tooltips even when the series were configured
        // to not render markers.
        if (tooltipTracking) {
            const closestDatum = this.pickClosestSeriesNodeDatum(offsetX, offsetY);
            if (closestDatum && closestDatum.point) {
                const { x, y } = closestDatum.point;
                const { canvas } = this.scene;
                const point = closestDatum.series.group.inverseTransformPoint(x, y);
                const canvasRect = canvas.element.getBoundingClientRect();
                this.onSeriesDatumPick({
                    pageX: Math.round(canvasRect.left + window.pageXOffset + point.x),
                    pageY: Math.round(canvasRect.top + window.pageYOffset + point.y)
                }, closestDatum, nodeDatum === closestDatum && pick ? pick.node as Shape : undefined, event);
            } else {
                hideTooltip = true;
            }
        }

        if (lastPick && (hideTooltip || !tooltipTracking)) {
            // Cursor moved from a non-marker node to empty space.
            this.dehighlightDatum();
            this.tooltip.toggle(false);
            this.lastPick = undefined;
        }
    }


protected onMouseDown(event: MouseEvent) {}


protected onMouseUp(event: MouseEvent) {}


protected onMouseOut(event: MouseEvent) {
        this.tooltip.toggle(false);
    }


protected onClick(event: MouseEvent) {
        if (this.checkSeriesNodeClick()) {
            return;
        }
        if (this.checkLegendClick(event)) {
            return;
        }
        this.fireEvent<ChartClickEvent>({
            type: 'click',
            event
        });
    }


private checkSeriesNodeClick(): boolean {
        const { lastPick } = this;

        if (lastPick && lastPick.event && lastPick.node) {
            const { event, datum } = lastPick;
            datum.series.fireNodeClickEvent(event, datum);
            return true;
        }

        return false;
    }


private onSeriesNodeClick(event: SourceEvent<Series>) {
        this.fireEvent({ ...event, type: 'seriesNodeClick' });
    }


private checkLegendClick(event: MouseEvent): boolean {
        const datum = this.legend.getDatumForPoint(event.offsetX, event.offsetY);

        if (datum) {
            const { id, itemId, enabled } = datum;
            const series = find(this.series, series => series.id === id);

            if (series) {
                series.toggleSeriesItem(itemId, !enabled);
                if (enabled) {
                    this.tooltip.toggle(false);
                }
                this.legend.fireEvent<LegendClickEvent>({
                    type: 'click',
                    event,
                    itemId,
                    enabled: !enabled
                });
                return true;
            }
        }

        return false;
    }


private handleLegendMouseMove(event: MouseEvent) {
        if (!this.legend.enabled) {
            return;
        }

        const { offsetX, offsetY } = event;
        const datum = this.legend.getDatumForPoint(offsetX, offsetY);

        const pointerInsideLegend = this.legendBBox.containsPoint(offsetX, offsetY);
        if (pointerInsideLegend) {
            if (!this.pointerInsideLegend) {
                this.pointerInsideLegend = true;
            }
        } else if (this.pointerInsideLegend) {
            this.pointerInsideLegend = false;
            // Dehighlight if the pointer was inside the legend and is now leaving it.
            if (this.highlightedDatum) {
                this.highlightedDatum = undefined;
                this.series.forEach(s => s.updatePending = true);
            }
            return;
        }

        const oldHighlightedDatum = this.highlightedDatum;

        if (datum) {
            const { id, itemId, enabled } = datum;

            if (enabled) {
                const series = find(this.series, series => series.id === id);

                if (series) {
                    this.highlightedDatum = {
                        series,
                        itemId,
                        datum: undefined
                    };
                }
            }
        }

        // Careful to only schedule updates when necessary.
        if ((this.highlightedDatum && !oldHighlightedDatum) ||
            (this.highlightedDatum && oldHighlightedDatum &&
                (this.highlightedDatum.series !== oldHighlightedDatum.series ||
                 this.highlightedDatum.itemId !== oldHighlightedDatum.itemId))) {
            this.series.forEach(s => s.updatePending = true);
        }
    }


private onSeriesDatumPick(meta: TooltipMeta, datum: SeriesNodeDatum, node?: Shape, event?: MouseEvent) {
        const { lastPick } = this;
        if (lastPick) {
            if (lastPick.datum === datum) { return; }
            this.dehighlightDatum();
        }

        this.lastPick = {
            datum,
            node,
            event
        };

        this.highlightDatum(datum);

        const html = datum.series.tooltip.enabled && datum.series.getTooltipHtml(datum);

        if (html) {
            this.tooltip.show(meta, html);
        }
    }


highlightDatum(datum: SeriesNodeDatum): void {
        this.scene.canvas.element.style.cursor = datum.series.cursor;
        this.highlightedDatum = datum;
        this.series.forEach(s => s.updatePending = true);
    }


dehighlightDatum(): void {
        this.scene.canvas.element.style.cursor = 'default';
        this.highlightedDatum = undefined;
        this.series.forEach(s => s.updatePending = true);
    }


function testLogLevels(logger: ILogger, minLevel: LogLevel) {
            const capturingConsole = new CapturingConsole();
            (logger as ConsoleLogger).out = capturingConsole;

            for (let level = LogLevel.Trace; level < LogLevel.None; level++) {
                const message = `Message at LogLevel.${LogLevel[level]}`;
                const expectedMessage = `${LogLevel[level]}: Message at LogLevel.${LogLevel[level]}`;
                logger.log(level, message);

                if (level >= minLevel) {
                    expect(capturingConsole.messages).toContain(expectedMessage);
                } else {
                    expect(capturingConsole.messages).not.toContain(expectedMessage);
                }
            }
        }


function createConnectionBuilder(logger?: ILogger): HubConnectionBuilder {
    // We don't want to spam test output with logs. This can be changed as needed
    return new HubConnectionBuilder()
        .configureLogging(logger || NullLogger.instance);
}


function createTestClient(pollSent: PromiseSource<HttpRequest>, pollCompleted: Promise<HttpResponse>, negotiateResponse?: any): TestHttpClient {
    let firstRequest = true;
    return new TestHttpClient()
        .on("POST", "http://example.com/negotiate?negotiateVersion=1", () => negotiateResponse || longPollingNegotiateResponse)
        .on("GET", /http:\/\/example.com\?id=123abc&_=.*/, (req) => {
            if (firstRequest) {
                firstRequest = false;
                return new HttpResponse(200);
            } else {
                pollSent.resolve(req);
                return pollCompleted;
            }
        });
}


function eachMissingValue(callback: (val: undefined | null, name: string) => void) {
    callback(null, "null");
    callback(undefined, "undefined");
}


() => {
            const builder = new HubConnectionBuilder();
            expect(() => builder.withUrl(val!)).toThrow(/The 'url' argument (is required|should not be empty)./);
        }


() => builder.withUrl(val!)


(val, name) => {
        it(`withHubProtocol throws if protocol is ${name}`, () => {
            const builder = new HubConnectionBuilder();
            expect(() => builder.withHubProtocol(val!)).toThrow("The 'protocol' argument is required.");
        });
    }


() => {
            const builder = new HubConnectionBuilder();
            expect(() => builder.withHubProtocol(val!)).toThrow("The 'protocol' argument is required.");
        }


() => builder.withHubProtocol(val!)


async () => {
        await VerifyLogger.run(async (logger) => {
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const connection = createConnectionBuilder()
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                    logger,
                })
                .build();

            await expect(connection.start()).rejects.toThrow("The underlying connection was closed before the hub handshake could complete.");
            expect(connection.state).toBe(HubConnectionState.Disconnected);

            expect((await pollSent.promise).url).toMatch(/http:\/\/example.com\?id=123abc.*/);
        });
    }


async (logger) => {
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const connection = createConnectionBuilder()
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                    logger,
                })
                .build();

            await expect(connection.start()).rejects.toThrow("The underlying connection was closed before the hub handshake could complete.");
            expect(connection.state).toBe(HubConnectionState.Disconnected);

            expect((await pollSent.promise).url).toMatch(/http:\/\/example.com\?id=123abc.*/);
        }


(req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                }


async () => {
        const protocol = new TestProtocol();

        const builder = createConnectionBuilder()
            .withUrl("http://example.com", HttpTransportType.WebSockets)
            .withHubProtocol(protocol);
        expect(builder.httpConnectionOptions!.transport).toBe(HttpTransportType.WebSockets);
    }


async () => {
        await VerifyLogger.run(async (logger) => {
            const protocol = new TestProtocol();

            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            let negotiateRequest!: HttpRequest;
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    negotiateRequest = req;
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });

            const connection = createConnectionBuilder()
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                    logger,
                })
                .withHubProtocol(protocol)
                .build();

            await expect(connection.start()).rejects.toThrow("The underlying connection was closed before the hub handshake could complete.");
            expect(connection.state).toBe(HubConnectionState.Disconnected);

            expect(negotiateRequest.content).toBe(`{"protocol":"${protocol.name}","version":1}\x1E`);
        });
    }


async (logger) => {
            const protocol = new TestProtocol();

            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            let negotiateRequest!: HttpRequest;
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    negotiateRequest = req;
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });

            const connection = createConnectionBuilder()
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                    logger,
                })
                .withHubProtocol(protocol)
                .build();

            await expect(connection.start()).rejects.toThrow("The underlying connection was closed before the hub handshake could complete.");
            expect(connection.state).toBe(HubConnectionState.Disconnected);

            expect(negotiateRequest.content).toBe(`{"protocol":"${protocol.name}","version":1}\x1E`);
        }


(req) => {
                    // Respond from the poll with the handshake response
                    negotiateRequest = req;
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                }


() => {
        function testLogLevels(logger: ILogger, minLevel: LogLevel) {
            const capturingConsole = new CapturingConsole();
            (logger as ConsoleLogger).out = capturingConsole;

            for (let level = LogLevel.Trace; level < LogLevel.None; level++) {
                const message = `Message at LogLevel.${LogLevel[level]}`;
                const expectedMessage = `${LogLevel[level]}: Message at LogLevel.${LogLevel[level]}`;
                logger.log(level, message);

                if (level >= minLevel) {
                    expect(capturingConsole.messages).toContain(expectedMessage);
                } else {
                    expect(capturingConsole.messages).not.toContain(expectedMessage);
                }
            }
        }

        eachMissingValue((val, name) => {
            it(`throws if logger is ${name}`, () => {
                const builder = new HubConnectionBuilder();
                expect(() => builder.configureLogging(val!)).toThrow("The 'logging' argument is required.");
            });
        });

        [
            LogLevel.None,
            LogLevel.Critical,
            LogLevel.Error,
            LogLevel.Warning,
            LogLevel.Information,
            LogLevel.Debug,
            LogLevel.Trace,
        ].forEach((minLevel) => {
            const levelName = LogLevel[minLevel];
            it(`accepts LogLevel.${levelName}`, async () => {
                const builder = new HubConnectionBuilder()
                    .configureLogging(minLevel);

                expect(builder.logger).toBeDefined();
                expect(builder.logger).toBeInstanceOf(ConsoleLogger);

                testLogLevels(builder.logger!, minLevel);
            });
        });

        const levelNames = Object.keys(ExpectedLogLevelMappings) as (keyof typeof ExpectedLogLevelMappings)[];
        for (const str of levelNames) {
            const mapped = ExpectedLogLevelMappings[str];
            const mappedName = LogLevel[mapped];
            it(`accepts "${str}" as an alias for LogLevel.${mappedName}`, async () => {
                const builder = new HubConnectionBuilder()
                    .configureLogging(str);

                expect(builder.logger).toBeDefined();
                expect(builder.logger).toBeInstanceOf(ConsoleLogger);

                testLogLevels(builder.logger!, mapped);
            });
        }

        it("allows logger to be replaced", async () => {
            let loggedMessages = 0;
            const logger = {
                log() {
                    loggedMessages += 1;
                },
            };
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const connection = createConnectionBuilder(logger)
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                })
                .build();

            try {
                await connection.start();
            } catch {
                // Ignore failures
            }

            expect(loggedMessages).toBeGreaterThan(0);
        });

        it("configures logger for both HttpConnection and HubConnection", async () => {
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const logger = new CaptureLogger();
            const connection = createConnectionBuilder(logger)
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                })
                .build();

            try {
                await connection.start();
            } catch {
                // Ignore failures
            }

            // A HubConnection message
            expect(logger.messages).toContain("Starting HubConnection.");

            // An HttpConnection message
            expect(logger.messages).toContain("Starting connection with transfer format 'Text'.");
        });

        it("does not replace HttpConnectionOptions logger if provided", async () => {
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const hubConnectionLogger = new CaptureLogger();
            const httpConnectionLogger = new CaptureLogger();
            const connection = createConnectionBuilder(hubConnectionLogger)
                .withUrl("http://example.com", {
                    httpClient: testClient,
                    logger: httpConnectionLogger,
                })
                .build();

            try {
                await connection.start();
            } catch {
                // Ignore failures
            }

            // A HubConnection message
            expect(hubConnectionLogger.messages).toContain("Starting HubConnection.");
            expect(httpConnectionLogger.messages).not.toContain("Starting HubConnection.");

            // An HttpConnection message
            expect(httpConnectionLogger.messages).toContain("Starting connection with transfer format 'Text'.");
            expect(hubConnectionLogger.messages).not.toContain("Starting connection with transfer format 'Text'.");
        });
    }


(val, name) => {
            it(`throws if logger is ${name}`, () => {
                const builder = new HubConnectionBuilder();
                expect(() => builder.configureLogging(val!)).toThrow("The 'logging' argument is required.");
            });
        }


() => {
                const builder = new HubConnectionBuilder();
                expect(() => builder.configureLogging(val!)).toThrow("The 'logging' argument is required.");
            }


() => builder.configureLogging(val!)


(minLevel) => {
            const levelName = LogLevel[minLevel];
            it(`accepts LogLevel.${levelName}`, async () => {
                const builder = new HubConnectionBuilder()
                    .configureLogging(minLevel);

                expect(builder.logger).toBeDefined();
                expect(builder.logger).toBeInstanceOf(ConsoleLogger);

                testLogLevels(builder.logger!, minLevel);
            });
        }


async () => {
                const builder = new HubConnectionBuilder()
                    .configureLogging(minLevel);

                expect(builder.logger).toBeDefined();
                expect(builder.logger).toBeInstanceOf(ConsoleLogger);

                testLogLevels(builder.logger!, minLevel);
            }


async () => {
                const builder = new HubConnectionBuilder()
                    .configureLogging(str);

                expect(builder.logger).toBeDefined();
                expect(builder.logger).toBeInstanceOf(ConsoleLogger);

                testLogLevels(builder.logger!, mapped);
            }


async () => {
            let loggedMessages = 0;
            const logger = {
                log() {
                    loggedMessages += 1;
                },
            };
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const connection = createConnectionBuilder(logger)
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                })
                .build();

            try {
                await connection.start();
            } catch {
                // Ignore failures
            }

            expect(loggedMessages).toBeGreaterThan(0);
        }


async () => {
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const logger = new CaptureLogger();
            const connection = createConnectionBuilder(logger)
                .withUrl("http://example.com", {
                    ...commonHttpOptions,
                    httpClient: testClient,
                })
                .build();

            try {
                await connection.start();
            } catch {
                // Ignore failures
            }

            // A HubConnection message
            expect(logger.messages).toContain("Starting HubConnection.");

            // An HttpConnection message
            expect(logger.messages).toContain("Starting connection with transfer format 'Text'.");
        }


async () => {
            const pollSent = new PromiseSource<HttpRequest>();
            const pollCompleted = new PromiseSource<HttpResponse>();
            const testClient = createTestClient(pollSent, pollCompleted.promise)
                .on("POST", "http://example.com?id=123abc", (req) => {
                    // Respond from the poll with the handshake response
                    pollCompleted.resolve(new HttpResponse(204, "No Content", "{}"));
                    return new HttpResponse(202);
                });
            const hubConnectionLogger = new CaptureLogger();
            const httpConnectionLogger = new CaptureLogger();
            const connection = createConnectionBuilder(hubConnectionLogger)
                .withUrl("http://example.com", {
                    httpClient: testClient,
                    logger: httpConnectionLogger,
                })
                .build();

            try {
                await connection.start();
            } catch {
                // Ignore failures
            }

            // A HubConnection message
            expect(hubConnectionLogger.messages).toContain("Starting HubConnection.");
            expect(httpConnectionLogger.messages).not.toContain("Starting HubConnection.");

            // An HttpConnection message
            expect(httpConnectionLogger.messages).toContain("Starting connection with transfer format 'Text'.");
            expect(hubConnectionLogger.messages).not.toContain("Starting connection with transfer format 'Text'.");
        }


() => {
        const builder = new HubConnectionBuilder().withUrl("http://example.com");
        expect(builder.reconnectPolicy).toBeUndefined();
    }


() => {
        const builder = new HubConnectionBuilder().withAutomaticReconnect();
        expect(() => builder.withAutomaticReconnect()).toThrow("A reconnectPolicy has already been set.");
    }


() => builder.withAutomaticReconnect()


() => {
        // From DefaultReconnectPolicy.ts
        const DEFAULT_RETRY_DELAYS_IN_MILLISECONDS = [0, 2000, 10000, 30000, null];
        const builder = new HubConnectionBuilder()
            .withAutomaticReconnect();

        let retryCount = 0;
        for (const delay of DEFAULT_RETRY_DELAYS_IN_MILLISECONDS) {
            const retryContext = {
                previousRetryCount: retryCount++,
                elapsedMilliseconds: 0,
                retryReason: new Error(),
            };

            expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContext)).toBe(delay);
        }
    }


() => {
        const customRetryDelays = [3, 1, 4, 1, 5, 9];
        const builder = new HubConnectionBuilder()
            .withAutomaticReconnect(customRetryDelays);

        let retryCount = 0;
        for (const delay of customRetryDelays) {
            const retryContext = {
                previousRetryCount: retryCount++,
                elapsedMilliseconds: 0,
                retryReason: new Error(),
            };

            expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContext)).toBe(delay);
        }

        const retryContextFinal = {
            previousRetryCount: retryCount++,
            elapsedMilliseconds: 0,
            retryReason: new Error(),
        };

        expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContextFinal)).toBe(null);
    }


() => {
        const customRetryDelays = [127, 0, 0, 1];
        const builder = new HubConnectionBuilder()
            .withAutomaticReconnect(new DefaultReconnectPolicy(customRetryDelays));

        let retryCount = 0;
        for (const delay of customRetryDelays) {
            const retryContext = {
                previousRetryCount: retryCount++,
                elapsedMilliseconds: 0,
                retryReason: new Error(),
            };

            expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContext)).toBe(delay);
        }

        const retryContextFinal = {
            previousRetryCount: retryCount++,
            elapsedMilliseconds: 0,
            retryReason: new Error(),
        };

        expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContextFinal)).toBe(null);
    }


() => negotiateResponse || longPollingNegotiateResponse


(req) => {
            if (firstRequest) {
                firstRequest = false;
                return new HttpResponse(200);
            } else {
                pollSent.resolve(req);
                return pollCompleted;
            }
        }


class CapturingConsole {
    public messages: any[] = [];

    public error(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }

    public warn(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }

    public info(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }

    public log(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }

    private static _stripPrefix(input: any): any {
        if (typeof input === "string") {
            input = input.replace(/\[.*\]\s+/, "");
        }
        return input;
    }
}


class CaptureLogger implements ILogger {
    public readonly messages: string[] = [];

    public log(logLevel: LogLevel, message: string): void {
        this.messages.push(message);
    }
}


class TestProtocol implements IHubProtocol {
    public name: string = "test";
    public version: number = 1;
    public transferFormat: TransferFormat = TransferFormat.Text;
    public parseMessages(input: string | ArrayBuffer, logger: ILogger): HubMessage[] {
        throw new Error("Method not implemented.");
    }
    public writeMessage(message: HubMessage): string | ArrayBuffer {
        // builds ping message in the `hubConnection` constructor
        return "";
    }
}


public error(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }


public warn(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }


public info(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }


public log(message: any) {
        this.messages.push(CapturingConsole._stripPrefix(message));
    }


private static _stripPrefix(input: any): any {
        if (typeof input === "string") {
            input = input.replace(/\[.*\]\s+/, "");
        }
        return input;
    }


log() {
                    loggedMessages += 1;
                }


public log(logLevel: LogLevel, message: string): void {
        this.messages.push(message);
    }


public parseMessages(input: string | ArrayBuffer, logger: ILogger): HubMessage[] {
        throw new Error("Method not implemented.");
    }


public writeMessage(message: HubMessage): string | ArrayBuffer {
        // builds ping message in the `hubConnection` constructor
        return "";
    }


({ variant, token, density }: StyledProps) => {
  if (!variant) {
    return ``
  }

  const {
    states: {
      focus: { outline: focusOutline },
      active: { outline: activeOutline },
    },
    boxShadow,
  } = token

  let spacings = input.spacings
  if (density === 'compact') {
    spacings = input.modes.compact.spacings
  }

  return css`
    border: none;
    ${spacingsTemplate(spacings)}
    ${outlineTemplate(activeOutline)}
    box-shadow: ${boxShadow};

    &:active,
    &:focus {
      outline-offset: 0;
      box-shadow: none;
      ${outlineTemplate(focusOutline)}
    }

    &:disabled {
      cursor: not-allowed;
      box-shadow: none;
      outline: none;
      &:focus,
      &:active {
        outline: none;
      }
    }
  `
}


type StyledProps = {
  token: InputToken
  variant: string
  density: string
}


export type TextareaProps = {
  /** Placeholder */
  placeholder?: string
  /** Variant */
  variant?: Variants
  /** Disabled state */
  disabled?: boolean
  /** Type */
  type?: string
  /** Read Only */
  readOnly?: boolean
  /** Specifies max rows for multiline input */
  rowsMax?: number
} & TextareaHTMLAttributes<HTMLTextAreaElement>


async (
  opts: {
    alwaysAuth?: boolean,
    ca?: string,
    cert?: string,
    fetchRetries?: number,
    fetchRetryFactor?: number,
    fetchRetryMaxtimeout?: number,
    fetchRetryMintimeout?: number,
    httpsProxy?: string,
    ignoreFile?: (filename: string) => boolean,
    key?: string,
    localAddress?: string,
    lock: boolean,
    lockStaleDuration?: number,
    networkConcurrency?: number,
    offline?: boolean,
    packageImportMethod?: 'auto' | 'hardlink' | 'copy' | 'reflink',
    proxy?: string,
    rawNpmConfig: object,
    registry?: string,
    store: string,
    strictSsl?: boolean,
    userAgent?: string,
    verifyStoreIntegrity?: boolean,
  },
) => {
  // TODO: either print a warning or just log if --no-lock is used
  const sopts = Object.assign(opts, {
    locks: opts.lock ? path.join(opts.store, '_locks') : undefined,
    registry: opts.registry || 'https://registry.npmjs.org/',
  })
  const resolve = createResolver(Object.assign(sopts, {
    fullMetadata: false,
    metaCache: new LRU({
      max: 10000,
      maxAge: 120 * 1000, // 2 minutes
    }) as any, // tslint:disable-line:no-any
  }))
  await mkdirp(sopts.store)
  const fsIsCaseSensitive = await dirIsCaseSensitive(sopts.store)
  logger.debug({
    // An undefined field would cause a crash of the logger
    // so converting it to null
    isCaseSensitive: typeof fsIsCaseSensitive === 'boolean'
      ? fsIsCaseSensitive : null,
    store: sopts.store,
  })
  const fetchers = createFetcher({ ...sopts, fsIsCaseSensitive })
  return {
    ctrl: await createStore(resolve, fetchers as {}, {
      locks: sopts.locks,
      lockStaleDuration: sopts.lockStaleDuration,
      networkConcurrency: sopts.networkConcurrency,
      packageImportMethod: sopts.packageImportMethod,
      store: sopts.store,
      verifyStoreIntegrity: typeof sopts.verifyStoreIntegrity === 'boolean' ?
        sopts.verifyStoreIntegrity : true,
    }),
    path: sopts.store,
  }
}


(traceFilters: TraceFilters) => {
	return {
		type: ActionTypes.updateTraceFilters,
		payload: traceFilters,
	};
}


export interface TagItem {
	key: string;
	value: string;
	operator: "equals" | "contains";
}


export interface LatencyValue {
	min: string;
	max: string;
}


export interface TraceFilters {
	tags?: TagItem[];
	service?: string;
	latency?: LatencyValue;
	operation?: string;
}


//define interface for action. Action creator always returns object of this type
export interface updateTraceFiltersAction {
	type: ActionTypes.updateTraceFilters;
	payload: TraceFilters;
}


file => !file.includes("/node_modules/")


file => !file.includes("/dist/")


file => !file.endsWith(".d.ts")


() => (
	<ul
		role="list"


file => (
			<li key={file.source}


(message: Message, error: Error, command: Command): Promise<Message> => {
	const embed = BetterEmbed.fromTemplate('complete', {
		client: message.client,
		color: 0xee2200,
		title: 'Code error :',
		description: cutIfTooLong(error.stack ?? error.toString(), 2048),
	});

	Logger.error(error, 'CodeError');
	return isOwner(message.author.id) ? message.channel.send(embed) : message.channel.send(`An error occurred while executing the \`${command.name}\` command.`);
}


export interface IOptions extends LoadOptions {
  /**
   * List of extensions to use for directory imports. Defaults to `['.yml', '.yaml']`.
   */
  ext?: string[];
  /**
   * Whether `safeLoad` or `load` should be used when loading YAML files via *js-yaml*. Defaults to `true`.
   */
  safe?: boolean;
}


export interface IPayload {
  paths: string | string[];
  strategy?: TStrategy;
  data?: any;
  recursive?: boolean;
}


export interface IFileDefinition {
  cwd: string;
  directory: string;
  name: string;
}


export type TStrategy = 'sequence' | 'shallow' | 'merge' | 'deep';


item => item.id === +id


@Injectable()
export class CoffeesService {
    private coffees: Coffee[] = [
        {
            id: 1,
            name: 'Shipwreck Roast',
            brand: 'Buddy Brew',
            flavors: ['chocolate','hazlenut'],
        },
    ];
    findAll() {
        return this.coffees;
      }

      findOne(id: string) {
        // throw 'A random error';
        const coffee = this.coffees.find(item => item.id === +id);
        if (!coffee) {
            // throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
            throw new NotFoundException(`Coffee #${id} not found--custom`)
        }
        return coffee;
      }

      create(createCoffeeDto: any) {
        this.coffees.push(createCoffeeDto);
        return createCoffeeDto;
      }

      update(id: string, updateCoffeeDto: any) {
        const existingCoffee = this.findOne(id);
        if (existingCoffee) {
          // update the existing entity
        }
      }

      remove(id: string) {
        const coffeeIndex = this.coffees.findIndex(item => item.id === +id);
        if (coffeeIndex >= 0) {
          this.coffees.splice(coffeeIndex, 1);
        }
    }
}


findAll() {
        return this.coffees;
      }


findOne(id: string) {
        // throw 'A random error';
        const coffee = this.coffees.find(item => item.id === +id);
        if (!coffee) {
            // throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
            throw new NotFoundException(`Coffee #${id} not found--custom`)
        }
        return coffee;
      }


create(createCoffeeDto: any) {
        this.coffees.push(createCoffeeDto);
        return createCoffeeDto;
      }


update(id: string, updateCoffeeDto: any) {
        const existingCoffee = this.findOne(id);
        if (existingCoffee) {
          // update the existing entity
        }
      }


remove(id: string) {
        const coffeeIndex = this.coffees.findIndex(item => item.id === +id);
        if (coffeeIndex >= 0) {
          this.coffees.splice(coffeeIndex, 1);
        }
    }


export default class LiveDemoComponent extends React.Component {
  state = { language: 'react' };

  props: {
    javascript?: string;
    react: string;
  };

  render(): JSX.Element {
    return (
      <div>
        <div
          style={{
            display: 'block',
            marginBottom: '8px',
            fontFamily: 'sans-serif',
          }}


render(): JSX.Element {
    return (
      <div>
        <div
          style={{
            display: 'block',
            marginBottom: '8px',
            fontFamily: 'sans-serif',
          }}


export interface IApplication extends LeasilyDocument {
  property: IProperty
  lease: ILease
  stage: APPLICATION_STAGES
  isClosed: boolean
  fee: number
  waitPeriodInDays: number
}


export interface IApplicationModel extends LeasilyModel<IApplication> {}


export enum APPLICATION_STAGES {
  AWAITING_COMPLETION,
  REQUESTING_BACKGROUND_CHECK,
  AWAITING_APPLICATION_REVIEW,
  RENTED
}


export default async function potalnode(kad: Kademlia, port: number) {
  const app = Express();

  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(bodyParser.json());
  app.use((_, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header(
      "Access-Control-Allow-Headers",
      "Origin, X-Requested-With, Content-Type, Accept"
    );
    res.header(
      "Access-Control-Allow-Methods",
      "GET, POST, PUT, DELETE, OPTIONS"
    );
    next();
  });

  app.listen(port, () => {
    console.log("Example app listening on port " + port);
  });

  app.post("/join", async (req: Express.Request, res: Express.Response) => {
    try {
      console.log("join", req.body);
      const kid = req.body.kid;
      if (kid) {
        console.log({ kid });
        const peer = PeerModule(kid);
        peers[kid] = peer;
        const offer = await peer.createOffer();
        return res.send({ offer, kid: kad.kid });
      }
    } catch (error) {}
  });

  app.post("/answer", async (req: Express.Request, res: Express.Response) => {
    try {
      const { answer, kid } = req.body;
      if (answer && kid) {
        const peer = peers[kid];
        await peer.setAnswer(answer);
        kad.add(peer);
        delete peers[kid];
        console.log("connected");
        return res.send("connected");
      }
    } catch (error) {}
  });
}


(_, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header(
      "Access-Control-Allow-Headers",
      "Origin, X-Requested-With, Content-Type, Accept"
    );
    res.header(
      "Access-Control-Allow-Methods",
      "GET, POST, PUT, DELETE, OPTIONS"
    );
    next();
  }


() => {
    console.log("Example app listening on port " + port);
  }


async (req: Express.Request, res: Express.Response) => {
    try {
      console.log("join", req.body);
      const kid = req.body.kid;
      if (kid) {
        console.log({ kid });
        const peer = PeerModule(kid);
        peers[kid] = peer;
        const offer = await peer.createOffer();
        return res.send({ offer, kid: kad.kid });
      }
    } catch (error) {}
  }


async (req: Express.Request, res: Express.Response) => {
    try {
      const { answer, kid } = req.body;
      if (answer && kid) {
        const peer = peers[kid];
        await peer.setAnswer(answer);
        kad.add(peer);
        delete peers[kid];
        console.log("connected");
        return res.send("connected");
      }
    } catch (error) {}
  }


(api: FormikProps<any>) => (
    <>
      <div className="c-statblock-editor__headers">
        <TextField label


keywordType => (
        <div key={keywordType}


powerType => (
        <div key={powerType}


api => (

      {this.state.renderError && (

          There was a problem with your statblock JSON, falling back to JSON
          editor.

      )}


submittedValues => {
    const {
      SaveAs,
      SaveAsCharacter,
      StatBlockJSON,
      ...submittedStatBlock
    } = submittedValues;

    let statBlockFromActiveEditor: StatBlock;
    if (this.state.editorMode == "standard") {
      statBlockFromActiveEditor = submittedStatBlock;
    } else {
      statBlockFromActiveEditor = JSON.parse(StatBlockJSON);
    }

    const editedStatBlock: StatBlock = {
      ...StatBlock.Default(),
      ...statBlockFromActiveEditor,
      Id: submittedStatBlock.Id,
      Name: submittedStatBlock.Name,
      Path: submittedStatBlock.Path,
      Version: process.env.VERSION || "unknown"
    };

    ConvertStringsToNumbersWhereNeeded(editedStatBlock);

    if (SaveAsCharacter && this.props.onSaveAsCharacter) {
      editedStatBlock.Id = probablyUniqueString();
      this.props.onSaveAsCharacter(editedStatBlock);
    } else if (SaveAs && this.props.onSaveAsCopy) {
      editedStatBlock.Id = probablyUniqueString();
      this.props.onSaveAsCopy(editedStatBlock);
    } else {
      this.props.onSave(editedStatBlock);
    }

    this.props.onClose();
  }


() => {
    this.props.onClose();
  }


() => {
    if (
      this.props.onDelete &&
      confirm(`Delete Statblock for ${this.props.statBlock.Name}?`)
    ) {
      this.props.onDelete();
      this.props.onClose();
    }
  }


(path: string, name: string) =>
      this.props.currentListings?.some(
        l => l.Meta().Path == path && l.Meta().Name == name
      )


l => l.Meta().Path == path && l.Meta().Name == name


(path: string, name: string) => JSON.stringify({ path, name })


values => {
    const errors: any = {};

    if (_.isEmpty(values.Name)) {
      errors.NameMissing = "Error: Name is required.";
    }

    if (this.state.editorMode === "json") {
      try {
        JSON.parse(values.StatBlockJSON);
      } catch (e) {
        errors.JSONParseError = e.message;
      }
    }

    if (!values.SaveAs) {
      return errors;
    }

    const path = values.Path || "";
    const name = values.Name || "";

    if (this.willOverwriteStatBlock(path, name)) {
      errors.PathAndName =
        "Error: This will overwrite an existing custom statblock.";
    }

    return errors;
  }


export class StatBlockEditor extends React.Component<
  StatBlockEditorProps,
  StatBlockEditorState
> {
  constructor(props) {
    super(props);
    this.state = { editorMode: "standard" };
  }

  public componentDidCatch(error, info) {
    this.setState({
      editorMode: "json",
      renderError: JSON.stringify(error)
    });
  }

  public render() {
    if (!this.props.statBlock) {
      return null;
    }

    const header =
      {
        combatant: "Edit Combatant Statblock",
        library: "Edit Library Statblock",
        persistentcharacter: "Edit Character Statblock"
      }[this.props.editorTarget] || "Edit StatBlock";

    const buttons = (
      <>
        <Button onClick={this.close} fontAwesomeIcon="times" />
        {this.props.onDelete && (
          <Button onClick={this.delete} fontAwesomeIcon="trash" />
        )}


export interface StatBlockEditorProps {
  statBlock: StatBlock;
  onSave: (statBlock: StatBlock) => void;
  onDelete?: () => void;
  onSaveAsCopy?: (statBlock: StatBlock) => void;
  onSaveAsCharacter?: (statBlock: StatBlock) => void;
  onClose: () => void;
  editorTarget: StatBlockEditorTarget;
  currentListings?: Listing<Listable>[];
}


interface StatBlockEditorState {
  editorMode: "standard" | "json";
  renderError?: string;
}


export type StatBlockEditorTarget =
  | "library"
  | "combatant"
  | "persistentcharacter";


public componentDidCatch(error, info) {
    this.setState({
      editorMode: "json",
      renderError: JSON.stringify(error)
    });
  }


public render() {
    if (!this.props.statBlock) {
      return null;
    }

    const header =
      {
        combatant: "Edit Combatant Statblock",
        library: "Edit Library Statblock",
        persistentcharacter: "Edit Character Statblock"
      }[this.props.editorTarget] || "Edit StatBlock";

    const buttons = (
      <>
        <Button onClick={this.close} fontAwesomeIcon="times" />
        {this.props.onDelete && (
          <Button onClick={this.delete} fontAwesomeIcon="trash" />
        )}


@NgModule({
    imports: [
        JigsawMobileListLiteModule, CommonModule,
        JigsawDemoDescriptionModule
    ],
    declarations: [ListLiteFullDemoComponent],
    exports: [ListLiteFullDemoComponent]
})
export class ListLiteFullDemoModule {
}


export class ClearNotificationAction extends Action {

	static readonly ID = CLEAR_NOTIFICATION;
	static readonly LABEL = localize('clearNotification', "Clear Notification");

	constructor(
		id: string,
		label: string,
		@ICommandService private readonly commandService: ICommandService
	) {
		super(id, label, ThemeIcon.asClassName(clearIcon));
	}

	override async run(notification: INotificationViewItem): Promise<void> {
		this.commandService.executeCommand(CLEAR_NOTIFICATION, notification);
	}
}


export class ClearAllNotificationsAction extends Action {

	static readonly ID = CLEAR_ALL_NOTIFICATIONS;
	static readonly LABEL = localize('clearNotifications', "Clear All Notifications");

	constructor(
		id: string,
		label: string,
		@ICommandService private readonly commandService: ICommandService
	) {
		super(id, label, ThemeIcon.asClassName(clearAllIcon));
	}

	override async run(): Promise<void> {
		this.commandService.executeCommand(CLEAR_ALL_NOTIFICATIONS);
	}
}


export class HideNotificationsCenterAction extends Action {

	static readonly ID = HIDE_NOTIFICATIONS_CENTER;
	static readonly LABEL = localize('hideNotificationsCenter', "Hide Notifications");

	constructor(
		id: string,
		label: string,
		@ICommandService private readonly commandService: ICommandService
	) {
		super(id, label, ThemeIcon.asClassName(hideIcon));
	}

	override async run(): Promise<void> {
		this.commandService.executeCommand(HIDE_NOTIFICATIONS_CENTER);
	}
}


export class ExpandNotificationAction extends Action {

	static readonly ID = EXPAND_NOTIFICATION;
	static readonly LABEL = localize('expandNotification', "Expand Notification");

	constructor(
		id: string,
		label: string,
		@ICommandService private readonly commandService: ICommandService
	) {
		super(id, label, ThemeIcon.asClassName(expandIcon));
	}

	override async run(notification: INotificationViewItem): Promise<void> {
		this.commandService.executeCommand(EXPAND_NOTIFICATION, notification);
	}
}


export class CollapseNotificationAction extends Action {

	static readonly ID = COLLAPSE_NOTIFICATION;
	static readonly LABEL = localize('collapseNotification', "Collapse Notification");

	constructor(
		id: string,
		label: string,
		@ICommandService private readonly commandService: ICommandService
	) {
		super(id, label, ThemeIcon.asClassName(collapseIcon));
	}

	override async run(notification: INotificationViewItem): Promise<void> {
		this.commandService.executeCommand(COLLAPSE_NOTIFICATION, notification);
	}
}


export class ConfigureNotificationAction extends Action {

	static readonly ID = 'workbench.action.configureNotification';
	static readonly LABEL = localize('configureNotification', "Configure Notification");

	constructor(
		id: string,
		label: string,
		readonly configurationActions: readonly IAction[]
	) {
		super(id, label, ThemeIcon.asClassName(configureIcon));
	}
}


export class CopyNotificationMessageAction extends Action {

	static readonly ID = 'workbench.action.copyNotificationMessage';
	static readonly LABEL = localize('copyNotification', "Copy Text");

	constructor(
		id: string,
		label: string,
		@IClipboardService private readonly clipboardService: IClipboardService
	) {
		super(id, label);
	}

	override run(notification: INotificationViewItem): Promise<void> {
		return this.clipboardService.writeText(notification.message.raw);
	}
}


export class NotificationActionRunner extends ActionRunner {

	constructor(
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@INotificationService private readonly notificationService: INotificationService
	) {
		super();
	}

	protected override async runAction(action: IAction, context: unknown): Promise<void> {
		this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: action.id, from: 'message' });

		if (isNotificationViewItem(context)) {
			// Log some additional telemetry specifically for actions
			// that are triggered from within notifications.
			this.telemetryService.publicLog2<NotificationActionMetrics, NotificationActionMetricsClassification>('notification:actionExecuted', {
				id: hash(context.message.original.toString()).toString(),
				actionLabel: action.label,
				source: context.sourceId || 'core',
				silent: context.silent
			});
		}

		// Run and make sure to notify on any error again
		try {
			await super.runAction(action, context);
		} catch (error) {
			this.notificationService.error(error);
		}
	}
}


interface NotificationActionMetrics {
	id: string;
	actionLabel: string;
	source: string;
	silent: boolean;
}


type NotificationActionMetricsClassification = {
	id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
	actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
	source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
	silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' };
};


override async run(notification: INotificationViewItem): Promise<void> {
		this.commandService.executeCommand(CLEAR_NOTIFICATION, notification);
	}


override async run(): Promise<void> {
		this.commandService.executeCommand(CLEAR_ALL_NOTIFICATIONS);
	}


override async run(): Promise<void> {
		this.commandService.executeCommand(HIDE_NOTIFICATIONS_CENTER);
	}


override async run(notification: INotificationViewItem): Promise<void> {
		this.commandService.executeCommand(EXPAND_NOTIFICATION, notification);
	}


override async run(notification: INotificationViewItem): Promise<void> {
		this.commandService.executeCommand(COLLAPSE_NOTIFICATION, notification);
	}


override run(notification: INotificationViewItem): Promise<void> {
		return this.clipboardService.writeText(notification.message.raw);
	}


protected override async runAction(action: IAction, context: unknown): Promise<void> {
		this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: action.id, from: 'message' });

		if (isNotificationViewItem(context)) {
			// Log some additional telemetry specifically for actions
			// that are triggered from within notifications.
			this.telemetryService.publicLog2<NotificationActionMetrics, NotificationActionMetricsClassification>('notification:actionExecuted', {
				id: hash(context.message.original.toString()).toString(),
				actionLabel: action.label,
				source: context.sourceId || 'core',
				silent: context.silent
			});
		}

		// Run and make sure to notify on any error again
		try {
			await super.runAction(action, context);
		} catch (error) {
			this.notificationService.error(error);
		}
	}


export default class Badge extends React.Component<BadgeProps, any> {
  static defaultProps = {
    prefixCls: 'ant-badge',
    scrollNumberPrefixCls: 'ant-scroll-number',
    count: null,
    showZero: false,
    dot: false,
    overflowCount: 99,
  };

  static propTypes = {
    count: PropTypes.node,
    showZero: PropTypes.bool,
    dot: PropTypes.bool,
    overflowCount: PropTypes.number,
  };

  getBadgeClassName() {
    const { prefixCls, className, status, children } = this.props;
    return classNames(className, prefixCls, {
      [`${prefixCls}-status`]: !!status,
      [`${prefixCls}-not-a-wrapper`]: !children,
    }) as string;
  }

  isZero() {
    const numberedDispayCount = this.getNumberedDispayCount();
    return numberedDispayCount === '0' || numberedDispayCount === 0;
  }

  isDot() {
    const { dot, status } = this.props;
    const isZero = this.isZero();
    return (dot && !isZero) || status;
  }

  isHidden() {
    const { showZero } = this.props;
    const displayCount = this.getDispayCount();
    const isZero = this.isZero();
    const isDot = this.isDot();
    const isEmpty = displayCount === null || displayCount === undefined || displayCount === '';
    return (isEmpty || (isZero && !showZero)) && !isDot;
  }

  getNumberedDispayCount() {
    const { count, overflowCount } = this.props;
    const displayCount =
      (count as number) > (overflowCount as number) ? `${overflowCount}+` : count;
    return displayCount as string | number | null;
  }

  getDispayCount() {
    const isDot = this.isDot();
    // dot mode don't need count
    if (isDot) {
      return '';
    }
    return this.getNumberedDispayCount();
  }

  getScollNumberTitle() {
    const { title, count } = this.props;
    if (title) {
      return title;
    }
    return typeof count === 'string' || typeof count === 'number' ? count : undefined;
  }

  getStyleWithOffset() {
    const { offset, style } = this.props;
    return offset
      ? {
          right: -parseInt(offset[0] as string, 10),
          marginTop: offset[1],
          ...style,
        }
      : style;
  }

  renderStatusText() {
    const { prefixCls, text } = this.props;
    const hidden = this.isHidden();
    return hidden || !text ? null : <span className={`${prefixCls}-status-text`}>{text};
  }

  renderDispayComponent() {
    const { count } = this.props;
    const customNode = count as React.ReactElement<any>;
    if (!customNode || typeof customNode !== 'object') {
      return undefined;
    }
    return React.cloneElement(customNode, {
      style: {
        ...this.getStyleWithOffset(),
        ...(customNode.props && customNode.props.style),
      },
    });
  }

  renderBadgeNumber() {
    const { count, prefixCls, scrollNumberPrefixCls, status } = this.props;

    const displayCount = this.getDispayCount();
    const isDot = this.isDot();
    const hidden = this.isHidden();

    const scrollNumberCls = classNames({
      [`${prefixCls}-dot`]: isDot,
      [`${prefixCls}-count`]: !isDot,
      [`${prefixCls}-multiple-words`]:
        !isDot && count && count.toString && count.toString().length > 1,
      [`${prefixCls}-status-${status}`]: !!status,
    });

    return hidden ? null : (
      <ScrollNumber
        prefixCls={scrollNumberPrefixCls}
        data-show={!hidden}
        className={scrollNumberCls}
        count={displayCount}
        displayComponent={this.renderDispayComponent()} // <Badge status="success" count={<Icon type="xxx" />}></Badge>
        title={this.getScollNumberTitle()}
        style={this.getStyleWithOffset()}
        key="scrollNumber"
      />
    );
  }

  render() {
    const {
      count,
      showZero,
      prefixCls,
      scrollNumberPrefixCls,
      overflowCount,
      className,
      style,
      children,
      dot,
      status,
      text,
      offset,
      title,
      ...restProps
    } = this.props;

    const scrollNumber = this.renderBadgeNumber();
    const statusText = this.renderStatusText();

    const statusCls = classNames({
      [`${prefixCls}-status-dot`]: !!status,
      [`${prefixCls}-status-${status}`]: !!status,
    });

    // <Badge status="success" />
    if (!children && status) {
      return (

          <span className={`${prefixCls}-status-text`}>{text}

      );
    }

    return (

        {children}
        <Animate
          component=""
          showProp="data-show"
          transitionName={children ? `${prefixCls}-zoom` : ''}


export interface BadgeProps {
  /** Number to show in badge */
  count?: React.ReactNode;
  showZero?: boolean;
  /** Max count to show */
  overflowCount?: number;
  /** whether to show red dot without number */
  dot?: boolean;
  style?: React.CSSProperties;
  prefixCls?: string;
  scrollNumberPrefixCls?: string;
  className?: string;
  status?: 'success' | 'processing' | 'default' | 'error' | 'warning';
  text?: string;
  offset?: [number | string, number | string];
  title?: string;
}


getBadgeClassName() {
    const { prefixCls, className, status, children } = this.props;
    return classNames(className, prefixCls, {
      [`${prefixCls}-status`]: !!status,
      [`${prefixCls}-not-a-wrapper`]: !children,
    }) as string;
  }


isZero() {
    const numberedDispayCount = this.getNumberedDispayCount();
    return numberedDispayCount === '0' || numberedDispayCount === 0;
  }


isDot() {
    const { dot, status } = this.props;
    const isZero = this.isZero();
    return (dot && !isZero) || status;
  }


isHidden() {
    const { showZero } = this.props;
    const displayCount = this.getDispayCount();
    const isZero = this.isZero();
    const isDot = this.isDot();
    const isEmpty = displayCount === null || displayCount === undefined || displayCount === '';
    return (isEmpty || (isZero && !showZero)) && !isDot;
  }


getNumberedDispayCount() {
    const { count, overflowCount } = this.props;
    const displayCount =
      (count as number) > (overflowCount as number) ? `${overflowCount}+` : count;
    return displayCount as string | number | null;
  }


getDispayCount() {
    const isDot = this.isDot();
    // dot mode don't need count
    if (isDot) {
      return '';
    }
    return this.getNumberedDispayCount();
  }


getScollNumberTitle() {
    const { title, count } = this.props;
    if (title) {
      return title;
    }
    return typeof count === 'string' || typeof count === 'number' ? count : undefined;
  }


getStyleWithOffset() {
    const { offset, style } = this.props;
    return offset
      ? {
          right: -parseInt(offset[0] as string, 10),
          marginTop: offset[1],
          ...style,
        }
      : style;
  }


renderStatusText() {
    const { prefixCls, text } = this.props;
    const hidden = this.isHidden();
    return hidden || !text ? null : <span className={`${prefixCls}-status-text`}>{text};
  }


renderDispayComponent() {
    const { count } = this.props;
    const customNode = count as React.ReactElement<any>;
    if (!customNode || typeof customNode !== 'object') {
      return undefined;
    }
    return React.cloneElement(customNode, {
      style: {
        ...this.getStyleWithOffset(),
        ...(customNode.props && customNode.props.style),
      },
    });
  }


renderBadgeNumber() {
    const { count, prefixCls, scrollNumberPrefixCls, status } = this.props;

    const displayCount = this.getDispayCount();
    const isDot = this.isDot();
    const hidden = this.isHidden();

    const scrollNumberCls = classNames({
      [`${prefixCls}-dot`]: isDot,
      [`${prefixCls}-count`]: !isDot,
      [`${prefixCls}-multiple-words`]:
        !isDot && count && count.toString && count.toString().length > 1,
      [`${prefixCls}-status-${status}`]: !!status,
    });

    return hidden ? null : (
      <ScrollNumber
        prefixCls={scrollNumberPrefixCls}
        data-show={!hidden}
        className={scrollNumberCls}
        count={displayCount}
        displayComponent={this.renderDispayComponent()} // <Badge status="success" count={<Icon type="xxx" />}></Badge>
        title={this.getScollNumberTitle()}
        style={this.getStyleWithOffset()}
        key="scrollNumber"
      />
    );
  }


render() {
    const {
      count,
      showZero,
      prefixCls,
      scrollNumberPrefixCls,
      overflowCount,
      className,
      style,
      children,
      dot,
      status,
      text,
      offset,
      title,
      ...restProps
    } = this.props;

    const scrollNumber = this.renderBadgeNumber();
    const statusText = this.renderStatusText();

    const statusCls = classNames({
      [`${prefixCls}-status-dot`]: !!status,
      [`${prefixCls}-status-${status}`]: !!status,
    });

    // <Badge status="success" />
    if (!children && status) {
      return (

          <span className={`${prefixCls}-status-text`}>{text}

      );
    }


return (
      <span {...restProps} className={this.getBadgeClassName()}>
        {children}
        <Animate
          component=""
          showProp="data-show"
          transitionName={children ? `${prefixCls}-zoom` : ''


([, todo]) => todo > 0


([done, todo]) => {
      return Math.round(done * 100 / todo);
    }


extracts => {
        return combineLatest([
          this.doCollectablesData(),
          this.doLogTrackingData(extracts)
        ]);
      }


([pages, leves, notebookDivision]) => {
        return pages.map(page => {
          this.done$.next(this.done$.value + 1);
          return page.map(tab => {
            (tab as any).divisionId = +Object.keys(notebookDivision).find(key => {
              return notebookDivision[key].pages.includes(tab.id);
            });
            const division = notebookDivision[(tab as any).divisionId];
            (tab as any).requiredForAchievement = /\d{1,2}-\d{1,2}/.test(division.name.en) || division.name.en.startsWith('Housing') || division.name.en.startsWith('Ornaments');
            tab.recipes = tab.recipes.map(entry => {
              (entry as any).leves = Object.entries<any>(leves)
                .filter(([, leve]) => {
                  return leve.items.some(i => i.itemId === entry.itemId);
                })
                .map(([id]) => +id);
              return entry;
            });
            return tab;
          });
        });
      }


page => {
          this.done$.next(this.done$.value + 1);
          return page.map(tab => {
            (tab as any).divisionId = +Object.keys(notebookDivision).find(key => {
              return notebookDivision[key].pages.includes(tab.id);
            });
            const division = notebookDivision[(tab as any).divisionId];
            (tab as any).requiredForAchievement = /\d{1,2}-\d{1,2}/.test(division.name.en) || division.name.en.startsWith('Housing') || division.name.en.startsWith('Ornaments');
            tab.recipes = tab.recipes.map(entry => {
              (entry as any).leves = Object.entries<any>(leves)
                .filter(([, leve]) => {
                  return leve.items.some(i => i.itemId === entry.itemId);
                })
                .map(([id]) => +id);
              return entry;
            });
            return tab;
          });
        }


tab => {
            (tab as any).divisionId = +Object.keys(notebookDivision).find(key => {
              return notebookDivision[key].pages.includes(tab.id);
            });
            const division = notebookDivision[(tab as any).divisionId];
            (tab as any).requiredForAchievement = /\d{1,2}-\d{1,2}/.test(division.name.en) || division.name.en.startsWith('Housing') || division.name.en.startsWith('Ornaments');
            tab.recipes = tab.recipes.map(entry => {
              (entry as any).leves = Object.entries<any>(leves)
                .filter(([, leve]) => {
                  return leve.items.some(i => i.itemId === entry.itemId);
                })
                .map(([id]) => +id);
              return entry;
            });
            return tab;
          }


key => {
              return notebookDivision[key].pages.includes(tab.id);
            }


entry => {
              (entry as any).leves = Object.entries<any>(leves)
                .filter(([, leve]) => {
                  return leve.items.some(i => i.itemId === entry.itemId);
                })
                .map(([id]) => +id);
              return entry;
            }


([, leve]) => {
                  return leve.items.some(i => i.itemId === entry.itemId);
                }


i => i.itemId === entry.itemId


([id]) => +id


([pages, notebookDivision, extracts]) => {
        return pages.map(page => {
          this.done$.next(this.done$.value + 1);
          return page.map(tab => {
            (tab as any).divisionId = +Object.keys(notebookDivision).find(key => {
              return notebookDivision[key].pages.includes(tab.id);
            });
            const division = notebookDivision[(tab as any).divisionId];
            (tab as any).requiredForAchievement = /\d{1,2}-\d{1,2}/.test(division.name.en) || division.name.en.startsWith('Housing');
            tab.items = tab.items.map(item => {
              (item as any).nodes = getItemSource(extracts[item.itemId], DataType.GATHERED_BY).nodes
                .slice(0, 3)
                .map(node => {
                  return {
                    gatheringNode: node,
                    alarms: node.limited ? this.alarmsFacade.generateAlarms(node) : []
                  };
                });
              return item;
            });
            return tab;
          });
        });
      }


page => {
          this.done$.next(this.done$.value + 1);
          return page.map(tab => {
            (tab as any).divisionId = +Object.keys(notebookDivision).find(key => {
              return notebookDivision[key].pages.includes(tab.id);
            });
            const division = notebookDivision[(tab as any).divisionId];
            (tab as any).requiredForAchievement = /\d{1,2}-\d{1,2}/.test(division.name.en) || division.name.en.startsWith('Housing');
            tab.items = tab.items.map(item => {
              (item as any).nodes = getItemSource(extracts[item.itemId], DataType.GATHERED_BY).nodes
                .slice(0, 3)
                .map(node => {
                  return {
                    gatheringNode: node,
                    alarms: node.limited ? this.alarmsFacade.generateAlarms(node) : []
                  };
                });
              return item;
            });
            return tab;
          });
        }


tab => {
            (tab as any).divisionId = +Object.keys(notebookDivision).find(key => {
              return notebookDivision[key].pages.includes(tab.id);
            });
            const division = notebookDivision[(tab as any).divisionId];
            (tab as any).requiredForAchievement = /\d{1,2}-\d{1,2}/.test(division.name.en) || division.name.en.startsWith('Housing');
            tab.items = tab.items.map(item => {
              (item as any).nodes = getItemSource(extracts[item.itemId], DataType.GATHERED_BY).nodes
                .slice(0, 3)
                .map(node => {
                  return {
                    gatheringNode: node,
                    alarms: node.limited ? this.alarmsFacade.generateAlarms(node) : []
                  };
                });
              return item;
            });
            return tab;
          }


item => {
              (item as any).nodes = getItemSource(extracts[item.itemId], DataType.GATHERED_BY).nodes
                .slice(0, 3)
                .map(node => {
                  return {
                    gatheringNode: node,
                    alarms: node.limited ? this.alarmsFacade.generateAlarms(node) : []
                  };
                });
              return item;
            }


node => {
                  return {
                    gatheringNode: node,
                    alarms: node.limited ? this.alarmsFacade.generateAlarms(node) : []
                  };
                }


([minBtnSpearNodes, fishingLog, spearFishingLog, parameters]) => {
        const fishingLogData$ = combineLatest(fishingLog.map(entry => {
          return this.getFshData(entry.itemId, entry.spot.id).pipe(
            map((fshData) => {
              const fish: any = {
                entry,
                id: entry.spot.id,
                itemId: entry.itemId,
                level: entry.level,
                icon: entry.icon,
                data: fshData
              };
              if (parameters[entry.itemId]) {
                fish.timed = parameters[entry.itemId].timed;
                fish.weathered = parameters[entry.itemId].weathered;
              }
              return fish;
            })
          );
        }));

        const spearFishingLogData$ = combineLatest(spearFishingLog.map(entry => {
          const spot = minBtnSpearNodes.find(n => n.items.includes(entry.itemId));
          return this.getFshData(entry.itemId, spot.id).pipe(
            map((data) => {
              return {
                entry,
                id: spot.id,
                itemId: entry.itemId,
                level: spot.level,
                data: data,
                timed: data[0].gatheringNode.limited,
                tug: data[0].gatheringNode.tug
              };
            })
          );
        }));

        return combineLatest([fishingLogData$, spearFishingLogData$]).pipe(
          map(([fishingFish, spearFishingFish]) => {
            return [fishingFish, spearFishingFish].map(log => {
              this.done$.next(this.done$.value + 1);
              return log.reduce((display, fish) => {
                const displayCopy = { ...display };
                let row = displayCopy.tabs.find(e => e.mapId === fish.entry.mapId);
                if (row === undefined) {
                  displayCopy.tabs.push({
                    id: fish.id,
                    mapId: fish.entry.mapId,
                    placeId: fish.entry.placeId,
                    done: 0,
                    total: 0,
                    spots: []
                  });
                  row = displayCopy.tabs[displayCopy.tabs.length - 1];
                }
                const spotId = fish.entry.spot ? fish.entry.spot.id : fish.entry.id;
                let spot = row.spots.find(s => s.id === spotId);
                if (spot === undefined) {
                  const coords = fish.entry.spot ? fish.entry.spot.coords : fish.entry.coords;
                  row.spots.push({
                    id: spotId,
                    placeId: fish.entry.zoneId,
                    mapId: fish.entry.mapId,
                    done: 0,
                    total: 0,
                    coords: coords,
                    fishes: []
                  });
                  spot = row.spots[row.spots.length - 1];
                }
                const { entry, ...fishRow } = fish;
                spot.fishes.push(fishRow);
                return displayCopy;
              }, { tabs: [], total: 0, done: 0 });
            });
          })
        );
      }


entry => {
          return this.getFshData(entry.itemId, entry.spot.id).pipe(
            map((fshData) => {
              const fish: any = {
                entry,
                id: entry.spot.id,
                itemId: entry.itemId,
                level: entry.level,
                icon: entry.icon,
                data: fshData
              };
              if (parameters[entry.itemId]) {
                fish.timed = parameters[entry.itemId].timed;
                fish.weathered = parameters[entry.itemId].weathered;
              }
              return fish;
            })
          );
        }


(fshData) => {
              const fish: any = {
                entry,
                id: entry.spot.id,
                itemId: entry.itemId,
                level: entry.level,
                icon: entry.icon,
                data: fshData
              };
              if (parameters[entry.itemId]) {
                fish.timed = parameters[entry.itemId].timed;
                fish.weathered = parameters[entry.itemId].weathered;
              }
              return fish;
            }


entry => {
          const spot = minBtnSpearNodes.find(n => n.items.includes(entry.itemId));
          return this.getFshData(entry.itemId, spot.id).pipe(
            map((data) => {
              return {
                entry,
                id: spot.id,
                itemId: entry.itemId,
                level: spot.level,
                data: data,
                timed: data[0].gatheringNode.limited,
                tug: data[0].gatheringNode.tug
              };
            })
          );
        }


n => n.items.includes(entry.itemId)


(data) => {
              return {
                entry,
                id: spot.id,
                itemId: entry.itemId,
                level: spot.level,
                data: data,
                timed: data[0].gatheringNode.limited,
                tug: data[0].gatheringNode.tug
              };
            }


([fishingFish, spearFishingFish]) => {
            return [fishingFish, spearFishingFish].map(log => {
              this.done$.next(this.done$.value + 1);
              return log.reduce((display, fish) => {
                const displayCopy = { ...display };
                let row = displayCopy.tabs.find(e => e.mapId === fish.entry.mapId);
                if (row === undefined) {
                  displayCopy.tabs.push({
                    id: fish.id,
                    mapId: fish.entry.mapId,
                    placeId: fish.entry.placeId,
                    done: 0,
                    total: 0,
                    spots: []
                  });
                  row = displayCopy.tabs[displayCopy.tabs.length - 1];
                }
                const spotId = fish.entry.spot ? fish.entry.spot.id : fish.entry.id;
                let spot = row.spots.find(s => s.id === spotId);
                if (spot === undefined) {
                  const coords = fish.entry.spot ? fish.entry.spot.coords : fish.entry.coords;
                  row.spots.push({
                    id: spotId,
                    placeId: fish.entry.zoneId,
                    mapId: fish.entry.mapId,
                    done: 0,
                    total: 0,
                    coords: coords,
                    fishes: []
                  });
                  spot = row.spots[row.spots.length - 1];
                }
                const { entry, ...fishRow } = fish;
                spot.fishes.push(fishRow);
                return displayCopy;
              }, { tabs: [], total: 0, done: 0 });
            });
          }


log => {
              this.done$.next(this.done$.value + 1);
              return log.reduce((display, fish) => {
                const displayCopy = { ...display };
                let row = displayCopy.tabs.find(e => e.mapId === fish.entry.mapId);
                if (row === undefined) {
                  displayCopy.tabs.push({
                    id: fish.id,
                    mapId: fish.entry.mapId,
                    placeId: fish.entry.placeId,
                    done: 0,
                    total: 0,
                    spots: []
                  });
                  row = displayCopy.tabs[displayCopy.tabs.length - 1];
                }
                const spotId = fish.entry.spot ? fish.entry.spot.id : fish.entry.id;
                let spot = row.spots.find(s => s.id === spotId);
                if (spot === undefined) {
                  const coords = fish.entry.spot ? fish.entry.spot.coords : fish.entry.coords;
                  row.spots.push({
                    id: spotId,
                    placeId: fish.entry.zoneId,
                    mapId: fish.entry.mapId,
                    done: 0,
                    total: 0,
                    coords: coords,
                    fishes: []
                  });
                  spot = row.spots[row.spots.length - 1];
                }
                const { entry, ...fishRow } = fish;
                spot.fishes.push(fishRow);
                return displayCopy;
              }, { tabs: [], total: 0, done: 0 });
            }


(display, fish) => {
                const displayCopy = { ...display };
                let row = displayCopy.tabs.find(e => e.mapId === fish.entry.mapId);
                if (row === undefined) {
                  displayCopy.tabs.push({
                    id: fish.id,
                    mapId: fish.entry.mapId,
                    placeId: fish.entry.placeId,
                    done: 0,
                    total: 0,
                    spots: []
                  });
                  row = displayCopy.tabs[displayCopy.tabs.length - 1];
                }
                const spotId = fish.entry.spot ? fish.entry.spot.id : fish.entry.id;
                let spot = row.spots.find(s => s.id === spotId);
                if (spot === undefined) {
                  const coords = fish.entry.spot ? fish.entry.spot.coords : fish.entry.coords;
                  row.spots.push({
                    id: spotId,
                    placeId: fish.entry.zoneId,
                    mapId: fish.entry.mapId,
                    done: 0,
                    total: 0,
                    coords: coords,
                    fishes: []
                  });
                  spot = row.spots[row.spots.length - 1];
                }
                const { entry, ...fishRow } = fish;
                spot.fishes.push(fishRow);
                return displayCopy;
              }


e => e.mapId === fish.entry.mapId


s => s.id === spotId


([dohTabs, dolTabs, fshTabs]) => {
      const finalLogTrackingData = [
        ...dohTabs,
        ...dolTabs
      ];
      this.downloadFile('log-tracker-page-data.json', finalLogTrackingData);
      this.downloadFile('fishing-log-tracker-page-data.json', fshTabs);
      res$.next();
    }


(job) => {
      return this.getCollectables(job).pipe(
        map(groups => {
          this.done$.next(this.done$.value + 1);
          return { job, groups };
        })
      );
    }


groups => {
          this.done$.next(this.done$.value + 1);
          return { job, groups };
        }


collectablesPageData => {
      const indexed = collectablesPageData.reduce((acc, { job, groups }) => {
        return {
          ...acc,
          [job]: groups
        };
      }, {});
      this.downloadFile('collectables-page-data.json', indexed);
      res$.next(indexed);
    }


(acc, { job, groups }) => {
        return {
          ...acc,
          [job]: groups
        };
      }


lazyItems => {
        const chunks = _.chunk(Object.keys(lazyItems), 100);
        this.totalTodo$.next(chunks.length);
        return requestsWithDelay(chunks.map(itemIds => {
          return this.http.get<any[]>(`https://www.garlandtools.org/db/doc/item/en/3/${itemIds.join(',')}.json`).pipe(
            map(items => this.serializer.deserialize<ItemData>(items.filter(i => !i.error).map(item => item.obj), [ItemData])),
            switchMap((items: ItemData[]) => {
              if (items.length === 0) {
                return of([]);
              }
              return combineLatest(items.map(data => {
                const item: any = {
                  id: data.item.id
                };
                return this.extractor.addDataToItem(item, data).pipe(
                  map(extract => {
                    delete extract.yield;
                    delete extract.requires;
                    delete extract.workingOnIt;
                    return extract;
                  })
                );
              })).pipe(
                first()
              );
            }),
            tap(() => {
              this.done$.next(this.done$.value + 1);
            })
          );
        }), 200);
      }


itemIds => {
          return this.http.get<any[]>(`https://www.garlandtools.org/db/doc/item/en/3/${itemIds.join(',')}.json`).pipe(
            map(items => this.serializer.deserialize<ItemData>(items.filter(i => !i.error).map(item => item.obj), [ItemData])),
            switchMap((items: ItemData[]) => {
              if (items.length === 0) {
                return of([]);
              }
              return combineLatest(items.map(data => {
                const item: any = {
                  id: data.item.id
                };
                return this.extractor.addDataToItem(item, data).pipe(
                  map(extract => {
                    delete extract.yield;
                    delete extract.requires;
                    delete extract.workingOnIt;
                    return extract;
                  })
                );
              })).pipe(
                first()
              );
            }),
            tap(() => {
              this.done$.next(this.done$.value + 1);
            })
          );
        }


items => this.serializer.deserialize<ItemData>(items.filter(i => !i.error).map(item => item.obj), [ItemData])


i => !i.error


item => item.obj


(items: ItemData[]) => {
              if (items.length === 0) {
                return of([]);
              }
              return combineLatest(items.map(data => {
                const item: any = {
                  id: data.item.id
                };
                return this.extractor.addDataToItem(item, data).pipe(
                  map(extract => {
                    delete extract.yield;
                    delete extract.requires;
                    delete extract.workingOnIt;
                    return extract;
                  })
                );
              })).pipe(
                first()
              );
            }


data => {
                const item: any = {
                  id: data.item.id
                };
                return this.extractor.addDataToItem(item, data).pipe(
                  map(extract => {
                    delete extract.yield;
                    delete extract.requires;
                    delete extract.workingOnIt;
                    return extract;
                  })
                );
              }


extract => {
                    delete extract.yield;
                    delete extract.requires;
                    delete extract.workingOnIt;
                    return extract;
                  }


() => {
              this.done$.next(this.done$.value + 1);
            }


items => {
      const extracts = [].concat.apply([], items).reduce((acc, i) => {
        acc[i.id] = i;
        return acc;
      }, {});
      this.downloadFile('extracts.json', extracts);
      res$.next(extracts);
    }


(acc, i) => {
        acc[i.id] = i;
        return acc;
      }


([collectables, collectablesShops]) => {
        return combineLatest(Object.keys(collectables)
          .filter(key => {
            const collectableEntry = collectables[key];
            if (collectableEntry.hwd || !collectableEntry.collectable) {
              return false;
            }
            const job = Object.keys(collectablesShops).find(sKey => {
              return collectablesShops[sKey].includes(collectableEntry.shopId);
            });
            return job !== undefined && (+job + 8) === jobId;
          })
          .map(key => {
            return {
              ...collectables[key],
              itemId: +key,
              amount: 1
            };
          })
          .reduce((acc, row) => {
            let group = acc.find(accRow => accRow.groupId === row.group);
            if (group === undefined) {
              acc.push({
                groupId: row.group,
                collectables: []
              });
              group = acc[acc.length - 1];
            }
            group.collectables.push(row);
            return acc;
          }, [])
          .map(group => {
            return safeCombineLatest(group.collectables
              .sort((a, b) => b.levelMax - a.levelMax)
              .map(collectable => {
                return combineLatest([
                  this.getExp(collectable, collectable.base.exp),
                  this.getExp(collectable, collectable.mid.exp),
                  this.getExp(collectable, collectable.high.exp)
                ]).pipe(
                  switchMap(([expBase, expMid, expHigh]) => {
                    collectable.expBase = expBase;
                    collectable.expMid = expMid;
                    collectable.expHigh = expHigh;
                    if ([16, 17, 18].includes(jobId)) {
                      return this.gatheringNodesService.getItemNodes(collectable.itemId, true).pipe(
                        map(nodes => {
                          collectable.nodes = nodes.map(gatheringNode => {
                            return {
                              gatheringNode,
                              alarms: gatheringNode.limited ? this.alarmsFacade.generateAlarms(gatheringNode) : []
                            };
                          });
                          return collectable;
                        })
                      );
                    }
                    return of(collectable);
                  })
                );
              })
            ).pipe(
              map(res => {
                group.collectables = res;
                return group;
              })
            );
          })
        );
      }


key => {
            const collectableEntry = collectables[key];
            if (collectableEntry.hwd || !collectableEntry.collectable) {
              return false;
            }
            const job = Object.keys(collectablesShops).find(sKey => {
              return collectablesShops[sKey].includes(collectableEntry.shopId);
            });
            return job !== undefined && (+job + 8) === jobId;
          }


sKey => {
              return collectablesShops[sKey].includes(collectableEntry.shopId);
            }


key => {
            return {
              ...collectables[key],
              itemId: +key,
              amount: 1
            };
          }


(acc, row) => {
            let group = acc.find(accRow => accRow.groupId === row.group);
            if (group === undefined) {
              acc.push({
                groupId: row.group,
                collectables: []
              });
              group = acc[acc.length - 1];
            }
            group.collectables.push(row);
            return acc;
          }


accRow => accRow.groupId === row.group


group => {
            return safeCombineLatest(group.collectables
              .sort((a, b) => b.levelMax - a.levelMax)
              .map(collectable => {
                return combineLatest([
                  this.getExp(collectable, collectable.base.exp),
                  this.getExp(collectable, collectable.mid.exp),
                  this.getExp(collectable, collectable.high.exp)
                ]).pipe(
                  switchMap(([expBase, expMid, expHigh]) => {
                    collectable.expBase = expBase;
                    collectable.expMid = expMid;
                    collectable.expHigh = expHigh;
                    if ([16, 17, 18].includes(jobId)) {
                      return this.gatheringNodesService.getItemNodes(collectable.itemId, true).pipe(
                        map(nodes => {
                          collectable.nodes = nodes.map(gatheringNode => {
                            return {
                              gatheringNode,
                              alarms: gatheringNode.limited ? this.alarmsFacade.generateAlarms(gatheringNode) : []
                            };
                          });
                          return collectable;
                        })
                      );
                    }
                    return of(collectable);
                  })
                );
              })
            ).pipe(
              map(res => {
                group.collectables = res;
                return group;
              })
            );
          }


(a, b) => b.levelMax - a.levelMax