chat_sdk 1.0.1 → 1.2.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/chat_sdk/adapter/base.rb +33 -5
  3. data/lib/chat_sdk/adapter/capabilities.rb +1 -1
  4. data/lib/chat_sdk/adapter/invocation.rb +18 -0
  5. data/lib/chat_sdk/ai/conversation_scope.rb +44 -0
  6. data/lib/chat_sdk/ai/converter.rb +16 -5
  7. data/lib/chat_sdk/ai/tool_builder.rb +18 -2
  8. data/lib/chat_sdk/ai/tool_executor.rb +64 -10
  9. data/lib/chat_sdk/ai.rb +2 -2
  10. data/lib/chat_sdk/author.rb +13 -2
  11. data/lib/chat_sdk/cards/actions_context.rb +9 -3
  12. data/lib/chat_sdk/cards/builder.rb +38 -1
  13. data/lib/chat_sdk/cards/node.rb +36 -0
  14. data/lib/chat_sdk/cards/renderer.rb +20 -0
  15. data/lib/chat_sdk/channel.rb +8 -0
  16. data/lib/chat_sdk/chat.rb +35 -2
  17. data/lib/chat_sdk/config.rb +48 -1
  18. data/lib/chat_sdk/dispatcher.rb +280 -33
  19. data/lib/chat_sdk/event_registry.rb +7 -2
  20. data/lib/chat_sdk/event_serializer.rb +192 -0
  21. data/lib/chat_sdk/events/action.rb +3 -2
  22. data/lib/chat_sdk/events/installed.rb +19 -0
  23. data/lib/chat_sdk/events/message_deleted.rb +19 -0
  24. data/lib/chat_sdk/events/message_updated.rb +17 -0
  25. data/lib/chat_sdk/events/uninstalled.rb +19 -0
  26. data/lib/chat_sdk/format/mentions.rb +61 -0
  27. data/lib/chat_sdk/history.rb +151 -0
  28. data/lib/chat_sdk/message.rb +8 -2
  29. data/lib/chat_sdk/modals/builder.rb +16 -0
  30. data/lib/chat_sdk/postable_message.rb +3 -2
  31. data/lib/chat_sdk/state/base.rb +16 -0
  32. data/lib/chat_sdk/state/memory.rb +31 -0
  33. data/lib/chat_sdk/streaming/stream.rb +6 -2
  34. data/lib/chat_sdk/testing/fake_adapter.rb +32 -8
  35. data/lib/chat_sdk/thread.rb +35 -6
  36. data/lib/chat_sdk/version.rb +1 -1
  37. data/lib/chat_sdk/webhook/endpoint.rb +3 -0
  38. data/lib/chat_sdk.rb +2 -2
  39. metadata +10 -1
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module ChatSDK
7
+ class EventSerializer
8
+ class << self
9
+ def dump(event)
10
+ data = common_event(event)
11
+ case event.type
12
+ when :mention, :subscribed_message, :direct_message, :message_updated
13
+ data["message"] = dump_message(event.message)
14
+ data["previous_message"] = dump_message(event.previous_message) if event.respond_to?(:previous_message) && event.previous_message
15
+ when :message_deleted
16
+ data.merge!(
17
+ "message_id" => event.message_id,
18
+ "previous_message" => dump_message(event.previous_message),
19
+ "deleted_at" => event.deleted_at&.iso8601
20
+ )
21
+ when :reaction
22
+ data.merge!("emoji" => event.emoji, "user_id" => event.user_id, "message_id" => event.message_id, "added" => event.added?)
23
+ when :action
24
+ data.merge!("action_id" => event.action_id, "value" => event.value, "values" => json_safe(event.values), "user" => dump_author(event.user), "trigger_id" => event.trigger_id)
25
+ when :slash_command
26
+ data.merge!("command" => event.command, "text" => event.text, "user_id" => event.user_id, "trigger_id" => event.trigger_id)
27
+ when :installed, :uninstalled
28
+ data.merge!(
29
+ "installation_id" => event.installation_id,
30
+ "team_id" => event.team_id,
31
+ "tenant_id" => event.tenant_id,
32
+ "conversation_id" => event.conversation_id,
33
+ "action" => event.action,
34
+ "user" => dump_author(event.user)
35
+ )
36
+ end
37
+ data
38
+ end
39
+
40
+ def load(data)
41
+ data = data.transform_keys(&:to_s)
42
+ type = data.fetch("type").to_sym
43
+ common = {
44
+ thread_id: data["thread_id"],
45
+ channel_id: data["channel_id"],
46
+ platform: data["platform"].to_sym,
47
+ adapter_name: data["adapter_name"].to_sym,
48
+ raw: data["raw"],
49
+ timestamp: parse_time(data["timestamp"])
50
+ }
51
+ case type
52
+ when :mention, :subscribed_message, :direct_message
53
+ class_name = {mention: Events::Mention, subscribed_message: Events::SubscribedMessage, direct_message: Events::DirectMessage}.fetch(type)
54
+ class_name.new(message: load_message(data["message"]), **common)
55
+ when :message_updated
56
+ Events::MessageUpdated.new(message: load_message(data["message"]), previous_message: load_message(data["previous_message"]), **common)
57
+ when :message_deleted
58
+ Events::MessageDeleted.new(
59
+ message_id: data["message_id"],
60
+ previous_message: load_message(data["previous_message"]),
61
+ deleted_at: parse_time(data["deleted_at"]),
62
+ **common
63
+ )
64
+ when :reaction
65
+ Events::Reaction.new(emoji: data["emoji"], user_id: data["user_id"], message_id: data["message_id"], added: data["added"], **common)
66
+ when :action
67
+ Events::Action.new(action_id: data["action_id"], value: data["value"], values: data["values"], user: load_author(data["user"]), trigger_id: data["trigger_id"], **common)
68
+ when :slash_command
69
+ common.delete(:thread_id)
70
+ Events::SlashCommand.new(command: data["command"], text: data["text"], user_id: data["user_id"], trigger_id: data["trigger_id"], **common)
71
+ when :installed, :uninstalled
72
+ common.delete(:thread_id)
73
+ common.delete(:channel_id)
74
+ event_class = if type == :installed
75
+ Events::Installed
76
+ else
77
+ Events::Uninstalled
78
+ end
79
+ event_class.new(
80
+ installation_id: data["installation_id"],
81
+ team_id: data["team_id"],
82
+ tenant_id: data["tenant_id"],
83
+ conversation_id: data["conversation_id"],
84
+ action: data["action"],
85
+ user: load_author(data["user"]),
86
+ **common
87
+ )
88
+ else
89
+ raise ArgumentError, "unsupported queued event type: #{type}"
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def common_event(event)
96
+ {
97
+ "type" => event.type.to_s,
98
+ "platform" => event.platform.to_s,
99
+ "adapter_name" => event.adapter_name.to_s,
100
+ "thread_id" => event.respond_to?(:thread_id) ? event.thread_id : nil,
101
+ "channel_id" => event.respond_to?(:channel_id) ? event.channel_id : nil,
102
+ "raw" => json_safe(event.raw),
103
+ "timestamp" => event.timestamp&.iso8601
104
+ }
105
+ end
106
+
107
+ def dump_message(message)
108
+ return unless message
109
+
110
+ {
111
+ "id" => message.id,
112
+ "text" => message.text,
113
+ "author" => dump_author(message.author),
114
+ "thread_id" => message.thread_id,
115
+ "channel_id" => message.channel_id,
116
+ "platform" => message.platform.to_s,
117
+ "attachments" => json_safe(message.attachments),
118
+ "links" => json_safe(message.links),
119
+ "reply_to" => dump_message(message.reply_to),
120
+ "subject" => json_safe(message.subject),
121
+ "raw" => json_safe(message.raw),
122
+ "timestamp" => message.timestamp&.iso8601,
123
+ "is_mention" => message.is_mention
124
+ }
125
+ end
126
+
127
+ def load_message(data)
128
+ return unless data
129
+
130
+ data = data.transform_keys(&:to_s)
131
+ Message.new(
132
+ id: data["id"],
133
+ text: data["text"],
134
+ author: load_author(data["author"]),
135
+ thread_id: data["thread_id"],
136
+ channel_id: data["channel_id"],
137
+ platform: data["platform"].to_sym,
138
+ attachments: data["attachments"] || [],
139
+ links: data["links"] || [],
140
+ reply_to: load_message(data["reply_to"]),
141
+ subject: data["subject"],
142
+ raw: data["raw"],
143
+ timestamp: parse_time(data["timestamp"]),
144
+ is_mention: data["is_mention"]
145
+ )
146
+ end
147
+
148
+ def dump_author(author)
149
+ return unless author
150
+
151
+ {
152
+ "id" => author.id,
153
+ "name" => author.name,
154
+ "platform" => author.platform.to_s,
155
+ "bot" => author.bot?,
156
+ "me" => author.me?,
157
+ "system" => author.system?,
158
+ "locale" => author.locale,
159
+ "email" => author.email,
160
+ "raw" => json_safe(author.raw)
161
+ }
162
+ end
163
+
164
+ def load_author(data)
165
+ return unless data
166
+
167
+ data = data.transform_keys(&:to_s)
168
+ Author.new(
169
+ id: data["id"],
170
+ name: data["name"],
171
+ platform: data["platform"].to_sym,
172
+ bot: data["bot"],
173
+ me: data["me"],
174
+ system: data["system"],
175
+ locale: data["locale"],
176
+ email: data["email"],
177
+ raw: data["raw"]
178
+ )
179
+ end
180
+
181
+ def parse_time(value)
182
+ Time.iso8601(value) if value
183
+ end
184
+
185
+ def json_safe(value)
186
+ JSON.parse(JSON.generate(value))
187
+ rescue JSON::GeneratorError, TypeError
188
+ nil
189
+ end
190
+ end
191
+ end
192
+ end
@@ -3,13 +3,14 @@
3
3
  module ChatSDK
