├── .gitignore ├── .travis.yml ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── async_partial.gemspec ├── bin ├── console └── setup ├── lib ├── async_partial.rb └── async_partial │ ├── handlers │ ├── erubi.rb │ ├── erubis.rb │ ├── haml.rb │ └── slim.rb │ └── railtie.rb └── test ├── async_partial_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | Gemfile.lock 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.6.0 5 | before_install: gem install bundler -v 1.16.1 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in async_partial.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Akira Matsuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AsyncPartial 2 | 3 | Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/async_partial`. To experiment with that code, run `bin/console` for an interactive prompt. 4 | 5 | TODO: Delete this and the text above, and describe your gem 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'async_partial' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install async_partial 22 | 23 | ## Usage 24 | 25 | TODO: Write usage instructions here 26 | 27 | ## Development 28 | 29 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 30 | 31 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 32 | 33 | ## Contributing 34 | 35 | Bug reports and pull requests are welcome on GitHub at https://github.com/amatsuda/async_partial. 36 | 37 | ## License 38 | 39 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /async_partial.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "async_partial" 6 | spec.version = '0.8.0'.freeze 7 | spec.authors = ["Akira Matsuda"] 8 | spec.email = ["ronnie@dio.jp"] 9 | 10 | spec.summary = 'Asynchronous partial renderer for Rails' 11 | spec.description = 'render asynchronously for the speed!' 12 | spec.homepage = 'https://github.com/amatsuda/async_partial' 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 16 | f.match(%r{^(test|spec|features)/}) 17 | end 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency 'rails' 23 | spec.add_dependency 'method_source' 24 | spec.add_dependency 'concurrent-ruby' 25 | 26 | spec.add_development_dependency 'bundler' 27 | spec.add_development_dependency 'rake' 28 | spec.add_development_dependency 'minitest' 29 | end 30 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "async_partial" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/async_partial.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'async_partial/railtie' 4 | 5 | module AsyncPartial 6 | class << self 7 | def executor 8 | @executor ||= Concurrent::ThreadPoolExecutor.new(min_threads: [2, Concurrent.processor_count - 1].max, max_threads: [2, Concurrent.processor_count - 1].max, max_queue: 0) 9 | end 10 | end 11 | 12 | module PartialRenderer 13 | private 14 | 15 | def render_partial 16 | if @locals.delete :async 17 | AsyncResult.new { super } 18 | else 19 | super 20 | end 21 | end 22 | 23 | def collection_with_template 24 | super.map! do |v| 25 | AsyncPartial::AsyncResult === v ? v.value : v 26 | end 27 | end 28 | end 29 | 30 | module CollectionPartialTemplateRenderer 31 | def render(view, locals, buffer = nil, &block) 32 | locals = locals.dup 33 | if locals.delete :async 34 | AsyncResult.new { super } 35 | else 36 | super 37 | end 38 | end 39 | end 40 | 41 | module PerThreadBufferStack 42 | def render(view, locals, buffer = nil, &block) 43 | buffer ||= ActionView::OutputBuffer.new 44 | (Thread.current[:output_buffers] ||= []).push buffer 45 | super 46 | ensure 47 | Thread.current[:output_buffers].pop 48 | end 49 | end 50 | 51 | module CaptureHelper 52 | def capture(*args, &block) 53 | buf = Thread.current[:output_buffers].last 54 | value = nil 55 | buffer = with_output_buffer(buf) { value = block.call(*args) } 56 | if (string = buffer.presence || value) && string.is_a?(String) 57 | ERB::Util.html_escape string 58 | end 59 | end 60 | 61 | # Simply rewind what's written in the buffer 62 | def with_output_buffer(buf = nil) #:nodoc: 63 | buffer_values_was = buf.buffer_values.clone 64 | yield 65 | buffer_values_was.each {|e| buf.buffer_values.shift if buf.buffer_values[0] == e} 66 | buf.to_s 67 | ensure 68 | buf.buffer_values = buffer_values_was 69 | end 70 | end 71 | 72 | module FutureUrlHelper 73 | def link_to(name = nil, options = nil, html_options = nil, &block) 74 | if ((Hash === options) && options.delete(:async)) || ((Hash === html_options) && html_options.delete(:async)) 75 | AsyncResult.new { super } 76 | else 77 | super 78 | end 79 | end 80 | end 81 | 82 | class AsyncResult 83 | def initialize(&block) 84 | @future = Concurrent::Future.execute(executor: AsyncPartial.executor, &block) 85 | end 86 | 87 | def html_safe? 88 | true 89 | end 90 | 91 | def to_s 92 | self 93 | end 94 | 95 | def value 96 | @future.value! 97 | end 98 | end 99 | 100 | module ArrayBuffer 101 | attr_accessor :buffer_values 102 | 103 | def initialize(*) 104 | super 105 | @buffer_values = [] 106 | end 107 | 108 | def <<(value) 109 | @buffer_values << [value, :<<] unless value.nil? 110 | self 111 | end 112 | alias :append= :<< 113 | 114 | def safe_concat(value) 115 | raise ActiveSupport::SafeBuffer::SafeConcatError unless html_safe? 116 | @buffer_values << [value, :safe_concat] unless value.nil? 117 | self 118 | end 119 | alias :safe_append= :safe_concat 120 | 121 | def safe_expr_append=(val) 122 | @buffer_values << [val, :safe_expr_append] unless val.nil? 123 | self 124 | end 125 | 126 | def to_s 127 | result = @buffer_values.each_with_object(ActiveSupport::SafeBuffer.new) do |(v, meth), buf| 128 | if meth == :<< 129 | if AsyncPartial::AsyncResult === v 130 | buf << v.value 131 | else 132 | buf << v.to_s 133 | end 134 | else 135 | if AsyncPartial::AsyncResult === v 136 | buf.safe_concat(v.value) 137 | else 138 | buf.safe_concat(v.to_s) 139 | end 140 | end 141 | end 142 | result.to_s 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/async_partial/handlers/erubi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActionView 4 | class Template 5 | module Handlers 6 | class ERB 7 | class ThreadSafeErubi < Erubi 8 | def initialize(input, properties = {}) 9 | @newline_pending = 0 10 | 11 | # Dup properties so that we don't modify argument 12 | properties = Hash[properties] 13 | properties[:preamble] = "output_buffer ||= ActionView::OutputBuffer.new;" 14 | properties[:postamble] = "output_buffer.to_s" 15 | properties[:bufvar] = "output_buffer" 16 | properties[:escapefunc] = "" 17 | 18 | # Call ::Erubi::Engine#initializer 19 | method(__method__).super_method.super_method.call input, properties 20 | end 21 | 22 | private 23 | 24 | eval Erubi.instance_method(:add_text).source.gsub('@output_buffer', '#{@bufvar}') 25 | eval Erubi.instance_method(:add_expression).source.gsub('@output_buffer', '#{@bufvar}') 26 | eval Erubi.instance_method(:flush_newline_if_pending).source.gsub('@output_buffer', '#{@bufvar}') 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/async_partial/handlers/erubis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'method_source' 4 | 5 | module ActionView 6 | class Template 7 | module Handlers 8 | class ERB 9 | class ThreadSafeErubis < Erubis 10 | eval Erubis.instance_method(:add_preamble).source.gsub('@output_buffer', 'output_buffer') 11 | eval Erubis.instance_method(:add_text).source.gsub('@output_buffer', 'output_buffer') 12 | eval Erubis.instance_method(:add_expr).source.gsub('@output_buffer', 'output_buffer') 13 | eval Erubis.instance_method(:add_expr_literal).source.gsub('@output_buffer', 'output_buffer') 14 | eval Erubis.instance_method(:add_expr_escaped).source.gsub('@output_buffer', 'output_buffer') 15 | eval Erubis.instance_method(:add_postamble).source.gsub('@output_buffer', 'output_buffer') 16 | eval Erubis.instance_method(:flush_newline_if_pending).source.gsub('@output_buffer', 'output_buffer') 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/async_partial/handlers/haml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AsyncPartial 4 | class HamlArrayBuffer < Array 5 | def html_safe 6 | map {|v| AsyncPartial::AsyncResult === v ? v.value : v}.join.html_safe 7 | end 8 | 9 | def rstrip! 10 | if last.frozen? 11 | if (stripped = last.dup.rstrip!) 12 | self[-1] = stripped 13 | end 14 | else 15 | last.rstrip! 16 | end 17 | if last.blank? 18 | last.pop 19 | rstrip! 20 | end 21 | self 22 | end 23 | end 24 | 25 | module HamlArrayBufferizer 26 | def initialize(*) 27 | super 28 | @buffer = AsyncPartial::HamlArrayBuffer.new 29 | end 30 | end 31 | 32 | Haml::Buffer.prepend HamlArrayBufferizer 33 | end 34 | -------------------------------------------------------------------------------- /lib/async_partial/handlers/slim.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Temple 4 | module Generators 5 | # Implements a threaded rails output buffer. 6 | # 7 | # output_buffer = ActionView::OutputBuffer.new 8 | # output_buffer.safe_concat "static" 9 | # output_buffer.safe_concat dynamic 10 | # output_buffer.to_s 11 | class ThreadedRailsOutputBuffer < RailsOutputBuffer 12 | set_options buffer_class: 'ActionView::OutputBuffer', buffer: 'output_buffer' 13 | 14 | def return_buffer 15 | "#{buffer}.to_s" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/async_partial/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AsyncPartial 4 | class Railtie < ::Rails::Railtie 5 | initializer 'async_partial' do 6 | ActiveSupport.on_load :action_view do 7 | ActionView::PartialRenderer.prepend AsyncPartial::PartialRenderer 8 | ActionView::OutputBuffer.prepend AsyncPartial::ArrayBuffer 9 | ActionView::Base.prepend AsyncPartial::CaptureHelper 10 | ActionView::Template.prepend AsyncPartial::PerThreadBufferStack 11 | ActionView::Template.prepend AsyncPartial::CollectionPartialTemplateRenderer 12 | ActionView::Base.prepend AsyncPartial::FutureUrlHelper 13 | 14 | begin 15 | require 'action_view/template/handlers/erb/erubi' 16 | require_relative 'handlers/erubi' 17 | 18 | ActionView::Template::Handlers::ERB.erb_implementation = ActionView::Template::Handlers::ERB::ThreadSafeErubi 19 | rescue LoadError 20 | begin 21 | require 'action_view/template/handlers/erb' 22 | require_relative 'handlers/erubis' 23 | 24 | ActionView::Template::Handlers::ERB.erb_implementation = ActionView::Template::Handlers::ERB::ThreadSafeErubis 25 | rescue LoadError 26 | raise 'No Erubi nor Erubis found.' 27 | end 28 | end 29 | end 30 | end 31 | 32 | if Gem.loaded_specs.detect {|g| g[0] == 'haml'} 33 | initializer 'async_partial_haml', after: :haml do 34 | require 'haml/buffer' 35 | require_relative 'handlers/haml' 36 | end 37 | end 38 | 39 | if Gem.loaded_specs.detect {|g| g[0] == 'slim'} 40 | initializer 'async_partial_slim', after: 'slim_rails.configure_template_digestor' do 41 | require 'temple' 42 | require 'slim' 43 | require 'temple/generators/rails_output_buffer' 44 | require_relative 'handlers/slim' 45 | 46 | Temple::Templates::Rails(Slim::Engine, register_as: :slim, generator: Temple::Generators::ThreadedRailsOutputBuffer, disable_capture: true, streaming: true) 47 | end 48 | end 49 | 50 | if Gem.loaded_specs.detect {|g| g[0] == 'faml'} 51 | initializer 'async_partial_faml', after: :faml do 52 | require 'temple' 53 | require 'temple/generators/rails_output_buffer' 54 | require_relative 'handlers/slim' 55 | 56 | ActionView::Template.register_template_handler(:haml, ->(template) { Faml::Engine.new(use_html_safe: true, generator: Temple::Generators::ThreadedRailsOutputBuffer, filename: template.identifier).call(template.source) }) 57 | ActionView::Template.register_template_handler(:faml, ->(template) { Faml::Engine.new(use_html_safe: true, generator: Temple::Generators::ThreadedRailsOutputBuffer, filename: template.identifier).call(template.source) }) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/async_partial_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AsyncPartialTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::AsyncPartial::VERSION 6 | end 7 | 8 | def test_it_does_something_useful 9 | assert false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "async_partial" 3 | 4 | require "minitest/autorun" 5 | --------------------------------------------------------------------------------