<commit_before>'use strict';

const { exec } = require('child_process');
const { join } = require('path');
const ApiGateway = require('./ApiGateway.js');
const ApiGatewayWebSocket = require('./ApiGatewayWebSocket.js');
const debugLog = require('./debugLog.js');
const {
  functionCacheCleanup,
  getFunctionOptions,
} = require('./functionHelper.js');
const { satisfiesVersionRange } = require('./utils/index.js');
const {
  defaults,
  commands: commandOptions,
  CUSTOM_OPTION,
  SERVER_SHUTDOWN_TIMEOUT,
  supportedRuntimes,
} = require('./config/index.js');
const { peerDependencies } = require('../package.json');

const { stringify } = JSON;

module.exports = class ServerlessOffline {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.service = serverless.service;
    this.log = serverless.cli.log.bind(serverless.cli);
    this.options = options;
    this.exitCode = 0;
    this.commands = {
      offline: {
        // add start nested options
        commands: {
          start: {
            lifecycleEvents: ['init', 'end'],
            options: commandOptions,
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: commandOptions,
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    };

    this.hooks = {
      'offline:start:init': this.start.bind(this),
      'offline:start': this.startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
    };
  }

  printBlankLine() {
    if (process.env.NODE_ENV !== 'test') {
      console.log();
    }
  }

  logPluginIssue() {
    this.log(
      'If you think this is an issue with the plugin please submit it, thanks!',
    );
    this.log('https://github.com/dherault/serverless-offline/issues');
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this._verifyServerlessVersionCompatibility();

    // Some users would like to know their environment outside of the handler
    process.env.IS_OFFLINE = true;

    this.mergeOptions();

    await this._buildApiGateway();
    await this.apiGateway.startServer();
    await this._buildApiGatewayWebSocket();

    this.setupEvents();

    if (this.apiGatewayWebSocket.hasWebsocketRoutes) {
      await this.apiGatewayWebSocket.startServer();
    }

    if (process.env.NODE_ENV !== 'test') {
      if (this.options.exec) {
        await this._executeShellScript();
      } else {
        await this._listenForTermination();
      }
    }
  }

  /**
   * Entry point for the plugin (sls offline) when running 'sls offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running sls offline that can be expected, but docs say that
   * 'sls offline start' will provide the init and end hooks for other plugins to consume
   * */
  async startWithExplicitEnd() {
    await this.start();
    this.end();
  }

  async _listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'));
    });

    this.log(`Got ${command} signal. Offline Halting...`);
  }

  _executeShellScript() {
    const command = this.options.exec;
    const options = {
      env: {
        IS_OFFLINE: true,
        ...this.service.provider.environment,
        ...this.originalEnvironment,
      },
    };

    this.log(`Offline executing script [${command}]`);

    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        this.log(`exec stdout: [${stdout}]`);
        this.log(`exec stderr: [${stderr}]`);

        if (error) {
          // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
          this.log(`Offline error executing script [${error}]`);
          this.exitCode = error.code || 1;
        }
        resolve();
      });
    });
  }

  async _buildApiGateway() {
    this.apiGateway = new ApiGateway(
      this.serverless,
      this.options,
      this.velocityContextOptions,
    );

    await this.apiGateway.registerPlugins();
    this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource
    this.apiGateway._create404Route(); // Not found handling
  }

  async _buildApiGatewayWebSocket() {
    this.apiGatewayWebSocket = new ApiGatewayWebSocket(
      this.serverless,
      this.options,
    );
    await this.apiGatewayWebSocket.createServer();
  }

  mergeOptions() {
    // stores the original process.env for assigning upon invoking the handlers
    this.originalEnvironment = { ...process.env };

    // In the constructor, stage and regions are set to undefined
    if (this.options.region === undefined) delete this.options.region;
    if (this.options.stage === undefined) delete this.options.stage;

    // TODO FIXME remove, leftover from default options
    const defaultsTEMP = {
      region: this.service.provider.region,
      stage: this.service.provider.stage,
    };

    // custom options
    const { [CUSTOM_OPTION]: customOptions } = this.service.custom || {};

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.options = {
      ...defaults,
      ...defaultsTEMP, // TODO FIXME, see above
      ...customOptions,
      ...this.options,
    };

    // Prefix must start and end with '/'
    if (!this.options.prefix.startsWith('/')) {
      this.options.prefix = `/${this.options.prefix}`;
    }
    if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';

    this.velocityContextOptions = {
      stage: this.options.stage,
      stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
    };

    // Parse CORS options
    this.options.corsAllowHeaders = this.options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',');
    this.options.corsAllowOrigin = this.options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',');
    this.options.corsExposedHeaders = this.options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',');

    if (this.options.corsDisallowCredentials) {
      this.options.corsAllowCredentials = false;
    }

    this.options.corsConfig = {
      credentials: this.options.corsAllowCredentials,
      exposedHeaders: this.options.corsExposedHeaders,
      headers: this.options.corsAllowHeaders,
      origin: this.options.corsAllowOrigin,
    };

    this.options.cacheInvalidationRegex = new RegExp(
      this.options.cacheInvalidationRegex,
    );

    this.log(`Starting Offline: ${this.options.stage}/${this.options.region}.`);
    debugLog('options:', this.options);
  }

  async end() {
    this.log('Halting offline server');
    functionCacheCleanup();
    await this.apiGateway.stop(SERVER_SHUTDOWN_TIMEOUT);

    if (process.env.NODE_ENV !== 'test') {
      process.exit(this.exitCode);
    }
  }

  setupEvents() {
    this._verifySupportedRuntime();

    const defaultContentType = 'application/json';
    const { apiKeys } = this.service.provider;
    const protectedRoutes = [];

    // for simple API Key authentication model
    if (apiKeys) {
      this.log(`Key with token: ${this.options.apiKey}`);

      if (this.options.noAuth) {
        this.log(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        );
      } else {
        this.log('Remember to use x-api-key on the request headers');
      }
    }

    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;
    }

    Object.entries(this.service.functions).forEach(
      ([functionName, functionObj]) => {
        const servicePath = join(
          this.serverless.config.servicePath,
          this.options.location,
        );

        const funOptions = getFunctionOptions(
          functionObj,
          functionName,
          servicePath,
          runtime,
        );

        debugLog(`funOptions ${stringify(funOptions, null, 2)} `);
        this.printBlankLine();
        debugLog(functionName, 'runtime', runtime);
        this.log(`Routes for ${functionName}:`);

        if (!functionObj.events) {
          functionObj.events = [];
        }

        // TODO `fun.name` is not set in the jest test run
        // possible serverless BUG?
        if (process.env.NODE_ENV !== 'test') {
          // Add proxy for lamda invoke
          functionObj.events.push({
            http: {
              integration: 'lambda',
              method: 'POST',
              path: `{apiVersion}/functions/${functionObj.name}/invocations`,
              request: {
                template: {
                  // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json'
                  'binary/octet-stream': '$input.body',
                },
              },
              response: {
                headers: {
                  'Content-Type': 'application/json',
                },
              },
            },
          });
        }

        // Adds a route for each http endpoint
        functionObj.events.forEach((event) => {
          const { http, websocket } = event;

          if (http) {
            this.apiGateway.createRoutes(
              functionName,
              functionObj,
              event,
              funOptions,
              servicePath,
              protectedRoutes,
              runtime,
              defaultContentType,
            );
          }

          if (websocket) {
            this.apiGatewayWebSocket.createWsAction(
              functionName,
              functionObj,
              event,
              funOptions,
              servicePath,
            );
          }
        });
      },
    );
  }

  _verifySupportedRuntime() {
    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;

      if (!runtime) {
        throw new Error(
          `Runtime "provided" is not supported by "Serverless-Offline".
           Please specify the additional "providedRuntime" option.
          `,
        );
      }
    }

    // print message but keep working (don't error out or exit process)
    if (!supportedRuntimes.has(runtime)) {
      this.printBlankLine();
      this.log(`Warning: found unsupported runtime '${runtime}'`);
    }
  }

  // TODO: missing tests
  _verifyServerlessVersionCompatibility() {
    const { version: currentVersion } = this.serverless;
    const { serverless: requiredVersion } = peerDependencies;

    const versionIsSatisfied = satisfiesVersionRange(
      requiredVersion,
      currentVersion,
    );

    if (!versionIsSatisfied) {
      this.log(
        `"Serverless-Offline" requires "Serverless" version ${requiredVersion} but found version ${currentVersion}.
         Be aware that functionality might be limited or has serious bugs.
         To avoid any issues update "Serverless" to a later version.
        `,
        'serverless-offline',
        { color: 'red' },
      );
    }
  }
};
<commit_msg>Fix options import<commit_after>'use strict';

const { exec } = require('child_process');
const { join } = require('path');
const ApiGateway = require('./ApiGateway.js');
const ApiGatewayWebSocket = require('./ApiGatewayWebSocket.js');
const debugLog = require('./debugLog.js');
const {
  functionCacheCleanup,
  getFunctionOptions,
} = require('./functionHelper.js');
const { satisfiesVersionRange } = require('./utils/index.js');
const {
  CUSTOM_OPTION,
  defaults,
  options: commandOptions,
  SERVER_SHUTDOWN_TIMEOUT,
  supportedRuntimes,
} = require('./config/index.js');
const { peerDependencies } = require('../package.json');

const { stringify } = JSON;

