smplkit 3.0.46 → 3.0.48
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/lib/smplkit/_generated/app/lib/smplkit_app_client/api/environments_api.rb +5 -2
- data/lib/smplkit/_generated/app/lib/smplkit_app_client/models/environment.rb +15 -3
- data/lib/smplkit/_generated/app/spec/api/environments_api_spec.rb +2 -1
- data/lib/smplkit/_generated/app/spec/models/environment_spec.rb +6 -0
- data/lib/smplkit/config/client.rb +172 -10
- data/lib/smplkit/management/buffer.rb +72 -0
- data/lib/smplkit/management/client.rb +75 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cb1e0f4a608cd5ade8c399fccc96b62f7d087b51038aa1a36ec74eb22a434bc
|
|
4
|
+
data.tar.gz: b024471ba9cf393fd36d123add01277c8baabb4cd44640905a669c4bc42d68b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86ab6f3765a4f091dc200ae01f8d902158c88c2f1107ba92d55656c67ec95149507b07885143b381bcdb7ca73ac5ed1f7d4dccc6c7b0798ce5b115db5b4d67c7
|
|
7
|
+
data.tar.gz: 6ba7af7b036be3d9dc15f04e564690aec69e179795d485e4514f6c57aa203bab11255522ea30693aa612e74099fc4d6d490bf7da88985701f1d5a95c0a50af19
|
|
@@ -280,10 +280,11 @@ module SmplkitGeneratedClient::App
|
|
|
280
280
|
end
|
|
281
281
|
|
|
282
282
|
# List Environments
|
|
283
|
-
# List all environments for the authenticated account. `filter[search]` does a case-insensitive substring match against the environment `key` and `name`. `filter[classification]` narrows the result to one classification (`STANDARD` or `AD_HOC`).
|
|
283
|
+
# List all environments for the authenticated account. `filter[search]` does a case-insensitive substring match against the environment `key` and `name`. `filter[classification]` narrows the result to one classification (`STANDARD` or `AD_HOC`). `filter[managed]` narrows by managed state (`true` or `false`).
|
|
284
284
|
# @param [Hash] opts the optional parameters
|
|
285
285
|
# @option opts [String] :filter_search Case-insensitive substring match against the environment `key` and `name`. An environment is returned if either field contains the search term.
|
|
286
286
|
# @option opts [String] :filter_classification Narrow the result to environments with the given classification. One of `STANDARD` or `AD_HOC`.
|
|
287
|
+
# @option opts [Boolean] :filter_managed Narrow the result to managed (`true`) or unmanaged (`false`) environments. Omit to return both.
|
|
287
288
|
# @option opts [String] :sort Field to sort by. Prefix with `-` for descending order. Default: `name`. Allowed values: `created_at`, `-created_at`, `key`, `-key`, `name`, `-name`, `updated_at`, `-updated_at`. (default to 'name')
|
|
288
289
|
# @option opts [Integer] :page_number 1-based page number to return. Optional; defaults to `1` when omitted. Must be `>= 1` — requests with a smaller value are rejected with a 400 error. (default to 1)
|
|
289
290
|
# @option opts [Integer] :page_size Number of items per page. Optional; defaults to `1000` when omitted. Must be between `1` and `1000` inclusive — requests outside that range are rejected with a 400 error. (default to 1000)
|
|
@@ -295,10 +296,11 @@ module SmplkitGeneratedClient::App
|
|
|
295
296
|
end
|
|
296
297
|
|
|
297
298
|
# List Environments
|
|
298
|
-
# List all environments for the authenticated account. `filter[search]` does a case-insensitive substring match against the environment `key` and `name`. `filter[classification]` narrows the result to one classification (`STANDARD` or `AD_HOC`).
|
|
299
|
+
# List all environments for the authenticated account. `filter[search]` does a case-insensitive substring match against the environment `key` and `name`. `filter[classification]` narrows the result to one classification (`STANDARD` or `AD_HOC`). `filter[managed]` narrows by managed state (`true` or `false`).
|
|
299
300
|
# @param [Hash] opts the optional parameters
|
|
300
301
|
# @option opts [String] :filter_search Case-insensitive substring match against the environment `key` and `name`. An environment is returned if either field contains the search term.
|
|
301
302
|
# @option opts [String] :filter_classification Narrow the result to environments with the given classification. One of `STANDARD` or `AD_HOC`.
|
|
303
|
+
# @option opts [Boolean] :filter_managed Narrow the result to managed (`true`) or unmanaged (`false`) environments. Omit to return both.
|
|
302
304
|
# @option opts [String] :sort Field to sort by. Prefix with `-` for descending order. Default: `name`. Allowed values: `created_at`, `-created_at`, `key`, `-key`, `name`, `-name`, `updated_at`, `-updated_at`. (default to 'name')
|
|
303
305
|
# @option opts [Integer] :page_number 1-based page number to return. Optional; defaults to `1` when omitted. Must be `>= 1` — requests with a smaller value are rejected with a 400 error. (default to 1)
|
|
304
306
|
# @option opts [Integer] :page_size Number of items per page. Optional; defaults to `1000` when omitted. Must be between `1` and `1000` inclusive — requests outside that range are rejected with a 400 error. (default to 1000)
|
|
@@ -319,6 +321,7 @@ module SmplkitGeneratedClient::App
|
|
|
319
321
|
query_params = opts[:query_params] || {}
|
|
320
322
|
query_params[:'filter[search]'] = opts[:'filter_search'] if !opts[:'filter_search'].nil?
|
|
321
323
|
query_params[:'filter[classification]'] = opts[:'filter_classification'] if !opts[:'filter_classification'].nil?
|
|
324
|
+
query_params[:'filter[managed]'] = opts[:'filter_managed'] if !opts[:'filter_managed'].nil?
|
|
322
325
|
query_params[:'sort'] = opts[:'sort'] if !opts[:'sort'].nil?
|
|
323
326
|
query_params[:'page[number]'] = opts[:'page_number'] if !opts[:'page_number'].nil?
|
|
324
327
|
query_params[:'page[size]'] = opts[:'page_size'] if !opts[:'page_size'].nil?
|
|
@@ -22,9 +22,12 @@ module SmplkitGeneratedClient::App
|
|
|
22
22
|
# Display color used by the console to badge the environment. Accepts any CSS color string.
|
|
23
23
|
attr_accessor :color
|
|
24
24
|
|
|
25
|
-
# `STANDARD` for environments the
|
|
25
|
+
# `STANDARD` for environments deliberately created (and shown by default in the environment grid); `AD_HOC` for auto-discovered environments seen in SDK traffic (hidden from the default view). Case-insensitive on input. Independent of the `managed` flag.
|
|
26
26
|
attr_accessor :classification
|
|
27
27
|
|
|
28
|
+
# When `true`, per-environment resource values can be set against this environment and it counts toward the account's managed-environments quota. When `false`, the environment is view-only: existing values are displayed for comparison but no new values can be written. Promotion and demotion flip this boolean via `PUT /api/v1/environments/{id}`; promotion is subject to the quota.
|
|
29
|
+
attr_accessor :managed
|
|
30
|
+
|
|
28
31
|
# When the environment was created.
|
|
29
32
|
attr_accessor :created_at
|
|
30
33
|
|
|
@@ -59,6 +62,7 @@ module SmplkitGeneratedClient::App
|
|
|
59
62
|
:'name' => :'name',
|
|
60
63
|
:'color' => :'color',
|
|
61
64
|
:'classification' => :'classification',
|
|
65
|
+
:'managed' => :'managed',
|
|
62
66
|
:'created_at' => :'created_at',
|
|
63
67
|
:'updated_at' => :'updated_at'
|
|
64
68
|
}
|
|
@@ -80,6 +84,7 @@ module SmplkitGeneratedClient::App
|
|
|
80
84
|
:'name' => :'String',
|
|
81
85
|
:'color' => :'String',
|
|
82
86
|
:'classification' => :'String',
|
|
87
|
+
:'managed' => :'Boolean',
|
|
83
88
|
:'created_at' => :'Time',
|
|
84
89
|
:'updated_at' => :'Time'
|
|
85
90
|
}
|
|
@@ -123,7 +128,13 @@ module SmplkitGeneratedClient::App
|
|
|
123
128
|
if attributes.key?(:'classification')
|
|
124
129
|
self.classification = attributes[:'classification']
|
|
125
130
|
else
|
|
126
|
-
self.classification = '
|
|
131
|
+
self.classification = 'STANDARD'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if attributes.key?(:'managed')
|
|
135
|
+
self.managed = attributes[:'managed']
|
|
136
|
+
else
|
|
137
|
+
self.managed = false
|
|
127
138
|
end
|
|
128
139
|
|
|
129
140
|
if attributes.key?(:'created_at')
|
|
@@ -209,6 +220,7 @@ module SmplkitGeneratedClient::App
|
|
|
209
220
|
name == o.name &&
|
|
210
221
|
color == o.color &&
|
|
211
222
|
classification == o.classification &&
|
|
223
|
+
managed == o.managed &&
|
|
212
224
|
created_at == o.created_at &&
|
|
213
225
|
updated_at == o.updated_at
|
|
214
226
|
end
|
|
@@ -222,7 +234,7 @@ module SmplkitGeneratedClient::App
|
|
|
222
234
|
# Calculates hash code according to all attributes.
|
|
223
235
|
# @return [Integer] Hash code
|
|
224
236
|
def hash
|
|
225
|
-
[name, color, classification, created_at, updated_at].hash
|
|
237
|
+
[name, color, classification, managed, created_at, updated_at].hash
|
|
226
238
|
end
|
|
227
239
|
|
|
228
240
|
# Builds the object from hash
|
|
@@ -83,10 +83,11 @@ describe 'EnvironmentsApi' do
|
|
|
83
83
|
|
|
84
84
|
# unit tests for list_environments
|
|
85
85
|
# List Environments
|
|
86
|
-
# List all environments for the authenticated account. `filter[search]` does a case-insensitive substring match against the environment `key` and `name`. `filter[classification]` narrows the result to one classification (`STANDARD` or `AD_HOC`).
|
|
86
|
+
# List all environments for the authenticated account. `filter[search]` does a case-insensitive substring match against the environment `key` and `name`. `filter[classification]` narrows the result to one classification (`STANDARD` or `AD_HOC`). `filter[managed]` narrows by managed state (`true` or `false`).
|
|
87
87
|
# @param [Hash] opts the optional parameters
|
|
88
88
|
# @option opts [String] :filter_search Case-insensitive substring match against the environment `key` and `name`. An environment is returned if either field contains the search term.
|
|
89
89
|
# @option opts [String] :filter_classification Narrow the result to environments with the given classification. One of `STANDARD` or `AD_HOC`.
|
|
90
|
+
# @option opts [Boolean] :filter_managed Narrow the result to managed (`true`) or unmanaged (`false`) environments. Omit to return both.
|
|
90
91
|
# @option opts [String] :sort Field to sort by. Prefix with `-` for descending order. Default: `name`. Allowed values: `created_at`, `-created_at`, `key`, `-key`, `name`, `-name`, `updated_at`, `-updated_at`.
|
|
91
92
|
# @option opts [Integer] :page_number 1-based page number to return. Optional; defaults to `1` when omitted. Must be `>= 1` — requests with a smaller value are rejected with a 400 error.
|
|
92
93
|
# @option opts [Integer] :page_size Number of items per page. Optional; defaults to `1000` when omitted. Must be between `1` and `1000` inclusive — requests outside that range are rejected with a 400 error.
|
|
@@ -49,6 +49,12 @@ describe SmplkitGeneratedClient::App::Environment do
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
describe 'test attribute "managed"' do
|
|
53
|
+
it 'should work' do
|
|
54
|
+
# assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
52
58
|
describe 'test attribute "created_at"' do
|
|
53
59
|
it 'should work' do
|
|
54
60
|
# assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
|
|
@@ -25,21 +25,24 @@ module Smplkit
|
|
|
25
25
|
|
|
26
26
|
# A live, dot-accessible view over a resolved configuration.
|
|
27
27
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
28
|
+
# Identity-stable per config key (the same instance is returned by
|
|
29
|
+
# repeat +client.config.get+ / +get_or_create+ calls). Every read goes
|
|
30
|
+
# through the underlying client's resolved-config cache, so WebSocket
|
|
31
|
+
# updates are picked up automatically — there is no +subscribe+ step.
|
|
31
32
|
class LiveConfigProxy
|
|
32
33
|
def initialize(client, key)
|
|
33
34
|
@client = client
|
|
34
35
|
@key = key
|
|
35
|
-
@snapshot = client._resolve_now(key)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def config_id = @key
|
|
39
|
+
|
|
38
40
|
def get(item_key, default = nil)
|
|
39
|
-
|
|
41
|
+
snapshot = current_values
|
|
42
|
+
return snapshot if item_key.nil?
|
|
40
43
|
|
|
41
44
|
keys = item_key.to_s.split(".")
|
|
42
|
-
keys.reduce(
|
|
45
|
+
keys.reduce(snapshot) do |scope, k|
|
|
43
46
|
break default if scope.nil?
|
|
44
47
|
|
|
45
48
|
scope.is_a?(Hash) ? scope[k] : default
|
|
@@ -51,13 +54,82 @@ module Smplkit
|
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def to_h
|
|
54
|
-
|
|
57
|
+
current_values.dup
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def refresh
|
|
58
|
-
|
|
61
|
+
# The cache is fully invalidated for this key — the next read
|
|
62
|
+
# re-resolves from the parent client.
|
|
63
|
+
@client._invalidate(@key)
|
|
59
64
|
self
|
|
60
65
|
end
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Typed getters (ADR-037 §2.13)
|
|
69
|
+
#
|
|
70
|
+
# Each registers the item (key, type, default, description) on first
|
|
71
|
+
# call within the process, then returns the resolved value. When the
|
|
72
|
+
# resolved value can't be coerced to the getter's type — including
|
|
73
|
+
# the "not yet set on the server" case — the in-code default is
|
|
74
|
+
# returned and a debug message is logged.
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def get_bool(item_key, default, description: nil)
|
|
78
|
+
register_item(item_key, "BOOLEAN", default, description)
|
|
79
|
+
value = current_values[item_key.to_s]
|
|
80
|
+
return default unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
81
|
+
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_int(item_key, default, description: nil)
|
|
86
|
+
register_item(item_key, "NUMBER", default, description)
|
|
87
|
+
value = current_values[item_key.to_s]
|
|
88
|
+
return default if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
89
|
+
return value if value.is_a?(Integer)
|
|
90
|
+
return value.to_i if value.is_a?(Float) && value == value.floor
|
|
91
|
+
|
|
92
|
+
default
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def get_float(item_key, default, description: nil)
|
|
96
|
+
register_item(item_key, "NUMBER", default, description)
|
|
97
|
+
value = current_values[item_key.to_s]
|
|
98
|
+
return default if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
99
|
+
return value.to_f if value.is_a?(Numeric)
|
|
100
|
+
|
|
101
|
+
default
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def get_string(item_key, default, description: nil)
|
|
105
|
+
register_item(item_key, "STRING", default, description)
|
|
106
|
+
value = current_values[item_key.to_s]
|
|
107
|
+
value.is_a?(String) ? value : default
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_json(item_key, default, description: nil)
|
|
111
|
+
register_item(item_key, "JSON", default, description)
|
|
112
|
+
snap = current_values
|
|
113
|
+
snap.key?(item_key.to_s) ? snap[item_key.to_s] : default
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def on_change(item_key = nil, &)
|
|
117
|
+
if item_key.nil?
|
|
118
|
+
@client.on_change(@key, &)
|
|
119
|
+
else
|
|
120
|
+
@client.on_change_item(@key, item_key.to_s, &)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def current_values
|
|
127
|
+
@client._resolve_now(@key)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def register_item(item_key, item_type, default, description)
|
|
131
|
+
@client._observe_item_declaration(@key, item_key.to_s, item_type, default, description)
|
|
132
|
+
end
|
|
61
133
|
end
|
|
62
134
|
|
|
63
135
|
# Synchronous config runtime namespace.
|
|
@@ -75,8 +147,10 @@ module Smplkit
|
|
|
75
147
|
|
|
76
148
|
@snapshots = {}
|
|
77
149
|
@raw_chains = {}
|
|
150
|
+
@proxies = {}
|
|
78
151
|
@global_listeners = []
|
|
79
152
|
@key_listeners = Hash.new { |h, k| h[k] = [] }
|
|
153
|
+
@item_listeners = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = [] } }
|
|
80
154
|
@connected = false
|
|
81
155
|
@lock = Mutex.new
|
|
82
156
|
end
|
|
@@ -85,6 +159,13 @@ module Smplkit
|
|
|
85
159
|
return if @connected
|
|
86
160
|
|
|
87
161
|
@environment = @parent._environment
|
|
162
|
+
|
|
163
|
+
# Per ADR-037 §2.14: flush any buffered discovery declarations
|
|
164
|
+
# BEFORE the lazy init touches the runtime so newly-declared
|
|
165
|
+
# configs are visible to the very first +get+. The flush itself
|
|
166
|
+
# swallows server/network failures.
|
|
167
|
+
@manage&.config&.flush
|
|
168
|
+
|
|
88
169
|
@ws_manager = @parent._ensure_ws
|
|
89
170
|
@ws_manager.on("config_changed") { |data| handle_config_changed(data) }
|
|
90
171
|
@ws_manager.on("config_deleted") { |data| handle_config_deleted(data) }
|
|
@@ -95,11 +176,35 @@ module Smplkit
|
|
|
95
176
|
start unless @connected
|
|
96
177
|
|
|
97
178
|
snapshot = resolve(config_key)
|
|
179
|
+
raise Smplkit::NotFoundError, "Config #{config_key.inspect} not found" if snapshot.nil?
|
|
98
180
|
return snapshot if model_class.nil?
|
|
99
181
|
|
|
100
182
|
model_class.new(snapshot)
|
|
101
183
|
end
|
|
102
184
|
|
|
185
|
+
# Declare a configuration from code; return a live, dict-like view.
|
|
186
|
+
#
|
|
187
|
+
# Idempotent — repeat calls with the same +id+ return the same
|
|
188
|
+
# +LiveConfigProxy+ instance. The first call queues a discovery
|
|
189
|
+
# payload (the config and any items declared via typed getters on
|
|
190
|
+
# the returned handle) for upload to +POST /api/v1/configs/bulk+ on
|
|
191
|
+
# next flush. Unlike +#get+, this method does not raise +NotFoundError+
|
|
192
|
+
# when the id is absent — discovery handles that case.
|
|
193
|
+
def get_or_create(config_id, parent: nil, name: nil, description: nil)
|
|
194
|
+
parent_id =
|
|
195
|
+
case parent
|
|
196
|
+
when nil then nil
|
|
197
|
+
when String then parent
|
|
198
|
+
when LiveConfigProxy then parent.config_id
|
|
199
|
+
else
|
|
200
|
+
raise ArgumentError,
|
|
201
|
+
"parent must be a String id or LiveConfigProxy; got #{parent.class.name}"
|
|
202
|
+
end
|
|
203
|
+
_observe_config_declaration(config_id, parent: parent_id, name: name, description: description)
|
|
204
|
+
start unless @connected
|
|
205
|
+
cached_proxy(config_id)
|
|
206
|
+
end
|
|
207
|
+
|
|
103
208
|
def get_string(item_key, default: nil, config: nil)
|
|
104
209
|
typed_get(item_key, default, config) { |v| v.is_a?(String) ? v : v.to_s }
|
|
105
210
|
end
|
|
@@ -123,7 +228,7 @@ module Smplkit
|
|
|
123
228
|
def live(config_key)
|
|
124
229
|
start unless @connected
|
|
125
230
|
|
|
126
|
-
|
|
231
|
+
cached_proxy(config_key)
|
|
127
232
|
end
|
|
128
233
|
|
|
129
234
|
def on_change(config_key = nil, &block)
|
|
@@ -137,6 +242,13 @@ module Smplkit
|
|
|
137
242
|
block
|
|
138
243
|
end
|
|
139
244
|
|
|
245
|
+
def on_change_item(config_key, item_key, &block)
|
|
246
|
+
raise ArgumentError, "on_change_item requires a block" unless block
|
|
247
|
+
|
|
248
|
+
@item_listeners[config_key][item_key.to_s] << block
|
|
249
|
+
block
|
|
250
|
+
end
|
|
251
|
+
|
|
140
252
|
def refresh
|
|
141
253
|
@lock.synchronize do
|
|
142
254
|
@snapshots.clear
|
|
@@ -146,15 +258,46 @@ module Smplkit
|
|
|
146
258
|
end
|
|
147
259
|
|
|
148
260
|
def _resolve_now(config_key)
|
|
149
|
-
resolve(config_key)
|
|
261
|
+
resolve(config_key) || {}
|
|
150
262
|
end
|
|
151
263
|
|
|
152
264
|
def _close
|
|
153
265
|
# No durable resources; symmetry stub.
|
|
154
266
|
end
|
|
155
267
|
|
|
268
|
+
# Discard cached state for +config_key+; the next resolve will refetch.
|
|
269
|
+
def _invalidate(config_key)
|
|
270
|
+
@lock.synchronize do
|
|
271
|
+
@snapshots.delete(config_key)
|
|
272
|
+
@raw_chains.delete(config_key)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Internal: queue a config declaration with the management buffer.
|
|
277
|
+
def _observe_config_declaration(config_id, parent:, name:, description:)
|
|
278
|
+
@manage.config.register_config(
|
|
279
|
+
config_id,
|
|
280
|
+
service: @service,
|
|
281
|
+
environment: @environment,
|
|
282
|
+
parent: parent,
|
|
283
|
+
name: name,
|
|
284
|
+
description: description
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Internal: queue a config item declaration with the management buffer.
|
|
289
|
+
def _observe_item_declaration(config_id, item_key, item_type, default, description)
|
|
290
|
+
@manage.config.register_config_item(config_id, item_key, item_type, default, description)
|
|
291
|
+
end
|
|
292
|
+
|
|
156
293
|
private
|
|
157
294
|
|
|
295
|
+
def cached_proxy(config_key)
|
|
296
|
+
@lock.synchronize do
|
|
297
|
+
@proxies[config_key] ||= LiveConfigProxy.new(self, config_key)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
158
301
|
def typed_get(item_key, default, config_key)
|
|
159
302
|
snapshot = config_key ? resolve(config_key) : merged_snapshot
|
|
160
303
|
key = item_key.to_s
|
|
@@ -190,6 +333,12 @@ module Smplkit
|
|
|
190
333
|
end
|
|
191
334
|
|
|
192
335
|
chain = fetch_chain(config_key)
|
|
336
|
+
# An empty chain means the config does not exist on the server.
|
|
337
|
+
# Callers that hit +get(key)+ must raise +NotFoundError+; callers
|
|
338
|
+
# that hold a +LiveConfigProxy+ get an empty Hash from
|
|
339
|
+
# +_resolve_now+ so typed getters fall back to defaults.
|
|
340
|
+
return nil if chain.nil? || chain.empty?
|
|
341
|
+
|
|
193
342
|
snapshot = Helpers.resolve_chain(chain, @environment)
|
|
194
343
|
@lock.synchronize do
|
|
195
344
|
@raw_chains[config_key] = chain
|
|
@@ -238,6 +387,19 @@ module Smplkit
|
|
|
238
387
|
rescue StandardError => e
|
|
239
388
|
Smplkit.debug("config", "listener raised: #{e.class}: #{e.message}")
|
|
240
389
|
end
|
|
390
|
+
# Item-scoped listeners — fire for every registered item on the
|
|
391
|
+
# changed config. We don't diff old vs new values here because
|
|
392
|
+
# the Ruby cache is invalidated wholesale per config; item-scoped
|
|
393
|
+
# listeners on this SDK fire on the "any change to this config"
|
|
394
|
+
# signal, mirroring the +LiveConfigProxy.on_change(item_key)+
|
|
395
|
+
# contract.
|
|
396
|
+
@item_listeners[config_key].each_value do |listeners|
|
|
397
|
+
listeners.each do |cb|
|
|
398
|
+
cb.call(event)
|
|
399
|
+
rescue StandardError => e
|
|
400
|
+
Smplkit.debug("config", "item listener raised: #{e.class}: #{e.message}")
|
|
401
|
+
end
|
|
402
|
+
end
|
|
241
403
|
end
|
|
242
404
|
|
|
243
405
|
def fire_change_listeners_all(source)
|
|
@@ -6,6 +6,7 @@ module Smplkit
|
|
|
6
6
|
CONTEXT_BATCH_FLUSH_SIZE = 100
|
|
7
7
|
FLAG_BATCH_FLUSH_SIZE = 50
|
|
8
8
|
LOGGER_BATCH_FLUSH_SIZE = 50
|
|
9
|
+
CONFIG_BATCH_FLUSH_SIZE = 50
|
|
9
10
|
|
|
10
11
|
# Thread-safe batch buffer for context registration.
|
|
11
12
|
class ContextRegistrationBuffer
|
|
@@ -89,6 +90,77 @@ module Smplkit
|
|
|
89
90
|
end
|
|
90
91
|
end
|
|
91
92
|
|
|
93
|
+
# Thread-safe batch buffer for config declarations. Mirrors Python's
|
|
94
|
+
# +_ConfigRegistrationBuffer+: per-config metadata is retained across
|
|
95
|
+
# flushes so post-drain deltas re-attribute correctly, and items are
|
|
96
|
+
# dedup'd per +(config_id, item_key)+ so an already-sent item is
|
|
97
|
+
# never re-sent.
|
|
98
|
+
class ConfigRegistrationBuffer
|
|
99
|
+
def initialize
|
|
100
|
+
@pending = {} # config_id -> { id:, items: {}, ...meta }
|
|
101
|
+
@meta = {} # config_id -> { service:, environment:, parent:, name:, description: }
|
|
102
|
+
@sent_items = {} # "#{config_id}::#{item_key}" -> true
|
|
103
|
+
@lock = Mutex.new
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Idempotent — first writer's metadata wins.
|
|
107
|
+
def declare(config_id, service:, environment:, parent: nil, name: nil, description: nil)
|
|
108
|
+
@lock.synchronize do
|
|
109
|
+
next if @meta.key?(config_id)
|
|
110
|
+
|
|
111
|
+
@meta[config_id] = {
|
|
112
|
+
service: service, environment: environment,
|
|
113
|
+
parent: parent, name: name, description: description
|
|
114
|
+
}
|
|
115
|
+
@pending[config_id] = build_entry(config_id)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Queue an item declaration for an already-declared config. Items
|
|
120
|
+
# already sent in a previous +drain+ are skipped.
|
|
121
|
+
def add_item(config_id, item_key, item_type, default, description = nil)
|
|
122
|
+
@lock.synchronize do
|
|
123
|
+
next unless @meta.key?(config_id)
|
|
124
|
+
next if @sent_items.key?("#{config_id}::#{item_key}")
|
|
125
|
+
|
|
126
|
+
entry = (@pending[config_id] ||= build_entry(config_id))
|
|
127
|
+
next if entry["items"].key?(item_key)
|
|
128
|
+
|
|
129
|
+
item = { "value" => default, "type" => item_type }
|
|
130
|
+
item["description"] = description unless description.nil?
|
|
131
|
+
entry["items"][item_key] = item
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Returns and clears the pending batch; records sent items.
|
|
136
|
+
def drain
|
|
137
|
+
@lock.synchronize do
|
|
138
|
+
entries = @pending.values
|
|
139
|
+
entries.each do |entry|
|
|
140
|
+
entry["items"].each_key { |item_key| @sent_items["#{entry["id"]}::#{item_key}"] = true }
|
|
141
|
+
end
|
|
142
|
+
@pending = {}
|
|
143
|
+
entries
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def pending_count
|
|
148
|
+
@lock.synchronize { @pending.size }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def build_entry(config_id)
|
|
154
|
+
meta = @meta[config_id]
|
|
155
|
+
entry = { "id" => config_id, "items" => {} }
|
|
156
|
+
%i[service environment parent name description].each do |k|
|
|
157
|
+
v = meta[k]
|
|
158
|
+
entry[k.to_s] = v unless v.nil?
|
|
159
|
+
end
|
|
160
|
+
entry
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
92
164
|
# Thread-safe batch buffer for logger discovery.
|
|
93
165
|
class LoggerRegistrationBuffer
|
|
94
166
|
def initialize
|
|
@@ -453,6 +453,59 @@ module Smplkit
|
|
|
453
453
|
class ConfigNamespace
|
|
454
454
|
def initialize(api_client)
|
|
455
455
|
@api = SmplkitGeneratedClient::Config::ConfigsApi.new(api_client)
|
|
456
|
+
@buffer = Management::ConfigRegistrationBuffer.new
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# ---------------------------------------------------------------
|
|
460
|
+
# Discovery API (ADR-037 §2.13/§2.14)
|
|
461
|
+
# ---------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
# Queue a configuration declaration for bulk-discovery upload.
|
|
464
|
+
# Called by +ConfigClient#get_or_create+. Threshold-flushes on a
|
|
465
|
+
# background thread once the pending buffer reaches the flush size.
|
|
466
|
+
def register_config(config_id, service:, environment:, parent: nil,
|
|
467
|
+
name: nil, description: nil)
|
|
468
|
+
@buffer.declare(config_id, service: service, environment: environment,
|
|
469
|
+
parent: parent, name: name, description: description)
|
|
470
|
+
trigger_background_flush_if_needed
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Queue a config item declaration. +register_config+ must have run
|
|
474
|
+
# first; items added without a prior declaration are dropped.
|
|
475
|
+
def register_config_item(config_id, item_key, item_type, default, description = nil)
|
|
476
|
+
@buffer.add_item(config_id, item_key, item_type, default, description)
|
|
477
|
+
trigger_background_flush_if_needed
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def pending_count
|
|
481
|
+
@buffer.pending_count
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Send any pending config declarations to
|
|
485
|
+
# +POST /api/v1/configs/bulk+. Per ADR-024 §2.9 the bulk endpoint is
|
|
486
|
+
# plan-limit-exempt; failures here never propagate to customer code.
|
|
487
|
+
def flush
|
|
488
|
+
batch = @buffer.drain
|
|
489
|
+
return if batch.empty?
|
|
490
|
+
|
|
491
|
+
items = batch.map do |entry|
|
|
492
|
+
SmplkitGeneratedClient::Config::ConfigBulkItem.new(
|
|
493
|
+
id: entry["id"],
|
|
494
|
+
service: entry["service"],
|
|
495
|
+
environment: entry["environment"],
|
|
496
|
+
parent: entry["parent"],
|
|
497
|
+
name: entry["name"],
|
|
498
|
+
description: entry["description"],
|
|
499
|
+
items: bulk_items_to_wire(entry["items"])
|
|
500
|
+
)
|
|
501
|
+
end
|
|
502
|
+
body = SmplkitGeneratedClient::Config::ConfigBulkRequest.new(configs: items)
|
|
503
|
+
begin
|
|
504
|
+
ErrorMapping.call { @api.bulk_register_configs(body) }
|
|
505
|
+
rescue StandardError => e
|
|
506
|
+
# Fire-and-forget per ADR-024 §2.9.
|
|
507
|
+
Smplkit.debug("registration", "config bulk register failed: #{e.class}: #{e.message}")
|
|
508
|
+
end
|
|
456
509
|
end
|
|
457
510
|
|
|
458
511
|
def list(page_number: nil, page_size: nil)
|
|
@@ -584,6 +637,28 @@ module Smplkit
|
|
|
584
637
|
|
|
585
638
|
{ "id" => config.id, "items" => items_hash, "environments" => environments }
|
|
586
639
|
end
|
|
640
|
+
|
|
641
|
+
def bulk_items_to_wire(items_hash)
|
|
642
|
+
return nil if items_hash.nil? || items_hash.empty?
|
|
643
|
+
|
|
644
|
+
items_hash.transform_values do |def_hash|
|
|
645
|
+
SmplkitGeneratedClient::Config::ConfigItemDefinition.new(
|
|
646
|
+
value: def_hash["value"],
|
|
647
|
+
type: def_hash["type"],
|
|
648
|
+
description: def_hash["description"]
|
|
649
|
+
)
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def trigger_background_flush_if_needed
|
|
654
|
+
return unless @buffer.pending_count >= Management::CONFIG_BATCH_FLUSH_SIZE
|
|
655
|
+
|
|
656
|
+
Thread.new do
|
|
657
|
+
flush
|
|
658
|
+
rescue StandardError => e
|
|
659
|
+
Smplkit.debug("registration", "threshold config flush failed: #{e.class}: #{e.message}")
|
|
660
|
+
end
|
|
661
|
+
end
|
|
587
662
|
end
|
|
588
663
|
|
|
589
664
|
class FlagsNamespace
|