ask-state-providers 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18cfafbe7783b55543313b0708ed885275c8bd954a63cf152251dbfb465fde49
4
- data.tar.gz: 60c56b1e649b86c893e8af07b446c3a022afb4d4b56d0419d74465b402ca36be
3
+ metadata.gz: 550bf5d05277f2107b94f57c09ec2189184af18ceb820a1538b07a1ed5b3981f
4
+ data.tar.gz: 3bb373e4312de695a6e71d76b3557e5933cc97edeae170cf3ff38c1247010c47
5
5
  SHA512:
6
- metadata.gz: 38f53d279574b58eddf256975467c8bb30047060e5e994093f38ced15feb61c7e02e34c9023b7ec02a669e1b94142ca9a26cdd9d27938c5f5d51aee5d61e0715
7
- data.tar.gz: be37ac572c9081ab10bf85f878a6285fd04b3522c731ae0bc28d472c855584e567d791b21953f6e013df1cbf5ebe3c48146ba91aded39c4aa08e2e2c80456f1e
6
+ metadata.gz: 9641c185a5128f8129e28094b8b7c605a5abc458f32685abdb7f121b18991c2eed62e61e37ce29d254e03310c0628f6bb3e98575877887e55e0cd8f0ea55843a
7
+ data.tar.gz: 823cd11970169cf011575f498f1c182a6ed1c76053ccf6cc00f28e840f68ab2dd80b84af9ae19dba474c6a250c284ea97ada092ed2d816979ad242c1457499f3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.3.0] — 2026-07-28
6
+
7
+ ### Added
8
+
9
+ - **`Ask::State::Memory`** — in-memory state backend, moved from `ask-core`. Same class name, same API. Requires `require "ask-state-providers"` instead of `require "ask"`.
10
+
11
+ ```ruby
12
+ require "ask-state-providers"
13
+ store = Ask::State::Memory.new
14
+ ```
15
+
5
16
  ## [0.2.0] — 2026-07-23
6
17
 
