npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

moleculer-io-3

v0.0.1

Published

An API GateWay service for moleculerjs using socket.io v3 - Originally by @tiaod

Downloads

3

Readme

LOGO GitHub license npm Known Vulnerabilities Downloads

The moleculer-io is the API gateway service for Moleculer using socket.io. Use it to publish your services.

  • Call moleculer actions by emiting Socket.io events.
  • Support Socket.io authorization (Default: socket.client.user => moleculer ctx.meta.user)
  • Whitelist.
  • Middlewares.
  • Broadcast events.
  • Joining and leaving rooms.
$ npm install moleculer-io

Usage

Init server

Using with Node http server:

const server = require('http').Server(app)
const SocketIOService = require("moleculer-io")
const ioService = broker.createService({
  name: 'io',
  mixins: [SocketIOService]
})

ioService.initSocketIO(server)

// Once the initSocketIO() was called, you can access the io object from ioService.io
broker.start()
server.listen(3000)

Or let moleculer-io create a server for you:

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000 //will call initSocketIO() on broker.start()
  }
})
broker.start()

Or maybe you want to use it with moleculer-web

const ApiService = require("moleculer-web");
const SocketIOService = require("moleculer-io")
broker.createService({
  name: 'gateway',
  mixins: [ApiService, SocketIOService], //Should after moleculer-web
  settings: {
    port: 3000
  }
})
broker.start()

In this case, moleculer-io will use the server created by moleculer-web .

Handle socket events

Server:

const IO = require('socket.io')
const {
  ServiceBroker
} = require('moleculer')
const SocketIOService = require('moleculer-io')

const broker = new ServiceBroker()

broker.createService({
  name: "math",
  actions: {
    add(ctx) {
      return Number(ctx.params.a) + Number(ctx.params.b);
    }
  }
})

const ioService = broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000
  }
})

broker.start()

By default, moleculer-io handle the call event which will proxy to moleculer's broker.call

Examples:

  • Call test.hello action without params: socket.emit('call','test.hello', callback)
  • Call math.add action with params: socket.emit('call','math.add', {a:25, b:13}, callback)
  • Get health info of node: socket.emit('call','$node.health', callback)
  • List all actions: socket.emit('call', '$node.list', callback)

Example client:

const io = require('socket.io-client')
const socket = io('http://localhost:3000')
socket.emit('call', 'math.add', { a: 123, b: 456},
function(err, res) {
  if (err) {
    console.error(err)
  } else {
    console.log('call success:', res)
  }
})

Handle multiple events

You can create multiple routes with different whitelist, calling options & authorization.

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000,
    io: {
      namespaces: {
        '/': {
          events: {
            'call': {
              whitelist: [
                'math.add'
              ],
              callOptions: {}
            },
            'adminCall': {
              whitelist: [
                'users.*',
                '$node.*'
              ]
            }
          }
        }
      }
    }
  }
})

Aliases

You can use alias names instead of action names.

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000,
    io: {
      namespaces: {
        '/': {
          events: {
            'call': {
              aliases: {
                'add': 'math.add'
              },
              whitelist: [
                'math.add'
              ],
              callOptions: {}
            }
          }
        }
      }
    }
  }
})

Then doing socket.emit('call','math.add', {a:25, b:13}, callback) on the client side will be equivalent to socket.emit('call','add', {a:25, b:13}, callback).

Mapping policy

The event has a mappingPolicy property to handle events without aliases.

Available options:

  • all - enable to handle all actions with or without aliases (default)
  • restrict - enable to handle only the actions with aliases
broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000,
    io: {
      namespaces: {
        '/': {
          events: {
            'call': {
              mappingPolicy: 'restrict',
              aliases: {
                'add': 'math.add'
              },
              callOptions: {}
            }
          }
        }
      }
    }
  }
})

Custom handler

You can make use of custom functions within the declaration of event handler.

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000,
    io: {
      namespaces: {
        '/': {
          events: {
            'call': {},
            'myCustomEventHandler': function(data, ack) { // write your handler function here.
              let socket = this
              socket.emit('hello', 'world')
              socket.$service.broker.call('math.add', {
                a: 123,
                b: 456
              })
            }
          }
        }
      }
    }
  }
})

