leonidas 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. data/CHANGELOG +1 -0
  2. data/Gemfile +15 -0
  3. data/Gemfile.lock +105 -0
  4. data/LICENSE +21 -0
  5. data/Manifest +63 -0
  6. data/README.md +213 -0
  7. data/Rakefile +42 -0
  8. data/assets/scripts/coffee/leonidas/client.coffee +15 -0
  9. data/assets/scripts/coffee/leonidas/commander.coffee +34 -0
  10. data/assets/scripts/coffee/leonidas/commands/command.coffee +9 -0
  11. data/assets/scripts/coffee/leonidas/commands/organizer.coffee +24 -0
  12. data/assets/scripts/coffee/leonidas/commands/processor.coffee +13 -0
  13. data/assets/scripts/coffee/leonidas/commands/stabilizer.coffee +13 -0
  14. data/assets/scripts/coffee/leonidas/commands/synchronizer.coffee +38 -0
  15. data/assets/scripts/js/lib/jquery.js +6 -0
  16. data/bin/leonidas.js +178 -0
  17. data/config/assets.rb +7 -0
  18. data/leonidas.gemspec +36 -0
  19. data/lib/leonidas.rb +14 -0
  20. data/lib/leonidas/app/app.rb +80 -0
  21. data/lib/leonidas/app/connection.rb +20 -0
  22. data/lib/leonidas/app/repository.rb +41 -0
  23. data/lib/leonidas/commands/aggregator.rb +31 -0
  24. data/lib/leonidas/commands/command.rb +24 -0
  25. data/lib/leonidas/commands/handler.rb +21 -0
  26. data/lib/leonidas/commands/processor.rb +30 -0
  27. data/lib/leonidas/dsl/configuration_expression.rb +17 -0
  28. data/lib/leonidas/memory_layer/memory_registry.rb +33 -0
  29. data/lib/leonidas/persistence_layer/persister.rb +54 -0
  30. data/lib/leonidas/persistence_layer/state_builder.rb +17 -0
  31. data/lib/leonidas/persistence_layer/state_loader.rb +22 -0
  32. data/lib/leonidas/routes/sync.rb +45 -0
  33. data/lib/leonidas/symbols.rb +17 -0
  34. data/spec/jasmine/jasmine.yml +44 -0
  35. data/spec/jasmine/runner.html +77 -0
  36. data/spec/jasmine/support/classes.coffee +16 -0
  37. data/spec/jasmine/support/helpers.coffee +22 -0
  38. data/spec/jasmine/support/mocks.coffee +19 -0
  39. data/spec/jasmine/support/objects.coffee +11 -0
  40. data/spec/jasmine/support/requirements.coffee +1 -0
  41. data/spec/jasmine/tests/client_spec.coffee +20 -0
  42. data/spec/jasmine/tests/commander_spec.coffee +69 -0
  43. data/spec/jasmine/tests/commands/command_spec.coffee +12 -0
  44. data/spec/jasmine/tests/commands/organizer_spec.coffee +70 -0
  45. data/spec/jasmine/tests/commands/processor_spec.coffee +22 -0
  46. data/spec/jasmine/tests/commands/stabilizer_spec.coffee +30 -0
  47. data/spec/jasmine/tests/commands/synchronizer_spec.coffee +72 -0
  48. data/spec/rspec/spec_helper.rb +4 -0
  49. data/spec/rspec/support/classes/app.rb +26 -0
  50. data/spec/rspec/support/classes/commands.rb +52 -0
  51. data/spec/rspec/support/classes/persistence.rb +56 -0
  52. data/spec/rspec/support/config.rb +3 -0
  53. data/spec/rspec/support/mocks.rb +15 -0
  54. data/spec/rspec/support/objects.rb +11 -0
  55. data/spec/rspec/unit/app/app_spec.rb +185 -0
  56. data/spec/rspec/unit/app/repository_spec.rb +114 -0
  57. data/spec/rspec/unit/commands/aggregator_spec.rb +103 -0
  58. data/spec/rspec/unit/commands/command.rb +17 -0
  59. data/spec/rspec/unit/commands/processor_spec.rb +30 -0
  60. data/spec/rspec/unit/dsl/configuration_expression_spec.rb +32 -0
  61. data/spec/rspec/unit/leonidas_spec.rb +26 -0
  62. data/spec/rspec/unit/memory_layer/memory_registry_spec.rb +85 -0
  63. data/spec/rspec/unit/persistence_layer/persister_spec.rb +84 -0
  64. data/spec/rspec/unit/persistence_layer/state_loader_spec.rb +29 -0
  65. metadata +166 -0