module.exports = class ServerlessOffline {
  constructor(serverless, options) {
    this.serverless = serverless;
    this.service = serverless.service;
    this.log = serverless.cli.log.bind(serverless.cli);
    this.options = options;
    this.exitCode = 0;
    this.commands = {
      offline: {
        // add start nested options
        commands: {
          start: {
            lifecycleEvents: ['init', 'end'],
            options: commandOptions,
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: commandOptions,
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    };

    this.hooks = {
      'offline:start:init': this.start.bind(this),
      'offline:start': this.startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
    };
  }

  printBlankLine() {
    if (process.env.NODE_ENV !== 'test') {
      console.log();
    }
  }

  logPluginIssue() {
    this.log(
      'If you think this is an issue with the plugin please submit it, thanks!',
    );
    this.log('https://github.com/dherault/serverless-offline/issues');
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this._verifyServerlessVersionCompatibility();

    // Some users would like to know their environment outside of the handler
    process.env.IS_OFFLINE = true;

    this.mergeOptions();

    await this._buildApiGateway();
    await this.apiGateway.startServer();
    await this._buildApiGatewayWebSocket();

    this.setupEvents();

    if (this.apiGatewayWebSocket.hasWebsocketRoutes) {
      await this.apiGatewayWebSocket.startServer();
    }

    if (process.env.NODE_ENV !== 'test') {
      if (this.options.exec) {
        await this._executeShellScript();
      } else {
        await this._listenForTermination();
      }
    }
  }

  /**
   * Entry point for the plugin (sls offline) when running 'sls offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running sls offline that can be expected, but docs say that
   * 'sls offline start' will provide the init and end hooks for other plugins to consume
   * */
  async startWithExplicitEnd() {
    await this.start();
    this.end();
  }

  async _listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'));
    });

    this.log(`Got ${command} signal. Offline Halting...`);
  }

  _executeShellScript() {
    const command = this.options.exec;
    const options = {
      env: {
        IS_OFFLINE: true,
        ...this.service.provider.environment,
        ...this.originalEnvironment,
      },
    };

    this.log(`Offline executing script [${command}]`);

    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        this.log(`exec stdout: [${stdout}]`);
        this.log(`exec stderr: [${stderr}]`);

        if (error) {
          // Use the failed command's exit code, proceed as normal so that shutdown can occur gracefully
          this.log(`Offline error executing script [${error}]`);
          this.exitCode = error.code || 1;
        }
        resolve();
      });
    });
  }

  async _buildApiGateway() {
    this.apiGateway = new ApiGateway(
      this.serverless,
      this.options,
      this.velocityContextOptions,
    );

    await this.apiGateway.registerPlugins();
    this.apiGateway._createResourceRoutes(); // HTTP Proxy defined in Resource
    this.apiGateway._create404Route(); // Not found handling
  }

  async _buildApiGatewayWebSocket() {
    this.apiGatewayWebSocket = new ApiGatewayWebSocket(
      this.serverless,
      this.options,
    );
    await this.apiGatewayWebSocket.createServer();
  }

  mergeOptions() {
    // stores the original process.env for assigning upon invoking the handlers
    this.originalEnvironment = { ...process.env };

    // In the constructor, stage and regions are set to undefined
    if (this.options.region === undefined) delete this.options.region;
    if (this.options.stage === undefined) delete this.options.stage;

    // TODO FIXME remove, leftover from default options
    const defaultsTEMP = {
      region: this.service.provider.region,
      stage: this.service.provider.stage,
    };

    // custom options
    const { [CUSTOM_OPTION]: customOptions } = this.service.custom || {};

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.options = {
      ...defaults,
      ...defaultsTEMP, // TODO FIXME, see above
      ...customOptions,
      ...this.options,
    };

    // Prefix must start and end with '/'
    if (!this.options.prefix.startsWith('/')) {
      this.options.prefix = `/${this.options.prefix}`;
    }
    if (!this.options.prefix.endsWith('/')) this.options.prefix += '/';

    this.velocityContextOptions = {
      stage: this.options.stage,
      stageVariables: {}, // this.service.environment.stages[this.options.stage].vars,
    };

    // Parse CORS options
    this.options.corsAllowHeaders = this.options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',');
    this.options.corsAllowOrigin = this.options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',');
    this.options.corsExposedHeaders = this.options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',');

    if (this.options.corsDisallowCredentials) {
      this.options.corsAllowCredentials = false;
    }

    this.options.corsConfig = {
      credentials: this.options.corsAllowCredentials,
      exposedHeaders: this.options.corsExposedHeaders,
      headers: this.options.corsAllowHeaders,
      origin: this.options.corsAllowOrigin,
    };

    this.options.cacheInvalidationRegex = new RegExp(
      this.options.cacheInvalidationRegex,
    );

    this.log(`Starting Offline: ${this.options.stage}/${this.options.region}.`);
    debugLog('options:', this.options);
  }

  async end() {
    this.log('Halting offline server');
    functionCacheCleanup();
    await this.apiGateway.stop(SERVER_SHUTDOWN_TIMEOUT);

    if (process.env.NODE_ENV !== 'test') {
      process.exit(this.exitCode);
    }
  }

  setupEvents() {
    this._verifySupportedRuntime();

    const defaultContentType = 'application/json';
    const { apiKeys } = this.service.provider;
    const protectedRoutes = [];

    // for simple API Key authentication model
    if (apiKeys) {
      this.log(`Key with token: ${this.options.apiKey}`);

      if (this.options.noAuth) {
        this.log(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        );
      } else {
        this.log('Remember to use x-api-key on the request headers');
      }
    }

    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;
    }

    Object.entries(this.service.functions).forEach(
      ([functionName, functionObj]) => {
        const servicePath = join(
          this.serverless.config.servicePath,
          this.options.location,
        );

        const funOptions = getFunctionOptions(
          functionObj,
          functionName,
          servicePath,
          runtime,
        );

        debugLog(`funOptions ${stringify(funOptions, null, 2)} `);
        this.printBlankLine();
        debugLog(functionName, 'runtime', runtime);
        this.log(`Routes for ${functionName}:`);

        if (!functionObj.events) {
          functionObj.events = [];
        }

        // TODO `fun.name` is not set in the jest test run
        // possible serverless BUG?
        if (process.env.NODE_ENV !== 'test') {
          // Add proxy for lamda invoke
          functionObj.events.push({
            http: {
              integration: 'lambda',
              method: 'POST',
              path: `{apiVersion}/functions/${functionObj.name}/invocations`,
              request: {
                template: {
                  // AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json'
                  'binary/octet-stream': '$input.body',
                },
              },
              response: {
                headers: {
                  'Content-Type': 'application/json',
                },
              },
            },
          });
        }

        // Adds a route for each http endpoint
        functionObj.events.forEach((event) => {
          const { http, websocket } = event;

          if (http) {
            this.apiGateway.createRoutes(
              functionName,
              functionObj,
              event,
              funOptions,
              servicePath,
              protectedRoutes,
              runtime,
              defaultContentType,
            );
          }

          if (websocket) {
            this.apiGatewayWebSocket.createWsAction(
              functionName,
              functionObj,
              event,
              funOptions,
              servicePath,
            );
          }
        });
      },
    );
  }

  _verifySupportedRuntime() {
    let { runtime } = this.service.provider;

    if (runtime === 'provided') {
      runtime = this.options.providedRuntime;

      if (!runtime) {
        throw new Error(
          `Runtime "provided" is not supported by "Serverless-Offline".
           Please specify the additional "providedRuntime" option.
          `,
        );
      }
    }

    // print message but keep working (don't error out or exit process)
    if (!supportedRuntimes.has(runtime)) {
      this.printBlankLine();
      this.log(`Warning: found unsupported runtime '${runtime}'`);
    }
  }

  // TODO: missing tests
  _verifyServerlessVersionCompatibility() {
    const { version: currentVersion } = this.serverless;
    const { serverless: requiredVersion } = peerDependencies;

    const versionIsSatisfied = satisfiesVersionRange(
      requiredVersion,
      currentVersion,
    );

    if (!versionIsSatisfied) {
      this.log(
        `"Serverless-Offline" requires "Serverless" version ${requiredVersion} but found version ${currentVersion}.
         Be aware that functionality might be limited or has serious bugs.
         To avoid any issues update "Serverless" to a later version.
        `,
        'serverless-offline',
        { color: 'red' },
      );
    }
  }
};
<|endoftext|>


<commit_before>/**
@module ember
@submodule ember-templates
*/

import { keyword } from 'htmlbars-runtime/hooks';
import closureAction from 'ember-routing-htmlbars/keywords/closure-action';

/**
  The `{{action}}` helper provides a way to pass triggers for behavior (usually
  just a function) between components, and into components from controllers.

  ### Passing functions with the action helper

  There are three contexts an action helper can be used in. The first two
  contexts to discuss are attribute context, and Handlebars value context.

  ```handlebars
  {{! An example of attribute context }}
  <div onclick={{action "save"}}></div>
  {{! Examples of Handlebars value context }}
  {{input on-input=(action "save")}}
  {{yield (action "refreshData") andAnotherParam}}
  ```

  In these contexts,
  the helper is called a "closure action" helper. It's behavior is simple:
  If passed a function name, read that function off the `actions` property
  of the current context. Once that function is read (or if a function was
  passed), create a closure over that function and any arguments.

  The resulting value of an action helper used this way is simply a function.
  For example with this attribute context example:

  ```handlebars
  {{! An example of attribute context }}
  <div onclick={{action "save"}}></div>
  ```

  The resulting template render logic would be:

  ```js
  var div = document.createElement('div');
  var actionFunction = (function(context){
    return function() {
      return context.actions.save.apply(context, arguments);
    };
  })(context);
  div.onclick = actionFunction;
  ```

  Thus when the div is clicked, the action on that context is called.
  Because the `actionFunction` is just a function, closure actions can be
  passed between components the still execute in the correct context.

  Here is an example action handler on a component:

  ```js
  export default Ember.Component.extend({
    actions: {
      save(/* event *\/) {
        this.get('model').save();
      }
    }
  });
  ```

  Actions are always looked up on the `actions` property of the current context.
  This avoids collisions in the naming of common actions, such as `destroy`.

  Two options can be passed to the `action` helper when it is used in this way.

  * `target=someProperty` will look to `someProperty` instead of the current
    context for the `actions` hash. This can be useful when targetting a
    service for actions.
  * `value="target.value"` will read the path `target.value` off the first
    argument to the action when it is called and rewrite the first argument
    to be that value. This is useful when attaching actions to event listeners.

  ### Invoking an action

  Closure actions curry both their scope and any arguments. When invoked, any
  additional arguments are added to the already curried list.

  Actions should be invoked using the [sendAction](/api/classes/Ember.Component.html#method_sendAction)
  method. The first argument to `sendAction` is the action to be called, and
  additional arguments are passed to the action function. This has interesting
  properties combined with currying of arguments. For example:

  ```js
  export default Ember.Component.extend({
    actions: {
      // Usage {{input on-input=(action (action 'setName' model) value="target.value")}}
      setName(model, name) {
        model.set('name', name);
      }
    }
  });
  ```

  The first argument (`model`) was curried over, and the run-time argument (`event`)
  becomes a second argument. Action calls be nested this way because each simply
  returns a function. Any function can be passed to the `{{action` helper, including
  other actions.

  Actions invoked with `sendAction` have the same currying behavior as demonstrated
  with `on-input` above. For example:

  ```js
  export default Ember.Component.extend({
    actions: {
      setName(model, name) {
        model.set('name', name);
      }
    }
  });
  ```

  ```handlebars
  {{my-input submit=(action 'setName' model)}}
  ```

  ```js
  // app/components/my-component.js
  export default Ember.Component.extend({
    click() {
      // Note that model is not passed, it was curried in the template
      this.sendAction('submit', 'bob');
    }
  });
  ```

  ### Attaching actions to DOM

  The third context the `{{action` helper can be used in we call "element space".
  For example:

  ```handlebars
  {{! An example of element space }}
  <div {{action "save"}}></div>
  ```

  Used this way, the `{{action}}` helper provides a useful shortcut for
  registering an HTML element within a template for a single DOM event and
  forwarding that interaction to the template's context (controller or component).

  If the context of a template is a controller, actions used this way will
  bubble to routes when the controller does not implement the specified action.
  Once an action hits a route, it will bubble through the route hierarchy.

  ### Event Propagation

  `{{action` helpers called in element space can control event bubbling.

  Events triggered through the action helper will automatically have
  `.preventDefault()` called on them. You do not need to do so in your event
  handlers. If you need to allow event propagation (to handle file inputs for
  example) you can supply the `preventDefault=false` option to the `{{action}}` helper:

  ```handlebars
  <div {{action "sayHello" preventDefault=false}}>
    <input type="file" />
    <input type="checkbox" />
  </div>
  ```

  To disable bubbling, pass `bubbles=false` to the helper:

  ```handlebars
  <button {{action 'edit' post bubbles=false}}>Edit</button>
  ```

  If you need the default handler to trigger you should either register your
  own event handler, or use event methods on your view class. See [Ember.View](/api/classes/Ember.View.html)
  'Responding to Browser Events' for more information.

  ### Specifying DOM event type

  `{{action` helpers called in element space can specify an event type.

  By default the `{{action}}` helper registers for DOM `click` events. You can
  supply an `on` option to the helper to specify a different DOM event name:

  ```handlebars
  <div {{action "anActionName" on="double-click"}}>
    click me
  </div>
  ```

  See [Event Names](/api/classes/Ember.View.html#toc_event-names) for a list of
  acceptable DOM event names.

  ### Specifying whitelisted modifier keys

  `{{action` helpers called in element space can specify modifier keys.

  By default the `{{action}}` helper will ignore click event with pressed modifier
  keys. You can supply an `allowedKeys` option to specify which keys should not be ignored.

  ```handlebars
  <div {{action "anActionName" allowedKeys="alt"}}>
    click me
  </div>
  ```

  This way the `{{action}}` will fire when clicking with the alt key pressed down.

  Alternatively, supply "any" to the `allowedKeys` option to accept any combination of modifier keys.

  ```handlebars
  <div {{action "anActionName" allowedKeys="any"}}>
    click me with any key pressed
  </div>
  ```

  ### Specifying a Target

  A `target` option can be provided to the helper to change
  which object will receive the method call. This option must be a path
  to an object, accessible in the current context:

  ```handlebars
  {{! app/templates/application.hbs }}
  <div {{action "anActionName" target=someService}}>
    click me
  </div>
  ```

  ```javascript
  // app/controllers/application.js
  export default Ember.Controller.extend({
    someService: Ember.inject.service()
  });
  ```

  @method action
  @for Ember.Templates.helpers
  @public
*/
export default function(morph, env, scope, params, hash, template, inverse, visitor) {
  if (morph) {
    keyword('@element_action', morph, env, scope, params, hash, template, inverse, visitor);
    return true;
  }

  return closureAction(morph, env, scope, params, hash, template, inverse, visitor);
}
<commit_msg>[DOC release] Fix `action` helper event type example <commit_after>/**
@module ember
@submodule ember-templates
*/

