Fun with Stamps. Episode 15. The @stamp/ modules ecosystem

Hello. I’m developer Vasyl Boroviak and welcome to the fifteenth episode of Vasyl Boroviak presents Fun with Stamps.


TL;DR

The home of various useful stamps: https://www.npmjs.com/~stamp

And useful modules:

More to come…

The ecosystem

We are growing the ecosystem of useful stamps (aka behaviors) and utility modules under this NPM namespace — https://www.npmjs.com/~stamp

Main goals:

  • Provide commonly needed features as modules.
  • Keep the modules as small and performant as possible.
  • Have minimum dependencies which are not part of the ecosystem.
  • Keep the modules compatible with each other forever. Best effort at least.

The technology choices of the ecosystem:

  • A mono repository using Lerna.
  • Source code is ES5. Meaning there is no transpilation and will never be.
  • Modules are all CommonJS. Will be updated to ES2015 modules when all major platforms start supporting it. That is — all popular browsers (khm… IE… khm…) and the node.js.

You are welcome to submit requests of what you want to be a part of the ecosystem.

Let’s quickly list most popular modules.

@stamp/collision stamp

If you need your method not to be overridden by another implementation then this stamp is for you. Find more examples and information here.

Here is how you protect you methods:

import stampit from '@stamp/it';
import Collision from '@stamp/collision';
const HasToken = stampit({
methods: {
getToken() { return 'my-security-token'; }
}
})
.compose(Collision)
.collisionSetup({forbid: ['getToken']}); // protect the method
const Connection = stampit(HasToken, { // compose in the HasToken
methods: {
establish() {
return myDatabase.connect({ token: this.getToken() });
},
getToken() { // THIS LINE WILL THROW AN ERROR
return 'fake-token';
}
}
});

The code above will throw an error: “Collision of method ‘getToken’ is forbidden”.

Also, the same @stamp/collision stamp can aggregate methods when you need it. It’s called the “deferred” method.

import {Border, Button, Graph} from './my-ui-components';
import Collision from '@stamp/collision';
const UiComponent = Collision.collisionSetup({defer: ['draw']})
.compose(Border, Button, Graph);
const component = UiComponent();
component.draw(); // will draw() all three primitives

The component.draw() method will invoke all three methods, once per each component.

Use Collision.collisionProtectAnyMethod() to protect all methods of all derived stamps.

In case of emergency you can reset the collision setup of your stamp with Collision.collisionSettingsReset().

@stamp/configure stamp

If you collect some kind of configuration inside the Stamp.compose.configuration metadata then this stamp will make it accessible inside the methods.

import jwt from 'jsonwebtoken';
import stampit from '@stamp/it';
import Configure from '@stamp/configure';
const Jwt = stampit(Configure, { // compose in the Configure stamp
conf: {
jwtSecret: process.env.SECRET
},
methods: {
createJwtToken(payload) {
return jwt.sign(
payload,
this.config.jwtSecret // this.config is private variable
);
},
verifyJwtToken(token) {
return jwt.verify(
token,
this.config.jwtSecret // this.config is private variable
);
}
}
});

@stamp/eventemmitable stamp

Every object created from your stamp will be the node.js event emitter.

import SystemMetricsPusher from './system-metrics-pusher';
import EventEmittable from '@stamp/evenemittable';
const EventPusher = SystemMetricsPusher
.compose(EventEmittable, { // objects become event emitter
methods: {
pushEvent(metric) {
this.emit('system-metric', metric);
return this.pushSystemMetric(metric);
}
}
});
const eventPusher = EventsPusher(); // THIS IS AN EVENT EMITTER
eventPusher.on('system-metric', function (data) {
console.log(data);
});
eventPusher.pushEvent({CPU: 30, MEM: 60}); // will log the data
// { CPU: 30, MEM: 60 }

@stamp/init-property stamp

If your stamp have a property which is also a stamp then the property will be replaced with an instance of that stamp.

import stampit from '@stamp/it';
import InitProperty from '@stamp/init-property';
import Oauth2 from './stamps/oauth2';
const GravatarClient = compose(InitProperty, {
properties: {
auth: Oauth2 // note the 'auth'
},
methods: {
getProfilePicture() {
const h = this.auth.getHttpHeader() // accessing this.auth
return fetch('gravatar.com', {
headers: { Authorization: h }
});
}
}
});
const gravatarClient = GravatarClient({
auth: { // Passing this data to the 'Oauth2' stamp !!!!!!
client_id: '1', client_secret: '2', grant_type: 'simple'
}
});

While creating the GravatarClient instance the dependecy property auth will br auto created. All you need is to make sure you pass the auth property to while creating the gravatarClient.

@stamp/privatize stamp

Probably the most requested feature. If you compose that stamp (aka behavior) to your other stamp then all the properties of object instances will not be accessible outside of your stamp methods. You can also hide methods.

import stampit from '@stamp/it';
import Privatize from '@stamp/privatize';
const Auth = stampit({
methods: {
getPassword() { return this.password; },
setPassword(pwd) { this.password = pwd; }
}
});
const AuthWithPrivateProperties = Auth.compose(Privatize);
const AuthWithPrivatePropertiesAndMethod =
AuthWithPrivateProperties.privatizeMethods('setPassword');
const auth1 = Auth();
// .setPassword() is available
auth1.setPassword('qwert1234');
// .password is available
console.log(auth1.password); // "qwer1234"
const auth2 = AuthWithPrivateProperties();
// .setPassword() is available
auth2.setPassword('qwert1234');
// .password is NOT available
console.log(auth2.password); // undefined
const auth3 = AuthWithPrivatePropertiesAndMethod();
// .setPassword() is NOT available
auth3.setPassword('qwert1234'); // Error: setPassword is undefined
// .password is NOT available too
console.log(auth3.password); // undefined

The Privatize stamp (aka behavior) creates a proxy object and wraps every non-private method.

@stamp/shortcut stamp

Handy shortcut static methods to simplify stamp composition.

import {
methods, props, deepProps, statics, deepStatics,
conf, deepConf, init, composers
} from '@stamp/shortcut';
const Stamp1 = methods({
method1() { }
});
const Stamp2 = props({
prop2: 2
});
const Stamp3 = init(function () {
console.log(3);
});
// etc

BUT! If you compose the Shortcut into your stamp then it will get (inherit, obtain, blend in) all the shortcut static functions.

import stampit from '@stamp/it';
import
Shortcut from '@stamp/shortcut';
const HasToken = stampit({
methods: {
getToken() { return 'my-security-token'; }
}
}).compose(Shortcut)'
const HasToken2 = HasToken
.methods({ myMethod() {} })
.props({ foo: 'foo' })
.deepProps({ deepFoo: { foo: 'foo 2' } })
.statics({ bar: 'bar' })
.deepStatics({ deepBar: { bar: 'bar 2' } })
.conf({ something: 'something' })
.deepConf({ deepSomething: { something: 'something' } })
.init((arg, {stamp, args, instance}) => { /* initializer */ })
.composers(({stamp, composables}) => { /* composer */ });

@stamp/it — the next version of stampit module

The @stamp/it module is almost identical to the original stampit module. The breaking changes are listed here.

@stamp/[NEXT] module

To be continued…

Submit your ideas here — https://github.com/stampit-org/stamp/issues

Have fun with stamps!