Skip to content

Commit 58a70af

Browse files
Merge pull request #3 from pulsar-edit/decaf-and-add-consumer-grouping
Decaf and add consumer grouping
2 parents e012c70 + 48f7bf2 commit 58a70af

File tree

9 files changed

+282
-132
lines changed

9 files changed

+282
-132
lines changed

spec/service-hub-spec.coffee

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,36 @@ describe "ServiceHub", ->
4848
hub.provide "b", "1.0.0", z: 3
4949
expect(services).toEqual [{x: 1}, {y: 2}]
5050

51+
it "invokes the callback with the newest version of a service provided in a given batch when 'consume' is called with an object", ->
52+
hub.provide "a",
53+
"1.0.0": {w: 1}
54+
"1.1.0": {x: 2}
55+
hub.provide "a",
56+
"1.2.0": {y: 3}
57+
hub.provide "b",
58+
"1.0.0": {z: 4}
59+
hub.provide "a",
60+
"1.1.0": { should_not_be_used: 999 },
61+
"1.2.0": { v: 0 }
62+
hub.provide "a",
63+
"1.0.0": { w: -1 }
64+
65+
services100 = []
66+
services110 = []
67+
services120 = []
68+
69+
hub.consume "a", {
70+
'1.0.0': (s) -> services100.push(s),
71+
'1.1.0': (s) -> services110.push(s),
72+
'1.2.0': (s) -> services120.push(s)
73+
}
74+
75+
# The first and third `a` providers should fulfill with only the higher
76+
# of the two services they expose.
77+
expect(services100).toEqual([{ w: -1 }])
78+
expect(services110).toEqual([{ x: 2 }])
79+
expect(services120).toEqual([{ y: 3 }, { v: 0 }])
80+
5181
it "can specify a key path that navigates into the contents of a service", ->
5282
hub.provide "a", "1.0.0", b: c: 1
5383
hub.provide "a", "1.0.0", d: e: 2

src/consumer.coffee

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/consumer.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const {Range} = require('semver');
2+
3+
class Consumer {
4+
constructor(keyPath, versionRange, callback) {
5+
this.keyPath = keyPath;
6+
7+
let versionConsumerObj;
8+
if (typeof versionRange === 'object') {
9+
// If the second argument is not a string, it must be an object of
10+
// version/function pairs, and the third argument must be omitted.
11+
if (typeof callback === 'function') {
12+
throw new TypeError('versionRange must be a string or an object of version/function pairs');
13+
}
14+
versionConsumerObj = versionRange;
15+
} else {
16+
versionConsumerObj = { [versionRange]: callback };
17+
}
18+
19+
// A consumer can expose multiple version ranges and multiple callback
20+
// functions. When matching up a single `Provider` to a single `Consumer`,
21+
// only the best version match (the highest version that satisfies at least
22+
// one version range) will be used.
23+
this.versionRangeMap = new Map();
24+
for (let [versionRange, callback] of Object.entries(versionConsumerObj)) {
25+
let range = new Range(versionRange);
26+
this.versionRangeMap.set(range, callback);
27+
}
28+
}
29+
30+
// If the consumer defines several version ranges, some of which may overlap,
31+
// the first range we encounter that matches the given version will be used.
32+
//
33+
// Since a consumer can list version ranges of arbitrary complexity, we
34+
// cannot choose the "highest" version range, nor can we specify a logical
35+
// order in which the version ranges are visited. Theoretically, they should
36+
// be enumerated in the order they're defined, but this is not a guarantee
37+
// made by the language. (Enumeration order is guaranteed for the map, but
38+
// not for the bare object we used to create the map.)
39+
//
40+
// Thus it's up to the developer to structure these version ranges to rule
41+
// out overlaps.
42+
callbackForVersion (targetVersion) {
43+
for (let [range, callback] of this.versionRangeMap) {
44+
if (!range.test(targetVersion)) continue;
45+
return callback;
46+
}
47+
return undefined;
48+
}
49+
}
50+
51+
module.exports = Consumer;

src/helpers.coffee

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/helpers.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
function getValueAtKeyPath (object, keyPath) {
3+
const keys = splitKeyPath(keyPath);
4+
for (var key of keys) {
5+
object = object[key];
6+
if (object == null) { return; }
7+
}
8+
return object;
9+
}
10+
11+
function setValueAtKeyPath (object, keyPath, value) {
12+
const keys = splitKeyPath(keyPath);
13+
while (keys.length > 1) {
14+
var key = keys.shift();
15+
object[key] ??= {};
16+
object = object[key];
17+
}
18+
object[keys.shift()] = value;
19+
}
20+
21+
function splitKeyPath (keyPath) {
22+
if (keyPath == null) { return []; }
23+
let startIndex = 0;
24+
const keys = [];
25+
for (let i = 0; i < keyPath.length; i++) {
26+
var char = keyPath[i];
27+
if ((char === '.') && ((i === 0) || (keyPath[i-1] !== '\\'))) {
28+
keys.push(keyPath.substring(startIndex, i));
29+
startIndex = i + 1;
30+
}
31+
}
32+
keys.push(keyPath.substr(startIndex, keyPath.length));
33+
return keys;
34+
}
35+
36+
module.exports = {
37+
getValueAtKeyPath,
38+
setValueAtKeyPath
39+
};

src/provider.coffee

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/provider.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const {gt, SemVer} = require('semver');
2+
const {CompositeDisposable} = require('event-kit');
3+
4+
const {getValueAtKeyPath, setValueAtKeyPath} = require('./helpers');
5+
6+
class Provider {
7+
constructor(keyPath, servicesByVersion) {
8+
this.consumersDisposable = new CompositeDisposable;
9+
this.servicesByVersion = {};
10+
this.versions = [];
11+
12+
for (let version in servicesByVersion) {
13+
let service = servicesByVersion[version];
14+
this.servicesByVersion[version] = {};
15+
this.versions.push(new SemVer(version));
16+
setValueAtKeyPath(this.servicesByVersion[version], keyPath, service);
17+
}
18+
19+
this.versions.sort((a, b) => b.compare(a));
20+
}
21+
22+
provide(consumer) {
23+
// A consumer can specify multiple version ranges, each with its own
24+
// callback. It's up to us to (a) find all the versions we advertise that
25+
// match up with versions that the consumer can consume, then (b) choose
26+
// the highest version number among these.
27+
let highestVersion;
28+
let highestVersionCallback;
29+
let highestVersionValue;
30+
31+
for (let version of this.versions) {
32+
let callback = consumer.callbackForVersion(version);
33+
if (!callback) continue;
34+
35+
// This version matches. Does the service name match?
36+
let value = getValueAtKeyPath(this.servicesByVersion[version.toString()], consumer.keyPath);
37+
if (!value) continue;
38+
39+
let isHighestVersion = !highestVersion || gt(version, highestVersion);
40+
if (!isHighestVersion) continue;
41+
42+
highestVersion = version;
43+
highestVersionCallback = callback;
44+
highestVersionValue = value;
45+
}
46+
47+
if (!highestVersionCallback) return;
48+
49+
let consumerDisposable = highestVersionCallback.call(null, highestVersionValue);
50+
51+
if (typeof consumerDisposable?.dispose === 'function') {
52+
this.consumersDisposable.add(consumerDisposable);
53+
}
54+
}
55+
56+
destroy() {
57+
this.consumersDisposable.dispose();
58+
}
59+
}
60+
61+
module.exports = Provider;

src/service-hub.coffee

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)