import { keyword } from 'htmlbars-runtime/hooks';
import closureAction from 'ember-routing-htmlbars/keywords/closure-action';

/**
  The `{{action}}` helper provides a way to pass triggers for behavior (usually
  just a function) between components, and into components from controllers.

  ### Passing functions with the action helper

  There are three contexts an action helper can be used in. The first two
  contexts to discuss are attribute context, and Handlebars value context.

  ```handlebars
  {{! An example of attribute context }}
  <div onclick={{action "save"}}></div>
  {{! Examples of Handlebars value context }}
  {{input on-input=(action "save")}}
  {{yield (action "refreshData") andAnotherParam}}
  ```

  In these contexts,
  the helper is called a "closure action" helper. It's behavior is simple:
  If passed a function name, read that function off the `actions` property
  of the current context. Once that function is read (or if a function was
  passed), create a closure over that function and any arguments.

  The resulting value of an action helper used this way is simply a function.
  For example with this attribute context example:

  ```handlebars
  {{! An example of attribute context }}
  <div onclick={{action "save"}}></div>
  ```

  The resulting template render logic would be:

  ```js
  var div = document.createElement('div');
  var actionFunction = (function(context){
    return function() {
      return context.actions.save.apply(context, arguments);
    };
  })(context);
  div.onclick = actionFunction;
  ```

  Thus when the div is clicked, the action on that context is called.
  Because the `actionFunction` is just a function, closure actions can be
  passed between components the still execute in the correct context.

  Here is an example action handler on a component:

  ```js
  export default Ember.Component.extend({
    actions: {
      save(/* event *\/) {
        this.get('model').save();
      }
    }
  });
  ```

  Actions are always looked up on the `actions` property of the current context.
  This avoids collisions in the naming of common actions, such as `destroy`.

  Two options can be passed to the `action` helper when it is used in this way.

  * `target=someProperty` will look to `someProperty` instead of the current
    context for the `actions` hash. This can be useful when targetting a
    service for actions.
  * `value="target.value"` will read the path `target.value` off the first
    argument to the action when it is called and rewrite the first argument
    to be that value. This is useful when attaching actions to event listeners.

  ### Invoking an action

  Closure actions curry both their scope and any arguments. When invoked, any
  additional arguments are added to the already curried list.

  Actions should be invoked using the [sendAction](/api/classes/Ember.Component.html#method_sendAction)
  method. The first argument to `sendAction` is the action to be called, and
  additional arguments are passed to the action function. This has interesting
  properties combined with currying of arguments. For example:

  ```js
  export default Ember.Component.extend({
    actions: {
      // Usage {{input on-input=(action (action 'setName' model) value="target.value")}}
      setName(model, name) {
        model.set('name', name);
      }
    }
  });
  ```

  The first argument (`model`) was curried over, and the run-time argument (`event`)
  becomes a second argument. Action calls be nested this way because each simply
  returns a function. Any function can be passed to the `{{action` helper, including
  other actions.

  Actions invoked with `sendAction` have the same currying behavior as demonstrated
  with `on-input` above. For example:

  ```js
  export default Ember.Component.extend({
    actions: {
      setName(model, name) {
        model.set('name', name);
      }
    }
  });
  ```

  ```handlebars
  {{my-input submit=(action 'setName' model)}}
  ```

  ```js
  // app/components/my-component.js
  export default Ember.Component.extend({
    click() {
      // Note that model is not passed, it was curried in the template
      this.sendAction('submit', 'bob');
    }
  });
  ```

  ### Attaching actions to DOM

  The third context the `{{action` helper can be used in we call "element space".
  For example:

  ```handlebars
  {{! An example of element space }}
  <div {{action "save"}}></div>
  ```

  Used this way, the `{{action}}` helper provides a useful shortcut for
  registering an HTML element within a template for a single DOM event and
  forwarding that interaction to the template's context (controller or component).

  If the context of a template is a controller, actions used this way will
  bubble to routes when the controller does not implement the specified action.
  Once an action hits a route, it will bubble through the route hierarchy.

  ### Event Propagation

  `{{action` helpers called in element space can control event bubbling.

  Events triggered through the action helper will automatically have
  `.preventDefault()` called on them. You do not need to do so in your event
  handlers. If you need to allow event propagation (to handle file inputs for
  example) you can supply the `preventDefault=false` option to the `{{action}}` helper:

  ```handlebars
  <div {{action "sayHello" preventDefault=false}}>
    <input type="file" />
    <input type="checkbox" />
  </div>
  ```

  To disable bubbling, pass `bubbles=false` to the helper:

  ```handlebars
  <button {{action 'edit' post bubbles=false}}>Edit</button>
  ```

  If you need the default handler to trigger you should either register your
  own event handler, or use event methods on your view class. See [Ember.View](/api/classes/Ember.View.html)
  'Responding to Browser Events' for more information.

  ### Specifying DOM event type

  `{{action` helpers called in element space can specify an event type.

  By default the `{{action}}` helper registers for DOM `click` events. You can
  supply an `on` option to the helper to specify a different DOM event name:

  ```handlebars
  <div {{action "anActionName" on="doubleClick"}}>
    click me
  </div>
  ```

  See [Event Names](/api/classes/Ember.View.html#toc_event-names) for a list of
  acceptable DOM event names.

  ### Specifying whitelisted modifier keys

  `{{action` helpers called in element space can specify modifier keys.

  By default the `{{action}}` helper will ignore click event with pressed modifier
  keys. You can supply an `allowedKeys` option to specify which keys should not be ignored.

  ```handlebars
  <div {{action "anActionName" allowedKeys="alt"}}>
    click me
  </div>
  ```

  This way the `{{action}}` will fire when clicking with the alt key pressed down.

  Alternatively, supply "any" to the `allowedKeys` option to accept any combination of modifier keys.

  ```handlebars
  <div {{action "anActionName" allowedKeys="any"}}>
    click me with any key pressed
  </div>
  ```

  ### Specifying a Target

  A `target` option can be provided to the helper to change
  which object will receive the method call. This option must be a path
  to an object, accessible in the current context:

  ```handlebars
  {{! app/templates/application.hbs }}
  <div {{action "anActionName" target=someService}}>
    click me
  </div>
  ```

  ```javascript
  // app/controllers/application.js
  export default Ember.Controller.extend({
    someService: Ember.inject.service()
  });
  ```

  @method action
  @for Ember.Templates.helpers
  @public
*/
export default function(morph, env, scope, params, hash, template, inverse, visitor) {
  if (morph) {
    keyword('@element_action', morph, env, scope, params, hash, template, inverse, visitor);
    return true;
  }

  return closureAction(morph, env, scope, params, hash, template, inverse, visitor);
}
<|endoftext|>


<commit_before>Divvy.View = {};

Divvy.View.init = function()
{
	this.win = Ti.UI.createWindow({
		barColor: Divvy.winBarColor,
		barImage: Divvy.winBarImage,
	});

	this.win.addEventListener('close', function(e){
		Divvy.View.close();
	});

	this.cameraButton = Ti.UI.createButton({
		systemButton: Ti.UI.iPhone.SystemButton.CAMERA,
		backgroundColor: '#00BFFF'
	});

	this.cameraDialog = Ti.UI.createOptionDialog({
		options: ['Take Photo', 'Choose Existing', 'Cancel'],
		cancel: 2
	});

	this.cameraButton.addEventListener('click', function(e){
		Divvy.View.cameraDialog.show();
	});

	this.cameraDialog.addEventListener('click', function(e){
		if (e.index == 0)
		{
			Ti.Media.showCamera({
				success: Divvy.View.savePhoto,
				allowEditing: false,
				mediaTypes: [Ti.Media.MEDIA_TYPE_PHOTO]
			});
		}
		else if (e.index == 1)
		{
			Ti.Media.openPhotoGallery({
				success: Divvy.View.savePhoto,
				allowEditing: false,
				mediaTyles: [Ti.Media.MEDIA_TYPE_PHOTO]
			});
		}
	});

	this.win.rightNavButton = this.cameraButton;

	this.scrollView = this.createScrollView();

	this.win.add(this.scrollView);

	this.uploadIndicator = Ti.UI.createProgressBar({
			//TODO: well, do the above, the uploadIndicator
	});
};

