chat 4.0.1 → 4.1.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.
package/dist/index.js CHANGED
@@ -163,22 +163,97 @@ var BaseFormatConverter = class {
163
163
  }
164
164
  };
165
165
 
166
+ // src/types.ts
167
+ var ConsoleLogger = class _ConsoleLogger {
168
+ constructor(level = "info", prefix = "chat-sdk") {
169
+ this.level = level;
170
+ this.prefix = prefix;
171
+ }
172
+ prefix;
173
+ shouldLog(level) {
174
+ const levels = ["debug", "info", "warn", "error", "silent"];
175
+ return levels.indexOf(level) >= levels.indexOf(this.level);
176
+ }
177
+ child(prefix) {
178
+ return new _ConsoleLogger(this.level, `${this.prefix}:${prefix}`);
179
+ }
180
+ // eslint-disable-next-line no-console
181
+ debug(message, ...args) {
182
+ if (this.shouldLog("debug"))
183
+ console.debug(`[${this.prefix}] ${message}`, ...args);
184
+ }
185
+ // eslint-disable-next-line no-console
186
+ info(message, ...args) {
187
+ if (this.shouldLog("info"))
188
+ console.info(`[${this.prefix}] ${message}`, ...args);
189
+ }
190
+ // eslint-disable-next-line no-console
191
+ warn(message, ...args) {
192
+ if (this.shouldLog("warn"))
193
+ console.warn(`[${this.prefix}] ${message}`, ...args);
194
+ }
195
+ // eslint-disable-next-line no-console
196
+ error(message, ...args) {
197
+ if (this.shouldLog("error"))
198
+ console.error(`[${this.prefix}] ${message}`, ...args);
199
+ }
200
+ };
201
+ var THREAD_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
202
+ var ChatError = class extends Error {
203
+ constructor(message, code, cause) {
204
+ super(message);
205
+ this.code = code;
206
+ this.cause = cause;
207
+ this.name = "ChatError";
208
+ }
209
+ };
210
+ var RateLimitError = class extends ChatError {
211
+ constructor(message, retryAfterMs, cause) {
212
+ super(message, "RATE_LIMITED", cause);
213
+ this.retryAfterMs = retryAfterMs;
214
+ this.name = "RateLimitError";
215
+ }
216
+ };
217
+ var LockError = class extends ChatError {
218
+ constructor(message, cause) {
219
+ super(message, "LOCK_FAILED", cause);
220
+ this.name = "LockError";
221
+ }
222
+ };
223
+ var NotImplementedError = class extends ChatError {
224
+ constructor(message, feature, cause) {
225
+ super(message, "NOT_IMPLEMENTED", cause);
226
+ this.feature = feature;
227
+ this.name = "NotImplementedError";
228
+ }
229
+ };
230
+
166
231
  // src/thread.ts
