From ecf3eff8d469a548c072bac66e12be49df87624c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 27 Feb 2023 15:17:32 -0500 Subject: [PATCH] Convert runMany and runManySync to use injectManyWithMeta Signed-off-by: Sebastian Malton --- packages/core/src/common/runnable/helpers.ts | 101 +++++++++++++++++ .../src/common/runnable/run-many-for.test.ts | 81 ++++---------- .../core/src/common/runnable/run-many-for.ts | 87 ++------------- .../common/runnable/run-many-sync-for.test.ts | 37 ++----- .../src/common/runnable/run-many-sync-for.ts | 102 +++++++++++------- packages/core/src/common/runnable/types.ts | 40 +++++++ 6 files changed, 242 insertions(+), 206 deletions(-) create mode 100644 packages/core/src/common/runnable/helpers.ts create mode 100644 packages/core/src/common/runnable/types.ts diff --git a/packages/core/src/common/runnable/helpers.ts b/packages/core/src/common/runnable/helpers.ts new file mode 100644 index 0000000000..fd8dec0bcd --- /dev/null +++ b/packages/core/src/common/runnable/helpers.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { DiContainerForInjection, Injectable, InjectionInstanceWithMeta, InjectionToken } from "@ogre-tools/injectable"; +import { getOrInsertSetFor, isDefined } from "../utils"; +import * as uuid from "uuid"; +import assert from "assert"; +import type { Runnable, RunnableSync, RunnableSyncWithId, RunnableWithId } from "./types"; + +const computedNextEdge = (traversed: string[], graph: Map>, currentId: string, seenIds: Set) => { + seenIds.add(currentId); + const currentNode = graph.get(currentId); + + assert(currentNode, `Runnable graph does not contain node with id="${currentId}"`); + + for (const nextId of currentNode.values()) { + if (traversed.includes(nextId)) { + throw new Error(`Cycle in runnable graph: "${traversed.join(`" -> "`)}" -> "${nextId}"`); + } + + computedNextEdge([...traversed, nextId], graph, nextId, seenIds); + } +}; + +export function verifyRunnablesAreDAG(injectionToken: InjectionToken, void>, runnables: RunnableWithId[]): void; +export function verifyRunnablesAreDAG(injectionToken: InjectionToken, void>, runnables: RunnableSyncWithId[]): void; + +export function verifyRunnablesAreDAG(injectionToken: InjectionToken, void> | InjectionToken, void>, runnables: (RunnableWithId[]) | (RunnableSyncWithId[])): void { + const rootId = uuid.v4(); + const runnableGraph = new Map>(); + const seenIds = new Set(); + const addRunnableId = getOrInsertSetFor(runnableGraph); + + // Build the Directed graph + for (const runnable of runnables) { + addRunnableId(runnable.id); + + if (runnable.runAfter.length === 0) { + addRunnableId(rootId).add(runnable.id); + } else { + for (const parentRunnable of runnable.runAfter) { + addRunnableId(parentRunnable.id).add(runnable.id); + } + } + } + + addRunnableId(rootId); + + // Do a DFS to find any cycles + computedNextEdge([], runnableGraph, rootId, seenIds); + + for (const id of runnableGraph.keys()) { + if (!seenIds.has(id)) { + const runnable = runnables.find(runnable => runnable.id === id); + + if (!runnable) { + throw new Error(`Runnable "${id}" is not part of the injection token "${injectionToken.id}"`); + } + + const runAfters = [runnable.runAfter] + .flat() + .filter(isDefined) + .map(runnable => runnable.id) + .join('", "'); + + throw new Error(`Runnable "${id}" is unreachable for injection token "${injectionToken.id}": run afters "${runAfters}" are a part of different injection tokens.`); + } + } +} + +export const convertToWithIdWith = (di: DiContainerForInjection) => { + const convertToWithIdPlain = (injectable: Injectable, Runnable, void>): RunnableWithId => { + const instance = di.inject(injectable); + + return ({ + id: injectable.id, + run: instance.run, + runAfter: [instance.runAfter] + .flat() + .filter(isDefined) + .map(convertToWithIdPlain), + }); + }; + + function convertToWithId(src: InjectionInstanceWithMeta>): RunnableWithId; + function convertToWithId(src: InjectionInstanceWithMeta>): RunnableSyncWithId; + + function convertToWithId(src: InjectionInstanceWithMeta> | InjectionInstanceWithMeta>): RunnableWithId | RunnableSyncWithId { + return ({ + id: src.meta.id, + run: src.instance.run, + runAfter: [src.instance.runAfter] + .flat() + .filter(isDefined) + .map(convertToWithIdPlain), + }); + } + + return convertToWithId; +}; diff --git a/packages/core/src/common/runnable/run-many-for.test.ts b/packages/core/src/common/runnable/run-many-for.test.ts index 6002a149db..ffbde3c92b 100644 --- a/packages/core/src/common/runnable/run-many-for.test.ts +++ b/packages/core/src/common/runnable/run-many-for.test.ts @@ -5,7 +5,7 @@ import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/injectable"; -import type { Runnable } from "./run-many-for"; +import type { Runnable } from "./types"; import { runManyFor } from "./run-many-for"; import { getPromiseStatus } from "../test-utils/get-promise-status"; import { runInAction } from "mobx"; @@ -28,7 +28,6 @@ describe("runManyFor", () => { const someInjectable = getInjectable({ id: "some-injectable", instantiate: () => ({ - id: "some-injectable", run: () => runMock("some-call"), }), injectionToken: someInjectionTokenForRunnables, @@ -37,7 +36,6 @@ describe("runManyFor", () => { const someOtherInjectable = getInjectable({ id: "some-other-injectable", instantiate: () => ({ - id: "some-other-injectable", run: () => runMock("some-other-call"), }), injectionToken: someInjectionTokenForRunnables, @@ -85,32 +83,25 @@ describe("runManyFor", () => { const someInjectable1 = getInjectable({ id: "some-injectable-1", - - instantiate: (di) => ({ - id: "some-injectable-1", + instantiate: () => ({ run: () => runMock("third-level-run"), - runAfter: di.inject(someInjectable2), + runAfter: someInjectable2, }), - injectionToken: someInjectionTokenForRunnables, }); const someInjectable2 = getInjectable({ id: "some-injectable-2", - - instantiate: (di) => ({ - id: "some-injectable-2", + instantiate: () => ({ run: () => runMock("second-level-run"), - runAfter: di.inject(someInjectable3), + runAfter: someInjectable3, }), - injectionToken: someInjectionTokenForRunnables, }); const someInjectable3 = getInjectable({ id: "some-injectable-3", instantiate: () => ({ - id: "some-injectable-3", run: () => runMock("first-level-run"), }), injectionToken: someInjectionTokenForRunnables, @@ -197,24 +188,18 @@ describe("runManyFor", () => { const someInjectable = getInjectable({ id: "some-runnable-1", - - instantiate: (di) => ({ - id: "some-runnable-1", + instantiate: () => ({ run: () => runMock("some-runnable-1"), - runAfter: di.inject(someOtherInjectable), + runAfter: someOtherInjectable, }), - injectionToken: someInjectionToken, }); const someOtherInjectable = getInjectable({ id: "some-runnable-2", - instantiate: () => ({ - id: "some-runnable-2", run: () => runMock("some-runnable-2"), }), - injectionToken: someOtherInjectionToken, }); @@ -244,38 +229,29 @@ describe("runManyFor", () => { const someInjectable = getInjectable({ id: "some-runnable-1", - - instantiate: (di) => ({ - id: "some-runnable-1", + instantiate: () => ({ run: () => runMock("some-runnable-1"), runAfter: [ - di.inject(someOtherInjectable), - di.inject(someSecondInjectable), + someOtherInjectable, + someSecondInjectable, ], }), - injectionToken: someInjectionToken, }); const someSecondInjectable = getInjectable({ id: "some-runnable-2", - instantiate: () => ({ - id: "some-runnable-2", run: () => runMock("some-runnable-2"), }), - injectionToken: someInjectionToken, }); const someOtherInjectable = getInjectable({ id: "some-runnable-3", - instantiate: () => ({ - id: "some-runnable-3", run: () => runMock("some-runnable-3"), }), - injectionToken: someOtherInjectionToken, }); @@ -306,23 +282,17 @@ describe("runManyFor", () => { const someInjectable = getInjectable({ id: "some-runnable-1", - instantiate: () => ({ - id: "some-runnable-1", run: (parameter) => runMock("run-of-some-runnable-1", parameter), }), - injectionToken: someInjectionTokenForRunnablesWithParameter, }); const someOtherInjectable = getInjectable({ id: "some-runnable-2", - instantiate: () => ({ - id: "some-runnable-2", run: (parameter) => runMock("run-of-some-runnable-2", parameter), }), - injectionToken: someInjectionTokenForRunnablesWithParameter, }); @@ -359,7 +329,6 @@ describe("runManyFor", () => { const runnableOneInjectable = getInjectable({ id: "runnable-1", instantiate: () => ({ - id: "runnable-1", run: () => runMock("runnable-1"), }), injectionToken: someInjectionToken, @@ -368,7 +337,6 @@ describe("runManyFor", () => { const runnableTwoInjectable = getInjectable({ id: "runnable-2", instantiate: () => ({ - id: "runnable-2", run: () => runMock("runnable-2"), runAfter: [], // shouldn't block being called }), @@ -377,42 +345,38 @@ describe("runManyFor", () => { const runnableThreeInjectable = getInjectable({ id: "runnable-3", - instantiate: (di) => ({ - id: "runnable-3", + instantiate: () => ({ run: () => runMock("runnable-3"), - runAfter: di.inject(runnableOneInjectable), + runAfter: runnableOneInjectable, }), injectionToken: someInjectionToken, }); const runnableFourInjectable = getInjectable({ id: "runnable-4", - instantiate: (di) => ({ - id: "runnable-4", + instantiate: () => ({ run: () => runMock("runnable-4"), - runAfter: [di.inject(runnableThreeInjectable)], // should be the same as an single item + runAfter: [runnableThreeInjectable], // should be the same as an single item }), injectionToken: someInjectionToken, }); const runnableFiveInjectable = getInjectable({ id: "runnable-5", - instantiate: (di) => ({ - id: "runnable-5", + instantiate: () => ({ run: () => runMock("runnable-5"), - runAfter: di.inject(runnableThreeInjectable), + runAfter: runnableThreeInjectable, }), injectionToken: someInjectionToken, }); const runnableSixInjectable = getInjectable({ id: "runnable-6", - instantiate: (di) => ({ - id: "runnable-6", + instantiate: () => ({ run: () => runMock("runnable-6"), runAfter: [ - di.inject(runnableFourInjectable), - di.inject(runnableFiveInjectable), + runnableFourInjectable, + runnableFiveInjectable, ], }), injectionToken: someInjectionToken, @@ -420,12 +384,11 @@ describe("runManyFor", () => { const runnableSevenInjectable = getInjectable({ id: "runnable-7", - instantiate: (di) => ({ - id: "runnable-7", + instantiate: () => ({ run: () => runMock("runnable-7"), runAfter: [ - di.inject(runnableFiveInjectable), - di.inject(runnableSixInjectable), + runnableFiveInjectable, + runnableSixInjectable, ], }), injectionToken: someInjectionToken, diff --git a/packages/core/src/common/runnable/run-many-for.ts b/packages/core/src/common/runnable/run-many-for.ts index 106cc74da1..88d55b1d1a 100644 --- a/packages/core/src/common/runnable/run-many-for.ts +++ b/packages/core/src/common/runnable/run-many-for.ts @@ -3,84 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable"; -import type { SingleOrMany } from "../utils"; -import { getOrInsert, getOrInsertSetFor, isDefined } from "../utils"; -import * as uuid from "uuid"; -import assert from "assert"; -import type { Asyncify } from "type-fest"; +import { getOrInsert } from "../utils"; import type TypedEventEmitter from "typed-emitter"; import EventEmitter from "events"; - -export interface Runnable { - id: string; - run: Run; - runAfter?: SingleOrMany>; -} - -type Run = (parameter: Param) => Promise | void; +import { convertToWithIdWith, verifyRunnablesAreDAG } from "./helpers"; +import type { RunnableWithId, Runnable, Run } from "./types"; +import type { Asyncify } from "type-fest"; export type RunMany = (injectionToken: InjectionToken, void>) => Asyncify>; -const computedNextEdge = (traversed: string[], graph: Map>, currentId: string, seenIds: Set) => { - seenIds.add(currentId); - const currentNode = graph.get(currentId); - - assert(currentNode, `Runnable graph does not contain node with id="${currentId}"`); - - for (const nextId of currentNode.values()) { - if (traversed.includes(nextId)) { - throw new Error(`Cycle in runnable graph: "${traversed.join(`" -> "`)}" -> "${nextId}"`); - } - - computedNextEdge([...traversed, nextId], graph, nextId, seenIds); - } -}; - -const verifyRunnablesAreDAG = (injectionToken: InjectionToken, void>, runnables: Runnable[]) => { - const rootId = uuid.v4(); - const runnableGraph = new Map>(); - const seenIds = new Set(); - const addRunnableId = getOrInsertSetFor(runnableGraph); - - // Build the Directed graph - for (const runnable of runnables) { - addRunnableId(runnable.id); - - if (!runnable.runAfter || (Array.isArray(runnable.runAfter) && runnable.runAfter.length === 0)) { - addRunnableId(rootId).add(runnable.id); - } else if (Array.isArray(runnable.runAfter)) { - for (const parentRunnable of runnable.runAfter) { - addRunnableId(parentRunnable.id).add(runnable.id); - } - } else { - addRunnableId(runnable.runAfter.id).add(runnable.id); - } - } - - addRunnableId(rootId); - - // Do a DFS to find any cycles - computedNextEdge([], runnableGraph, rootId, seenIds); - - for (const id of runnableGraph.keys()) { - if (!seenIds.has(id)) { - const runnable = runnables.find(runnable => runnable.id === id); - - if (!runnable) { - throw new Error(`Runnable "${id}" is not part of the injection token "${injectionToken.id}"`); - } - - const runAfters = [runnable.runAfter] - .flat() - .filter(isDefined) - .map(runnable => runnable.id) - .join('", "'); - - throw new Error(`Runnable "${id}" is unreachable for injection token "${injectionToken.id}": run afters "${runAfters}" are a part of different injection tokens.`); - } - } -}; - interface BarrierEvent { finish: (id: string) => void; } @@ -116,10 +47,8 @@ class DynamicBarrier { const executeRunnableWith = (param: Param) => { const barrier = new DynamicBarrier(); - return async (runnable: Runnable): Promise => { - const parentRunnables = [runnable.runAfter].flat().filter(isDefined); - - for (const parentRunnable of parentRunnables) { + return async (runnable: RunnableWithId): Promise => { + for (const parentRunnable of runnable.runAfter) { await barrier.blockOn(parentRunnable.id); } @@ -129,9 +58,11 @@ const executeRunnableWith = (param: Param) => { }; export function runManyFor(di: DiContainerForInjection): RunMany { + const convertToWithId = convertToWithIdWith(di); + return (injectionToken: InjectionToken, void>) => async (param: Param) => { const executeRunnable = executeRunnableWith(param); - const allRunnables = di.injectMany(injectionToken); + const allRunnables = di.injectManyWithMeta(injectionToken).map(x => convertToWithId(x)); verifyRunnablesAreDAG(injectionToken, allRunnables); diff --git a/packages/core/src/common/runnable/run-many-sync-for.test.ts b/packages/core/src/common/runnable/run-many-sync-for.test.ts index 3aadcad0bf..e66c51d0b1 100644 --- a/packages/core/src/common/runnable/run-many-sync-for.test.ts +++ b/packages/core/src/common/runnable/run-many-sync-for.test.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/injectable"; -import type { RunnableSync } from "./run-many-sync-for"; import { runManySyncFor } from "./run-many-sync-for"; +import type { RunnableSync } from "./types"; describe("runManySyncFor", () => { describe("given hierarchy, when running many", () => { @@ -22,7 +22,6 @@ describe("runManySyncFor", () => { const someInjectable = getInjectable({ id: "some-injectable", instantiate: () => ({ - id: "some-injectable", run: () => runMock("some-call"), }), injectionToken: someInjectionTokenForRunnables, @@ -31,7 +30,6 @@ describe("runManySyncFor", () => { const someOtherInjectable = getInjectable({ id: "some-other-injectable", instantiate: () => ({ - id: "some-other-injectable", run: () => runMock("some-other-call"), }), injectionToken: someInjectionTokenForRunnables, @@ -66,32 +64,25 @@ describe("runManySyncFor", () => { const someInjectable1 = getInjectable({ id: "some-injectable-1", - - instantiate: (di) => ({ - id: "some-injectable-1", + instantiate: () => ({ run: () => void runMock("third-level-run"), - runAfter: di.inject(someInjectable2), + runAfter: someInjectable2, }), - injectionToken: someInjectionTokenForRunnables, }); const someInjectable2 = getInjectable({ id: "some-injectable-2", - - instantiate: (di) => ({ - id: "some-injectable-2", + instantiate: () => ({ run: () => void runMock("second-level-run"), - runAfter: di.inject(someInjectable3), + runAfter: someInjectable3, }), - injectionToken: someInjectionTokenForRunnables, }); const someInjectable3 = getInjectable({ id: "some-injectable-3", instantiate: () => ({ - id: "some-injectable-3", run: () => void runMock("first-level-run"), }), injectionToken: someInjectionTokenForRunnables, @@ -124,24 +115,18 @@ describe("runManySyncFor", () => { const someInjectable = getInjectable({ id: "some-runnable-1", - - instantiate: (di) => ({ - id: "some-runnable-1", + instantiate: () => ({ run: () => runMock("some-runnable-1"), - runAfter: di.inject(someOtherInjectable), + runAfter: someOtherInjectable, }), - injectionToken: someInjectionToken, }); const someOtherInjectable = getInjectable({ id: "some-runnable-2", - instantiate: () => ({ - id: "some-runnable-2", run: () => runMock("some-runnable-2"), }), - injectionToken: someOtherInjectionToken, }); @@ -152,7 +137,7 @@ describe("runManySyncFor", () => { ); return expect(() => runMany()).toThrow( - /Tried to get a composite but encountered missing parent ids: "some-runnable-2".\n\nAvailable parent ids are:\n"[0-9a-z-]+",\n"some-runnable-1"/, + /Runnable "some-runnable-1" is unreachable for injection token "some-injection-token": run afters "some-runnable-2" are a part of different injection tokens./, ); }); @@ -172,23 +157,17 @@ describe("runManySyncFor", () => { const someInjectable = getInjectable({ id: "some-runnable-1", - instantiate: () => ({ - id: "some-runnable-1", run: (parameter) => void runMock("run-of-some-runnable-1", parameter), }), - injectionToken: someInjectionTokenForRunnablesWithParameter, }); const someOtherInjectable = getInjectable({ id: "some-runnable-2", - instantiate: () => ({ - id: "some-runnable-2", run: (parameter) => void runMock("run-of-some-runnable-2", parameter), }), - injectionToken: someInjectionTokenForRunnablesWithParameter, }); diff --git a/packages/core/src/common/runnable/run-many-sync-for.ts b/packages/core/src/common/runnable/run-many-sync-for.ts index 08dba2f72d..13aac9fb37 100644 --- a/packages/core/src/common/runnable/run-many-sync-for.ts +++ b/packages/core/src/common/runnable/run-many-sync-for.ts @@ -3,53 +3,75 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable"; -import type { Composite } from "../utils/composite/get-composite/get-composite"; -import { getCompositeFor } from "../utils/composite/get-composite/get-composite"; -import * as uuid from "uuid"; - -export interface RunnableSync { - id: string; - run: RunSync; - runAfter?: RunnableSync; -} - -/** - * NOTE: this is the worse of two evils. This makes sure that `RunnableSync` always is sync. - * If the return type is `void` instead then async functions (those return `Promise`) can - * coerce to it. - */ -type RunSync = (parameter: Param) => undefined; +import type { Disposer } from "../utils"; +import type { RunnableSync, RunSync, RunnableSyncWithId } from "./types"; +import { convertToWithIdWith, verifyRunnablesAreDAG } from "./helpers"; +import type TypedEventEmitter from "typed-emitter"; +import EventEmitter from "events"; export type RunManySync = (injectionToken: InjectionToken, void>) => RunSync; -function runCompositeRunnableSyncs(param: Param, composite: Composite>): undefined { - composite.value.run(param); - composite.children.map(composite => runCompositeRunnableSyncs(param, composite)); +class SyncBarrier { + private readonly finishedIds = new Set(); + private readonly events: TypedEventEmitter void>> = new EventEmitter(); - return undefined; + setFinished(id: string): void { + this.finishedIds.add(id); + this.events.emit(id); + } + + onceParentsAreFinished(id: string, parentIds: string[], action: () => void) { + const finishers = new Map(); + + const checkAndRun = () => { + if (finishers.size === 0) { + action(); + this.setFinished(id); + } + }; + + for (const parentId of parentIds) { + if (this.finishedIds.has(parentId)) { + continue; + } + + const onParentFinished = () => { + this.events.removeListener(parentId, onParentFinished); + finishers.delete(parentId); + checkAndRun(); + }; + + finishers.set(parentId, onParentFinished); + this.events.once(parentId, onParentFinished); + } + + checkAndRun(); + } } +const executeRunnableWith = (param: Param) => { + const barrier = new SyncBarrier(); + + return (runnable: RunnableSyncWithId) => { + barrier.onceParentsAreFinished( + runnable.id, + runnable.runAfter.map(r => r.id), + () => runnable.run(param), + ); + }; +}; + export function runManySyncFor(di: DiContainerForInjection): RunManySync { - return (injectionToken: InjectionToken, void>) => (param: Param): undefined => { - const allRunnables = di.injectMany(injectionToken); - const rootId = uuid.v4(); - const getCompositeRunnables = getCompositeFor>({ - getId: (runnable) => runnable.id, - getParentId: (runnable) => ( - runnable.id === rootId - ? undefined - : runnable.runAfter?.id ?? rootId - ), - }); - const composite = getCompositeRunnables([ - // This is a dummy runnable to conform to the requirements of `getCompositeFor` to only have one root - { - id: rootId, - run: () => undefined, - }, - ...allRunnables, - ]); + const convertToWithId = convertToWithIdWith(di); - return runCompositeRunnableSyncs(param, composite); + return (injectionToken: InjectionToken, void>) => (param: Param): undefined => { + const executeRunnable = executeRunnableWith(param); + const allRunnables = di.injectManyWithMeta(injectionToken).map(convertToWithId); + + verifyRunnablesAreDAG(injectionToken, allRunnables); + + allRunnables.forEach(executeRunnable); + + return undefined; }; } diff --git a/packages/core/src/common/runnable/types.ts b/packages/core/src/common/runnable/types.ts new file mode 100644 index 0000000000..4fa7647cad --- /dev/null +++ b/packages/core/src/common/runnable/types.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { Injectable } from "@ogre-tools/injectable"; +import type { SingleOrMany } from "../utils"; + +export type Run = (parameter: Param) => Promise | void; + +export interface Runnable { + id?: never; + run: Run; + readonly runAfter?: SingleOrMany, Runnable, void>>; +} + +export interface RunnableWithId { + run: Run; + readonly id: string; + readonly runAfter: RunnableWithId[]; +} + +export interface RunnableSync { + id?: never; + run: RunSync; + runAfter?: SingleOrMany, RunnableSync, void>>; +} + +export interface RunnableSyncWithId { + run: RunSync; + readonly id: string; + readonly runAfter: RunnableSyncWithId[]; +} + +/** + * NOTE: this is the worse of two evils. This makes sure that `RunnableSync` always is sync. + * If the return type is `void` instead then async functions (those return `Promise`) can + * coerce to it. + */ +export type RunSync = (parameter: Param) => undefined;