statelydb 0.1.1
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 +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
|