concurrent_pipeline 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,34 +1,72 @@
1
1
  module ConcurrentPipeline
2
2
  module Stores
3
3
  class Schema
4
- STORAGE = {
5
- yaml: Storage::Yaml
6
- }
4
+ Record = Data.define(:name, :table, :block)
5
+ Migration = Data.define(:version, :block)
7
6
 
8
- def build(name, attrs:)
9
- records.fetch(name).new(attrs)
7
+ class RecordContext
8
+ attr_reader :schema_instance, :table_name
9
+
10
+ def initialize(schema_instance)
11
+ @schema_instance = schema_instance
12
+ @table_name = nil
13
+ end
14
+
15
+ def schema(table_name, &block)
16
+ # Store the table name for later use
17
+ @table_name = table_name
18
+
19
+ # Prepend schema migrations to front of the line
20
+ # Wrap the block in a create_table call
21
+ migration_block = Proc.new do
22
+ create_table(table_name, &block)
23
+ end
24
+
25
+ schema_instance.prepend_migration(table_name, &migration_block)
26
+ end
27
+
28
+ def method_missing(...)
29
+ end
30
+
31
+ def respond_to_missing?(...)
32
+ true
33
+ end
34
+ end
35
+
36
+ attr_reader :migrations, :records
37
+ def initialize
38
+ @migrations = []
39
+ @records = {}
40
+ @migration_counter = 1
10
41
  end
11
42
 
12
- def storage(type = nil, **attrs)
13
- @storage = STORAGE.fetch(type).new(**attrs) if type
14
- @storage
43
+ def dir(path = nil)
44
+ @dir = path if path
45
+ @dir
15
46
  end
16
47
 
17
- def record(name, &)
18
- records[name] = Class.new(Record) do
19
- define_singleton_method(:name) { "PipelineRecord.#{name}" }
20
- define_singleton_method(:record_name) { name }
48
+ def migrate(version = @migration_counter += 1, &block)
49
+ migrations << Migration.new(version: version, block: block)
50
+ end
21
51
 
22
- class_exec(&)
52
+ def prepend_migration(version, &block)
53
+ migrations.unshift(Migration.new(version: version, block: block))
54
+ end
23
55
 
24
- define_method(:inspect) do
25
- "#<#{self.class.name} #{attributes.inspect[0..100]}>"
26
- end
56
+ def record(name, table: nil, &block)
57
+ # If block is given, execute it in RecordContext to extract schema calls
58
+ extracted_table = table
59
+ if block
60
+ context = RecordContext.new(self)
61
+ context.instance_exec(&block)
62
+ extracted_table ||= context.table_name
27
63
  end
28
- end
29
64
 
30
- def records
31
- @records ||= {}
65
+ records[name] = Record.new(
66
+ name: name,
67
+ table: extracted_table,
68
+ block: block
69
+ )
32
70
  end
33
71
  end
34
72
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConcurrentPipeline
4
- VERSION = "1.0.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -8,7 +8,7 @@ module ConcurrentPipeline
8
8
  class Error < StandardError; end
9
9
 
10
10
  class << self
11
- def store(&)
11
+ def store(type = :yaml, &)
12
12
  Store.define(&)
13
13
  end
14
14
 
metadata CHANGED
@@ -1,57 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concurrent_pipeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pete Kinnecom
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-13 00:00:00.000000000 Z
11
+ date: 2026-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2.7'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '2.7'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: yaml
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.4'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.4'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: async
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '2.35'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '2.35'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '8.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '8.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.4'
55
83
  description:
56
84
  email:
57
85
  - git@k7u7.com
@@ -66,17 +94,16 @@ files:
66
94
  - concurrency.md
67
95
  - concurrent_pipeline.gemspec
68
96
  - lib/concurrent_pipeline.rb
97
+ - lib/concurrent_pipeline/errors.rb
69
98
  - lib/concurrent_pipeline/pipeline.rb
