chat_sdk-state-mysql 0.2.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/chat_sdk/state/mysql.rb +199 -0
  3. metadata +71 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6400f4beca077fbd4362accc33b1d2207721972cbc14aaf4dae2eb9f2e22883e
4
+ data.tar.gz: ee821cebf2ec30e940c8d9e64495fe71f28323628eb58a562c25e17eb3a7cc92
5
+ SHA512:
6
+ metadata.gz: 586a519179e2bb027ef69fc1fb19284f8c5001d9676e6c48dafb9482fc0a98e79d1a45e8deae449a0cdefeba38b0ff671a6c293e0f3da8942f77c525be04c342
7
+ data.tar.gz: 582293dd5c2675748260e3d2242fdfb88b620f7d51fcccc999d14fa30a0b20b8713a98c12ebc2e75524964cd4e4ecbfc85605cc5df7ca623d616ed0d50bad206
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chat_sdk"
4
+ require "mysql2"
5
+ require "json"
6
+
7
+ module ChatSDK
8
+ module State
9
+ class Mysql < Base
10
+ def initialize(url: nil, connection: nil, key_prefix: "chat_sdk", auto_migrate: true)
11
+ @conn = connection || connect(url || ENV["MYSQL_URL"] || ENV["DATABASE_URL"] || "mysql2://root@localhost/chat_sdk_dev")
12
+ @key_prefix = key_prefix
13
+ ensure_tables if auto_migrate
14
+ end
15
+
16
+ attr_reader :conn
17
+
18
+ # Subscriptions
19
+
20
+ def subscribe(thread_id)
21
+ stmt = @conn.prepare(
22
+ "INSERT IGNORE INTO chat_sdk_subscriptions (key_prefix, thread_id) VALUES (?, ?)"
23
+ )
24
+ stmt.execute(@key_prefix, thread_id)
25
+ end
26
+
27
+ def unsubscribe(thread_id)
28
+ stmt = @conn.prepare(
29
+ "DELETE FROM chat_sdk_subscriptions WHERE key_prefix = ? AND thread_id = ?"
30
+ )
31
+ stmt.execute(@key_prefix, thread_id)
32
+ end
33
+
34
+ def subscribed?(thread_id)
35
+ stmt = @conn.prepare(
36
+ "SELECT 1 FROM chat_sdk_subscriptions WHERE key_prefix = ? AND thread_id = ?"
37
+ )
38
+ result = stmt.execute(@key_prefix, thread_id)
39
+ result.count > 0
40
+ end
41
+
42
+ # Locks
43
+
44
+ def acquire_lock(key, owner:, ttl:)
45
+ ttl_seconds = ttl.to_f
46
+ del_stmt = @conn.prepare(
47
+ "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND lock_key = ? AND expires_at < NOW()"
48
+ )
49
+ del_stmt.execute(@key_prefix, key)
50
+ ins_stmt = @conn.prepare(
51
+ "INSERT IGNORE INTO chat_sdk_locks (key_prefix, lock_key, owner, expires_at) " \
52
+ "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl_seconds} SECOND))"
53
+ )
54
+ ins_stmt.execute(@key_prefix, key, owner)
55
+ @conn.affected_rows > 0
56
+ end
57
+
58
+ def release_lock(key, owner:)
59
+ stmt = @conn.prepare(
60
+ "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND lock_key = ? AND owner = ?"
61
+ )
62
+ stmt.execute(@key_prefix, key, owner)
63
+ @conn.affected_rows > 0
64
+ end
65
+
66
+ def force_lock(key, owner:, ttl:)
67
+ ttl_seconds = ttl.to_f
68
+ stmt = @conn.prepare(
69
+ "INSERT INTO chat_sdk_locks (key_prefix, lock_key, owner, expires_at) " \
70
+ "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl_seconds} SECOND)) " \
71
+ "ON DUPLICATE KEY UPDATE owner = VALUES(owner), expires_at = VALUES(expires_at)"
72
+ )
73
+ stmt.execute(@key_prefix, key, owner)
74
+ true
75
+ end
76
+
77
+ # Key-value store
78
+
79
+ def get(key)
80
+ stmt = @conn.prepare(
81
+ "SELECT value FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ? AND (expires_at IS NULL OR expires_at > NOW())"
82
+ )
83
+ result = stmt.execute(@key_prefix, key)
84
+ row = result.first
85
+ return JSON.parse(row["value"]) if row
86
+
87
+ nil
88
+ end
89
+
90
+ def set(key, value, ttl: nil)
91
+ serialized = JSON.generate(value)
92
+ stmt = if ttl
93
+ @conn.prepare(
94
+ "INSERT INTO chat_sdk_cache (key_prefix, cache_key, value, expires_at) " \
95
+ "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl.to_f} SECOND)) " \
96
+ "ON DUPLICATE KEY UPDATE value = VALUES(value), expires_at = VALUES(expires_at)"
97
+ )
98
+ else
99
+ @conn.prepare(
100
+ "INSERT INTO chat_sdk_cache (key_prefix, cache_key, value, expires_at) " \
101
+ "VALUES (?, ?, ?, NULL) " \
102
+ "ON DUPLICATE KEY UPDATE value = VALUES(value), expires_at = VALUES(expires_at)"
103
+ )
104
+ end
105
+ stmt.execute(@key_prefix, key, serialized)
106
+ value
107
+ end
108
+
109
+ def delete(key)
110
+ stmt1 = @conn.prepare(
111
+ "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ?"
112
+ )
113
+ stmt1.execute(@key_prefix, key)
114
+ stmt2 = @conn.prepare(
115
+ "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND lock_key = ?"
116
+ )
117
+ stmt2.execute(@key_prefix, key)
118
+ end
119
+
120
+ def set_if_absent(key, value, ttl: nil)
121
+ serialized = JSON.generate(value)
122
+ del_stmt = @conn.prepare(
123
+ "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND cache_key = ? AND expires_at IS NOT NULL AND expires_at < NOW()"
124
+ )
125
+ del_stmt.execute(@key_prefix, key)
126
+ stmt = if ttl
127
+ @conn.prepare(
128
+ "INSERT IGNORE INTO chat_sdk_cache (key_prefix, cache_key, value, expires_at) " \
129
+ "VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL #{ttl.to_f} SECOND))"
130
+ )
131
+ else
132
+ @conn.prepare(
133
+ "INSERT IGNORE INTO chat_sdk_cache (key_prefix, cache_key, value, expires_at) " \
134
+ "VALUES (?, ?, ?, NULL)"
135
+ )
136
+ end
137
+ stmt.execute(@key_prefix, key, serialized)
138
+ @conn.affected_rows > 0
139
+ end
140
+
141
+ # Cleanup
142
+
143
+ def clear
144
+ s1 = @conn.prepare("DELETE FROM chat_sdk_subscriptions WHERE key_prefix = ?")
145
+ s1.execute(@key_prefix)
146
+ s2 = @conn.prepare("DELETE FROM chat_sdk_locks WHERE key_prefix = ?")
147
+ s2.execute(@key_prefix)
148
+ s3 = @conn.prepare("DELETE FROM chat_sdk_cache WHERE key_prefix = ?")
149
+ s3.execute(@key_prefix)
150
+ end
151
+
152
+ def cleanup_expired
153
+ s1 = @conn.prepare(
154
+ "DELETE FROM chat_sdk_cache WHERE key_prefix = ? AND expires_at IS NOT NULL AND expires_at < NOW()"
155
+ )
156
+ s1.execute(@key_prefix)
157
+ s2 = @conn.prepare(
158
+ "DELETE FROM chat_sdk_locks WHERE key_prefix = ? AND expires_at < NOW()"
159
+ )
160
+ s2.execute(@key_prefix)
161
+ end
162
+
163
+ private
164
+
165
+ def connect(url)
166
+ Mysql2::Client.new(url)
167
+ end
168
+
169
+ def ensure_tables
170
+ @conn.query(<<~SQL)
171
+ CREATE TABLE IF NOT EXISTS chat_sdk_subscriptions (
172
+ key_prefix VARCHAR(255) NOT NULL,
173
+ thread_id VARCHAR(512) NOT NULL,
174
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
175
+ PRIMARY KEY (key_prefix, thread_id)
176
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
177
+ SQL
178
+ @conn.query(<<~SQL)
179
+ CREATE TABLE IF NOT EXISTS chat_sdk_locks (
180
+ key_prefix VARCHAR(255) NOT NULL,
181
+ lock_key VARCHAR(512) NOT NULL,
182
+ owner VARCHAR(255) NOT NULL,
183
+ expires_at TIMESTAMP NOT NULL,
184
+ PRIMARY KEY (key_prefix, lock_key)
185
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
186
+ SQL
187
+ @conn.query(<<~SQL)
188
+ CREATE TABLE IF NOT EXISTS chat_sdk_cache (
189
+ key_prefix VARCHAR(255) NOT NULL,
190
+ cache_key VARCHAR(512) NOT NULL,
191
+ value JSON NOT NULL,
192
+ expires_at TIMESTAMP NULL DEFAULT NULL,
193
+ PRIMARY KEY (key_prefix, cache_key)
194
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
195
+ SQL
196
+ end
197
+ end
198
+ end
199
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chat_sdk-state-mysql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Quentin Rousseau
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: chat_sdk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mysql2
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.5'
40
+ description: MySQL-backed conversation state storage for the ChatSDK framework with
41
+ auto-migration and JSON storage
42
+ email:
43
+ - quentin@rootly.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/chat_sdk/state/mysql.rb
49
+ homepage: https://github.com/rootlyhq/chat-sdk
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ rubygems_mfa_required: 'true'
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 4.0.16
69
+ specification_version: 4
70
+ summary: MySQL state backend for ChatSDK
71
+ test_files: []