Joki is an publish-subscriber and service provider library. Reasons for writing this can be found at the of the this Readme file from the chapter Why?!.
Joki has no runtime dependencies, but it should be used from within babel compiled code.
Joki is currently used in production, but is still considered to be in early stages of development. So use at your own risk. =)
A simple example project can be found from joki-react-simple-example project.
By using npm
npm i joki
Or if you prefer yarn
yarn add joki
Joki exports two functions. First one is called createJoki
and it is used to create an actual instance of Joki. The second one is createMockService which is used mainly for testing. Variable called ìdentifier
is also exported and it contains the current version of Joki.
Creating a new Joki instance is a simple process
import { createJoki } from 'joki';
const jokiInstance = createJoki();
The createJoki
function takes one optional object as a parameter that contains the options set for this instance.
example:
{
debug: boolean, // defaults to false
debugWithWarn: boolean, // defaults to false
noInit: boolean, // defaults to undefined (identical to setting false),
noLongKey: boolean // defaults to false
}
When debug is set to true, Joki will start printing information to the console about's it internal processes.
When debugWithWarn is set to true, Joki will print to the console with console.warn instead of console.debug (NEW in 0.9.3)
When noInit is set to true, Joki will not call initialization Event to services automatically.
When noLongKey is set to true, Joki will not add a longKey
argument to each joki event.
At the base of the joki there is an old concept of publish-subscribe pattern and it could be used just like simple message queue with commands on
and trigger
. Joki also has a concept of specialized subscriber called service to make writing application logic more undestandable and coherent. The events inside joki are JavaScript Objects and referred in this documentation as Joki Events.
Joki events can be sent with three separate functions: trigger
, ask
and broadcast
.
const jokiEvent = {
key: "myEventKey",
to: "targetOnlyThisService",
from: "whoIsSendingThisMessage",
debug: true|false
}
The main parameter of the Joki Event is key. It is the name of the triggered event that subscibers are listening. It must be a string or an array of strings if multiple subscribers are to be triggered at the same time.
The value of parameter to must be an id of a registered service. If the to parameter is present in the event Joki sends this event directly to this service and does not trigger other subsribers or services. This parameter can also be an array of strings and the event is sent all those services.
The from parameter is mandatory when sending broadcasts. It should alse be attached to events triggered from within services and have the id of the service triggering the event.
NEW in 0.9.3 The debug parameter is optional and can be added to a single event. This will activate debugging for this event only.
Other parameters can be added to the event object too, but most of the data should be wrapped within a parameter called body or something similar as some parts of the Joki will modify the root event object.
NOTICE! The parameters syncAsk, longKey and broadcast are also reserved as they are used by Joki internally.
Since 0.9.2 Joki will add a parameter longKey to each event it recieves. This parameter is a combination of broadcast, to, from and key parameters, that makes writing event handlers on services easier as they can only compare one value.
LongKey has 4 parts in it: From>Broadcast>To:Key
, but parts that are not part of the event are also missing from the long key. Here
are some examples:
eventObject | longKey |
---|---|
{to: "Service", key: "test" } | Service:test |
{from: "Suite", to: "Service", key: "test" } | Suite>Service:test |
{from: "Suite", broadcast: true, key: "test"} | Suite>BC>test |
As the process of creating this key for each event is not without some cpu cost, this feature can also be turned of by setting the noLongKey
parameter to true
when initializing Joki instance.
The joki instance has the following api calls:
on
- Subscribe to events triggered in Jokitrigger
- Trigger an event inside Jokiask
- Trigger an event and expect a return value from itbroadcast
- Trigger a broadcast event to all services and to specific events listening for these broadcastsaddService
- Register a service to the JokiremoveService
- Remove a service from the JokilistServices
- List all services currently registered to this Joki instanceinitServices
- Send initialization call to all registered ServicesonInitialize
- Set a callback(s) to be triggered after services has been initialized (NEW in 0.9.3)listeners
- List all listeners currently subscribed to this Joki instanceoptions
- list, get or set options for this joki Instance
All examples below will expect that an instance of Joki called jokiInstance
has been created with createJoki
function.
const unsubscribeFn = jokiInstance.on(Object);
Create a new subcriber listening to events.
const off = jokiInstance.on({
key: "listenTothisKey",
from: "listenEventsFromThisSource",
fn: (event) => { console.log("Do stuff"); }
});
off();
The on
function will add a new subscriber to the Joki Instance and returns an anonymous function that can be used to unsubscribe it.
The object used to defined the subscriber has three parameters:
- key - String || Array[String] - Listen to Joki events with this key
- from - String - Only trigger this event if the from of the event is defined and equals to this.
- fn - function - This function is triggered with the triggering Joki Event as an argument.
The subscriber must always have the fn defined. Either or both key or from must be defined. When only key is defined, all Joki Events with an identical key will trigger this subscriber. If only from is defined for the subscriber, all Joki Events with identical from will trigger this Subscriber. If both are defined, only Joki Events matching with both key and from will trigger.
The normal use case for from is to capture all events triggered by a defined service.
The callback function only takes one argument and it is the Joki Event that was triggered.
jokiInstance.trigger(JokiEventObject);
Trigger subscribers or services with Joki Events without return value.
example:
jokiInstance.trigger({
key: "somethingHappened",
body: {
/// my data
}
});
Check the Joki Event chapter above for more information about the format of the Joki Event Object
NOTICE! If parameter to is defined, no subscribers are triggered, only the target service or services. And if no to is defined, no services are triggered.
const results = jokiInstance.ask(JokiEventObject); // synchronous ask
jokiInstance.ask(JokiEventObject).then(results => .... ); // asynchronous ask
Trigger event and expect a return value.
example:
jokiInstance.ask({
key: "somethingHappened",
body: {
/// my data
}
})
.then( results => {
// Do stuff
})
.catch(err => {
// On err
});
ask
is a wrapper for trigger
that is used when a return values is expected. The Joki Event object is extended with a key syncAsk, which defaults to false. When syncAsk is false the ask
function return a promise which will trigger when all services or subscribers have executed their handlers. If the syncAsk is set to true, this is done synchronously and the execution of the program will wait until for handlers to finish.
The return value will be an object when the Joki Event had a parameter to defined. For example if we call two services called alpha and beta:
jokiInstance.ask({
to: ["alpha", "beta"],
key: "doStuff"
}).then(response => .... )
The response object will wrap the response of each service with the serviceId as a key:
{
alpha: Return value from alpha service,
beta: Return value from beta service
}
When calling subscribers the return value will be an array of responses from subscribers that triggered.
jokiInstance.broadcast(JokiEventObject);
Send an event to all services and subscribers listening to this key.
example:
jokiInstance.broadcast({
key: "somethingVeryImportant",
from: "ImportantService",
body: {
// my data
}
});
broadcast
will trigger every service that has been registered and all subscribers listening to this key. The from parameter is mandatory in broadcast events.
The broadcast
is separated from the trigger
so that the user cannot make broadcast events without actually writing broadcast :). And of course because this allows the broadcast function itself to be a lot simpler and thus faster.
NOTICE! broadcast
will add a parameter broadcast to the Joki Event Object sent to services and subscribers so that they know that this event is actually a broadcast and treat it accordingly.
jokiInstance.addService(ServiceObject);
Registers a new service to Joki instance.
jokiInstance.addService({
id: "MyService",
fn: (jokiEvent) => {
switch(jokiEvent.key) {
// Do stuff
}
}
});
The service object has two paramters: id containing an unique string id for the service and fn containing the event handler function.
The id should always be a desciptive name for the service as it is used by the Joki Events to parameter and thus makes the event triggers a lot more descriptive and easier to follow.
The service itself can be a class or function, Joki library itself does not care.
jokiInstance.removeService(serviceId);
Removes the service from the Joki instance.
jokiInstance.removeService("MyService");
Services are rarely removed from Joki after initialization unless they have been created dynamically and temporarily.
const arrayOfServiceIds = jokiInstance.listServices();
Returns an array of registered service id's.
jokiInstance.initServices(dataObject);
Sends an initialization event to all registered services.
The initialization event looks like this:
{
from: "JOKI",
key: "initialize",
body: dataObject,
}
This event can be used to do initialization of services after for example authorization is finished.
NOTICE! If the service is added after the initialization is done, the initialization is called on it immediately. This can be prevented by setting the option noInit to true.
jokiInstance.onInitialize(functionCallBack)
Register a callback function that is executed when the initServices is called. The Services are initialized first and then these callbacks are executed. The idea here is to allow for example setting some dafault data to a services, without changing the service itself. The same dataObject that is sent to services, is also sent to each callback.
jokiInstance.onInitialize(data => {
console.log("Initialization executed with data", data.init === true);
});
jokiInstance.initServices({init: true});
const arrayOfListeners = jokiInstance.listners();
List all subscribers in this Joki instance.
The return value is an array of objects where each object contains each subscribers key and from values.
Getting a list of currently subscribers is mainly usuful only for debugging and testing purposes and should not really be used in production code for anything.
list: const optionsObject = jokiInstance.options();
get: const optionValue = jokiInstance.options(optionKeyString);
set: jokiInstance.options(optionKeyString, newValue);
List, get or set option values for the current Joki instance.
createMockService
factory function is used to create a simple services for Joki. It's main purpose is to provide a simple tool for mocking services with simple responses to events in tests.
NOTICE! This function may be renamed to something like createSimpleService
or createStaticService
if it is found to be good enough for those purposes. Although at the moment the plan is to create separate functions for these functionalities.
Example:
import { createJoki, createMockService } from 'joki';
const jokiInstance = createJoki();
createMockService(jokiInstance, "MockService", {
"getDetails": { id: "id1", name: "Name"},
"getList": [{id: "id1", name: "Name"}],
"isValid": (event) => event.body === true,
"send": (event) => { joki.trigger({key: "hey", from: "MockService", body: "foobar"})}
});
The 'createMockService' takes 3 arguments.
- jokiInstance - This must be a valid instance of Joki.
- serviceId - Service Id must a be a unique string
- eventHandlers - This is an object where keys equal to event keys and the value is either returned as is or it can be a function.
If the eventHandlers value is a function that returns a value or static value a ServiceUpdate function is sent by the Mock Service.
The idea for Joki has been floating in my mind for quite a while. A microservice inspired state container for React, which would allow React to function as a view library and separating the actual application logic and state to separate services. When React Hooks were released it allowed an easier lifecycle handling.
After using Redux (with different friends), Mobx and Mobx-State-Tree I found them good when the focus was in the user interface itself and will keep using them in the future. But Idea of Joki was to solve more data oriented problem, where asynchronous calls and sockets to backend services are even more common than clicks on the page. Where state updates will be triggered not by user but by events from the server. Only visible thing of these sites to public web is usually a login page, if even that.
Joki allows us to write services that do their stuff and keep their state contained within themselves. It allows us to dynamically change services under the same id for example depending on the users access rights etc.
MIT