bunnyquery 1.0.5 → 1.0.6

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 (2) hide show
  1. package/bunnyquery.js +65 -39
  2. package/package.json +1 -1
package/bunnyquery.js CHANGED
@@ -34,9 +34,12 @@
34
34
 
35
35
  // ---- MCP OAuth constants (mirrors src/code/mcp_oauth.ts) ---------------
36
36
  // The embed has no build-time env, so we always point at the production
37
- // MCP host. Override BunnyQuery.MCP_BASE_URL before init() to swap.
37
+ // MCP host. Override BunnyQuery.MCP_BASE_URL before init() to swap, or
38
+ // pass `true` as the third arg to BunnyQuery.init() to target the dev
39
+ // host (mcp-dev.broadwayinc.computer).
38
40
  const DEFAULTS = {
39
41
  MCP_BASE_URL: 'https://mcp.broadwayinc.computer',
42
+ MCP_DEV_BASE_URL: 'https://mcp-dev.broadwayinc.computer',
40
43
  MCP_NAME: 'BunnyQuery',
41
44
  DEFAULT_CLAUDE_MODEL: 'claude-sonnet-4-6',
42
45
  DEFAULT_OPENAI_MODEL: 'gpt-5.4',
@@ -58,6 +61,11 @@
58
61
  const TOKEN_KEY = STORAGE_PREFIX + 'mcp_token';
59
62
  const STATE_KEY = STORAGE_PREFIX + 'mcp_state';
60
63
 
64
+ // Resolve the active MCP base URL each time it's needed so a runtime
65
+ // override of `BunnyQuery.MCP_BASE_URL` (e.g. via the `dev` flag on
66
+ // init()) is reflected in subsequent send/auth calls.
67
+ const _mcpBaseUrl = () => (global.BunnyQuery && global.BunnyQuery.MCP_BASE_URL) || DEFAULTS.MCP_BASE_URL;
68
+
61
69
  // ----- helpers ----------------------------------------------------------
62
70
  const $ = (tag, attrs, children) => {
63
71
  const el = document.createElement(tag);
@@ -376,7 +384,7 @@
376
384
  {
377
385
  type: 'url',
378
386
  name: DEFAULTS.MCP_NAME,
379
- url: DEFAULTS.MCP_BASE_URL,
387
+ url: _mcpBaseUrl(),
380
388
  authorization_token: '$ACCESS_TOKEN',
381
389
  },
382
390
  ],
@@ -422,7 +430,7 @@
422
430
  {
423
431
  type: 'mcp',
424
432
  server_label: DEFAULTS.MCP_NAME,
425
- server_url: DEFAULTS.MCP_BASE_URL,
433
+ server_url: _mcpBaseUrl(),
426
434
  require_approval: 'never',
427
435
  headers: { Authorization: 'Bearer $ACCESS_TOKEN' },
428
436
  },
@@ -576,7 +584,7 @@
576
584
 
577
585
  this.readOnly = !!(info.opt && info.opt.freeze_database);
578
586
 
579
- this.oauth = new McpOAuth(BunnyQuery.MCP_BASE_URL || DEFAULTS.MCP_BASE_URL);
587
+ this.oauth = new McpOAuth(_mcpBaseUrl());
580
588
 
581
589
  this.messages = [];
582
590
  this.attachments = [];
@@ -585,6 +593,7 @@
585
593
  this.sending = false;
586
594
  this.uploading = false;
587
595
  this.pendingTimer = null;
596
+ this._pollingPending = false;
588
597
  this._loadingOlder = false;
589
598
 
590
599
  this.refs = {};
@@ -593,11 +602,17 @@
593
602
  this._bootstrap();
594
603
  }
595
604
 
596
- static async init(skapi, elementId) {
605
+ static async init(skapi, elementId, dev) {
597
606
  const container = typeof elementId === 'string'
598
607
  ? document.getElementById(elementId)
599
608
  : elementId;
600
609
  if (!container) throw new Error(`BunnyQuery: container "${elementId}" not found`);
610
+ // When `dev` is truthy, route every MCP call (Claude tool URL,
611
+ // OpenAI tool URL, and OAuth) to the dev host. Setting the
612
+ // static here makes the change visible to any subsequent
613
+ // BunnyQuery instance on the page; pass `false` (or omit) to
614
+ // pin back to production.
615
+ BunnyQuery.MCP_BASE_URL = dev ? DEFAULTS.MCP_DEV_BASE_URL : DEFAULTS.MCP_BASE_URL;
601
616
  const info = await skapi.__connection;
602
617
  console.log('[BunnyQuery] initializing with info', info);
603
618
  return new BunnyQuery(skapi, info, container);
@@ -1124,46 +1139,57 @@
1124
1139
  }
1125
1140
  }