There is an internal pointer in socket objects:

  • socket.$service is pointed to this service instance.

Handler hooks

The event handler has before & after call hooks. You can use it to set ctx.meta, access socket object or modify the response data.

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    io: {
      namespaces: {
        '/': {
          events: {
            'call': {
              whitelist: [
                'math.*'
              ],
              onBeforeCall: async function(ctx, socket, action, params, callOptions) { //before hook
                  console.log('before hook:', params)
                },
              onAfterCall: async function(ctx, socket, res) { //after hook
                console.log('after hook', res)
                // res: The respose data.
              }
            }
          }
        }
      }
    }
  }
})

Calling options

The handler has a callOptions property which is passed to broker.call. So you can set timeout, retryCount or fallbackResponse options for routes.

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    io: {
      namespaces: {
        '/': {
          events: {
            'call': {
              callOptions: {
                timeout: 500,
                retryCount: 0,
                fallbackResponse(ctx, err) { ...
                }
              }
            }
          }
        }
      }
    }
  }
})

Note: If you provie a meta field here, it replace the socketGetMeta method's result.

Middlewares

Register middlewares. Both namespace middlewares and packet middlewares are supported.

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    io: {
      namespaces: {
        '/': {
          middlewares: [ //Namespace level middlewares, equipment to namespace.use()
            function(socket, next) {
              if (socket.request.headers.cookie) return next();
              next(new Error('Authentication error'));
            }
          ],
          packetMiddlewares: [ // equipment to socket.use()
            function(packet, next) {
              if (packet.doge === true) return next();
              next(new Error('Not a doge error'));
            }
          ],
          events: {
            'call': {}
          }
        }
      }
    }
  }
})

Note: In middlewares the this is always pointed to the Service instance.

Authorization

You can implement authorization. Do 2 things to enable it.

  • Set authorization: true in your namespace
  • Define the socketAuthorize method in service.
broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    io: {
      namespaces: {
        '/': {
          authorization: true, // First thing
          events: {
            'call': {
              whitelist: [
                'math.*',
                'accounts.*'
              ]
            }
          }
        }
      }
    }
  },
  methods: {
    // Second thing
    async socketAuthorize(socket, eventHandler){
      let accessToken = socket.handshake.query.token
      if (accessToken) {
        try{
          let user = await this.broker.call("user.verifyToken", {accessToken})
          return {id: user.id, email: user.email, token: accessToken}  // valid credential, return the user
        }catch(err){
          throw new UnAuthorizedError() // invalid credentials
        }
      } else {
        // anonymous user
        return
      }
    }
  }
})

Client:

const socket = io({
  query: {
    token: '12345'
  }
})

See examples/full

Also you could overwrite the socketGetMeta method to add more addition meta info. The default socketGetMeta method is:

socketGetMeta(socket){
  return {
    user: socket.client.user,
    $rooms: Object.keys(socket.rooms)
  }
}

Example to add more additional info:

broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  methods: {
    socketGetMeta(socket) { //construct the meta object.
      return {
        user: socket.client.user,
        $rooms: Object.keys(socket.rooms),
        socketId: socket.id
      }
    },
    // In addition, you can also customize the place where user is stored.
    // Here is the default method the save user:
    socketSaveMeta(socket, ctx) {
      socket.client.user = ctx.meta.user
    }
  }
})

If you want to authorize a user after socket connected, you can write an action to do it.

broker.createService({
  name: 'accounts',
  actions: {
    login(ctx){
      if(ctx.params.user == 'tiaod' && ctx.params.password == 'pass'){
        ctx.meta.user = {id:'tiaod'}
      }
    },
    getUserInfo(ctx){
      return ctx.meta.user
    }
  }
})

Joining and leaving rooms

In your action, set ctx.meta.$join or ctx.meta.$leave to the rooms you want to join or leave.

eg.

ctx.meta.$join = 'room1' //Join room1
ctx.meta.$join = ['room1', 'room2'] // Join room1 and room2

ctx.meta.$leave = 'room1' //Leave room1
ctx.meta.$leave = ['room1', 'room2'] // Leave room1 and room2

After the action finished, moleculer-io will join or leave the room you specified.

Example room management service:

