class Capsium::Reactor::Mount
One mounted package: the URL prefix it answers under plus its serving state (routes, merged view; ARCHITECTURE.md section 7). The reactor serves a list of mounts resolved by longest-prefix matching. Default mount points: the first source at “/”, each additional source at “/<metadata.name>/”.
Constants
- Entry
-
A mount entry as accepted from the CLI or a JSON config file: “path” may be nil (the default is assigned on build), “source” is a package directory or .cap file, “store” optionally overrides the global package store for this mount.
- ROOT_PATH
- SPEC_SEPARATOR
Attributes
Package
String
Public Class Methods
(Array[Entry] entries, ?store: (Package::Store | String)? store, ?registry: (Capsium::Registry | String)? registry) → Array[Mount]
Source
# File lib/capsium/reactor/mount.rb, line 66 def self.build(entries, store: nil, registry: nil) built = [] entries.each_with_index do |entry, index| package_store = entry.store || store package = load_package(entry.source, package_store, registry) path = entry.path || default_path(index, package) built << new(path: path, package: package, store: package_store, registry: registry) end check_conflicts!(built) built rescue StandardError built.each { |mount| mount.package.cleanup } raise end
Builds the mounts for a list of entries: loads each package, assigns default paths (first entry “/”, each additional “/<metadata.name>/”) and rejects duplicate prefixes with a MountConflictError. On failure every package loaded so far is cleaned up again.
(String config_file) → Array[Entry]
Source
# File lib/capsium/reactor/mount.rb, line 40 def self.config_entries(config_file) doc = JSON.parse(File.read(config_file)) mounts = doc.is_a?(Hash) ? doc["mounts"] : nil unless mounts.is_a?(Array) && mounts.all?(Hash) raise Error, "Invalid mount config #{config_file}: expected " \ '{"mounts": [{"path": ..., "source": ...}]}' end mounts.map do |entry| unless entry["source"].is_a?(String) raise Error, "Invalid mount config #{config_file}: every mount " \ "needs a \"source\"" end Entry.new(path: entry["path"], source: entry["source"], store: entry["store"]) end rescue JSON::ParserError => e raise Error, "Invalid mount config #{config_file}: #{e.message}" end
The mount entries of a JSON config file: {“mounts”: [{“path”: “/”, “source”: “dir-or.cap”, “store”: “…”}]}
(Package | String source, (Package::Store | String)? store, (Capsium::Registry | String)? registry) → Package
Source
# File lib/capsium/reactor/mount.rb, line 84 def self.load_package(source, store, registry) return source unless source.is_a?(String) Package.new(source, store: store, registry: registry) end
Loads a mount source (a package directory, .cap file or an already-built Package) with the given resolution context.
(path: String? path, package: Package | String package, ?store: (Package::Store | String)? store, ?registry: (Capsium::Registry | String)? registry, ?workdir: String? workdir) → void
Source
# File lib/capsium/reactor/mount.rb, line 114 def initialize(path:, package:, store: nil, registry: nil, workdir: nil) @path = self.class.normalize_path(path) @store = store @registry = registry @package = self.class.load_package(package, store, registry) attach_workdir(workdir) if workdir end
(String? path) → String
Source
# File lib/capsium/reactor/mount.rb, line 107 def self.normalize_path(path) return ROOT_PATH if path.nil? || path == ROOT_PATH normalized = path.start_with?(ROOT_PATH) ? path : "#{ROOT_PATH}#{path}" normalized.chomp(ROOT_PATH) end
Canonical mount prefix: leading “/”, no trailing “/” (except the root itself).
(String spec) → Entry
Source
# File lib/capsium/reactor/mount.rb, line 29 def self.parse_spec(spec) path, separator, source = spec.to_s.partition(SPEC_SEPARATOR) if separator.empty? || path.empty? || source.empty? raise Error, "Invalid mount spec #{spec.inspect} (expected PATH=SOURCE)" end Entry.new(path: path, source: source, store: nil) end
Parses a “–mount PATH=SOURCE” command-line value into an Entry.
Public Instance Methods
(String workdir) → void
Source
# File lib/capsium/reactor/mount.rb, line 125 def attach_workdir(workdir) @overlay = Overlay.new(root: File.join(workdir, "overlays", @package.name)) end
Attaches the reactor workdir: the writable overlay (topmost layer, ARCHITECTURE.md section 5a) lives under it. Reads always resolve through the overlay; writes require writable?.
() → ContentApi
Source
# File lib/capsium/reactor/mount.rb, line 173 def content_api = @content_api ||= ContentApi.new(self)
() → DataApi
Source
# File lib/capsium/reactor/mount.rb, line 171 def data_api = @data_api ||= DataApi.new(self)
(Package::Dataset dataset) → untyped
Source
# File lib/capsium/reactor/mount.rb, line 167 def dataset_data(dataset) @overlay ? @overlay.dataset_data(dataset) : dataset.data end
The dataset as served: base data merged with the overlay’s mutation log.
() → bool
Source
# File lib/capsium/reactor/mount.rb, line 177 def graphql? @package.storage.datasets.any? { |dataset| !dataset.config.sqlite? } end
Whether this mount exposes a GraphQL API (any file-backed dataset; SQLite datasets are skipped).
() → GraphqlApi
Source
# File lib/capsium/reactor/mount.rb, line 181 def graphql_api = @graphql_api ||= GraphqlApi.new(self)
(String request_path) → String
Source
# File lib/capsium/reactor/mount.rb, line 144 def inner_path(request_path) inner = request_path.delete_prefix(path) return ROOT_PATH if inner.empty? return inner if inner.start_with?(ROOT_PATH) "#{ROOT_PATH}#{inner}" end
The package-local path for a request path under this mount (“/” when the request hits the mount prefix itself).
(String request_path) → bool
Source
# File lib/capsium/reactor/mount.rb, line 137 def matches?(request_path) path == ROOT_PATH || request_path == path || request_path.start_with?("#{path}#{ROOT_PATH}") end
Whether this mount answers the given request path: the prefix itself or anything below it. The root mount answers everything.
() → Package::MergedView
Source
# File lib/capsium/reactor/mount.rb, line 157 def merged_view @merged_view ||= if @overlay @package.merged_view(extra_layers: [@overlay.layer]) else @package.merged_view end end
The merged content view; the overlay is always the topmost layer when a workdir is attached (hot-swap: content writes and tombstones resolve on the next request).
() → Package
Source
# File lib/capsium/reactor/mount.rb, line 199 def reload @merged_view = nil @graphql_api = nil @package = Package.new(@package.path, store: @store, registry: @registry) end
Reloads the package from its (prepared) path, e.g. after the filesystem listener noticed changes. The overlay survives (it lives in the reactor workdir, not in the package).
() → Package::Routes
Source
# File lib/capsium/reactor/mount.rb, line 152 def routes = @package.routes
() → Array[String]
Source
# File lib/capsium/reactor/mount.rb, line 190 def server_paths return [path] unless path == ROOT_PATH routes.config.routes.map(&:serving_path) end
The WEBrick mount points serving this mount: every serving path for the root mount (unchanged single-package behavior), the prefix itself for a non-root mount (longest-prefix matching routes everything below it to the reactor).
() → String
Source
# File lib/capsium/reactor/mount.rb, line 184 def summary = "#{@package.name} #{@package.metadata.version}"
The one-line “name version” summary used in reactor logs.
() → bool
Source
# File lib/capsium/reactor/mount.rb, line 131 def writable? !@overlay.nil? && @package.metadata.read_only != true end
Writable unless the package declares “readOnly”: true; writes then produce 403s and no overlay state is recorded.