4
4
  module Events
5
5
  class Action < Base
6
- attr_reader :action_id, :value, :user, :thread_id, :channel_id, :trigger_id
6
+ attr_reader :action_id, :value, :values, :user, :thread_id, :channel_id, :trigger_id
7
7
  attr_accessor :thread
8
8
 
9
- def initialize(action_id:, thread_id:, channel_id:, value: nil, user: nil, trigger_id: nil, **kwargs)
9
+ def initialize(action_id:, thread_id:, channel_id:, value: nil, values: nil, user: nil, trigger_id: nil, **kwargs)
10
10
  super(type: :action, **kwargs)
11
11
  @action_id = action_id
12
12
  @value = value
13
+ @values = values
13
14
  @user = user
14
15
  @thread_id = thread_id
15
16
  @channel_id = channel_id
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class Installed < Base
6
+ attr_reader :installation_id, :team_id, :tenant_id, :conversation_id, :action, :user
7
+
8
+ def initialize(conversation_id:, action:, installation_id: nil, team_id: nil, tenant_id: nil, user: nil, **kwargs)
9
+ super(type: :installed, **kwargs)
10
+ @installation_id = installation_id
11
+ @team_id = team_id
12
+ @tenant_id = tenant_id
13
+ @conversation_id = conversation_id
14
+ @action = action
15
+ @user = user
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class MessageDeleted < Base
6
+ attr_reader :message_id, :previous_message, :thread_id, :channel_id, :deleted_at
7
+ attr_accessor :thread
8
+
9
+ def initialize(message_id:, thread_id:, channel_id:, previous_message: nil, deleted_at: nil, **kwargs)
10
+ super(type: :message_deleted, **kwargs)
11
+ @message_id = message_id
12
+ @previous_message = previous_message
13
+ @thread_id = thread_id
14
+ @channel_id = channel_id
15
+ @deleted_at = deleted_at
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class MessageUpdated < Base
6
+ attr_reader :message, :previous_message, :thread_id, :channel_id
7
+
8
+ def initialize(message:, thread_id:, channel_id:, previous_message: nil, **kwargs)
9
+ super(type: :message_updated, **kwargs)
10
+ @message = message
11
+ @previous_message = previous_message
12
+ @thread_id = thread_id
13
+ @channel_id = channel_id
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class Uninstalled < Base
6
+ attr_reader :installation_id, :team_id, :tenant_id, :conversation_id, :action, :user
7
+
8
+ def initialize(conversation_id:, action:, installation_id: nil, team_id: nil, tenant_id: nil, user: nil, **kwargs)
9
+ super(type: :uninstalled, **kwargs)
10
+ @installation_id = installation_id
11
+ @team_id = team_id
12
+ @tenant_id = tenant_id
13
+ @conversation_id = conversation_id
14
+ @action = action
15
+ @user = user
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Format
5
+ module Mentions
6
+ class << self
7
+ def mask_code_spans(text)
8
+ characters = text.to_s.chars
9
+ masked = characters.dup
10
+ opening = nil
11
+ opening_length = nil
12
+ fence = false
13
+ index = 0
14
+
15
+ while index < characters.length
16
+ if characters[index] == "`"
17
+ finish = index + 1
18
+ finish += 1 while finish < characters.length && characters[finish] == "`"
19
+ length = finish - index
20
+
21
+ if opening
22
+ closes = fence ? length >= 3 : length == opening_length
23
+ if closes
24
+ blank_span(masked, opening, finish)
25
+ opening = nil
26
+ opening_length = nil
27
+ fence = false
28
+ else
29
+ blank_span(masked, index, finish)
30
+ end
31
+ else
32
+ opening = index
33
+ opening_length = length
34
+ fence = length >= 3
35
+ blank_span(masked, index, finish)
36
+ end
37
+
38
+ index = finish
39
+ else
40
+ if opening
41
+ masked[index] = " " unless characters[index] == "\n" || characters[index] == "\r"
42
+ end
43
+ index += 1
44
+ end
45
+ end
46
+
47
+ masked[opening..] = characters[opening..] if opening
48
+ masked.join
49
+ end
50
+
51
+ private
52
+
53
+ def blank_span(characters, first, finish)
54
+ (first...finish).each do |index|
55
+ characters[index] = " " unless characters[index] == "\n" || characters[index] == "\r"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "securerandom"
5
+
6
+ module ChatSDK
7
+ class History
8
+ attr_reader :user, :thread, :channel
9
+
10
+ def initialize(chat)
11
+ @user = UserHistory.new(chat)
12
+ @thread = ThreadHistory.new(chat)
13
+ @channel = ChannelHistory.new(chat)
14
+ end
15
+
16
+ class UserHistory
17
+ def initialize(chat)
18
+ @chat = chat
19
+ end
20
+
21
+ def append(thread, message, user_key: nil)
22
+ user_key ||= resolve_user_key(thread, message)
23
+ return nil unless user_key
24
+
25
+ entry = normalize_entry(thread, message, user_key)
26
+ entries = Array(@chat.state.get(storage_key(user_key)))
27
+ entries << entry
28
+ max = @chat.config.history_user[:max_per_user]
29
+ entries = entries.last(max) if max && max != false
30
+ @chat.state.set(storage_key(user_key), entries, ttl: @chat.config.history_user[:retention])
31
+ entry
32
+ end
33
+
34
+ def list(user_key:, limit: 50, platforms: nil, thread_id: nil, roles: nil)
35
+ entries = Array(@chat.state.get(storage_key(user_key)))
36
+ entries = entries.select { |entry| Array(platforms).map(&:to_s).include?(entry["platform"].to_s) } if platforms
37
+ entries = entries.select { |entry| entry["thread_id"] == thread_id } if thread_id
38
+ entries = entries.select { |entry| Array(roles).map(&:to_s).include?(entry["role"].to_s) } if roles
39
+ limit ? entries.last(limit) : entries
40
+ end
41
+
42
+ def count(user_key:)
43
+ Array(@chat.state.get(storage_key(user_key))).length
44
+ end
45
+
46
+ def delete(user_key:)
47
+ count = Array(@chat.state.get(storage_key(user_key))).length
48
+ @chat.state.delete(storage_key(user_key))
49
+ {deleted: count}
50
+ end
51
+
52
+ def to_prompt_entries(entries)
53
+ entries.map { |entry| {role: entry["role"], content: entry["text"]} }
54
+ end
55
+
56
+ private
57
+
58
+ def resolve_user_key(thread, message)
59
+ resolver = @chat.config.history_user[:identity]
60
+ return resolver.call(thread, message) if resolver
61
+ message.author.email if message.is_a?(Message) && message.author&.email
62
+ rescue => e
63
+ ChatSDK::Log.warn("History identity resolver failed: #{e.message}")
64
+ nil
65
+ end
66
+
67
+ def normalize_entry(thread, message, user_key)
68
+ if message.is_a?(Message)
69
+ {
70
+ "id" => SecureRandom.uuid,
71
+ "platform_message_id" => message.id,
72
+ "user_key" => user_key,
73
+ "role" => message.author&.bot? ? "assistant" : "user",
74
+ "text" => message.text.to_s,
75
+ "platform" => message.platform.to_s,
76
+ "thread_id" => thread.id,
77
+ "timestamp" => (message.timestamp || Time.now).iso8601
78
+ }
79
+ else
80
+ data = message.transform_keys(&:to_sym)
81
+ {
82
+ "id" => SecureRandom.uuid,
83
+ "platform_message_id" => data[:platform_message_id] || data[:id],
84
+ "user_key" => user_key,
85
+ "role" => (data[:role] || "assistant").to_s,
86
+ "text" => data[:text].to_s,
87
+ "platform" => (data[:platform] || thread.adapter.name).to_s,
88
+ "thread_id" => thread.id,
89
+ "timestamp" => (data[:timestamp] || Time.now).iso8601
90
+ }
91
+ end
92
+ end
93
+
94
+ def storage_key(user_key)
95
+ "chat_sdk:history:user:#{user_key}"
96
+ end
97
+ end
98
+
99
+ class ThreadHistory
100
+ def initialize(chat)
101
+ @chat = chat
102
+ end
103
+
104
+ def list(thread, cursor: nil, limit: 50)
105
+ unless thread.is_a?(ChatSDK::Thread)
106
+ raise ArgumentError, "thread history requires a ChatSDK::Thread"
107
+ end
108
+
109
+ messages, next_cursor = thread.adapter.fetch_messages(
110
+ channel_id: thread.channel_id,
111
+ thread_id: thread.id,
112
+ cursor: cursor,
113
+ limit: limit
114
+ )
115
+ {messages: messages, next_cursor: next_cursor}
116
+ end
117
+ end
118
+
119
+ class ChannelHistory
120
+ def initialize(chat)
121
+ @chat = chat
122
+ end
123
+
124
+ def list_messages(channel, cursor: nil, limit: 50)
125
+ unless channel.is_a?(ChatSDK::Channel)
126
+ raise ArgumentError, "channel history requires a ChatSDK::Channel"
127
+ end
128
+
129
+ messages, next_cursor = channel.adapter.fetch_channel_messages(
130
+ channel_id: channel.id,
131
+ cursor: cursor,
132
+ limit: limit
133
+ )
134
+ {messages: messages, next_cursor: next_cursor}
135
+ end
136
+
137
+ def list_threads(channel, cursor: nil, limit: 50)
138
+ unless channel.is_a?(ChatSDK::Channel)
139
+ raise ArgumentError, "channel history requires a ChatSDK::Channel"
140
+ end
141
+
142
+ threads, next_cursor = channel.adapter.list_threads(
143
+ channel_id: channel.id,
144
+ cursor: cursor,
145
+ limit: limit
146
+ )
147
+ {threads: threads, next_cursor: next_cursor}
148
+ end
149
+ end
150
+ end
151
+ end
@@ -3,10 +3,12 @@
3
3
  module ChatSDK
