@openclaw/twitch 2026.5.22 → 2026.5.24-beta.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.
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as twitchPlugin } from "./plugin-Bhtgv1oH.js";
1
+ import { t as twitchPlugin } from "./plugin-BWX0hpqA.js";
2
2
  import { n as setTwitchRuntime } from "./runtime-B5If4b0g.js";
3
3
  export { setTwitchRuntime, twitchPlugin };
@@ -1,2 +1,2 @@
1
- import { t as twitchPlugin } from "./plugin-Bhtgv1oH.js";
1
+ import { t as twitchPlugin } from "./plugin-BWX0hpqA.js";
2
2
  export { twitchPlugin };
@@ -1,4 +1,4 @@
1
- import { n as stripMarkdownForTwitch, r as getOrCreateClientManager } from "./plugin-Bhtgv1oH.js";
1
+ import { n as stripMarkdownForTwitch, r as getOrCreateClientManager } from "./plugin-BWX0hpqA.js";
2
2
  import { t as getTwitchRuntime } from "./runtime-B5If4b0g.js";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -22,7 +22,10 @@ var TwitchClientManager = class {
22
22
  constructor(logger) {
23
23
  this.logger = logger;
24
24
  this.clients = /* @__PURE__ */ new Map();
25
+ this.pendingClients = /* @__PURE__ */ new Map();
26
+ this.connectionPromises = /* @__PURE__ */ new Map();
25
27
  this.messageHandlers = /* @__PURE__ */ new Map();
28
+ this.messageHandlerTokens = /* @__PURE__ */ new Map();
26
29
  }
27
30
  /**
28
31
  * Create an auth provider for the account.
@@ -34,16 +37,17 @@ var TwitchClientManager = class {
34
37
  clientId: account.clientId,
35
38
  clientSecret: account.clientSecret
36
39
  });
37
- await authProvider.addUserForToken({
38
- accessToken: normalizedToken,
39
- refreshToken: account.refreshToken ?? null,
40
- expiresIn: account.expiresIn ?? null,
41
- obtainmentTimestamp: account.obtainmentTimestamp ?? Date.now()
42
- }, TWITCH_CHAT_AUTH_INTENTS).then((userId) => {
40
+ try {
41
+ const userId = await authProvider.addUserForToken({
42
+ accessToken: normalizedToken,
43
+ refreshToken: account.refreshToken ?? null,
44
+ expiresIn: account.expiresIn ?? null,
45
+ obtainmentTimestamp: account.obtainmentTimestamp ?? Date.now()
46
+ }, TWITCH_CHAT_AUTH_INTENTS);
43
47
  this.logger.info(`Added user ${userId} to RefreshingAuthProvider for ${account.username}`);
44
- }).catch((err) => {
45
- this.logger.error(`Failed to add user to RefreshingAuthProvider: ${formatErrorMessage(err)}`);
46
- });
48
+ } catch (err) {
49
+ throw new Error(`Failed to add user to RefreshingAuthProvider: ${formatErrorMessage(err)}`, { cause: err });
50
+ }
47
51
  authProvider.onRefresh((userId, token) => {
48
52
  this.logger.info(`Access token refreshed for user ${userId} (expires in ${token.expiresIn ? `${token.expiresIn}s` : "unknown"})`);
49
53
  });
@@ -64,6 +68,17 @@ var TwitchClientManager = class {
64
68
  const key = this.getAccountKey(account);
65
69
  const existing = this.clients.get(key);
66
70
  if (existing) return existing;
71
+ const pending = this.connectionPromises.get(key);
72
+ if (pending) return pending;
73
+ const connection = this.createConnectedClient(key, account, cfg, accountId);
74
+ this.connectionPromises.set(key, connection);
75
+ try {
76
+ return await connection;
77
+ } finally {
78
+ if (this.connectionPromises.get(key) === connection) this.connectionPromises.delete(key);
79
+ }
80
+ }
81
+ async createConnectedClient(key, account, cfg, accountId) {
67
82
  const tokenResolution = resolveTwitchToken(cfg, { accountId });
68
83
  if (!tokenResolution.token) {
69
84
  this.logger.error(`Missing Twitch token for account ${account.username} (set channels.twitch.accounts.${account.username}.token or OPENCLAW_TWITCH_ACCESS_TOKEN for default)`);
@@ -107,11 +122,62 @@ var TwitchClientManager = class {
107
122
  }
108
123
  });
109
124
  this.setupClientHandlers(client, account);
110
- client.connect();
125
+ this.pendingClients.set(key, client);
126
+ try {
127
+ await this.connectClient(client, account);
128
+ if (this.pendingClients.get(key) !== client) {
129
+ client.quit();
130
+ throw new Error(`Twitch connection cancelled for ${account.username}`);
131
+ }
132
+ this.pendingClients.delete(key);
133
+ } catch (error) {
134
+ if (this.pendingClients.get(key) === client) this.pendingClients.delete(key);
135
+ throw error;
136
+ }
111
137
  this.clients.set(key, client);
112
138
  this.logger.info(`Connected to Twitch as ${account.username}`);
113
139
  return client;
114
140
  }
141
+ async connectClient(client, account) {
142
+ const connectTimeoutMs = 15e3;
143
+ await new Promise((resolve, reject) => {
144
+ let settled = false;
145
+ let authRetryPending = false;
146
+ const listeners = [];
147
+ let timeout;
148
+ const finish = (error) => {
149
+ if (settled) return;
150
+ settled = true;
151
+ if (timeout) clearTimeout(timeout);
152
+ for (const listener of listeners) listener.unbind();
153
+ if (error) {
154
+ try {
155
+ client.quit();
156
+ } catch {}
157
+ reject(error);
158
+ return;
159
+ }
160
+ resolve();
161
+ };
162
+ listeners.push(client.onAuthenticationSuccess(() => finish()), client.onAuthenticationFailure((text) => {
163
+ authRetryPending = true;
164
+ this.logger.warn(`Twitch authentication failed for ${account.username}; waiting for retry, disconnect, or timeout: ${text}`);
165
+ }), client.onDisconnect((manual, reason) => {
166
+ if (authRetryPending && !manual) {
167
+ this.logger.debug?.(`Twitch disconnected during auth retry for ${account.username}: ${formatErrorMessage(reason)}`);
168
+ return;
169
+ }
170
+ finish(reason ?? /* @__PURE__ */ new Error(manual ? `Twitch connection cancelled for ${account.username}` : `Twitch disconnected before ready for ${account.username}`));
171
+ }));
172
+ timeout = setTimeout(() => finish(/* @__PURE__ */ new Error(`Timed out connecting to Twitch as ${account.username}`)), connectTimeoutMs);
173
+ timeout.unref?.();
174
+ try {
175
+ client.connect();
176
+ } catch (error) {
177
+ finish(error instanceof Error ? error : new Error(String(error)));
178
+ }
179
+ });
180
+ }
115
181
  /**
116
182
  * Set up message and event handlers for a client
117
183
  */
