Class: Closure::Sources

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/closure/sources.rb

Overview

This class is responsible for scanning source files and managing dependencies.

Defined Under Namespace

Classes: BaseJsNotFoundError, MultipleBaseJsError

Constant Summary collapse

GOOG_REGEX_STRING =

Using regular expressions may seem clunky, but the Python scripts did it this way and I’ve not see it fail in practice.

'^\s*goog\.%s\s*\(\s*[\'"]([^\)]+)[\'"]\s*\)'
PROVIDE_REGEX =
Regexp.new(GOOG_REGEX_STRING % 'provide')
REQUIRE_REGEX =
Regexp.new(GOOG_REGEX_STRING % 'require')
BASE_JS_REGEX =

Google Closure Library base.js is the file with no provides, no requires, and defines goog a particular way.

/^var goog = goog \|\| \{\};/
ENV_FLAG =

Flag env so that refresh is never run more than once per request

'closure.sources_fresh'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dwell = 1.0) ⇒ Sources

Returns a new instance of Sources.

Parameters:

  • dwell (Float) (defaults to: 1.0)

    in seconds.



48
49
50
51
52
53
54
55
# File 'lib/closure/sources.rb', line 48

def initialize(dwell = 1.0)
  @dwell = dwell
  @files = {}
  @sources = []
  @semaphore = Mutex.new
  @last_been_run = nil
  reset_all_computed_instance_vars
end

Instance Attribute Details

#dwellFloat

Limits how often a full refresh is allowed to run. Blocked threads can trigger unneeded refreshes in rare scenarios. Also sent to browser in cache-control for frames performance. Caching, lazy loading, and flagging (of env) make up the remaining techniques for good performance.

Returns:

  • (Float)


64
65
66
# File 'lib/closure/sources.rb', line 64

def dwell
  @dwell
end

Instance Method Details

#add(directory, path = nil) ⇒ Sources

Adds a new directory of source files.

Parameters:

  • path (String) (defaults to: nil)

    Where to mount on the http server.

  • directory (String)

    Filesystem location of your sources.

Returns:



71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/closure/sources.rb', line 71

def add(directory, path=nil)
  raise "immutable once used" if @last_been_run
  if path
    raise "path must start with /" unless path =~ %r{^/}
    path = '' if path == '/'
    raise "path must not end with /" if path =~ %r{/$}
    raise "path already exists" if @sources.find{|s|s[0]==path}
  end
  raise "directory already exists" if @sources.find{|s|s[1]==directory}
  @sources << [File.expand_path(directory), path]
  @sources.sort! {|a,b| (b[1]||'') <=> (a[1]||'')}
  self
end

#base_js(env = {}) ⇒ String

Determine the path_info and query_string for loading base_js.

Returns:

  • (String)


95
96
97
98
99
100
101
102
103
104
# File 'lib/closure/sources.rb', line 95

def base_js(env={})
  if (goog = @goog) and @last_been_run
    return "#{goog[:base_js]}?#{goog[:base_js_mtime].to_i}"
  end
  @semaphore.synchronize do
    refresh(env)
    raise BaseJsNotFoundError unless @goog
    @goog[:base_js]
  end
end

#deps_js(env = {}) ⇒ String

Determine the path_info for where deps_js is located.

Returns:

  • (String)


109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/closure/sources.rb', line 109

def deps_js(env={})
  # Because Server uses this on every call, it's best to never lock.
  # We grab a local goog so we don't need the lock if everything looks good.
  # This works because #refresh creates new @goog hashes instead of modifying.
  if (goog = @goog) and @last_been_run
    return goog[:deps_js]
  end
  @semaphore.synchronize do
    refresh(env)
    raise BaseJsNotFoundError unless @goog
    @goog[:deps_js]
  end
end

#deps_response(base, env = {}) ⇒ Rack::Response

Builds a Rack::Response to serve a dynamic deps.js

Returns:

  • (Rack::Response)


