See AUTHORS file for credits
Flakey.js is compiled by concat'ing the src files in this directory in the following order:
This assembled file is then saved in ../flakey.coffee
and compiled into ../flakey.js and ../flakey.min.js
Flakey = {
diff_patch: new diff_match_patch()
settings: {
diff_text: true
container: undefined
read_backend: 'memory'
base_model_endpoint: null #'/api'
socketio_server: null
enabled_local_backend: true
}
status: {
server_online: undefined
}
}Put JQuery into No-Conflict mode, so that we don't interfere with any other library's
jQuery.noConflict()
$ = Flakey.$ = jQueryMake JSON accessible too
JSON = Flakey.JSON = JSONFlakey's "contructor." This should be called on page load to customize settings and init the model backend controller.
Flakey.init = (config) ->Setup config
for own key, value of config
Flakey.settings[key] = valueInit this now so the new settings take effect
Flakey.models.backend_controller = new Flakey.models.BackendController()Flakey.util = {Run a function asynchronously
async: (fn) ->
setTimeout(fn, 0)Deep Compare 2 objects, recursing down through arrays and objects so that we can compare only primitive types. Return true if they are equal
deep_compare: (a, b) ->
if typeof a != typeof b
return false
compare_objects = (a, b) ->
for key, value of a
if not b[key]?
return false
for key, value of b
if not a[key]?
return false
for key, value of a
if value
switch typeof value
when 'object'
if not compare_objects(value, b[key])
return false
else
if value != b[key]
return false
else
if b[key]
return false
return true
switch typeof a
when 'object'
if not compare_objects(a, b)
return false
else
if a != b
return false
return true guid: () ->
guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
guid = guid.replace(/[xy]/g, (c) ->
r = Math.random() * 16 | 0
if c == 'x'
v = r
else
v = r & 3 | 8
v.toString(16).toUpperCase()
return v
)
return guidReturn current URL hash
get_hash: () ->
hash = window.location.hash
if hash.indexOf('#') == 0
hash = hash.slice(1)
return hash querystring : {Parse a querystring and return an obj of key/values
parse: (str) ->
if not str or str.constructor != String
return {}
pairs = str.split('&')
params = {}
for pair in pairs
pair = pair.split('=')
key = decodeURIComponent(pair[0])
value = decodeURIComponent(pair[1])
params[key] = value
return paramsBuild a querystring out of an obj
build: (params) ->
if not params or params.constructor != Object
return ''
pairs = []
for own key, value of params
pairs.push "#{encodeURIComponent(key)}=#{encodeURIComponent(value)}"
return pairs.join('&')Update the page's current querystring. Setting merge to true will add your params to the current querystring. By default, your params wipe out the current querystring.
update: (params, merge = false) ->
hash = Flakey.util.get_hash()
if hash.indexOf('?')
hash = hash.split('?')
location = hash[0]
query = Flakey.util.querystring.parse(hash[1])
else
location = hash
query = {}
if merge
$.extend(query, params)
else
query = params
window.location.hash = "#{location}?#{Flakey.util.querystring.build(query)}"
}
}class Events
events: {}Register a new function to be triggered by the given event. You may optionally provide a namespace to protect you app's events.
register: (event, fn, namespace = 'flakey') ->
if @events[namespace] == undefined @events[namespace] = {}
if @events[namespace][event] == undefined
@events[namespace][event] = []
@events[namespace][event].push(fn)
return @events[namespace][event]Trigger an event by passing it's name and optionally the namespace and data to send to each listening function
trigger: (event, namespace = 'flakey', data = {}) ->
if @events[namespace] == undefined @events[namespace] = {}
if @events[namespace][event] == undefined
return
output = []
for fn in @events[namespace][event]
output.push(fn(event, namespace, data))
return outputWipeout all registered functions from a namesapce. Dangerous.
clear: (namespace = 'flakey') -> @events[namespace] = {}
Flakey.events = new Events()class Model @model_name: null
@fields: ['id']If your model has a constructor, it should call this via super()
constructor: (init_values) -> @id = Flakey.util.guid()
@versions = []
for own key, value of init_values
@[key] = value @all: () ->
set = []
for obj in Flakey.models.backend_controller.all(@model_name)
m = new @()
m.import(obj)
set.push(m)
return setQuery for a set of objects by a query object. e.g. Model.find({name: 'sam'})
@find: (query) ->
ar = Flakey.models.backend_controller.find(@model_name, query)
if not ar.length
return []
set = []
for item in ar
m = new @()
m.import(item)
set.push(m)
return setQuery for a single object by it's id
@get: (id) ->
obj = Flakey.models.backend_controller.get(@model_name, id)
if not obj
return undefined
m = new @()
m.import(obj)
return mFull Text Search fields based on the given string / regular expression
@search: (re, fields = @fields) ->
query = {}
for key in fields
query[key] = re
ar = Flakey.models.backend_controller.search(@model_name, query)
if not ar.length
return []
set = []
for item in ar
m = new @()
m.import(item)
set.push(m)
return setDelete this instance from all backends. Just like a real database, this is irreversible.
delete: () ->
Flakey.models.backend_controller.delete(@constructor.model_name, @id)
event_key = "model_#{ @constructor.model_name.toLowerCase() }_updated"
Flakey.events.trigger(event_key, undefined)Rollback this instance. If version_id is numeric, rollback that many versions. If it's a version_id, rollback to that version.
rollback: (version_id) ->
if parseInt(version_id) < @versions.length
for i in Array(parseInt(version_id)) @pop_version()
else
exists = false
for version in @versions
if version.version_id == version_id
exists = true
if not exists
throw new ReferenceError("Version #{ version_id } does not exist.")
latest = @versions[@versions.length - 1]
while latest.version_id != version_id and @versions.length > 1
@pop_version()
latest = @versions[@versions.length - 1]
@import {
id: @id
versions: @versions
}
@write()Save this instance and write it to persistant storage.
save: (callback) ->
new_obj = @export()
old_obj = @evolve()
diff = @diff(new_obj, old_obj)
if Object.keys(diff).length > 0 @push_version(diff)
@write(callback)
else if callback?
callback()
return trueCompares two objects (old and new) and returns a delta object representing the changes
diff: (new_obj, old_obj) ->
save = {}
for key in @constructor.fields
if not Flakey.util.deep_compare(new_obj[key], old_obj[key])
switch new_obj[key].constructor
when ObjectUse $.extend as a bit of a hack to get a deep copy of the object instead of a reference.
save[key] = $.extend(true, {}, new_obj[key])
when Array
save[key] = $.extend(true, [], new_obj[key])
when String
old_obj[key] = if old_obj[key]? then old_obj[key].toString() else ''
if Flakey.settings.diff_text
patches = Flakey.diff_patch.patch_make(old_obj[key], new_obj[key])
save[key] = {
constructor: 'Patch'
patch_text: Flakey.diff_patch.patch_toText(patches)
}
else
save[key] = new_obj[key]
else
save[key] = new_obj[key]
return saveFreeze the instance's current fields into an object and return it.
export: () ->
obj = {}
for field in @constructor.fields
obj[field] = @[field]
return objGiven a version history and a version_id to stop at, evolve a blank object to the to the state at version_id. If version_id is ommited, evolve over the entire version history. If versions is ommited, get the version history of the current object.
evolve: (version_id, versions) ->
obj = {}
if versions == undefined or versions.constructor != Array
saved = Flakey.models.backend_controller.get(@constructor.model_name, @id)
versions = if saved? then saved.versions else {}
for rev in versions
for own key, value of rev.fields
switch value.constructor
when 'Patch'
patches = Flakey.diff_patch.patch_fromText(value.patch_text)
obj[key] = Flakey.diff_patch.patch_apply(patches, obj[key] || '')[0]
when Object
obj[key] = $.extend(true, {}, value)
when Array
obj[key] = $.extend(true, [], value)
else
obj[key] = value
if version_id != undefined and version_id == rev.version_id
return obj
return objTake a stored object from the backend controller, and loads it's data into this instance
import: (obj) -> @versions = obj.versions
@id = obj.id
for key in @constructor.fields
@[key] = undefined
for own key, value of @evolve(undefined, @versions)
@[key] = valuePop a version off the history array
pop_version: () ->
if @versions.length > 0 @versions.pop()Push a diff onto the history array
push_version: (diff) ->
version_id = Flakey.util.guid()
version = {
version_id: version_id,
time: +(new Date()),
fields: $.extend(true, {}, diff)
}
Object.freeze(version) @versions.push(version)Write the version history into persistent storage.
write: (callback) ->
Flakey.util.async () =>
Flakey.models.backend_controller.save(@constructor.model_name, @id, @versions)
if callback? then callback()Trigger the saved event
event_key = "model_#{ @constructor.model_name.toLowerCase() }_updated"
Flakey.events.trigger(event_key, undefined)The backend controler takes care of managing multiple storage backends and querying them for data. You should never need to call any of these methods from outside this file.
class BackendController
constructor: () -> @delim = ':::'
@backends = {
memory: {
log_key: 'flakey-memory-log'
pending_log: []
interface: new MemoryBackend()
}
}
if Flakey.settings.enabled_local_backend
@backends['local'] = {
log_key: 'flakey-local-log'
pending_log: []
interface: new LocalBackend()
}
if Flakey.settings.base_model_endpoint
@backends['server'] = {
log_key: 'flakey-server-log'
pending_log: []
interface: new ServerBackend()
}
if Flakey.settings.socketio_server
@backends['socketio'] = {
log_key: 'flakey-socketio-log'
pending_log: []
interface: new SocketIOBackend()
}
@read = Flakey.settings.read_backend || 'memory' # Backend to use for read operations
@load_logs()Pass through methods
all: (name) ->
return @backends[@read].interface.all(name)
get: (name, id) ->
return @backends[@read].interface.get(name, id)
find: (name, query) ->
return @backends[@read].interface.find(name, query, false)
search: (name, re) ->
return @backends[@read].interface.find(name, re, true)Save an item to the backends
save: (name, id, versions, backends = @backends) ->
for own bname, backend of backends
log_msg = 'save' + @delim + JSON.stringify([name, id, versions])
if backend.pending_log.length
backend.pending_log.push(log_msg) @commit_logs()
@exec_log()
return false
if not backend.interface.save(name, id, versions)
backend.pending_log.push(log_msg)
@commit_logs()
return false
return trueDelete an item from the backends
delete: (name, id, backends = @backends) ->
for own bname, backend of backends
log_msg = 'delete' + @delim + JSON.stringify([name, id])
if backend.pending_log.length
backend.pending_log.push(log_msg) @commit_logs()
@exec_log()
return false
if not backend.interface.delete(name, id)
backend.pending_log.push(log_msg)
@commit_logs()
return false
return trueExecute a backlog of transactions
exec_log: () ->
for own name, backend of @backends
log = backend.pending_log
for msg in log
if msg?
action = msg.split(@delim)
fn = Flakey.models.backend_controller.backends[name].interface[action[0]]
params = JSON.parse(action[1])
bends = {}
bends[name] = backend
params.push(bends)
if fn.apply(Flakey.models.backend_controller.backends[name].interface, params)
backend.pending_log.shift()
else
break; @commit_logs()Write log to localStorage
commit_logs: (backends = @backends) ->
for own name, backend of backends
localStorage[backend.log_key] = JSON.stringify(backend.pending_log)
return trueLoad logs from localStorage
load_logs: (backends = @backends) ->
for own name, backend of backends
if not localStorage[backend.log_key]?
break;
backend.pending_log = JSON.parse(localStorage[backend.log_key])
return trueSync backends
sync: (name, backends = @backends) ->
store = {}
for own bname, backend of backends
for item in backend.interface.all(name)
if item.id in Object.keys(store)
store[item.id].versions = @merge_version_lists(item.versions, store[item.id].versions)
else
store[item.id] = item
output = []
for own key, item of store
output.push(item)
for own bname, backend of backends
backend.interface._write(name, output)Trigger the saved event
event_key = "model_#{ name.toLowerCase() }_updated"
Flakey.events.trigger(event_key, undefined)Merge 2 version lists
merge_version_lists: (a, b) ->
temp = {}
for rev in a.concat(b)
if rev.time in Object.keys(temp)
if rev.id != temp[rev.time].id
for own key, value of rev.fields
temp[rev.time].fields[key] = value
else
temp[rev.time] = rev
else
temp[rev.time] = rev
keys = Object.keys(temp)
keys.sort((a, b) -> return a - b)
output = []
for key in keys
output.push(temp[key])
return outputBase object for providing persistance to Model objects. This gets extended by other classes for various methods (server, localStorage, etc)
class BackendList all objects from the given store
all: (name) ->
store = @_read(name)
if not store
return []
return storeDelete an item by id
delete: (name, id) ->
store = @_read(name)
index = @_query_by_id(name, id)
if index == -1
return true
store.splice(index, 1)
return @_write(name, store)Find a set of objects from the given store by performing a query on them. Pass in an object of params to compare, ex: {title: ['eq', 'The Hitchhikers Guide'], vol: ['gt', 2]} Only looks at the latest version of an object, not any previous revisions
find: (name, query, full_text = false) ->
store = @_read(name)
set = if full_text then @_search(name, query) else @_query(name, query)
out = []
for i in set
out.push store[i]
return outGet an object from the given store by its id
get: (name, id) ->
store = @_read(name)
index = @_query_by_id(name, id)
if index == -1
return undefined
return store[index]Save an object to the store
save: (name, id, versions) ->
store = @_read(name)
if not store
store = []
index = @_query_by_id(name, id)
obj = {
id: id,
versions: versions
}
if index == -1
store.push(obj)
else
store[index] = obj
return @_write(name, store)Query for an index set by performing search This is SLOW
_query: (name, query) ->
store = @_read(name)
if not store
return []
set = []
i = 0
for obj in store
rendered = @_render_obj(obj)
match = true
for own key, value of query
if rendered[key] != value
match = false
if match
set.push i
i++
return setQuery for index by id. Always returns single index, never a set
_query_by_id: (name, id) ->
store = @_read(name)
if not store
return -1
i = 0
for obj in store
if obj.id == id
return i
i++
return -1Render the latest version of an object by evolving it through all its versions
_render_obj: (obj) ->
output = {}
for rev in obj.versions
for own key, value of rev.fields
switch value.constructor
when 'Patch'
patches = Flakey.diff_patch.patch_fromText(value.patch_text)
obj[key] = Flakey.diff_patch.patch_apply(patches, obj[key] || '')[0]
when Object
obj[key] = $.extend(true, {}, value)
when Array
obj[key] = $.extend(true, [], value)
else
obj[key] = value
return objFull Text Search
_search: (name, query) ->
store = @_read(name)
if not store
return []
for own key, value of query
query[key] = new RegExp(value, "g")
set = []
i = 0
for obj in store
rendered = @_render_obj(obj)
match = false
for own key, value of query
if value.exec(rendered[key])
match = true
if match
set.push i
i++
return setVirtual Backend that simply stores data in an Object (window.memcache)
class MemoryBackend extends Backend
constructor: () ->
if not window.memcache
window.memcache = {}
_read: (name) ->
return $.extend(true, [], window.memcache[name])
_write: (name, store) ->
window.memcache[name] = $.extend(true, [], store)
return trueLocalStorage Backend
class LocalBackend extends Backend
constructor: () -> @prefix = 'flakey-'
_read: (name) ->
if not localStorage[@prefix + name]
localStorage[@prefix + name] = JSON.stringify([])
store = JSON.parse(localStorage[@prefix + name])
return store
_write: (name, store) ->
localStorage[@prefix + name] = JSON.stringify(store)
return trueServer storage backend
class ServerBackend extends Backend
constructor: () -> @server_cache = {}
build_endpoint_url: (name, id, params) ->
url = "#{Flakey.settings.base_model_endpoint}/#{name}"
if id? then url += "/#{id}"
if params? and params.constructor == Object
querystring = Flakey.util.querystring.build(params)
url += "?#{querystring}"
return urlList all objects from the given store
all: (name) ->
store = false
$.ajax({
async: false
url: @build_endpoint_url(name)
dataType: 'json'
error: () ->
Flakey.status.server_online = false
success: (data) ->
Flakey.status.server_online = true
store = data
type: 'GET'
})
(@save_to_cache(name, obj.id, obj.versions) for obj in store)
return storeGet an object from the given store by its id
get: (name, id) ->
obj = false
$.ajax({
async: false
url: @build_endpoint_url(name, id)
dataType: 'json'
error: () ->
Flakey.status.server_online = false
success: (data) ->
Flakey.status.server_online = true
obj = data
type: 'GET'
}) @save_to_cache(name, obj.id, obj.versions)
return objRetreive Object from server cache
get_from_cache: (name, id) ->
if @server_cache[name]
if @server_cache[name][id]
return @server_cache[name][id]
return undefinedSave an object to the store
save: (name, id, versions, force_write) ->
proposed_obj = {id: id, versions: versions}
cached_obj = @get_from_cache(name, id)Compare cached object to proposed save object Only actually save if they are different, or if force_write flag is true
if Flakey.util.deep_compare(proposed_obj, cached_obj) and force_write != true
return trueWrite objects to server
status = false
$.ajax({
async: false
url: @build_endpoint_url(name, id)
data: Flakey.util.querystring.build({id: id, versions: JSON.stringify(versions)})
dataType: 'json'
error: () ->
Flakey.status.server_online = false
success: () ->
Flakey.status.server_online = true
status = true
type: 'POST'
})
return statusSave object to server_cache
save_to_cache: (name, id, versions) -> @server_cache[name] = @server_cache[name] || {}
@server_cache[name][id] = $.extend(true, {}, {id: id, versions: versions})Delete an item by id
delete: (name, id) ->
status = false
$.ajax({
async: false
url: @build_endpoint_url(name, id)
dataType: 'json'
error: () ->
Flakey.status.server_online = false
success: () ->
Flakey.status.server_online = true
status = true
type: 'DELETE'
})
return status
_query: (name, query) ->
throw new TypeError("_query not supported on server backend")
_query_by_id: (name, id) ->
throw new TypeError("_query_by_id not supported on server backend")
_read: (name) ->
return @all(name)
_write: (name, store) ->
status = true
for item in store
if not @save(name, item.id, item.versions)
status = false
return statusServer storage backend
class SocketIOBackend extends Backend
constructor: () -> @status = true
@socket = window.socket = io.connect(Flakey.settings.socketio_server)
@server_cache = {}
@socket.on 'sync', (set) =>
names = []
for obj in set
if obj.name not in names
names.push obj.name
@save_to_cache(obj.name, obj.id, obj.versions)
for name in names
Flakey.models.backend_controller.sync(name)
@socket.emit 'fetch_all'List all objects from the given store
all: (name) ->
store = [] @server_cache[name] = @server_cache[name] || {}
(store.push(@get_from_cache(name, id)) for id in Object.keys(@server_cache[name]))
return storeGet an object from the given store by its id
get: (name, id) ->
return @get_from_cache(name, id)Retreive Object from server cache
get_from_cache: (name, id) ->
if @server_cache[name]
if @server_cache[name][id]
return @server_cache[name][id]
return undefinedSave an object to the store
save: (name, id, versions, force_write) ->
proposed_obj = {id: id, versions: versions}
cached_obj = @get_from_cache(name, id)Compare cached object to proposed save object Only actually save if they are different, or if force_write flag is true
if Flakey.util.deep_compare(proposed_obj, cached_obj) and force_write != true
return trueWrite objects to server
@socket.emit 'write', {name: name, id: id, versions: versions}, () =>
@save_to_cache name, id, versions
return @statusSave object to server_cache
save_to_cache: (name, id, versions) -> @server_cache[name] = @server_cache[name] || {}
@server_cache[name][id] = $.extend(true, {}, {id: id, versions: versions})Delete an item by id
delete: (name, id) -> @socket.emit('delete', {name: name, id: id})
return @status
_query: (name, query) ->
throw new TypeError("_query not supported on socketio backend")
_query_by_id: (name, id) ->
throw new TypeError("_query_by_id not supported on socketio backend")
_read: (name) ->
return @all(name)
_write: (name, store) ->
status = true
for item in store
if not @save(name, item.id, item.versions)
status = false
return statusExport into the main flakey object
Flakey.models = {
Model: Model,
BackendController: BackendController,
backend_controller: null
}Subclass this to make controllers for your apps
class Controller
constructor: (config = {}) -> @active = @active || false
@actions = @actions || {}
@id = @id || ''
@class_name = @class_name || ''
@parent = @parent || null
@container = @container || null
@container_html = @container_html || ''
@subcontrollers = @subcontrollers || []
@query_params = @query_params || {}Build an HTML container to hold this controller
@container = $(document.createElement('div'))
@container.html(@container_html)
@parent = config.parent || Flakey.settings.container
@parent.append(@container)
@container.attr('id', @id)
for name in @class_name.split(' ')
@container.addClass(name)Append another controller to this one. It will always mimic the active/passive state of this controller
append: () ->
for Contr in arguments
contr = new Contr({parent: @parent}) @subcontrollers.push(contr)Just a stub
render: () -> @html('')Bind actions to JQuery events
bind_actions: () ->
for own key, fn of @actions
key_parts = key.split(' ')
action = key_parts.shift()
hotkey = ""
if action in ['keydown', 'keyup', 'keypress']
hotkey = key_parts.shift()
if (hotkey.indexOf('#') != -1 or hotkey.indexOf('.') != -1) and hotkey.indexOf('+') == -1
key_parts.shift(hotkey)
hotkey = ""
selector = key_parts.join(' ')
if selector.length <= 0
selector = document
if hotkey
$(selector).bind(action, hotkey, @[fn])
else
$(selector).bind(action, @[fn])Unbind actions from JQuery events
unbind_actions: () ->
$(document).unbind()Set the container html to the given string. Generally you can pass the output of a template render right into this.
html: (htm) -> @container_html = htm
@container.html(@container_html)
Flakey.events.trigger('html_updated')Make this controller active by setting its active class and binding events
make_active: () -> @active = true
@render()
@bind_actions()
@container.removeClass('passive').addClass('active')
for sub in @subcontrollers
sub.make_active()Make this controller inactive and unbind its events.
make_inactive: () -> @active = false
@unbind_actions()
@container.removeClass('active').addClass('passive')
for sub in @subcontrollers
sub.make_inactive()Set the @query_params attribute
set_queryparams: (params) -> @query_params = params
for sub in @subcontrollers
sub.set_queryparams(params)Subclass a stack to manage a stack of controllers and make sure only one is ever visible at a time. Very similar interface to an actual controller
class Stack
constructor: (config = {}) -> @id = @id || ''
@class_name = @class_name || ''
@active = @active || false
@controllers = @controllers || {}
@routes = @routes || {}
@default = @default || ''
@active_controller = @active_controller || ''
@parent = @parent || null
@query_params = @query_params || {}
@container = $(document.createElement('div'))
@container.attr('id', @id)
for name in @class_name.split(' ')
@container.addClass(name)
@container.html(@container_html)
@parent = config.parent || Flakey.settings.container
@parent.append(@container)
for own name, contr of @controllers
@controllers[name] = new contr({parent: @container})Make sure we resolve a controller any time the location hash changes.
window.addEventListener('hashchange', @resolve, false)Resolve the location hash to a controller and make it active
resolve: () =>
hash = Flakey.util.get_hash()
new_controller = undefined
if hash.length > 0
if hash.indexOf('?') != -1
hash = hash.split('?')
location = hash[0]
querystring = hash[1]
else
location = hash
querystring = ''
new_controller = undefined
for own route, controller_name of @routes
regex = new RegExp(route)
if location.match(route)
new_controller = controller_name
if not new_controller
window.location.hash = "##{@default}"
return @active_controller = new_controllerParse query params from the hash and send them to the active controller
@controllers[@active_controller].set_queryparams(Flakey.util.querystring.parse(querystring))Make all the other controllers inactive
for own name, controller of @controllers
if name != @active_controller @controllers[name].make_inactive()
if @active
@controllers[@active_controller].make_active()
@controllers[@active_controller].render()
return @controllers[@active_controller]Make this stack active
make_active: () -> @resolve()
if @controllers[@active_controller] != undefined
@controllers[@active_controller].make_active()
@controllers[@active_controller].render()
@active = trueMake this stack inactive
make_inactive: () ->
if @controllers[@active_controller] != undefined @controllers[@active_controller].make_inactive()
@active = false
set_queryparams: (params) ->
@query_params = params
Flakey.controllers = {
Stack: Stack,
Controller: Controller
}class Template
constructor: (eco, name) -> @eco = eco
@name = nameRender this template with the given context object, return the resulting string
render: (context = {}) ->
return @eco(context)Call this to load a Flakey.Template object from a compiled eco template.
get_template = (name, tobj) ->
template = tobj.ecoTemplates[name]
return new Template(template, name)
Flakey.templates = {
get_template
Template: Template
}Make this available via CommonJS
if module?
module.exports = FlakeyAssign it to the window object, if we're in a browser and a window exists.
if window
window.Flakey = Flakey