Divvy.View.createScrollView = function()
{
	var scrollView = Ti.UI.createScrollView({
		contentWidth: 320,
		contentHeight: 'auto',
		top: 0,
		showVerticalScrollIndicator: true,
		backgroundColor: 'white'
	});

	scrollView.addEventListener('touchstart',function(e)
	{
		e.source.opacity = 0.7;
	});

	scrollView.addEventListener('touchend',function(e)
	{
		e.source.opacity = 1.0;
		Divvy.Preview.open(e.source.imageNumber,Divvy.View.numOfImages,e.source.imageId);
	});

	return scrollView;
};

Divvy.View.open = function(name, id)
{
	this.win.title = name;
	this.win.id = id;

	this.refresh();
	Divvy.open(this.win);
};

Divvy.View.close = function()
{
	this.win.remove(this.scrollView);
	delete this.scrollView;
	this.scrollView = this.createScrollView();
	this.win.add(this.scrollView);
};

Divvy.View.refresh = function()
{
	this.numOfImages = 0;
	Network.cache.run (
		Divvy.url + 'thumbnails/'+Divvy.View.win.id+"/-1/asc",
		Network.CACHE_INVALIDATE,
		Divvy.View.onRefreshSuccess,
		Divvy.View.onRefreshError
	);
};

Divvy.View.onRefreshSuccess = function(data, date, status, user, xhr)
{
	try
	{
		data = JSON.parse(data);
	}
	catch (excp)
	{
		Divvy.View.onRefreshError(Network.PARSE_ERROR, 0);
		return;
	}

	if (data.status == 'error')
	{
		Divvy.View.onRefreshError(data.error, 0);
		return;
	}

	var thumbnails = data.thumbnails;
	var i = 0;
	for (var j in thumbnails)
	{
		Divvy.View.scrollView.add(Divvy.View.generateImageThumbnail(i, thumbnails[j].id, thumbnails[j].url));
		i++;
	}
};

Divvy.View.onRefreshError = function(status, httpStatus)
{
	alert("Couldn't get bucket information. ("+status+")");
};

Divvy.View.savePhoto = function(e)
{
	var image = e.media;

	/*
	 * iPhone 4S image dimensions
	 * fullres: 3264x2448 (h x w)
	 * halfres: 1632x1224 (h x w)
	 *
	 * iPhone 4 image dimensions
	 * fullres: 1936x2592 (h x w)
	 * halfres: 968x612 (h x w)
	 */
	var targetHeight = 968;
	var targetWidth = 612;

	if (image.height > targetHeight || image.width > targetWidth)
	{
		if (image.height > image.width)
		{
			var newHeight = targetHeight;
			var newWidth = (targetHeight/image.height)*image.width;
		}
		else
		{
			var newWidth = targetWidth;
			var newHeight = (targetWidth/image.width)*image.height;
		}

		var resizedImage = Ti.UI.createImageView({
			image: image,
			width: newWidth, height: newHeight
		});

		image = resizedImage.toImage();
	}

	Network.cache.asyncPost(
		Divvy.url + 'upload',
		{ image:  image, bucket_id: Divvy.View.win.id },
		Divvy.View.onSendSuccess,
		Divvy.View.onSendError,
		Divvy.View.onSendStream
	);
};

Divvy.View.onSendSuccess = function(data, date, status, user, xhr)
{
	try
	{
		data = JSON.parse(data);
	}
	catch (excp)
	{
		Divvy.View.onSendError(Network.PARSE_ERROR, 0);
		return;
	}

	if (data.status == 'error')
		Divvy.View.onSendError(data.error, 0);

	Divvy.View.refresh();
	Divvy.Buckets.refresh();
};

Divvy.View.onSendError = function (status, httpStatus)
{
	alert('Image upload failed, please try again. ('+status+')');
};

Divvy.View.onSendStream  = function(progress)
{
	Divvy.View.uploadProgress.value = progress;
};

Divvy.View.generateImageThumbnail = function(num,id,image)
{
	this.numOfImages++;
	var x = num % 4;
	var y = Math.floor(num / 4);

	var padding = 4;
	var dimension = 75;

	var thumbnail = Ti.UI.createImageView({
		width: dimension, height: dimension,
		top: ((dimension+padding)*y)+padding, left: ((dimension+padding)*x)+padding,
		//TODO: set default image
		hires: true,
		borderWidth: 1,
		borderColor: '#ccc',
		imageNumber: num + 1,
		imageFile: image,
		imageId: id,
		backgroundColor: '#000000',
	});

	Network.cache.run(
		image,
		168, //1 week
		Divvy.View.onImageCacheSuccess,
		Divvy.View.onImageCacheError,
		thumbnail
	);

	return thumbnail;
};

Divvy.View.onImageCacheSuccess = function(data, date, status, user, xhr)
{
	user.image = data;
};

Divvy.View.onImageCacheError = function(status, httpStatus)
{
	//do nothing
};<commit_msg>Added activity indicators.<commit_after>Divvy.View = {};

Divvy.View.init = function()
{
	this.win = Ti.UI.createWindow({
		barColor: Divvy.winBarColor,
		barImage: Divvy.winBarImage,
	});

	this.win.addEventListener('close', function(e){
		Divvy.View.close();
	});

	this.cameraButton = Ti.UI.createButton({
		systemButton: Ti.UI.iPhone.SystemButton.CAMERA,
		backgroundColor: '#00BFFF'
	});

	this.cameraDialog = Ti.UI.createOptionDialog({
		options: ['Take Photo', 'Choose Existing', 'Cancel'],
		cancel: 2
	});

	this.cameraButton.addEventListener('click', function(e){
		Divvy.View.cameraDialog.show();
	});

	this.cameraDialog.addEventListener('click', function(e){
		if (e.index == 0)
		{
			Ti.Media.showCamera({
				success: Divvy.View.savePhoto,
				allowEditing: false,
				mediaTypes: [Ti.Media.MEDIA_TYPE_PHOTO]
			});
		}
		else if (e.index == 1)
		{
			Ti.Media.openPhotoGallery({
				success: Divvy.View.savePhoto,
				allowEditing: false,
				mediaTyles: [Ti.Media.MEDIA_TYPE_PHOTO]
			});
		}
	});

	this.win.rightNavButton = this.cameraButton;

	this.scrollView = this.createScrollView();

	this.win.add(this.scrollView);

	this.uploadIndicator = Ti.UI.createProgressBar({
		width: 250,
		min: 0,
		max: 1,
		value: 0,
		color: '#fff',
		message: 'Uploading Photo',
		font: {fontSize: 14, fontWeight: 'bold'},
		style: Ti.UI.iPhone.ProgressBarStyle.PLAIN,
	});

	this.uploadIndicator.show();

	this.flexSpace = Ti.UI.createButton({
		systemButton: Ti.UI.iPhone.SystemButton.FLEXIBLE_SPACE
	});
};

Divvy.View.createScrollView = function()
{
	var scrollView = Ti.UI.createScrollView({
		contentWidth: 320,
		contentHeight: 'auto',
		top: 0,
		showVerticalScrollIndicator: true,
		backgroundColor: 'white'
	});

	scrollView.addEventListener('touchstart',function(e)
	{
		e.source.opacity = 0.7;
	});

	scrollView.addEventListener('touchend',function(e)
	{
		e.source.opacity = 1.0;
		Divvy.Preview.open(e.source.imageNumber,Divvy.View.numOfImages,e.source.imageId);
	});

	return scrollView;
};

Divvy.View.open = function(name, id)
{
	this.win.title = name;
	this.win.id = id;

	this.refresh();
	Divvy.open(this.win);
};

Divvy.View.close = function()
{
	this.win.remove(this.scrollView);
	delete this.scrollView;
	this.scrollView = this.createScrollView();
	this.win.add(this.scrollView);
};

Divvy.View.refresh = function()
{
	this.numOfImages = 0;
	Network.cache.run (
		Divvy.url + 'thumbnails/'+Divvy.View.win.id+"/-1/asc",
		Network.CACHE_INVALIDATE,
		Divvy.View.onRefreshSuccess,
		Divvy.View.onRefreshError
	);
};

Divvy.View.onRefreshSuccess = function(data, date, status, user, xhr)
{
	try
	{
		data = JSON.parse(data);
	}
	catch (excp)
	{
		Divvy.View.onRefreshError(Network.PARSE_ERROR, 0);
		return;
	}

	if (data.status == 'error')
	{
		Divvy.View.onRefreshError(data.error, 0);
		return;
	}

	var thumbnails = data.thumbnails;
	var i = 0;
	for (var j in thumbnails)
	{
		Divvy.View.scrollView.add(Divvy.View.generateImageThumbnail(i, thumbnails[j].id, thumbnails[j].url));
		i++;
	}
};

Divvy.View.onRefreshError = function(status, httpStatus)
{
	alert("Couldn't get bucket information. ("+status+")");
};

Divvy.View.savePhoto = function(e)
{
	var image = e.media;

	/*
	 * iPhone 4S image dimensions
	 * fullres: 3264x2448 (h x w)
	 * halfres: 1632x1224 (h x w)
	 *
	 * iPhone 4 image dimensions
	 * fullres: 1936x2592 (h x w)
	 * halfres: 968x612 (h x w)
	 */
	var targetHeight = 968;
	var targetWidth = 612;

	if (image.height > targetHeight || image.width > targetWidth)
	{
		if (image.height > image.width)
		{
			var newHeight = targetHeight;
			var newWidth = (targetHeight/image.height)*image.width;
		}
		else
		{
			var newWidth = targetWidth;
			var newHeight = (targetWidth/image.width)*image.height;
		}

		var resizedImage = Ti.UI.createImageView({
			image: image,
			width: newWidth, height: newHeight
		});

		image = resizedImage.toImage();
	}

	Divvy.View.win.setToolbar([Divvy.View.flexSpace, Divvy.View.uploadIndicator, Divvy.View.flexSpace]);

	Network.cache.asyncPost(
		Divvy.url + 'upload',
		{ image:  image, bucket_id: Divvy.View.win.id },
		Divvy.View.onSendSuccess,
		Divvy.View.onSendError,
		Divvy.View.onSendStream
	);
};

Divvy.View.onSendSuccess = function(data, date, status, user, xhr)
{
	try
	{
		data = JSON.parse(data);
	}
	catch (excp)
	{
		Divvy.View.onSendError(Network.PARSE_ERROR, 0);
		return;
	}

	if (data.status == 'error')
		Divvy.View.onSendError(data.error, 0);

	Divvy.View.refresh();
	Divvy.Buckets.refresh();
	Divvy.View.win.setToolbar(null, {animated: true});
};