70
99
  - lib/concurrent_pipeline/pipelines/processors/asynchronous.rb
71
100
  - lib/concurrent_pipeline/pipelines/processors/locker.rb
101
+ - lib/concurrent_pipeline/pipelines/processors/result.rb
72
102
  - lib/concurrent_pipeline/pipelines/processors/synchronous.rb
73
103
  - lib/concurrent_pipeline/pipelines/schema.rb
74
104
  - lib/concurrent_pipeline/shell.rb
75
105
  - lib/concurrent_pipeline/store.rb
76
106
  - lib/concurrent_pipeline/stores/schema.rb
77
- - lib/concurrent_pipeline/stores/schema/record.rb
78
- - lib/concurrent_pipeline/stores/storage/yaml.rb
79
- - lib/concurrent_pipeline/stores/storage/yaml/fs.rb
80
107
  - lib/concurrent_pipeline/version.rb
81
108
  homepage: https://github.com/petekinnecom/concurrent_pipeline
82
109
  licenses:
@@ -1,47 +0,0 @@
1
- module ConcurrentPipeline
2
- module Stores
3
- class Schema
4
- class Record
5
- class << self
6
- def attribute(name, **options)
7
- attributes << name
8
- attribute_defaults[name] = options[:default] if options.key?(:default)
9
-
10
- define_method(name) do
11
- attributes[name]
12
- end
13
-
14
- define_method("#{name}=") do |value|
15
- @attributes[name] = value
16
- end
17
- end
18
-
19
- def attributes
20
- @attributes ||= []
21
- end
22
-
23
- def attribute_defaults
24
- @attribute_defaults ||= {}
25
- end
26
-
27
- def inherited(mod)
28
- mod.attribute(:id)
29
- end
30
- end
31
-
32
- attr_reader :attributes
33
- def initialize(attributes = {})
34
- # Apply defaults for missing attributes
35
- defaults = self.class.attribute_defaults
36
- @attributes = self.class.attributes.each_with_object({}) do |attr_name, hash|
37
- if attributes.key?(attr_name)
38
- hash[attr_name] = attributes[attr_name]
39
- elsif defaults.key?(attr_name)
40
- hash[attr_name] = defaults[attr_name]
41
- end
42
- end
43
- end
44
- end
45
- end
46
- end
47
- end
@@ -1,140 +0,0 @@
1
- require "yaml"
2
- require "fileutils"
3
-
4
- module ConcurrentPipeline
5
- module Stores
6
- module Storage
7
- class Yaml
8
- class Fs
9
- @@mutex = Mutex.new
10
-
11
- attr_reader :dir
12
-
13
- def initialize(dir:)
14
- @dir = dir
15
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
16
- FileUtils.mkdir_p(versions_dir) unless Dir.exist?(versions_dir)
17
- end
18
-
19
- def read_version(version_number)
20
- @@mutex.synchronize do
21
- if version_number == 0
22
- {}
23
- else
24
- current_ver = unsafe_current_version_number
25
-
26
- # If requesting the current/latest version, read from latest.yml
27
- if version_number == current_ver
28
- if File.exist?(latest_file_path)
29
- data = File.read(latest_file_path).then { YAML.load(_1, aliases: true) || {} }
30
- # Normalize keys: convert record names and ID keys to strings for consistency
31
- data.transform_keys(&:to_s).transform_values do |records|
32
- records.transform_keys(&:to_s)
33
- end
34
- else
35
- {}
36
- end
37
- else
38
- # Reading a historical version from versions/ directory
39
- file_path = version_file_path(version_number)
40
- if File.exist?(file_path)
41
- data = File.read(file_path).then { YAML.load(_1, aliases: true) || {} }
42
- # Normalize keys: convert record names and ID keys to strings for consistency
43
- data.transform_keys(&:to_s).transform_values do |records|
44
- records.transform_keys(&:to_s)
45
- end
46
- else
47
- {}
48
- end
49
- end
50
- end
51
- end
52
- end
53
-
54
- def write_version(version_number, data)
55
- @@mutex.synchronize do
56
- # Copy current latest.yml to versions directory if it exists
57
- if File.exist?(latest_file_path)
58
- current_version = unsafe_current_version_number
59
- if current_version > 0
60
- version_path = version_file_path(current_version)
61
- FileUtils.cp(latest_file_path, version_path)
62
- end
63
- end
64
-
65
- # Write new data to latest.yml
66
- File.write(latest_file_path, YAML.dump(data))
67
- end
68
- end
69
-
70
- def current_version_number
71
- @@mutex.synchronize do
72
- unsafe_current_version_number
73
- end
74
- end
75
-
76
- def version_files
77
- @@mutex.synchronize do
78
- unsafe_version_files
79
- end
80
- end
81
-
82
- def delete_version(version_number)
83
- @@mutex.synchronize do
84
- current_ver = unsafe_current_version_number
85
-
86
- # If deleting the latest version
87
- if version_number == current_ver
88
- File.delete(latest_file_path) if File.exist?(latest_file_path)
89
- else
90
- # Deleting an archived version
91
- file_path = version_file_path(version_number)
92
- File.delete(file_path) if File.exist?(file_path)
93
- end
94
- end
95
- end
96
-
97
- def restore_version(version_number)
98
- @@mutex.synchronize do
99
- version_path = version_file_path(version_number)
100
-
101
- if File.exist?(version_path)
102
- # Copy the version file to latest.yml
103
- FileUtils.cp(version_path, latest_file_path)
104
- else
105
- raise "Version #{version_number} does not exist"
106
- end
107
- end
108
- end
109
-
110
- private
111
-
112
- def unsafe_current_version_number
113
- if File.exist?(latest_file_path)
114
- # Count existing version files + 1 for the latest
115
- unsafe_version_files.length + 1
116
- else
117
- 0
118
- end
119
- end
120
-
121
- def unsafe_version_files
122
- Dir.glob(File.join(versions_dir, "*.yml")).sort
123
- end
124
-
125
- def version_file_path(version_num)
126
- File.join(versions_dir, "%04d.yml" % version_num)
127
- end
128
-
129
- def latest_file_path
130
- File.join(dir, "data.yml")
131
- end
132
-
133
- def versions_dir
134
- File.join(dir, "versions")
135
- end
136
- end
137
- end
138
- end
139
- end
140
- end
@@ -1,196 +0,0 @@
1
- module ConcurrentPipeline
2
- module Stores
3
- module Storage
4
- class Yaml
5
- attr_reader :fs, :version_number
6
-
7
- def initialize(dir:, version_number: nil)
8
- @fs = Fs.new(dir: dir)
9
- @version_number = version_number
10
- end
11
-
12
- def in_transaction?
13
- !transaction_operations.nil?
14
- end
15
-
16
- def transaction(&block)
17
- begin_transaction
18
- begin
19
- yield
20
- commit_transaction
21
- rescue => e
22
- rollback_transaction
23
- raise e
24
- end
25
- end
26
-
27
- def create(name:, attrs:)
28
- in_txn = in_transaction?
29
-
30
- raise "Cannot write to non-current version" unless writeable?
31
-
32
- id = attrs[:id] || attrs["id"]
33
- raise "Record must have an id" unless id
34
-
35
- # Always buffer the operation
36
- buffer_operation(
37
- type: :create,
38
- name: name.to_s,
39
- id: id.to_s,
40
- attrs: attrs.transform_keys(&:to_s)
41
- )
42
-
43
- # Flush immediately if not in a transaction
44
- flush_buffer unless in_txn
45
- end
46
-
47
- def update(name:, id:, attrs:)
48
- in_txn = in_transaction?
49
-
50
- raise "Cannot write to non-current version" unless writeable?
51
-
52
- # Always buffer the operation
53
- buffer_operation(
54
- type: :update,
55
- name: name.to_s,
56
- id: id.to_s,
57
- attrs: attrs.transform_keys(&:to_s)
58
- )
59
-
60
- # Flush immediately if not in a transaction
61
- flush_buffer unless in_txn
62
- end
63
-
64
- def all(name:)
65
- data = load_data
66
- records = data[name.to_s] || {}
67
- records.values.map { |attrs| attrs.transform_keys(&:to_sym) }
68
- end
69
-
70
- def versions
71
- current_ver = current_version_number
72
- (1..current_ver).map { |idx| self.class.new(dir: fs.dir, version_number: idx) }
73
- end
74
-
75
- def restore
76
- current_version = version_number || current_version_number
77
-
78
- # Delete all versions after this one
79
- fs.version_files.each_with_index do |file, idx|
80
- version_num = idx + 1
81
- if version_num > current_version
82
- fs.delete_version(version_num)
83
- end
84
- end
85
-
86
- # If restoring to a historical version (not current), move it to latest.yml
87
- if version_number && version_number < current_version_number
88
- fs.restore_version(version_number)
89
- end
90
-
91
- # Return a new writeable storage at this version
92
- self.class.new(dir: fs.dir)
93
- end
94
-
95
- def writeable?
96
- version_number.nil? || version_number == current_version_number
97
- end
98
-
99
- private
100
-
101
- def begin_transaction
102
- raise "Transaction already in progress" if transaction_operations
103
-
104
- self.transaction_operations = []
105
- end
106
-
107
- def commit_transaction
108
- raise "No transaction in progress" unless transaction_operations
109
-
110
- flush_buffer
111
- end
112
-
113
- def rollback_transaction
114
- self.transaction_operations = nil
115
- end
116
-
117
- TRANSACTION_KEY = :yaml_storage_transaction_operations
118
-
119
- def transaction_operations
120
- Fiber[TRANSACTION_KEY]
121
- end
122
-
123
- def transaction_operations=(value)
124
- Fiber[TRANSACTION_KEY] = value
125
- end
126
-
127
- def buffer_operation(op)
128
- # If in a transaction, append to the transaction buffer
129
- if transaction_operations
130
- transaction_operations << op
131
- else
132
- # If not in a transaction, initialize a temporary buffer
133
- self.transaction_operations = [op]
134
- end
135
- end
136
-
137
- def flush_buffer
138
- return unless transaction_operations
139
-
140
- # Load current data and apply all buffered operations
141
- data = load_current_data
142
- transaction_operations.each do |op|
143
- apply_operation(data, op)
144
- end
145
-
146
- write_new_version(data)
147
- self.transaction_operations = nil
148
- end
149
-
150
- def apply_operation(data, op)
151
- case op[:type]
152
- when :create
153
- data[op[:name]] ||= {}
154
- data[op[:name]][op[:id]] = op[:attrs]
155
- when :update
156
- records = data[op[:name]] || {}
157
- if records[op[:id]]
158
- records[op[:id]].merge!(op[:attrs])
159
- else
160
- raise "Record not found: #{op[:name]} with id #{op[:id].inspect}"
161
- end
162
- end
163
- end
164
-
165
- def load_current_data
166
- load_data
167
- end
168
-
169
- def load_data
170
- if version_number
171
- # Reading a historical version from the versions/ directory
172
- target_version = version_number
173
- fs.read_version(target_version)
174
- else
175
- # Reading the current latest.yml
176
- current_ver = current_version_number
177
- if current_ver == 0
178
- {}
179
- else
180
- fs.read_version(current_ver)
181
- end
182
- end
183
- end
184
-
185
- def write_new_version(data)
186
- next_version = current_version_number + 1
187
- fs.write_version(next_version, data)
188
- end
189
-
190
- def current_version_number
191
- fs.current_version_number
192
- end
193
- end
194
- end
195
- end
196
- end