broker.createService({
  name: 'rooms',
  actions: {
    join(ctx){
      ctx.meta.$join = ctx.params.room
    },
    leave(ctx){
      ctx.meta.$leave = ctx.params.room
    },
    list(ctx){
      return ctx.meta.$rooms
    }
  }
})

Broadcast

If you want to broadcast event to socket.io from moleculer service:

broker.call('io.broadcast', {
  namespace:'/', //optional
  event:'hello',
  args: ['my', 'friends','!'], //optional
  volatile: true, //optional
  local: true, //optional
  rooms: ['room1', 'room2'] //optional
})

Note: You should change the 'io' to the service name you created.

CORS

moleculer-io will pick the settings.cors.origin option and use it to validate the request. (Which is also compatible with moleculer-web! )

broker.createService({
  name: 'io',
  mixins: [ApiGateway, SocketIOService],
  settings:{
		cors: {
			origin: ["http://example.com"], //Moleculer-io only pick up this option and set it to io.origins()
			methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
			allowedHeaders: [],
			exposedHeaders: [],
			credentials: false,
			maxAge: 3600
		}
	}
})

For detail see https://socket.io/docs/server-api/#server-origins-fn

Using multiple instances

If you plan for a highly available setup (launching multiple instances of this service behind a Load Balancer), you will have to take some extra steps. Due to the nature of WebSockets these instances will need a PubSub capable broker to connect to, in order to broadcast messages to sockets which are connected to other instances. For a more in depth explanation of this concept, and additional steps that have to be taken (such as Load Balancer configuration), refere to the Socket.io Documentation.

In order to interconnect this service with other services, start the service with an adapter:

const broker = new ServiceBroker({
    transporter: "redis://redis:6379"
})
broker.createService({
  name: 'io',
  mixins: [SocketIOService],
  settings: {
    port: 3000,
    io: {
      options: {
        adapter: require("socket.io-redis")("redis://redis:6379")
      }
    }
  }
})

Full settings

settings: {
  port: 3000,
  io: {
    options: {}, //socket.io options
    namespaces: {
      '/':{
        authorization: false,
        middlewares: [],
        packetMiddlewares:[],
        events: {
          call: {
            mappingPolicy: 'all',
            aliases: {
              'add': 'math.add'
            },
            whitelist: [
              'math.*'
            ],
            callOptions:{},
            onBeforeCall: async function(ctx, socket, action, params, callOptions){
              ctx.meta.socketid = socket.id
            },
            onAfterCall:async function(ctx, socket, res){
             socket.emit('afterCall', res)
            }
          }
        }
      }
    }
  }
}

Change logs

1.1.3: Merge #27

1.1.1: Fix #18

1.1.0: Add cors config

1.0.9: Fix #17

1.0.8: Fix #12

1.0.7: Add settings.server options.

1.0.6: Set the babel targets.node to 'current'

1.0.5: Bug fix.

1.0.4: Bug fix.

1.0.3: Add aliases and mappingPolicy event properties.

1.0.2: socketAuthorize method can return the user now. Add socketSaveUser method.

1.0.1: Bug fix.

1.0.0: See Migrate to 1.x.

0.13.4: Fix bug of multiple custom event handler.

0.13.3: Add internal pointer to service instance, make socket.$service pointed to service instance.

0.13.2: Added socket.io adapter options for intercommunication of multiple instances

0.13.1: Add request logger.

0.13.0: moleculer-io can now get alone well with moleculer-web, you can use them together! - Note that settings.options now become to settings.io.

0.12.1: CustomHandler context now bind to the service instance.

0.12.0: Change ctx.meta.$user to ctx.meta.user, add saveUser method.

0.11.0: Bind middlewares context to service instance.

0.10.0: Add action visibility support. See Action visibility

0.9.1: Fix ServiceNotFoundError message.

0.9.0: Upgrade to [email protected], no breaking changes.

0.8.1: Fix io.broadcast error.

0.8.0: Add ctx.meta.$rooms, and ctx.meta.$join ctx.meta.$leave

0.7.0: Add hooks.

0.6.0: Modify settings format. again :)

0.5.0: Add broadcast to socket.io rooms

0.4.0: Modify settings format.

0.3.0: Add login handler.

0.2.0: Add initServer method.

License

Moleculer-io is available under the MIT license.