Divvy.View.onSendError = function (status, httpStatus)
{
	alert('Image upload failed, please try again. ('+status+')');
	Divvy.View.win.setToolbar(null, {animated: true});
};

Divvy.View.onSendStream = function(progress)
{
	Divvy.View.uploadIndicator.value = progress;
};

Divvy.View.generateImageThumbnail = function(num,id,image)
{
	this.numOfImages++;
	var x = num % 4;
	var y = Math.floor(num / 4);

	var padding = 4;
	var dimension = 75;

	var thumbnail = Ti.UI.createImageView({
		width: dimension, height: dimension,
		top: ((dimension+padding)*y)+padding, left: ((dimension+padding)*x)+padding,
		//TODO: set default image
		hires: true,
		borderWidth: 1,
		borderColor: '#ccc',
		imageNumber: num + 1,
		imageFile: image,
		imageId: id,
		backgroundColor: '#000000',
	});

	Network.cache.run(
		image,
		168, //1 week
		Divvy.View.onImageCacheSuccess,
		Divvy.View.onImageCacheError,
		thumbnail
	);

	return thumbnail;
};

Divvy.View.onImageCacheSuccess = function(data, date, status, user, xhr)
{
	user.image = data;
};

Divvy.View.onImageCacheError = function(status, httpStatus)
{
	//do nothing
};<|endoftext|>


<commit_before>import process, { exit } from 'node:process'
import { log } from '@serverless/utils/log.js'
import chalk from 'chalk'
import {
  commandOptions,
  CUSTOM_OPTION,
  defaultOptions,
  SERVER_SHUTDOWN_TIMEOUT,
} from './config/index.js'

export default class ServerlessOffline {
  #cliOptions = null

  #http = null

  #lambda = null

  #options = null

  #schedule = null

  #serverless = null

  #webSocket = null

  constructor(serverless, cliOptions) {
    this.#cliOptions = cliOptions
    this.#serverless = serverless

    this.commands = {
      offline: {
        // add start nested options
        commands: {
          functionsUpdated: {
            lifecycleEvents: ['cleanup'],
            type: 'entrypoint',
          },
          start: {
            lifecycleEvents: ['init', 'ready', 'end'],
            options: commandOptions,
            usage:
              'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
          },
        },
        lifecycleEvents: ['start'],
        options: commandOptions,
        usage: 'Simulates API Gateway to call your lambda functions offline.',
      },
    }

    this.hooks = {
      'offline:functionsUpdated:cleanup': this.#cleanupFunctions.bind(this),
      'offline:start': this.#startWithExplicitEnd.bind(this),
      'offline:start:end': this.end.bind(this),
      'offline:start:init': this.start.bind(this),
      'offline:start:ready': this.#ready.bind(this),
    }
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this.#mergeOptions()

    const { httpEvents, lambdas, scheduleEvents, webSocketEvents } =
      this.#getEvents()

    // if (lambdas.length > 0) {
    await this.#createLambda(lambdas)
    // }

    const eventModules = []

    if (httpEvents.length > 0) {
      eventModules.push(this.#createHttp(httpEvents))
    }

    if (!this.#options.disableScheduledEvents && scheduleEvents.length > 0) {
      eventModules.push(this.#createSchedule(scheduleEvents))
    }

    if (webSocketEvents.length > 0) {
      eventModules.push(this.#createWebSocket(webSocketEvents))
    }

    await Promise.all(eventModules)
  }

  async #ready() {
    await this.#listenForTermination()
  }

  async end(skipExit) {
    log.info('Halting offline server')

    const eventModules = []

    if (this.#lambda) {
      eventModules.push(this.#lambda.cleanup())
      eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    if (this.#http) {
      eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    // if (this.#schedule) {
    //   eventModules.push(this.#schedule.stop())
    // }

    if (this.#webSocket) {
      eventModules.push(this.#webSocket.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    await Promise.all(eventModules)

    if (!skipExit) {
      exit(0)
    }
  }

  async #cleanupFunctions() {
    if (this.#lambda) {
      log.debug('Forcing cleanup of Lambda functions')
      await this.#lambda.cleanup()
    }
  }

  /**
   * Entry point for the plugin (serverless offline) when running 'serverless offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running serverless offline that can be expected, but docs say that
   * 'serverless offline start' will provide the init and end hooks for other plugins to consume
   * */
  async #startWithExplicitEnd() {
    await this.start()
    await this.#ready()
    this.end()
  }

  async #listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'))
    })

    log.info(`Got ${command} signal. Offline Halting...`)
  }

  async #createLambda(lambdas, skipStart) {
    const { default: Lambda } = await import('./lambda/index.js')

    this.#lambda = new Lambda(this.#serverless, this.#options)

    this.#lambda.create(lambdas)

    if (!skipStart) {
      await this.#lambda.start()
    }
  }

  async #createHttp(events, skipStart) {
    const { default: Http } = await import('./events/http/index.js')

    this.#http = new Http(this.#serverless, this.#options, this.#lambda)

    await this.#http.createServer()

    this.#http.create(events)

    // HTTP Proxy defined in Resource
    this.#http.createResourceRoutes()

    // Not found handling
    // we have to create the 404 routes last, otherwise we could have
    // collisions with catch all routes, e.g. any (proxy+}
    this.#http.create404Route()

    if (!skipStart) {
      await this.#http.start()
    }
  }

  async #createSchedule(events) {
    const { default: Schedule } = await import('./events/schedule/index.js')

    this.#schedule = new Schedule(
      this.#lambda,
      this.#serverless.service.provider.region,
    )

    this.#schedule.create(events)
  }

  async #createWebSocket(events) {
    const { default: WebSocket } = await import('./events/websocket/index.js')

    this.#webSocket = new WebSocket(
      this.#serverless,
      this.#options,
      this.#lambda,
    )

    this.#webSocket.create(events)

    return this.#webSocket.start()
  }

  #mergeOptions() {
    const {
      service: { custom = {}, provider },
    } = this.#serverless

    const customOptions = custom[CUSTOM_OPTION]

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.#options = {
      ...defaultOptions,
      ...customOptions,
      ...this.#cliOptions,
    }

    // Parse CORS options
    this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsAllowOrigin = this.#options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsExposedHeaders = this.#options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',')

    this.#options.corsConfig = {
      credentials: !this.#options.corsDisallowCredentials,
      exposedHeaders: this.#options.corsExposedHeaders,
      headers: this.#options.corsAllowHeaders,
      origin: this.#options.corsAllowOrigin,
    }

    log.notice()
    log.notice(
      `Starting Offline at stage ${provider.stage} ${chalk.gray(
        `(${provider.region})`,
      )}`,
    )
    log.notice()
    log.debug('options:', this.#options)
  }

  #getEvents() {
    const { service } = this.#serverless

    const httpEvents = []
    const lambdas = []
    const scheduleEvents = []
    const webSocketEvents = []

    const functionKeys = service.getAllFunctions()

    let hasPrivateHttpEvent = false

    functionKeys.forEach((functionKey) => {
      const functionDefinition = service.getFunction(functionKey)

      lambdas.push({ functionDefinition, functionKey })

      const events = service.getAllEventsInFunction(functionKey) || []

      events.forEach((event) => {
        const { http, httpApi, schedule, websocket } = event

        if ((http || httpApi) && functionDefinition.handler) {
          const httpEvent = {
            functionKey,
            handler: functionDefinition.handler,
            http: http || httpApi,
          }

          if (httpApi) {
            // Ensure definitions for 'httpApi' events are objects so that they can be marked
            // with an 'isHttpApi' property (they are handled differently to 'http' events)
            if (typeof httpEvent.http === 'string') {
              httpEvent.http = {
                routeKey: httpEvent.http === '*' ? '$default' : httpEvent.http,
              }
            } else if (typeof httpEvent.http === 'object') {
              if (!httpEvent.http.method) {
                log.warning(
                  `Event definition is missing a method for function "${functionKey}"`,
                )
                httpEvent.http.method = ''
              }
              if (
                httpEvent.http.method === '*' &&
                httpEvent.http.path === '*'
              ) {
                httpEvent.http.routeKey = '$default'
              } else {
                const resolvedMethod =
                  httpEvent.http.method === '*'
                    ? 'ANY'
                    : httpEvent.http.method.toUpperCase()
                httpEvent.http.routeKey = `${resolvedMethod} ${httpEvent.http.path}`
              }
              // Clear these properties to avoid confusion (they will be derived from the routeKey
              // when needed later)
              delete httpEvent.http.method
              delete httpEvent.http.path
            } else {
              log.warning(
                `Event definition must be a string or object but received ${typeof httpEvent.http} for function "${functionKey}"`,
              )
              httpEvent.http.routeKey = ''
            }

            httpEvent.http.isHttpApi = true
            if (
              functionDefinition.httpApi &&
              functionDefinition.httpApi.payload
            ) {
              httpEvent.http.payload = functionDefinition.httpApi.payload
            } else {
              httpEvent.http.payload =
                service.provider.httpApi && service.provider.httpApi.payload
                  ? service.provider.httpApi.payload
                  : '2.0'
            }
          }

          if (http && http.private) {
            hasPrivateHttpEvent = true
          }

          httpEvents.push(httpEvent)
        }

        if (schedule) {
          scheduleEvents.push({
            functionKey,
            schedule,
          })
        }

        if (websocket) {
          webSocketEvents.push({
            functionKey,
            websocket,
          })
        }
      })
    })

    // for simple API Key authentication model
    if (hasPrivateHttpEvent) {
      log.notice(`Key with token: ${this.#options.apiKey}`)

      if (this.#options.noAuth) {
        log.notice(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        )
      } else {
        log.notice('Remember to use x-api-key on the request headers')
      }
    }

    return {
      httpEvents,
      lambdas,
      scheduleEvents,
      webSocketEvents,
    }
  }

  // TODO FIXME
  // TEMP quick fix to expose for testing, look for better solution
  internals() {
    return {
      createHttp: (events, skipStart) => {
        return this.#createHttp(events, skipStart)
      },

      createLambda: (lambdas, skipStart) => {
        return this.#createLambda(lambdas, skipStart)
      },

      getApiGatewayServer: () => {
        return this.#http.getServer()
      },

      getEvents: () => {
        return this.#getEvents()
      },

      mergeOptions: () => {
        this.#mergeOptions()
      },
    }
  }
}
<commit_msg>refactor: declare fields, remove from constructor<commit_after>import process, { exit } from 'node:process'
import { log } from '@serverless/utils/log.js'
import chalk from 'chalk'
import {
  commandOptions,
  CUSTOM_OPTION,
  defaultOptions,
  SERVER_SHUTDOWN_TIMEOUT,
} from './config/index.js'

