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

@ashnazg/yog

v0.3.2

Published

a websocket server/client message bus

Downloads

20

Readme


title: "@ashnazg/yog" sidebar_label: yog

it's an auth / interruption-resilient / RPC / broadcast library for browser <-> node.js server that provides a high level interface over the low level websockets protocol.

new

Client

It decorates the client pubsub implementation:

const pubsub = require('@ashnazg/pubsub'); // ~/projects/ashnazg-npm/pubsub/pubsub.js
const yog = require('@ashnazg/yog/bare-client2'); // ~/projects/ashnazg-npm/yog/bare-client2.js
const myhub = pubsub();
yog.bare(myhub);

// not returning promises from (re)connect() as this is just the bottom layer of a repeatable-reconnect system
myhub.bare.connect('ws://localhost:5000', 'foo');
myhub.on('bare:open', () => {
	myhub.bare.send({t:"hello world"});
});
myhub.on('answer', msg => {
	console.log("they answered with", msg);
});

Other bare-level members: reconnect(), close(), send({tuple}), state() === 'OPEN', async request({tuple})

old

server side

io layer: bare server sockets

turns socket events into a server stream of: io:open, io:close, io:error, and actual data tuples that have a "t" field (of type string or number)

RPC and register

If you use server.register({hooks...}) to make your singleton handlers for inbound events, your hooks gain:

  1. the ability to respond RPC style by just returning fields
  2. access to each other: server.hooks[other_responder]
server.register({
	showRoom({ctx, room_id}) {
		return {room_contents: []};
	},
	player_joined({ctx}) {
		const uid = ctx.account.uid;
		const avatar = process(uid);
		const room = server.hooks.showRoom({ctx, room_id: avatar.room_id});
		room.your_uid = uid;
		return room;
	}
});

RPC support:

client.request('player_joined').then(resp => {
	console.log('my uid', resp.your_uid);
	console.log('I see in the room', resp.room_contents);
});

auth layer: server auth and security tricks

blocks all requests til auth:login packet has been accepted by replying with auth:open

auth:attack reports brute force blocking statistics auth:pending means that the client will be granted access after the delay auth:open (posted internally and externally) means the client is allowed to talk

because all auth:login field validation is passed to the caller, the auth() config can be whatever, {user,pass} for a bare system up to fancy stuff like {user,pass,token,nonce}

session: takes auth and adds token-based steps for creation/expiration/auth

const session = require('@ashnazg/yog/session-server.js');
const server = session.listen({
	session_manager,
	lifespan: milliseconds, //a this param is only used if session_manager is using pattern 2 below. it defaults to 24 hours.
	checkPass(user, pass) { return user_id; },
	getUid(username) { return user_id; } // defaults to 'username'. this is like checkPass but is only used to perform the user->id mapping for token-handling, in which we don't have a password in the request.
});

session takes a state storage interface -- there are two options you can use:

  1. if your provided DB supplies these two functions, I'll just use them.
const session_manager = {
	generateSessionToken(user) { return new token; },
	isValidSession(user, token) { return true ; }
};
  1. or a plain old JSON "db" and a save() method:
const session_manager = {
	db: {}, // yog/session will create a tokens map on this object
	save() { persist the above db however you want; }
}

Conveniently, dumbfile has exactly that interface:

const dumb = require('@ashnazg/dumbfile'); // ~/projects/ashnazg-npm/dumbfile/json.js
const session_manager = dumb.load(process.env.HOME + '/.socks-tokens.json');

