From f38b91cedaa0754286c641bea056450a70df8d63 Mon Sep 17 00:00:00 2001 From: Carles Escrig Royo Date: Sun, 4 Jun 2023 14:45:04 +0200 Subject: [PATCH] feat: allows to propagete providers named reset to its dependents --- README.md | 11 ++-- grunt/config/grunt-contrib-jasmine.json | 4 ++ src/Bottle/middleware.js | 29 +++++---- src/Bottle/provider.js | 12 ++-- src/Bottle/reset-providers.js | 40 ++++++++++-- src/Bottle/service.js | 6 ++ src/api.js | 2 + src/globals.js | 11 ++++ test/spec/provider.spec.js | 82 +++++++++++++++++++++++-- 9 files changed, 167 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9398681..b0356bf 100644 --- a/README.md +++ b/README.md @@ -381,11 +381,14 @@ Param | Type | Details **Provider** | *Function* | A constructor function that will be instantiated as a singleton. Should expose a function called `$get` that will be used as a factory to instantiate the service. #### resetProviders(names) -Param | Type | Details -:--------------------------|:-----------|:-------- -**names**
*(optional)*| *Array* | An array of strings which contains names of the providers to be reset. +Param | Type | Details +:------------------------------|:-----------|:-------- +**names**
*(optional)* | *Array* | An array of strings which contains names of the providers to be reset. +**propagate**
*(optional)*| *Boolean* | Propagate the reset to all providers that depend on the previous list. + +Used to reset providers for the next reference to re-instantiate the provider. -Used to reset providers for the next reference to re-instantiate the provider. If `names` param is passed, will reset only the named providers. +If `names` param is passed, will reset only the named providers. When reseting an specific list of providers, it is possible to also propagate the reset to providers that depend on those. #### register(Obj) #### container.$register(Obj) diff --git a/grunt/config/grunt-contrib-jasmine.json b/grunt/config/grunt-contrib-jasmine.json index a29fa9d..c0f339f 100644 --- a/grunt/config/grunt-contrib-jasmine.json +++ b/grunt/config/grunt-contrib-jasmine.json @@ -1,5 +1,9 @@ { "jasmine" : { + "options": { + "version": "3.8.0", + "noSandbox": true + }, "tests" : { "src" : "dist/bottle.js", "options" : { diff --git a/src/Bottle/middleware.js b/src/Bottle/middleware.js index 56c75de..8e41312 100644 --- a/src/Bottle/middleware.js +++ b/src/Bottle/middleware.js @@ -8,14 +8,24 @@ * @return void */ var applyMiddleware = function applyMiddleware(middleware, name, instance, container) { + var bottle = this; var descriptor = { configurable : true, - enumerable : true - }; - if (middleware.length) { - descriptor.get = function getWithMiddlewear() { - var index = 0; - var next = function nextMiddleware(err) { + enumerable : true, + get : function getWithMiddlewear() { + var captureTarget,serviceDependents, index, next; + if (bottle.capturingDepsOf.length) { + captureTarget = bottle.capturingDepsOf[bottle.capturingDepsOf.length - 1]; + serviceDependents = bottle.dependents[name] = bottle.dependents[name] || []; + if (serviceDependents.indexOf(captureTarget) === -1) { + serviceDependents.push(captureTarget); + } + } + if (!middleware.length) { + return instance; + } + index = 0; + next = function nextMiddleware(err) { if (err) { throw err; } @@ -25,11 +35,8 @@ var applyMiddleware = function applyMiddleware(middleware, name, instance, conta }; next(); return instance; - }; - } else { - descriptor.value = instance; - descriptor.writable = true; - } + }, + }; Object.defineProperty(container, name, descriptor); diff --git a/src/Bottle/provider.js b/src/Bottle/provider.js index 2b11aea..69b1454 100644 --- a/src/Bottle/provider.js +++ b/src/Bottle/provider.js @@ -28,9 +28,9 @@ var getWithGlobal = function getWithGlobal(collection, name) { * @return Bottle */ var createProvider = function createProvider(name, Provider) { - var providerName, properties, container, id, decorators, middlewares; + var bottle, providerName, properties, container, decorators, middlewares; - id = this.id; + bottle = this; container = this.container; decorators = this.decorators; middlewares = this.middlewares; @@ -55,14 +55,18 @@ var createProvider = function createProvider(name, Provider) { var provider = container[providerName]; var instance; if (provider) { + bottle.capturingDepsOf.push(name); // filter through decorators instance = getWithGlobal(decorators, name).reduce(reducer, provider.$get(container)); + bottle.capturingDepsOf.pop(); delete container[providerName]; delete container[name]; } - return instance === undefined ? instance : applyMiddleware(getWithGlobal(middlewares, name), - name, instance, container); + if (instance === undefined) { + return instance; + } + return applyMiddleware.call(bottle, getWithGlobal(middlewares, name), name, instance, container); } }; diff --git a/src/Bottle/reset-providers.js b/src/Bottle/reset-providers.js index 9775b1a..634021c 100644 --- a/src/Bottle/reset-providers.js +++ b/src/Bottle/reset-providers.js @@ -5,30 +5,58 @@ * @return void */ var removeProviderMap = function resetProvider(name) { + var parts = splitHead(name); + if (parts.length > 1) { + removeProviderMap.call(getNestedBottle.call(this, parts[0]), parts[1]); + } delete this.providerMap[name]; delete this.container[name]; delete this.container[name + PROVIDER_SUFFIX]; }; +/** + * Clears a reseted service from the dependencies tracker. + * + * @param String name + * @return void + */ +var removeFromDeps = function removeFromDeps(name) { + var parts = splitHead(name); + if (parts.length > 1) { + removeFromDeps.call(getNestedBottle.call(this, parts[0]), parts[1]); + } + Object.keys(this.dependents).forEach(function clearDependents(serviceName) { + if (this.dependents[serviceName]) { + this.dependents[serviceName] = this.dependents[serviceName] + .filter(function (dependent) { return dependent !== name; }); + } + }, this); +}; + /** * Resets providers on a bottle instance. If 'names' array is provided, only the named providers will be reset. * * @param Array names + * @param Boolean [propagate] * @return void */ -var resetProviders = function resetProviders(names) { - var tempProviders = this.originalProviders; +var resetProviders = function resetProviders(names, propagate) { var shouldFilter = Array.isArray(names); - Object.keys(this.originalProviders).forEach(function resetProvider(originalProviderName) { if (shouldFilter && names.indexOf(originalProviderName) === -1) { return; } - var parts = originalProviderName.split(DELIMITER); + var parts = splitHead(originalProviderName); if (parts.length > 1) { - parts.forEach(removeProviderMap, getNestedBottle.call(this, parts[0])); + resetProviders.call(getNestedBottle.call(this, parts[0]), [parts[1]], propagate); + } + if (shouldFilter && propagate && this.dependents[originalProviderName]) { + this.resetProviders(this.dependents[originalProviderName], propagate); + } + if (shouldFilter) { + removeFromDeps.call(this, originalProviderName); } removeProviderMap.call(this, originalProviderName); - this.provider(originalProviderName, tempProviders[originalProviderName]); + this.provider(originalProviderName, this.originalProviders[originalProviderName]); }, this); }; diff --git a/src/Bottle/service.js b/src/Bottle/service.js index a2ffe09..5c8db59 100644 --- a/src/Bottle/service.js +++ b/src/Bottle/service.js @@ -8,6 +8,12 @@ var createService = function createService(name, Service, isClass) { var deps = arguments.length > 3 ? slice.call(arguments, 3) : []; var bottle = this; + deps.forEach(function registerDependents(otherService) { + var serviceDependents = bottle.dependents[otherService] = bottle.dependents[otherService] || []; + if (serviceDependents.indexOf(name) === -1) { + serviceDependents.push(name); + } + }); return factory.call(this, name, function GenericFactory() { var serviceFactory = Service; // alias for jshint var args = deps.map(getNestedService, bottle.container); diff --git a/src/api.js b/src/api.js index 9e846f6..4464348 100644 --- a/src/api.js +++ b/src/api.js @@ -11,7 +11,9 @@ Bottle = function Bottle(name) { this.id = id++; + this.capturingDepsOf = []; this.decorators = {}; + this.dependents = {}; this.middlewares = {}; this.nested = {}; this.providerMap = {}; diff --git a/src/globals.js b/src/globals.js index 47cb291..027bbe0 100644 --- a/src/globals.js +++ b/src/globals.js @@ -69,3 +69,14 @@ var getNestedBottle = function getNestedBottle(name) { var getNestedService = function getNestedService(fullname) { return fullname.split(DELIMITER).reduce(getNested, this); }; + +/** + * Split a dot-notation string on head segment and rest segment. + * + * @param String fullname + * @return Array + */ +var splitHead = function splitHead(fullname) { + var parts = fullname.split(DELIMITER); + return parts.length > 1 ? [parts[0], parts.slice(1).join(DELIMITER)] : [parts[0]]; +}; diff --git a/test/spec/provider.spec.js b/test/spec/provider.spec.js index 5efdeb6..51e57c0 100644 --- a/test/spec/provider.spec.js +++ b/test/spec/provider.spec.js @@ -189,16 +189,88 @@ expect(i).toEqual(2); expect(j).toEqual(1); }); - it('allows for sub containers to re-initiate as well', function() { + it('allows to propagate the reset to the providers that depend on the selected service names', function() { + var i = 0; + var j = 0; + var k = 0; + var l = 0; + var b = new Bottle(); + var FirstService= function() { i = ++i; }; + var SecondProvider = function() { + j = ++j; + this.$get = function(container) { return { _first: container.First }; }; + }; + var ThirdFactory = function(container) { k = ++k; return { _first: container.First }; }; + b.factory('Zero', function () { l = ++l; return 0; }); + b.service('First', FirstService, 'Zero'); + b.provider('Second', SecondProvider); + b.factory('Third', ThirdFactory); + expect(b.container.Second._first instanceof FirstService).toBe(true); + expect(b.container.Third._first instanceof FirstService).toBe(true); + expect(i).toEqual(1); + expect(j).toEqual(1); + expect(k).toEqual(1); + expect(l).toEqual(1); + b.resetProviders(['First'], true); + expect(b.container.Second._first instanceof FirstService).toBe(true); + expect(b.container.Third._first instanceof FirstService).toBe(true); + expect(i).toEqual(2); + expect(j).toEqual(2); + expect(k).toEqual(2); + expect(l).toEqual(1); + b.resetProviders(['Zero'], true); + expect(b.container.Second._first instanceof FirstService).toBe(true); + expect(b.container.Third._first instanceof FirstService).toBe(true); + expect(i).toEqual(3); + expect(j).toEqual(3); + expect(k).toEqual(3); + expect(l).toEqual(2); + }); + it('will cleanup service dependents if a service is redefined', function() { + var i = 0; + var j = 0; + var k = 0; + var b = new Bottle(); + var FirstService= function() { i = ++i; }; + var SecondService = function() { j = ++j; }; + var ThirdService = function() { k = ++k; }; + b.service('First', FirstService); + b.service('Thing.Second', SecondService, 'First'); + b.service('Third', ThirdService, 'Thing.Second'); + expect(b.container.First instanceof FirstService).toBe(true); + expect(b.container.Thing.Second instanceof SecondService).toBe(true); + expect(b.container.Third instanceof ThirdService).toBe(true); + expect(i).toEqual(1); + expect(j).toEqual(1); + expect(k).toEqual(1); + b.resetProviders(['Thing.Second'], true); + // Redefined to have no dependencies + b.service('Thing.Second', SecondService); + expect(b.container.First instanceof FirstService).toBe(true); + expect(b.container.Thing.Second instanceof SecondService).toBe(true); + expect(b.container.Third instanceof ThirdService).toBe(true); + expect(i).toEqual(1); + expect(j).toEqual(2); + expect(k).toEqual(2); + // No propagation will happen given that no service depends on First anymore + b.resetProviders(['First'], true); + expect(b.container.First instanceof FirstService).toBe(true); + expect(b.container.Thing.Second instanceof SecondService).toBe(true); + expect(b.container.Third instanceof ThirdService).toBe(true); + expect(i).toEqual(2); + expect(j).toEqual(2); + expect(k).toEqual(2); + }); + it('allows for deep sub containers to re-initiate as well', function() { var i = 0; var b = new Bottle(); var ThingProvider = function() { i = ++i; this.$get = function() { return this; }; }; - b.provider('Thing.Something', ThingProvider); - expect(b.container.Thing.Something instanceof ThingProvider).toBe(true); + b.provider('Thing.In.Something', ThingProvider); + expect(b.container.Thing.In.Something instanceof ThingProvider).toBe(true); // Intentionally calling twice to prove the construction is cached until reset - expect(b.container.Thing.Something instanceof ThingProvider).toBe(true); + expect(b.container.Thing.In.Something instanceof ThingProvider).toBe(true); b.resetProviders(); - expect(b.container.Thing.Something instanceof ThingProvider).toBe(true); + expect(b.container.Thing.In.Something instanceof ThingProvider).toBe(true); expect(i).toEqual(2); }); it('will not break if a nested container has multiple children', function() {