export default class ServerlessOffline {
  #cliOptions = null

  #http = null

  #lambda = null

  #options = null

  #schedule = null

  #serverless = null

  #webSocket = null

  commands = {
    offline: {
      // add start nested options
      commands: {
        functionsUpdated: {
          lifecycleEvents: ['cleanup'],
          type: 'entrypoint',
        },
        start: {
          lifecycleEvents: ['init', 'ready', 'end'],
          options: commandOptions,
          usage:
            'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
        },
      },
      lifecycleEvents: ['start'],
      options: commandOptions,
      usage: 'Simulates API Gateway to call your lambda functions offline.',
    },
  }

  hooks = {
    'offline:functionsUpdated:cleanup': this.#cleanupFunctions.bind(this),
    'offline:start': this.#startWithExplicitEnd.bind(this),
    'offline:start:end': this.end.bind(this),
    'offline:start:init': this.start.bind(this),
    'offline:start:ready': this.#ready.bind(this),
  }

  constructor(serverless, cliOptions) {
    this.#cliOptions = cliOptions
    this.#serverless = serverless
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this.#mergeOptions()

    const { httpEvents, lambdas, scheduleEvents, webSocketEvents } =
      this.#getEvents()

    // if (lambdas.length > 0) {
    await this.#createLambda(lambdas)
    // }

    const eventModules = []

    if (httpEvents.length > 0) {
      eventModules.push(this.#createHttp(httpEvents))
    }

    if (!this.#options.disableScheduledEvents && scheduleEvents.length > 0) {
      eventModules.push(this.#createSchedule(scheduleEvents))
    }

    if (webSocketEvents.length > 0) {
      eventModules.push(this.#createWebSocket(webSocketEvents))
    }

    await Promise.all(eventModules)
  }

  async #ready() {
    await this.#listenForTermination()
  }

  async end(skipExit) {
    log.info('Halting offline server')

    const eventModules = []

    if (this.#lambda) {
      eventModules.push(this.#lambda.cleanup())
      eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    if (this.#http) {
      eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    // if (this.#schedule) {
    //   eventModules.push(this.#schedule.stop())
    // }

    if (this.#webSocket) {
      eventModules.push(this.#webSocket.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    await Promise.all(eventModules)

    if (!skipExit) {
      exit(0)
    }
  }

  async #cleanupFunctions() {
    if (this.#lambda) {
      log.debug('Forcing cleanup of Lambda functions')
      await this.#lambda.cleanup()
    }
  }

  /**
   * Entry point for the plugin (serverless offline) when running 'serverless offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running serverless offline that can be expected, but docs say that
   * 'serverless offline start' will provide the init and end hooks for other plugins to consume
   * */
  async #startWithExplicitEnd() {
    await this.start()
    await this.#ready()
    this.end()
  }

  async #listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'))
    })

    log.info(`Got ${command} signal. Offline Halting...`)
  }

  async #createLambda(lambdas, skipStart) {
    const { default: Lambda } = await import('./lambda/index.js')

    this.#lambda = new Lambda(this.#serverless, this.#options)

    this.#lambda.create(lambdas)

    if (!skipStart) {
      await this.#lambda.start()
    }
  }

  async #createHttp(events, skipStart) {
    const { default: Http } = await import('./events/http/index.js')

    this.#http = new Http(this.#serverless, this.#options, this.#lambda)

    await this.#http.createServer()

    this.#http.create(events)

    // HTTP Proxy defined in Resource
    this.#http.createResourceRoutes()

    // Not found handling
    // we have to create the 404 routes last, otherwise we could have
    // collisions with catch all routes, e.g. any (proxy+}
    this.#http.create404Route()

    if (!skipStart) {
      await this.#http.start()
    }
  }

  async #createSchedule(events) {
    const { default: Schedule } = await import('./events/schedule/index.js')

    this.#schedule = new Schedule(
      this.#lambda,
      this.#serverless.service.provider.region,
    )

    this.#schedule.create(events)
  }

  async #createWebSocket(events) {
    const { default: WebSocket } = await import('./events/websocket/index.js')

    this.#webSocket = new WebSocket(
      this.#serverless,
      this.#options,
      this.#lambda,
    )

    this.#webSocket.create(events)

    return this.#webSocket.start()
  }

  #mergeOptions() {
    const {
      service: { custom = {}, provider },
    } = this.#serverless

    const customOptions = custom[CUSTOM_OPTION]

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.#options = {
      ...defaultOptions,
      ...customOptions,
      ...this.#cliOptions,
    }

    // Parse CORS options
    this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsAllowOrigin = this.#options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsExposedHeaders = this.#options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',')

    this.#options.corsConfig = {
      credentials: !this.#options.corsDisallowCredentials,
      exposedHeaders: this.#options.corsExposedHeaders,
      headers: this.#options.corsAllowHeaders,
      origin: this.#options.corsAllowOrigin,
    }

    log.notice()
    log.notice(
      `Starting Offline at stage ${provider.stage} ${chalk.gray(
        `(${provider.region})`,
      )}`,
    )
    log.notice()
    log.debug('options:', this.#options)
  }

  #getEvents() {
    const { service } = this.#serverless

    const httpEvents = []
    const lambdas = []
    const scheduleEvents = []
    const webSocketEvents = []

    const functionKeys = service.getAllFunctions()

    let hasPrivateHttpEvent = false

    functionKeys.forEach((functionKey) => {
      const functionDefinition = service.getFunction(functionKey)

      lambdas.push({ functionDefinition, functionKey })

      const events = service.getAllEventsInFunction(functionKey) || []

      events.forEach((event) => {
        const { http, httpApi, schedule, websocket } = event

        if ((http || httpApi) && functionDefinition.handler) {
          const httpEvent = {
            functionKey,
            handler: functionDefinition.handler,
            http: http || httpApi,
          }

          if (httpApi) {
            // Ensure definitions for 'httpApi' events are objects so that they can be marked
            // with an 'isHttpApi' property (they are handled differently to 'http' events)
            if (typeof httpEvent.http === 'string') {
              httpEvent.http = {
                routeKey: httpEvent.http === '*' ? '$default' : httpEvent.http,
              }
            } else if (typeof httpEvent.http === 'object') {
              if (!httpEvent.http.method) {
                log.warning(
                  `Event definition is missing a method for function "${functionKey}"`,
                )
                httpEvent.http.method = ''
              }
              if (
                httpEvent.http.method === '*' &&
                httpEvent.http.path === '*'
              ) {
                httpEvent.http.routeKey = '$default'
              } else {
                const resolvedMethod =
                  httpEvent.http.method === '*'
                    ? 'ANY'
                    : httpEvent.http.method.toUpperCase()
                httpEvent.http.routeKey = `${resolvedMethod} ${httpEvent.http.path}`
              }
              // Clear these properties to avoid confusion (they will be derived from the routeKey
              // when needed later)
              delete httpEvent.http.method
              delete httpEvent.http.path
            } else {
              log.warning(
                `Event definition must be a string or object but received ${typeof httpEvent.http} for function "${functionKey}"`,
              )
              httpEvent.http.routeKey = ''
            }

            httpEvent.http.isHttpApi = true
            if (
              functionDefinition.httpApi &&
              functionDefinition.httpApi.payload
            ) {
              httpEvent.http.payload = functionDefinition.httpApi.payload
            } else {
              httpEvent.http.payload =
                service.provider.httpApi && service.provider.httpApi.payload
                  ? service.provider.httpApi.payload
                  : '2.0'
            }
          }

          if (http && http.private) {
            hasPrivateHttpEvent = true
          }

          httpEvents.push(httpEvent)
        }

        if (schedule) {
          scheduleEvents.push({
            functionKey,
            schedule,
          })
        }

        if (websocket) {
          webSocketEvents.push({
            functionKey,
            websocket,
          })
        }
      })
    })

    // for simple API Key authentication model
    if (hasPrivateHttpEvent) {
      log.notice(`Key with token: ${this.#options.apiKey}`)

      if (this.#options.noAuth) {
        log.notice(
          'Authorizers are turned off. You do not need to use x-api-key header.',
        )
      } else {
        log.notice('Remember to use x-api-key on the request headers')
      }
    }

    return {
      httpEvents,
      lambdas,
      scheduleEvents,
      webSocketEvents,
    }
  }

  // TODO FIXME
  // TEMP quick fix to expose for testing, look for better solution
  internals() {
    return {
      createHttp: (events, skipStart) => {
        return this.#createHttp(events, skipStart)
      },

      createLambda: (lambdas, skipStart) => {
        return this.#createLambda(lambdas, skipStart)
      },

      getApiGatewayServer: () => {
        return this.#http.getServer()
      },

      getEvents: () => {
        return this.#getEvents()
      },

      mergeOptions: () => {
        this.#mergeOptions()
      },
    }
  }
}
<|endoftext|>


<commit_before>/*global module:true, require:false*/

var bane = require("bane");
var websocket = require("../lib/websocket/");
var when = require("when");

function Mopidy(settings) {
    if (!(this instanceof Mopidy)) {
        return new Mopidy(settings);
    }

    this._settings = this._configure(settings || {});
    this._console = this._getConsole();

    this._backoffDelay = this._settings.backoffDelayMin;
    this._pendingRequests = {};
    this._webSocket = null;

    bane.createEventEmitter(this);
    this._delegateEvents();

    if (this._settings.autoConnect) {
        this.connect();
    }
}

Mopidy.ConnectionError = function (message) {
    this.name = "ConnectionError";
    this.message = message;
};
Mopidy.ConnectionError.prototype = new Error();
Mopidy.ConnectionError.prototype.constructor = Mopidy.ConnectionError;

Mopidy.ServerError = function (message) {
    this.name = "ServerError";
    this.message = message;
};
Mopidy.ServerError.prototype = new Error();
Mopidy.ServerError.prototype.constructor = Mopidy.ServerError;

Mopidy.WebSocket = websocket.Client;

Mopidy.prototype._configure = function (settings) {
    var currentHost = (typeof document !== "undefined" &&
        document.location.host) || "localhost";
    settings.webSocketUrl = settings.webSocketUrl ||
        "ws://" + currentHost + "/mopidy/ws";

    if (settings.autoConnect !== false) {
        settings.autoConnect = true;
    }

    settings.backoffDelayMin = settings.backoffDelayMin || 1000;
    settings.backoffDelayMax = settings.backoffDelayMax || 64000;

    return settings;
};

Mopidy.prototype._getConsole = function () {
    var console = typeof console !== "undefined" && console || {};

    console.log = console.log || function () {};
    console.warn = console.warn || function () {};
    console.error = console.error || function () {};

    return console;
};