4
4
  class Message
5
5
  attr_reader :id, :text, :author, :thread_id, :channel_id,
6
- :platform, :attachments, :raw, :timestamp
6
+ :platform, :attachments, :links, :reply_to, :subject, :raw, :timestamp
7
+ attr_accessor :is_mention
7
8
 
8
9
  def initialize(id:, text:, author:, thread_id:, channel_id:, platform:,
9
- attachments: [], raw: nil, timestamp: nil)
10
+ attachments: [], links: [], reply_to: nil, subject: nil, raw: nil,
11
+ timestamp: nil, is_mention: nil)
10
12
  @id = id
11
13
  @text = text
12
14
  @author = author
@@ -14,8 +16,12 @@ module ChatSDK
14
16
  @channel_id = channel_id
15
17
  @platform = platform
16
18
  @attachments = attachments
19
+ @links = links
20
+ @reply_to = reply_to
21
+ @subject = subject
17
22
  @raw = raw
18
23
  @timestamp = timestamp
24
+ @is_mention = is_mention
19
25
  end
20
26
 
21
27
  def ==(other)
@@ -32,6 +32,22 @@ module ChatSDK
32
32
  @children << ChatSDK::Cards::Node.new(:input, attributes: attrs, children: ctx.nodes)
33
33
  end
