-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from Contargo/add-connector
Add connector module.
- Loading branch information
Showing
4 changed files
with
226 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters