Class: Klenod::Build::Graph

Inherits:
Object
  • Object
show all
Includes:
TSort
Defined in:
lib/klenod/build/graph.rb,
lib/klenod/build/graph/invalidator.rb

Defined Under Namespace

Classes: AsyncResult, FailedModule, InFlightLoad, Invalidator

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source_dir:, plugins:, mode: :development, asset_generation_concurrency: AssetGenerationQueue::DEFAULT_CONCURRENCY, asset_download_concurrency: AssetGenerationQueue::DEFAULT_DOWNLOAD_CONCURRENCY, profiler: nil) ⇒ Graph

Returns a new instance of Graph.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/klenod/build/graph.rb', line 62

def initialize(
  source_dir:,
  plugins:,
  mode: :development,
  asset_generation_concurrency: AssetGenerationQueue::DEFAULT_CONCURRENCY,
  asset_download_concurrency: AssetGenerationQueue::DEFAULT_DOWNLOAD_CONCURRENCY,
  profiler: nil
)
  @profiler = profiler || Profiler.new
  @resolver = Resolver.new(source_dir: source_dir, profiler: @profiler)
  @plugins = plugins
  @mode = mode
  @asset_generation_queue =
    AssetGenerationQueue.new(
      concurrency: asset_generation_concurrency,
      download_concurrency: asset_download_concurrency
    )
  @records = {}
  @mods = {}
  @virtual_sources = {}
  @virtual_metadata = {}
  @virtual_owners = {}
  @loading_tasks = {}
end

Instance Attribute Details

#asset_generation_queueObject (readonly)

Returns the value of attribute asset_generation_queue.



59
60
61
# File 'lib/klenod/build/graph.rb', line 59

def asset_generation_queue
  @asset_generation_queue
end

#modeObject (readonly)

Returns the value of attribute mode.



59
60
61
# File 'lib/klenod/build/graph.rb', line 59

def mode
  @mode
end

#modsObject (readonly)

Returns the value of attribute mods.



59
60
61
# File 'lib/klenod/build/graph.rb', line 59

def mods
  @mods
end

#pluginsObject (readonly)

Returns the value of attribute plugins.



60
61
62
# File 'lib/klenod/build/graph.rb', line 60

def plugins
  @plugins
end

#profilerObject (readonly)

Returns the value of attribute profiler.



59
60
61
# File 'lib/klenod/build/graph.rb', line 59

def profiler
  @profiler
end

#recordsObject (readonly)

Returns the value of attribute records.



59
60
61
# File 'lib/klenod/build/graph.rb', line 59

def records
  @records
end

Instance Method Details

#absolute_path(module_id) ⇒ Object



196
197
198
# File 'lib/klenod/build/graph.rb', line 196

def absolute_path(module_id)
  @resolver.absolute_path(module_id)
end

#asset(output_path) ⇒ Object



124
125
126
# File 'lib/klenod/build/graph.rb', line 124

def asset(output_path)
  assets.fetch(output_path)
end

#asset_references_for_module(record_or_module_id, type: nil, content_type: nil, recursive: true) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/klenod/build/graph.rb', line 147

def asset_references_for_module(record_or_module_id, type: nil, content_type: nil, recursive: true)
  seen_assets = {}
  module_ids_for_assets(record_or_module_id, recursive: recursive, type: type)
    .each_with_index
    .flat_map do |module_id, index|
      module_assets = @records.fetch(module_id).assets
      module_assets.filter_map do |asset|
        next unless asset_matches?(asset, type: type, content_type: content_type)
        next if seen_assets.key?(asset.output_path)

        seen_assets[asset.output_path] = true
        Runtime::AssetReference.new(index:, asset:)
      end
    end
end

#assetsObject



120
121
122
# File 'lib/klenod/build/graph.rb', line 120

def assets
  @records.values.select { |record| record.status == :loaded }.flat_map(&:assets).to_h { |asset| [asset.output_path, asset] }
end

#assets_for(logical_name) ⇒ Object



138
139
140
# File 'lib/klenod/build/graph.rb', line 138

def assets_for(logical_name)
  assets.values.select { |asset| asset.logical_name == logical_name.to_s }
end

#assets_for_module(record_or_module_id, type: nil, content_type: nil, recursive: true) ⇒ Object



142
143
144
145
# File 'lib/klenod/build/graph.rb', line 142

def assets_for_module(record_or_module_id, type: nil, content_type: nil, recursive: true)
  asset_references_for_module(record_or_module_id, type: type, content_type: content_type, recursive: recursive)
    .map(&:asset)
end

#bundle(entrypoints:) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/klenod/build/graph.rb', line 99

def bundle(entrypoints:)
  loaded_entrypoints =
    @profiler.measure(:entrypoints, count: entrypoints.length) do
      entrypoints.to_h do |entrypoint|
        dependency = Dependency.create(specifier: entrypoint, importer_id: nil, kind: :entrypoint)
        resolved = resolve_dependency(dependency)
        [entrypoint, collect_module(resolved.module_id).id.to_s]
      end
    end
  @profiler.measure(:runtime_dependencies) { collect_all_runtime_dependencies }

  @profiler.measure(:bundle_specs) do
    Runtime::Bundle.new(
      loaded_entrypoints,
      runtime_module_specs,
      runtime_asset_specs,
      source_root: source_dir.to_s
    )
  end
end

#collect(specifier) ⇒ Object



93
94
95
96
97
# File 'lib/klenod/build/graph.rb', line 93

def collect(specifier)
  dependency = Dependency.create(specifier: specifier, importer_id: nil, kind: :entrypoint)
  resolved = resolve_dependency(dependency)
  collect_module(resolved.module_id)
end

#collect_module(module_id, force: false) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/klenod/build/graph.rb', line 280

def collect_module(module_id, force: false)
  raise_import_cycle_if_present(module_id) unless force

  if !force
    if @mode == :build && (cached = @records[module_id])
      @profiler.count(:collect_module_cache_hit)
      raise_failed_module!(cached)
      return cached
    end

    return @loading_tasks.fetch(module_id).wait if @loading_tasks.key?(module_id)

    begin
      in_flight_load = InFlightLoad.create
      @loading_tasks[module_id] = in_flight_load
      result = AsyncResult.capture { collect_module_now(module_id, force: force) }
      in_flight_load.finish(result)
      return AsyncResult.unwrap(result)
    ensure
      @loading_tasks.delete(module_id) if @loading_tasks[module_id].equal?(in_flight_load)
    end
  end

  collect_module_now(module_id, force: force)
end

#collect_module_now(module_id, force: false) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/klenod/build/graph.rb', line 306

def collect_module_now(module_id, force: false)
  with_loading_stack(module_id) do
    loaded_source = read_module_source(module_id)
    source = loaded_source.source
    source_hash = loaded_source.source_hash || Hashing.hexdigest(source)
    cached = @records[module_id]

    if cached&.source_hash == source_hash && !force
      raise_failed_module!(cached)
      @profiler.count(:collect_module_cache_hit)
      return cached
    end
    @profiler.count(:collect_module_cache_miss)

    transform = loaded_source.transform || transform_module_source(module_id, source)
    resolved_dependencies = resolve_transform_dependencies(module_id, transform)
    dependency_records = collect_eager_dependency_records(resolved_dependencies)
    transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
    assert_supported_transform!(module_id, source, transform)
    transformed_hash = Hashing.hexdigest(transform.code)
    record = build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, cached)

    @records[module_id] = record
    @mods.delete(module_id)
    @profiler.progress(:collect_module, module_id: record.id.to_s, total_records: @records.length)
    record
  end
end

#each_asset(&block) ⇒ Object



163
164
165
166
167
# File 'lib/klenod/build/graph.rb', line 163

def each_asset(&block)
  return enum_for(:each_asset) unless block

  assets.each_value(&block)
end

#evaluate_module(module_id) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/klenod/build/graph.rb', line 335

def evaluate_module(module_id)
  return @mods.fetch(module_id) if @mods.key?(module_id)

  record = @records.fetch(module_id) { collect_module(module_id) }
  raise_failed_module!(record)
  evaluate_eager_dependencies(record.resolved_dependencies)
  dependency_records = dependency_records_for(eager_dependencies(record.resolved_dependencies))
  mod =
    Runtime::Mod.new(
      module_id.to_s,
      record.transformed_source,
      imports: imports_for(record.resolved_dependencies, dependency_records),
      source_map: record.source_map,
      version: record.version,
      eval_path: eval_path_for(module_id)
    )

  @mods[module_id] = mod
end

#evaluated?(record_or_module_id) ⇒ Boolean

Returns:

  • (Boolean)


133
134
135
136
# File 'lib/klenod/build/graph.rb', line 133

def evaluated?(record_or_module_id)
  mod = @mods[module_id_for(record_or_module_id)]
  mod && !mod.is_a?(FailedModule)
end

#exports(record_or_module_id) ⇒ Object



128
129
130
131
# File 'lib/klenod/build/graph.rb', line 128

def exports(record_or_module_id)
  module_id = module_id_for(record_or_module_id)
  evaluate_module(module_id).const_get(:Exports)
end

#invalidate_paths(changed_paths, removed_paths: []) ⇒ Object



225
226
227
228
# File 'lib/klenod/build/graph.rb', line 225

def invalidate_paths(changed_paths, removed_paths: [])
  Invalidator.new(self, @resolver, source_loader: method(:read_module_source))
    .invalidate_paths(changed_paths, removed_paths: removed_paths)
end

#load(specifier) ⇒ Object



87
88
89
90
91
# File 'lib/klenod/build/graph.rb', line 87

def load(specifier)
  dependency = Dependency.create(specifier: specifier, importer_id: nil, kind: :entrypoint)
  resolved = resolve_dependency(dependency)
  load_module(resolved.module_id)
end

#load_module(module_id, force: false, reevaluate: false) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/klenod/build/graph.rb', line 230

def load_module(module_id, force: false, reevaluate: false)
  raise_import_cycle_if_present(module_id) unless force || reevaluate

  if !force && !reevaluate
    return @loading_tasks.fetch(module_id).wait if @loading_tasks.key?(module_id)

    begin
      in_flight_load = InFlightLoad.create
      @loading_tasks[module_id] = in_flight_load
      result = AsyncResult.capture { load_module_now(module_id, force: force, reevaluate: reevaluate) }
      in_flight_load.finish(result)
      return AsyncResult.unwrap(result)
    ensure
      @loading_tasks.delete(module_id) if @loading_tasks[module_id].equal?(in_flight_load)
    end
  end

  load_module_now(module_id, force: force, reevaluate: reevaluate)
end

#load_module_now(module_id, force: false, reevaluate: false) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/klenod/build/graph.rb', line 250

def load_module_now(module_id, force: false, reevaluate: false)
  with_loading_stack(module_id) do
    loaded_source = read_module_source(module_id)
    source = loaded_source.source
    source_hash = loaded_source.source_hash || Hashing.hexdigest(source)
    cached = @records[module_id]

    if cached&.source_hash == source_hash && !force && !reevaluate
      raise_failed_module!(cached)
      @profiler.count(:load_module_cache_hit)
      evaluate_module(module_id) unless @mods.key?(module_id)
      return cached
    end
    @profiler.count(:load_module_cache_miss)

    transform = loaded_source.transform || transform_module_source(module_id, source)
    resolved_dependencies = resolve_transform_dependencies(module_id, transform)
    dependency_records = load_eager_dependency_records(resolved_dependencies)
    transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
    assert_supported_transform!(module_id, source, transform)
    transformed_hash = Hashing.hexdigest(transform.code)
    mod = instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached)
    record = build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, mod)

    @records[module_id] = record
    @mods[module_id] = mod
    record
  end
end

#module_id_for(record_or_module_id) ⇒ Object



366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/klenod/build/graph.rb', line 366

def module_id_for(record_or_module_id)
  case record_or_module_id
  when ModuleRecord
    record_or_module_id.id
  when ModuleId
    record_or_module_id
  else
    ref = record_or_module_id.to_s
    @records.each_key.find { |module_id| module_id.to_s == ref } ||
      module_id_for_canonical_ref(ref) ||
      module_id_for_absolute_ref(ref) ||
      raise(KeyError, "No module loaded for #{record_or_module_id.inspect}")
  end
end

#register_virtual_module(module_id, source, owner_id: nil, metadata: {}) ⇒ Object



204
205
206
207
208
# File 'lib/klenod/build/graph.rb', line 204

def register_virtual_module(module_id, source, owner_id: nil, metadata: {})
  @virtual_sources[module_id] = source
  @virtual_metadata[module_id] = .freeze
  @virtual_owners[module_id] = owner_id if owner_id
end

#resolve_dependency(dependency) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/klenod/build/graph.rb', line 175

def resolve_dependency(dependency)
  if (virtual_module_id = dependency.[:virtual_module_id])
    @profiler.count(:resolve_virtual_metadata_hit)
    return ResolvedDependency.new(dependency, virtual_module_id, {virtual: true})
  end

  @plugins.each do |plugin|
    @profiler.count(:plugin_resolve_check)
    resolved =
      @profiler.measure(:plugin_resolve, plugin: plugin.class.name, specifier: dependency.specifier) do
        plugin.resolve(dependency, self)
      end
    @profiler.count(:plugin_resolve_hit) if resolved
    return resolved if resolved
  end

  @profiler.measure(:resolver_resolve, specifier: dependency.specifier) do
    @resolver.resolve(dependency)
  end
end

#source_dirObject



200
201
202
# File 'lib/klenod/build/graph.rb', line 200

def source_dir
  @resolver.source_dir
end

#tsort_each_child(node, &block) ⇒ Object



359
360
361
362
363
364
# File 'lib/klenod/build/graph.rb', line 359

def tsort_each_child(node, &block)
  record = @records.fetch(node)
  return if record.status != :loaded

  record.resolved_dependencies.map(&:module_id).each(&block)
end

#tsort_each_node(&block) ⇒ Object



355
356
357
# File 'lib/klenod/build/graph.rb', line 355

def tsort_each_node(&block)
  @records.each_key(&block)
end

#unregister_virtual_modules(owner_id) ⇒ Object



214
215
216
217
218
219
220
221
222
223
# File 'lib/klenod/build/graph.rb', line 214

def unregister_virtual_modules(owner_id)
  module_ids = @virtual_owners.filter_map { |module_id, owner| module_id if owner == owner_id }
  module_ids.each do |module_id|
    @virtual_sources.delete(module_id)
    @virtual_metadata.delete(module_id)
    @virtual_owners.delete(module_id)
    @records.delete(module_id)
    @mods.delete(module_id)
  end
end

#virtual_module_metadata(module_id) ⇒ Object



210
211
212
# File 'lib/klenod/build/graph.rb', line 210

def (module_id)
  @virtual_metadata.fetch(module_id, {})
end

#wait_for_assetsObject



169
170
171
172
173
# File 'lib/klenod/build/graph.rb', line 169

def wait_for_assets
  with_async_task do |task|
    each_asset.map { |asset| task.async { asset.wait } }.each(&:wait)
  end
end