statelydb 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/api/db/continue_list_pb.rb +18 -0
- data/lib/api/db/delete_pb.rb +20 -0
- data/lib/api/db/get_pb.rb +21 -0
- data/lib/api/db/item_pb.rb +19 -0
- data/lib/api/db/item_property_pb.rb +17 -0
- data/lib/api/db/list_pb.rb +25 -0
- data/lib/api/db/list_token_pb.rb +17 -0
- data/lib/api/db/put_pb.rb +21 -0
- data/lib/api/db/scan_root_paths_pb.rb +19 -0
- data/lib/api/db/service_pb.rb +25 -0
- data/lib/api/db/service_services_pb.rb +101 -0
- data/lib/api/db/sync_list_pb.rb +24 -0
- data/lib/api/db/transaction_pb.rb +38 -0
- data/lib/api/errors/error_details_pb.rb +17 -0
- data/lib/common/auth/auth0_token_provider.rb +124 -0
- data/lib/common/auth/interceptor.rb +82 -0
- data/lib/common/auth/token_provider.rb +18 -0
- data/lib/common/error_interceptor.rb +39 -0
- data/lib/common/net/conn.rb +26 -0
- data/lib/error.rb +62 -0
- data/lib/key_path.rb +67 -0
- data/lib/statelydb.rb +333 -0
- data/lib/token.rb +34 -0
- data/lib/transaction/queue.rb +44 -0
- data/lib/transaction/transaction.rb +398 -0
- data/lib/uuid.rb +68 -0
- metadata +111 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "error"
|
4
|
+
require "grpc"
|
5
|
+
|
6
|
+
module StatelyDB
|
7
|
+
module Common
|
8
|
+
# GRPC interceptor to convert errors to StatelyDB::Error
|
9
|
+
class ErrorInterceptor < GRPC::ClientInterceptor
|
10
|
+
# client unary interceptor
|
11
|
+
def request_response(request:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument
|
12
|
+
yield
|
13
|
+
rescue Exception => e
|
14
|
+
raise StatelyDB::Error.from(e)
|
15
|
+
end
|
16
|
+
|
17
|
+
# client streaming interceptor
|
18
|
+
def client_streamer(requests:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument
|
19
|
+
yield
|
20
|
+
rescue Exception => e
|
21
|
+
raise StatelyDB::Error.from(e)
|
22
|
+
end
|
23
|
+
|
24
|
+
# server streaming interceptor
|
25
|
+
def server_streamer(request:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument
|
26
|
+
yield
|
27
|
+
rescue Exception => e
|
28
|
+
raise StatelyDB::Error.from(e)
|
29
|
+
end
|
30
|
+
|
31
|
+
# bidirectional streaming interceptor
|
32
|
+
def bidi_streamer(requests:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument
|
33
|
+
yield
|
34
|
+
rescue Exception => e
|
35
|
+
raise StatelyDB::Error.from(e)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "grpc"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module StatelyDB
|
7
|
+
module Common
|
8
|
+
# A module for Stately Cloud networking code
|
9
|
+
module Net
|
10
|
+
# Create a new gRPC channel
|
11
|
+
# @param [String] endpoint The endpoint to connect to
|
12
|
+
# @return [GRPC::Core::Channel] The new channel
|
13
|
+
def self.new_channel(endpoint: "https://api.stately.cloud")
|
14
|
+
endpoint_uri = URI(endpoint)
|
15
|
+
creds = GRPC::Core::ChannelCredentials.new
|
16
|
+
call_creds = GRPC::Core::CallCredentials.new(proc {})
|
17
|
+
creds = if endpoint_uri.scheme == "http"
|
18
|
+
:this_channel_is_insecure
|
19
|
+
else
|
20
|
+
creds.compose(call_creds)
|
21
|
+
end
|
22
|
+
GRPC::Core::Channel.new(endpoint_uri.authority, {}, creds)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/error.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add the pb dir to the LOAD_PATH because generated proto imports are not relative and
|
4
|
+
# we don't want the protos polluting the main namespace.
|
5
|
+
# Tracking here: https://github.com/grpc/grpc/issues/6164
|
6
|
+
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/api"
|
7
|
+
|
8
|
+
require "api/errors/error_details_pb"
|
9
|
+
|
10
|
+
module StatelyDB
|
11
|
+
# The Error class contains common StatelyDB error types.
|
12
|
+
class Error < StandardError
|
13
|
+
# The gRPC/Connect Code for this error.
|
14
|
+
attr_reader :code
|
15
|
+
# The more fine-grained Stately error code, which is a human-readable string.
|
16
|
+
attr_reader :stately_code
|
17
|
+
# The upstream cause of the error, if available.
|
18
|
+
attr_reader :cause
|
19
|
+
|
20
|
+
# @param [String] message
|
21
|
+
# @param [Integer] code
|
22
|
+
# @param [String] stately_code
|
23
|
+
# @param [Exception] cause
|
24
|
+
def initialize(message, code: nil, stately_code: nil, cause: nil)
|
25
|
+
# Turn a gRPC status code into a human-readable string. e.g. 3 -> "InvalidArgument"
|
26
|
+
code_str = if code > 0
|
27
|
+
GRPC::Core::StatusCodes.constants.find do |c|
|
28
|
+
GRPC::Core::StatusCodes.const_get(c) === code
|
29
|
+
end.to_s.split("_").collect(&:capitalize).join
|
30
|
+
else
|
31
|
+
"Unknown"
|
32
|
+
end
|
33
|
+
|
34
|
+
super("(#{code_str}/#{stately_code}): #{message}")
|
35
|
+
@code = code
|
36
|
+
@stately_code = stately_code
|
37
|
+
@cause = cause
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert any exception into a StatelyDB::Error.
|
41
|
+
# @param [Exception] error
|
42
|
+
# @return [StatelyDB::Error]
|
43
|
+
def self.from(error)
|
44
|
+
return error if error.is_a?(StatelyDB::Error)
|
45
|
+
|
46
|
+
if error.is_a?(GRPC::BadStatus)
|
47
|
+
status = error.to_rpc_status
|
48
|
+
|
49
|
+
unless status.nil? || status.details.empty?
|
50
|
+
raw_detail = status.details[0]
|
51
|
+
if raw_detail.type_url == "type.googleapis.com/stately.errors.StatelyErrorDetails"
|
52
|
+
error_details = Stately::Errors::StatelyErrorDetails.decode(raw_detail.value)
|
53
|
+
return new(error_details.message, code: error.code, stately_code: error_details.stately_code,
|
54
|
+
cause: error_details.upstream_cause)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
new(error.message, code: GRPC::Codes::StatusCodes::Unknown, stately_code: "Unknown", cause: error)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/key_path.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StatelyDB
|
4
|
+
# KeyPath is a helper class for constructing key paths.
|
5
|
+
class KeyPath
|
6
|
+
def initialize
|
7
|
+
super
|
8
|
+
@path = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Appends a new path segment.
|
12
|
+
# @param [String] namespace
|
13
|
+
# @param [String] identifier
|
14
|
+
# @return [KeyPath]
|
15
|
+
def with(namespace, identifier = nil)
|
16
|
+
if identifier.nil?
|
17
|
+
@path << namespace
|
18
|
+
return self
|
19
|
+
end
|
20
|
+
@path << "#{namespace}-#{self.class.to_key_id(identifier)}"
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String]
|
25
|
+
def to_str
|
26
|
+
"/".dup.concat(@path.join("/"))
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
def inspect
|
31
|
+
to_str
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String]
|
35
|
+
def to_s
|
36
|
+
to_str
|
37
|
+
end
|
38
|
+
|
39
|
+
# Appends a new path segment.
|
40
|
+
# @param [String] namespace
|
41
|
+
# @param [String] identifier
|
42
|
+
# @return [KeyPath]
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# keypath = KeyPath.with("genres", "rock").with("artists", "the-beatles")
|
46
|
+
def self.with(namespace, identifier = nil)
|
47
|
+
new.with(namespace, identifier)
|
48
|
+
end
|
49
|
+
|
50
|
+
# If the value is a binary string, encode it as a url-safe base64 string with padding removed.
|
51
|
+
# Note that we also prepend the value with the ~ sigil to indicate that it is a base64 string.
|
52
|
+
#
|
53
|
+
# @param [String, StatelyDB::UUID, #to_s] value The value to convert to a key id.
|
54
|
+
# @return [String]
|
55
|
+
def self.to_key_id(value)
|
56
|
+
if value.is_a?(StatelyDB::UUID)
|
57
|
+
"~#{value.to_base64}"
|
58
|
+
elsif value.is_a?(String) && value.encoding == Encoding::BINARY
|
59
|
+
b64_value = [value].pack("m0").tr("=", "").tr("+/", "-_")
|
60
|
+
"~#{b64_value}"
|
61
|
+
else
|
62
|
+
# Any other value is just converted to a string
|
63
|
+
value.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/statelydb.rb
ADDED
@@ -0,0 +1,333 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add the pb dir to the LOAD_PATH because generated proto imports are not relative and
|
4
|
+
# we don't want the protos polluting the main namespace.
|
5
|
+
# Tracking here: https://github.com/grpc/grpc/issues/6164
|
6
|
+
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/api"
|
7
|
+
|
8
|
+
require "api/db/service_services_pb"
|
9
|
+
require "common/auth/auth0_token_provider"
|
10
|
+
require "common/auth/interceptor"
|
11
|
+
require "common/net/conn"
|
12
|
+
require "common/error_interceptor"
|
13
|
+
require "grpc"
|
14
|
+
require "json"
|
15
|
+
require "net/http"
|
16
|
+
|
17
|
+
require "transaction/transaction"
|
18
|
+
require "transaction/queue"
|
19
|
+
require "error"
|
20
|
+
require "key_path"
|
21
|
+
require "token"
|
22
|
+
require "uuid"
|
23
|
+
|
24
|
+
module StatelyDB
|
25
|
+
# Client is a client for interacting with the Stately Cloud API.
|
26
|
+
class Client
|
27
|
+
# Initialize a new StatelyDB Client
|
28
|
+
#
|
29
|
+
# @param store_id [Integer] the StatelyDB to use for all operations with this client.
|
30
|
+
# @param schema [Module] the schema module to use for mapping StatelyDB Items.
|
31
|
+
# @param token_provider [Common::Auth::TokenProvider] the token provider to use for authentication.
|
32
|
+
# @param channel [GRPC::Core::Channel] the gRPC channel to use for communication.
|
33
|
+
def initialize(store_id: nil,
|
34
|
+
schema: StatelyDB::Types,
|
35
|
+
token_provider: Common::Auth::Auth0TokenProvider.new,
|
36
|
+
channel: Common::Net.new_channel)
|
37
|
+
raise "store_id is required" if store_id.nil?
|
38
|
+
raise "schema is required" if schema.nil?
|
39
|
+
|
40
|
+
auth_interceptor = Common::Auth::Interceptor.new(token_provider:)
|
41
|
+
error_interceptor = Common::ErrorInterceptor.new
|
42
|
+
|
43
|
+
@stub = Stately::Db::DatabaseService::Stub.new(nil, nil, channel_override: channel,
|
44
|
+
interceptors: [error_interceptor, auth_interceptor])
|
45
|
+
@store_id = store_id.to_i
|
46
|
+
@schema = schema
|
47
|
+
@allow_stale = false
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set whether to allow stale results for all operations with this client. This produces a new client
|
51
|
+
# with the allow_stale flag set.
|
52
|
+
# @param allow_stale [Boolean] whether to allow stale results
|
53
|
+
# @return [StatelyDB::Client] a new client with the allow_stale flag set
|
54
|
+
# @example
|
55
|
+
# client.with_allow_stale(true).get("/ItemType-identifier")
|
56
|
+
def with_allow_stale(allow_stale)
|
57
|
+
new_client = clone
|
58
|
+
new_client.instance_variable_set(:@allow_stale, allow_stale)
|
59
|
+
new_client
|
60
|
+
end
|
61
|
+
|
62
|
+
# Fetch a single Item from a StatelyDB Store at the given key_path.
|
63
|
+
#
|
64
|
+
# @param key_path [String] the path to the item
|
65
|
+
# @return [StatelyDB::Item, NilClass] the Item or nil if not found
|
66
|
+
# @raise [StatelyDB::Error] if the parameters are invalid or if the item is not found
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# client.get("/ItemType-identifier")
|
70
|
+
def get(key_path)
|
71
|
+
resp = get_batch(key_path)
|
72
|
+
|
73
|
+
# Always return a single Item.
|
74
|
+
resp.first
|
75
|
+
end
|
76
|
+
|
77
|
+
# Fetch a batch of Items from a StatelyDB Store at the given key_paths.
|
78
|
+
#
|
79
|
+
# @param key_paths [String, Array<String>] the paths to the items
|
80
|
+
# @return [Array<StatelyDB::Item>, NilClass] the items or nil if not found
|
81
|
+
# @raise [StatelyDB::Error] if the parameters are invalid or if the item is not found
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# client.data.get_batch("/ItemType-identifier", "/ItemType-identifier2")
|
85
|
+
def get_batch(*key_paths)
|
86
|
+
key_paths = Array(key_paths).flatten
|
87
|
+
req = Stately::Db::GetRequest.new(
|
88
|
+
store_id: @store_id,
|
89
|
+
gets:
|
90
|
+
key_paths.map { |key_path| Stately::Db::GetItem.new(key_path: String(key_path)) },
|
91
|
+
allow_stale: @allow_stale
|
92
|
+
)
|
93
|
+
|
94
|
+
resp = @stub.get(req)
|
95
|
+
resp.items.map do |result|
|
96
|
+
@schema.unmarshal_item(stately_item: result)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Begin listing Items from a StatelyDB Store at the given prefix.
|
101
|
+
#
|
102
|
+
# @param prefix [String] the prefix to list
|
103
|
+
# @param limit [Integer] the maximum number of items to return
|
104
|
+
# @param sort_property [String] the property to sort by
|
105
|
+
# @param sort_direction [Symbol] the direction to sort by (:ascending or :descending)
|
106
|
+
# @return [Array<StatelyDB::Item>, StatelyDB::Token] the list of Items and the token
|
107
|
+
#
|
108
|
+
# @example
|
109
|
+
# client.data.begin_list("/ItemType-identifier", limit: 10, sort_direction: :ascending)
|
110
|
+
def begin_list(prefix,
|
111
|
+
limit: 100,
|
112
|
+
sort_property: nil,
|
113
|
+
sort_direction: :ascending)
|
114
|
+
sort_direction = sort_direction == :ascending ? 0 : 1
|
115
|
+
|
116
|
+
req = Stately::Db::BeginListRequest.new(
|
117
|
+
store_id: @store_id,
|
118
|
+
key_path_prefix: String(prefix),
|
119
|
+
limit:,
|
120
|
+
sort_property:,
|
121
|
+
sort_direction:,
|
122
|
+
allow_stale: @allow_stale
|
123
|
+
)
|
124
|
+
resp = @stub.begin_list(req)
|
125
|
+
process_list_response(resp)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Continue listing Items from a StatelyDB Store using a token.
|
129
|
+
#
|
130
|
+
# @param token [StatelyDB::Token] the token to continue from
|
131
|
+
# @return [Array<StatelyDB::Item>, StatelyDB::Token] the list of Items and the token
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
# (items, token) = client.data.begin_list("/ItemType-identifier")
|
135
|
+
# client.data.continue_list(token)
|
136
|
+
def continue_list(token)
|
137
|
+
req = Stately::Db::ContinueListRequest.new(
|
138
|
+
token_data: token.token_data
|
139
|
+
)
|
140
|
+
resp = @stub.continue_list(req)
|
141
|
+
process_list_response(resp)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Sync a list of Items from a StatelyDB Store.
|
145
|
+
#
|
146
|
+
# @param token [StatelyDB::Token] the token to sync from
|
147
|
+
# @return [StatelyDB::SyncResult] the result of the sync operation
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# (items, token) = client.data.begin_list("/ItemType-identifier")
|
151
|
+
# client.data.sync_list(token)
|
152
|
+
def sync_list(token)
|
153
|
+
req = Stately::Db::SyncListRequest.new(
|
154
|
+
token_data: token.token_data
|
155
|
+
)
|
156
|
+
resp = @stub.sync_list(req)
|
157
|
+
process_sync_response(resp)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Put an Item into a StatelyDB Store at the given key_path.
|
161
|
+
#
|
162
|
+
# @param item [StatelyDB::Item] a StatelyDB Item
|
163
|
+
# @return [StatelyDB::Item] the item that was stored
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# client.data.put(my_item)
|
167
|
+
def put(item)
|
168
|
+
resp = put_batch(item)
|
169
|
+
|
170
|
+
# Always return a single Item.
|
171
|
+
resp.first
|
172
|
+
end
|
173
|
+
|
174
|
+
# Put a batch of Items into a StatelyDB Store.
|
175
|
+
#
|
176
|
+
# @param items [StatelyDB::Item, Array<StatelyDB::Item>] the items to store
|
177
|
+
# @return [Array<StatelyDB::Item>] the items that were stored
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# client.data.put_batch(item1, item2)
|
181
|
+
def put_batch(*items)
|
182
|
+
items = Array(items).flatten
|
183
|
+
req = Stately::Db::PutRequest.new(
|
184
|
+
store_id: @store_id,
|
185
|
+
puts: items.map do |item|
|
186
|
+
Stately::Db::PutItem.new(
|
187
|
+
item: item.send("marshal_stately")
|
188
|
+
)
|
189
|
+
end
|
190
|
+
)
|
191
|
+
resp = @stub.put(req)
|
192
|
+
|
193
|
+
resp.items.map do |result|
|
194
|
+
@schema.unmarshal_item(stately_item: result)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Delete one or more Items from a StatelyDB Store at the given key_paths.
|
199
|
+
#
|
200
|
+
# @param key_paths [String, Array<String>] the paths to the items
|
201
|
+
# @raise [StatelyDB::Error::InvalidParameters] if the parameters are invalid
|
202
|
+
# @raise [StatelyDB::Error::NotFound] if the item is not found
|
203
|
+
# @return [void] nil
|
204
|
+
#
|
205
|
+
# @example
|
206
|
+
# client.data.delete("/ItemType-identifier", "/ItemType-identifier2")
|
207
|
+
def delete(*key_paths)
|
208
|
+
key_paths = Array(key_paths).flatten
|
209
|
+
req = Stately::Db::DeleteRequest.new(
|
210
|
+
store_id: @store_id,
|
211
|
+
deletes: key_paths.map { |key_path| Stately::Db::DeleteItem.new(key_path: String(key_path)) }
|
212
|
+
)
|
213
|
+
@stub.delete(req)
|
214
|
+
nil
|
215
|
+
end
|
216
|
+
|
217
|
+
# Transaction takes a block and executes the block within a transaction.
|
218
|
+
# If the block raises an exception, the transaction is rolled back.
|
219
|
+
# If the block completes successfully, the transaction is committed.
|
220
|
+
#
|
221
|
+
# @return [StatelyDB::Transaction::Transaction::Result] the result of the transaction
|
222
|
+
# @raise [StatelyDB::Error::InvalidParameters] if the parameters are invalid
|
223
|
+
# @raise [StatelyDB::Error::NotFound] if the item is not found
|
224
|
+
# @raise [Exception] if any other exception is raised
|
225
|
+
#
|
226
|
+
# @example
|
227
|
+
# client.data.transaction do |txn|
|
228
|
+
# txn.put(item: my_item)
|
229
|
+
# txn.put(item: another_item)
|
230
|
+
# end
|
231
|
+
def transaction
|
232
|
+
txn = StatelyDB::Transaction::Transaction.new(stub: @stub, store_id: @store_id, schema: @schema)
|
233
|
+
txn.begin
|
234
|
+
yield txn
|
235
|
+
txn.commit
|
236
|
+
rescue StatelyDB::Error
|
237
|
+
raise
|
238
|
+
# Handle any other exceptions and abort the transaction. We're rescuing Exception here
|
239
|
+
# because we want to catch all exceptions, including those that don't inherit from StandardError.
|
240
|
+
rescue Exception => e
|
241
|
+
txn.abort
|
242
|
+
|
243
|
+
# Calling raise with no parameters re-raises the original exception
|
244
|
+
raise StatelyDB::Error.from(e)
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
# Process a list response from begin_list or continue_list
|
250
|
+
#
|
251
|
+
# @param resp [Stately::Db::ListResponse] the response to process
|
252
|
+
# @return [(Array<StatelyDB::Item>, StatelyDB::Token)] the list of Items and the token
|
253
|
+
# @api private
|
254
|
+
# @!visibility private
|
255
|
+
def process_list_response(resp)
|
256
|
+
items = []
|
257
|
+
token = nil
|
258
|
+
resp.each do |r|
|
259
|
+
case r.response
|
260
|
+
when :result
|
261
|
+
r.result.items.map do |result|
|
262
|
+
items << @schema.unmarshal_item(stately_item: result)
|
263
|
+
end
|
264
|
+
when :finished
|
265
|
+
raw_token = r.finished.token
|
266
|
+
token = StatelyDB::Token.new(token_data: raw_token.token_data,
|
267
|
+
can_continue: raw_token.can_continue,
|
268
|
+
can_sync: raw_token.can_sync)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
[items, token]
|
272
|
+
end
|
273
|
+
|
274
|
+
# Process a sync response from sync_list
|
275
|
+
#
|
276
|
+
# @param resp [Stately::Db::SyncResponse] the response to process
|
277
|
+
# @return [StatelyDB::SyncResult] the result of the sync operation
|
278
|
+
# @api private
|
279
|
+
# @!visibility private
|
280
|
+
def process_sync_response(resp)
|
281
|
+
changed_items = []
|
282
|
+
deleted_item_paths = []
|
283
|
+
token = nil
|
284
|
+
is_reset = false
|
285
|
+
resp.each do |r|
|
286
|
+
case r.response
|
287
|
+
when :result
|
288
|
+
r.result.changed_items.each do |item|
|
289
|
+
changed_items << @schema.unmarshal_item(stately_item: item)
|
290
|
+
end
|
291
|
+
r.result.deleted_items.each do |item|
|
292
|
+
deleted_item_paths << item.key_path
|
293
|
+
end
|
294
|
+
when :reset
|
295
|
+
is_reset = true
|
296
|
+
when :finished
|
297
|
+
raw_token = r.finished.token
|
298
|
+
token = StatelyDB::Token.new(token_data: raw_token.token_data,
|
299
|
+
can_continue: raw_token.can_continue,
|
300
|
+
can_sync: raw_token.can_sync)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
SyncResult.new(changed_items:, deleted_item_paths:, is_reset:, token:)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# SyncResult represents the results of a sync operation.
|
308
|
+
#
|
309
|
+
# @attr_reader changed_items [Array<StatelyDB::Item>] the items that were changed
|
310
|
+
# @attr_reader deleted_item_paths [Array<String>] the key paths that were deleted
|
311
|
+
# @attr_reader is_reset [Boolean] whether the sync operation reset the token
|
312
|
+
# @attr_reader token [StatelyDB::Token] the token to continue from
|
313
|
+
class SyncResult
|
314
|
+
attr_reader :changed_items, :deleted_item_paths, :is_reset, :token
|
315
|
+
|
316
|
+
# @param changed_items [Array<StatelyDB::Item>] the items that were changed
|
317
|
+
# @param deleted_item_paths [Array<String>] the key paths that were deleted
|
318
|
+
# @param is_reset [Boolean] whether the sync operation reset the token
|
319
|
+
# @param token [StatelyDB::Token] the token to continue from
|
320
|
+
def initialize(changed_items:, deleted_item_paths:, is_reset:, token:)
|
321
|
+
@changed_items = changed_items
|
322
|
+
@deleted_item_paths = deleted_item_paths
|
323
|
+
@is_reset = is_reset
|
324
|
+
@token = token
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# StatelyDB::Item is a base class for all StatelyDB Items. This class is provided in documentation
|
329
|
+
# to show the expected interface for a StatelyDB Item, but in practice the SDK will return a subclass
|
330
|
+
# of this class that is generated from the schema.
|
331
|
+
class Item < Object
|
332
|
+
end
|
333
|
+
end
|
data/lib/token.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StatelyDB
|
4
|
+
# The Token type contains a continuation token for list and sync operations along with metadata about the ability
|
5
|
+
# to sync or continue listing based on the last operation performed.
|
6
|
+
#
|
7
|
+
# Ths StatelyDB SDK vends this Token type for list and sync operations. Consumers should not need to construct this
|
8
|
+
# type directly.
|
9
|
+
class Token
|
10
|
+
# @!visibility private
|
11
|
+
attr_accessor :token_data
|
12
|
+
|
13
|
+
# @param [String] token_data
|
14
|
+
# @param [Boolean] can_continue
|
15
|
+
# @param [Boolean] can_sync
|
16
|
+
def initialize(token_data:, can_continue:, can_sync:)
|
17
|
+
@token_data = token_data
|
18
|
+
@can_continue = can_continue
|
19
|
+
@can_sync = can_sync
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if the list operation can be continued, otherwise false.
|
23
|
+
# @return [Boolean]
|
24
|
+
def can_continue?
|
25
|
+
!!@can_continue
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns true if the sync operation can be continued, otherwise false.
|
29
|
+
# @return [Boolean]
|
30
|
+
def can_sync?
|
31
|
+
!!@can_sync
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StatelyDB
|
4
|
+
module Transaction
|
5
|
+
# TransactionQueue is a wrapper around Thread::Queue that implements Enumerable
|
6
|
+
class Queue < Thread::Queue
|
7
|
+
# @!attribute [r] last_message_id
|
8
|
+
# @return [Integer, nil] The ID of the last message, or nil if there is no message.
|
9
|
+
attr_reader :last_message_id
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
@last_message_id = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# next_message_id returns the next message ID, which is the current size of the queue + 1.
|
17
|
+
# This value is consumed by the StatelyDB transaction as a monotonically increasing MessageID.
|
18
|
+
# @return [Integer]
|
19
|
+
def next_message_id
|
20
|
+
@last_message_id += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
# Iterates over each element in the queue, yielding each element to the given block.
|
24
|
+
#
|
25
|
+
# @yield [Object] Gives each element in the queue to the block.
|
26
|
+
# @return [void]
|
27
|
+
def each
|
28
|
+
loop do
|
29
|
+
yield pop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Iterates over each item in the queue, yielding each item to the given block.
|
34
|
+
#
|
35
|
+
# @yield [Object] Gives each item in the queue to the block.
|
36
|
+
# @return [void]
|
37
|
+
def each_item
|
38
|
+
loop do
|
39
|
+
yield pop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|