@@ -148,21 +214,37 @@ var TwitchClientManager = class {
148
214
  */
149
215
  onMessage(account, handler) {
150
216
  const key = this.getAccountKey(account);
217
+ const token = Symbol(key);
151
218
  this.messageHandlers.set(key, handler);
219
+ this.messageHandlerTokens.set(key, token);
152
220
  return () => {
153
- this.messageHandlers.delete(key);
221
+ if (this.messageHandlerTokens.get(key) === token) {
222
+ this.messageHandlers.delete(key);
223
+ this.messageHandlerTokens.delete(key);
224
+ }
154
225
  };
155
226
  }
227
+ clearMessageHandler(key) {
228
+ this.messageHandlers.delete(key);
229
+ this.messageHandlerTokens.delete(key);
230
+ }
156
231
  /**
157
232
  * Disconnect a client
158
233
  */
159
234
  async disconnect(account) {
160
235
  const key = this.getAccountKey(account);
161
236
  const client = this.clients.get(key);
237
+ const pendingClient = this.pendingClients.get(key);
238
+ if (pendingClient) {
239
+ pendingClient.quit();
240
+ this.pendingClients.delete(key);
241
+ this.connectionPromises.delete(key);
242
+ this.clearMessageHandler(key);
243
+ }
162
244
  if (client) {
163
245
  client.quit();
164
246
  this.clients.delete(key);
165
- this.messageHandlers.delete(key);
247
+ this.clearMessageHandler(key);
166
248
  this.logger.info(`Disconnected ${key}`);
167
249
  }
168
250
  }
@@ -170,9 +252,13 @@ var TwitchClientManager = class {
170
252
  * Disconnect all clients
171
253
  */
172
254
  async disconnectAll() {
255
+ this.pendingClients.forEach((client) => client.quit());
173
256
  this.clients.forEach((client) => client.quit());
257
+ this.pendingClients.clear();
258
+ this.connectionPromises.clear();
174
259
  this.clients.clear();
175
260
  this.messageHandlers.clear();
261
+ this.messageHandlerTokens.clear();
176
262
  this.logger.info(" Disconnected all clients");
177
263
  }
178
264
  /**
@@ -206,6 +292,8 @@ var TwitchClientManager = class {
206
292
  */
207
293
  clearForTest() {
208
294
  this.clients.clear();
295
+ this.pendingClients.clear();
296
+ this.connectionPromises.clear();
209
297
  this.messageHandlers.clear();
210
298
  }
211
299
  };
@@ -260,9 +348,12 @@ function getClientManager(accountId) {
260
348
  async function removeClientManager(accountId) {
261
349
  const entry = registry.get(accountId);
262
350
  if (!entry) return;
263
- await entry.manager.disconnectAll();
264
- registry.delete(accountId);
265
- entry.logger.info(`Unregistered client manager for account: ${accountId}`);
351
+ try {
352
+ await entry.manager.disconnectAll();
353
+ } finally {
354
+ registry.delete(accountId);
355
+ entry.logger.info(`Unregistered client manager for account: ${accountId}`);
356
+ }
266
357
  }
267
358
  //#endregion
268
359
  //#region extensions/twitch/src/utils/markdown.ts
@@ -1257,7 +1348,7 @@ const twitchPlugin = createChatChannelPlugin({
1257
1348
  await runStoppablePassiveMonitor({
1258
1349
  abortSignal: ctx.abortSignal,
1259
1350
  start: async () => {
1260
- const { monitorTwitchProvider } = await import("./monitor-Bf1feqSo.js");
1351
+ const { monitorTwitchProvider } = await import("./monitor-ClcKuSAz.js");
1261
1352
  return monitorTwitchProvider({
1262
1353
  account,
1263
1354
  accountId,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/twitch",
3
- "version": "2026.5.22",
3
+ "version": "2026.5.24-beta.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/twitch",
9
- "version": "2026.5.22",
9
+ "version": "2026.5.24-beta.1",
10
10
  "dependencies": {
11
11
  "@twurple/api": "8.1.4",
12
12
  "@twurple/auth": "8.1.4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/twitch",
3
- "version": "2026.5.22",
3
+ "version": "2026.5.24-beta.1",
4
4
  "description": "OpenClaw Twitch channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,10 +24,10 @@
24
24
  "minHostVersion": ">=2026.4.10"
25
25
  },
26
26
  "compat": {
27
- "pluginApi": ">=2026.5.22"
27
+ "pluginApi": ">=2026.5.24-beta.1"
28
28
  },
29
29
  "build": {
30
- "openclawVersion": "2026.5.22"
30
+ "openclawVersion": "2026.5.24-beta.1"
31
31
  },
32
32
  "channel": {
33
33
  "id": "twitch",
@@ -55,7 +55,7 @@
55
55
  "README.md"
56
56
  ],
57
57
  "peerDependencies": {
58
- "openclaw": ">=2026.5.22"
58
+ "openclaw": ">=2026.5.24-beta.1"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "openclaw": {