34
34
 
35
+ def date_input(id:, label:, initial_value: nil, placeholder: nil, optional: false)
36
+ attrs = {id: id, label: label, input_type: :date, optional: optional}
37
+ attrs[:initial_value] = initial_value if initial_value
38
+ attrs[:placeholder] = placeholder if placeholder
39
+ @children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
40
+ end
41
+
42
+ def number_input(id:, label:, min: nil, max: nil, initial_value: nil, placeholder: nil, decimal: false, optional: false)
43
+ attrs = {id: id, label: label, input_type: :number, decimal: decimal, optional: optional}
44
+ attrs[:min] = min unless min.nil?
45
+ attrs[:max] = max unless max.nil?
46
+ attrs[:initial_value] = initial_value unless initial_value.nil?
47
+ attrs[:placeholder] = placeholder if placeholder
48
+ @children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
49
+ end
50
+
35
51
  def static_text(content)
36
52
  @children << ChatSDK::Cards::Node.new(:text, attributes: {content: content})
37
53
  end
@@ -2,14 +2,15 @@
2
2
 
3
3
  module ChatSDK
4
4
  class PostableMessage
5
- attr_reader :text, :card, :attachments, :metadata
5
+ attr_reader :text, :card, :attachments, :metadata, :ai_generated
6
6
 