@@ -0,0 +1,16 @@
1
+ globalize class IncrementHandler
2
+
3
+ constructor: (@state)->
4
+
5
+ handles: (command)-> command.name is "increment"
6
+
7
+ run: (command)-> @state.integer++
8
+
9
+
10
+ globalize class PopCharHandler
11
+
12
+ constructor: (@state)->
13
+
14
+ handles: (command)-> command.name is "pop-char"
15
+
16
+ run: (command)-> @state.string = @state.string.slice(0,-1)
@@ -0,0 +1,22 @@
1
+ (exports ? this).globalize = (fxn, name=null)=>
2
+ functionName = if name is null then fxn.name else name
3
+ (exports ? this)[functionName] = fxn
4
+
5
+
6
+ # MOCKING #
7
+ (exports ? this).mocks = {}
8
+
9
+ (exports ? this).addMock = (name, obj)=>
10
+ (exports ? this).mocks[name] = obj
11
+
12
+
13
+ # RUNTIME PATCHING #
14
+ (exports ? this).patches = {}
15
+
16
+ (exports ? this).patch = (method, patch)=>
17
+ (exports ? this).patches[method.name] = method
18
+ method = patch
19
+
20
+ (exports ? this).restore = (method)=>
21
+ method = (exports ? this).patches[method.name]
22
+ delete (exports ? this).patches[method.name]
@@ -0,0 +1,19 @@
1
+ addMock "syncPushResponse",
2
+ success: true
3
+ message: 'commands received'
4
+ data: { }
5
+
6
+ addMock "syncPullResponse",
7
+ success: true
8
+ message: 'commands retrieved'
9
+ data:
10
+ commands: [
11
+ { name: 'pop-char', data: { }, timestamp: 2 },
12
+ { name: 'increment', data: { }, timestamp: 6 },
13
+ { name: 'increment', data: { }, timestamp: 8 }
14
+ ]
15
+ currentClients: [
16
+ { id: "2345", lastUpdate: 2 },
17
+ { id: "3456", lastUpdate: 8 }
18
+ ]
19
+ stableTimestamp: 2
@@ -0,0 +1,11 @@
1
+ Command = require 'leonidas/commands/command'
2
+ Client = require 'leonidas/client'
3
+
4
+ buildCommand = (timestamp, name="increment", data={})->
5
+ new Command(name, data, timestamp)
6
+
7
+ buildClient = ->
8
+ new Client("app 1", { integer: 1, string: "test" })
9
+
10
+ globalize(buildCommand, "buildCommand")
11
+ globalize(buildClient, "buildClient")
@@ -0,0 +1 @@
1
+ require "lib/jquery"
@@ -0,0 +1,20 @@
1
+ Client = require 'leonidas/client'
2
+
3
+ describe "Client", ->
4
+ client = null
5
+
6
+ beforeEach ->
7
+ client = new Client("app 1", { test: "test" })
8
+ client.activeState = { test: "different" }
9
+
10
+ describe "#revertState", ->
11
+
12
+ it "will revert the active state to the locked state", ->
13
+ client.revertState()
14
+ expect(client.activeState).toEqual { test: "test" }
15
+
16
+ describe "#lockState", ->
17
+
18
+ it "will lock the state to the active state", ->
19
+ client.lockState()
20
+ expect(client.lockedState).toEqual { test: "different" }
@@ -0,0 +1,69 @@
1
+ Organizer = require "leonidas/commands/organizer"
2
+ Processor = require "leonidas/commands/processor"
3
+ Stabilizer = require "leonidas/commands/stabilizer"
4
+ Synchronizer = require "leonidas/commands/synchronizer"
5
+ Commander = require 'leonidas/commander'
6
+
7
+ describe "Commander", ->
8
+ commander = null
9
+ client = null
10
+ organizer = null
11
+ synchronizer = null
12
+
13
+ beforeEach ->
14
+ client = buildClient()
15
+ organizer = new Organizer()
16
+ processor = new Processor([ new PopCharHandler(client.activeState) ])
17
+ stabilizer = new Stabilizer(client, organizer, processor)
18
+ synchronizer = new Synchronizer("http://mydomain.com/sync", client, organizer, stabilizer)
19
+ spyOn(synchronizer, "push")
20
+ spyOn(synchronizer, "pull")
21
+ commander = new Commander(organizer, processor, stabilizer, synchronizer)
22
+
23
+ describe "::default", ->
24
+
25
+ it "will return a default commander using the built in classes", ->
26
+ commander = Commander.default(client, [ new PopCharHandler("tim") ], "http://mydomain.com/sync")
27
+ expect(commander.constructor).toEqual Commander
28
+
29
+ describe "#startSync", ->
30
+
31
+ beforeEach ->
32
+ jasmine.Clock.useMock()
33
+
34
+ it "will set the synchronizer to begin pushing updates", ->
35
+ commander.startSync()
36
+ jasmine.Clock.tick(5500)
37
+ expect(synchronizer.push.calls.length).toEqual 5
38
+
39
+ it "will set the synchronizer to begin pulling updates", ->
40
+ commander.startSync()
41
+ jasmine.Clock.tick(11000)
42
+ expect(synchronizer.pull.calls.length).toEqual 2
43
+
44
+ describe "#stopSync", ->
45
+
46
+ beforeEach ->
47
+ jasmine.Clock.useMock()
48
+
49
+ it "will stop the synchronizer from pushing updates", ->
50
+ commander.startSync()
51
+ commander.stopSync()
52
+ jasmine.Clock.tick(10000)
53
+ expect(synchronizer.push).not.toHaveBeenCalled()
54
+
55
+ it "will stop the synchronizer from pulling updates", ->
56
+ commander.startSync()
57
+ commander.stopSync()
58
+ jasmine.Clock.tick(10000)
59
+ expect(synchronizer.pull).not.toHaveBeenCalled()
60
+
61
+ describe "#issueCommand", ->
62
+
63
+ it "will generate an unsynchronized command", ->
64
+ commander.issueCommand "pop-char", {}
65
+ expect(organizer.unsyncedCommands.length).toEqual 1
66
+
67
+ it "will run the command to update the local client state", ->
68
+ commander.issueCommand "pop-char", {}
69
+ expect(client.activeState.string).toEqual "tes"
@@ -0,0 +1,12 @@
1
+ Command = require 'leonidas/commands/command'
2
+
3
+ describe "Command", ->
4
+ command = null
5
+
6
+ beforeEach ->
7
+ command = new Command("test", { testData: "test" }, 25)
8
+
9
+ describe "#toHash", ->
10
+
11
+ it "will return the command serialized as a hash", ->
12
+ expect(command.toHash()).toEqual { name: "test", data: { testData: "test" }, timestamp: 25 }
@@ -0,0 +1,70 @@
1
+ Organizer = require 'leonidas/commands/organizer'
2
+
3
+ describe "Organizer", ->
4
+ command1 = command2 = command3 = command4 = null
5
+ organizer = null
6
+
7
+ beforeEach ->
8
+ command1 = buildCommand(1)
9
+ command2 = buildCommand(2, "pop-char")
10
+ command3 = buildCommand(3, "pop-char")
11
+ command4 = buildCommand(4)
12
+ organizer = new Organizer()
13
+
14
+ describe "#addCommand", ->
15
+
16
+ it "will add unsynced commands", ->
17
+ organizer.addCommand command1
18
+ expect(organizer.unsyncedCommands).toEqual [ command1 ]
19
+
20
+ it "will add synced commands", ->
21
+ organizer.addCommand command1, false
22
+ expect(organizer.syncedCommands).toEqual [ command1 ]
23
+
24
+ describe "#addCommands", ->
25
+
26
+ it "will add multiple unsynced commands", ->
27
+ organizer.addCommands [ command1, command2 ]
28
+ expect(organizer.unsyncedCommands).toEqual [ command1, command2 ]
29
+
30
+ it "will add multiple synced commands", ->
31
+ organizer.addCommands [ command1, command2 ], false
32
+ expect(organizer.syncedCommands).toEqual [ command1, command2 ]
33
+
34
+ describe "#markAsSynced", ->
35
+
36
+ it "will add the requested commands to the syncedCommands list", ->
37
+ organizer.addCommands [ command1, command2, command3 ]
38
+ organizer.markAsSynced [ command1, command2 ]
39
+ expect(organizer.syncedCommands).toEqual [ command1, command2 ]
40
+
41
+ it "will remove the requested commands from the unsyncedCommands list", ->
42
+ organizer.addCommands [ command1, command2, command3 ]
43
+ organizer.markAsSynced [ command1, command2 ]
44
+ expect(organizer.unsyncedCommands).toEqual [ command3 ]
45
+
46
+ describe "#markAsInactive", ->
47
+
48
+ it "will add the requested commands to the inactiveCommands list", ->
49
+ organizer.addCommands [ command1, command2, command3 ]
50
+ organizer.markAsInactive [ command1, command2 ]
51
+ expect(organizer.inactiveCommands).toEqual [ command1, command2 ]
52
+
53
+ it "will remove requested commands from the syncedCommands list", ->
54
+ organizer.addCommands [ command1, command2, command3 ], false
55
+ organizer.markAsInactive [ command1, command2 ]
56
+ expect(organizer.syncedCommands).toEqual [ command3 ]
57
+
58
+ describe "#activeCommands", ->
59
+
60
+ it "will return a concatenated list of synced and unsynced commands", ->
61
+ organizer.addCommands [ command2, command4 ]
62
+ organizer.addCommands [ command1, command3 ], false
63
+ expect(organizer.activeCommands().length).toEqual 4
64
+ expect(command in organizer.activeCommands()).toBeTruthy() for command in organizer.unsyncedCommands
65
+ expect(command in organizer.activeCommands()).toBeTruthy() for command in organizer.syncedCommands
66
+
67
+ it "will sort the list of commands by timestamp", ->
68
+ organizer.addCommands [ command2, command4 ]
69
+ organizer.addCommands [ command1, command3 ], false
70
+ expect(organizer.activeCommands()).toEqual [ command1, command2, command3, command4 ]
@@ -0,0 +1,22 @@
1
+ Client = require 'leonidas/client'
2
+ Processor = require 'leonidas/commands/processor'
3
+
4
+ describe "Processor", ->
5
+ processor = null
6
+ client = null
7
+
8
+ beforeEach ->
9
+ client = buildClient()
10
+ processor = new Processor([ new IncrementHandler(client.activeState), new PopCharHandler(client.activeState)])
11
+
12
+ describe "#processCommand", ->
13
+
14
+ it "will run a command", ->
15
+ processor.processCommand buildCommand(1)
16
+ expect(client.activeState).toEqual { integer: 2, string: "test" }
17
+
18
+ describe "#processCommands", ->
19
+
20
+ it "will run multiple commands", ->
21
+ processor.processCommands [ buildCommand(1), buildCommand(2, "pop-char") ]
22
+ expect(client.activeState).toEqual { integer: 2, string: "tes" }
@@ -0,0 +1,30 @@
1
+ Client = require 'leonidas/client'
2
+ Organizer = require 'leonidas/commands/organizer'
3
+ Processor = require 'leonidas/commands/processor'
4
+ Stabilizer = require 'leonidas/commands/stabilizer'
5
+
6
+ describe "Stabilizer", ->
7
+ stabilizer = null
8
+ client = null
9
+ organizer = null
10
+
11
+ beforeEach ->
12
+ client = buildClient()
13
+ organizer = new Organizer()
14
+ organizer.addCommands [ buildCommand(1), buildCommand(2, "pop-char"), buildCommand(3, "pop-char"), buildCommand(4) ], false
15
+ processor = new Processor([ new IncrementHandler(client.activeState), new PopCharHandler(client.activeState)])
16
+ stabilizer = new Stabilizer(client, organizer, processor)
17
+
18
+ describe "#stabilize", ->
19
+
20
+ it "will update the locked state to the state at the given timestamp", ->
21
+ stabilizer.stabilize 2
22
+ expect(client.lockedState).toEqual { integer: 2, string: "tes" }
23
+
24
+ it "will deactivate the stable commands in the command organizer", ->
25
+ stabilizer.stabilize 2
26
+ expect(organizer.activeCommands()).toEqual [ buildCommand(3, "pop-char"), buildCommand(4) ]
27
+
28
+ it "will process the remaining active commands to leave the active state entirely current", ->
29
+ stabilizer.stabilize 2
30
+ expect(client.activeState).toEqual { integer: 3, string: "te" }
@@ -0,0 +1,72 @@
1
+ Organizer = require 'leonidas/commands/organizer'
2
+ Processor = require 'leonidas/commands/processor'
3
+ Stabilizer = require 'leonidas/commands/stabilizer'
4
+ Synchronizer = require 'leonidas/commands/synchronizer'
5
+
6
+ describe "Synchronizer", ->
7
+ command1 = command4 = command5 = command7 = null
8
+ client = null
9
+ organizer = null
10
+ synchronizer = null
11
+
12
+ beforeEach ->
13
+ client = buildClient()
14
+ organizer = new Organizer()
15
+ command1 = buildCommand(1)
16
+ command4 = buildCommand(4, "pop-char")
17
+ command5 = buildCommand(5, "pop-char")
18
+ command7 = buildCommand(7)
19
+ processor = new Processor([ new IncrementHandler(client.activeState), new PopCharHandler(client.activeState)])
20
+ stabilizer = new Stabilizer(client, organizer, processor)
21
+ synchronizer = new Synchronizer("http://mydomain.com/sync", client, organizer, stabilizer)
22
+
23
+ describe "#push", ->
24
+
25
+ beforeEach ->
26
+ organizer.addCommands [ command1, command4 ]
27
+
28
+ describe "when successful", ->
29
+
30
+ it "will mark the commands pushed as synced", ->
31
+ spyOn($,"ajax").andCallFake( (params)-> params.success(mocks.syncPushResponse))
32
+ synchronizer.push()
33
+ expect(organizer.syncedCommands).toEqual [ command1, command4 ]
34
+ expect(organizer.unsyncedCommands).toEqual [ ]
35
+
36
+ it "will not mark unsynced commands added since push was called as synced", ->
37
+ spyOn($,"ajax").andCallFake( (params)->
38
+ organizer.addCommands [ command5, command7 ]
39
+ params.success(mocks.syncPushResponse)
40
+ )
41
+ synchronizer.push()
42
+ expect(organizer.syncedCommands).toEqual [ command1, command4 ]
43
+ expect(organizer.unsyncedCommands).toEqual [ command5, command7 ]
44
+
45
+ describe "#pull", ->
46
+
47
+ describe "when successful", ->
48
+
49
+ beforeEach ->
50
+ organizer.addCommands [ command1, command4 ], false
51
+ spyOn($,"ajax").andCallFake( (params)-> params.success(mocks.syncPullResponse))
52
+
53
+ it "will update the list of external clients and their latest timestamps", ->
54
+ synchronizer.pull()
55
+ expect(synchronizer.externalClients).toEqual [ { id: "2345", lastUpdate: 2 }, { id: "3456", lastUpdate: 8 } ]
56
+
57
+ it "will add the list of received commands as synced commands", ->
58
+ synchronizer.pull()
59
+ expect(organizer.syncedCommands.length).toEqual 3
60
+ expect(organizer.syncedCommands[0].toHash()).toEqual { name: 'pop-char', data: { }, timestamp: 4 },
61
+ expect(organizer.syncedCommands[1].toHash()).toEqual { name: 'increment', data: { }, timestamp: 6 },
62
+ expect(organizer.syncedCommands[2].toHash()).toEqual { name: 'increment', data: { }, timestamp: 8 }
63
+
64
+ it "will lock to a new stable state", ->
65
+ synchronizer.pull()
66
+ expect(client.lockedState).toEqual { integer: 2, string: "tes" }
67
+
68
+ it "will deactivate stable commands", ->
69
+ synchronizer.pull()
70
+ expect(organizer.inactiveCommands.length).toEqual 2
71
+ expect(organizer.inactiveCommands[0].toHash()).toEqual { name: 'increment', data: { }, timestamp: 1 },
72
+ expect(organizer.inactiveCommands[1].toHash()).toEqual { name: 'pop-char', data: { }, timestamp: 2 },
@@ -0,0 +1,4 @@
1
+ require 'leonidas'
2
+ %w(app commands persistence).each {|file| require_relative "support/classes/#{file}"}
3
+ require_relative 'support/mocks'
4
+ require_relative 'support/objects'
@@ -0,0 +1,26 @@
1
+ module TestClasses
2
+
3
+ class TestApp
4
+ include ::Leonidas::App::App
5
+
6
+ def initialize(name="app 1")
7
+ @name = name
8
+ @persist_state = false
9
+ @locked_state = { value: 0 }
10
+ @active_state = { value: 1 }
11
+ @connections = [ ]
12
+ @processor = ::Leonidas::Commands::Processor.new([ IncrementHandler.new(self), MultiplyHandler.new(self) ])
13
+ end
14
+
15
+ def state=(val)
16
+ @locked_state = val.dup
17
+ @active_state = val.dup
18
+ end
19
+
20
+ end
21
+
22
+ class TestRepositoryContainer
23
+ include ::Leonidas::App::AppRepository
24
+ end
25
+
26
+ end
@@ -0,0 +1,52 @@
1
+ module TestClasses
2
+
3
+ class IncrementHandler
4
+ include ::Leonidas::Commands::Handler
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def handles?(command)
11
+ command.name == "increment"
12
+ end
13
+
14
+ def run(command)
15
+ @app.current_state[:value] += command.data[:increment_by]
16
+ end
17
+
18
+ def persist(command)
19
+ TestClasses::PersistentState.value += command.data[:increment_by]
20
+ end
21
+ end
22
+
23
+ class MultiplyHandler
24
+ include ::Leonidas::Commands::Handler
25
+
26
+ def initialize(app)
27
+ @app = app
28
+ end
29
+
30
+ def handles?(command)
31
+ command.name == "multiply"
32
+ end
33
+
34
+ def run(command)
35
+ @app.current_state[:value] *= command.data[:multiply_by]
36
+ end
37
+
38
+ def persist(command)
39
+ TestClasses::PersistentState.value *= command.data[:multiply_by]
40
+ end
41
+ end
42
+
43
+ class TestAggregator
44
+ include ::Leonidas::Commands::Aggregator
45
+
46
+ def initialize
47
+ @active_commands = [ ]
48
+ @inactive_commands = [ ]
49
+ end
50
+ end
51
+
52
+ end