7
18
  ### Changed
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Ask
6
+ module State
7
+ # In-process state backend backed by a Hash.
8
+ # All operations are thread-safe via a Mutex.
9
+ # Data is not persisted — lost on process exit.
10
+ #
11
+ # Available automatically when using +ask-state-providers+.
12
+ # For lightweight use without the full providers gem, see
13
+ # {Ask::State::Adapter} for the abstract contract.
14
+ class Memory < Adapter
15
+ def initialize
16
+ @data = {}
17
+ @locks = {}
18
+ @queues = {}
19
+ @lists = {}
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ # -- key-value --
24
+
25
+ def get(key)
26
+ @mutex.synchronize do
27
+ expiry = @data[key]&.dig(:expires_at)
28
+ return nil if expiry && Time.now >= expiry
29
+
30
+ @data[key]&.dig(:value)
31
+ end
32
+ end
33
+
34
+ def set(key, value, ttl: nil)
35
+ @mutex.synchronize do
36
+ @data[key] = {
37
+ value: value,
38
+ expires_at: ttl ? Time.now + ttl : nil
39
+ }
40
+ end
41
+ end
42
+
43
+ def delete(key)
44
+ @mutex.synchronize { @data.delete(key) }
45
+ end
46
+
47
+ def set_if_not_exists(key, value, ttl: nil)
48
+ @mutex.synchronize do
49
+ if @data.key?(key)
50
+ expiry = @data[key][:expires_at]
51
+ return false if expiry.nil? || Time.now < expiry
52
+ @data.delete(key)
53
+ end
54
+
55
+ @data[key] = {
56
+ value: value,
57
+ expires_at: ttl ? Time.now + ttl : nil
58
+ }
59
+ true
60
+ end
61
+ end
62
+
63
+ def clear
64
+ @mutex.synchronize do
65
+ @data.clear
66
+ @locks.clear
67
+ @queues.clear
68
+ @lists.clear
69
+ end
70
+ end
71
+
72
+ def exists?(key)
73
+ @mutex.synchronize do
74
+ expiry = @data[key]&.dig(:expires_at)
75
+ return false if expiry && Time.now >= expiry
76
+ @data.key?(key)
77
+ end
78
+ end
79
+
80
+ def keys(pattern: nil)
81
+ @mutex.synchronize do
82
+ active = @data.select do |_k, v|
83
+ expiry = v[:expires_at]
84
+ expiry.nil? || Time.now < expiry
85
+ end
86
+
87
+ keys = active.keys.map(&:to_s)
88
+ return keys unless pattern
89
+
90
+ regex = self.class.glob_to_regex(pattern)
91
+ keys.select { |k| k.match?(regex) }
92
+ end
93
+ end
94
+
95
+ # -- locking --
96
+
97
+ def acquire_lock(key, ttl: 10)
98
+ @mutex.synchronize do
99
+ existing = @locks[key]
100
+ if existing.nil? || existing.expired?
101
+ lock = Lock.new(
102
+ id: key,
103
+ token: SecureRandom.hex(16),
104
+ expires_at: Time.now + ttl
105
+ )
106
+ @locks[key] = lock
107
+ lock
108
+ end
109
+ end
110
+ end
111
+
112
+ def release_lock(key, lock)
113
+ @mutex.synchronize do
114
+ current = @locks[key]
115
+ if current && current.token == lock.token && !current.expired?
116
+ @locks.delete(key)
117
+ true
118
+ else
119
+ false
120
+ end
121
+ end
122
+ end
123
+
124
+ # -- queues --
125
+
126
+ def enqueue(queue, value)
127
+ @mutex.synchronize do
128
+ @queues[queue] ||= []
129
+ entry = QueueEntry.new(
130
+ id: SecureRandom.uuid,
131
+ value: value,
132
+ enqueued_at: Time.now
133
+ )
134
+ @queues[queue] << entry
135
+ entry
136
+ end
137
+ end
138
+
139
+ def dequeue(queue)
140
+ @mutex.synchronize do
141
+ q = @queues[queue]
142
+ return nil unless q&.any?
143
+ q.shift
144
+ end
145
+ end
146
+
147
+ def queue_depth(queue)
148
+ @mutex.synchronize do
149
+ (@queues[queue] || []).length
150
+ end
151
+ end
152
+
153
+ # -- lists --
154
+
155
+ def list_append(key, value, max_length: nil)
156
+ @mutex.synchronize do
157
+ @lists[key] ||= []
158
+ @lists[key] << value
159
+ @lists[key].shift if max_length && @lists[key].length > max_length
160
+ end
161
+ end
162
+
163
+ def list_range(key, start = 0, stop = -1)
164
+ @mutex.synchronize do
165
+ list = @lists[key] || []
166
+ return list if start == 0 && stop == -1
167
+ stop = list.length - 1 if stop == -1 || stop >= list.length
168
+ return [] if start > stop
169
+ list[start..stop] || []
170
+ end
171
+ end
172
+
173
+ def list_remove(key, value)
174
+ @mutex.synchronize do
175
+ list = @lists[key]
176
+ return 0 unless list
177
+ before = list.length
178
+ list.delete(value)
179
+ before - list.length
180
+ end
181
+ end
182
+
183
+ # -- lifecycle --
184
+
185
+ def close
186
+ @mutex.synchronize do
187
+ @data.clear
188
+ @locks.clear
189
+ @queues.clear
190
+ @lists.clear
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ def glob_to_regex(pattern)
197
+ self.class.glob_to_regex(pattern)
198
+ end
199
+ end
200
+ end
201
+ end
@@ -114,6 +114,36 @@ module Ask
114
114
 
115
115
  def clear
116
116
  @client.query("DELETE FROM state_store")
117
+ @client.query("DELETE FROM locks")
118
+ @client.query("DELETE FROM queues")
119
+ @client.query("DELETE FROM lists")
120
+ end
121
+
122
+ def exists?(key)
123
+ result = @client.prepare(<<~SQL).execute(key, Time.now.utc.strftime("%Y-%m-%d %H:%M:%S.%3N"))
124
+ SELECT 1 FROM state_store
125
+ WHERE `key` = ? AND (expires_at IS NULL OR expires_at > ?)
126
+ SQL
127
+ result.count > 0
128
+ end
129
+
130
+ def keys(pattern: nil)
131
+ sql, params = if pattern
132
+ like = self.class.glob_to_like(pattern)
133
+ [<<~SQL, [like]]
134
+ SELECT `key` FROM state_store
135
+ WHERE `key` LIKE ? AND (expires_at IS NULL OR expires_at > ?)
136
+ SQL
137
+ else
138
+ [<<~SQL, []]
139
+ SELECT `key` FROM state_store
140
+ WHERE (expires_at IS NULL OR expires_at > ?)
141
+ SQL
142
+ end
143
+ now = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S.%3N")
144
+ stmt = @client.prepare(sql)
145
+ rows = stmt.execute(*params, now)
146
+ rows.map { |r| r["key"] }
117
147
  end
