UNPKG

21.1 kBtext/coffeescriptView Raw
1"use strict";
2StandardLib = require 'art-standard-lib'
3WebpackHotLoader = require './WebpackHotLoader'
4
5{
6 capitalize, decapitalize, log
7 isFunction, objectName,
8 isPlainObject, functionName, isString
9 isPlainArray
10 Unique
11 callStack
12 Log
13 log
14 inspectedObjectLiteral
15 MinimalBaseObject
16 getModuleBeingDefined
17 concatInto
18 mergeInto
19 merge
20 neq
21 isString
22 object
23 getSuperclass
24} = StandardLib
25
26{nextUniqueObjectId} = Unique
27
28ExtendablePropertyMixin = require './ExtendablePropertyMixin'
29
30module.exports = class BaseClass extends ExtendablePropertyMixin MinimalBaseObject
31 @objectsCreated: 0
32 @objectsCreatedByType: {}
33 @resetStats: =>
34 @objectsCreated = 0
35 @objectsCreatedByType = {}
36
37 # override to dynamically set a class's name (useful for programmatically generated classes)
38 # NOTE: must use klass.getName() and not klass.name if you want to "see" dynamically assigned class-names
39 @_name: null
40
41 ###
42 NOTE: only hasOwnProperties are considered! Inherited properties are not touched.
43 IN:
44 targetObject: object will be altered to be an "imprint" of fromObject
45 fromObject: object pattern used to imprint targetObject
46 preserveState:
47 false:
48 targetObject has every property updated to exactly match fromObject
49
50 This includes:
51 1. delete properties in targetObject that are not in fromObject
52 2. add every property in fromObject but not in targetObject
53 3. overwriting every property in targetObject also in fromObject
54
55 true:
56 Attempts to preserve the state of targetObject while updating its functionality.
57 This means properties which are functions in either object are updated.
58
59 WARNING: This is a grey area for JavaScript. It is not entirely clear what is
60 state and what is 'functionality'. I, SBD, have made the following heuristic decisions:
61
62 Imprint actions taken when preserving State:
63
64 1. DO NOTHING to properties in targetObject that are not in fromObject
65 2. add every property in fromObject but not in targetObject
66 3. properties in targetObject that are also in fromObject are updated
67 if one of the following are true:
68 - isFunction fromObject[propName]
69 - isFunction targetObject[propName]
70 - propName does NOT start with "_"
71 NOTE: property existance is detected using Object.getOwnPropertyDescriptor
72
73 ###
74 thoroughDeleteProperty = (object, propName) ->
75 Object.defineProperty object, propName,
76 configurable: true
77 writable: false
78 value: 1
79
80 delete object[propName]
81
82 nonImprintableProps = ["__proto__", "prototype"]
83
84 @imprintObject: imprintObject = (targetObject, sourceObject, preserveState = false, returnActionsTaken) ->
85 targetPropertyNames = Object.getOwnPropertyNames targetObject
86 sourcePropertyNames = Object.getOwnPropertyNames sourceObject
87
88 if returnActionsTaken
89 addedProps =
90 removedProps =
91 changedProps = undefined
92
93 unless preserveState
94 for targetPropName in targetPropertyNames when !(targetPropName in sourcePropertyNames)
95 (removedProps?=[]).push targetPropName if returnActionsTaken
96 thoroughDeleteProperty targetObject, targetPropName
97
98 for sourcePropName in sourcePropertyNames when !(sourcePropName in nonImprintableProps)
99 targetPropDescriptor = Object.getOwnPropertyDescriptor targetObject, sourcePropName
100 sourcePropDescriptor = Object.getOwnPropertyDescriptor sourceObject, sourcePropName
101
102 sourceValueIsFunction = isFunction sourceValue = sourcePropDescriptor.value
103 targetValueIsFunction = isFunction targetValue = targetPropDescriptor?.value
104 if (
105 !preserveState || !targetPropDescriptor ||
106 sourceValueIsFunction || targetValueIsFunction ||
107 !sourcePropName.match /^_/
108 )
109 if returnActionsTaken
110 if !targetPropDescriptor
111 (addedProps?=[]).push sourcePropName if sourcePropName != "_name"
112 else
113 if neqResult = neq sourceValue, targetValue, true
114 (changedProps?=[]).push sourcePropName
115
116 Object.defineProperty targetObject, sourcePropName, sourcePropDescriptor
117
118 if returnActionsTaken
119 (removedProps || changedProps || addedProps) &&
120 merge {removedProps, changedProps, addedProps}
121
122 else
123 sourceObject
124
125 ###
126 imprints both the class and its prototype.
127
128 preserved in spite of imprintObject's rules:
129 @namespace
130 @::constructor
131 ###
132 @imprintFromClass: (updatedKlass, returnActionsTaken) ->
133 unless updatedKlass == @
134 {namespace, namespacePath, _name} = @
135 oldConstructor = @::constructor
136
137 classUpdates = imprintObject @, updatedKlass, true, returnActionsTaken
138 prototypeUpdates = imprintObject @::, updatedKlass::, false, returnActionsTaken
139
140 @::constructor = oldConstructor
141 @namespace = namespace
142 @namespacePath = namespacePath
143 @_name = _name
144
145 if returnActionsTaken
146 merge
147 class: classUpdates
148 prototype: prototypeUpdates
149 else
150 @
151
152 @getHotReloadKey: -> @getName()
153
154 ###
155 IN:
156 _module should be the CommonJS 'module'
157 klass: class object which extends BaseClass
158
159 liveClass:
160 On the first load, liveClass gets set.
161 Each subsequent hot-load UPDATES liveClass,
162 but liveClass always points to the initially created class object.
163
164 OUT: the result of the call to liveClass.postCreate()
165
166 postCreate is passed:
167 hotReloaded: # true if this is anything but the initial load
168 classModuleState:
169 liveClass: # the original liveClass
170 hotUpdatedFromClass: # the most recently hot-loaded class
171 hotReloadVersion: # number starting at 0 and incremented with each hot reload
172 _module: # the CommonJs module
173
174 EFFECTS:
175 The following two methods are invoked on liveClass:
176
177 if hot-reloading
178 liveClass.imprintFromClass klass
179
180 # always:
181 liveClass.postCreate hotReloaded, classModuleState, _module
182
183 ###
184 @createWithPostCreate: createWithPostCreate = (a, b) ->
185 klass = if b
186 _module = a
187 b
188 else a
189
190 # TODO - maybe we should make an NPM just for defineModule, so this is cleaner?
191 _module ||= getModuleBeingDefined() || global.__definingModule
192
193 # if hot reloading is not supported:
194 return klass unless klass?.postCreate
195 unless _module?.hot
196 return klass.postCreate(
197 hotReloadEnabled: false
198 hotReloaded: false
199 classModuleState: {}
200 module: _module
201 ) || klass
202
203 # hot reloading supported:
204 WebpackHotLoader.runHot _module, (moduleState) ->
205 hotReloadKey = klass.getHotReloadKey()
206 if classModuleState = moduleState[hotReloadKey]
207 # hot reloaded!
208 {liveClass} = classModuleState
209 hotReloaded = true
210
211 classModuleState.hotReloadVersion++
212 classModuleState.hotUpdatedFromClass = klass
213
214 # set namespaceProps in case it uses them internally
215 # NOTE: everyone else will access these props through liveClass, which is already correct
216 liveClass.namespace._setChildNamespaceProps liveClass.getName(), klass
217
218 klass._name = liveClass._name
219 liveClass.classModuleState = classModuleState
220 updates = liveClass.imprintFromClass klass, true
221
222 log "Art.ClassSystem.BaseClass #{liveClass.getName?()} HotReload":
223 version: classModuleState.hotReloadVersion
224 updates: updates
225 else
226 # initial load
227 hotReloaded = false
228
229 klass._hotClassModuleState =
230 moduleState[hotReloadKey] = klass.classModuleState = classModuleState =
231 liveClass: liveClass = klass
232 hotUpdatedFromClass: null
233 hotReloadVersion: 0
234
235 liveClass.postCreate
236 hotReloadEnabled: true
237 hotReloaded: hotReloaded
238 classModuleState: classModuleState
239 module: _module
240
241 # depricated alias
242 @createHotWithPostCreate: (a, b) ->
243 log.error "createHotWithPostCreate is DEPRICATED"
244 createWithPostCreate a, b
245
246 ###
247 called every load
248 IN: options:
249 NOTE: hot-loading inputs are only set if this class created as follows:
250 createHotWithPostCreate module, class Foo extends BaseClass
251
252 hotReload: true/false
253 true if this class was hot-reloaded
254
255 hotReloadEnabled: true/false
256
257 classModuleState:
258 liveClass: the first-loaded version of the class.
259 This is the official version of the class at all times.
260 The hot-reloaded version of the class is "imprinted" onto the liveClass
261 but otherwise is not used (but can be accessed via classModuleState.hotUpdatedFromClass)
262 hotUpdatedFromClass: The most recently loaded version of the class.
263 hotReloadVersion: number, starting at 1, and counting up each load
264
265 classModuleState is a plain-object specific to the class and its CommonJS module. If there is
266 more than one hot-loaded class in the same module, each will have its own classModuleState.
267
268 SBD NOTE: Though we could allow clients to add fields to classModuleState, I think it works
269 just as well, and is cleaner, if any state is stored in the actual class objects and
270 persisted via postCreate.
271
272 module: the CommonJs module object.
273
274 {hotReloadEnabled, hotReloaded, classModuleState, module} = options
275 ###
276 @postCreate: (options) ->
277 # TODO - once we switch fully to ES6, we should revisit how we handle @namespace
278 # Normally, @namespace and @namespacePath get set by the parent NeptuneNamespace's index file AFTER postCreate.
279 # However, if you need to require a file directly without requiring everything else in the namespace,
280 # you can add: @setNamespace require './namespace'
281 # to your class and you'll still get all the useful namespace functions.
282 # The above command makes your file work either way - as part of the full namespace or
283 # included by itself.
284 # SBD: I pulled the following code because it breaks hot-reloading.
285 # @namespacePath = if @namespace = @_namespace ? null
286 # "#{@namespace.namespacePath}.#{@getName()}"
287 # else
288 # null
289
290 if @getIsAbstractClass()
291 @postCreateAbstractClass options
292 else
293 @postCreateConcreteClass options
294
295 @setNamespace: (ns) ->
296 @_namespace = ns
297
298 @postCreateAbstractClass: (options) -> @
299 @postCreateConcreteClass: (options) -> @
300
301 # excludedKeys = ["__super__", "namespace", "namespacePath"].concat Object.keys Neptune.Base
302
303 constructor: ->
304 @__uniqueId = null
305 # Object.defineProperty @, "__uniqueId",
306 # enumerable: false
307 # value: null
308 # Neptune.Lib.Art.DevTools.Profiler.sample && Neptune.Lib.Art.DevTools.Profiler.sample()
309 # type = @classPathName
310 # BaseClass.objectsCreatedByType[type] = (BaseClass.objectsCreatedByType[type]||0) + 1
311 # BaseClass.objectsCreated++
312
313 # True if object implementsInterface all methods (an array of strings)
314 # (i.e. the named properties are all functions)
315 @implementsInterface: (object, methods) ->
316 for method in methods
317 return false unless typeof object[method] is "function"
318 true
319
320 #####################################
321 # Module-like features (mixins)
322 #####################################
323 ###
324 mix-in class methods
325 Define getters/setters example:
326 class MyMixin
327 included: ->
328 @getter foo: -> @_foo
329 @setter foo: (v) -> @_foo = v
330
331 NOTE! This will NOT include any properties you defined with getter or setter!
332 NOTE! This only copies over values if there aren't already values in the included-into class
333 This somewhat mirrors Ruby's include where the included-into-class's methods take precidence.
334 However, if you include two modules in a row, the first module gets priority here.
335 In ruby the second module gets priority (I believe).
336
337 DEPRICATED!!!
338 Time to do it "right" - and it's just a simple pattern:
339 Justin Fagnani figured this out. Thanks!
340 Read More:
341 http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
342
343 To define a mixin:
344
345 MyMixin = (superClass) ->
346 class MyMixin extends superClass
347 ... write your mixin as-if it were part of the normal inheritance hierachy
348
349 To use a mixin:
350
351 class MyClass extends MyMixin MySuperClass
352
353 To use two mixins:
354
355 class MyClass extends MyMixin1 MyMixin2 MySuperClass
356 ###
357 warnedAboutIncludeOnce = false
358 @include: (obj) ->
359 log.error "DEPRICATED: BaseClass.include. Use pattern."
360 unless warnedAboutIncludeOnce
361 warnedAboutIncludeOnce = true
362 console.warn """
363 Mixin pattern:
364
365 To define a mixin:
366
367 MyMixin = (superClass) ->
368 class MyMixin extends superClass
369 ... write your mixin as-if it were part of the normal inheritance hierachy
370
371 To use a mixin:
372
373 class MyClass extends MyMixin MySuperClass
374
375 To use two mixins:
376
377 class MyClass extends MyMixin1 MyMixin2 MySuperClass
378 """
379
380 for key, value of obj when key != 'included'
381 @[key] = value unless @[key]
382
383 for key, value of obj.prototype when key
384 @::[key] = value unless @::[key]
385
386 obj.included? @
387 this
388
389 ######################################################
390 # Class Info
391 ######################################################
392 @getNamespacePath: ->
393 if @namespacePath?.match @getName()
394 @namespacePath
395 else
396 @namespacePath = "(no parent namespace).#{@getName()}"
397
398 @getNamespacePathWithExtendsInfo: ->
399 "#{@getNamespacePath()} extends #{getSuperclass(@).getNamespacePath()}"
400
401 # DEPRICATED - use NN stuff
402 # @classGetter
403 # classPath: -> @namespace.namespacePath
404 # classPathArray: -> @namespacePathArray ||= @getClassPath().split "."
405 # classPathName: ->
406 # if p = @namespace?.namespacePath
407 # p + "." + @getClassName()
408 # else
409 # @getClassName()
410
411 @getClassName: (klass = @) ->
412 klass.getName?() || klass.name
413
414
415 ######################################################
416 # inspect
417 ######################################################
418
419 ###
420 inspect: ->
421 IN: ()
422 OUT: string
423
424 Can override with same or alternate, recursion-block-supported signature:
425 IN: (inspector) ->
426 OUT: if inspector then null else string
427
428 To handle the case where the inspector is not set, we
429 recommneded declaring your 'inspect' as follows:
430 inspect: (inspector) ->
431 return StandardLib.inspect @ unless inspector
432 # ...
433 # custom code which writes all output to inspector.put
434 # and uses inspector.inspect for inspecting sub-objects
435 # ...
436 null
437
438 EFFECT:
439 call inspector.put one or multiple times with strings to add to the inspected output
440 call inspector.inspect foo to sub-inspect other objects WITH RECURSION BLOCK
441
442 # Example 1:
443 inspect: (inspector) ->
444 return StandardLib.inspect @ unless inspector
445 inspector.put @getNamespacePath()
446
447 # Example 2:
448 inspect: ->
449 @getNamespacePath()
450 ###
451 @inspect: -> @getNamespacePath()
452
453 inspect: -> "<#{@class.namespacePath}>"
454
455 ###
456 getInspectedObjects: -> plainObjects
457
458 usually implemented this way:
459 @getter inspectedObjects: -> plainObjects or objects which implement "inspect"
460
461 TODO: I think I want to refactor inspectedObjects to ONLY return near-JSON-compatible objects:
462 1. strings
463 2. maps
464 3. arrays
465
466 Everything else should be rendered to a string. In general, strings should Eval to the object
467 they represent:
468
469 toInspectedObject(null): 'null' # null becomes a string
470 toInspectedObject(true): 'true' # true becomes a string
471 toInspectedObject(false): 'false' # false becomes a string
472 toInspectedObject(undefined): 'undefined' # undefined becomes a string
473 toInspectedObject('hi'): '"hi"' # ESCAPED
474 toInspectedObject((a) -> a): 'function(a){return a;}'
475 toInspectedObject(rgbColor()) "rgbColor('#000000')"
476
477 NOTE: inspectedObjects differs from plainObjects. The latter should be 100% JSON,
478 and should return actual values where JSON allows, otherwise, return JSON data structures
479 that encode the object's information in a human-readable format, ideally one that can be
480 used as an input to the constructor of the object's class to recreate the original object.
481
482 plainObjects:
483 null: null
484 true: true
485 false: false
486 'str': 'str' # NOT escaped
487 undefined: null
488 ((a) -> a): 'function(a){return a;}'
489 rgbColor(): r: 0, g: 0, b: 0, a: 0
490
491 You can provide this function for fine-grained control of what Inspector2 outputs and hence
492 what DomConsole displays.
493
494 If you would like for a string to appear without quotes, use:
495 {inspect: -> 'your string without quotes here'}
496 ###
497
498 @getter
499 inspectObjects: ->
500 console.warn "inspectObjects/getInspectObjects is DEPRICATED. Use: inspectedObjects/getInspectedObjects"
501 @getInspectedObjects()
502
503 inspectedObjects: ->
504 inspectedObjectLiteral "<#{@class?.getNamespacePath()}>"
505
506 @classGetter
507 inspectedObjects: ->
508 inspectedObjectLiteral "class #{@getNamespacePath()}"
509
510 ######################################################
511 # Abstract Classes
512 ######################################################
513
514 ###
515 Define this class as an abstract class. Implicitly it means
516 any class it extends is also abstract, at least in this context.
517
518 Definition: Abstract classes are not intended to every be instantiated.
519 i.e.: never do: new MyAbstractClass
520
521 TODO: in Debug mode, in the constructor:
522 throw new Error "cannot instantiate abstract classes" if @class.getIsAbstractClass()
523
524 ###
525 @abstractClass: ->
526 throw new Error "abstract classes cannot also be singleton" if @getIsSingletonClass()
527 @_firstAbstractAncestor = @
528
529 @classGetter
530 superclass: -> getSuperclass @
531 isAbstractClass: -> !(@prototype instanceof @_firstAbstractAncestor)
532 isConcreteClass: -> !@getIsAbstractClass()
533 abstractPrototype: -> @_firstAbstractAncestor.prototype
534 firstAbstractAncestor: -> @_firstAbstractAncestor
535 isSingletonClass: -> @_singleton?.class == @
536 concretePrototypeProperties: ->
537 abstractClassPrototype = @getAbstractClass().prototype
538 object @prototype, when: (v, k) ->
539 k != "constructor" &&
540 abstractClassPrototype[k] != v
541
542 @getAbstractClass: -> @_firstAbstractAncestor
543
544 # BaseClass is an abstract-class
545 @abstractClass()
546
547 @propertyIsAbstract: (propName) ->
548 @getAbstractClass().prototype[propName] == @prototype[propName]
549
550 @propertyIsConcrete: (propName) ->
551 @getAbstractClass().prototype[propName] != @prototype[propName]
552
553 ######################################################
554 # SingletonClasses
555 ######################################################
556
557 ###
558 SBD2017: this is the new path for singleton classes.
559 WHY: We can elliminate the need to DECLARE classes singleton.
560 Instead, we can just access the singleton for any class, if needed.
561 TODO: once we are 100% CaffeineScript, switch this to a @classGetter
562 ###
563 @getSingleton: getSingleton = ->
564 if @_singleton?.class == @
565 @_singleton
566 else
567 throw new Error "singleton classes cannot be abstract" if @getIsAbstractClass()
568 @_singleton = new @
569
570 ###
571 creates the classGetter "singleton" which returns a single instance of the current class.
572
573 IN: args are passed to the singleton constructor
574 OUT: null
575
576 The singleton instance is created on demand the first time it is accessed.
577
578 SBD2017: Possibly depricated; maybe we just need a singleton getter for everyone?
579 The problem is coffeescript doesn't properly inherit class getters.
580 BUT ES6 and CaffeineScript DO. So, when we switch over, I think we can do this.
581 ###
582 @singletonClass: ->
583 throw new Error "singleton classes cannot be abstract" if @getIsAbstractClass()
584
585 @classGetter
586 singleton: getSingleton
587 "#{decapitalize functionName @}": -> @getSingleton()
588
589 null
590
591 ######################################################
592 # Instance Methods
593 ######################################################
594
595 @getter
596 className: -> @class.getClassName()
597 class: -> @constructor
598 keys: -> Object.keys @
599 namespacePath: -> @class.getNamespacePath()
600 classPathNameAndId: -> "#{@classPathName}:#{@objectId}"
601 uniqueId: -> @__uniqueId ||= nextUniqueObjectId() # unique across all things
602 objectId: -> @__uniqueId ||= nextUniqueObjectId() # number unique across objects
603
604 # freeze this object safely
605 freeze: ->
606 @getUniqueId() # ensure we have the unique id set
607 Object.freeze @
608 @
609
610 implementsInterface: (methods) -> Function.BaseClass.implementsInterface @, methods
611 tap: (f)-> f(@);@