zero_ruby 0.1.0.alpha5 → 0.1.0.alpha6
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/zero_ruby/lmid_store.rb +20 -0
- data/lib/zero_ruby/lmid_stores/active_record_store.rb +47 -0
- data/lib/zero_ruby/push_processor.rb +52 -9
- data/lib/zero_ruby/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3eab5f6867bccc496d6bb976fd8d6494c05888f085b53c7c92a62ff6cfcf1a8
|
|
4
|
+
data.tar.gz: dc879e22f7adf86659842f72763ef5ce4fea593a446116f185d4e5c9a72636c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3c49d57a02dafcd912302ac4205a1a168a52711c6cf33f0ec078867b244ce1f0afca16887bf2da6a65a911efdd3ecbd11ccef526e614dc03062a209ded8e847
|
|
7
|
+
data.tar.gz: 44cfd5b1bc95f58f12de4b24803bdb7f1cf4dc38d85470553c86d5e6f79c4c3271072b83a774ace776282a16982ba068554211c18fb21d68e24c8415ba2166fa
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# zero_ruby
|
|
2
2
|
|
|
3
|
-
A Ruby gem for handling [Zero](https://zero.rocicorp.dev/) mutations with type safety, validation, and full protocol support.
|
|
3
|
+
A Ruby gem for handling [Zero](https://zero.rocicorp.dev/) mutations with type safety, validation, and full protocol support. Compatible with Zero 0.25.12.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
data/lib/zero_ruby/lmid_store.rb
CHANGED
|
@@ -39,5 +39,25 @@ module ZeroRuby
|
|
|
39
39
|
def transaction(&block)
|
|
40
40
|
raise NotImplementedError, "#{self.class}#transaction must be implemented"
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Persist a mutation result so clients can read it via replication.
|
|
44
|
+
# Used to surface error results back to the client.
|
|
45
|
+
#
|
|
46
|
+
# @param client_group_id [String] The client group ID
|
|
47
|
+
# @param client_id [String] The client ID
|
|
48
|
+
# @param mutation_id [Integer] The mutation ID
|
|
49
|
+
# @param result [Hash, String] The mutation result to persist
|
|
50
|
+
def write_mutation_result(client_group_id, client_id, mutation_id, result)
|
|
51
|
+
raise NotImplementedError, "#{self.class}#write_mutation_result must be implemented"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Delete mutation results, called by _zero_cleanupResults to remove acknowledged results.
|
|
55
|
+
#
|
|
56
|
+
# @param args [Hash] Cleanup arguments from the _zero_cleanupResults mutation.
|
|
57
|
+
# For single/legacy format: { "clientGroupID" => String, "clientID" => String, "upToMutationID" => Integer }
|
|
58
|
+
# For bulk format: { "type" => "bulk", "clientGroupID" => String, "clientIDs" => Array<String> }
|
|
59
|
+
def delete_mutation_results(args)
|
|
60
|
+
raise NotImplementedError, "#{self.class}#delete_mutation_results must be implemented"
|
|
61
|
+
end
|
|
42
62
|
end
|
|
43
63
|
end
|
|
@@ -52,6 +52,53 @@ module ZeroRuby
|
|
|
52
52
|
model_class.transaction(&block)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Write a mutation result to the zero_0.mutations table.
|
|
56
|
+
#
|
|
57
|
+
# @param client_group_id [String] The client group ID
|
|
58
|
+
# @param client_id [String] The client ID
|
|
59
|
+
# @param mutation_id [Integer] The mutation ID
|
|
60
|
+
# @param result [Hash, String] The mutation result. Hashes are serialized to JSON for storage.
|
|
61
|
+
def write_mutation_result(client_group_id, client_id, mutation_id, result)
|
|
62
|
+
result_json = begin
|
|
63
|
+
result.is_a?(String) ? result : result.to_json
|
|
64
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
|
65
|
+
{error: "app", message: "Error result could not be serialized"}.to_json
|
|
66
|
+
end
|
|
67
|
+
sql = model_class.sanitize_sql_array([<<~SQL.squish, {client_group_id:, client_id:, mutation_id:, result: result_json}])
|
|
68
|
+
INSERT INTO zero_0.mutations ("clientGroupID", "clientID", "mutationID", "result")
|
|
69
|
+
VALUES (:client_group_id, :client_id, :mutation_id, :result::text::json)
|
|
70
|
+
SQL
|
|
71
|
+
|
|
72
|
+
model_class.connection.execute(sql)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Delete mutation results from the zero_0.mutations table.
|
|
76
|
+
#
|
|
77
|
+
# @param args [Hash] Cleanup arguments
|
|
78
|
+
def delete_mutation_results(args)
|
|
79
|
+
client_group_id = args["clientGroupID"]
|
|
80
|
+
|
|
81
|
+
sql = if args["type"] == "bulk"
|
|
82
|
+
client_ids = args["clientIDs"]
|
|
83
|
+
model_class.sanitize_sql_array([<<~SQL.squish, {client_group_id:}])
|
|
84
|
+
DELETE FROM zero_0.mutations
|
|
85
|
+
WHERE "clientGroupID" = :client_group_id
|
|
86
|
+
AND "clientID" = ANY(ARRAY[#{client_ids.map { |id| model_class.connection.quote(id) }.join(",")}])
|
|
87
|
+
SQL
|
|
88
|
+
else
|
|
89
|
+
client_id = args["clientID"]
|
|
90
|
+
up_to_mutation_id = args["upToMutationID"]
|
|
91
|
+
model_class.sanitize_sql_array([<<~SQL.squish, {client_group_id:, client_id:, up_to_mutation_id:}])
|
|
92
|
+
DELETE FROM zero_0.mutations
|
|
93
|
+
WHERE "clientGroupID" = :client_group_id
|
|
94
|
+
AND "clientID" = :client_id
|
|
95
|
+
AND "mutationID" <= :up_to_mutation_id
|
|
96
|
+
SQL
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
model_class.connection.execute(sql)
|
|
100
|
+
end
|
|
101
|
+
|
|
55
102
|
private
|
|
56
103
|
|
|
57
104
|
def default_model_class
|
|
@@ -35,6 +35,11 @@ module ZeroRuby
|
|
|
35
35
|
results = []
|
|
36
36
|
|
|
37
37
|
mutations.each_with_index do |mutation_data, index|
|
|
38
|
+
if mutation_data["name"] == "_zero_cleanupResults"
|
|
39
|
+
handle_cleanup_results(mutation_data)
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
|
|
38
43
|
result = process_mutation_with_lmid(mutation_data, client_group_id, context)
|
|
39
44
|
results << result
|
|
40
45
|
rescue OutOfOrderMutationError => e
|
|
@@ -89,7 +94,13 @@ module ZeroRuby
|
|
|
89
94
|
result = lmid_store.transaction do
|
|
90
95
|
last_mutation_id = lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
91
96
|
check_lmid!(client_id, mutation_id, last_mutation_id)
|
|
92
|
-
|
|
97
|
+
begin
|
|
98
|
+
user_block.call
|
|
99
|
+
rescue ZeroRuby::Error
|
|
100
|
+
raise
|
|
101
|
+
rescue => e
|
|
102
|
+
raise ZeroRuby::Error.new(e.message)
|
|
103
|
+
end
|
|
93
104
|
end
|
|
94
105
|
phase = :post_commit
|
|
95
106
|
result
|
|
@@ -107,23 +118,55 @@ module ZeroRuby
|
|
|
107
118
|
# Application errors - advance LMID based on phase, return error response
|
|
108
119
|
# Pre-transaction/transaction: LMID advanced separately
|
|
109
120
|
# Post-commit: LMID already committed with transaction
|
|
121
|
+
error_response = format_error_response(e)
|
|
110
122
|
if phase != :post_commit
|
|
111
|
-
|
|
123
|
+
persist_mutation_failure(client_group_id, client_id, mutation_id, error_response)
|
|
112
124
|
end
|
|
113
|
-
{id: mutation_id_obj, result:
|
|
125
|
+
{id: mutation_id_obj, result: error_response}
|
|
114
126
|
rescue => e
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
if phase == :transaction
|
|
128
|
+
# Infrastructure error (user code errors already wrapped by transact_proc)
|
|
129
|
+
raise TransactionError.new("Transaction failed: #{e.message}")
|
|
130
|
+
else
|
|
131
|
+
# User code error in pre-transaction or post-commit phase
|
|
132
|
+
error_response = {error: "app", message: e.message}
|
|
133
|
+
if phase != :post_commit
|
|
134
|
+
persist_mutation_failure(client_group_id, client_id, mutation_id, error_response)
|
|
135
|
+
end
|
|
136
|
+
{id: mutation_id_obj, result: error_response}
|
|
137
|
+
end
|
|
117
138
|
end
|
|
118
139
|
|
|
119
|
-
# Persist LMID advancement after
|
|
120
|
-
#
|
|
121
|
-
|
|
140
|
+
# Persist LMID advancement and mutation error result after a failure.
|
|
141
|
+
# Advances the LMID so the failed mutation is not re-executed on retry,
|
|
142
|
+
# and writes the error result so clients can read it via replication.
|
|
143
|
+
def persist_mutation_failure(client_group_id, client_id, mutation_id, error_result)
|
|
122
144
|
lmid_store.transaction do
|
|
123
145
|
lmid_store.fetch_and_increment(client_group_id, client_id)
|
|
146
|
+
lmid_store.write_mutation_result(client_group_id, client_id, mutation_id, error_result)
|
|
147
|
+
end
|
|
148
|
+
rescue => e
|
|
149
|
+
warn "[ZeroRuby] Failed to persist mutation failure for " \
|
|
150
|
+
"client_group=#{client_group_id} client=#{client_id} mutation=#{mutation_id}: " \
|
|
151
|
+
"#{e.class}: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Handle _zero_cleanupResults mutations by deleting acknowledged results.
|
|
155
|
+
# Errors are caught and logged as warnings without propagating, matching
|
|
156
|
+
# the Zero protocol behavior where cleanup failures must not abort the push batch.
|
|
157
|
+
def handle_cleanup_results(mutation_data)
|
|
158
|
+
args = mutation_data["args"]
|
|
159
|
+
args = args.first if args.is_a?(Array)
|
|
160
|
+
unless args.is_a?(Hash) && args["clientGroupID"]
|
|
161
|
+
warn "[ZeroRuby] _zero_cleanupResults: invalid args: #{args.inspect}"
|
|
162
|
+
return
|
|
163
|
+
end
|
|
164
|
+
lmid_store.transaction do
|
|
165
|
+
lmid_store.delete_mutation_results(args)
|
|
124
166
|
end
|
|
125
167
|
rescue => e
|
|
126
|
-
warn "
|
|
168
|
+
warn "[ZeroRuby] _zero_cleanupResults failed for " \
|
|
169
|
+
"clientGroupID=#{args&.dig("clientGroupID")}: #{e.class}: #{e.message}"
|
|
127
170
|
end
|
|
128
171
|
|
|
129
172
|
# Validate LMID against the post-increment value.
|
data/lib/zero_ruby/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zero_ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.alpha6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Serban
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-02-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: dry-struct
|