118
148
 
119
149
  # -- distributed locking --
@@ -116,7 +116,40 @@ module Ask
116
116
  end
117
117
 
118
118
  def clear
119
- @pool.with { |conn| conn.exec("DELETE FROM state_store") }
119
+ @pool.with do |conn|
120
+ conn.exec("DELETE FROM state_store")
121
+ conn.exec("DELETE FROM locks")
122
+ conn.exec("DELETE FROM queues")
123
+ conn.exec("DELETE FROM lists")
124
+ end
125
+ end
126
+
127
+ def exists?(key)
128
+ @pool.with do |conn|
129
+ result = conn.exec_params(<<~SQL, [key, Time.now.utc])
130
+ SELECT 1 FROM state_store
131
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > $2)
132
+ SQL
133
+ result.ntuples > 0
134
+ end
135
+ end
136
+
137
+ def keys(pattern: nil)
138
+ @pool.with do |conn|
139
+ sql, params = if pattern
140
+ like = self.class.glob_to_like(pattern)
141
+ [<<~SQL, [like, Time.now.utc]]
142
+ SELECT key FROM state_store
143
+ WHERE key LIKE $1 AND (expires_at IS NULL OR expires_at > $2)
144
+ SQL
145
+ else
146
+ [<<~SQL, [Time.now.utc]]
147
+ SELECT key FROM state_store
148
+ WHERE (expires_at IS NULL OR expires_at > $1)
149
+ SQL
150
+ end
151
+ conn.exec_params(sql, params).map { |r| r["key"] }
152
+ end
120
153
  end
121
154
 
122
155
  # -- distributed locking --
@@ -58,6 +58,17 @@ module Ask
58
58
  @redis.call("DEL", *keys) if keys.any?
59
59
  end
60
60
 
61
+ def exists?(key)
62
+ @redis.call("EXISTS", prefixed(key)) == 1
63
+ end
64
+
65
+ def keys(pattern: nil)
66
+ glob = pattern ? "#{NAMESPACE}:#{pattern}" : "#{NAMESPACE}:*"
67
+ raw = @redis.call("KEYS", glob)
68
+ prefix_len = NAMESPACE.length + 1
69
+ raw.map { |k| k[prefix_len..] }
70
+ end
71
+
61
72
  # -- distributed locking --
62
73
 
63
74
  def acquire_lock(key, ttl: 10)
@@ -117,7 +128,6 @@ module Ask
117
128
  @redis.call("RPUSH", prefixed("list:#{key}"), JSON.generate(value))
118
129
  return unless max_length
119
130
 
120
- # LTRIM keeps only the specified range (negative indices, so -max_length means keep newest max_length)
121
131
  @redis.call("LTRIM", prefixed("list:#{key}"), -max_length, -1)
122
132
  end
123
133
 
@@ -13,6 +13,8 @@ module Ask
13
13
  # key-value storage, distributed locking, message queues, and ordered
14
14
  # lists. Uses the +sqlite3+ gem.
15
15
  #
16
+ # Thread-safe via internal Mutex.
17
+ #
16
18
  # @example
17
19
  # store = Ask::State::Providers::SQLite.new(path: "sessions.db")
18
20
  # store.set("key", { hello: "world" })
@@ -23,6 +25,7 @@ module Ask
23
25
  def initialize(path: "sessions.db", **pragmas)
24
26
  require "sqlite3"
25
27
 
28
+ @mutex = Mutex.new
26
29
  @db = SQLite3::Database.new(path)
27
30
  @db.results_as_hash = true
28
31
  @db.busy_timeout = 5000
@@ -44,181 +47,235 @@ module Ask
44
47
  # -- key-value --
45
48
 
46
49
  def get(key)
47
- row = @db.get_first_row(<<~SQL, [key, Time.now.to_f])
48
- SELECT value FROM state_store
49
- WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)
50
- SQL
51
- row ? JSON.parse(row["value"]) : nil
50
+ @mutex.synchronize do
51
+ row = @db.get_first_row(<<~SQL, [key, Time.now.to_f])
52
+ SELECT value FROM state_store
53
+ WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)
54
+ SQL
55
+ row ? JSON.parse(row["value"]) : nil
56
+ end
52
57
  end
