Skip to content

Commit

Permalink
Merge pull request #5 from Contargo/add-connector
Browse files Browse the repository at this point in the history
Add connector module.
  • Loading branch information
olle authored May 13, 2019
2 parents 744ac85 + 14c8231 commit f3abda0
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 0 deletions.
101 changes: 101 additions & 0 deletions src/connector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { signalFn } from "./signal";

/**
* Connectors
*
* A connector is a helper to simplify building signal circuits. Connectors
* give access to signals. By providing a connectors identifier it's possible
* to connect to a specific signal without the need of explicit interconnections
* in your code (loose coupling). Connectors can be created by passing a
* function that returns a connectors state. The connector will then take care
* of the signal creation and lifetime management for you.
*/

let registry = new Map();
let signalCache = new Map();

function cacheAndReturn(connectorId, signal) {
// remove freed signals from the cache
signal.onFree(() => signalCache.delete(connectorId));
signalCache.set(connectorId, signal);
return signal;
}

/**
* Returns a function that calls `fn` with a list of values extracted from the
* provided signals returned by `inputsFn`. Arguments passed to the returned
* function are transparently passed to `fn`. `inputsFn` is a function that
* returns one or many input signals.
*
* @example
*
* let fn = withInputSignals(
* () => signal("foo"),
* (s, arg) => s + arg,
* );
* fn("bar"); // "foobar"
*/
export function withInputSignals(inputsFn, fn) {
return (...args) => {
let inputs = inputsFn(...args);
let values = Array.isArray(inputs)
? inputs.map(s => s.value())
: inputs.value();
return fn(values, ...args);
};
}

/**
* Connects to the connector identified by `connectorId`. As a result, a signal
* is returned which can then be used to access a stream of values reactively
* changing over time.
*
* Note: for any given call to `connect` there must be a previous call to
* `connector`, registering a computation function for `connectorId`.
*/
export function connect(connectorId) {
if (signalCache.has(connectorId)) {
return signalCache.get(connectorId);
}
let connectorFn = registry.get(connectorId);
if (connectorFn) {
return cacheAndReturn(connectorId, connectorFn(connectorId));
}
console.warn("no connector registered for:", connectorId);
}

/**
* Registers a connector identified by `connectorId`. `connectorId` is a simple
* keyword. `computationFn` is a function which gets passed one argument,
* `connectorId` and must return the connectors state.
*
* The computation function is wrapped inside a signal, therefore the connector
* re-computes whenever a state change in any referenced input signal gets
* detected.
*/
export function connector(connectorId, computationFn) {
return rawConnector(connectorId, connectorId =>
signalFn(() => computationFn(connectorId)),
);
}

/**
* Registers a raw connector identified by `connectorId`. `connectorId` is a
* simple keyword. `connectorFn` is a function which gets one argument,
* `connectorId` and must return a `signalFn`.
*/
export function rawConnector(connectorId, connectorFn) {
if (signalCache.has(connectorId)) {
signalCache.delete(connectorId);
}
registry.set(connectorId, connectorFn);
return connectorFn;
}

/**
* Clears all registered connectors.
*/
export function clearConnectors() {
registry.clear();
signalCache.clear();
}
106 changes: 106 additions & 0 deletions src/connector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
clearConnectors,
connect,
connector,
rawConnector,
withInputSignals,
} from "./connector";
import { signal, signalFn } from "./signal";

global.console.warn = jest.fn();

beforeEach(() => clearConnectors());

describe("withInputSignals", () => {
it("injects extracted value from single input signal", () => {
let fn = withInputSignals(
() => signal("foo"),
(s, ...args) => s + args.join(""),
);
expect(fn("bar")).toBe("foobar");
});
it("injects extracted values from multiple input signals", () => {
let fn = withInputSignals(
() => [signal("foo"), signal("bar")],
([s1, s2], ...args) => s1 + s2 + args.join(""),
);
expect(fn("baz")).toBe("foobarbaz");
});
});

describe("connect", () => {
it("returns connector signal", () => {
connector("foo", () => "bar");
expect(connect("foo").value()).toBe("bar");
});
it("returns undefined for unknown connectors", () => {
expect(connect("bar")).toBeUndefined();
expect(global.console.warn).toHaveBeenCalledWith(
"no connector registered for:",
"bar",
);
});
it("caches signals from connectors", () => {
connector("foo", () => {});
const s1 = connect("foo");
const s2 = connect("foo");
expect(s1).toBe(s2);
});
it("removes freed signals from cache", () => {
connector("foo", () => {});
const s1 = connect("foo");
s1.free();
const s2 = connect("foo");
expect(s1).not.toBe(s2);
});
});

describe("connector", () => {
it("passes connector id to computation function", () => {
connector("bar", id => {
expect(id).toBe("bar");
return true;
});
expect(connect("bar").value()).toBe(true);
});
it("removes cached signals when overwriting", () => {
connector("foo", () => {});
const s1 = connect("foo");
connector("foo", () => {});
const s2 = connect("foo");
expect(s1).not.toBe(s2);
});
});

describe("rawConnector", () => {
it("transparently registers provided signal", () => {
let signal = signalFn(() => "foo");
rawConnector("foo", () => signal);
expect(connect("foo")).toBe(signal);
});
it("removes cached signals when overwriting", () => {
rawConnector("foo", () => signalFn(() => "foo"));
const s1 = connect("foo");
rawConnector("foo", () => signalFn(() => "foo"));
const s2 = connect("foo");
expect(s1).not.toBe(s2);
});
});

describe("clearConnectors", () => {
it("clears registered connectors", () => {
connector("foo", () => "bar");
expect(connect("foo")).toBeDefined();
clearConnectors();
expect(connect("foo")).toBeUndefined();
});
it("removes cached signals", () => {
connector("foo", () => {});
const s1 = connect("foo");
clearConnectors();
const s2 = connect("foo");
expect(s1).not.toBe(s2);
expect(s1).toBeTruthy();
expect(s2).toBeFalsy();
});
});
6 changes: 6 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export {
connect,
connector,
rawConnector,
withInputSignals,
} from "./connector";
export { signal, signalFn } from "./signal";
13 changes: 13 additions & 0 deletions src/signal.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
/**
* Signals
*
* A signal is a container for state information that changes over time.
* Signals can depend on other signals (inputs). By creating signals and putting
* them together you build a circuit of signals. State changes will be
* propagated through the signal circuit starting from the signal where the
* state change happened. The state change might force dependant signals to also
* change their state which then leads to state change propagation to their
* dependant signals in the circuit and so on. The propagation stops as soon as
* there are no more signals reacting to state changes.
*/

/**
* A signal is a container used to store state information. A signal can be made
* to change state by calling `reset` or `update`.
Expand Down

0 comments on commit f3abda0

Please sign in to comment.