From a39e54c675db18dc63db848afc02011337a7a495 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Tue, 27 Feb 2018 04:16:17 -0800 Subject: [PATCH] custom-server-actionhero (#3905) --- .gitignore | 3 +- examples/custom-server-actionhero/README.md | 99 ++++++++++++ .../actions/render.js | 15 ++ .../custom-server-actionhero/config/api.js | 97 ++++++++++++ .../custom-server-actionhero/config/errors.js | 148 ++++++++++++++++++ .../custom-server-actionhero/config/i18n.js | 36 +++++ .../custom-server-actionhero/config/logger.js | 60 +++++++ .../config/plugins.js | 27 ++++ .../custom-server-actionhero/config/redis.js | 49 ++++++ .../custom-server-actionhero/config/routes.js | 9 ++ .../config/servers/socket.js | 37 +++++ .../config/servers/web.js | 121 ++++++++++++++ .../config/servers/websocket.js | 62 ++++++++ .../custom-server-actionhero/config/tasks.js | 61 ++++++++ .../initializers/next.js | 32 ++++ .../custom-server-actionhero/locales/en.json | 34 ++++ .../custom-server-actionhero/package.json | 17 ++ examples/custom-server-actionhero/pages/a.js | 1 + examples/custom-server-actionhero/pages/b.js | 1 + .../custom-server-actionhero/pages/index.js | 9 ++ 20 files changed, 917 insertions(+), 1 deletion(-) create mode 100644 examples/custom-server-actionhero/README.md create mode 100644 examples/custom-server-actionhero/actions/render.js create mode 100644 examples/custom-server-actionhero/config/api.js create mode 100644 examples/custom-server-actionhero/config/errors.js create mode 100644 examples/custom-server-actionhero/config/i18n.js create mode 100644 examples/custom-server-actionhero/config/logger.js create mode 100644 examples/custom-server-actionhero/config/plugins.js create mode 100644 examples/custom-server-actionhero/config/redis.js create mode 100644 examples/custom-server-actionhero/config/routes.js create mode 100644 examples/custom-server-actionhero/config/servers/socket.js create mode 100644 examples/custom-server-actionhero/config/servers/web.js create mode 100644 examples/custom-server-actionhero/config/servers/websocket.js create mode 100644 examples/custom-server-actionhero/config/tasks.js create mode 100644 examples/custom-server-actionhero/initializers/next.js create mode 100644 examples/custom-server-actionhero/locales/en.json create mode 100644 examples/custom-server-actionhero/package.json create mode 100644 examples/custom-server-actionhero/pages/a.js create mode 100644 examples/custom-server-actionhero/pages/b.js create mode 100644 examples/custom-server-actionhero/pages/index.js diff --git a/.gitignore b/.gitignore index e351dbc6..96da5cb7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,9 @@ node_modules package-lock.json test/node_modules -# logs +# logs & pids *.log +pids # coverage .nyc_output diff --git a/examples/custom-server-actionhero/README.md b/examples/custom-server-actionhero/README.md new file mode 100644 index 00000000..e4c0d6e5 --- /dev/null +++ b/examples/custom-server-actionhero/README.md @@ -0,0 +1,99 @@ +# Running Next.JS and React /inside/ of ActionHero + +This server will render dynamic next.js/react pages on some routes, and normal ActionHero API requests on others. +This configuration works with both Next and ActionHero hot reloading of code. + +A more detailed example showcasing how to use fetch and web sockets to interact with your API can be found here: https://github.com/actionhero/next-in-actionhero + +## To install: +(assuming you have [node](http://nodejs.org/) and NPM installed) + +`npm install` + +## To Run: +`npm start` + + +## How does this work? + +1. Create an initializer to load next.js and create a handler that can extract the normal node `req` and `res` from the connection + +```js +// initializers/next.js + +const {Initializer, api} = require('actionhero') +const next = require('next') + +module.exports = class NextInitializer extends Initializer { + constructor () { + super() + this.name = 'next' + } + + async initialize () { + api.next = { + render: async (connection) => { + if (connection.type !== 'web') { throw new Error('Connections for NEXT apps must be of type "web"') } + const req = connection.rawConnection.req + const res = connection.rawConnection.res + return api.next.handle(req, res) + } + } + + api.next.dev = (api.env === 'development') + if (api.next.dev) { api.log('Running next in development mode...') } + + api.next.app = next({dev: api.next.dev}) + api.next.handle = api.next.app.getRequestHandler() + await api.next.app.prepare() + } + + async stop () { + await api.next.app.close() + } +} +``` + +2. Create an action which will run the above `api.next.render(connection)`. Note that we will not be relying on ActionHero to respond to the client's request in this case, and leave that up to next (via: `data.toRender = false`) + +```js +// actions/next.js + +const {Action, api} = require('actionhero') + +module.exports = class CreateChatRoom extends Action { + constructor () { + super() + this.name = 'render' + this.description = 'I render the next.js react website' + } + + async run (data) { + data.toRender = false + return api.next.render(data.connection) + } +} + +``` + +3. Tell ActionHero to use the api rather than the file server as the top-level route in `api.config.servers.web.rootEndpointType = 'api'`. This will allows "/" to listen to API requests. Also update `api.config.general.paths.public = [ path.join(__dirname, '/../static') ]`. In this configuration, the next 'static' renderer will take priority over the ActionHero 'public file' api. Note that any static assets (CSS, fonts, etc) will need to be in "./static" rather than "./public". + +Note that this is where the websocket server, if you enable it, will place the `ActionheroWebsocketClient` libraray. + +4. Configure a wild-card route at the lowest priority of your GET handler to catch all web requests that aren't caught by other actions: + +```js +// config/routes.js + +exports['default'] = { + routes: (api) => { + return { + get: [ + { path: '/time', action: 'time' }, + + { path: '/', matchTrailingPathParts: true, action: 'render' } + ] + } + } +} +``` diff --git a/examples/custom-server-actionhero/actions/render.js b/examples/custom-server-actionhero/actions/render.js new file mode 100644 index 00000000..60bd2700 --- /dev/null +++ b/examples/custom-server-actionhero/actions/render.js @@ -0,0 +1,15 @@ +'use strict' +const {Action, api} = require('actionhero') + +module.exports = class CreateChatRoom extends Action { + constructor () { + super() + this.name = 'render' + this.description = 'I render the next.js react website' + } + + async run (data) { + data.toRender = false + return api.next.render(data.connection) + } +} diff --git a/examples/custom-server-actionhero/config/api.js b/examples/custom-server-actionhero/config/api.js new file mode 100644 index 00000000..3e85fc02 --- /dev/null +++ b/examples/custom-server-actionhero/config/api.js @@ -0,0 +1,97 @@ +'use strict' + +const path = require('path') + +exports['default'] = { + general: (api) => { + const packageJSON = require(api.projectRoot + path.sep + 'package.json') + + return { + apiVersion: packageJSON.version, + serverName: packageJSON.name, + // id can be set here, or it will be generated dynamically. + // Be sure that every server you run has a unique ID (which will happen when generated dynamically) + // id: 'myActionHeroServer', + // A unique token to your application that servers will use to authenticate to each other + serverToken: 'change-me', + // the redis prefix for actionhero's cache objects + cachePrefix: 'actionhero:cache:', + // the redis prefix for actionhero's cache/lock objects + lockPrefix: 'actionhero:lock:', + // how long will a lock last before it exipres (ms)? + lockDuration: 1000 * 10, // 10 seconds + // Watch for changes in actions and tasks, and reload/restart them on the fly + developmentMode: true, + // How many pending actions can a single connection be working on + simultaneousActions: 5, + // allow connections to be created without remoteIp and remotePort (they will be set to 0) + enforceConnectionProperties: true, + // disables the whitelisting of client params + disableParamScrubbing: false, + // params you would like hidden from any logs + filteredParams: [], + // values that signify missing params + missingParamChecks: [null, '', undefined], + // The default filetype to server when a user requests a directory + directoryFileType: 'index.html', + // What log-level should we use for file requests? + fileRequestLogLevel: 'info', + // The default priority level given to middleware of all types (action, connection, say, and task) + defaultMiddlewarePriority: 100, + // Which channel to use on redis pub/sub for RPC communication + channel: 'actionhero', + // How long to wait for an RPC call before considering it a failure + rpcTimeout: 5000, + // should CLI methods and help include internal ActionHero CLI methods? + cliIncludeInternal: true, + // configuration for your actionhero project structure + paths: { + 'action': [path.join(__dirname, '/../actions')], + 'task': [path.join(__dirname, '/../tasks')], + 'public': [path.join(__dirname, '/../static')], + 'pid': [path.join(__dirname, '/../pids')], + 'log': [path.join(__dirname, '/../log')], + 'server': [path.join(__dirname, '/../servers')], + 'cli': [path.join(__dirname, '/../bin')], + 'initializer': [path.join(__dirname, '/../initializers')], + 'plugin': [path.join(__dirname, '/../node_modules')], + 'locale': [path.join(__dirname, '/../locales')] + }, + // hash containing chat rooms you wish to be created at server boot + startingChatRooms: { + // format is {roomName: {authKey, authValue}} + // 'secureRoom': {authorized: true}, + } + } + } +} + +exports.test = { + general: (api) => { + return { + id: 'test-server-' + process.pid, + serverToken: 'serverToken-' + process.pid, + developmentMode: true, + startingChatRooms: { + 'defaultRoom': {}, + 'otherRoom': {} + }, + paths: { + 'locale': [ + // require('os').tmpdir() + require('path').sep + 'locales', + path.join(__dirname, '/../locales') + ] + }, + rpcTimeout: 3000 + } + } +} + +exports.production = { + general: (api) => { + return { + fileRequestLogLevel: 'debug', + developmentMode: false + } + } +} diff --git a/examples/custom-server-actionhero/config/errors.js b/examples/custom-server-actionhero/config/errors.js new file mode 100644 index 00000000..458afb0f --- /dev/null +++ b/examples/custom-server-actionhero/config/errors.js @@ -0,0 +1,148 @@ +'use strict' + +// error messages can be strings of objects +exports['default'] = { + errors: (api) => { + return { + '_toExpand': false, + + // /////////////// + // SERIALIZERS // + // /////////////// + + serializers: { + servers: { + web: (error) => { + if (error.message) { + return String(error.message) + } else { + return error + } + }, + websocket: (error) => { + if (error.message) { + return String(error.message) + } else { + return error + } + }, + socket: (error) => { + if (error.message) { + return String(error.message) + } else { + return error + } + }, + specHelper: (error) => { + if (error.message) { + return 'Error: ' + String(error.message) + } else { + return error + } + } + } + }, + + // /////////// + // ACTIONS // + // /////////// + + // When a params for an action is invalid + invalidParams: (data, validationErrors) => { + if (validationErrors.length >= 0) { return validationErrors[0] } + return data.connection.localize('actionhero.errors.invalidParams') + }, + + // When a required param for an action is not provided + missingParams: (data, missingParams) => { + return data.connection.localize(['actionhero.errors.missingParams', {param: missingParams[0]}]) + }, + + // user requested an unknown action + unknownAction: (data) => { + return data.connection.localize('actionhero.errors.unknownAction') + }, + + // action not useable by this client/server type + unsupportedServerType: (data) => { + return data.connection.localize(['actionhero.errors.unsupportedServerType', {type: data.connection.type}]) + }, + + // action failed because server is mid-shutdown + serverShuttingDown: (data) => { + return data.connection.localize('actionhero.errors.serverShuttingDown') + }, + + // action failed because this client already has too many pending acitons + // limit defined in api.config.general.simultaneousActions + tooManyPendingActions: (data) => { + return data.connection.localize('actionhero.errors.tooManyPendingActions') + }, + + dataLengthTooLarge: (maxLength, receivedLength) => { + return api.i18n.localize(['actionhero.errors.dataLengthTooLarge', {maxLength: maxLength, receivedLength: receivedLength}]) + }, + + // /////////////// + // FILE SERVER // + // /////////////// + + // The body message to accompany 404 (file not found) errors regarding flat files + // You may want to load in the contnet of 404.html or similar + fileNotFound: (connection) => { + return connection.localize(['actionhero.errors.fileNotFound']) + }, + + // user didn't request a file + fileNotProvided: (connection) => { + return connection.localize('actionhero.errors.fileNotProvided') + }, + + // something went wrong trying to read the file + fileReadError: (connection, error) => { + return connection.localize(['actionhero.errors.fileReadError', {error: String(error)}]) + }, + + // /////////////// + // CONNECTIONS // + // /////////////// + + verbNotFound: (connection, verb) => { + return connection.localize(['actionhero.errors.verbNotFound', {verb: verb}]) + }, + + verbNotAllowed: (connection, verb) => { + return connection.localize(['actionhero.errors.verbNotAllowed', {verb: verb}]) + }, + + connectionRoomAndMessage: (connection) => { + return connection.localize('actionhero.errors.connectionRoomAndMessage') + }, + + connectionNotInRoom: (connection, room) => { + return connection.localize(['actionhero.errors.connectionNotInRoom', {room: room}]) + }, + + connectionAlreadyInRoom: (connection, room) => { + return connection.localize(['actionhero.errors.connectionAlreadyInRoom', {room: room}]) + }, + + connectionRoomHasBeenDeleted: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomHasBeenDeleted') + }, + + connectionRoomNotExist: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomNotExist') + }, + + connectionRoomExists: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomExists') + }, + + connectionRoomRequired: (room) => { + return api.i18n.localize('actionhero.errors.connectionRoomRequired') + } + + } + } +} diff --git a/examples/custom-server-actionhero/config/i18n.js b/examples/custom-server-actionhero/config/i18n.js new file mode 100644 index 00000000..979beef7 --- /dev/null +++ b/examples/custom-server-actionhero/config/i18n.js @@ -0,0 +1,36 @@ +exports['default'] = { + i18n: (api) => { + return { + // visit https://github.com/mashpie/i18n-node to see all configuration options + // locale path can be configired from within ./config/api.js + locales: ['en'], + + // how would you like your lanaguages to fall back if a translation string is missing? + fallbacks: { + // 'es': 'en' + }, + + // configure i18n to allow for object-style key lookup + objectNotation: true, + + // should actionhero append any missing translations to the locale file? + updateFiles: true, + + // this will configure logging and error messages in the log(s) + defaultLocale: 'en', + + // the name of the method by which to determine the connection's locale + // by default, every request will be in the 'en' locale + // this method will be called witin the localiazation middleware on all requests + determineConnectionLocale: 'api.i18n.determineConnectionLocale' + } + } +} + +exports.test = { + i18n: (api) => { + return { + updateFiles: true + } + } +} diff --git a/examples/custom-server-actionhero/config/logger.js b/examples/custom-server-actionhero/config/logger.js new file mode 100644 index 00000000..a67e323c --- /dev/null +++ b/examples/custom-server-actionhero/config/logger.js @@ -0,0 +1,60 @@ +'use strict' + +const fs = require('fs') +const cluster = require('cluster') + +exports['default'] = { + logger: (api) => { + let logger = {transports: []} + + // console logger + if (cluster.isMaster) { + logger.transports.push(function (api, winston) { + return new (winston.transports.Console)({ + colorize: true, + level: 'info', + timestamp: function () { return api.id + ' @ ' + new Date().toISOString() } + }) + }) + } + + // file logger + logger.transports.push(function (api, winston) { + if (api.config.general.paths.log.length === 1) { + const logDirectory = api.config.general.paths.log[0] + try { + fs.mkdirSync(logDirectory) + } catch (e) { + if (e.code !== 'EEXIST') { + throw (new Error('Cannot create log directory @ ' + logDirectory)) + } + } + } + + return new (winston.transports.File)({ + filename: api.config.general.paths.log[0] + '/' + api.pids.title + '.log', + level: 'info', + timestamp: function () { return api.id + ' @ ' + new Date().toISOString() } + }) + }) + + // the maximum length of param to log (we will truncate) + logger.maxLogStringLength = 100 + + // you can optionally set custom log levels + // logger.levels = {good: 0, bad: 1}; + + // you can optionally set custom log colors + // logger.colors = {good: 'blue', bad: 'red'}; + + return logger + } +} + +exports.test = { + logger: (api) => { + return { + transports: null + } + } +} diff --git a/examples/custom-server-actionhero/config/plugins.js b/examples/custom-server-actionhero/config/plugins.js new file mode 100644 index 00000000..01b01572 --- /dev/null +++ b/examples/custom-server-actionhero/config/plugins.js @@ -0,0 +1,27 @@ +exports['default'] = { + plugins: (api) => { + /* + If you want to use plugins in your application, include them here: + + return { + 'myPlugin': { path: __dirname + '/../node_modules/myPlugin' } + } + + You can also toggle on or off sections of a plugin to include (default true for all sections): + + return { + 'myPlugin': { + path: __dirname + '/../node_modules/myPlugin', + actions: true, + tasks: true, + initializers: true, + servers: true, + public: true, + cli: true + } + } + */ + + return {} + } +} diff --git a/examples/custom-server-actionhero/config/redis.js b/examples/custom-server-actionhero/config/redis.js new file mode 100644 index 00000000..f392d929 --- /dev/null +++ b/examples/custom-server-actionhero/config/redis.js @@ -0,0 +1,49 @@ +let host = process.env.REDIS_HOST || '127.0.0.1' +let port = process.env.REDIS_PORT || 6379 +let db = process.env.REDIS_DB || 0 +let password = process.env.REDIS_PASSWORD || null +const maxBackoff = 1000 + +if (process.env.REDIS_URL) { + password = process.env.REDIS_URL.match(/redis:\/\/.*:(.*)@.*:\d*$/i)[1] + host = process.env.REDIS_URL.match(/redis:\/\/.*:.*@(.*):\d*$/i)[1] + port = parseInt(process.env.REDIS_URL.match(/redis:\/\/.*:.*@.*:(\d*)$/i)[1]) +} + +exports['default'] = { + redis: (api) => { + // konstructor: The redis client constructor method. All redis methods must be promises + // args: The arguments to pass to the constructor + // buildNew: is it `new konstructor()` or just `konstructor()`? + + function retryStrategy (times) { + if (times === 1) { + const error = 'Unable to connect to Redis - please check your Redis config!' + if (process.env.NODE_ENV === 'test') { console.error(error) } else { api.log(error, 'error') } + return 5000 + } + return Math.min(times * 50, maxBackoff) + } + + return { + enabled: true, + + '_toExpand': false, + client: { + konstructor: require('ioredis'), + args: [{ port: port, host: host, password: password, db: db, retryStrategy: retryStrategy }], + buildNew: true + }, + subscriber: { + konstructor: require('ioredis'), + args: [{ port: port, host: host, password: password, db: db, retryStrategy: retryStrategy }], + buildNew: true + }, + tasks: { + konstructor: require('ioredis'), + args: [{ port: port, host: host, password: password, db: db, retryStrategy: retryStrategy }], + buildNew: true + } + } + } +} diff --git a/examples/custom-server-actionhero/config/routes.js b/examples/custom-server-actionhero/config/routes.js new file mode 100644 index 00000000..c126f8e5 --- /dev/null +++ b/examples/custom-server-actionhero/config/routes.js @@ -0,0 +1,9 @@ +exports['default'] = { + routes: (api) => { + return { + get: [ + { path: '/', matchTrailingPathParts: true, action: 'render' } + ] + } + } +} diff --git a/examples/custom-server-actionhero/config/servers/socket.js b/examples/custom-server-actionhero/config/servers/socket.js new file mode 100644 index 00000000..f4c918d2 --- /dev/null +++ b/examples/custom-server-actionhero/config/servers/socket.js @@ -0,0 +1,37 @@ +'use strict' + +exports['default'] = { + servers: { + socket: (api) => { + return { + enabled: (process.env.ENABLE_TCP_SERVER !== undefined), + // TCP or TLS? + secure: false, + // Passed to tls.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Port or Socket + port: 5000, + // Which IP to listen on (use 0.0.0.0 for all) + bindIP: '0.0.0.0', + // Enable TCP KeepAlive pings on each connection? + setKeepAlive: false, + // Delimiter string for incoming messages + delimiter: '\n', + // Maximum incoming message string length in Bytes (use 0 for Infinite) + maxDataLength: 0 + } + } + } +} + +exports.test = { + servers: { + socket: (api) => { + return { + enabled: true, + port: 1001 + (process.pid % 64535), + secure: false + } + } + } +} diff --git a/examples/custom-server-actionhero/config/servers/web.js b/examples/custom-server-actionhero/config/servers/web.js new file mode 100644 index 00000000..6ad3dd09 --- /dev/null +++ b/examples/custom-server-actionhero/config/servers/web.js @@ -0,0 +1,121 @@ +'use strict' + +const os = require('os') + +exports['default'] = { + servers: { + web: (api) => { + return { + enabled: true, + // HTTP or HTTPS? + secure: false, + // Passed to https.createServer if secure=true. Should contain SSL certificates + serverOptions: {}, + // Should we redirect all traffic to the first host in this array if hte request header doesn't match? + // i.e.: [ 'https://www.site.com' ] + allowedRequestHosts: process.env.ALLOWED_HOSTS ? process.env.ALLOWED_HOSTS.split(',') : [], + // Port or Socket Path + port: process.env.PORT || 8080, + // Which IP to listen on (use '0.0.0.0' for all; '::' for all on ipv4 and ipv6) + // Set to `null` when listening to socket + bindIP: '0.0.0.0', + // Any additional headers you want actionhero to respond with + httpHeaders: { + 'X-Powered-By': api.config.general.serverName, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE', + 'Access-Control-Allow-Headers': 'Content-Type' + }, + // Route that actions will be served from; secondary route against this route will be treated as actions, + // IE: /api/?action=test == /api/test/ + urlPathForActions: 'api', + // Route that static files will be served from; + // path (relative to your project root) to serve static content from + // set to `null` to disable the file server entirely + urlPathForFiles: 'public', + // When visiting the root URL, should visitors see 'api' or 'file'? + // Visitors can always visit /api and /public as normal + rootEndpointType: 'api', + // simple routing also adds an 'all' route which matches /api/:action for all actions + simpleRouting: true, + // queryRouting allows an action to be defined via a URL param, ie: /api?action=:action + queryRouting: true, + // The cache or (if etags are enabled) next-revalidation time to be returned for all flat files served from /public; defined in seconds + flatFileCacheDuration: 60, + // Add an etag header to requested flat files which acts as fingerprint that changes when the file is updated; + // Client will revalidate the fingerprint at latest after flatFileCacheDuration and reload it if the etag (and therfore the file) changed + // or continue to use the cached file if it's still valid + enableEtag: true, + // should we save the un-parsed HTTP POST/PUT payload to connection.rawConnection.params.rawBody? + saveRawBody: false, + // How many times should we try to boot the server? + // This might happen if the port is in use by another process or the socketfile is claimed + bootAttempts: 1, + // Settings for determining the id of an http(s) request (browser-fingerprint) + fingerprintOptions: { + cookieKey: 'sessionID', + toSetCookie: true, + onlyStaticElements: false, + settings: { + path: '/', + expires: 3600000 + } + }, + // Options to be applied to incoming file uploads. + // More options and details at https://github.com/felixge/node-formidable + formOptions: { + uploadDir: os.tmpdir(), + keepExtensions: false, + maxFieldsSize: 1024 * 1024 * 100 + }, + // Should we pad JSON responses with whitespace to make them more human-readable? + // set to null to disable + padding: 2, + // Options to configure metadata in responses + metadataOptions: { + serverInformation: true, + requesterInformation: true + }, + // When true, returnErrorCodes will modify the response header for http(s) clients if connection.error is not null. + // You can also set connection.rawConnection.responseHttpCode to specify a code per request. + returnErrorCodes: true, + // should this node server attempt to gzip responses if the client can accept them? + // this will slow down the performance of actionhero, and if you need this funcionality, it is recommended that you do this upstream with nginx or your load balancer + compress: false, + // options to pass to the query parser + // learn more about the options @ https://github.com/hapijs/qs + queryParseOptions: {} + } + } + } +} + +exports.production = { + servers: { + web: (api) => { + return { + padding: null, + metadataOptions: { + serverInformation: false, + requesterInformation: false + } + } + } + } +} + +exports.test = { + servers: { + web: (api) => { + return { + secure: false, + port: process.env.PORT || 1000 + (process.pid % 64535), + matchExtensionMime: true, + metadataOptions: { + serverInformation: true, + requesterInformation: true + } + } + } + } +} diff --git a/examples/custom-server-actionhero/config/servers/websocket.js b/examples/custom-server-actionhero/config/servers/websocket.js new file mode 100644 index 00000000..f72c0975 --- /dev/null +++ b/examples/custom-server-actionhero/config/servers/websocket.js @@ -0,0 +1,62 @@ +'use strict' + +// Note that to use the websocket server, you also need the web server enabled + +exports['default'] = { + servers: { + websocket: (api) => { + return { + enabled: true, + // you can pass a FQDN (string) here or 'window.location.origin' + clientUrl: 'window.location.origin', + // Directory to render client-side JS. + // Path should start with "/" and will be built starting from api.config..general.paths.public + clientJsPath: 'javascript/', + // the name of the client-side JS file to render. Both `.js` and `.min.js` versions will be created + // do not include the file exension + // set to `undefined` to not render the client-side JS on boot + clientJsName: 'ActionheroWebsocketClient', + // should the server signal clients to not reconnect when the server is shutdown/reboot + destroyClientsOnShutdown: false, + + // websocket Server Options: + server: { + // authorization: null, + // pathname: '/primus', + // parser: 'JSON', + // transformer: 'websockets', + // plugin: {}, + // timeout: 35000, + // origins: '*', + // methods: ['GET','HEAD','PUT','POST','DELETE','OPTIONS'], + // credentials: true, + // maxAge: '30 days', + // exposed: false, + }, + + // websocket Client Options: + client: { + apiPath: '/api' // the api base endpoint on your actionhero server + // reconnect: {}, + // timeout: 10000, + // ping: 25000, + // pong: 10000, + // strategy: "online", + // manual: false, + // websockets: true, + // network: true, + // transport: {}, + // queueSize: Infinity, + } + } + } + } +} + +exports['test'] = { + servers: { + websocket: (api) => { + return { clientUrl: null } + } + } +} diff --git a/examples/custom-server-actionhero/config/tasks.js b/examples/custom-server-actionhero/config/tasks.js new file mode 100644 index 00000000..f32d4d42 --- /dev/null +++ b/examples/custom-server-actionhero/config/tasks.js @@ -0,0 +1,61 @@ +exports['default'] = { + tasks: (api) => { + return { + // Should this node run a scheduler to promote delayed tasks? + scheduler: false, + // what queues should the taskProcessors work? + queues: ['*'], + // Logging levels of task workers + workerLogging: { + failure: 'error', // task failure + success: 'info', // task success + start: 'info', + end: 'info', + cleaning_worker: 'info', + poll: 'debug', + job: 'debug', + pause: 'debug', + internalError: 'error', + multiWorkerAction: 'debug' + }, + // Logging levels of the task scheduler + schedulerLogging: { + start: 'info', + end: 'info', + poll: 'debug', + enqueue: 'debug', + reEnqueue: 'debug', + working_timestamp: 'debug', + transferred_job: 'debug' + }, + // how long to sleep between jobs / scheduler checks + timeout: 5000, + // at minimum, how many parallel taskProcessors should this node spawn? + // (have number > 0 to enable, and < 1 to disable) + minTaskProcessors: 0, + // at maximum, how many parallel taskProcessors should this node spawn? + maxTaskProcessors: 0, + // how often should we check the event loop to spawn more taskProcessors? + checkTimeout: 500, + // how many ms would constitue an event loop delay to halt taskProcessors spawning? + maxEventLoopDelay: 5, + // When we kill off a taskProcessor, should we disconnect that local redis connection? + toDisconnectProcessors: true, + // Customize Resque primitives, replace null with required replacement. + resque_overrides: { + queue: null, + multiWorker: null, + scheduler: null + } + } + } +} + +exports.test = { + tasks: (api) => { + return { + timeout: 100, + checkTimeout: 50 + } + } +} diff --git a/examples/custom-server-actionhero/initializers/next.js b/examples/custom-server-actionhero/initializers/next.js new file mode 100644 index 00000000..2297e70c --- /dev/null +++ b/examples/custom-server-actionhero/initializers/next.js @@ -0,0 +1,32 @@ +'use strict' +const {Initializer, api} = require('actionhero') +const next = require('next') + +module.exports = class NextInitializer extends Initializer { + constructor () { + super() + this.name = 'next' + } + + async initialize () { + api.next = { + render: async (connection) => { + if (connection.type !== 'web') { throw new Error('Connections for NEXT apps must be of type "web"') } + const req = connection.rawConnection.req + const res = connection.rawConnection.res + return api.next.handle(req, res) + } + } + + api.next.dev = (api.env === 'development') + if (api.next.dev) { api.log('Running next in development mode...') } + + api.next.app = next({dev: api.next.dev}) + api.next.handle = api.next.app.getRequestHandler() + await api.next.app.prepare() + } + + async stop () { + await api.next.app.close() + } +} diff --git a/examples/custom-server-actionhero/locales/en.json b/examples/custom-server-actionhero/locales/en.json new file mode 100644 index 00000000..11b004f3 --- /dev/null +++ b/examples/custom-server-actionhero/locales/en.json @@ -0,0 +1,34 @@ +{ + "actionhero": { + "welcomeMessage": "Hello! Welcome to the actionhero api", + "goodbyeMessage": "Bye!", + "cache": { + "objectNotFound": "Object not found", + "objectLocked": "Object locked", + "objectExpired": "Object expired" + }, + "errors": { + "invalidParams": "validation error", + "missingParams": "{{param}} is a required parameter for this action", + "unknownAction": "unknown action or invalid apiVersion", + "unsupportedServerType": "this action does not support the {{type}} connection type", + "serverShuttingDown": "the server is shutting down", + "tooManyPendingActions": "you have too many pending requests", + "dataLengthTooLarge": "data length is too big ({{maxLength}} received/{{receivedLength}} max)", + "fileNotFound": "That file is not found", + "fileNotProvided": "file is a required param to send a file", + "fileReadError": "error reading file: {{error}}", + "verbNotFound": "I do not know know to perform this verb ({{verb}})", + "verbNotAllowed": "verb not found or not allowed ({{verb}})", + "connectionRoomAndMessage": "both room and message are required", + "connectionNotInRoom": "connection not in this room ({{room}})", + "connectionAlreadyInRoom": "connection already in this room ({{room}})", + "connectionRoomHasBeenDeleted": "this room has been deleted", + "connectionRoomNotExist": "room does not exist", + "connectionRoomExists": "room exists", + "connectionRoomRequired": "a room is required" + } + }, + "Your random number is {{number}}": "Your random number is {{number}}", + "Node Healthy": "Node Healthy" +} diff --git a/examples/custom-server-actionhero/package.json b/examples/custom-server-actionhero/package.json new file mode 100644 index 00000000..469e9a09 --- /dev/null +++ b/examples/custom-server-actionhero/package.json @@ -0,0 +1,17 @@ +{ + "engines": { + "node": ">=8.0.0" + }, + "dependencies": { + "actionhero": "^18.1.2", + "ioredis": "^3.2.2", + "isomorphic-fetch": "^2.2.1", + "next": "^5.0.1-canary.9", + "react": "^16.1.1", + "react-dom": "^16.1.1", + "ws": "^4.1.0" + }, + "scripts": { + "start": "actionhero start" + } +} diff --git a/examples/custom-server-actionhero/pages/a.js b/examples/custom-server-actionhero/pages/a.js new file mode 100644 index 00000000..16eb9898 --- /dev/null +++ b/examples/custom-server-actionhero/pages/a.js @@ -0,0 +1 @@ +export default () =>
a
diff --git a/examples/custom-server-actionhero/pages/b.js b/examples/custom-server-actionhero/pages/b.js new file mode 100644 index 00000000..3791f48b --- /dev/null +++ b/examples/custom-server-actionhero/pages/b.js @@ -0,0 +1 @@ +export default () =>
b
diff --git a/examples/custom-server-actionhero/pages/index.js b/examples/custom-server-actionhero/pages/index.js new file mode 100644 index 00000000..d044fc1e --- /dev/null +++ b/examples/custom-server-actionhero/pages/index.js @@ -0,0 +1,9 @@ +import React from 'react' +import Link from 'next/link' + +export default () => ( + +)