This repository has been archived by the owner on Apr 29, 2019. It is now read-only.
/
hinoki.coffee
518 lines (449 loc) · 16.7 KB
/
hinoki.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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
((root, factory) ->
# amd
if ('function' is typeof define) and define.amd?
define(['bluebird', 'lodash', 'helfer'], factory)
# nodejs
else if exports?
module.exports = factory(
require('bluebird')
require('lodash')
require('helfer')
require('fs')
require('path')
)
# other
else
unless root.Promise?
throw new Error 'missing global variable `Promise`'
unless root._?
throw new Error 'missing global variable `_`'
unless root.helfer?
throw new Error 'missing global variable `helfer`'
root.hinoki = factory(root.Promise, root._, root.helfer)
)(this, (Promise, _, helfer, fsModule, pathModule) ->
################################################################################
# get
# polymorphic
hinoki = (arg1, arg2, arg3) ->
source = hinoki.source arg1
if arg3?
lifetimes = helfer.coerceToArray arg2
keyOrKeysOrFunction = arg3
else
lifetimes = [{}]
keyOrKeysOrFunction = arg2
cacheTarget = 0
if 'function' is typeof keyOrKeysOrFunction
keys = hinoki.getKeysToInject(keyOrKeysOrFunction)
paths = _.map keys, helfer.coerceToArray
return hinoki.getValuesAndCacheTarget(
source,
lifetimes,
paths,
cacheTarget
).promise.spread(keyOrKeysOrFunction)
if Array.isArray keyOrKeysOrFunction
keys = helfer.coerceToArray(keyOrKeysOrFunction)
paths = _.map keys, helfer.coerceToArray
return hinoki.getValuesAndCacheTarget(
source,
lifetimes,
paths,
cacheTarget
).promise
path = helfer.coerceToArray(keyOrKeysOrFunction)
return hinoki.getValueAndCacheTarget(
source,
lifetimes,
path,
cacheTarget
).promise
hinoki.isNodejs = fsModule?.statSync? and fsModule?.readdirSync?
# monomorphic
hinoki.PromiseAndCacheTarget = (promise, cacheTarget) ->
this.promise = promise
this.cacheTarget = cacheTarget
return this
# monomorphic
hinoki.getValuesAndCacheTarget = (source, lifetimes, paths, cacheTarget) ->
# result.cacheTarget is determined synchronously
nextCacheTarget = cacheTarget
# result.promise is fulfilled asynchronously
promise = Promise.all(_.map(paths, (path) ->
result = hinoki.getValueAndCacheTarget(
source
lifetimes
path
cacheTarget
)
nextCacheTarget = Math.max(nextCacheTarget, result.cacheTarget)
return result.promise
))
return new hinoki.PromiseAndCacheTarget promise, nextCacheTarget
# monomorphic
hinoki.getValueAndCacheTarget = (source, lifetimes, path, cacheTarget) ->
key = path[0]
# look if there already is a value for that key in one of the lifetimes
lifetimeIndex = helfer.findIndexWhereProperty lifetimes, key
unless lifetimeIndex is -1
valueOrPromise = lifetimes[lifetimeIndex][key]
promise =
if helfer.isThenable valueOrPromise
# if the value is already being constructed
# wait for that instead of starting a second construction.
hinoki.debug? {
event: 'lifetimeHasPromise'
path: path
promise: valueOrPromise
lifetime: lifetimes[lifetimeIndex]
lifetimeIndex: lifetimeIndex
}
valueOrPromise
else
hinoki.debug? {
event: 'lifetimeHasValue'
path: path
value: valueOrPromise
lifetime: lifetimes[lifetimeIndex]
lifetimeIndex: lifetimeIndex
}
Promise.resolve valueOrPromise
return new hinoki.PromiseAndCacheTarget promise, lifetimeIndex
# we have no value
# look if there is a factory for that key in the source
factory = source(key)
unless factory?
return new hinoki.PromiseAndCacheTarget(
Promise.reject(new hinoki.NotFoundError(path))
cacheTarget
)
unless hinoki.isFactory factory
return new hinoki.PromiseAndCacheTarget(
Promise.reject new hinoki.BadFactoryError path, factory
cacheTarget
)
hinoki.debug? {
event: 'sourceReturnedFactory'
path: path
factory: factory
}
# we've got a factory.
# lets make a value
# first lets resolve the dependencies of the factory
dependencyKeys = hinoki.baseGetKeysToInject factory, true
dependencyKeysIndex = -1
dependencyKeysLength = dependencyKeys.length
dependencyPaths = []
while ++dependencyKeysIndex < dependencyKeysLength
dependencyKey = dependencyKeys[dependencyKeysIndex]
newPath = path.slice()
newPath.unshift dependencyKey
# is this key already in the path?
# if so then it would introduce a circular dependency.
if -1 isnt path.indexOf dependencyKey
return new hinoki.PromiseAndCacheTarget(
Promise.reject(new hinoki.CircularDependencyError(newPath))
cacheTarget
)
dependencyPaths.push newPath
# no cycle - yeah!
# this code is reached synchronously from the start of the function call
# without interleaving.
if dependencyPaths.length isnt 0
result = hinoki.getValuesAndCacheTarget(
source,
lifetimes,
dependencyPaths,
cacheTarget
)
dependenciesPromise = result.promise
nextCacheTarget = result.cacheTarget
else
dependenciesPromise = Promise.resolve([])
nextCacheTarget = cacheTarget
factoryCallResultPromise = dependenciesPromise.then (dependencyValues) ->
# the dependencies are ready!
# we can finally call the factory!
return hinoki.callFactory(path, factory, dependencyValues)
# cache the promise:
# this code is reached synchronously from the start of the function call
# without interleaving.
# its important that the factoryCallResultPromise is added
# to lifetimes[maxCacheTarget] synchronously
# because as soon as control is given back to the scheduler
# another process might request the value as well.
# this way that process just reuses the factoryCallResultPromise
# instead of building it all over again.
# invariant:
# if we reach this line we are guaranteed that:
# lifetimes[nextCacheTarget][key] is undefined
# because we checked that synchronously
unless factory.__nocache
lifetimes[nextCacheTarget][key] = factoryCallResultPromise
returnPromise = factoryCallResultPromise
.then (value) ->
# cache
unless factory.__nocache
lifetimes[nextCacheTarget][key] = value
return value
.catch (error) ->
# prevent errored promises from being reused
# and allow further requests for the errored keys to succeed.
unless factory.__nocache
delete lifetimes[nextCacheTarget][key]
return Promise.reject error
return new hinoki.PromiseAndCacheTarget(returnPromise, nextCacheTarget)
# try catch prevents functions from being optimized.
# wrapping it into a function keeps the unoptimized part small.
hinoki.tryCatch = (fun, args) ->
try
return fun.apply null, args
catch error
if helfer.isError error
return error
else
return new Error error.toString()
# returns a promise
hinoki.callFactoryFunction = (path, factoryFunction, args) ->
result = hinoki.tryCatch(factoryFunction, args)
if helfer.isUndefined result
# note that a null value is allowed!
return Promise.reject new hinoki.FactoryReturnedUndefinedError path, factoryFunction
if helfer.isError(result)
return Promise.reject new hinoki.ErrorInFactory path, factoryFunction, result
if helfer.isThenable result
hinoki.debug? {
event: 'factoryReturnedPromise'
path: path
promise: result
factory: factoryFunction
}
return result
.then (value) ->
hinoki.debug? {
event: 'promiseResolved'
path: path
value: value
factory: factoryFunction
}
return value
.catch (rejection) ->
Promise.reject new hinoki.PromiseRejectedError path, factoryFunction, rejection
hinoki.debug? {
event: 'factoryReturnedValue'
path: path
value: result
factory: factoryFunction
}
return Promise.resolve result
hinoki.callFactoryObjectArray = (path, factoryObject, dependenciesObject) ->
iterator = (f, key) ->
newPath = path.slice()
newPath[0] += '[' + key + ']'
unless hinoki.isFactory f
return Promise.reject new hinoki.BadFactoryError newPath, f
if 'function' is typeof f
dependencyKeys = hinoki.getKeysToInject f
dependencies = _.map dependencyKeys, (dependencyKey) ->
dependenciesObject[dependencyKey]
return hinoki.callFactoryFunction(newPath, f, dependencies)
else if 'object' is typeof f
# supports nesting
return hinoki.callFactoryObjectArray(newPath, f, dependenciesObject)
if Array.isArray factoryObject
Promise.all(factoryObject).map(iterator)
else if 'object' is typeof factoryObject
keys = Object.keys(factoryObject)
length = keys.length
i = -1
result = {}
while ++i < length
key = keys[i]
# ignore special properties:
# special properties are those starting with `__`
# like: `__inject`, `__file`, ...
unless 0 is key.indexOf '__'
result[key] = iterator(factoryObject[key], key)
return Promise.props result
hinoki.callFactory = (path, factory, dependencyValues) ->
if 'function' is typeof factory
return hinoki.callFactoryFunction path, factory, dependencyValues
else
dependencyKeys = hinoki.getKeysToInject factory
dependenciesObject = _.zipObject dependencyKeys, dependencyValues
return hinoki.callFactoryObjectArray path, factory, dependenciesObject
################################################################################
# errors
# constructors for errors which are catchable with bluebirds `catch`
# the base error for all other hinoki errors
# not to be instantiated directly
hinoki.BaseError = ->
helfer.inherits hinoki.BaseError, Error
hinoki.NotFoundError = (path) ->
this.name = 'NotFoundError'
this.message = "neither value nor factory found for `#{path[0]}` in path `#{hinoki.pathToString path}`"
if Error.captureStackTrace?
# second argument excludes the constructor from inclusion in the stack trace
Error.captureStackTrace(this, this.constructor)
this.path = path
return
helfer.inherits hinoki.NotFoundError, hinoki.BaseError
hinoki.CircularDependencyError = (path) ->
this.name = 'CircularDependencyError'
this.message = "circular dependency `#{hinoki.pathToString path}`"
if Error.captureStackTrace?
Error.captureStackTrace(this, this.constructor)
this.path = path
return
helfer.inherits hinoki.CircularDependencyError, hinoki.BaseError
hinoki.ErrorInFactory = (path, factory, error) ->
this.name = 'ErrorInFactory'
this.message = "error in factory for `#{path[0]}`. original error `#{error.toString()}`"
if Error.captureStackTrace?
Error.captureStackTrace(this, this.constructor)
this.path = path
this.factory = factory
this.error = error
return
helfer.inherits hinoki.ErrorInFactory, hinoki.BaseError
hinoki.FactoryReturnedUndefinedError = (path, factory) ->
this.name = 'FactoryReturnedUndefinedError'
this.message = "factory for `#{path[0]}` returned undefined"
if Error.captureStackTrace?
Error.captureStackTrace(this, this.constructor)
this.path = path
this.factory = factory
return
helfer.inherits hinoki.FactoryReturnedUndefinedError, hinoki.BaseError
hinoki.PromiseRejectedError = (path, factory, error) ->
this.name = 'PromiseRejectedError'
this.message = "promise returned from factory for `#{path[0]}` was rejected. original error `#{error.toString()}`"
if Error.captureStackTrace?
Error.captureStackTrace(this, this.constructor)
this.path = path
this.factory = factory
this.error = error
return
helfer.inherits hinoki.PromiseRejectedError, hinoki.BaseError
hinoki.BadFactoryError = (path, factory) ->
this.name = 'BadFactoryError'
this.message = "factory for `#{path[0]}` has to be a function, object of factories or array of factories but is `#{typeof factory}`"
if Error.captureStackTrace?
Error.captureStackTrace(this, this.constructor)
this.path = path
this.factory = factory
return
helfer.inherits hinoki.BadFactoryError, hinoki.BaseError
################################################################################
# helper
hinoki.pathToString = (path) ->
path.join ' <- '
hinoki.getKeysToInject = (factory) ->
hinoki.baseGetKeysToInject factory, false
hinoki.baseGetKeysToInject = (factory, cache) ->
if factory.__inject?
return factory.__inject
type = typeof factory
if ('object' is type) or ('function' is type)
if ('function' is type)
keys = helfer.parseFunctionArguments factory
else
keysSet = {}
_.forEach factory, (subFactory) ->
subKeys = hinoki.baseGetKeysToInject(subFactory, cache)
_.forEach subKeys, (subKey) ->
keysSet[subKey] = true
keys = Object.keys(keysSet)
if cache
factory.__inject = keys
return keys
return []
hinoki.isFactory = (value) ->
type = typeof value
(type is 'function') or Array.isArray(value) or (type is 'object')
################################################################################
# functions for working with sources
# returns an object containing all the exported properties
# of all `*.js` and `*.coffee` files in `filepath`.
# if `filepath` is a directory recurse into every file and subdirectory.
if hinoki.isNodejs
# TODO better name
hinoki.requireSource = (filepath) ->
unless 'string' is typeof filepath
throw new Error 'argument must be a string'
hinoki.baseRequireSource filepath, {}
# TODO call this something like fromExports
hinoki.baseRequireSource = (filepath, object) ->
stat = fsModule.statSync(filepath)
if stat.isFile()
extension = pathModule.extname(filepath)
if extension isnt '.js' and extension isnt '.coffee'
return
# coffeescript is only required on demand when the project contains .coffee files
# in order to support pure javascript projects
if extension is '.coffee'
require('coffee-script/register')
exports = require(filepath)
Object.keys(exports).map (key) ->
unless hinoki.isFactory exports[key]
throw new Error('export is not a factory: ' + key + ' in :' + filepath)
if object[key]?
throw new Error('duplicate export: ' + key + ' in: ' + filepath + '. first was in: ' + object[key].__file)
object[key] = exports[key]
# add filename as metadata
object[key].__file = filepath
else if stat.isDirectory()
filenames = fsModule.readdirSync(filepath)
filenames.forEach (filename) ->
hinoki.baseRequireSource pathModule.join(filepath, filename), object
return object
hinoki.source = (arg) ->
if 'function' is typeof arg
return arg
if Array.isArray arg
coercedSources = _.map arg, hinoki.source
source = (key) ->
# try all sources in order
index = -1
length = arg.length
while ++index < length
result = coercedSources[index](key)
if result?
return result
return null
source.keys = ->
keys = []
_.each coercedSources, (source) ->
if source.keys?
keys = keys.concat(source.keys())
return keys
return source
if 'string' is typeof arg
unless hinoki.isNodejs
throw new Error 'string sources only work on Node.js because they need the filesystem module to be present'
return hinoki.source hinoki.requireSource arg
if 'object' is typeof arg
source = (key) ->
arg[key]
source.keys = ->
Object.keys(arg)
return source
throw new Error 'argument must be a function, string, object or array of these'
hinoki.decorateSourceToAlsoLookupWithPrefix = (innerSource, prefix) ->
source = (key) ->
result = innerSource(key)
if result?
return result
if 0 is key.indexOf(prefix)
return null
# factory that resolves to the same value
wrapperFactory = (wrapped) -> wrapped
wrapperFactory.__inject = [prefix + key]
return wrapperFactory
if innerSource.keys?
source.keys = innerSource.keys
return source
################################################################################
# return the hinoki object from the factory
return hinoki
)