7
- def initialize(text: nil, card: nil, attachments: [], metadata: {})
7
+ def initialize(text: nil, card: nil, attachments: [], metadata: {}, ai_generated: false)
8
8
  raise ArgumentError, "text or card required" if text.nil? && card.nil?
9
9
  @text = text
10
10
  @card = card
11
11
  @attachments = attachments
12
12
  @metadata = metadata
13
+ @ai_generated = ai_generated
13
14
  end
14
15
 
15
16
  def card?
@@ -42,6 +42,22 @@ module ChatSDK
42
42
  def set_if_absent(key, value, ttl: nil)
43
43
  raise NotImplementedError
44
44
  end
45
+
46
+ def enqueue(key, value, max_size:, drop: :drop_oldest)
47
+ raise NotImplementedError
48
+ end
49
+
50
+ def drain_queue(key)
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def queue_depth(key)
55
+ raise NotImplementedError
56
+ end
57
+
58
+ def extend_lock(key, owner:, ttl:)
59
+ raise NotImplementedError
60
+ end
45
61
  end
46
62
  end
47
63
  end
@@ -9,6 +9,7 @@ module ChatSDK
9
9
  @expirations = {}
10
10
  @subscriptions = Set.new
11
11
  @locks = {}
12
+ @queues = {}
12
13
  end
