Class: Capsium::Reactor::Overlay

Inherits:
Object
  • Object
show all
Defined in:
lib/capsium/reactor/overlay.rb,
sig/capsium/reactor/overlay.rbs

Overview

The writable top overlay layer of a mounted package (ARCHITECTURE.md section 5a): an append-only record of writes in the reactor workdir (content files, tombstones, per-dataset mutation logs). The immutable package never changes on disk.

Defined Under Namespace

Classes: ConflictError, Error, ItemNotFoundError, SchemaViolationError, UnsupportedDatasetError

Constant Summary collapse

TOMBSTONE_FILE =

Returns:

  • (String)
Package::MergedView::TOMBSTONE_FILE
SAFE_NAME_PATTERN =

Returns:

  • (Regexp)
/\A[\w.-]+\z/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:) ⇒ Overlay

Returns a new instance of Overlay.

Parameters:

  • root: (String root)


48
49
50
51
52
# File 'lib/capsium/reactor/overlay.rb', line 48

def initialize(root:)
  @root = root
  @tombstones = load_tombstones
  @logs = {}
end

Instance Attribute Details

#rootString (readonly)

Returns the value of attribute root.

Returns:

  • (String)


46
47
48
# File 'lib/capsium/reactor/overlay.rb', line 46

def root
  @root
end

#tombstonesSet[String] (readonly)

Returns the value of attribute tombstones.

Returns:

  • (Set[String])


46
47
48
# File 'lib/capsium/reactor/overlay.rb', line 46

def tombstones
  @tombstones
end

Instance Method Details

#append_item(dataset, item) ⇒ String

Appends an item and returns its assigned id: the "id" field convention, else the 1-based index as a string.

Parameters:

Returns:

  • (String)


110
111
112
113
114
115
116
117
118
119
120
# File 'lib/capsium/reactor/overlay.rb', line 110

def append_item(dataset, item)
  current = items(dataset)
  explicit = item_id(item)
  if explicit && find_index(current, explicit)
    raise ConflictError, "item id #{explicit} already exists in #{dataset.name}"
  end

  validate_candidate!(dataset, current + [item])
  record_op(dataset.name, "op" => "append", "item" => item)
  explicit || (current.size + 1).to_s
end

#content_rootString

Returns:

  • (String)


54
# File 'lib/capsium/reactor/overlay.rb', line 54

def content_root = File.join(@root, "content")

#data_rootString

Returns:

  • (String)


56
# File 'lib/capsium/reactor/overlay.rb', line 56

def data_root = File.join(@root, "data")

#dataset_data(dataset) ⇒ Object

The dataset as served: base data merged with the mutation log.

Parameters:

Returns:

  • (Object)


86
87
88
89
90
# File 'lib/capsium/reactor/overlay.rb', line 86

def dataset_data(dataset)
  return dataset.data if mutation_log(dataset.name).empty?

  items(dataset)
end

#delete_content(content_relative) ⇒ void

This method returns an undefined value.

Tombstones a content path: it resolves 404 even when a lower layer holds the file (ARCHITECTURE.md section 5a).

Parameters:

  • content_relative (String)


78
79
80
81
82
# File 'lib/capsium/reactor/overlay.rb', line 78

def delete_content(content_relative)
  FileUtils.rm_f(safe_content_path(content_relative))
  @tombstones.add(content_relative)
  persist_tombstones
end

#delete_item(dataset, id) ⇒ nil

Parameters:

Returns:

  • (nil)


136
137
138
139
140
141
142
143
# File 'lib/capsium/reactor/overlay.rb', line 136

def delete_item(dataset, id)
  unless find_index(items(dataset), id)
    raise ItemNotFoundError, "no item #{id} in dataset #{dataset.name}"
  end

  record_op(dataset.name, "op" => "delete", "id" => id)
  nil
end

#item(dataset, id) ⇒ Object

Parameters:

Returns:

  • (Object)

Raises:



100
101
102
103
104
105
106
# File 'lib/capsium/reactor/overlay.rb', line 100

def item(dataset, id)
  current = items(dataset)
  found = find_index(current, id)
  raise ItemNotFoundError, "no item #{id} in dataset #{dataset.name}" unless found

  current[found]
end

#item_id(item) ⇒ String?

The item id convention: an "id" field, else nil (positional).

Parameters:

  • item (Object)

Returns:

  • (String, nil)


146
147
148
# File 'lib/capsium/reactor/overlay.rb', line 146

def item_id(item)
  item["id"].to_s if item.is_a?(Hash) && item.key?("id")
end

#items(dataset) ⇒ Array[untyped]

The merged item collection.

Parameters:

Returns:

  • (Array[untyped])


94
95
96
97
98
# File 'lib/capsium/reactor/overlay.rb', line 94

def items(dataset)
  mutation_log(dataset.name).inject(base_items(dataset)) do |items, record|
    apply_op(items, record)
  end
end

#layerPackage::MergedView::Layer

The overlay as the topmost MergedView layer.



60
61
62
63
64
# File 'lib/capsium/reactor/overlay.rb', line 60

def layer
  Package::MergedView::Layer.new(root: content_root,
                                 visibility: "private",
                                 tombstones: @tombstones)
end

#mutations?(name) ⇒ Boolean

Whether the dataset has recorded mutations.

Parameters:

  • name (String)

Returns:

  • (Boolean)


151
# File 'lib/capsium/reactor/overlay.rb', line 151

def mutations?(name) = !mutation_log(name).empty?

#replace_item(dataset, id, item) ⇒ void

This method returns an undefined value.

Parameters:

Raises:



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/capsium/reactor/overlay.rb', line 122

def replace_item(dataset, id, item)
  current = items(dataset)
  index = find_index(current, id)
  raise ItemNotFoundError, "no item #{id} in dataset #{dataset.name}" unless index

  explicit = item_id(item)
  if explicit && explicit != id
    raise ConflictError, "body id #{explicit} does not match #{id}"
  end

  validate_candidate!(dataset, replaced(current, index, item))
  record_op(dataset.name, "op" => "replace", "id" => id, "item" => item)
end

#write_content(content_relative, body) ⇒ void

This method returns an undefined value.

Writes a content file (create/overwrite), clearing any tombstone for the path so it is served again.

Parameters:

  • content_relative (String)
  • body (String)


68
69
70
71
72
73
74
# File 'lib/capsium/reactor/overlay.rb', line 68

def write_content(content_relative, body)
  path = safe_content_path(content_relative)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, body)
  @tombstones.delete(content_relative)
  persist_tombstones
end