Mopidy.prototype._delegateEvents = function () {
    // Remove existing event handlers
    this.off("websocket:close");
    this.off("websocket:error");
    this.off("websocket:incomingMessage");
    this.off("websocket:open");
    this.off("state:offline");

    // Register basic set of event handlers
    this.on("websocket:close", this._cleanup);
    this.on("websocket:error", this._handleWebSocketError);
    this.on("websocket:incomingMessage", this._handleMessage);
    this.on("websocket:open", this._resetBackoffDelay);
    this.on("websocket:open", this._getApiSpec);
    this.on("state:offline", this._reconnect);
};

Mopidy.prototype.connect = function () {
    if (this._webSocket) {
        if (this._webSocket.readyState === Mopidy.WebSocket.OPEN) {
            return;
        } else {
            this._webSocket.close();
        }
    }

    this._webSocket = this._settings.webSocket ||
        new Mopidy.WebSocket(this._settings.webSocketUrl);

    this._webSocket.onclose = function (close) {
        this.emit("websocket:close", close);
    }.bind(this);

    this._webSocket.onerror = function (error) {
        this.emit("websocket:error", error);
    }.bind(this);

    this._webSocket.onopen = function () {
        this.emit("websocket:open");
    }.bind(this);

    this._webSocket.onmessage = function (message) {
        this.emit("websocket:incomingMessage", message);
    }.bind(this);
};

Mopidy.prototype._cleanup = function (closeEvent) {
    Object.keys(this._pendingRequests).forEach(function (requestId) {
        var resolver = this._pendingRequests[requestId];
        delete this._pendingRequests[requestId];
        var error = new Mopidy.ConnectionError("WebSocket closed");
        error.closeEvent = closeEvent;
        resolver.reject(error);
    }.bind(this));

    this.emit("state:offline");
};

Mopidy.prototype._reconnect = function () {
    this.emit("reconnectionPending", {
        timeToAttempt: this._backoffDelay
    });

    setTimeout(function () {
        this.emit("reconnecting");
        this.connect();
    }.bind(this), this._backoffDelay);

    this._backoffDelay = this._backoffDelay * 2;
    if (this._backoffDelay > this._settings.backoffDelayMax) {
        this._backoffDelay = this._settings.backoffDelayMax;
    }
};

Mopidy.prototype._resetBackoffDelay = function () {
    this._backoffDelay = this._settings.backoffDelayMin;
};

Mopidy.prototype.close = function () {
    this.off("state:offline", this._reconnect);
    this._webSocket.close();
};

Mopidy.prototype._handleWebSocketError = function (error) {
    this._console.warn("WebSocket error:", error.stack || error);
};

Mopidy.prototype._send = function (message) {
    switch (this._webSocket.readyState) {
    case Mopidy.WebSocket.CONNECTING:
        return when.reject(
            new Mopidy.ConnectionError("WebSocket is still connecting"));
    case Mopidy.WebSocket.CLOSING:
        return when.reject(
            new Mopidy.ConnectionError("WebSocket is closing"));
    case Mopidy.WebSocket.CLOSED:
        return when.reject(
            new Mopidy.ConnectionError("WebSocket is closed"));
    default:
        var deferred = when.defer();
        message.jsonrpc = "2.0";
        message.id = this._nextRequestId();
        this._pendingRequests[message.id] = deferred.resolver;
        this._webSocket.send(JSON.stringify(message));
        this.emit("websocket:outgoingMessage", message);
        return deferred.promise;
    }
};

Mopidy.prototype._nextRequestId = (function () {
    var lastUsed = -1;
    return function () {
        lastUsed += 1;
        return lastUsed;
    };
}());

Mopidy.prototype._handleMessage = function (message) {
    try {
        var data = JSON.parse(message.data);
        if (data.hasOwnProperty("id")) {
            this._handleResponse(data);
        } else if (data.hasOwnProperty("event")) {
            this._handleEvent(data);
        } else {
            this._console.warn(
                "Unknown message type received. Message was: " +
                message.data);
        }
    } catch (error) {
        if (error instanceof SyntaxError) {
            this._console.warn(
                "WebSocket message parsing failed. Message was: " +
                message.data);
        } else {
            throw error;
        }
    }
};

Mopidy.prototype._handleResponse = function (responseMessage) {
    if (!this._pendingRequests.hasOwnProperty(responseMessage.id)) {
        this._console.warn(
            "Unexpected response received. Message was:", responseMessage);
        return;
    }

    var error;
    var resolver = this._pendingRequests[responseMessage.id];
    delete this._pendingRequests[responseMessage.id];

    if (responseMessage.hasOwnProperty("result")) {
        resolver.resolve(responseMessage.result);
    } else if (responseMessage.hasOwnProperty("error")) {
        error = new Mopidy.ServerError(responseMessage.error.message);
        error.code = responseMessage.error.code;
        error.data = responseMessage.error.data;
        resolver.reject(error);
        this._console.warn("Server returned error:", responseMessage.error);
    } else {
        error = new Error("Response without 'result' or 'error' received");
        error.data = {response: responseMessage};
        resolver.reject(error);
        this._console.warn(
            "Response without 'result' or 'error' received. Message was:",
            responseMessage);
    }
};

Mopidy.prototype._handleEvent = function (eventMessage) {
    var type = eventMessage.event;
    var data = eventMessage;
    delete data.event;

    this.emit("event:" + this._snakeToCamel(type), data);
};

Mopidy.prototype._getApiSpec = function () {
    return this._send({method: "core.describe"})
        .then(this._createApi.bind(this), this._handleWebSocketError)
        .then(null, this._handleWebSocketError);
};

Mopidy.prototype._createApi = function (methods) {
    var caller = function (method) {
        return function () {
            var params = Array.prototype.slice.call(arguments);
            return this._send({
                method: method,
                params: params
            });
        }.bind(this);
    }.bind(this);

    var getPath = function (fullName) {
        var path = fullName.split(".");
        if (path.length >= 1 && path[0] === "core") {
            path = path.slice(1);
        }
        return path;
    };

    var createObjects = function (objPath) {
        var parentObj = this;
        objPath.forEach(function (objName) {
            objName = this._snakeToCamel(objName);
            parentObj[objName] = parentObj[objName] || {};
            parentObj = parentObj[objName];
        }.bind(this));
        return parentObj;
    }.bind(this);

    var createMethod = function (fullMethodName) {
        var methodPath = getPath(fullMethodName);
        var methodName = this._snakeToCamel(methodPath.slice(-1)[0]);
        var object = createObjects(methodPath.slice(0, -1));
        object[methodName] = caller(fullMethodName);
        object[methodName].description = methods[fullMethodName].description;
        object[methodName].params = methods[fullMethodName].params;
    }.bind(this);

    Object.keys(methods).forEach(createMethod);
    this.emit("state:online");
};

Mopidy.prototype._snakeToCamel = function (name) {
    return name.replace(/(_[a-z])/g, function (match) {
        return match.toUpperCase().replace("_", "");
    });
};

module.exports = Mopidy;
<commit_msg>js: Use .catch() to simplify error handling<commit_after>/*global module:true, require:false*/

var bane = require("bane");
var websocket = require("../lib/websocket/");
var when = require("when");

function Mopidy(settings) {
    if (!(this instanceof Mopidy)) {
        return new Mopidy(settings);
    }

    this._settings = this._configure(settings || {});
    this._console = this._getConsole();

    this._backoffDelay = this._settings.backoffDelayMin;
    this._pendingRequests = {};
    this._webSocket = null;

    bane.createEventEmitter(this);
    this._delegateEvents();

    if (this._settings.autoConnect) {
        this.connect();
    }
}

Mopidy.ConnectionError = function (message) {
    this.name = "ConnectionError";
    this.message = message;
};
Mopidy.ConnectionError.prototype = new Error();
Mopidy.ConnectionError.prototype.constructor = Mopidy.ConnectionError;

Mopidy.ServerError = function (message) {
    this.name = "ServerError";
    this.message = message;
};
Mopidy.ServerError.prototype = new Error();
Mopidy.ServerError.prototype.constructor = Mopidy.ServerError;

Mopidy.WebSocket = websocket.Client;

Mopidy.prototype._configure = function (settings) {
    var currentHost = (typeof document !== "undefined" &&
        document.location.host) || "localhost";
    settings.webSocketUrl = settings.webSocketUrl ||
        "ws://" + currentHost + "/mopidy/ws";

    if (settings.autoConnect !== false) {
        settings.autoConnect = true;
    }

    settings.backoffDelayMin = settings.backoffDelayMin || 1000;
    settings.backoffDelayMax = settings.backoffDelayMax || 64000;

    return settings;
};

Mopidy.prototype._getConsole = function () {
    var console = typeof console !== "undefined" && console || {};

    console.log = console.log || function () {};
    console.warn = console.warn || function () {};
    console.error = console.error || function () {};

    return console;
};

Mopidy.prototype._delegateEvents = function () {
    // Remove existing event handlers
    this.off("websocket:close");
    this.off("websocket:error");
    this.off("websocket:incomingMessage");
    this.off("websocket:open");
    this.off("state:offline");

    // Register basic set of event handlers
    this.on("websocket:close", this._cleanup);
    this.on("websocket:error", this._handleWebSocketError);
    this.on("websocket:incomingMessage", this._handleMessage);
    this.on("websocket:open", this._resetBackoffDelay);
    this.on("websocket:open", this._getApiSpec);
    this.on("state:offline", this._reconnect);
};

Mopidy.prototype.connect = function () {
    if (this._webSocket) {
        if (this._webSocket.readyState === Mopidy.WebSocket.OPEN) {
            return;
        } else {
            this._webSocket.close();
        }
    }

    this._webSocket = this._settings.webSocket ||
        new Mopidy.WebSocket(this._settings.webSocketUrl);

    this._webSocket.onclose = function (close) {
        this.emit("websocket:close", close);
    }.bind(this);

    this._webSocket.onerror = function (error) {
        this.emit("websocket:error", error);
    }.bind(this);

    this._webSocket.onopen = function () {
        this.emit("websocket:open");
    }.bind(this);

    this._webSocket.onmessage = function (message) {
        this.emit("websocket:incomingMessage", message);
    }.bind(this);
};

Mopidy.prototype._cleanup = function (closeEvent) {
    Object.keys(this._pendingRequests).forEach(function (requestId) {
        var resolver = this._pendingRequests[requestId];
        delete this._pendingRequests[requestId];
        var error = new Mopidy.ConnectionError("WebSocket closed");
        error.closeEvent = closeEvent;
        resolver.reject(error);
    }.bind(this));

    this.emit("state:offline");
};

Mopidy.prototype._reconnect = function () {
    this.emit("reconnectionPending", {
        timeToAttempt: this._backoffDelay
    });

    setTimeout(function () {
        this.emit("reconnecting");
        this.connect();
    }.bind(this), this._backoffDelay);

    this._backoffDelay = this._backoffDelay * 2;
    if (this._backoffDelay > this._settings.backoffDelayMax) {
        this._backoffDelay = this._settings.backoffDelayMax;
    }
};

