@leverage/core
v4.1.4
Published
Core utilities for Leverage
Downloads
7
Readme
What is Leverage?
👩💻 A small, fast, flexible system to orchestrate your next application!
With Leverage you can easily create:
- An HTTP server
- A WebSocket server
- 👩💭 Anything else you can imagine!
Install
# Using NPM
npm install @leverage/core
# Using Yarn
yarn add @leverage/core
Examples
Just getting started with Leverage and want to learn from a real-world example? Take a look at these projects!
Documentation
Table Of Contents
Units
Leverage's job is to help wire together your application, installing things only once all dependencies are satisfied. To accomplish this, Leverage standardizes the "pieces" of an application into three different kinds:
- Plugin
- Component
- Service
Each unit contains a configuration object that specifies two properties. First, config.is
tells Leverage what kind of unit it is and can be either "plugin"
, "component"
, or "service"
. Second, config.type
tells Leverage what type this unit is for. The config.type
value can be any string value and serves as a "name" for a plugin and set of components.
Plugins
A Plugin is a unit that has an install()
method which is passed any added components of the same type as the plugin. For example, if you create a plugin where config.type
is "http"
, any components that also have config.type
set to "http"
will be passed to your plugin's install()
method.
Here is an example with a plugin that simply logs out the components it installs:
// index.js
import { add } from "@leverage/core";
import plugin from "./plugin";
import component from "./component";
add(plugin, component);
// plugin.js
import { useConfig } from "@leverage/core";
export const init = () => {
useConfig({
is: "plugin",
type: "example",
});
};
export const install = (component) => {
console.log("installing component: ", component);
// => { init: Function, myValue: 42 }
};
// component.js
import { useConfig } from "@leverage/core";
export const init = () => {
useConfig({
is: "component",
type: "example",
});
};
export const myValue = 42;
Components
A Component is a unit that is passed to a Plugin of the same type. Components serve as ways to configure the systems that Plugins create. For example, a component with the type "http"
may be used to configure a route for the HTTP server that the HTTP plugin manages. Here's an example of an HTTP component from @leverage/plugin-http
:
// index.js
import { add } from "@leverage/core";
import { http } from "@leverage/plugin-http";
import component from "./component";
add(http, component);
// component.js
import { useHTTP } from "@leverage/plugin-http";
export const init = () => {
useHTTP({
route: "/:name",
method: "get",
});
};
// The HTTP plugin calls `component.http` when a request comes in.
export const http = (req, res) => {
res.write(`Hello, ${req.params.name}!`);
res.end();
};
Services
A Service is a unit that has access to the same hooks and lifecycle that other units do, but is not directly connected to a Plugin or Component. Services are great for abstracting databases, creating state stores, or other things that don't fit into the Plugin/Component model. Here's an example of how you might create a database service:
// index.js
import { add } from "@leverage/core";
import { http } from "@leverage/plugin-http";
import component from "./component";
import service from "./service";
add(http, component, service);
// service.js
import db from "my-db-library";
import { useConfig, useKeyRef, useInstallEffect } from "@leverage/core";
export const init = () => {
useConfig({
is: "service",
type: "database",
});
const connectionRef = useKeyRef("connection");
useInstallEffect(() => {
db.connect((connection) => {
connectionRef.current = connection;
});
});
};
export const findUser = (name) => {
const connection = useKeyRef("connection");
// Database isn't ready yet!
if (!connection) return;
return connection.find("user", { name });
};
// component.js
import { useDependencies, useService } from "@leverage/core";
import { useHTTP } from "@leverage/plugin-http";
export const init = () => {
useHTTP({
route: "/:name",
method: "get",
});
useDependencies({
service: "database",
});
};
// The HTTP plugin calls `component.http` when a request comes in.
export const http = (req, res) => {
const db = useService("database");
const user = db.findUser(req.params.name);
res.json(user);
res.end();
};
Lifecycle
A Unit has two different parts to its lifecycle: uninitialized
and installed
.
Uninitialized
When a Unit is added to Leverage, it starts as uninitialized
. Leverage will immediately run the Unit's init
function. In the uninitialized
phase, a Unit is limited in what hooks it has access to. During this phase, a Unit can use the following hooks:
useConfig
useDependencies
useEvent
useHooks
useInstallEffect
useIs
useKeyRef
useType
useUnit
In order to successfully initialize, a Unit's init
function must configure the config.is
and config.type
properties using useConfig
, useIs
, or useType
.
Installed
Once all dependencies for a Unit are satisfied, a unit will be installed
by Leverage. During this phase, any install effects created with useInstallEffect
are run. Future useInstallEffect
calls will fire immediately. Additionally, plugins are passed components of the same type. During this phase, a Unit can use the following hooks:
useConfig
useDependencies
useEffect
useEvent
useHooks
useInstallEffect
useIs
useKeyRef
usePlugin
useRef
useService
useState
useType
useUnit
However, an install effect created with useInstallEffect
is limited to the following hooks:
useConfig
useDependencies
useEvent
useHooks
useIs
useKeyRef
usePlugin
useService
useType
useUnit
Hooks
Hooks are functions that can be used inside of a Unit's methods to interact with Leverage. Common examples of use cases are to:
- Configure a Unit
- Run a function after a Unit has been installed
- Manage Unit state
- Get an installed Plugin or Service
A Unit's exposed methods are automatically wrapped to support hooks. Any functions that are not exported will not be wrapped with hook support.
useConfig
The useConfig
hook can be used in two different ways. It can be used inside of an init
function to set or get the configuration. After a unit is initialized, useConfig
can be used to get the current configuration.
import { useConfig, useInstallEffect } from "@leverage/core";
export const init = () => {
/*
* Here, `useConfig` **sets** the configuration to...
*
* {
* "is": "component",
* "type": "http"
* }
*
* Then this new configuration is returned and stored in the
* `config` variable.
*/
const config = useConfig({
is: "component",
type: "http",
});
/*
* Here, `useConfig` updates the configuration to...
*
* {
* "is": "component",
* "type": "http",
* "http": {
* "route": "/",
* "method": "get"
* }
* }
*
* Then this new configuration is returned and stored in the
* `config` variable.
*/
const newConfig = useConfig({
http: {
route: "/",
method: "get",
},
});
/*
* Here, `useConfig` **gets** the configuration and stores it in the
* `currentConfig` variable.
*/
const currentConfig = useConfig();
useInstallEffect(() => {
/*
* Here, `useConfig` **gets** the configuration and stores it in
* the `finalConfig` variable.
*/
const finalConfig = useConfig();
});
};
export const http = () => {
/*
* Here, `useConfig` **gets** the configuration and stores it in the
* `config` variable.
*/
const config = useConfig();
};
useDependencies
The useDependencies
hook can be used in two different ways. It can be used inside of an init
function to set or get the dependencies. After a unit is initialized, useDependencies
can be used to get the current dependencies.
import { useDependencies, useInstallEffect } from "@leverage/core";
export const init = () => {
/*
* Here, `useDependencies` **sets** the configuration to...
*
* {
* "plugins": ["http"],
* "services": ["db"]
* }
*
* Then this new dependency configuration is returned and stored in
* the `dependencies` variable.
*/
const dependencies = useDependencies({
plugins: ["http"],
services: ["db"],
});
/*
* Here, `useDependencies` updates the configuration to...
*
* {
* "plugins": ["http", "websocket"],
* "services": ["db"]
* }
*
* Then this new dependency configuration is returned and stored in
* the `dependencies` variable.
*/
const newDependencies = useDependencies({
plugins: ["websocket"],
});
/*
* Here, `useDependencies` **gets** the dependencies and stores it in
* the `currentDependencies` variable.
*/
const currentDependencies = useDependencies();
useInstallEffect(() => {
/*
* Here, `useDependencies` **gets** the dependencies and stores it
* in the `finalDependencies` variable.
*/
const finalDependencies = useDependencies();
});
};
export const callback = () => {
/*
* Here, `useDependencies` **gets** the dependencies and stores it in the
* `dependencies` variable.
*/
const dependencies = useDependencies();
};
useEffect
The useEffect
hook can be used once a unit has been installed to run side effects
when the effect's dependencies change. The useEffect
hook also supports a cleanup
function which can be returned from the effect callback.
import { useEffect } from "@leverage/core";
export const init = () => {
/* ... */
};
export const callback = () => {
/*
* Here, `useEffect` will run on every call.
*/
useEffect(() => {
console.log("Hello, World!");
});
/*
* Here, `useEffect` will run exactly once for the life of this unit.
*/
useEffect(() => {
console.log("Hello, World!");
}, []);
/*
* Here, `useEffect` will run whenever `myValue` changes.
*/
const myValue = Math.random();
useEffect(() => {
console.log("Hello, World!");
}, [myValue]);
/*
* Here, `useEffect` will run once. When this unit is uninstalled or removed,
* the cleanup callback will be executed.
*/
useEffect(() => {
console.log("Hello, World!");
return () => {
console.log("Hello, Cleanup!");
};
}, []);
};
useHooks
The useHooks
hook can be used to wrap functions in order to use hook calls inside of them. This is most useful for plugins in order to allow for asynchronous actions that may lack context otherwise.
import { useConfig, useHooks } from "@leverage/core";
export const init = () => {
useConfig({
is: "plugin",
type: "example",
});
/*
* Calling `useHooks` gives us a `withHooks` helper that
* is bound to the current Unit. In this case, it is the
* example plugin.
*/
const withHooks = useHooks();
/*
* Here, we wrap a function using `withHooks`.
*/
const handleAsyncAction = withHooks(() => {
/*
* Hooks can safely be used within this function.
*/
const config = useConfig();
// => { is: "plugin", type: "example" }
});
doSomethingAsync().then(handleAsyncAction);
};
useInstallEffect
The useInstallEffect
hook can be used during initialization to run side effects
once the Unit is installed. The useInstallEffect
hook also supports a cleanup
function which can be returned from the effect callback.
This hook is particularly useful for one-time configuration, expensive setup processes, or interacting with other installed Units.
import { useInstallEffect } from "@leverage/core";
export const init = () => {
/*
* Here, `useInstallEffect` runs once this unit is installed.
* When this unit is uninstalled or removed, the cleanup callback
* will be executed.
*/
useInstallEffect(() => {
console.log("Hello, World!");
return () => {
console.log("Hello, Cleanup!");
};
});
};
useIs
The useIs
hook can be used in two different ways. It can be used inside of an init
function to set or get the configuration's is
value. After a unit is initialized, useIs
can be used to get the current configuration's is
value.
import { useIs, useInstallEffect } from "@leverage/core";
export const init = () => {
/*
* Here, `useIs` **sets** the configuration to...
*
* {
* "is": "component",
* }
*
* Then this new configuration's `is` value is returned and stored in the
* `is` variable.
*/
const is = useIs("component");
/*
* Here, `useIs` **gets** the configuration's `is` value and stores it in the
* `currentIs` variable.
*/
const currentIs = useIs();
useInstallEffect(() => {
/*
* Here, `useIs` **gets** the configuration's `is` value and stores it in
* the `finalIs` variable.
*/
const finalIs = useIs();
});
};
export const callback = () => {
/*
* Here, `useIs` **gets** the configuration's `is` and stores it in the
* `is` variable.
*/
const is = useIs();
};
useKeyRef
The useKeyRef
hook can be used to create or fetch an existing Ref for storing persistent data. This hook is particularly useful because it can be used during and after initialization.
import { useKeyRef, useInstallEffect } from "@leverage/core";
export const init = () => {
/*
* Here, `useKeyRef` creates a new ref using the key "my-key" and an initial
* value of 42. The returned Ref is an object that looks like:
*
* {
* current: 42
* }
*/
const ref = useKeyRef("my-key", 42);
/*
* Here, `useKeyRef` **gets** the Ref for "my-key". This is the same object as
* the variable `ref` holds.
*
* ref === sameRef
* //=> true
*/
const sameRef = useKeyRef("my-key");
useInstallEffect(() => {
/*
* Here, `useKeyRef` **gets** the Ref for "my-key".
*/
const stillSameRef = useKeyRef("my-key");
});
};
export const callback = () => {
/*
* Here, `useKeyRef` **gets** the Ref for "my-key".
*/
const ref = useKeyRef("my-key");
};
usePlugin
The usePlugin
hook can be used to get an installed Plugin. This hook can only be
used after initialization. In order to get a Plugin, the Plugin's type should be
added to the Unit's dependencies.
import { usePlugin, useInstallEffect } from "@leverage/core";
export const init = () => {
useDependencies({
plugins: ["http"],
});
useInstallEffect(() => {
/*
* Here, `usePlugin` gets the "http" plugin.
*/
const plugin = usePlugin("http");
});
};
export const callback = () => {
/*
* Here, `usePlugin` gets the "http" plugin.
*/
const plugin = usePlugin("http");
};
useRef
The useRef
hook can be used once a unit has been installed to create a Ref that can store persistent data.
import { useRef, useEffect } from "@leverage/core";
export const init = () => {
/* ... */
};
export const callback = () => {
/*
* Here, `useRef` creates or gets an existing Ref, using an initial value of 42.* * The returned Ref is an object that looks like:
*
* {
* current: 42
* }
*/
const ref = useRef(42);
};
useService
The useService
hook can be used to get an installed Service. This hook can only be
used after initialization. In order to get a Service, the Service's type should be
added to the Unit's dependencies.
import { useService, useInstallEffect } from "@leverage/core";
export const init = () => {
useDependencies({
services: ["db"],
});
useInstallEffect(() => {
/*
* Here, `useService` gets the "db" plugin.
*/
const service = useService("db");
});
};
export const callback = () => {
/*
* Here, `useService` gets the "http" plugin.
*/
const service = useService("db");
};
useEmitter
The useEmitter
hook can be used to get the event emitter object.
import { useEmitter } from "@leverage/core";
export const init = () => {
useConfig({
is: "plugin",
type: "example",
});
/*
* Here, `useEmitter` will get the emitter object.
*/
const emitter = useEmitter();
/*
* The emitter can then be used to handle events.
*/
emitter.emit("my-event");
const handler = () => {};
/*
* Listen to "my-event" events.
*/
emitter.on("my-event", handler);
/*
* Stop listening to "my-event" events.
*/
emitter.off("my-event", handler);
/*
* Listen to "my-event" only once.
*/
emitter.once("my-event", () => {});
};
useEvent
The useEvent
hook can be used to handle events.
import { useEvent } from "@leverage/core";
export const init = () => {
useConfig({
is: "plugin",
type: "example",
});
/*
* Here, `useEvent` will handle the "example:message" event.
*
* For Example:
*
* emit(
* "example:message",
* "Hello, World"
* )
*/
useEvent("example:configure", (message) => {
console.log(message);
//=> logs "Hello, World"
});
};
useState
The useState
hook can be used once a unit has been installed to store persistent state between calls. This hook should only be used in the primary method for your
Unit.
import { useState } from "@leverage/core";
export const init = () => {
/* ... */
};
export const callback = () => {
/*
* Here, `useEffect` will run on every call.
*/
const [value, setValue] = useState(42);
/*
* On the first call, the default value is used. On subsequent calls,
* the existing value is used.
*/
value === 42;
/*
* Calling the state setter will update the value for the _next_ call.
* This does **not** update `value` in-place.
*/
setValue(1337);
};
useType
The useType
hook can be used in two different ways. It can be used inside of an init
function to set or get the configuration's type
value. After a unit is initialized, useType
can be used to get the current configuration's is
value.
import { useType, useInstallEffect } from "@leverage/core";
export const init = () => {
/*
* Here, `useType` **sets** the configuration to...
*
* {
* "type": "http",
* }
*
* Then this new configuration's `type` value is returned and stored in the
* `type` variable.
*/
const type = useType("http");
/*
* Here, `useType` **gets** the configuration's `type` value and stores it in the
* `currentType` variable.
*/
const currentType = useType();
useInstallEffect(() => {
/*
* Here, `useType` **gets** the configuration's `type` value and stores it in
* the `finalType` variable.
*/
const finalType = useType();
});
};
export const callback = () => {
/*
* Here, `useType` **gets** the configuration's `type` and stores it in the
* `type` variable.
*/
const type = useType();
};
useUnit
The useUnit
hook can be used to get the current unit. This is mostly useful when you want a reference to the current unit to pass between boundaries. useUnit
can also be useful when you need to call the wrapped version of unit methods.
import { useConfig, useUnit } from "@leverage/core";
export const init = () => {
useConfig({
is: "plugin",
type: "example",
});
/*
* Here, `useType` gets the current unit.
*/
const self = useUnit("http");
/*
* Any exported members can be accessed.
*/
self.callback();
};
export const callback = () => {
/* ... */
};