126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/closure/sources.rb', line 126

def deps_response(base, env={})
  @semaphore.synchronize do
    refresh(env)
    base = Pathname.new(base)
    unless @deps[base]
      response = @deps[base] ||= Rack::Response.new
      response.write "// Dynamic Deps by Closure Script\n"
      @files.sort{|a,b|(a[1][:path]||'')<=>(b[1][:path]||'')}.each do |filename, dep|
        if dep[:path]
          path = Pathname.new(dep[:path]).relative_path_from(base)
          path = "#{path}?#{dep[:mtime].to_i}"
          response.write "goog.addDependency(#{path.dump}, #{dep[:provide].inspect}, #{dep[:require].inspect});\n"
        end
      end
      response.headers['Content-Type'] = 'application/javascript'
      response.headers['Cache-Control'] = "max-age=#{[1,@dwell.floor].max}, private, must-revalidate"
      response.headers['Last-Modified'] = Time.now.httpdate
    end
    mod_since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']) rescue nil
    if mod_since == Time.httpdate(@deps[base].headers['Last-Modified'])
      Rack::Response.new [], 304 # Not Modified
    else
      @deps[base]
    end
  end
end

#each {|path, directory| ... } ⇒ Object

Yields path and directory for each of the added sources.

Yields:

  • (path, directory)


88
89
90
# File 'lib/closure/sources.rb', line 88

def each
  @sources.each { |directory, path| yield directory, path }
end

#files_for(namespace, filenames = nil, env = {}) ⇒ Array<String>

Calculate all required files for a namespace.

Parameters:

  • namespace (String)

Returns:

  • (Array<String>)

    New Array of filenames.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/closure/sources.rb', line 157

def files_for(namespace, filenames=nil, env={})
  ns = nil
  @semaphore.synchronize do
    refresh(env)
    # Pivot the deps to a namespace hash
    # @ns is cleared when any requires or provides changes
    unless @ns
      @ns = {}
      @files.each do |filename, dep|
        dep[:provide].each do |provide|
          if @ns[provide]
            @ns = nil
            raise "Namespace #{provide.dump} provided more than once."
          end
          @ns[provide] = {
            :filename => filename,
            :require => dep[:require]
          }
        end
      end
    end
    ns = @ns
    if !filenames or filenames.empty?
      raise BaseJsNotFoundError unless @goog
      filenames ||= []
      filenames << @goog[:base_filename]
    end
  end
  # Since @ns is only unset, not modified, by another thread, we
  # can work with a local reference.  This has been finely tuned and
  # runs fast, but it's still nice to release any other threads early.
  calcdeps(ns, namespace, filenames)
end

#invalidate(env) ⇒ Object

Certain Script operations, such as building Templates, will need to invalidate the cache.



222
223
224
225
# File 'lib/closure/sources.rb', line 222

def invalidate(env)
  env.delete ENV_FLAG
  @last_been_run = Time.at 0
end

#namespaces_for(filename, env = {}) ⇒ String

Return all provided and required namespaces for a file.

Parameters:

  • filename (String)

Returns:

  • (String)


210
211
212
213
214
215
216
217
# File 'lib/closure/sources.rb', line 210

def namespaces_for(filename, env={})
  @semaphore.synchronize do
    refresh(env)
    file = @files[filename]
    raise "#{filename.dump} not found" unless file
    file[:provide] + file[:require]
  end
end

#src_for(filename, env = {}) ⇒ String

Calculate the file server path for a filename

Parameters:

  • filename (String)

Returns:

  • (String)


195
196
197
198
199
200
201
202
203
204
# File 'lib/closure/sources.rb', line 195

def src_for(filename, env={})
  @semaphore.synchronize do
    refresh(env)
    file = @files[filename]
    unless file and file.has_key? :path
      raise "#{filename.dump} is not available from file server"
    end
    "#{file[:path]}?#{file[:mtime].to_i}"
  end
end