53
58
 
54
59
  def set(key, value, ttl: nil)
55
- @db.execute(<<~SQL, [key, JSON.generate(value), ttl ? Time.now.to_f + ttl : nil])
56
- INSERT OR REPLACE INTO state_store (key, value, expires_at)
57
- VALUES (?, ?, ?)
58
- SQL
60
+ @mutex.synchronize do
61
+ @db.execute(<<~SQL, [key, JSON.generate(value), ttl ? Time.now.to_f + ttl : nil])
62
+ INSERT OR REPLACE INTO state_store (key, value, expires_at)
63
+ VALUES (?, ?, ?)
64
+ SQL
65
+ end
59
66
  end
60
67
 
61
68
  def delete(key)
62
- @db.execute("DELETE FROM state_store WHERE key = ?", [key])
69
+ @mutex.synchronize do
70
+ @db.execute("DELETE FROM state_store WHERE key = ?", [key])
71
+ end
63
72
  end
64
73
 
65
74
  def set_if_not_exists(key, value, ttl: nil)
66
- now = Time.now.to_f
67
- expires = ttl ? now + ttl : nil
75
+ @mutex.synchronize do
76
+ now = Time.now.to_f
77
+ expires = ttl ? now + ttl : nil
68
78
 
69
- @db.transaction do
70
79
  row = @db.get_first_row(
71
80
  "SELECT 1 FROM state_store WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)",
72
81
  [key, now]
73
82
  )
74
- next false if row
83
+ return false if row
75
84
 
85
+ # Key doesn't exist or is expired — delete any leftovers, then insert
76
86
  @db.execute("DELETE FROM state_store WHERE key = ?", [key])
77
87
  @db.execute(<<~SQL, [key, JSON.generate(value), expires])
78
88
  INSERT INTO state_store (key, value, expires_at)
79
89
  VALUES (?, ?, ?)
80
90
  SQL
81
- next true
91
+ true
82
92
  end
83
93
  end
84
94
 
85
95
  def clear
86
- @db.execute("DELETE FROM state_store")
96
+ @mutex.synchronize do
97
+ @db.execute("DELETE FROM state_store")
98
+ @db.execute("DELETE FROM locks")
99
+ @db.execute("DELETE FROM queues")
100
+ @db.execute("DELETE FROM lists")
101
+ end
102
+ end
103
+
104
+ def exists?(key)
105
+ @mutex.synchronize do
106
+ row = @db.get_first_row(<<~SQL, [key, Time.now.to_f])
107
+ SELECT 1 FROM state_store
108
+ WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)
109
+ SQL
110
+ !row.nil?
111
+ end
112
+ end
113
+
114
+ def keys(pattern: nil)
115
+ @mutex.synchronize do
116
+ now = Time.now.to_f
117
+ sql, params = if pattern
118
+ like = self.class.glob_to_like(pattern)
119
+ [<<~SQL, [like, now]]
120
+ SELECT key FROM state_store
121
+ WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > ?)
122
+ SQL
123
+ else
124
+ [<<~SQL, [now]]
125
+ SELECT key FROM state_store
126
+ WHERE (expires_at IS NULL OR expires_at > ?)
127
+ SQL
128
+ end
129
+ @db.execute(sql, params).map { |r| r["key"] }
130
+ end
87
131
  end
88
132
 
89
133
  # -- distributed locking --
90
134
 
91
135
  def acquire_lock(key, ttl: 10)
92
- now = Time.now.to_f
93
- expires_at_time = Time.now + ttl
94
- token = SecureRandom.hex(16)
136
+ @mutex.synchronize do
137
+ now = Time.now.to_f
138
+ expires_at_time = Time.now + ttl
139
+ token = SecureRandom.hex(16)
95
140
 
96
- acquired = @db.transaction do
97
141
  row = @db.get_first_row(
98
142
  "SELECT 1 FROM locks WHERE key = ? AND expires_at > ?",
99
143
  [key, now]
100
144
  )
101
- next false if row
145
+ return nil if row
102
146
 
103
147
  @db.execute("DELETE FROM locks WHERE key = ?", [key])
