feat-sdk 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/lib/feat/client.rb +27 -0
- data/lib/feat/datafile.rb +30 -0
- data/lib/feat/streaming.rb +18 -7
- data/lib/feat/version.rb +1 -1
- 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: 4dfb1cd9905d46890db88dea9411a6dc3755208286fbbec6565d8742459d5008
|
|
4
|
+
data.tar.gz: 63c030396f376ff362b3d0d5b7c07a7cc926b4dd5141c9516988dbcb87a3419f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 347f444e5f8354f9b4b4d8957c812bde443a502b7c6cc54623df05983abe332cafd77f37ffebbb01d8cd8e8940d280a8004068e16403398721b2bb6bcc2ec4c6
|
|
7
|
+
data.tar.gz: e192084e64f93ff51ab4e14aa4384ec0954be13f6e4996c00371185deee30a9fe67b718d3065a07b64d2bd15dd98a4bfcf0677bb54e67a0959015ad542504fee
|
data/lib/feat/client.rb
CHANGED
|
@@ -135,9 +135,36 @@ module Feat
|
|
|
135
135
|
api_key: @api_key,
|
|
136
136
|
transport: transport,
|
|
137
137
|
on_put: ->(parsed) { store_datafile(parsed, parsed["etag"]) },
|
|
138
|
+
on_patch: ->(parsed) { apply_patch(parsed) },
|
|
138
139
|
)
|
|
139
140
|
end
|
|
140
141
|
|
|
142
|
+
# Apply a streamed `patch` delta. Version-gated: the delta is merged only
|
|
143
|
+
# when the in-memory datafile's version equals the patch's +from+, so the
|
|
144
|
+
# result is exactly the +to+ snapshot. On any gap or mismatch the patch is
|
|
145
|
+
# ignored - a reconnect reseeds a full `put` and the safety poll backstops.
|
|
146
|
+
# Runs under the same mutex as store_datafile, so a subsequent evaluation
|
|
147
|
+
# sees the merged delta atomically. Returns true when applied.
|
|
148
|
+
def apply_patch(patch)
|
|
149
|
+
from = patch["from"]
|
|
150
|
+
to = patch["to"]
|
|
151
|
+
# Reject malformed or out-of-order deltas before touching the datafile:
|
|
152
|
+
# both bounds must be integers and the patch must move strictly forward.
|
|
153
|
+
# Without the `to > from` guard a backward `to` would roll the in-memory
|
|
154
|
+
# version backward and break version ordering.
|
|
155
|
+
return false unless from.is_a?(Integer) && to.is_a?(Integer) && to > from
|
|
156
|
+
|
|
157
|
+
@mutex.synchronize do
|
|
158
|
+
current = @datafile
|
|
159
|
+
return false if current.nil?
|
|
160
|
+
return false unless current.version == from
|
|
161
|
+
|
|
162
|
+
@datafile = Datafile.merge_patch(current, patch)
|
|
163
|
+
@etag = patch["etag"] if patch["etag"]
|
|
164
|
+
end
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
|
|
141
168
|
def poll_loop
|
|
142
169
|
until @stop
|
|
143
170
|
interruptible_sleep(@poll_interval) { @stop }
|
data/lib/feat/datafile.rb
CHANGED
|
@@ -49,6 +49,36 @@ module Feat
|
|
|
49
49
|
)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Merge a patch delta onto an existing File and return a new File. Pure:
|
|
53
|
+
# +current+ is not mutated. Added or changed flags/segments are built
|
|
54
|
+
# from their wire objects and override by key; removed keys are dropped.
|
|
55
|
+
# version, etag, and generatedAt advance to the patch's values (etag and
|
|
56
|
+
# generatedAt fall back to the current ones when the patch omits them).
|
|
57
|
+
# Raises if a flag or segment object is malformed; the caller treats that
|
|
58
|
+
# as a no-op and ignores the patch.
|
|
59
|
+
def self.merge_patch(current, patch)
|
|
60
|
+
flags = current.flags.dup
|
|
61
|
+
(patch["flags"] || {}).each { |k, v| flags[k] = build_flag(v) }
|
|
62
|
+
(patch["removedFlags"] || []).each { |k| flags.delete(k) }
|
|
63
|
+
|
|
64
|
+
segments = current.segments.dup
|
|
65
|
+
(patch["segments"] || {}).each { |k, v| segments[k] = build_segment(v) }
|
|
66
|
+
(patch["removedSegments"] || []).each { |k| segments.delete(k) }
|
|
67
|
+
|
|
68
|
+
File.new(
|
|
69
|
+
schemaVersion: current.schemaVersion,
|
|
70
|
+
envId: current.envId,
|
|
71
|
+
envKey: current.envKey,
|
|
72
|
+
projectId: current.projectId,
|
|
73
|
+
version: patch["to"],
|
|
74
|
+
etag: patch["etag"] || current.etag,
|
|
75
|
+
generatedAt: patch["generatedAt"] || current.generatedAt,
|
|
76
|
+
flags: flags,
|
|
77
|
+
segments: segments,
|
|
78
|
+
contextKinds: current.contextKinds
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
52
82
|
def self.build_flag(d)
|
|
53
83
|
FlagSpec.new(
|
|
54
84
|
id: d["id"],
|
data/lib/feat/streaming.rb
CHANGED
|
@@ -87,8 +87,9 @@ module Feat
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
# Holds a long-lived SSE connection to the datafile stream endpoint and
|
|
90
|
-
# invokes +on_put+ with the parsed datafile for every `put` frame
|
|
91
|
-
#
|
|
90
|
+
# invokes +on_put+ with the parsed datafile for every `put` frame and
|
|
91
|
+
# +on_patch+ with the parsed delta for every `patch` frame. Runs on its
|
|
92
|
+
# own thread, reconnects with exponential backoff, and stops cleanly.
|
|
92
93
|
class StreamingClient
|
|
93
94
|
include InterruptibleSleep
|
|
94
95
|
|
|
@@ -106,7 +107,7 @@ module Feat
|
|
|
106
107
|
TERMINAL_STREAM_CODES = [401, 403].freeze
|
|
107
108
|
JOIN_TIMEOUT_SECONDS = 5
|
|
108
109
|
|
|
109
|
-
def initialize(url:, api_key:, transport:, on_put:, on_error: nil,
|
|
110
|
+
def initialize(url:, api_key:, transport:, on_put:, on_patch: nil, on_error: nil,
|
|
110
111
|
initial_backoff: DEFAULT_INITIAL_BACKOFF,
|
|
111
112
|
max_backoff: DEFAULT_MAX_BACKOFF,
|
|
112
113
|
min_uptime: DEFAULT_MIN_UPTIME,
|
|
@@ -115,6 +116,7 @@ module Feat
|
|
|
115
116
|
@api_key = api_key
|
|
116
117
|
@transport = transport
|
|
117
118
|
@on_put = on_put
|
|
119
|
+
@on_patch = on_patch
|
|
118
120
|
@on_error = on_error
|
|
119
121
|
@initial_backoff = initial_backoff
|
|
120
122
|
@max_backoff = max_backoff
|
|
@@ -213,8 +215,19 @@ module Feat
|
|
|
213
215
|
end
|
|
214
216
|
|
|
215
217
|
def handle_event(event)
|
|
216
|
-
|
|
218
|
+
case event[:event]
|
|
219
|
+
when "put" then with_parsed_payload(event) { |parsed| @on_put.call(parsed) }
|
|
220
|
+
when "patch" then with_parsed_payload(event) { |parsed| @on_patch&.call(parsed) }
|
|
221
|
+
end
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
notify_error(e)
|
|
224
|
+
end
|
|
217
225
|
|
|
226
|
+
# Decode the event's JSON data field and hand it to the block. A missing,
|
|
227
|
+
# empty, or unparseable payload is dropped silently (the next frame or a
|
|
228
|
+
# reconnect recovers); any error the block raises bubbles to the caller's
|
|
229
|
+
# rescue and is surfaced via on_error.
|
|
230
|
+
def with_parsed_payload(event)
|
|
218
231
|
raw = event[:data]
|
|
219
232
|
return if raw.nil? || raw.empty?
|
|
220
233
|
|
|
@@ -225,9 +238,7 @@ module Feat
|
|
|
225
238
|
return
|
|
226
239
|
end
|
|
227
240
|
|
|
228
|
-
|
|
229
|
-
rescue StandardError => e
|
|
230
|
-
notify_error(e)
|
|
241
|
+
yield parsed
|
|
231
242
|
end
|
|
232
243
|
|
|
233
244
|
# We deliberately do not send a Last-Event-ID header to resume. The
|
data/lib/feat/version.rb
CHANGED