13
14
 
14
15
  def subscribe(thread_id)
@@ -47,6 +48,16 @@ module ChatSDK
47
48
  end
48
49
  end
49
50
 
51
+ def extend_lock(key, owner:, ttl:)
52
+ @mutex.synchronize do
53
+ expire_if_needed(key)
54
+ return false unless @locks[key] && @locks[key][:owner] == owner
55
+
56
+ @locks[key][:expires_at] = Time.now.to_f + ttl
57
+ true
58
+ end
59
+ end
60
+
50
61
  def get(key)
51
62
  @mutex.synchronize do
52
63
  expire_if_needed(key)
@@ -80,12 +91,32 @@ module ChatSDK
80
91
  end
81
92
  end
82
93
 
94
+ def enqueue(key, value, max_size:, drop: :drop_oldest)
95
+ @mutex.synchronize do
96
+ queue = (@queues[key] ||= [])
97
+ return queue.length if queue.length >= max_size && drop.to_sym == :drop_newest
98
+
99
+ queue << value
100
+ queue.shift while queue.length > max_size
101
+ queue.length
102
+ end
103
+ end
104
+
105
+ def drain_queue(key)
106
+ @mutex.synchronize { @queues.delete(key) || [] }
107
+ end
108
+
109
+ def queue_depth(key)
110
+ @mutex.synchronize { Array(@queues[key]).length }
111
+ end
112
+
83
113
  def clear
84
114
  @mutex.synchronize do
85
115
  @store.clear
86
116
  @expirations.clear
87
117
  @subscriptions.clear
88
118
  @locks.clear
119
+ @queues.clear
89
120
  end
90
121
  end
91
122
 
@@ -3,6 +3,8 @@
3
3
  module ChatSDK
4
4
  module Streaming
5
5
  class Stream
6
+ include ChatSDK::Adapter::Invocation
7
+
6
8
  attr_reader :adapter, :channel_id, :thread_id, :update_interval
7
9
 
8
10
  def initialize(adapter:, channel_id:, thread_id:, placeholder: nil, update_interval: 0.5)
@@ -50,10 +52,12 @@ module ChatSDK
50
52
  return if @buffer.empty?
51
53
 
52
54
  if @message_id && adapter.supports?(:edit_messages)
53
- adapter.edit_message(
55
+ call_adapter(
56
+ :edit_message,
54
57
  channel_id: channel_id,
55
58
  message_id: @message_id,
56
- message: PostableMessage.new(text: @buffer.dup)
59
+ message: PostableMessage.new(text: @buffer.dup),
60
+ thread_id: thread_id
57
61
  )
58
62
  elsif !@message_id
59
63
  result = adapter.post_message(