104
148
  @db.execute(<<~SQL, [key, expires_at_time.to_f, token])
105
149
  INSERT INTO locks (key, expires_at, token)
106
150
  VALUES (?, ?, ?)
107
151
  SQL
108
- next true
109
- end
110
152
 
111
- acquired ? Lock.new(id: key, token: token, expires_at: expires_at_time) : nil
153
+ Lock.new(id: key, token: token, expires_at: expires_at_time)
154
+ end
112
155
  end
113
156
 
114
157
  def release_lock(key, lock)
115
- @db.execute(
116
- "DELETE FROM locks WHERE key = ? AND token = ?",
117
- [key, lock.token]
118
- )
119
- @db.changes > 0
158
+ @mutex.synchronize do
159
+ @db.execute(
160
+ "DELETE FROM locks WHERE key = ? AND token = ?",
161
+ [key, lock.token]
162
+ )
163
+ @db.changes > 0
164
+ end
120
165
  end
121
166
 
122
167
  # -- message queues --
123
168
 
124
169
  def enqueue(queue, value)
125
- @db.execute(<<~SQL, [queue, JSON.generate(value), Time.now.iso8601])
126
- INSERT INTO queues (queue_name, value, enqueued_at)
127
- VALUES (?, ?, ?)
128
- SQL
129
- id = @db.last_insert_row_id
130
- QueueEntry.new(id: id.to_s, value: value, enqueued_at: Time.now)
170
+ @mutex.synchronize do
171
+ @db.execute(<<~SQL, [queue, JSON.generate(value), Time.now.iso8601])
172
+ INSERT INTO queues (queue_name, value, enqueued_at)
173
+ VALUES (?, ?, ?)
174
+ SQL
175
+ id = @db.last_insert_row_id
176
+ QueueEntry.new(id: id.to_s, value: value, enqueued_at: Time.now)
177
+ end
131
178
  end
132
179
 
133
180
  def dequeue(queue)
134
- row = @db.get_first_row(<<~SQL, [queue])
135
- DELETE FROM queues
136
- WHERE id = (
137
- SELECT id FROM queues
138
- WHERE queue_name = ?
139
- ORDER BY id ASC
140
- LIMIT 1
141
- )
142
- RETURNING id, value, enqueued_at
143
- SQL
144
- return nil unless row
181
+ @mutex.synchronize do
182
+ row = @db.get_first_row(<<~SQL, [queue])
183
+ DELETE FROM queues
184
+ WHERE id = (
185
+ SELECT id FROM queues
186
+ WHERE queue_name = ?
187
+ ORDER BY id ASC
188
+ LIMIT 1
189
+ )
190
+ RETURNING id, value, enqueued_at
191
+ SQL
192
+ return nil unless row
145
193
 
146
- QueueEntry.new(
147
- id: row["id"].to_s,
148
- value: JSON.parse(row["value"]),
149
- enqueued_at: Time.parse(row["enqueued_at"])
150
- )
194
+ QueueEntry.new(
195
+ id: row["id"].to_s,
196
+ value: JSON.parse(row["value"]),
197
+ enqueued_at: Time.parse(row["enqueued_at"])
198
+ )
199
+ end
151
200
  end
152
201
 
153
202
  def queue_depth(queue)
154
- row = @db.get_first_row(
155
- "SELECT COUNT(*) AS cnt FROM queues WHERE queue_name = ?", [queue]
156
- )
157
- row["cnt"]
203
+ @mutex.synchronize do
204
+ row = @db.get_first_row(
205
+ "SELECT COUNT(*) AS cnt FROM queues WHERE queue_name = ?", [queue]
206
+ )
207
+ row["cnt"]
208
+ end
158
209
  end
159
210
 
160
211
  # -- ordered lists --
161
212
 
162
213
  def list_append(key, value, max_length: nil)
163
- serialized = JSON.generate(value)
214
+ @mutex.synchronize do
215
+ serialized = JSON.generate(value)
164
216
 
165
- @db.execute(<<~SQL, [key, serialized])
166
- INSERT INTO lists (list_key, value)
167
- VALUES (?, ?)
168
- SQL
217
+ @db.execute(<<~SQL, [key, serialized])
218
+ INSERT INTO lists (list_key, value)
219
+ VALUES (?, ?)
220
+ SQL
169
221
 
170
- return unless max_length
222
+ return unless max_length
171
223
 