Important thing to remember about user ids and the optional support for anonymous connections. (And the fact that I have to say that means this design is being too clever -- but I'm not going to slow down right now over that.)

if your checkPass(user, pass) returns zero, that means to accept the connection but not bother creating a token for it -- anonymous connections will always just pass the publically shared user/password.

auth-client

This util wraps bounce-client in an auth system that can manage session cookies and prompt the user for credentials.

All of these utils will decorate the pubsub you give them with the websocket-wrapper methods. f

  • All functions provided by manager return promises.
    • if successful, it resolves to an auth response.
    • it resolves to {success: false, evt: {reason,code}} (not a rejection) for stopping points that are not logic errors:
      • connection lost before auth handshake completed
      • connection not attempted because session state was not found
      • can't start connecting, no network
      • server rejected creds

The library init() needs 4 to 7 parameters:

  1. a pubsub (which could be the engine from a project using mischievous -- that's the way I use this lib.)
  2. a callback: promptForCreds({user, pass, error})=>promise({user,pass}) It's given any known creds (such as a username stored in localStorage) and must return either:
    • a promise for ({user,pass}) when the user submits them
    • a promise for the string "cancelled" if the user backs out
    • the error parameter is undefined in the first round of prompting; on later rounds, it's set to the server's response on an auth fail so the UI can display it to the user.
  3. a callback showSpinner() for telling the UI that an auth attempt is in progress.
  4. a callback hideSpinner() that will be called before the auth attempt takes the next step, whether that's to show the login prompt again, or to resolving the auth attempt's promise.
  5. a server-local api path to use in prod: if you give it /yog then in production, the websocket will connect to wss://{current dns name}:{current port}/yog
  6. a dev endpoint in the form of either a port number, or a full URL like ws://localhost:5000 (that's the default if dev is undefined.)
  7. an optional websocket protocol name

Prod vs dev: since NODE_ENV isn't directly available in the browser, connect() determines 'is prod' by seeing if the webserver is using https.

import {init} from '@ashnazg/yog/auth-client';
const pub = require('@ashnazg/yog/pubsub.js').pubsub();

function prompt({user, pass, error}) {
	if (error) console.error(error);
	return q({user: "anony", pass: "mouse");
}
function spin() {
	console.log("look busy! auth is in progress");
}
function fin() {
	console.log("clear out the busy indicator");
}
const prod_path = '/yog';
const dev_port = 5000;
const protocol = 'sample-protocol';
const manager = init(pub, prompt, spin, fin, prod_path, dev_port, protocol);

login

This prompts the user, and for as many attempts as it takes til either the server accepts the creds, or the user hits cancel in your prompt UI, login will call connect(...).

Login will read/write browser state to make reconnecting easier:

  1. on start, pull "user" from localStorage if it's available.
  2. on successful auth, set "user" into localStorage and re
    1. if that auth response includes "token", store that token in cookie "session" so tryCookie() can find it late.
  3. if user cancels the prompt UI, login will clear the above two storage fields and return false
const auth_response = await manager.login();
if (auth_response === false) {
	return 'user gave up';
}
console.log(auth_response.user_name); // or whatever your server responds with

tryCookie

This is for background reconnection after a network event or browser-reset; if the user/session fields from a successful login() are there, tryCookie will try to auth to the server with {user,token}

If either field is unavailable, tryCookie will invoke login()

If you want to just test in the background without risking a login() popup, pass it false: tryCookie(false) will just return false if there's no session.

const auth_response = await manager.tryCookie(false);
if (auth_response === false) {
	console.log("I can either push the user to login by calling manager.login, or maybe my app has a login button to enable at this point.");
} else {
	console.log(auth_response.user_name); // or whatever your server responds with
}

connect

This is the programmatic way to directly supply {user, pass}. In the envisioned flow, you don't need to call this, as tryCookie and login do this, but it's exported to the app as I've found it useful. (For example, in one app, there's a public {user,pass} pair for readonly access to the API.)

If the server auth response includes {reason: 'token expired'}, then connect() will auto-launch login().

const auth_response = await manager.connect({user: 'anony', pass: 'mouse'});
if (auth_response === false) {
	console.log("could not connect");
} else {
	console.log(auth_response.user_name); // or whatever your server responds with
}

bare-client

This provides the base layer for websockets. It adds these functions to your pubsub:

send

con.send('type', {fields}) works much like a pubsub(type, {fields}) like pubsub, you can skip the 2nd parameter if event 'type' needs no other fields.

request

This has the same two params as send, but request adds a 'q' field to the network packet; the yog server uses 'q' as a RPC ID. If your server-side endpoint function returns a message body, yog will add the same 'q' value that the client picked and send that body back to the client. The goal of this is to allow the client to easily distinguish which request goes with which response.

// client side
con.request('readfile', {name: '/etc/passwd'}).then(resp => console.log("user entries:", resp);
con.request('readfile', {name: '/etc/nginx/nginx.conf'}).then(resp => console.log("nginx conf:", resp);

// server side
server.register({
	readfile({name, ctx}) {
		return {errors:[{code: 404, message: "I'm not going to give you "+name}]};
	}
});

Since the above readfile() returned a body with no {t:'type'} field, the RPC handler will default it to the request's type.

If the server really has to send a different type back in response, add a third parameter string to your request call:

con.request('readfile', {fields}, 'response-event-type').then(resp => {});

close

con.close() closes and deletes the socket. This does not impact your on() listeners.

reconnect

Closes any existing connection, and then reattempt.

isOpen

con.isOpen() returns a bool

readyState

con.readyState() allows you to look at the connection's current lifecycle step.

const bounce = require('./bounce-client.js');
if (con.readyState() === bounce.CLOSING) {
	// too late to send anything or bother calling close()
}

bounce: is a client-side layer that repeats auth cookies to resume connectivity

state: never connected, connected, retrying gave_up retrying, resume

1012 is the close code for "I'm bouncing the server"

queue layer:

if a message can't be sent, it's queued until it can.

state layer:

versions the world, xfers world deltas

server side notes

io.listen(port...)

auth.listen(... auth() checker)

close codes

https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent vs http codes https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

future

  • FYI https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState

  • TODO: move to node-buffer on both ends, here's a browser-version:

  • https://github.com/feross/buffer

  • but find out what mobiles support.

  • https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send

    • https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType
    • https://developer.mozilla.org/en-US/docs/Web/API/Blob
    • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
  • OOH!

  • https://blog.mgechev.com/2015/02/06/parsing-binary-protocol-data-javascript-typedarrays-blobs/

  • responding in binary is straightforward:

  • https://github.com/websockets/ws#sending-binary-data