Mopidy.prototype._resetBackoffDelay = function () {
    this._backoffDelay = this._settings.backoffDelayMin;
};

Mopidy.prototype.close = function () {
    this.off("state:offline", this._reconnect);
    this._webSocket.close();
};

Mopidy.prototype._handleWebSocketError = function (error) {
    this._console.warn("WebSocket error:", error.stack || error);
};

Mopidy.prototype._send = function (message) {
    switch (this._webSocket.readyState) {
    case Mopidy.WebSocket.CONNECTING:
        return when.reject(
            new Mopidy.ConnectionError("WebSocket is still connecting"));
    case Mopidy.WebSocket.CLOSING:
        return when.reject(
            new Mopidy.ConnectionError("WebSocket is closing"));
    case Mopidy.WebSocket.CLOSED:
        return when.reject(
            new Mopidy.ConnectionError("WebSocket is closed"));
    default:
        var deferred = when.defer();
        message.jsonrpc = "2.0";
        message.id = this._nextRequestId();
        this._pendingRequests[message.id] = deferred.resolver;
        this._webSocket.send(JSON.stringify(message));
        this.emit("websocket:outgoingMessage", message);
        return deferred.promise;
    }
};

Mopidy.prototype._nextRequestId = (function () {
    var lastUsed = -1;
    return function () {
        lastUsed += 1;
        return lastUsed;
    };
}());

Mopidy.prototype._handleMessage = function (message) {
    try {
        var data = JSON.parse(message.data);
        if (data.hasOwnProperty("id")) {
            this._handleResponse(data);
        } else if (data.hasOwnProperty("event")) {
            this._handleEvent(data);
        } else {
            this._console.warn(
                "Unknown message type received. Message was: " +
                message.data);
        }
    } catch (error) {
        if (error instanceof SyntaxError) {
            this._console.warn(
                "WebSocket message parsing failed. Message was: " +
                message.data);
        } else {
            throw error;
        }
    }
};

Mopidy.prototype._handleResponse = function (responseMessage) {
    if (!this._pendingRequests.hasOwnProperty(responseMessage.id)) {
        this._console.warn(
            "Unexpected response received. Message was:", responseMessage);
        return;
    }

    var error;
    var resolver = this._pendingRequests[responseMessage.id];
    delete this._pendingRequests[responseMessage.id];

    if (responseMessage.hasOwnProperty("result")) {
        resolver.resolve(responseMessage.result);
    } else if (responseMessage.hasOwnProperty("error")) {
        error = new Mopidy.ServerError(responseMessage.error.message);
        error.code = responseMessage.error.code;
        error.data = responseMessage.error.data;
        resolver.reject(error);
        this._console.warn("Server returned error:", responseMessage.error);
    } else {
        error = new Error("Response without 'result' or 'error' received");
        error.data = {response: responseMessage};
        resolver.reject(error);
        this._console.warn(
            "Response without 'result' or 'error' received. Message was:",
            responseMessage);
    }
};

Mopidy.prototype._handleEvent = function (eventMessage) {
    var type = eventMessage.event;
    var data = eventMessage;
    delete data.event;

    this.emit("event:" + this._snakeToCamel(type), data);
};

Mopidy.prototype._getApiSpec = function () {
    return this._send({method: "core.describe"})
        .then(this._createApi.bind(this))
        .catch(this._handleWebSocketError);
};

Mopidy.prototype._createApi = function (methods) {
    var caller = function (method) {
        return function () {
            var params = Array.prototype.slice.call(arguments);
            return this._send({
                method: method,
                params: params
            });
        }.bind(this);
    }.bind(this);

    var getPath = function (fullName) {
        var path = fullName.split(".");
        if (path.length >= 1 && path[0] === "core") {
            path = path.slice(1);
        }
        return path;
    };

    var createObjects = function (objPath) {
        var parentObj = this;
        objPath.forEach(function (objName) {
            objName = this._snakeToCamel(objName);
            parentObj[objName] = parentObj[objName] || {};
            parentObj = parentObj[objName];
        }.bind(this));
        return parentObj;
    }.bind(this);

    var createMethod = function (fullMethodName) {
        var methodPath = getPath(fullMethodName);
        var methodName = this._snakeToCamel(methodPath.slice(-1)[0]);
        var object = createObjects(methodPath.slice(0, -1));
        object[methodName] = caller(fullMethodName);
        object[methodName].description = methods[fullMethodName].description;
        object[methodName].params = methods[fullMethodName].params;
    }.bind(this);

    Object.keys(methods).forEach(createMethod);
    this.emit("state:online");
};

Mopidy.prototype._snakeToCamel = function (name) {
    return name.replace(/(_[a-z])/g, function (match) {
        return match.toUpperCase().replace("_", "");
    });
};

module.exports = Mopidy;
<|endoftext|>


<commit_before>'use strict';

function convertPmMessage(config, conns) {
  if (!config.pmMessage.convert) {
    return Promise.resolve();
  } else {
    return new Promise((resolve, reject) => {

      function cleanUpMongo() {
        if (!config.pmMessage.cleanup) {
          return Promise.resolve();
        }
        console.log('[PmMessage][Mongo] Deleting all pm data.');
        return new Promise((resolve, reject) => {
          conns.mongo.collection('mail').deleteMany({}, (err, res) => {
            if (err) {
              reject(err);
            }
            else {
              resolve();
            }
          })
        });
      }

      function fetchPmList(tail) {
        console.log(`[PmMessage][MySQL] Fetching messages from table: cbs_ucenter_pm_messages_${tail}`);
        return new Promise((resolve, reject) => {
          conns.mysql.query(
            `SELECT cbs_ucenter_pm_messages_${tail}.authorid AS sender ` +
            `     , cbs_ucenter_pm_members.uid AS receiver ` +
            `     , cbs_ucenter_pm_messages_${tail}.message ` +
            `     , cbs_ucenter_pm_messages_${tail}.dateline ` +
            `FROM cncalc.cbs_ucenter_pm_messages_${tail} ` +
            `INNER JOIN cncalc.cbs_ucenter_pm_members ` +
            `ON cbs_ucenter_pm_messages_${tail}.plid = cbs_ucenter_pm_members.plid`
          , (err, data) => {
            if (err) {
              reject(err);
            } else {
              resolve(data);
            }
          })
        })
      }

      function prepareUserData() {
        console.log('[PmMessage][Mongo] Fetching user data.');
        return new Promise((resolve, reject) => {
          conns.mongo.collection('common_member').find({}).toArray((err, data) => {
            let userMap = {};
            data.forEach(item => userMap[item.uid] = item._id);
            resolve(userMap);
          })
        });
      }

      cleanUpMongo().then(() => {
        Promise.all([prepareUserData(), ...Array(10).fill(0).map((el, off) => off).map(fetchPmList)])
          .then(values => {
            let uidMap = values.shift();
            let pms = values.reduce((a, b) => [...a, ...b]).filter(el => el.sender !== el.receiver).map(el => {
              return {
                from: {
                  id: uidMap[el.sender],
                  read: true,
                },
                to: {
                  id: uidMap[el.receiver],
                  read: true,
                },
                date: el.dateline * 1000,
                content: el.message,
                encoding: 'discuz'
              }
            });
            console.log('[PmMessage][Mongo] Inserting data into MongoDB...');
            conns.mongo.collection('mail').insertMany(pms, (err, res) => {
              if (err) {
                reject(err);
              }
              else {
                resolve();
              }
            });
          })
          .catch(e => reject(e));
      }).catch(e => reject(e));

    });
  }
}

module.exports = convertPmMessage;
<commit_msg>Update message schema<commit_after>'use strict';

function convertPmMessage(config, conns) {
  if (!config.pmMessage.convert) {
    return Promise.resolve();
  } else {
    return new Promise((resolve, reject) => {

      function cleanUpMongo() {
        if (!config.pmMessage.cleanup) {
          return Promise.resolve();
        }
        console.log('[PmMessage][Mongo] Deleting all pm data.');
        return new Promise((resolve, reject) => {
          conns.mongo.collection('mail').deleteMany({}, (err, res) => {
            if (err) {
              reject(err);
            }
            else {
              resolve();
            }
          })
        });
      }

      function fetchPmList(tail) {
        console.log(`[PmMessage][MySQL] Fetching messages from table: cbs_ucenter_pm_messages_${tail}`);
        return new Promise((resolve, reject) => {
          conns.mysql.query(
            `SELECT cbs_ucenter_pm_messages_${tail}.authorid AS sender ` +
            `     , cbs_ucenter_pm_members.uid AS receiver ` +
            `     , cbs_ucenter_pm_messages_${tail}.message ` +
            `     , cbs_ucenter_pm_messages_${tail}.dateline ` +
            `FROM cncalc.cbs_ucenter_pm_messages_${tail} ` +
            `INNER JOIN cncalc.cbs_ucenter_pm_members ` +
            `ON cbs_ucenter_pm_messages_${tail}.plid = cbs_ucenter_pm_members.plid`
          , (err, data) => {
            if (err) {
              reject(err);
            } else {
              resolve(data);
            }
          })
        })
      }

      function prepareUserData() {
        console.log('[PmMessage][Mongo] Fetching user data.');
        return new Promise((resolve, reject) => {
          conns.mongo.collection('common_member').find({}).toArray((err, data) => {
            let userMap = {};
            data.forEach(item => userMap[item.uid] = item._id);
            resolve(userMap);
          })
        });
      }

      cleanUpMongo().then(() => {
        Promise.all([prepareUserData(), ...Array(10).fill(0).map((el, off) => off).map(fetchPmList)])
          .then(values => {
            let uidMap = values.shift();
            let sessionMap = {};
            let pms = values.reduce((a, b) => [...a, ...b]).filter(el => el.sender !== el.receiver).forEach(el => {
              let key = [el.sender, el.receiver].sort();
              key = key.map(i => i.toString()).join('');
              if (!sessionMap[key]) {
                sessionMap[key] = {
                  members: [uidMap[el.sender], uidMap[el.receiver]],
                  timeline: [],
                }
              }

              const session = sessionMap[key];
              session.timeline.push({
                content: el.message,
                from: uidMap[el.sender],
                date: el.dateline * 1000,
              });
            })

            let array = [];
            for (let key of Object.keys(sessionMap)) {
              array.push(sessionMap[key]);
            }

            console.log('[PmMessage][Mongo] Inserting data into MongoDB...');

            conns.mongo.collection('message').insertMany(array, (err, res) => {
              if (err) {
                reject(err);
              }
              else {
                resolve();
              }
            });
          })
          .catch(e => reject(e));
      }).catch(e => reject(e));

    });
  }
}

module.exports = convertPmMessage;
<|endoftext|>