172
- # Keep only the newest max_length items by deleting those with the
173
- # smallest id values beyond the threshold
174
- row = @db.get_first_row(<<~SQL, [key, max_length])
175
- SELECT MIN(id) AS cutoff FROM (
176
- SELECT id FROM lists
177
- WHERE list_key = ?
178
- ORDER BY id DESC
179
- LIMIT ?
180
- )
181
- SQL
182
- return unless row && row["cutoff"]
224
+ row = @db.get_first_row(<<~SQL, [key, max_length])
225
+ SELECT MIN(id) AS cutoff FROM (
226
+ SELECT id FROM lists
227
+ WHERE list_key = ?
228
+ ORDER BY id DESC
229
+ LIMIT ?
230
+ )
231
+ SQL
232
+ return unless row && row["cutoff"]
183
233
 
184
- @db.execute(<<~SQL, [key, row["cutoff"]])
185
- DELETE FROM lists WHERE list_key = ? AND id < ?
186
- SQL
234
+ @db.execute(<<~SQL, [key, row["cutoff"]])
235
+ DELETE FROM lists WHERE list_key = ? AND id < ?
236
+ SQL
237
+ end
187
238
  end
188
239
 
189
240
  def list_range(key, start = 0, stop = -1)
190
- if stop == -1
191
- rows = @db.execute(<<~SQL, [key, start])
192
- SELECT value FROM lists
193
- WHERE list_key = ?
194
- ORDER BY id ASC
195
- LIMIT -1 OFFSET ?
196
- SQL
197
- else
198
- limit = stop - start + 1
199
- rows = @db.execute(<<~SQL, [key, limit, start])
200
- SELECT value FROM lists
201
- WHERE list_key = ?
202
- ORDER BY id ASC
203
- LIMIT ? OFFSET ?
204
- SQL
241
+ @mutex.synchronize do
242
+ rows = if stop == -1
243
+ @db.execute(<<~SQL, [key, start])
244
+ SELECT value FROM lists
245
+ WHERE list_key = ?
246
+ ORDER BY id ASC
247
+ LIMIT -1 OFFSET ?
248
+ SQL
249
+ else
250
+ limit = stop - start + 1
251
+ @db.execute(<<~SQL, [key, limit, start])
252
+ SELECT value FROM lists
253
+ WHERE list_key = ?
254
+ ORDER BY id ASC
255
+ LIMIT ? OFFSET ?
256
+ SQL
257
+ end
258
+ rows.map { |r| JSON.parse(r["value"]) }
205
259
  end
206
- rows.map { |r| JSON.parse(r["value"]) }
207
260
  end
208
261
 
209
262
  def list_remove(key, value)
210
- serialized = JSON.generate(value)
211
- @db.execute(
212
- "DELETE FROM lists WHERE list_key = ? AND value = ?",
213
- [key, serialized]
214
- )
215
- @db.changes
263
+ @mutex.synchronize do
264
+ serialized = JSON.generate(value)
265
+ @db.execute(
266
+ "DELETE FROM lists WHERE list_key = ? AND value = ?",
267
+ [key, serialized]
268
+ )
269
+ @db.changes
270
+ end
216
271
  end
217
272
 
218
273
  # -- lifecycle --
219
274
 
220
275
  def close
221
- @db.close
276
+ @mutex.synchronize do
277
+ @db.close
278
+ end
222
279
  end
223
280
 
224
281
  private
@@ -3,7 +3,7 @@
3
3
  module Ask
4
4
  module State
5
5
  module Providers
6
- VERSION = "0.2.0"
6
+ VERSION = "0.3.0"
7
7
  end
8
8
  end
9
9
  end
@@ -3,6 +3,9 @@
3
3
  require "ask"
4
4
  require "ask/state"
5
5
 
6
+ # Load the in-memory state backend (default adapter, previously in ask-core)
7
+ require_relative "ask/state/memory"
8
+
6
9
  module Ask
7
10
  module State
8
11
  module Providers
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-state-providers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -79,6 +79,7 @@ files:
79
79
  - LICENSE
80
80
  - README.md
81
81
  - lib/ask-state-providers.rb
82
+ - lib/ask/state/memory.rb
82
83
  - lib/ask/state/providers/mysql.rb
83
84
  - lib/ask/state/providers/postgres.rb
84
85
  - lib/ask/state/providers/redis.rb