232
+ var THREAD_STATE_KEY_PREFIX = "thread-state:";
233
+ function isAsyncIterable(value) {
234
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
235
+ }
167
236
  var ThreadImpl = class {
168
237
  id;
169
238
  adapter;
170
239
  channelId;
171
240
  isDM;
172
- state;
241
+ _stateAdapter;
173
242
  _recentMessages = [];
174
243
  _isSubscribedContext;
244
+ /** Current message context for streaming - provides userId/teamId */
245
+ _currentMessage;
246
+ /** Update interval for fallback streaming */
247
+ _streamingUpdateIntervalMs;
175
248
  constructor(config) {
176
249
  this.id = config.id;
177
250
  this.adapter = config.adapter;
178
251
  this.channelId = config.channelId;
179
252
  this.isDM = config.isDM ?? false;
180
- this.state = config.state;
253
+ this._stateAdapter = config.stateAdapter;
181
254
  this._isSubscribedContext = config.isSubscribedContext ?? false;
255
+ this._currentMessage = config.currentMessage;
256
+ this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
182
257
  if (config.initialMessage) {
183
258
  this._recentMessages = [config.initialMessage];
184
259
  }
@@ -189,6 +264,29 @@ var ThreadImpl = class {
189
264
  set recentMessages(messages) {
190
265
  this._recentMessages = messages;
191
266
  }
267
+ /**
268
+ * Get the current thread state.
269
+ * Returns null if no state has been set.
270
+ */
271
+ get state() {
272
+ return this._stateAdapter.get(
273
+ `${THREAD_STATE_KEY_PREFIX}${this.id}`
274
+ );
275
+ }
276
+ /**
277
+ * Set the thread state. Merges with existing state by default.
278
+ * State is persisted for 30 days.
279
+ */
280
+ async setState(newState, options) {
281
+ const key = `${THREAD_STATE_KEY_PREFIX}${this.id}`;
282
+ if (options?.replace) {
283
+ await this._stateAdapter.set(key, newState, THREAD_STATE_TTL_MS);
284
+ } else {
285
+ const existing = await this._stateAdapter.get(key);
286
+ const merged = { ...existing, ...newState };
287
+ await this._stateAdapter.set(key, merged, THREAD_STATE_TTL_MS);
288
+ }
289
+ }
192
290
  get allMessages() {
193
291
  const adapter = this.adapter;
194
292
  const threadId = this.id;
@@ -220,18 +318,21 @@ var ThreadImpl = class {
220
318
  if (this._isSubscribedContext) {
221
319
  return true;
222
320
  }
223
- return this.state.isSubscribed(this.id);
321
+ return this._stateAdapter.isSubscribed(this.id);
224
322
  }
225
323
  async subscribe() {
226
- await this.state.subscribe(this.id);
324
+ await this._stateAdapter.subscribe(this.id);
227
325
  if (this.adapter.onThreadSubscribe) {
228
326
  await this.adapter.onThreadSubscribe(this.id);
229
327
  }
230
328
  }
231
329
  async unsubscribe() {
232
- await this.state.unsubscribe(this.id);
330
+ await this._stateAdapter.unsubscribe(this.id);
233
331
  }
234
332
  async post(message) {
333
+ if (isAsyncIterable(message)) {
334
+ return this.handleStream(message);
335
+ }
235
336
  let postable = message;
236
337
  if (isJSX(message)) {
237
338
  const card = toCardElement(message);
@@ -243,9 +344,93 @@ var ThreadImpl = class {
243
344
  const rawMessage = await this.adapter.postMessage(this.id, postable);
244
345
  return this.createSentMessage(rawMessage.id, postable);
245
346
  }
347
+ /**
348
+ * Handle streaming from an AsyncIterable.
349
+ * Uses adapter's native streaming if available, otherwise falls back to post+edit.
350
+ */
351
+ async handleStream(textStream) {
352
+ const options = {};
353
+ if (this._currentMessage) {
354
+ options.recipientUserId = this._currentMessage.author.userId;
355
+ const raw = this._currentMessage.raw;
356
+ options.recipientTeamId = raw?.team_id ?? raw?.team;
357
+ }
358
+ if (this.adapter.stream) {
359
+ let accumulated = "";
360
+ const wrappedStream = {
361
+ [Symbol.asyncIterator]: () => {
362
+ const iterator = textStream[Symbol.asyncIterator]();
363
+ return {
364
+ async next() {
365
+ const result = await iterator.next();
366
+ if (!result.done) {
367
+ accumulated += result.value;
368
+ }
369
+ return result;
370
+ }
371
+ };
372
+ }
373
+ };
374
+ const raw = await this.adapter.stream(this.id, wrappedStream, options);
375
+ return this.createSentMessage(raw.id, accumulated);
376
+ }
377
+ return this.fallbackStream(textStream, options);
378
+ }
246
379
  async startTyping() {
247
380
  await this.adapter.startTyping(this.id);
248
381
  }
382
+ /**
383
+ * Fallback streaming implementation using post + edit.
384
+ * Used when adapter doesn't support native streaming.
385
+ * Uses recursive setTimeout to send updates every intervalMs (default 500ms).
386
+ * Schedules next update only after current edit completes to avoid overwhelming slow services.
387
+ */
388
+ async fallbackStream(textStream, options) {
389
+ const intervalMs = options?.updateIntervalMs ?? this._streamingUpdateIntervalMs;
390
+ const msg = await this.adapter.postMessage(this.id, "...");
391
+ let accumulated = "";
392
+ let lastEditContent = "...";
393
+ let stopped = false;
394
+ let pendingEdit = null;
395
+ let timerId = null;
396
+ const doEditAndReschedule = async () => {
397
+ if (stopped) return;
398
+ if (accumulated !== lastEditContent) {
399
+ const content = accumulated;
400
+ try {
401
+ await this.adapter.editMessage(this.id, msg.id, content);
402
+ lastEditContent = content;
403
+ } catch {
404
+ }
405
+ }
406
+ if (!stopped) {
407
+ timerId = setTimeout(() => {
408
+ pendingEdit = doEditAndReschedule();
409
+ }, intervalMs);
410
+ }
411
+ };
412
+ timerId = setTimeout(() => {
413
+ pendingEdit = doEditAndReschedule();
414
+ }, intervalMs);
415
+ try {
416
+ for await (const chunk of textStream) {
417
+ accumulated += chunk;
418
+ }
419
+ } finally {
420
+ stopped = true;
421
+ if (timerId) {
422
+ clearTimeout(timerId);
423
+ timerId = null;
424
+ }
425
+ }
426
+ if (pendingEdit) {
427
+ await pendingEdit;
428
+ }
429
+ if (accumulated !== lastEditContent) {
430
+ await this.adapter.editMessage(this.id, msg.id, accumulated);
431
+ }
432
+ return this.createSentMessage(msg.id, accumulated);
433
+ }
249
434
  async refresh() {
250
435
  const messages = await this.adapter.fetchMessages(this.id, { limit: 50 });
251
436
  this._recentMessages = messages;
@@ -351,78 +536,15 @@ function extractMessageContent(message) {
351
536
  throw new Error("Invalid PostableMessage format");
352
537
  }
353
538
 
354
- // src/types.ts
355
- var ConsoleLogger = class _ConsoleLogger {
356
- constructor(level = "info", prefix = "chat-sdk") {
357
- this.level = level;
358
- this.prefix = prefix;
359
- }
360
- prefix;
361
- shouldLog(level) {
362
- const levels = ["debug", "info", "warn", "error", "silent"];
363
- return levels.indexOf(level) >= levels.indexOf(this.level);
364
- }
365
- child(prefix) {
366
- return new _ConsoleLogger(this.level, `${this.prefix}:${prefix}`);
367
- }
368
- // eslint-disable-next-line no-console
369
- debug(message, ...args) {
370
- if (this.shouldLog("debug"))
371
- console.debug(`[${this.prefix}] ${message}`, ...args);
372
- }
373
- // eslint-disable-next-line no-console
374
- info(message, ...args) {
375
- if (this.shouldLog("info"))
376
- console.info(`[${this.prefix}] ${message}`, ...args);
377
- }
378
- // eslint-disable-next-line no-console
379
- warn(message, ...args) {
380
- if (this.shouldLog("warn"))
381
- console.warn(`[${this.prefix}] ${message}`, ...args);
382
- }
383
- // eslint-disable-next-line no-console
384
- error(message, ...args) {
385
- if (this.shouldLog("error"))
386
- console.error(`[${this.prefix}] ${message}`, ...args);
387
- }
388
- };
389
- var ChatError = class extends Error {
390
- constructor(message, code, cause) {
391
- super(message);
392
- this.code = code;
393
- this.cause = cause;
394
- this.name = "ChatError";
395
- }
396
- };
397
- var RateLimitError = class extends ChatError {
398
- constructor(message, retryAfterMs, cause) {
399
- super(message, "RATE_LIMITED", cause);
400
- this.retryAfterMs = retryAfterMs;
401
- this.name = "RateLimitError";
402
- }
403
- };
404
- var LockError = class extends ChatError {
405
- constructor(message, cause) {
406
- super(message, "LOCK_FAILED", cause);
407
- this.name = "LockError";
408
- }
409
- };
410
- var NotImplementedError = class extends ChatError {
411
- constructor(message, feature, cause) {
412
- super(message, "NOT_IMPLEMENTED", cause);
413
- this.feature = feature;
414
- this.name = "NotImplementedError";
415
- }
416
- };
417
-
418
539
  // src/chat.ts
419
540
  var DEFAULT_LOCK_TTL_MS = 3e4;
420
541
  var DEDUPE_TTL_MS = 6e4;
421
542
  var Chat = class {
422
543
  adapters;
423
- state;
544
+ _stateAdapter;
424
545
  userName;
425
546
  logger;
547
+ _streamingUpdateIntervalMs;
426
548
  mentionHandlers = [];
427
549
  messagePatterns = [];
428
550
  subscribedMessageHandlers = [];
@@ -439,8 +561,9 @@ var Chat = class {
439
561
  webhooks;
440
562
  constructor(config) {
441
563
  this.userName = config.userName;
442
- this.state = config.state;
564
+ this._stateAdapter = config.state;
443
565
  this.adapters = /* @__PURE__ */ new Map();
566
+ this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500;
444
567
  if (!config.logger) {
445
568
  this.logger = new ConsoleLogger("info");
446
569
  } else if (typeof config.logger === "string") {
@@ -485,7 +608,7 @@ var Chat = class {
485
608
  }
486
609
  async doInitialize() {
487
610
  this.logger.info("Initializing chat instance...");
488
- await this.state.connect();
611
+ await this._stateAdapter.connect();
489
612
  this.logger.debug("State connected");
490
613
  const initPromises = Array.from(this.adapters.values()).map(
491
614
  async (adapter) => {
@@ -506,7 +629,7 @@ var Chat = class {
506
629
  */
507
630
  async shutdown() {
508
631
  this.logger.info("Shutting down chat instance...");
509
- await this.state.disconnect();
632
+ await this._stateAdapter.disconnect();
510
633
  this.initialized = false;
511
634
  this.initPromise = null;
512
635
  this.logger.info("Chat instance shut down");
@@ -682,7 +805,7 @@ var Chat = class {
682
805
  });
683
806
  return;
684
807
  }
685
- const isSubscribed = await this.state.isSubscribed(event.threadId);
808
+ const isSubscribed = await this._stateAdapter.isSubscribed(event.threadId);
686
809
  const thread = await this.createThread(
687
810
  event.adapter,
688
811
  event.threadId,
@@ -734,7 +857,7 @@ var Chat = class {
734
857
  this.logger.error("Reaction event missing adapter");
735
858
  return;
736
859
  }
737
- const isSubscribed = await this.state.isSubscribed(event.threadId);
860
+ const isSubscribed = await this._stateAdapter.isSubscribed(event.threadId);
738
861
  const thread = await this.createThread(
739
862
  event.adapter,
740
863
  event.threadId,
@@ -776,7 +899,7 @@ var Chat = class {
776
899
  }
777
900
  }
778
901
  getState() {
779
- return this.state;
902
+ return this._stateAdapter;
780
903
  }
781
904
  getUserName() {
782
905
  return this.userName;
@@ -876,7 +999,7 @@ var Chat = class {
876
999
  return;
877
1000
  }
878
1001
  const dedupeKey = `dedupe:${adapter.name}:${message.id}`;
879
- const alreadyProcessed = await this.state.get(dedupeKey);
1002
+ const alreadyProcessed = await this._stateAdapter.get(dedupeKey);
880
1003
  if (alreadyProcessed) {
881
1004
  this.logger.debug("Skipping duplicate message", {
882
1005
  adapter: adapter.name,
@@ -884,8 +1007,11 @@ var Chat = class {
884
1007
  });
885
1008
  return;
886
1009
  }
887
- await this.state.set(dedupeKey, true, DEDUPE_TTL_MS);
888
- const lock = await this.state.acquireLock(threadId, DEFAULT_LOCK_TTL_MS);
1010
+ await this._stateAdapter.set(dedupeKey, true, DEDUPE_TTL_MS);
1011
+ const lock = await this._stateAdapter.acquireLock(
1012
+ threadId,
1013
+ DEFAULT_LOCK_TTL_MS
1014
+ );
889
1015
  if (!lock) {
890
1016
  this.logger.warn("Could not acquire lock on thread", { threadId });
891
1017
  throw new LockError(
@@ -895,7 +1021,7 @@ var Chat = class {
895
1021
  this.logger.debug("Lock acquired", { threadId, token: lock.token });
896
1022
  try {
897
1023
  message.isMention = this.detectMention(adapter, message);
898
- const isSubscribed = await this.state.isSubscribed(threadId);
1024
+ const isSubscribed = await this._stateAdapter.isSubscribed(threadId);
899
1025
  this.logger.debug("Subscription check", {
900
1026
  threadId,
901
1027
  isSubscribed,
@@ -951,7 +1077,7 @@ var Chat = class {
951
1077
  });
952
1078
  }
953
1079
  } finally {
954
- await this.state.releaseLock(lock);
1080
+ await this._stateAdapter.releaseLock(lock);
955
1081
  this.logger.debug("Lock released", { threadId });
956
1082
  }
957
1083
  }
@@ -963,10 +1089,12 @@ var Chat = class {
963
1089
  id: threadId,
964
1090
  adapter,
965
1091
  channelId,
966
- state: this.state,
1092
+ stateAdapter: this._stateAdapter,
967
1093
  initialMessage,
968
1094
  isSubscribedContext,
969
- isDM
1095
+ isDM,
1096
+ currentMessage: initialMessage,
1097
+ streamingUpdateIntervalMs: this._streamingUpdateIntervalMs
970
1098
  });
971
1099
  }
972
1100
  /**
@@ -1405,6 +1533,7 @@ export {
1405
1533
  NotImplementedError,
1406
1534
  RateLimitError,
1407
1535
  Section2 as Section,
1536
+ THREAD_STATE_TTL_MS,
1408
1537
  blockquote,
1409
1538
  codeBlock,
1410
1539
  convertEmojiPlaceholders,