diff --git a/Gemfile b/Gemfile index ee42dc4e..718693e0 100644 --- a/Gemfile +++ b/Gemfile @@ -71,8 +71,8 @@ group :test do gem 'rspec-rails' gem 'database_cleaner' gem 'capybara' #, '2.0.3' - gem 'selenium-webdriver' - #gem 'capybara-webkit', '~>0.14.2' # version 1.1.0 does not yet compile in mavericks + #gem 'selenium-webdriver' + gem 'capybara-webkit' #, '~>0.14.2' # version 1.1.0 does not yet compile in mavericks gem 'turnip' gem 'launchy' gem 'fuubar' diff --git a/Gemfile.lock b/Gemfile.lock index 09c3ebc0..32a5b74b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,8 +99,9 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - childprocess (0.3.9) - ffi (~> 1.0, >= 1.0.11) + capybara-webkit (1.1.0) + capybara (~> 2.0, >= 2.0.2) + json chunky_png (1.2.9) climate_control (0.0.3) activesupport (>= 3.0) @@ -182,20 +183,19 @@ GEM faye-websocket (0.7.1) eventmachine (>= 0.12.0) websocket-driver (>= 0.3.1) - ffi (1.9.3) fssm (0.2.10) - fuubar (1.3.0) + fuubar (1.3.2) rspec (>= 2.14.0, < 3.1.0) ruby-progressbar (~> 1.3) gherkin (2.12.2) multi_json (~> 1.3) haml (4.0.4) tilt - haml-rails (0.5.2) - actionpack (~> 4.0.1) - activesupport (~> 4.0.1) + haml-rails (0.5.3) + actionpack (>= 4.0.1) + activesupport (>= 4.0.1) haml (>= 3.1, < 5.0) - railties (~> 4.0.1) + railties (>= 4.0.1) handlebars-source (1.1.2) hashie (2.0.5) hike (1.2.3) @@ -277,7 +277,7 @@ GEM activesupport (= 4.0.2) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.0) + rake (10.1.1) rest-client (1.6.7) mime-types (>= 1.16) rqrcode (0.4.2) @@ -297,17 +297,11 @@ GEM rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) ruby-progressbar (1.3.2) - rubyzip (1.1.0) - sass (3.2.12) + sass (3.2.13) sass-rails (4.0.1) railties (>= 4.0.0, < 5.0) sass (>= 3.1.10) sprockets-rails (~> 2.0.0) - selenium-webdriver (2.38.0) - childprocess (>= 0.2.5) - multi_json (~> 1.0) - rubyzip (~> 1.0) - websocket (~> 1.0.4) simple_form (3.0.1) actionpack (>= 4.0.0, < 4.1) activemodel (>= 4.0.0, < 4.1) @@ -348,12 +342,11 @@ GEM gherkin (>= 2.5) rspec (~> 2.0) tzinfo (0.3.38) - uglifier (2.3.3) + uglifier (2.4.0) execjs (>= 0.3.0) json (>= 1.8.0) warden (1.2.3) rack (>= 1.0) - websocket (1.0.7) websocket-driver (0.3.1) xpath (2.0.0) nokogiri (~> 1.3) @@ -366,6 +359,7 @@ DEPENDENCIES bootstrap-sass (~> 2.3.2.2) bourbon capybara + capybara-webkit cmtool! coffee-rails compass-rails @@ -395,7 +389,6 @@ DEPENDENCIES rqrcode rspec-rails sass-rails - selenium-webdriver simple_form simply_stored! slim-rails diff --git a/app/controllers/suppliers/lists_controller.rb b/app/controllers/suppliers/lists_controller.rb index 6abe571e..8e47dc2c 100644 --- a/app/controllers/suppliers/lists_controller.rb +++ b/app/controllers/suppliers/lists_controller.rb @@ -73,7 +73,7 @@ module Suppliers # POST /lists # POST /lists.json def create - @list = List.new(params[:list]) + @list = List.new(list_params) @list.supplier = current_supplier respond_to do |format| @@ -102,7 +102,7 @@ module Suppliers @list = List.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) respond_to do |format| - if @list.update_attributes(params[:list]) + if @list.update_attributes(list_params) format.html { redirect_to [:suppliers, @list], notice: t('action.update.successfull', model: List.model_name.human) } format.json { head :no_content } format.js { head :no_content } @@ -126,5 +126,11 @@ module Suppliers format.json { head :no_content } end end + + private + + def list_params + params.require(:list).permit(:state, :needs_help, :needs_payment, :closed_at, :join_requests, :price, :is_paid, :paid_at, :table_id, :section_id) + end end end diff --git a/app/controllers/suppliers/product_categories_controller.rb b/app/controllers/suppliers/product_categories_controller.rb index c6162788..785ae422 100644 --- a/app/controllers/suppliers/product_categories_controller.rb +++ b/app/controllers/suppliers/product_categories_controller.rb @@ -42,7 +42,7 @@ module Suppliers # POST /product_categories # POST /product_categories.json def create - @product_category = ProductCategory.new(params[:product_category]) + @product_category = ProductCategory.new(product_category_params) @product_category.supplier = current_supplier respond_to do |format| @@ -62,7 +62,7 @@ module Suppliers @product_category = ProductCategory.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) respond_to do |format| - if @product_category.update_attributes(params[:product_category]) + if @product_category.update_attributes(product_category_params) format.html { redirect_to [:suppliers, :product_categories], notice: t('action.update.successfull', model: ProductCategory.model_name.human) } format.json { head :no_content } else @@ -92,5 +92,11 @@ module Suppliers CouchPotato.database.couchrest_database.bulk_save(@product_categories) render nothing: true end + + private + + def product_category_params + params.require(:product_category).permit(:name, :start_from, :end_on, :full_day, product_ids: [], week_days: []) + end end end diff --git a/app/controllers/suppliers/products_controller.rb b/app/controllers/suppliers/products_controller.rb index 73ef52da..2e1fd2cb 100644 --- a/app/controllers/suppliers/products_controller.rb +++ b/app/controllers/suppliers/products_controller.rb @@ -43,12 +43,12 @@ module Suppliers # POST /products # POST /products.json def create - @product = Product.new(params[:product]) + @product = Product.new(product_params) @product.supplier = current_supplier respond_to do |format| if @product.save - format.html { redirect_to [:suppliers, @product], notice: t('action.create.successfull', model: Product.model_name.human) } + format.html { redirect_to [:suppliers, :products], notice: t('action.create.successfull', model: Product.model_name.human) } format.json { render json: @product, status: :created, location: @product } else format.html { render action: "new" } @@ -63,8 +63,8 @@ module Suppliers @product = Product.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) respond_to do |format| - if @product.update_attributes(params[:product]) - format.html { redirect_to [:suppliers, @product], notice: t('action.update.successfull', model: Product.model_name.human) } + if @product.update_attributes(product_params) + format.html { redirect_to [:suppliers, :products], notice: t('action.update.successfull', model: Product.model_name.human) } format.json { head :no_content } else format.html { render action: "edit" } @@ -90,5 +90,11 @@ module Suppliers product_categories = ProductCategory.for_supplier_in_time(current_supplier, @time) render json: {categories: product_categories.map(&:to_client_format).select(&:present?)} end + + private + + def product_params + params.require(:product).permit(:name, :code, :price, product_category_ids: []) + end end end diff --git a/app/controllers/suppliers/sections_controller.rb b/app/controllers/suppliers/sections_controller.rb index c1b43490..747de876 100644 --- a/app/controllers/suppliers/sections_controller.rb +++ b/app/controllers/suppliers/sections_controller.rb @@ -54,7 +54,7 @@ module Suppliers # POST /sections # POST /sections.json def create - @section = Section.new(params[:section]) + @section = Section.new(section_params) @section.supplier = current_supplier respond_to do |format| @@ -74,7 +74,7 @@ module Suppliers @section = Section.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) respond_to do |format| - if @section.update_attributes(params[:section]) + if @section.update_attributes(section_params) format.html { redirect_to [:suppliers, @section], notice: t('action.update.successfull', model: Section.model_name.human) } format.json { head :no_content } else @@ -157,5 +157,11 @@ module Suppliers @orders = @list ? @list.active_orders : [] render layout: false end + + private + + def section_params + params.require(:section).permit(:title, :path, :width, :height) + end end end diff --git a/app/controllers/suppliers/tables_controller.rb b/app/controllers/suppliers/tables_controller.rb index 7265e6b4..ec184861 100644 --- a/app/controllers/suppliers/tables_controller.rb +++ b/app/controllers/suppliers/tables_controller.rb @@ -43,7 +43,7 @@ module Suppliers # POST /tables # POST /tables.json def create - @table = Table.new(params[:table]) + @table = Table.new(table_params) @table.supplier = current_supplier respond_to do |format| @@ -63,8 +63,8 @@ module Suppliers @table= Table.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) respond_to do |format| - if @table.update_attributes(params[:table]) - format.html { redirect_to [:suppliers, @table], notice: t('action.update.successfull', model: Table.model_name.human) } + if @table.update_attributes(table_params) + format.html { redirect_to [:suppliers, @table.section || @table], notice: t('action.update.successfull', model: Table.model_name.human) } format.json { head :no_content } format.js { head :no_content } else @@ -92,5 +92,11 @@ module Suppliers @tables.select!{|t| t.section_id == params[:section_id]} if params[:section_id].present? render layout: 'qr_sheet' end + + private + + def table_params + params.require(:table).permit(:number, :section_id, :position_x, :position_y) + end end end diff --git a/app/models/product.rb b/app/models/product.rb index 5e4a576f..dd5a183a 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -21,15 +21,25 @@ class Product def product_category_ids=(ids) @product_category_ids = ids.select(&:present?) + is_dirty end private + def persist_product_category_ids - return unless @product_category_ids.present? - database.load(@product_category_ids).each do |product_category| - product_category.product_ids ||= [] - product_category.product_ids |= [id] - product_category.save + @product_category_ids ||= [] + existing_product_categories = product_categories + + # do nothing if nothing has changed + return if @product_category_ids == existing_product_categories.map(&:id) + + # clear removed product categories + existing_product_categories.reject{|pc| @product_category_ids.include?(pc.id) }.each{|pc| pc.remove_product(self) } + + + # Add product to newly added product categories + database.load(@product_category_ids - existing_product_categories.map(&:id)).each do |product_category| + product_category.add_product(self) end end diff --git a/app/models/section.rb b/app/models/section.rb index 0e907461..375e7a04 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -130,6 +130,7 @@ class Section end return saves.all? end + def arrange_tables_in_columns_of(n) return unless n.present? n = n.to_i diff --git a/app/views/suppliers/products/_form.html.slim b/app/views/suppliers/products/_form.html.slim index 1bb70909..5596cc9f 100644 --- a/app/views/suppliers/products/_form.html.slim +++ b/app/views/suppliers/products/_form.html.slim @@ -13,6 +13,6 @@ br .form-actions - = f.button :submit, class: 'btn-primary' + = f.button :submit, class: 'btn-primary save-product-button' ' = link_to t("helpers.links.cancel"), suppliers_products_path, class: 'btn' diff --git a/app/views/suppliers/products/index.html.slim b/app/views/suppliers/products/index.html.slim index 171c3788..0f4f5d73 100644 --- a/app/views/suppliers/products/index.html.slim +++ b/app/views/suppliers/products/index.html.slim @@ -14,14 +14,14 @@ th.actions data-t="helpers.actions.title" =t 'helpers.actions.title' tbody - @products.each do |product| - tr + tr class="product-row-#{product.id}" td.link= link_to product.name, [:suppliers, product] td= product.code td.currency=currency product.price td.link= product.category_links namespace: :suppliers td.timestamp data-time=product.created_at.utc.iso8601 td.actions - = link_to t('helpers.links.edit'), [:edit, :suppliers, product], class: 'btn btn-mini' + = link_to t('helpers.links.edit'), [:edit, :suppliers, product], class: 'btn btn-mini edit-resource-button' ' = link_to t("helpers.links.destroy"), [:suppliers, product], method: :delete, data: {confirm: are_you_sure? }, class: 'btn btn-mini btn-danger' - else diff --git a/app/views/suppliers/tables/_form.html.slim b/app/views/suppliers/tables/_form.html.slim index 8bd0caf6..615cf0ab 100644 --- a/app/views/suppliers/tables/_form.html.slim +++ b/app/views/suppliers/tables/_form.html.slim @@ -9,6 +9,6 @@ .controls = f.collection_select :section_id, current_supplier.sections, :id, :title, include_blank: "[#{t('supplier.tables.has_no_section')}]" .form-actions - = f.submit nil, class: 'btn btn-primary' + = f.submit nil, class: 'btn btn-primary save-table-button' ' = link_to t("helpers.links.cancel"), suppliers_tables_path, class: 'btn' diff --git a/spec/acceptance/suppliers/product_generation.feature b/spec/acceptance/suppliers/product_generation.feature new file mode 100644 index 00000000..948cf766 --- /dev/null +++ b/spec/acceptance/suppliers/product_generation.feature @@ -0,0 +1,18 @@ +Feature: Adding product + + Scenario: Adding a product + Given there is a confirmed and open supplier + And I am signed in as supplier + And there are 2 supplier product categories + And the supplier visits the new product page + And the supplier fills in the new product form selecting the first product category + When the supplier submits the product form + Then the new product with proper properties linked to the first product category should have been created + And the supplier should be on the product overview path + When the supplier clicks on the edit product button + And the supplier unchecks the first product category and checks the last product category + And the supplier submits the product form + Then the supplier product should only be linked to the last product category + And the supplier should be on the product overview path + + diff --git a/spec/acceptance/suppliers/table_generation.feature b/spec/acceptance/suppliers/table_generation.feature new file mode 100644 index 00000000..b2e3f2fe --- /dev/null +++ b/spec/acceptance/suppliers/table_generation.feature @@ -0,0 +1,18 @@ +Feature: Adding product + + Scenario: Adding a product + Given there is a confirmed and open supplier + And I am signed in as supplier + And there are 2 supplier sections + And the supplier visits the new table page + And the supplier fills in the new table form selecting the first section + When the supplier submits the table form + Then the new supplier table with proper properties should have been created + And the supplier should be on the table section page + When the supplier visits the edit table page + And the supplier changes the table number to 9 and section to the last section + And the supplier submits the table form + Then the supplier table should have number 9 and be linked to the last section + And the supplier should be on the table section page + + diff --git a/spec/acceptance_steps/suppliers/product_category_steps.rb b/spec/acceptance_steps/suppliers/product_category_steps.rb index 1291d70c..09ec4223 100644 --- a/spec/acceptance_steps/suppliers/product_category_steps.rb +++ b/spec/acceptance_steps/suppliers/product_category_steps.rb @@ -49,3 +49,7 @@ step "the the product category is active on wednesday and only linked to the las @product_category.week_days.should == [0, 0, 0, 1, 0, 0, 0] @product_category.product_ids.should == [@products.last.id] end + +step "there are :count supplier product categories" do |count| + @product_categories = create_list :product_category, count.to_i, supplier: @supplier +end diff --git a/spec/acceptance_steps/suppliers/product_steps.rb b/spec/acceptance_steps/suppliers/product_steps.rb index 0e92eddf..1e22ef3b 100644 --- a/spec/acceptance_steps/suppliers/product_steps.rb +++ b/spec/acceptance_steps/suppliers/product_steps.rb @@ -1,4 +1,50 @@ +step "the supplier visits the new product page" do + visit new_suppliers_product_path +end + step "there are :count supplier products" do |count| @products = create_list :product, count.to_i, supplier: @supplier end + +step "the supplier fills in the new product form selecting the first product category" do + find('#product_name').set 'New product' + find('#product_code').set 'NL0487' + find('#product_price').set '6.42' + find("#product-category-checker-#{@product_categories.first.id}").set true +end + +step "the supplier submits the product form" do + find('.save-product-button').click +end + + +step "the new product with proper properties linked to the first product category should have been created" do + sleep 1 + @product = Product.find_by_name 'New product' + @product.code.should == 'NL0487' + @product.price.should == 6.42 + @product_categories.each(&:reload) + @product.product_categories.should == [@product_categories.first] +end + +step "the supplier should be on the product overview path" do + route_should_be 'suppliers/products#index' +end + +step "the supplier clicks on the edit product button" do + within ".product-row-#{@product.id}" do + find('.edit-resource-button').click + end +end + +step "the supplier unchecks the first product category and checks the last product category" do + find("#product-category-checker-#{@product_categories.first.id}").set false + find("#product-category-checker-#{@product_categories.last.id}").set true +end + +step "the supplier product should only be linked to the last product category" do + @product.reload + @product_categories.each(&:reload) + @product.product_categories.should == [@product_categories.last] +end diff --git a/spec/acceptance_steps/suppliers/section_global_steps.rb b/spec/acceptance_steps/suppliers/section_global_steps.rb new file mode 100644 index 00000000..b8301850 --- /dev/null +++ b/spec/acceptance_steps/suppliers/section_global_steps.rb @@ -0,0 +1,3 @@ +step "there are 2 supplier sections" do + @sections = create_list :section, 2, supplier: @supplier +end diff --git a/spec/acceptance_steps/suppliers/table_steps.rb b/spec/acceptance_steps/suppliers/table_steps.rb new file mode 100644 index 00000000..25eb79c1 --- /dev/null +++ b/spec/acceptance_steps/suppliers/table_steps.rb @@ -0,0 +1,40 @@ + +step "the supplier visits the new table page" do + visit new_suppliers_table_path +end + +step "the supplier fills in the new table form selecting the first section" do + find('#table_number').set '7' + section_option = find(%|option[value="#{@sections.first.id}"]|) + select section_option.text, from: 'table_section_id' +end + +step "the supplier submits the table form" do + find('.save-table-button').click +end + +step "the new supplier table with proper properties should have been created" do + @table = Table.find_by_number 7 + @table.section_id.should == @sections.first.id +end + +step "the supplier should be on the table section page" do + @table.reload + route_should_be 'suppliers/sections#show', id: @table.section_id +end + +step "the supplier visits the edit table page" do + visit edit_suppliers_table_path(@table) +end + +step "the supplier changes the table number to :number and section to the last section" do |number| + find('#table_number').set number + section_option = find(%|option[value="#{@sections.last.id}"]|) + select section_option.text, from: 'table_section_id' +end + +step "the supplier table should have number :number and be linked to the last section" do |number| + @table.reload + @table.number.should == number.to_i + @table.section_id.should == @sections.last.id +end diff --git a/spec/models/product_spec.rb b/spec/models/product_spec.rb index 42c46d72..32424c80 100644 --- a/spec/models/product_spec.rb +++ b/spec/models/product_spec.rb @@ -2,5 +2,33 @@ require 'spec_helper' describe Product do + describe 'update product category through ids' do + + it 'works' do + supplier = create :supplier + pc1 = create :product_category, supplier: supplier + pc2 = create :product_category, supplier: supplier + + product = build :product, supplier: supplier, product_category_ids: [pc1.id] + product.save.should be_true + product.reload + pc1.reload + pc2.reload + pc1.product_ids.should == [product.id] + pc2.product_ids.should_not be_present + + product.update_attributes product_category_ids: [pc2.id] + product.reload + pc1.reload + pc2.reload + product.product_categories.should == [pc2] + + # empty set also works + product.update_attributes product_category_ids: [''] + product.reload + product.product_categories.should be_empty + end + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b61c9e27..793f8c6d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,8 +14,8 @@ Dir.glob("spec/acceptance_steps/**/*steps.rb") { |f| load f, true } I18n.locale =I18n.default_locale Devise.stretches = 1 -#Capybara.javascript_driver = :webkit -Capybara.javascript_driver = :selenium +Capybara.javascript_driver = :webkit +#Capybara.javascript_driver = :selenium module FactoryAttributesFor def attributes_for(obj, options={}) @@ -27,20 +27,6 @@ module SpecSelectorHelpers '.navbar-fixed-top' end - # allows tests like: - # route_should_be 'agama_groups#index' - def route_should_be(route_def) - route_hash = case route_def - when String - controller_name, action_name = route_def.split('#') - #action_name = 'index' unless action_name.present? - {controller: controller_name, action: action_name} - else - route_def - end - Rails.application.routes.recognize_path(page.current_path).should include route_hash - end - # Uses the click_on method for capybara def click_on_translation(key) text = I18n.t(key) diff --git a/spec/support/route_helpers.rb b/spec/support/route_helpers.rb index 30453f30..2558bee5 100644 --- a/spec/support/route_helpers.rb +++ b/spec/support/route_helpers.rb @@ -1,7 +1,7 @@ module SpecRouteHelpers # allows tests like: # route_should_be 'agama_groups#index' - def route_should_be(route_def) + def route_should_be(route_def, options = {}) route_hash = case route_def when String controller_name, action_name = route_def.split('#') @@ -10,6 +10,7 @@ module SpecRouteHelpers else route_def end + route_hash.merge! options Rails.application.routes.recognize_path(page.current_path).should include route_hash end end diff --git a/vendor/assets/javascripts/faye.js b/vendor/assets/javascripts/faye.js index 8ff931d7..eb3daa2f 100644 --- a/vendor/assets/javascripts/faye.js +++ b/vendor/assets/javascripts/faye.js @@ -1,2 +1,2541 @@ -var Faye=(typeof Faye==='object')?Faye:{};if(typeof window!=='undefined')window.Faye=Faye;Faye.extend=function(a,b,d){if(!b)return a;for(var f in b){if(!b.hasOwnProperty(f))continue;if(a.hasOwnProperty(f)&&d===false)continue;if(a[f]!==b[f])a[f]=b[f]}return a};Faye.extend(Faye,{VERSION:'0.8.6',BAYEUX_VERSION:'1.0',ID_LENGTH:160,JSONP_CALLBACK:'jsonpcallback',CONNECTION_TYPES:['long-polling','cross-origin-long-polling','callback-polling','websocket','eventsource','in-process'],MANDATORY_CONNECTION_TYPES:['long-polling','callback-polling','in-process'],ENV:(function(){return this})(),random:function(a){a=a||this.ID_LENGTH;if(a>32){var b=Math.ceil(a/32),d='';while(b--)d+=this.random(32);var f=d.split(''),g='';while(f.length>0)g+=f.pop();return g}var h=Math.pow(2,a)-1,i=h.toString(36).length,d=Math.floor(Math.random()*h).toString(36);while(d.length0)j();k=false};var n=function(){i+=1;l()};n()},toJSON:function(a){if(this.stringify)return this.stringify(a,function(key,value){return(this[key]instanceof Array)?this[key]:value});return JSON.stringify(a)},logger:function(a){if(typeof console!=='undefined')console.log(a)},timestamp:function(){var b=new Date(),d=b.getFullYear(),f=b.getMonth()+1,g=b.getDate(),h=b.getHours(),i=b.getMinutes(),k=b.getSeconds();var j=function(a){return a<10?'0'+a:String(a)};return j(d)+'-'+j(f)+'-'+j(g)+' '+j(h)+':'+j(i)+':'+j(k)}});Faye.Class=function(a,b){if(typeof a!=='function'){b=a;a=Object}var d=function(){if(!this.initialize)return this;return this.initialize.apply(this,arguments)||this};var f=function(){};f.prototype=a.prototype;d.prototype=new f();Faye.extend(d.prototype,b);return d};Faye.Namespace=Faye.Class({initialize:function(){this._e={}},exists:function(a){return this._e.hasOwnProperty(a)},generate:function(){var a=Faye.random();while(this._e.hasOwnProperty(a))a=Faye.random();return this._e[a]=a},release:function(a){delete this._e[a]}});Faye.Error=Faye.Class({initialize:function(a,b,d){this.code=a;this.params=Array.prototype.slice.call(b);this.message=d},toString:function(){return this.code+':'+this.params.join(',')+':'+this.message}});Faye.Error.parse=function(a){a=a||'';if(!Faye.Grammar.ERROR.test(a))return new this(null,[],a);var b=a.split(':'),d=parseInt(b[0]),f=b[1].split(','),a=b[2];return new this(d,f,a)};Faye.Error.versionMismatch=function(){return new this(300,arguments,"Version mismatch").toString()};Faye.Error.conntypeMismatch=function(){return new this(301,arguments,"Connection types not supported").toString()};Faye.Error.extMismatch=function(){return new this(302,arguments,"Extension mismatch").toString()};Faye.Error.badRequest=function(){return new this(400,arguments,"Bad request").toString()};Faye.Error.clientUnknown=function(){return new this(401,arguments,"Unknown client").toString()};Faye.Error.parameterMissing=function(){return new this(402,arguments,"Missing required parameter").toString()};Faye.Error.channelForbidden=function(){return new this(403,arguments,"Forbidden channel").toString()};Faye.Error.channelUnknown=function(){return new this(404,arguments,"Unknown channel").toString()};Faye.Error.channelInvalid=function(){return new this(405,arguments,"Invalid channel").toString()};Faye.Error.extUnknown=function(){return new this(406,arguments,"Unknown extension").toString()};Faye.Error.publishFailed=function(){return new this(407,arguments,"Failed to publish").toString()};Faye.Error.serverError=function(){return new this(500,arguments,"Internal server error").toString()};Faye.Deferrable={callback:function(a,b){if(!a)return;if(this._w==='succeeded')return a.apply(b,this._j);this._k=this._k||[];this._k.push([a,b])},timeout:function(a,b){var d=this;var f=Faye.ENV.setTimeout(function(){d.setDeferredStatus('failed',b)},a*1000);this._x=f},errback:function(a,b){if(!a)return;if(this._w==='failed')return a.apply(b,this._j);this._l=this._l||[];this._l.push([a,b])},setDeferredStatus:function(){if(this._x)Faye.ENV.clearTimeout(this._x);var a=Array.prototype.slice.call(arguments),b=a.shift(),d;this._w=b;this._j=a;if(b==='succeeded')d=this._k;else if(b==='failed')d=this._l;if(!d)return;var f;while(f=d.shift())f[0].apply(f[1],this._j)}};Faye.Publisher={countListeners:function(a){if(!this._3||!this._3[a])return 0;return this._3[a].length},bind:function(a,b,d){this._3=this._3||{};var f=this._3[a]=this._3[a]||[];f.push([b,d])},unbind:function(a,b,d){if(!this._3||!this._3[a])return;if(!b){delete this._3[a];return}var f=this._3[a],g=f.length;while(g--){if(b!==f[g][0])continue;if(d&&f[g][1]!==d)continue;f.splice(g,1)}},trigger:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();if(!this._3||!this._3[b])return;var d=this._3[b].slice(),f;for(var g=0,h=d.length;gd[b])return;var a=Array.prototype.slice.apply(a),f=' ['+b.toUpperCase()+'] [Faye',g=this.className,h=a.shift().replace(/\?/g,function(){try{return Faye.toJSON(a.shift())}catch(e){return'[Object]'}});for(var i in Faye){if(g)continue;if(typeof Faye[i]!=='function')continue;if(this instanceof Faye[i])g=i}if(g)f+='.'+g;f+='] ';Faye.logger(Faye.timestamp()+f+h)}};(function(){for(var d in Faye.Logging.LOG_LEVELS)(function(a,b){Faye.Logging[a]=function(){this.log(arguments,a)}})(d,Faye.Logging.LOG_LEVELS[d])})();Faye.Grammar={LOWALPHA:/^[a-z]$/,UPALPHA:/^[A-Z]$/,ALPHA:/^([a-z]|[A-Z])$/,DIGIT:/^[0-9]$/,ALPHANUM:/^(([a-z]|[A-Z])|[0-9])$/,MARK:/^(\-|\_|\!|\~|\(|\)|\$|\@)$/,STRING:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*$/,TOKEN:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+$/,INTEGER:/^([0-9])+$/,CHANNEL_SEGMENT:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+$/,CHANNEL_SEGMENTS:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*$/,CHANNEL_NAME:/^\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*$/,WILD_CARD:/^\*{1,2}$/,CHANNEL_PATTERN:/^(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*\/\*{1,2}$/,VERSION_ELEMENT:/^(([a-z]|[A-Z])|[0-9])(((([a-z]|[A-Z])|[0-9])|\-|\_))*$/,VERSION:/^([0-9])+(\.(([a-z]|[A-Z])|[0-9])(((([a-z]|[A-Z])|[0-9])|\-|\_))*)*$/,CLIENT_ID:/^((([a-z]|[A-Z])|[0-9]))+$/,ID:/^((([a-z]|[A-Z])|[0-9]))+$/,ERROR_MESSAGE:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*$/,ERROR_ARGS:/^(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*(,(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)*$/,ERROR_CODE:/^[0-9][0-9][0-9]$/,ERROR:/^([0-9][0-9][0-9]:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*(,(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)*:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*|[0-9][0-9][0-9]::(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)$/};Faye.Extensible={addExtension:function(a){this._6=this._6||[];this._6.push(a);if(a.added)a.added(this)},removeExtension:function(a){if(!this._6)return;var b=this._6.length;while(b--){if(this._6[b]!==a)continue;this._6.splice(b,1);if(a.removed)a.removed(this)}},pipeThroughExtensions:function(d,f,g,h){this.debug('Passing through ? extensions: ?',d,f);if(!this._6)return g.call(h,f);var i=this._6.slice();var k=function(a){if(!a)return g.call(h,a);var b=i.shift();if(!b)return g.call(h,a);if(b[d])b[d](a,k);else k(a)};k(f)}};Faye.extend(Faye.Extensible,Faye.Logging);Faye.Channel=Faye.Class({initialize:function(a){this.id=this.name=a},push:function(a){this.trigger('message',a)},isUnused:function(){return this.countListeners('message')===0}});Faye.extend(Faye.Channel.prototype,Faye.Publisher);Faye.extend(Faye.Channel,{HANDSHAKE:'/meta/handshake',CONNECT:'/meta/connect',SUBSCRIBE:'/meta/subscribe',UNSUBSCRIBE:'/meta/unsubscribe',DISCONNECT:'/meta/disconnect',META:'meta',SERVICE:'service',expand:function(a){var b=this.parse(a),d=['/**',a];var f=b.slice();f[f.length-1]='*';d.push(this.unparse(f));for(var g=1,h=b.length;g=Math.pow(2,32))this._g=0;return this._g.toString(36)},_F:function(a){Faye.extend(this._8,a);if(this._8.reconnect===this.HANDSHAKE&&this._1!==this.DISCONNECTED){this._1=this.UNCONNECTED;this._0=null;this._B()}},_G:function(a){if(!a.channel||a.data===undefined)return;this.info('Client ? calling listeners for ? with ?',this._0,a.channel,a.data);this._2.distributeMessage(a)},_I:function(){if(!this._q)return;this._q=null;this.info('Closed connection for ?',this._0)},_B:function(){this._I();var a=this;Faye.ENV.setTimeout(function(){a.connect()},this._8.interval)}});Faye.extend(Faye.Client.prototype,Faye.Deferrable);Faye.extend(Faye.Client.prototype,Faye.Publisher);Faye.extend(Faye.Client.prototype,Faye.Logging);Faye.extend(Faye.Client.prototype,Faye.Extensible);Faye.Transport=Faye.extend(Faye.Class({MAX_DELAY:0.0,batching:true,initialize:function(a,b){this._7=a;this.endpoint=b;this._c=[]},close:function(){},send:function(a,b){this.debug('Client ? sending message to ?: ?',this._7._0,this.endpoint,a);if(!this.batching)return this.request([a],b);this._c.push(a);this._J=b;if(a.channel===Faye.Channel.HANDSHAKE)return this.addTimeout('publish',0.01,this.flush,this);if(a.channel===Faye.Channel.CONNECT)this._r=a;if(this.shouldFlush&&this.shouldFlush(this._c))return this.flush();this.addTimeout('publish',this.MAX_DELAY,this.flush,this)},flush:function(){this.removeTimeout('publish');if(this._c.length>1&&this._r)this._r.advice={timeout:0};this.request(this._c,this._J);this._r=null;this._c=[]},receive:function(a){this.debug('Client ? received from ?: ?',this._7._0,this.endpoint,a);for(var b=0,d=a.length;b=200&&b<300)||b===304||b===1223);if(!d){m();h();return k.trigger('down')}try{a=JSON.parse(j.responseText)}catch(e){}m();if(a){k.receive(a);k.trigger('up')}else{h();k.trigger('down')}};j.send(Faye.toJSON(f))}}),{isUsable:function(a,b,d,f){d.call(f,Faye.URI.parse(b).isSameOrigin())}});Faye.Transport.register('long-polling',Faye.Transport.XHR);Faye.Transport.CORS=Faye.extend(Faye.Class(Faye.Transport,{request:function(b,d){var f=Faye.ENV.XDomainRequest?XDomainRequest:XMLHttpRequest,g=new f(),h=this.retry(b,d),i=this;g.open('POST',this.endpoint,true);if(g.setRequestHeader)g.setRequestHeader('Pragma','no-cache');var k=function(){if(!g)return false;g.onload=g.onerror=g.ontimeout=g.onprogress=null;g=null;Faye.ENV.clearTimeout(l);return true};g.onload=function(){var a=null;try{a=JSON.parse(g.responseText)}catch(e){}k();if(a){i.receive(a);i.trigger('up')}else{h();i.trigger('down')}};var j=function(){k();h();i.trigger('down')};var l=Faye.ENV.setTimeout(j,1.5*1000*d);g.onerror=j;g.ontimeout=j;g.onprogress=function(){};g.send('message='+encodeURIComponent(Faye.toJSON(b)))}}),{isUsable:function(a,b,d,f){if(Faye.URI.parse(b).isSameOrigin())return d.call(f,false);if(Faye.ENV.XDomainRequest)return d.call(f,Faye.URI.parse(b).protocol===Faye.URI.parse(Faye.ENV.location).protocol);if(Faye.ENV.XMLHttpRequest){var g=new Faye.ENV.XMLHttpRequest();return d.call(f,g.withCredentials!==undefined)}return d.call(f,false)}});Faye.Transport.register('cross-origin-long-polling',Faye.Transport.CORS);Faye.Transport.JSONP=Faye.extend(Faye.Class(Faye.Transport,{shouldFlush:function(a){var b={message:Faye.toJSON(a),jsonp:'__jsonp'+Faye.Transport.JSONP._v+'__'};var d=Faye.URI.parse(this.endpoint,b).toURL();return d.length>=Faye.Transport.MAX_URL_LENGTH},request:function(b,d){var f={message:Faye.toJSON(b)},g=document.getElementsByTagName('head')[0],h=document.createElement('script'),i=Faye.Transport.JSONP.getCallbackName(),k=Faye.URI.parse(this.endpoint,f),j=this.retry(b,d),l=this;Faye.ENV[i]=function(a){o();l.receive(a);l.trigger('up')};var n=Faye.ENV.setTimeout(function(){o();j();l.trigger('down')},1.5*1000*d);var o=function(){if(!Faye.ENV[i])return false;Faye.ENV[i]=undefined;try{delete Faye.ENV[i]}catch(e){}Faye.ENV.clearTimeout(n);h.parentNode.removeChild(h);return true};k.params.jsonp=i;h.type='text/javascript';h.src=k.toURL();g.appendChild(h)}}),{_v:0,getCallbackName:function(){this._v+=1;return'__jsonp'+this._v+'__'},isUsable:function(a,b,d,f){d.call(f,true)}});Faye.Transport.register('callback-polling',Faye.Transport.JSONP); -// sourceMappingURL=faye-browser-min.js.map +(function() { +'use strict'; + +var Faye = { + VERSION: '1.0.1', + + BAYEUX_VERSION: '1.0', + ID_LENGTH: 160, + JSONP_CALLBACK: 'jsonpcallback', + CONNECTION_TYPES: ['long-polling', 'cross-origin-long-polling', 'callback-polling', 'websocket', 'eventsource', 'in-process'], + + MANDATORY_CONNECTION_TYPES: ['long-polling', 'callback-polling', 'in-process'], + + ENV: (typeof window !== 'undefined') ? window : global, + + extend: function(dest, source, overwrite) { + if (!source) return dest; + for (var key in source) { + if (!source.hasOwnProperty(key)) continue; + if (dest.hasOwnProperty(key) && overwrite === false) continue; + if (dest[key] !== source[key]) + dest[key] = source[key]; + } + return dest; + }, + + random: function(bitlength) { + bitlength = bitlength || this.ID_LENGTH; + return csprng(bitlength, 36); + }, + + clientIdFromMessages: function(messages) { + var connect = this.filter([].concat(messages), function(message) { + return message.channel === '/meta/connect'; + }); + return connect[0] && connect[0].clientId; + }, + + copyObject: function(object) { + var clone, i, key; + if (object instanceof Array) { + clone = []; + i = object.length; + while (i--) clone[i] = Faye.copyObject(object[i]); + return clone; + } else if (typeof object === 'object') { + clone = (object === null) ? null : {}; + for (key in object) clone[key] = Faye.copyObject(object[key]); + return clone; + } else { + return object; + } + }, + + commonElement: function(lista, listb) { + for (var i = 0, n = lista.length; i < n; i++) { + if (this.indexOf(listb, lista[i]) !== -1) + return lista[i]; + } + return null; + }, + + indexOf: function(list, needle) { + if (list.indexOf) return list.indexOf(needle); + + for (var i = 0, n = list.length; i < n; i++) { + if (list[i] === needle) return i; + } + return -1; + }, + + map: function(object, callback, context) { + if (object.map) return object.map(callback, context); + var result = []; + + if (object instanceof Array) { + for (var i = 0, n = object.length; i < n; i++) { + result.push(callback.call(context || null, object[i], i)); + } + } else { + for (var key in object) { + if (!object.hasOwnProperty(key)) continue; + result.push(callback.call(context || null, key, object[key])); + } + } + return result; + }, + + filter: function(array, callback, context) { + if (array.filter) return array.filter(callback, context); + var result = []; + for (var i = 0, n = array.length; i < n; i++) { + if (callback.call(context || null, array[i], i)) + result.push(array[i]); + } + return result; + }, + + asyncEach: function(list, iterator, callback, context) { + var n = list.length, + i = -1, + calls = 0, + looping = false; + + var iterate = function() { + calls -= 1; + i += 1; + if (i === n) return callback && callback.call(context); + iterator(list[i], resume); + }; + + var loop = function() { + if (looping) return; + looping = true; + while (calls > 0) iterate(); + looping = false; + }; + + var resume = function() { + calls += 1; + loop(); + }; + resume(); + }, + + // http://assanka.net/content/tech/2009/09/02/json2-js-vs-prototype/ + toJSON: function(object) { + if (!this.stringify) return JSON.stringify(object); + + return this.stringify(object, function(key, value) { + return (this[key] instanceof Array) ? this[key] : value; + }); + } +}; + +if (typeof module !== 'undefined') + module.exports = Faye; +else if (typeof window !== 'undefined') + window.Faye = Faye; + +Faye.Class = function(parent, methods) { + if (typeof parent !== 'function') { + methods = parent; + parent = Object; + } + + var klass = function() { + if (!this.initialize) return this; + return this.initialize.apply(this, arguments) || this; + }; + + var bridge = function() {}; + bridge.prototype = parent.prototype; + + klass.prototype = new bridge(); + Faye.extend(klass.prototype, methods); + + return klass; +}; + +(function() { +var EventEmitter = Faye.EventEmitter = function() {}; + +/* +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +var isArray = typeof Array.isArray === 'function' + ? Array.isArray + : function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]' + } +; +function indexOf (xs, x) { + if (xs.indexOf) return xs.indexOf(x); + for (var i = 0; i < xs.length; i++) { + if (x === xs[i]) return i; + } + return -1; +} + + +EventEmitter.prototype.emit = function(type) { + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events || !this._events.error || + (isArray(this._events.error) && !this._events.error.length)) + { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + } + + if (!this._events) return false; + var handler = this._events[type]; + if (!handler) return false; + + if (typeof handler == 'function') { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + return true; + + } else if (isArray(handler)) { + var args = Array.prototype.slice.call(arguments, 1); + + var listeners = handler.slice(); + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + return true; + + } else { + return false; + } +}; + +// EventEmitter is defined in src/node_events.cc +// EventEmitter.prototype.emit() is also defined there. +EventEmitter.prototype.addListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('addListener only takes instances of Function'); + } + + if (!this._events) this._events = {}; + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } else if (isArray(this._events[type])) { + // If we've already got an array, just append. + this._events[type].push(listener); + } else { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + var self = this; + self.on(type, function g() { + self.removeListener(type, g); + listener.apply(this, arguments); + }); + + return this; +}; + +EventEmitter.prototype.removeListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('removeListener only takes instances of Function'); + } + + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events || !this._events[type]) return this; + + var list = this._events[type]; + + if (isArray(list)) { + var i = indexOf(list, listener); + if (i < 0) return this; + list.splice(i, 1); + if (list.length == 0) + delete this._events[type]; + } else if (this._events[type] === listener) { + delete this._events[type]; + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + this._events = {}; + return this; + } + + // does not use listeners(), so no side effect of creating _events[type] + if (type && this._events && this._events[type]) this._events[type] = null; + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + if (!this._events) this._events = {}; + if (!this._events[type]) this._events[type] = []; + if (!isArray(this._events[type])) { + this._events[type] = [this._events[type]]; + } + return this._events[type]; +}; + +})(); + +Faye.Namespace = Faye.Class({ + initialize: function() { + this._used = {}; + }, + + exists: function(id) { + return this._used.hasOwnProperty(id); + }, + + generate: function() { + var name = Faye.random(); + while (this._used.hasOwnProperty(name)) + name = Faye.random(); + return this._used[name] = name; + }, + + release: function(id) { + delete this._used[id]; + } +}); + +(function() { +'use strict'; + +var timeout = setTimeout; + +var defer; +if (typeof setImmediate === 'function') + defer = function(fn) { setImmediate(fn) }; +else if (typeof process === 'object' && process.nextTick) + defer = function(fn) { process.nextTick(fn) }; +else + defer = function(fn) { timeout(fn, 0) }; + +var PENDING = 0, + FULFILLED = 1, + REJECTED = 2; + +var RETURN = function(x) { return x }, + THROW = function(x) { throw x }; + +var Promise = function(task) { + this._state = PENDING; + this._callbacks = []; + this._errbacks = []; + + if (typeof task !== 'function') return; + var self = this; + + task(function(value) { fulfill(self, value) }, + function(reason) { reject(self, reason) }); +}; + +Promise.prototype.then = function(callback, errback) { + var next = {}, self = this; + + next.promise = new Promise(function(fulfill, reject) { + next.fulfill = fulfill; + next.reject = reject; + + registerCallback(self, callback, next); + registerErrback(self, errback, next); + }); + return next.promise; +}; + +var registerCallback = function(promise, callback, next) { + if (typeof callback !== 'function') callback = RETURN; + var handler = function(value) { invoke(callback, value, next) }; + if (promise._state === PENDING) { + promise._callbacks.push(handler); + } else if (promise._state === FULFILLED) { + handler(promise._value); + } +}; + +var registerErrback = function(promise, errback, next) { + if (typeof errback !== 'function') errback = THROW; + var handler = function(reason) { invoke(errback, reason, next) }; + if (promise._state === PENDING) { + promise._errbacks.push(handler); + } else if (promise._state === REJECTED) { + handler(promise._reason); + } +}; + +var invoke = function(fn, value, next) { + defer(function() { _invoke(fn, value, next) }); +}; + +var _invoke = function(fn, value, next) { + var called = false, outcome, type, then; + + try { + outcome = fn(value); + type = typeof outcome; + then = outcome !== null && (type === 'function' || type === 'object') && outcome.then; + + if (outcome === next.promise) + return next.reject(new TypeError('Recursive promise chain detected')); + + if (typeof then !== 'function') return next.fulfill(outcome); + + then.call(outcome, function(v) { + if (called) return; + called = true; + _invoke(RETURN, v, next); + }, function(r) { + if (called) return; + called = true; + next.reject(r); + }); + + } catch (error) { + if (called) return; + called = true; + next.reject(error); + } +}; + +var fulfill = Promise.fulfill = Promise.resolve = function(promise, value) { + if (promise._state !== PENDING) return; + + promise._state = FULFILLED; + promise._value = value; + promise._errbacks = []; + + var callbacks = promise._callbacks, cb; + while (cb = callbacks.shift()) cb(value); +}; + +var reject = Promise.reject = function(promise, reason) { + if (promise._state !== PENDING) return; + + promise._state = REJECTED; + promise._reason = reason; + promise._callbacks = []; + + var errbacks = promise._errbacks, eb; + while (eb = errbacks.shift()) eb(reason); +}; + +Promise.defer = defer; + +Promise.deferred = Promise.pending = function() { + var tuple = {}; + + tuple.promise = new Promise(function(fulfill, reject) { + tuple.fulfill = tuple.resolve = fulfill; + tuple.reject = reject; + }); + return tuple; +}; + +Promise.fulfilled = Promise.resolved = function(value) { + return new Promise(function(fulfill, reject) { fulfill(value) }); +}; + +Promise.rejected = function(reason) { + return new Promise(function(fulfill, reject) { reject(reason) }); +}; + +if (typeof Faye === 'undefined') + module.exports = Promise; +else + Faye.Promise = Promise; + +})(); + +Faye.Set = Faye.Class({ + initialize: function() { + this._index = {}; + }, + + add: function(item) { + var key = (item.id !== undefined) ? item.id : item; + if (this._index.hasOwnProperty(key)) return false; + this._index[key] = item; + return true; + }, + + forEach: function(block, context) { + for (var key in this._index) { + if (this._index.hasOwnProperty(key)) + block.call(context, this._index[key]); + } + }, + + isEmpty: function() { + for (var key in this._index) { + if (this._index.hasOwnProperty(key)) return false; + } + return true; + }, + + member: function(item) { + for (var key in this._index) { + if (this._index[key] === item) return true; + } + return false; + }, + + remove: function(item) { + var key = (item.id !== undefined) ? item.id : item; + var removed = this._index[key]; + delete this._index[key]; + return removed; + }, + + toArray: function() { + var array = []; + this.forEach(function(item) { array.push(item) }); + return array; + } +}); + +Faye.URI = { + isURI: function(uri) { + return uri && uri.protocol && uri.host && uri.path; + }, + + isSameOrigin: function(uri) { + var location = Faye.ENV.location; + return uri.protocol === location.protocol && + uri.hostname === location.hostname && + uri.port === location.port; + }, + + parse: function(url) { + if (typeof url !== 'string') return url; + var uri = {}, parts, query, pairs, i, n, data; + + var consume = function(name, pattern) { + url = url.replace(pattern, function(match) { + uri[name] = match; + return ''; + }); + uri[name] = uri[name] || ''; + }; + + consume('protocol', /^[a-z]+\:/i); + consume('host', /^\/\/[^\/\?#]+/); + + if (!/^\//.test(url) && !uri.host) + url = Faye.ENV.location.pathname.replace(/[^\/]*$/, '') + url; + + consume('pathname', /^[^\?#]*/); + consume('search', /^\?[^#]*/); + consume('hash', /^#.*/); + + uri.protocol = uri.protocol || Faye.ENV.location.protocol; + + if (uri.host) { + uri.host = uri.host.substr(2); + parts = uri.host.split(':'); + uri.hostname = parts[0]; + uri.port = parts[1] || ''; + } else { + uri.host = Faye.ENV.location.host; + uri.hostname = Faye.ENV.location.hostname; + uri.port = Faye.ENV.location.port; + } + + uri.pathname = uri.pathname || '/'; + uri.path = uri.pathname + uri.search; + + query = uri.search.replace(/^\?/, ''); + pairs = query ? query.split('&') : []; + data = {}; + + for (i = 0, n = pairs.length; i < n; i++) { + parts = pairs[i].split('='); + data[decodeURIComponent(parts[0] || '')] = decodeURIComponent(parts[1] || ''); + } + + uri.query = data; + + uri.href = this.stringify(uri); + return uri; + }, + + stringify: function(uri) { + var string = uri.protocol + '//' + uri.hostname; + if (uri.port) string += ':' + uri.port; + string += uri.pathname + this.queryString(uri.query) + (uri.hash || ''); + return string; + }, + + queryString: function(query) { + var pairs = []; + for (var key in query) { + if (!query.hasOwnProperty(key)) continue; + pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(query[key])); + } + if (pairs.length === 0) return ''; + return '?' + pairs.join('&'); + } +}; + +Faye.Error = Faye.Class({ + initialize: function(code, params, message) { + this.code = code; + this.params = Array.prototype.slice.call(params); + this.message = message; + }, + + toString: function() { + return this.code + ':' + + this.params.join(',') + ':' + + this.message; + } +}); + +Faye.Error.parse = function(message) { + message = message || ''; + if (!Faye.Grammar.ERROR.test(message)) return new this(null, [], message); + + var parts = message.split(':'), + code = parseInt(parts[0]), + params = parts[1].split(','), + message = parts[2]; + + return new this(code, params, message); +}; + + + + +Faye.Error.versionMismatch = function() { + return new this(300, arguments, 'Version mismatch').toString(); +}; + +Faye.Error.conntypeMismatch = function() { + return new this(301, arguments, 'Connection types not supported').toString(); +}; + +Faye.Error.extMismatch = function() { + return new this(302, arguments, 'Extension mismatch').toString(); +}; + +Faye.Error.badRequest = function() { + return new this(400, arguments, 'Bad request').toString(); +}; + +Faye.Error.clientUnknown = function() { + return new this(401, arguments, 'Unknown client').toString(); +}; + +Faye.Error.parameterMissing = function() { + return new this(402, arguments, 'Missing required parameter').toString(); +}; + +Faye.Error.channelForbidden = function() { + return new this(403, arguments, 'Forbidden channel').toString(); +}; + +Faye.Error.channelUnknown = function() { + return new this(404, arguments, 'Unknown channel').toString(); +}; + +Faye.Error.channelInvalid = function() { + return new this(405, arguments, 'Invalid channel').toString(); +}; + +Faye.Error.extUnknown = function() { + return new this(406, arguments, 'Unknown extension').toString(); +}; + +Faye.Error.publishFailed = function() { + return new this(407, arguments, 'Failed to publish').toString(); +}; + +Faye.Error.serverError = function() { + return new this(500, arguments, 'Internal server error').toString(); +}; + + +Faye.Deferrable = { + then: function(callback, errback) { + var self = this; + if (!this._promise) + this._promise = new Faye.Promise(function(fulfill, reject) { + self._fulfill = fulfill; + self._reject = reject; + }); + + if (arguments.length === 0) + return this._promise; + else + return this._promise.then(callback, errback); + }, + + callback: function(callback, context) { + return this.then(function(value) { callback.call(context, value) }); + }, + + errback: function(callback, context) { + return this.then(null, function(reason) { callback.call(context, reason) }); + }, + + timeout: function(seconds, message) { + this.then(); + var self = this; + this._timer = Faye.ENV.setTimeout(function() { + self._reject(message); + }, seconds * 1000); + }, + + setDeferredStatus: function(status, value) { + if (this._timer) Faye.ENV.clearTimeout(this._timer); + + var promise = this.then(); + + if (status === 'succeeded') + this._fulfill(value); + else if (status === 'failed') + this._reject(value); + else + delete this._promise; + } +}; + +Faye.Publisher = { + countListeners: function(eventType) { + return this.listeners(eventType).length; + }, + + bind: function(eventType, listener, context) { + var slice = Array.prototype.slice, + handler = function() { listener.apply(context, slice.call(arguments)) }; + + this._listeners = this._listeners || []; + this._listeners.push([eventType, listener, context, handler]); + return this.on(eventType, handler); + }, + + unbind: function(eventType, listener, context) { + this._listeners = this._listeners || []; + var n = this._listeners.length, tuple; + + while (n--) { + tuple = this._listeners[n]; + if (tuple[0] !== eventType) continue; + if (listener && (tuple[1] !== listener || tuple[2] !== context)) continue; + this._listeners.splice(n, 1); + this.removeListener(eventType, tuple[3]); + } + } +}; + +Faye.extend(Faye.Publisher, Faye.EventEmitter.prototype); +Faye.Publisher.trigger = Faye.Publisher.emit; + +Faye.Timeouts = { + addTimeout: function(name, delay, callback, context) { + this._timeouts = this._timeouts || {}; + if (this._timeouts.hasOwnProperty(name)) return; + var self = this; + this._timeouts[name] = Faye.ENV.setTimeout(function() { + delete self._timeouts[name]; + callback.call(context); + }, 1000 * delay); + }, + + removeTimeout: function(name) { + this._timeouts = this._timeouts || {}; + var timeout = this._timeouts[name]; + if (!timeout) return; + clearTimeout(timeout); + delete this._timeouts[name]; + }, + + removeAllTimeouts: function() { + this._timeouts = this._timeouts || {}; + for (var name in this._timeouts) this.removeTimeout(name); + } +}; + +Faye.Logging = { + LOG_LEVELS: { + fatal: 4, + error: 3, + warn: 2, + info: 1, + debug: 0 + }, + + writeLog: function(messageArgs, level) { + if (!Faye.logger) return; + + var messageArgs = Array.prototype.slice.apply(messageArgs), + banner = '[Faye', + klass = this.className, + + message = messageArgs.shift().replace(/\?/g, function() { + try { + return Faye.toJSON(messageArgs.shift()); + } catch (e) { + return '[Object]'; + } + }); + + for (var key in Faye) { + if (klass) continue; + if (typeof Faye[key] !== 'function') continue; + if (this instanceof Faye[key]) klass = key; + } + if (klass) banner += '.' + klass; + banner += '] '; + + if (typeof Faye.logger[level] === 'function') + Faye.logger[level](banner + message); + else if (typeof Faye.logger === 'function') + Faye.logger(banner + message); + } +}; + +(function() { + for (var key in Faye.Logging.LOG_LEVELS) + (function(level, value) { + Faye.Logging[level] = function() { + this.writeLog(arguments, level); + }; + })(key, Faye.Logging.LOG_LEVELS[key]); +})(); + +Faye.Grammar = { + CHANNEL_NAME: /^\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*$/, + CHANNEL_PATTERN: /^(\/(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)))+)*\/\*{1,2}$/, + ERROR: /^([0-9][0-9][0-9]:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*(,(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)*:(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*|[0-9][0-9][0-9]::(((([a-z]|[A-Z])|[0-9])|(\-|\_|\!|\~|\(|\)|\$|\@)| |\/|\*|\.))*)$/, + VERSION: /^([0-9])+(\.(([a-z]|[A-Z])|[0-9])(((([a-z]|[A-Z])|[0-9])|\-|\_))*)*$/ +}; + +Faye.Extensible = { + addExtension: function(extension) { + this._extensions = this._extensions || []; + this._extensions.push(extension); + if (extension.added) extension.added(this); + }, + + removeExtension: function(extension) { + if (!this._extensions) return; + var i = this._extensions.length; + while (i--) { + if (this._extensions[i] !== extension) continue; + this._extensions.splice(i,1); + if (extension.removed) extension.removed(this); + } + }, + + pipeThroughExtensions: function(stage, message, request, callback, context) { + this.debug('Passing through ? extensions: ?', stage, message); + + if (!this._extensions) return callback.call(context, message); + var extensions = this._extensions.slice(); + + var pipe = function(message) { + if (!message) return callback.call(context, message); + + var extension = extensions.shift(); + if (!extension) return callback.call(context, message); + + var fn = extension[stage]; + if (!fn) return pipe(message); + + if (fn.length >= 3) extension[stage](message, request, pipe); + else extension[stage](message, pipe); + }; + pipe(message); + } +}; + +Faye.extend(Faye.Extensible, Faye.Logging); + +Faye.Channel = Faye.Class({ + initialize: function(name) { + this.id = this.name = name; + }, + + push: function(message) { + this.trigger('message', message); + }, + + isUnused: function() { + return this.countListeners('message') === 0; + } +}); + +Faye.extend(Faye.Channel.prototype, Faye.Publisher); + +Faye.extend(Faye.Channel, { + HANDSHAKE: '/meta/handshake', + CONNECT: '/meta/connect', + SUBSCRIBE: '/meta/subscribe', + UNSUBSCRIBE: '/meta/unsubscribe', + DISCONNECT: '/meta/disconnect', + + META: 'meta', + SERVICE: 'service', + + expand: function(name) { + var segments = this.parse(name), + channels = ['/**', name]; + + var copy = segments.slice(); + copy[copy.length - 1] = '*'; + channels.push(this.unparse(copy)); + + for (var i = 1, n = segments.length; i < n; i++) { + copy = segments.slice(0, i); + copy.push('**'); + channels.push(this.unparse(copy)); + } + + return channels; + }, + + isValid: function(name) { + return Faye.Grammar.CHANNEL_NAME.test(name) || + Faye.Grammar.CHANNEL_PATTERN.test(name); + }, + + parse: function(name) { + if (!this.isValid(name)) return null; + return name.split('/').slice(1); + }, + + unparse: function(segments) { + return '/' + segments.join('/'); + }, + + isMeta: function(name) { + var segments = this.parse(name); + return segments ? (segments[0] === this.META) : null; + }, + + isService: function(name) { + var segments = this.parse(name); + return segments ? (segments[0] === this.SERVICE) : null; + }, + + isSubscribable: function(name) { + if (!this.isValid(name)) return null; + return !this.isMeta(name) && !this.isService(name); + }, + + Set: Faye.Class({ + initialize: function() { + this._channels = {}; + }, + + getKeys: function() { + var keys = []; + for (var key in this._channels) keys.push(key); + return keys; + }, + + remove: function(name) { + delete this._channels[name]; + }, + + hasSubscription: function(name) { + return this._channels.hasOwnProperty(name); + }, + + subscribe: function(names, callback, context) { + if (!callback) return; + var name; + for (var i = 0, n = names.length; i < n; i++) { + name = names[i]; + var channel = this._channels[name] = this._channels[name] || new Faye.Channel(name); + channel.bind('message', callback, context); + } + }, + + unsubscribe: function(name, callback, context) { + var channel = this._channels[name]; + if (!channel) return false; + channel.unbind('message', callback, context); + + if (channel.isUnused()) { + this.remove(name); + return true; + } else { + return false; + } + }, + + distributeMessage: function(message) { + var channels = Faye.Channel.expand(message.channel); + + for (var i = 0, n = channels.length; i < n; i++) { + var channel = this._channels[channels[i]]; + if (channel) channel.trigger('message', message.data); + } + } + }) +}); + +Faye.Envelope = Faye.Class({ + initialize: function(message, timeout) { + this.id = message.id; + this.message = message; + + if (timeout !== undefined) this.timeout(timeout / 1000, false); + } +}); + +Faye.extend(Faye.Envelope.prototype, Faye.Deferrable); + +Faye.Publication = Faye.Class(Faye.Deferrable); + +Faye.Subscription = Faye.Class({ + initialize: function(client, channels, callback, context) { + this._client = client; + this._channels = channels; + this._callback = callback; + this._context = context; + this._cancelled = false; + }, + + cancel: function() { + if (this._cancelled) return; + this._client.unsubscribe(this._channels, this._callback, this._context); + this._cancelled = true; + }, + + unsubscribe: function() { + this.cancel(); + } +}); + +Faye.extend(Faye.Subscription.prototype, Faye.Deferrable); + +Faye.Client = Faye.Class({ + UNCONNECTED: 1, + CONNECTING: 2, + CONNECTED: 3, + DISCONNECTED: 4, + + HANDSHAKE: 'handshake', + RETRY: 'retry', + NONE: 'none', + + CONNECTION_TIMEOUT: 60, + DEFAULT_RETRY: 5, + MAX_REQUEST_SIZE: 2048, + + DEFAULT_ENDPOINT: '/bayeux', + INTERVAL: 0, + + initialize: function(endpoint, options) { + this.info('New client created for ?', endpoint); + + this._options = options || {}; + this.endpoint = Faye.URI.parse(endpoint || this.DEFAULT_ENDPOINT); + this.endpoints = this._options.endpoints || {}; + this.transports = {}; + this.cookies = Faye.CookieJar && new Faye.CookieJar(); + this.headers = {}; + this.ca = this._options.ca; + this._disabled = []; + this._retry = this._options.retry || this.DEFAULT_RETRY; + + for (var key in this.endpoints) + this.endpoints[key] = Faye.URI.parse(this.endpoints[key]); + + this.maxRequestSize = this.MAX_REQUEST_SIZE; + + this._state = this.UNCONNECTED; + this._channels = new Faye.Channel.Set(); + this._messageId = 0; + + this._responseCallbacks = {}; + + this._advice = { + reconnect: this.RETRY, + interval: 1000 * (this._options.interval || this.INTERVAL), + timeout: 1000 * (this._options.timeout || this.CONNECTION_TIMEOUT) + }; + + if (Faye.Event && Faye.ENV.onbeforeunload !== undefined) + Faye.Event.on(Faye.ENV, 'beforeunload', function() { + if (Faye.indexOf(this._disabled, 'autodisconnect') < 0) + this.disconnect(); + }, this); + }, + + disable: function(feature) { + this._disabled.push(feature); + }, + + setHeader: function(name, value) { + this.headers[name] = value; + }, + + // Request + // MUST include: * channel + // * version + // * supportedConnectionTypes + // MAY include: * minimumVersion + // * ext + // * id + // + // Success Response Failed Response + // MUST include: * channel MUST include: * channel + // * version * successful + // * supportedConnectionTypes * error + // * clientId MAY include: * supportedConnectionTypes + // * successful * advice + // MAY include: * minimumVersion * version + // * advice * minimumVersion + // * ext * ext + // * id * id + // * authSuccessful + handshake: function(callback, context) { + if (this._advice.reconnect === this.NONE) return; + if (this._state !== this.UNCONNECTED) return; + + this._state = this.CONNECTING; + var self = this; + + this.info('Initiating handshake with ?', Faye.URI.stringify(this.endpoint)); + this._selectTransport(Faye.MANDATORY_CONNECTION_TYPES); + + this._send({ + channel: Faye.Channel.HANDSHAKE, + version: Faye.BAYEUX_VERSION, + supportedConnectionTypes: [this._transport.connectionType] + + }, function(response) { + + if (response.successful) { + this._state = this.CONNECTED; + this._clientId = response.clientId; + + this._selectTransport(response.supportedConnectionTypes); + + this.info('Handshake successful: ?', this._clientId); + + this.subscribe(this._channels.getKeys(), true); + if (callback) Faye.Promise.defer(function() { callback.call(context) }); + + } else { + this.info('Handshake unsuccessful'); + Faye.ENV.setTimeout(function() { self.handshake(callback, context) }, this._advice.interval); + this._state = this.UNCONNECTED; + } + }, this); + }, + + // Request Response + // MUST include: * channel MUST include: * channel + // * clientId * successful + // * connectionType * clientId + // MAY include: * ext MAY include: * error + // * id * advice + // * ext + // * id + // * timestamp + connect: function(callback, context) { + if (this._advice.reconnect === this.NONE) return; + if (this._state === this.DISCONNECTED) return; + + if (this._state === this.UNCONNECTED) + return this.handshake(function() { this.connect(callback, context) }, this); + + this.callback(callback, context); + if (this._state !== this.CONNECTED) return; + + this.info('Calling deferred actions for ?', this._clientId); + this.setDeferredStatus('succeeded'); + this.setDeferredStatus('unknown'); + + if (this._connectRequest) return; + this._connectRequest = true; + + this.info('Initiating connection for ?', this._clientId); + + this._send({ + channel: Faye.Channel.CONNECT, + clientId: this._clientId, + connectionType: this._transport.connectionType + + }, this._cycleConnection, this); + }, + + // Request Response + // MUST include: * channel MUST include: * channel + // * clientId * successful + // MAY include: * ext * clientId + // * id MAY include: * error + // * ext + // * id + disconnect: function() { + if (this._state !== this.CONNECTED) return; + this._state = this.DISCONNECTED; + + this.info('Disconnecting ?', this._clientId); + + this._send({ + channel: Faye.Channel.DISCONNECT, + clientId: this._clientId + + }, function(response) { + if (!response.successful) return; + this._transport.close(); + delete this._transport; + }, this); + + this.info('Clearing channel listeners for ?', this._clientId); + this._channels = new Faye.Channel.Set(); + }, + + // Request Response + // MUST include: * channel MUST include: * channel + // * clientId * successful + // * subscription * clientId + // MAY include: * ext * subscription + // * id MAY include: * error + // * advice + // * ext + // * id + // * timestamp + subscribe: function(channel, callback, context) { + if (channel instanceof Array) + return Faye.map(channel, function(c) { + return this.subscribe(c, callback, context); + }, this); + + var subscription = new Faye.Subscription(this, channel, callback, context), + force = (callback === true), + hasSubscribe = this._channels.hasSubscription(channel); + + if (hasSubscribe && !force) { + this._channels.subscribe([channel], callback, context); + subscription.setDeferredStatus('succeeded'); + return subscription; + } + + this.connect(function() { + this.info('Client ? attempting to subscribe to ?', this._clientId, channel); + if (!force) this._channels.subscribe([channel], callback, context); + + this._send({ + channel: Faye.Channel.SUBSCRIBE, + clientId: this._clientId, + subscription: channel + + }, function(response) { + if (!response.successful) { + subscription.setDeferredStatus('failed', Faye.Error.parse(response.error)); + return this._channels.unsubscribe(channel, callback, context); + } + + var channels = [].concat(response.subscription); + this.info('Subscription acknowledged for ? to ?', this._clientId, channels); + subscription.setDeferredStatus('succeeded'); + }, this); + }, this); + + return subscription; + }, + + // Request Response + // MUST include: * channel MUST include: * channel + // * clientId * successful + // * subscription * clientId + // MAY include: * ext * subscription + // * id MAY include: * error + // * advice + // * ext + // * id + // * timestamp + unsubscribe: function(channel, callback, context) { + if (channel instanceof Array) + return Faye.map(channel, function(c) { + return this.unsubscribe(c, callback, context); + }, this); + + var dead = this._channels.unsubscribe(channel, callback, context); + if (!dead) return; + + this.connect(function() { + this.info('Client ? attempting to unsubscribe from ?', this._clientId, channel); + + this._send({ + channel: Faye.Channel.UNSUBSCRIBE, + clientId: this._clientId, + subscription: channel + + }, function(response) { + if (!response.successful) return; + + var channels = [].concat(response.subscription); + this.info('Unsubscription acknowledged for ? from ?', this._clientId, channels); + }, this); + }, this); + }, + + // Request Response + // MUST include: * channel MUST include: * channel + // * data * successful + // MAY include: * clientId MAY include: * id + // * id * error + // * ext * ext + publish: function(channel, data) { + var publication = new Faye.Publication(); + + this.connect(function() { + this.info('Client ? queueing published message to ?: ?', this._clientId, channel, data); + + this._send({ + channel: channel, + data: data, + clientId: this._clientId + + }, function(response) { + if (response.successful) + publication.setDeferredStatus('succeeded'); + else + publication.setDeferredStatus('failed', Faye.Error.parse(response.error)); + }, this); + }, this); + + return publication; + }, + + receiveMessage: function(message) { + var id = message.id, timeout, callback; + + if (message.successful !== undefined) { + callback = this._responseCallbacks[id]; + delete this._responseCallbacks[id]; + } + + this.pipeThroughExtensions('incoming', message, null, function(message) { + if (!message) return; + + if (message.advice) this._handleAdvice(message.advice); + this._deliverMessage(message); + + if (callback) callback[0].call(callback[1], message); + }, this); + + if (this._transportUp === true) return; + this._transportUp = true; + this.trigger('transport:up'); + }, + + messageError: function(messages, immediate) { + var retry = this._retry, + self = this, + id, message, timeout; + + for (var i = 0, n = messages.length; i < n; i++) { + message = messages[i]; + id = message.id; + + if (immediate) + this._transportSend(message); + else + Faye.ENV.setTimeout(function() { self._transportSend(message) }, retry * 1000); + } + + if (immediate || this._transportUp === false) return; + this._transportUp = false; + this.trigger('transport:down'); + }, + + _selectTransport: function(transportTypes) { + Faye.Transport.get(this, transportTypes, this._disabled, function(transport) { + this.debug('Selected ? transport for ?', transport.connectionType, Faye.URI.stringify(transport.endpoint)); + + if (transport === this._transport) return; + if (this._transport) this._transport.close(); + + this._transport = transport; + }, this); + }, + + _send: function(message, callback, context) { + if (!this._transport) return; + message.id = message.id || this._generateMessageId(); + + this.pipeThroughExtensions('outgoing', message, null, function(message) { + if (!message) return; + if (callback) this._responseCallbacks[message.id] = [callback, context]; + this._transportSend(message); + }, this); + }, + + _transportSend: function(message) { + if (!this._transport) return; + + var timeout = 1.2 * (this._advice.timeout || this._retry * 1000), + envelope = new Faye.Envelope(message, timeout); + + envelope.errback(function(immediate) { + this.messageError([message], immediate); + }, this); + + this._transport.send(envelope); + }, + + _generateMessageId: function() { + this._messageId += 1; + if (this._messageId >= Math.pow(2,32)) this._messageId = 0; + return this._messageId.toString(36); + }, + + _handleAdvice: function(advice) { + Faye.extend(this._advice, advice); + + if (this._advice.reconnect === this.HANDSHAKE && this._state !== this.DISCONNECTED) { + this._state = this.UNCONNECTED; + this._clientId = null; + this._cycleConnection(); + } + }, + + _deliverMessage: function(message) { + if (!message.channel || message.data === undefined) return; + this.info('Client ? calling listeners for ? with ?', this._clientId, message.channel, message.data); + this._channels.distributeMessage(message); + }, + + _cycleConnection: function() { + if (this._connectRequest) { + this._connectRequest = null; + this.info('Closed connection for ?', this._clientId); + } + var self = this; + Faye.ENV.setTimeout(function() { self.connect() }, this._advice.interval); + } +}); + +Faye.extend(Faye.Client.prototype, Faye.Deferrable); +Faye.extend(Faye.Client.prototype, Faye.Publisher); +Faye.extend(Faye.Client.prototype, Faye.Logging); +Faye.extend(Faye.Client.prototype, Faye.Extensible); + +Faye.Transport = Faye.extend(Faye.Class({ + MAX_DELAY: 0, + batching: true, + + initialize: function(client, endpoint) { + this._client = client; + this.endpoint = endpoint; + this._outbox = []; + }, + + close: function() {}, + + encode: function(envelopes) { + return ''; + }, + + send: function(envelope) { + var message = envelope.message; + + this.debug('Client ? sending message to ?: ?', + this._client._clientId, Faye.URI.stringify(this.endpoint), message); + + if (!this.batching) return this.request([envelope]); + + this._outbox.push(envelope); + + if (message.channel === Faye.Channel.HANDSHAKE) + return this.addTimeout('publish', 0.01, this.flush, this); + + if (message.channel === Faye.Channel.CONNECT) + this._connectMessage = message; + + this.flushLargeBatch(); + this.addTimeout('publish', this.MAX_DELAY, this.flush, this); + }, + + flush: function() { + this.removeTimeout('publish'); + + if (this._outbox.length > 1 && this._connectMessage) + this._connectMessage.advice = {timeout: 0}; + + this.request(this._outbox); + + this._connectMessage = null; + this._outbox = []; + }, + + flushLargeBatch: function() { + var string = this.encode(this._outbox); + if (string.length < this._client.maxRequestSize) return; + var last = this._outbox.pop(); + this.flush(); + if (last) this._outbox.push(last); + }, + + receive: function(envelopes, responses) { + var n = envelopes.length; + while (n--) envelopes[n].setDeferredStatus('succeeded'); + + responses = [].concat(responses); + + this.debug('Client ? received from ?: ?', + this._client._clientId, Faye.URI.stringify(this.endpoint), responses); + + for (var i = 0, n = responses.length; i < n; i++) + this._client.receiveMessage(responses[i]); + }, + + handleError: function(envelopes, immediate) { + var n = envelopes.length; + while (n--) envelopes[n].setDeferredStatus('failed', immediate); + }, + + _getCookies: function() { + var cookies = this._client.cookies; + if (!cookies) return ''; + + return cookies.getCookies({ + domain: this.endpoint.hostname, + path: this.endpoint.path, + secure: this.endpoint.protocol === 'https:' + }).toValueString(); + }, + + _storeCookies: function(setCookie) { + if (!setCookie || !this._client.cookies) return; + setCookie = [].concat(setCookie); + var cookie; + + for (var i = 0, n = setCookie.length; i < n; i++) { + cookie = this._client.cookies.setCookie(setCookie[i]); + cookie = cookie[0] || cookie; + cookie.domain = cookie.domain || this.endpoint.hostname; + } + } + +}), { + get: function(client, allowed, disabled, callback, context) { + var endpoint = client.endpoint; + + Faye.asyncEach(this._transports, function(pair, resume) { + var connType = pair[0], klass = pair[1], + connEndpoint = client.endpoints[connType] || endpoint; + + if (Faye.indexOf(disabled, connType) >= 0) + return resume(); + + if (Faye.indexOf(allowed, connType) < 0) { + klass.isUsable(client, connEndpoint, function() {}); + return resume(); + } + + klass.isUsable(client, connEndpoint, function(isUsable) { + if (!isUsable) return resume(); + var transport = klass.hasOwnProperty('create') ? klass.create(client, connEndpoint) : new klass(client, connEndpoint); + callback.call(context, transport); + }); + }, function() { + throw new Error('Could not find a usable connection type for ' + Faye.URI.stringify(endpoint)); + }); + }, + + register: function(type, klass) { + this._transports.push([type, klass]); + klass.prototype.connectionType = type; + }, + + _transports: [] +}); + +Faye.extend(Faye.Transport.prototype, Faye.Logging); +Faye.extend(Faye.Transport.prototype, Faye.Timeouts); + +Faye.Event = { + _registry: [], + + on: function(element, eventName, callback, context) { + var wrapped = function() { callback.call(context) }; + + if (element.addEventListener) + element.addEventListener(eventName, wrapped, false); + else + element.attachEvent('on' + eventName, wrapped); + + this._registry.push({ + _element: element, + _type: eventName, + _callback: callback, + _context: context, + _handler: wrapped + }); + }, + + detach: function(element, eventName, callback, context) { + var i = this._registry.length, register; + while (i--) { + register = this._registry[i]; + + if ((element && element !== register._element) || + (eventName && eventName !== register._type) || + (callback && callback !== register._callback) || + (context && context !== register._context)) + continue; + + if (register._element.removeEventListener) + register._element.removeEventListener(register._type, register._handler, false); + else + register._element.detachEvent('on' + register._type, register._handler); + + this._registry.splice(i,1); + register = null; + } + } +}; + +if (Faye.ENV.onunload !== undefined) Faye.Event.on(Faye.ENV, 'unload', Faye.Event.detach, Faye.Event); + +/* + json2.js + 2013-05-26 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== 'object') { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function () { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + Faye.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = Faye.stringify; + } + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); + +Faye.Transport.WebSocket = Faye.extend(Faye.Class(Faye.Transport, { + UNCONNECTED: 1, + CONNECTING: 2, + CONNECTED: 3, + + batching: false, + + isUsable: function(callback, context) { + this.callback(function() { callback.call(context, true) }); + this.errback(function() { callback.call(context, false) }); + this.connect(); + }, + + request: function(envelopes) { + this._pending = this._pending || new Faye.Set(); + for (var i = 0, n = envelopes.length; i < n; i++) this._pending.add(envelopes[i]); + + this.callback(function(socket) { + if (!socket) return; + var messages = Faye.map(envelopes, function(e) { return e.message }); + socket.send(Faye.toJSON(messages)); + }, this); + this.connect(); + }, + + connect: function() { + if (Faye.Transport.WebSocket._unloaded) return; + + this._state = this._state || this.UNCONNECTED; + if (this._state !== this.UNCONNECTED) return; + this._state = this.CONNECTING; + + var socket = this._createSocket(); + if (!socket) return this.setDeferredStatus('failed'); + + var self = this; + + socket.onopen = function() { + if (socket.headers) self._storeCookies(socket.headers['set-cookie']); + self._socket = socket; + self._state = self.CONNECTED; + self._everConnected = true; + self._ping(); + self.setDeferredStatus('succeeded', socket); + }; + + var closed = false; + socket.onclose = socket.onerror = function() { + if (closed) return; + closed = true; + + var wasConnected = (self._state === self.CONNECTED); + socket.onopen = socket.onclose = socket.onerror = socket.onmessage = null; + + delete self._socket; + self._state = self.UNCONNECTED; + self.removeTimeout('ping'); + self.setDeferredStatus('unknown'); + + var pending = self._pending ? self._pending.toArray() : []; + delete self._pending; + + if (wasConnected) { + self.handleError(pending, true); + } else if (self._everConnected) { + self.handleError(pending); + } else { + self.setDeferredStatus('failed'); + } + }; + + socket.onmessage = function(event) { + var messages = JSON.parse(event.data), + envelopes = [], + envelope; + + if (!messages) return; + messages = [].concat(messages); + + for (var i = 0, n = messages.length; i < n; i++) { + if (messages[i].successful === undefined) continue; + envelope = self._pending.remove(messages[i]); + if (envelope) envelopes.push(envelope); + } + self.receive(envelopes, messages); + }; + }, + + close: function() { + if (!this._socket) return; + this._socket.close(); + }, + + _createSocket: function() { + var url = Faye.Transport.WebSocket.getSocketUrl(this.endpoint), + options = {headers: Faye.copyObject(this._client.headers), ca: this._client.ca}; + + options.headers['Cookie'] = this._getCookies(); + + if (Faye.WebSocket) return new Faye.WebSocket.Client(url, [], options); + if (Faye.ENV.MozWebSocket) return new MozWebSocket(url); + if (Faye.ENV.WebSocket) return new WebSocket(url); + }, + + _ping: function() { + if (!this._socket) return; + this._socket.send('[]'); + this.addTimeout('ping', this._client._advice.timeout/2000, this._ping, this); + } + +}), { + PROTOCOLS: { + 'http:': 'ws:', + 'https:': 'wss:' + }, + + create: function(client, endpoint) { + var sockets = client.transports.websocket = client.transports.websocket || {}; + sockets[endpoint.href] = sockets[endpoint.href] || new this(client, endpoint); + return sockets[endpoint.href]; + }, + + getSocketUrl: function(endpoint) { + endpoint = Faye.copyObject(endpoint); + endpoint.protocol = this.PROTOCOLS[endpoint.protocol]; + return Faye.URI.stringify(endpoint); + }, + + isUsable: function(client, endpoint, callback, context) { + this.create(client, endpoint).isUsable(callback, context); + } +}); + +Faye.extend(Faye.Transport.WebSocket.prototype, Faye.Deferrable); +Faye.Transport.register('websocket', Faye.Transport.WebSocket); + +if (Faye.Event) + Faye.Event.on(Faye.ENV, 'beforeunload', function() { + Faye.Transport.WebSocket._unloaded = true; + }); + +Faye.Transport.EventSource = Faye.extend(Faye.Class(Faye.Transport, { + initialize: function(client, endpoint) { + Faye.Transport.prototype.initialize.call(this, client, endpoint); + if (!Faye.ENV.EventSource) return this.setDeferredStatus('failed'); + + this._xhr = new Faye.Transport.XHR(client, endpoint); + + endpoint = Faye.copyObject(endpoint); + endpoint.pathname += '/' + client._clientId; + + var socket = new EventSource(Faye.URI.stringify(endpoint)), + self = this; + + socket.onopen = function() { + self._everConnected = true; + self.setDeferredStatus('succeeded'); + }; + + socket.onerror = function() { + if (self._everConnected) { + self._client.messageError([]); + } else { + self.setDeferredStatus('failed'); + socket.close(); + } + }; + + socket.onmessage = function(event) { + self.receive([], JSON.parse(event.data)); + }; + + this._socket = socket; + }, + + close: function() { + if (!this._socket) return; + this._socket.onopen = this._socket.onerror = this._socket.onmessage = null; + this._socket.close(); + delete this._socket; + }, + + isUsable: function(callback, context) { + this.callback(function() { callback.call(context, true) }); + this.errback(function() { callback.call(context, false) }); + }, + + encode: function(envelopes) { + return this._xhr.encode(envelopes); + }, + + request: function(envelopes) { + this._xhr.request(envelopes); + } + +}), { + isUsable: function(client, endpoint, callback, context) { + var id = client._clientId; + if (!id) return callback.call(context, false); + + Faye.Transport.XHR.isUsable(client, endpoint, function(usable) { + if (!usable) return callback.call(context, false); + this.create(client, endpoint).isUsable(callback, context); + }, this); + }, + + create: function(client, endpoint) { + var sockets = client.transports.eventsource = client.transports.eventsource || {}, + id = client._clientId; + + endpoint = Faye.copyObject(endpoint); + endpoint.pathname += '/' + (id || ''); + var url = Faye.URI.stringify(endpoint); + + sockets[url] = sockets[url] || new this(client, endpoint); + return sockets[url]; + } +}); + +Faye.extend(Faye.Transport.EventSource.prototype, Faye.Deferrable); +Faye.Transport.register('eventsource', Faye.Transport.EventSource); + +Faye.Transport.XHR = Faye.extend(Faye.Class(Faye.Transport, { + encode: function(envelopes) { + var messages = Faye.map(envelopes, function(e) { return e.message }); + return Faye.toJSON(messages); + }, + + request: function(envelopes) { + var path = this.endpoint.path, + xhr = Faye.ENV.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest(), + self = this; + + xhr.open('POST', path, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Pragma', 'no-cache'); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + + var headers = this._client.headers; + for (var key in headers) { + if (!headers.hasOwnProperty(key)) continue; + xhr.setRequestHeader(key, headers[key]); + } + + var abort = function() { xhr.abort() }; + Faye.Event.on(Faye.ENV, 'beforeunload', abort); + + xhr.onreadystatechange = function() { + if (!xhr || xhr.readyState !== 4) return; + + var parsedMessage = null, + status = xhr.status, + text = xhr.responseText, + successful = (status >= 200 && status < 300) || status === 304 || status === 1223; + + Faye.Event.detach(Faye.ENV, 'beforeunload', abort); + xhr.onreadystatechange = function() {}; + xhr = null; + + if (!successful) return self.handleError(envelopes); + + try { + parsedMessage = JSON.parse(text); + } catch (e) {} + + if (parsedMessage) + self.receive(envelopes, parsedMessage); + else + self.handleError(envelopes); + }; + + xhr.send(this.encode(envelopes)); + } +}), { + isUsable: function(client, endpoint, callback, context) { + callback.call(context, Faye.URI.isSameOrigin(endpoint)); + } +}); + +Faye.Transport.register('long-polling', Faye.Transport.XHR); + +Faye.Transport.CORS = Faye.extend(Faye.Class(Faye.Transport, { + encode: function(envelopes) { + var messages = Faye.map(envelopes, function(e) { return e.message }); + return 'message=' + encodeURIComponent(Faye.toJSON(messages)); + }, + + request: function(envelopes) { + var xhrClass = Faye.ENV.XDomainRequest ? XDomainRequest : XMLHttpRequest, + xhr = new xhrClass(), + headers = this._client.headers, + self = this, + key; + + xhr.open('POST', Faye.URI.stringify(this.endpoint), true); + + if (xhr.setRequestHeader) { + xhr.setRequestHeader('Pragma', 'no-cache'); + for (key in headers) { + if (!headers.hasOwnProperty(key)) continue; + xhr.setRequestHeader(key, headers[key]); + } + } + + var cleanUp = function() { + if (!xhr) return false; + xhr.onload = xhr.onerror = xhr.ontimeout = xhr.onprogress = null; + xhr = null; + }; + + xhr.onload = function() { + var parsedMessage = null; + try { + parsedMessage = JSON.parse(xhr.responseText); + } catch (e) {} + + cleanUp(); + + if (parsedMessage) + self.receive(envelopes, parsedMessage); + else + self.handleError(envelopes); + }; + + xhr.onerror = xhr.ontimeout = function() { + cleanUp(); + self.handleError(envelopes); + }; + + xhr.onprogress = function() {}; + xhr.send(this.encode(envelopes)); + } +}), { + isUsable: function(client, endpoint, callback, context) { + if (Faye.URI.isSameOrigin(endpoint)) + return callback.call(context, false); + + if (Faye.ENV.XDomainRequest) + return callback.call(context, endpoint.protocol === Faye.ENV.location.protocol); + + if (Faye.ENV.XMLHttpRequest) { + var xhr = new Faye.ENV.XMLHttpRequest(); + return callback.call(context, xhr.withCredentials !== undefined); + } + return callback.call(context, false); + } +}); + +Faye.Transport.register('cross-origin-long-polling', Faye.Transport.CORS); + +Faye.Transport.JSONP = Faye.extend(Faye.Class(Faye.Transport, { + encode: function(envelopes) { + var messages = Faye.map(envelopes, function(e) { return e.message }); + var url = Faye.copyObject(this.endpoint); + url.query.message = Faye.toJSON(messages); + url.query.jsonp = '__jsonp' + Faye.Transport.JSONP._cbCount + '__'; + return Faye.URI.stringify(url); + }, + + request: function(envelopes) { + var messages = Faye.map(envelopes, function(e) { return e.message }), + head = document.getElementsByTagName('head')[0], + script = document.createElement('script'), + callbackName = Faye.Transport.JSONP.getCallbackName(), + endpoint = Faye.copyObject(this.endpoint), + self = this; + + endpoint.query.message = Faye.toJSON(messages); + endpoint.query.jsonp = callbackName; + + Faye.ENV[callbackName] = function(data) { + if (!Faye.ENV[callbackName]) return false; + Faye.ENV[callbackName] = undefined; + try { delete Faye.ENV[callbackName] } catch (e) {} + script.parentNode.removeChild(script); + self.receive(envelopes, data); + }; + + script.type = 'text/javascript'; + script.src = Faye.URI.stringify(endpoint); + head.appendChild(script); + } +}), { + _cbCount: 0, + + getCallbackName: function() { + this._cbCount += 1; + return '__jsonp' + this._cbCount + '__'; + }, + + isUsable: function(client, endpoint, callback, context) { + callback.call(context, true); + } +}); + +Faye.Transport.register('callback-polling', Faye.Transport.JSONP); + +})();