1126
1141
 
1142
+ // Polling state machine. Invariant: at most ONE of the following
1143
+ // is true at any moment — (a) `pendingTimer` is armed, or (b)
1144
+ // `_pollingPending` is true (fetch in flight). Any call to
1145
+ // `_schedulePendingPoll` while either is true is a no-op; the
1146
+ // in-flight cycle's `finally` will continue the loop.
1147
+ //
1148
+ // This replaces an earlier clear-and-rearm pattern that, under
1149
+ // overlapping callers (`_sendMessage`, `_loadFirstHistoryPage`,
1150
+ // and the poll's own `finally`), could leave two timers
1151
+ // concurrently armed. Each tick's `finally` then scheduled
1152
+ // another, and the effective polling rate kept compounding —
1153
+ // the chat looked like it was polling faster and faster.
1127
1154
  _schedulePendingPoll() {
1128
- if (this.pendingTimer) clearTimeout(this.pendingTimer);
1129
- if (!this._anyPending()) return;
1130
- // Bail if a poll is already in flight — its `finally` will
1131
- // schedule the next one. Without this guard, any caller that
1132
- // fires while we're awaiting `getChatHistory` (e.g. another
1133
- // _sendMessage, _loadFirstHistoryPage, or our own previous
1134
- // tick whose timer already elapsed) would spawn a second
1135
- // concurrent poller, and each subsequent cycle would compound
1136
- // — making the polling appear faster and faster.
1155
+ if (this.pendingTimer != null) return;
1137
1156
  if (this._pollingPending) return;
1138
- this.pendingTimer = setTimeout(async () => {
1157
+ if (!this._anyPending()) return;
1158
+ this.pendingTimer = setTimeout(() => {
1139
1159
  this.pendingTimer = null;
1140
- this._pollingPending = true;
1141
- try {
1142
- const res = await getChatHistory(
1143
- this.skapi,
1144
- { service: this.serviceId, owner: this.ownerId, platform: this.platform },
1145
- { ascending: false }
1146
- );
1147
- const fresh = mapHistoryToMessages(
1148
- this._filterByClearHorizon((res && res.list) || []),
1149
- this.platform
1150
- );
1151
- // Keep older messages whose ids are not on the freshly
1152
- // fetched first page. The fresh page is appended last so
1153
- // it always reflects the latest server state.
1154
- const freshIds = new Set(fresh.filter((m) => m.id).map((m) => m.id));
1155
- const older = this.messages.filter((m) => m.id && !freshIds.has(m.id));
1156
- this.messages = older.concat(fresh);
1157
- this._renderMessages();
1158
- } catch (err) {
1159
- console.error('[BunnyQuery] pending poll failed', err);
1160
- } finally {
1161
- this._pollingPending = false;
1162
- this._schedulePendingPoll();
1163
- }
1160
+ this._runPendingPoll();
1164
1161
  }, DEFAULTS.PENDING_POLL_INTERVAL_MS);
1165
1162
  }
1166
1163
 
1164
+ async _runPendingPoll() {
1165
+ // Defensive: if somehow re-entered, drop the duplicate.
1166
+ if (this._pollingPending) return;
1167
+ this._pollingPending = true;
1168
+ try {
1169
+ const res = await getChatHistory(
1170
+ this.skapi,
1171
+ { service: this.serviceId, owner: this.ownerId, platform: this.platform },
1172
+ { ascending: false }
1173
+ );
1174
+ const fresh = mapHistoryToMessages(
1175
+ this._filterByClearHorizon((res && res.list) || []),
1176
+ this.platform
1177
+ );
1178
+ // Keep older messages whose ids are not on the freshly
1179
+ // fetched first page. The fresh page is appended last so
1180
+ // it always reflects the latest server state.
1181
+ const freshIds = new Set(fresh.filter((m) => m.id).map((m) => m.id));
1182
+ const older = this.messages.filter((m) => m.id && !freshIds.has(m.id));
1183
+ this.messages = older.concat(fresh);
1184
+ this._renderMessages();
1185
+ } catch (err) {
1186
+ console.error('[BunnyQuery] pending poll failed', err);
1187
+ } finally {
1188
+ this._pollingPending = false;
1189
+ this._schedulePendingPoll();
1190
+ }
1191
+ }
1192
+
1167
1193
  // ---- attachments --------------------------------------------------
1168
1194
  _onAttachFromInput(e) {
1169
1195
  const files = Array.from(e.target.files || []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "BunnyQuery AI chat client",
5
5
  "main": "bunnyquery.js",
6
6
  "files": [