-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathServiceContainer.coffee
272 lines (218 loc) · 8.27 KB
/
ServiceContainer.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
define [
'cord!errors'
'cord!utils/Future'
'cord!services/Logger'
'underscore'
], (errors, Future, Logger, _) ->
class ServiceDefinition
constructor: (@name, @deps, @factory, @serviceContainer) ->
class ServiceContainer
# unique identifier of the serviceContainer
_uid = null
constructor: ->
# registered definitions. Keys are name, values are instance of ServiceDefinition
@_definitions = {}
# already resolved services. Keys are name of service
@_instances = {}
# futures of already scheduled service factories. Keys are name, values are futures
@_pendingFactories = {}
@logger = new Logger(this)
@set 'logger', @logger
isDefined: (name) ->
###
Is service defined
@param string path - Service name
@return bool
###
_(@_definitions).has(name) or _(@_instances).has(name)
reset: (name, resetDependants = true) ->
###
Reset service.
@return {Future<undefined>}
###
if @_instances[name] instanceof ServiceContainer
return
# reset pending service only after instantiation
if _(@_pendingFactories).has(name)
@_pendingFactories[name].clear()
delete @_pendingFactories[name]
delete @_instances[name]
@resetDependants(name) if resetDependants
Future.resolved()
resetDependants: (name) ->
###
Reset those who are dependant on this service
@param string name - service name
###
if not @_definitions[name]
return
for key, definition of @_definitions when name in definition.deps
@reset(key)
return
clearServices: ->
###
Clear and reset all services
###
for serviceName in @getNames()
if @isReady(serviceName)
@getService(serviceName).then (service) ->
service.clear?() if _.isObject(service)
@reset(serviceName)
set: (name, instance) ->
@reset(name)
@_instances[name] = instance
@_pendingFactories[name] = Future.resolved(instance)
this
getNames: ->
###
Get all defined services
@return array
###
_.union(_(@_definitions).keys(), _(@_instances).keys())
allInstances: ->
###
This method returns map with all initialized instances. Name of services are in keys
###
_.clone(@_instances)
def: (name, deps, factory) ->
###
Registers a new service definition in serviceContainer
###
if _.isFunction(deps) and factory == undefined
factory = deps
deps = []
@_definitions[name] = new ServiceDefinition(name, deps ? [], factory, this)
eval: (name, readyCb) ->
###
Evaluates a service, on ready call
Deprecated, please use getService()!
###
@getService(name)
.then (instance) ->
readyCb?(instance)
return # Avoid to possible Future result of readyCb
.catch (e) -> throw new Error("Eval for service `#{name}` failed with #{e}")
getService: (name) ->
###
Returns service by it's name. Like `eval` but promise-like.
@param {String} serviceName
@return {Future[Any]}
###
return @_pendingFactories[name] if _(@_pendingFactories).has(name)
# Call a factory for a service
if not _(@_definitions).has(name)
return Future.rejected(new errors.ConfigError("There is no registered definition for called service '#{name}'"))
def = @_definitions[name]
localPendingFactoy = @_pendingFactories[name] = Future.single("Factory of service \"#{name}\"")
# Ensure, that all of dependencies are loaded before factory call
Future.all(def.deps.map((dep) => @getService(dep)), "Deps for `#{name}`").then (services) =>
# call a factory with 2 parameters, get & done. On done resolve a result.
deps = _.object(def.deps, services)
get = (depName) ->
if _(deps).has(depName)
deps[depName]
else
throw new Error("Service #{depName} is not loaded yet. Did you forget to specify it in `deps` section of #{name}?")
locked = false
done = (err, instance) =>
try
throw new Error('Done was already called') if locked
locked = true
if err?
throw err
else if instance instanceof Error
throw instance
else
@_instances[name] = instance
localPendingFactoy.resolve(instance)
catch err
localPendingFactoy.reject(err)
return # we should not return future from this callback!
res =
try
def.factory(get, done)
catch factoryError
factoryError
if res instanceof Error
done(res)
else if def.factory.length < 2
###
If function arguments length 1 or 0, it means that function does not uses done() function inside.
In this case function creates service in sync mode, or returns Future object.
###
if res instanceof Future
res
.then (instance) => done(null, instance)
.catch (error) => done(error)
else
done(null, res)
.catch (e) =>
localPendingFactoy.reject(e)
return # we should not return rejected future from this callback!
localPendingFactoy.catch (e) =>
# Remove rejected factory from map
delete @_pendingFactories[name]
throw e
isReady: (name) ->
_(@_instances).has(name)
get: (name) =>
###
Gets service in a synchronized mode. This method passed as `get` callback to factory of service def
###
if not @isReady(name)
throw new errors.ConfigError("Service #{name} is not loaded yet. Did you forget to specify it in `deps` section?")
@_instances[name]
injectServices: (target) ->
###
Injects services from the service container into the given target object using @inject property of the object's
class, containing array of service names need to be injected. Services are injected as a object properties with
the relevant name.
@param Object target the instance to be injected to
@return Future completed when all services asyncronously loaded and assigned into the target object
###
injectFutures = []
if target.constructor.inject
if _.isFunction target.constructor.inject
services = target.constructor.inject()
else
services = target.constructor.inject
injectService = (serviceAlias, serviceName) =>
if @isDefined(serviceName)
injectFutures.push(
@getService(serviceName)
.then (service) =>
target[serviceAlias] = service
return
.nameSuffix("Inject #{serviceName} to #{target.constructor.name}")
)
else
@logger.warn "Container::injectServices #{ serviceName } for target #{ target.constructor.name } is not defined" if global.config?.debug.service
if _.isArray services
for serviceName in services
injectService serviceName, serviceName
else
for serviceAlias, serviceName of services
injectService serviceAlias, serviceName
Future.all(injectFutures, "Container::injectServices(#{target.constructor.name})").then -> target
autoStartServices: (services) ->
###
Auto-starts services from the given list which has `autoStart` flag enabled.
Returns immediately, doesn't wait for the services.
@param {Object} services Service description map from the bundle configs.
###
for serviceName, info of services when info.autoStart
do (serviceName) =>
@getService(serviceName).catch (error) =>
if not (error instanceof errors.AuthError) and
not (error instanceof errors.ConfigError) # supress "no BACKEND_HOST" error
@logger.warn "Container::autoStartServices::getService(#{serviceName}) " +
" failed with error: ", error
return
uid: ->
###
Unique identifier of this ServiceContainer
@return string
###
if not @_uid?
@_uid = _.uniqueId()
@_uid