@atezer/figma-mcp-bridge 1.2.1 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -14,40 +14,20 @@ Bu changelog'a ekleme oncesi surumlerin tam ayrintilari icin `git log` kullanila
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
- ### Bridge (port yonetimi)
17
+ (Yaklasan degisiklikler buraya.)
18
18
 
19
- - **Sabit port stratejisi:** Otomatik port taramasi (5454-5470 sirali deneme) kaldirildi. Bridge artik yapilandirilan porta dogrudan baglanir; port mesgulse HTTP health-check ile canli F-MCP / olu surec / farkli servis ayirt edilir; olu port icin kisa gecikmeli tek retry.
20
- - **Graceful shutdown:** `local-plugin-only.ts`'e SIGINT/SIGTERM handler eklendi -- IDE veya Claude kapandiginda `bridge.stop()` cagrilarak port aninda serbest birakilir (olu port sorununun ana duzeltmesi).
21
- - **probePort edge case:** `FIGMA_BRIDGE_HOST=0.0.0.0` durumunda port probe'u `127.0.0.1` uzerinden yapilir.
19
+ ## [1.2.2] - 2026-04-01
22
20
 
23
- ### Dokumantasyon
24
-
25
- - [docs/MULTI_INSTANCE.md](docs/MULTI_INSTANCE.md): "Tek MCP = tum pencereler ayni oturum" bolumu, **"Paralel gorevler (Claude + Cursor + ikinci hat)"** bolumu (mimari, port tablosu, plugin Advanced uyarisi, Cursor paylasimli MCP notu, audit log cakisma notu).
26
- - [docs/CLAUDE_DESKTOP_CONFIG.md](docs/CLAUDE_DESKTOP_CONFIG.md): coklu `mcpServers` ornegi (5455 + 5470 farkli sunucu adlariyla).
27
- - [KURULUM.md](KURULUM.md): Claude config "sik gorulen hatalar" ozeti.
28
- - [README.md](README.md): Port catismasi uyarisi guncellemesi.
29
-
30
- ### Araclar
31
-
32
- - `npm run check-ports` -- [`scripts/check-ports.sh`](scripts/check-ports.sh): 5454-5470 arasinda LISTEN durumundaki surecleri listeler (paralel gorev dogrulamasi ve sorun giderme icin).
33
-
34
- ### Cursor skills (F-MCP)
35
-
36
- - Yeni skill'ler: `audit-figma-design-system`, `fix-figma-design-system-finding`, `apply-figma-design-system` (tuval ici design system audit/fix/apply; F-MCP Bridge arac eslemesi).
37
- - Mevcut F-MCP skill'lerine karsilikli **F-MCP skill koordinasyonu** bolumleri eklendi.
38
- - Tuval skill'lerinde `figma_get_metadata` kaldirildi; Bridge ile uyum icin `figma_get_file_data` / `figma_get_component` / `figma_get_design_context` eslemesi; **design-drift-detector** koordinasyonunda tipik sira (implement - drift) netlestirildi; **audit** icinde zincir performans notlari.
39
- - [.cursor/skills/f-mcp/SKILL_INDEX.md](.cursor/skills/f-mcp/SKILL_INDEX.md): tum skill'lerin dizini, workspace koku (FCM) notu, ozet akis.
40
- - `npm run validate:fmcp-skills` -- [`scripts/validate-fmcp-skills-tools.mjs`](scripts/validate-fmcp-skills-tools.mjs): skill `.md` icindeki `figma_*` adlarini `src/local.ts`, `src/local-plugin-only.ts`, `src/core/figma-tools.ts` icindeki `registerTool` birlesimne gore dogrular.
41
- - GitHub Actions: [`.github/workflows/ci.yml`](.github/workflows/ci.yml) -- `master` / `main` icin PR ve push'ta `npm run validate:fmcp-skills` zorunlu.
21
+ GitHub Release: [v1.2.2](https://github.com/atezer/FMCP/releases/tag/v1.2.2); govde: [docs/releases/v1.2.2-body.md](docs/releases/v1.2.2-body.md).
42
22
 
43
23
  ### Plugin (F-MCP Bridge)
44
24
 
45
- - Gelismis panel: **Otomatik tara** dugmesi -- port alaniyla tek porta kilitlenmeyi kaldirip 5454-5470 taramasini yeniden baslatir.
46
- - Advanced panel kapatildiginda ayni kilit kalkar (ilk yuklemede cift baglanti tetiklenmez).
25
+ - **Multi-client `fileKey`:** UI WebSocket `onopen` bazen plugin ana parçacığından gelen `FILE_IDENTITY` mesajından önce çalışıyordu; ilk `ready` `fileKey`/`fileName` olmadan gidince köprü (`PluginBridgeServer`) o client’ı `null` anahtarla listeliyordu. `pushBridgeFileIdentity()` eklendi: kimlik geldikten sonra açık soket varsa `ready` yeniden gönderiliyor; `figma_list_connected_files` ve `fileKey` ile yönlendirme tüm pencerelerde (FigJam + birden fazla Figma tarayıcı sekmesi) tutarlı çalışır.
47
26
 
48
- ### Surec (bakimcilar)
27
+ ### Dokumantasyon
49
28
 
50
- - Sonraki surum: `CHANGELOG.md` guncelle - `docs/releases/vX.Y.Z-body.md` olustur - [RELEASE_NOTES_TEMPLATE.md](docs/RELEASE_NOTES_TEMPLATE.md) icindeki `gh release create` / `gh release edit` ile GitHub Release ac veya guncelle.
29
+ - [README.md](README.md): Multi-client bölümünde kimlik zamanlaması notu (1.2.2+).
30
+ - [FUTURE.md](FUTURE.md): Sürüm satırı ve tamamlanan madde özeti.
51
31
 
52
32
  ## [1.2.1] - 2026-04-01
53
33
 
package/README.md CHANGED
@@ -90,7 +90,7 @@ Varsayılan NPM `main` ve `figma-mcp-bridge` komutu **tam mod**dur; plugin ile y
90
90
 
91
91
  | Ne | Nerede |
92
92
  | --- | --- |
93
- | **Sürüm numarası** | [`package.json`](package.json) içindeki `version` (ör. **1.2.1**) |
93
+ | **Sürüm numarası** | [`package.json`](package.json) içindeki `version` (ör. **1.2.2**) |
94
94
  | **Değişiklik özeti** | [CHANGELOG.md](CHANGELOG.md) |
95
95
  | **Yayın bildirimi** | GitHub’da [Releases](https://github.com/atezer/FMCP/releases) — *Watch* → *Custom* → *Releases* ile e-posta bildirimi |
96
96
  | **npm paketi** | [@atezer/figma-mcp-bridge](https://www.npmjs.com/package/@atezer/figma-mcp-bridge) — sürüm geçmişi npm sayfasında |
@@ -110,7 +110,7 @@ Plugin'in **"ready (:5454)"** olması için **önce** MCP bridge sunucusu çalı
110
110
 
111
111
  ### En basit kurulum (NPX — repo indirmeden)
112
112
 
113
- Repo klonlamadan, sadece Node.js ve tek bir config ile kurulum. **NPX güncelleme:** `@latest` bir sonraki çalıştırmada genelde yeni sürümü indirir; `npx` önbelleği eski paketi tutuyorsa `npx clear-npx-cache` (veya belirli sürüm: `@atezer/figma-mcp-bridge@1.2.0`) kullanın. Ayrıntı: [Sürüm ve güncellemeler](#sürüm-ve-güncellemeler).
113
+ Repo klonlamadan, sadece Node.js ve tek bir config ile kurulum. **NPX güncelleme:** `@latest` bir sonraki çalıştırmada genelde yeni sürümü indirir; `npx` önbelleği eski paketi tutuyorsa `npx clear-npx-cache` (veya belirli sürüm: `@atezer/figma-mcp-bridge@1.2.2`) kullanın. Ayrıntı: [Sürüm ve güncellemeler](#sürüm-ve-güncellemeler).
114
114
 
115
115
 
116
116
  | Adım | Yapılacak |
@@ -281,6 +281,8 @@ Hangi **linki** verirseniz, istek o linkteki dosyaya yönlendirilir; diğer penc
281
281
 
282
282
  **Manuel fileKey:** Her plugin bağlantısı kendini `fileKey` ile tanıtır. `figma_list_connected_files` ile bağlı dosyaları listeleyip, diğer tool'larda `fileKey` parametresi ile hedef dosyayı belirtebilirsiniz. `fileKey` ve `figmaUrl` belirtilmezse en son bağlanan dosyaya gider (geriye uyumlu).
283
283
 
284
+ **Kimlik zamanlaması (1.2.2+):** WebSocket bazen ana iş parçacığından gelen dosya kimliğinden önce açılabiliyordu; köprüde bir bağlantı `fileKey: null` görünebiliyordu. Plugin artık `FILE_IDENTITY` geldikten sonra `ready` mesajını yeniden göndererek sunucu kaydını güncelliyor. Çoklu sekme kullanıyorsanız bu sürümdeki `f-mcp-plugin` kaynağını yeniden import edin veya npm paketindeki `f-mcp-plugin` ile hizalayın ([CHANGELOG.md](CHANGELOG.md) **\[1.2.2\]**).
285
+
284
286
  **Kullanım:**
285
287
 
286
288
  1. Birden fazla Figma/FigJam dosyasında (Desktop veya tarayıcı) plugin'i açın → her biri **"ready (:5454)"** gösterir.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CORS allowlist for Cloud Mode + remote MCP (claude.ai, v0, Lovable, etc.).
3
+ */
4
+ const DEFAULT_ALLOWED_ORIGINS = [
5
+ "https://claude.ai",
6
+ "https://www.claude.ai",
7
+ "https://v0.dev",
8
+ "https://www.v0.dev",
9
+ "https://lovable.dev",
10
+ "https://www.lovable.dev",
11
+ ];
12
+ export function getAllowedCorsOrigin(request, extra) {
13
+ const origin = request.headers.get("Origin");
14
+ if (!origin)
15
+ return null;
16
+ const set = new Set([...DEFAULT_ALLOWED_ORIGINS, ...(extra ?? [])]);
17
+ return set.has(origin) ? origin : null;
18
+ }
19
+ export function corsHeaders(request, extraOrigins) {
20
+ const o = getAllowedCorsOrigin(request, extraOrigins);
21
+ const base = {
22
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
23
+ "Access-Control-Allow-Headers": "Content-Type, Accept, Authorization, mcp-session-id, Mcp-Protocol-Version, mcp-protocol-version",
24
+ "Access-Control-Expose-Headers": "mcp-session-id",
25
+ "Access-Control-Max-Age": "86400",
26
+ };
27
+ if (o)
28
+ base["Access-Control-Allow-Origin"] = o;
29
+ else
30
+ base["Access-Control-Allow-Origin"] = "*";
31
+ return base;
32
+ }
33
+ export function withCors(request, response, extraOrigins) {
34
+ const h = new Headers(response.headers);
35
+ const ch = corsHeaders(request, extraOrigins);
36
+ for (const [k, v] of Object.entries(ch)) {
37
+ h.set(k, v);
38
+ }
39
+ return new Response(response.body, { status: response.status, headers: h });
40
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cloud Mode pairing / bind / rate-limit helpers (KV: reuses OAUTH_STATE binding).
3
+ * Key prefixes are reserved to avoid collisions with OAuth state tokens (64+ hex).
4
+ */
5
+ export const FMCP_PAIR_PREFIX = "fmcp_pair:";
6
+ export const FMCP_BIND_PREFIX = "fmcp_bind:";
7
+ export const FMCP_RL_PREFIX = "fmcp_rl:";
8
+ export const PAIRING_TTL_SEC = 300;
9
+ export const BIND_TTL_SEC = 86400;
10
+ export const PAIRING_CODE_LENGTH = 6;
11
+ const CODE_CHARS = "23456789ABCDEFGHJKMNPQRSTVWXYZ";
12
+ export function generatePairingCode() {
13
+ let out = "";
14
+ const arr = new Uint8Array(PAIRING_CODE_LENGTH);
15
+ crypto.getRandomValues(arr);
16
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
17
+ out += CODE_CHARS[arr[i] % CODE_CHARS.length];
18
+ }
19
+ return out;
20
+ }
21
+ export function generatePairingSecret() {
22
+ const a = new Uint8Array(16);
23
+ crypto.getRandomValues(a);
24
+ return Array.from(a, (b) => b.toString(16).padStart(2, "0")).join("");
25
+ }
26
+ export async function putPairing(kv, code, record) {
27
+ await kv.put(`${FMCP_PAIR_PREFIX}${code}`, JSON.stringify(record), {
28
+ expirationTtl: PAIRING_TTL_SEC,
29
+ });
30
+ }
31
+ export async function getPairing(kv, code) {
32
+ const raw = await kv.get(`${FMCP_PAIR_PREFIX}${code}`, "text");
33
+ if (!raw)
34
+ return null;
35
+ try {
36
+ return JSON.parse(raw);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export async function deletePairing(kv, code) {
43
+ await kv.delete(`${FMCP_PAIR_PREFIX}${code}`);
44
+ }
45
+ export async function putBind(kv, mcpSessionId, record) {
46
+ await kv.put(`${FMCP_BIND_PREFIX}${mcpSessionId}`, JSON.stringify(record), {
47
+ expirationTtl: BIND_TTL_SEC,
48
+ });
49
+ }
50
+ export async function getBind(kv, mcpSessionId) {
51
+ const raw = await kv.get(`${FMCP_BIND_PREFIX}${mcpSessionId}`, "text");
52
+ if (!raw)
53
+ return null;
54
+ try {
55
+ return JSON.parse(raw);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ export async function deleteBind(kv, mcpSessionId) {
62
+ await kv.delete(`${FMCP_BIND_PREFIX}${mcpSessionId}`);
63
+ }
64
+ export async function rateLimitAllow(kv, key, limit, windowSec) {
65
+ const now = Date.now();
66
+ const raw = await kv.get(key, "text");
67
+ let stamps = [];
68
+ if (raw) {
69
+ try {
70
+ stamps = JSON.parse(raw).t ?? [];
71
+ }
72
+ catch {
73
+ stamps = [];
74
+ }
75
+ }
76
+ const windowMs = windowSec * 1000;
77
+ stamps = stamps.filter((ts) => now - ts < windowMs);
78
+ if (stamps.length >= limit)
79
+ return false;
80
+ stamps.push(now);
81
+ await kv.put(key, JSON.stringify({ t: stamps }), { expirationTtl: Math.max(windowSec * 2, 120) });
82
+ return true;
83
+ }
84
+ export function clientIp(request) {
85
+ return request.headers.get("CF-Connecting-IP") || request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim() || "unknown";
86
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * HTTP + WebSocket routes for FMCP Cloud Mode (pairing, plugin bridge).
3
+ */
4
+ import { corsHeaders, getAllowedCorsOrigin, withCors } from "./cloud-cors.js";
5
+ import { clientIp, FMCP_RL_PREFIX, generatePairingCode, generatePairingSecret, PAIRING_TTL_SEC, putPairing, rateLimitAllow, getPairing, } from "./cloud-mode-kv.js";
6
+ const PAIRING_HTTP_LIMIT = 20;
7
+ const PAIRING_HTTP_WINDOW_SEC = 60;
8
+ function json(data, status = 200, request) {
9
+ const headers = { "Content-Type": "application/json" };
10
+ if (request)
11
+ Object.assign(headers, corsHeaders(request));
12
+ return new Response(JSON.stringify(data), { status, headers });
13
+ }
14
+ function baseUrl(request) {
15
+ const u = new URL(request.url);
16
+ return `${u.protocol}//${u.host}`;
17
+ }
18
+ export async function handleCloudModeRoutes(request, env) {
19
+ const url = new URL(request.url);
20
+ if (url.pathname === "/fmcp-cloud/plugin") {
21
+ if (request.method === "OPTIONS") {
22
+ return new Response(null, { headers: corsHeaders(request) });
23
+ }
24
+ if (request.headers.get("Upgrade") !== "websocket") {
25
+ return json({ error: "expected_websocket_upgrade" }, 426, request);
26
+ }
27
+ const code = url.searchParams.get("code")?.trim().toUpperCase();
28
+ const secret = url.searchParams.get("secret");
29
+ if (!code || !secret) {
30
+ return json({ error: "missing_code_or_secret" }, 400, request);
31
+ }
32
+ const pair = await getPairing(env.OAUTH_STATE, code);
33
+ if (!pair || pair.secret !== secret) {
34
+ return json({ error: "invalid_or_expired_pairing" }, 401, request);
35
+ }
36
+ const id = env.FMCP_RELAY.idFromName(`pair:${code}`);
37
+ const res = await env.FMCP_RELAY.get(id).fetch(request);
38
+ return withCors(request, res);
39
+ }
40
+ if (url.pathname === "/fmcp-cloud/pairing") {
41
+ if (request.method === "OPTIONS") {
42
+ return new Response(null, { headers: corsHeaders(request) });
43
+ }
44
+ if (request.method !== "POST") {
45
+ return json({ error: "method_not_allowed" }, 405, request);
46
+ }
47
+ const token = env.FMCP_PAIRING_TOKEN;
48
+ if (token) {
49
+ const auth = request.headers.get("Authorization");
50
+ const expected = `Bearer ${token}`;
51
+ if (auth !== expected) {
52
+ return json({ error: "unauthorized" }, 401, request);
53
+ }
54
+ }
55
+ const ip = clientIp(request);
56
+ const rlKey = `${FMCP_RL_PREFIX}pair:${ip}`;
57
+ const ok = await rateLimitAllow(env.OAUTH_STATE, rlKey, PAIRING_HTTP_LIMIT, PAIRING_HTTP_WINDOW_SEC);
58
+ if (!ok) {
59
+ return json({ error: "rate_limited" }, 429, request);
60
+ }
61
+ const code = generatePairingCode();
62
+ const secret = generatePairingSecret();
63
+ const record = { secret, createdAt: Date.now() };
64
+ await putPairing(env.OAUTH_STATE, code, record);
65
+ const origin = baseUrl(request);
66
+ const wsProto = url.protocol === "https:" ? "wss:" : "ws:";
67
+ const wsHost = url.host;
68
+ const pluginWsUrl = `${wsProto}//${wsHost}/fmcp-cloud/plugin?code=${encodeURIComponent(code)}&secret=${encodeURIComponent(secret)}`;
69
+ return json({
70
+ ok: true,
71
+ code,
72
+ secret,
73
+ expiresInSeconds: PAIRING_TTL_SEC,
74
+ pluginWebSocketUrl: pluginWsUrl,
75
+ hint: "Paste code and secret into F-MCP plugin Cloud Mode, or share code + secret with your AI to call fmcp_cloud_bind.",
76
+ }, 200, request);
77
+ }
78
+ if (url.pathname === "/fmcp-cloud/health") {
79
+ return json({ ok: true, cloudMode: true }, 200, request);
80
+ }
81
+ return null;
82
+ }
83
+ /** Merge restrictive CORS onto MCP / SSE responses when Origin matches allowlist. */
84
+ export function maybeTightenMcpCors(request, response) {
85
+ const origin = request.headers.get("Origin");
86
+ if (!origin)
87
+ return response;
88
+ const allowed = getAllowedCorsOrigin(request);
89
+ if (!allowed)
90
+ return response;
91
+ const h = new Headers(response.headers);
92
+ h.set("Access-Control-Allow-Origin", allowed);
93
+ if (!h.has("Access-Control-Expose-Headers")) {
94
+ h.set("Access-Control-Expose-Headers", "mcp-session-id");
95
+ }
96
+ return new Response(response.body, { status: response.status, headers: h });
97
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * FMCP Cloud Mode — per-pairing-code Durable Object.
3
+ * Holds the Figma plugin WebSocket and forwards PluginBridge RPC to the plugin.
4
+ */
5
+ const RPC_TIMEOUT_MS = 120_000;
6
+ export class FmcpRelaySession {
7
+ constructor(ctx, env) {
8
+ this.ctx = ctx;
9
+ this.pending = new Map();
10
+ this.env = env;
11
+ }
12
+ relayNameFromId() {
13
+ return this.ctx.id.toString();
14
+ }
15
+ async fetch(request) {
16
+ const url = new URL(request.url);
17
+ if (request.method === "POST" && url.pathname.endsWith("/disconnect")) {
18
+ for (const s of this.ctx.getWebSockets()) {
19
+ try {
20
+ s.close(1000, "disconnect");
21
+ }
22
+ catch {
23
+ /* ignore */
24
+ }
25
+ }
26
+ return Response.json({ ok: true, closed: true });
27
+ }
28
+ if (request.method === "POST" && url.pathname.endsWith("/rpc")) {
29
+ return this.handleRpc(request);
30
+ }
31
+ if (request.method === "GET" && url.pathname.endsWith("/status")) {
32
+ const sockets = this.ctx.getWebSockets();
33
+ const connected = sockets.length > 0;
34
+ return Response.json({
35
+ ok: true,
36
+ pluginConnected: connected,
37
+ relayId: this.relayNameFromId(),
38
+ });
39
+ }
40
+ if (request.headers.get("Upgrade") !== "websocket") {
41
+ return new Response("Not found", { status: 404 });
42
+ }
43
+ const pair = new WebSocketPair();
44
+ const [client, server] = Object.values(pair);
45
+ this.ctx.acceptWebSocket(server);
46
+ return new Response(null, { status: 101, webSocket: client });
47
+ }
48
+ async handleRpc(request) {
49
+ let body;
50
+ try {
51
+ body = (await request.json());
52
+ }
53
+ catch {
54
+ return Response.json({ ok: false, error: "invalid_json" }, { status: 400 });
55
+ }
56
+ const method = body.method;
57
+ if (!method || typeof method !== "string") {
58
+ return Response.json({ ok: false, error: "missing_method" }, { status: 400 });
59
+ }
60
+ const sockets = this.ctx.getWebSockets();
61
+ if (!sockets || sockets.length === 0) {
62
+ return Response.json({ ok: false, error: "plugin_not_connected" }, { status: 503 });
63
+ }
64
+ const ws = sockets[0];
65
+ const id = `req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
66
+ const payload = JSON.stringify({
67
+ id,
68
+ method,
69
+ params: body.params ?? {},
70
+ });
71
+ try {
72
+ const result = await new Promise((resolve, reject) => {
73
+ const timeout = setTimeout(() => {
74
+ this.pending.delete(id);
75
+ reject(new Error(`Plugin bridge request '${method}' timed out after ${RPC_TIMEOUT_MS}ms`));
76
+ }, RPC_TIMEOUT_MS);
77
+ this.pending.set(id, { resolve, reject, timeout });
78
+ ws.send(payload);
79
+ });
80
+ return Response.json({ ok: true, result });
81
+ }
82
+ catch (e) {
83
+ const msg = e instanceof Error ? e.message : String(e);
84
+ return Response.json({ ok: false, error: msg }, { status: 500 });
85
+ }
86
+ }
87
+ async webSocketMessage(ws, message) {
88
+ const text = typeof message === "string" ? message : new TextDecoder().decode(message);
89
+ let msg;
90
+ try {
91
+ msg = JSON.parse(text);
92
+ }
93
+ catch {
94
+ return;
95
+ }
96
+ const t = msg.type;
97
+ if (t === "pong" || t === "keepalive")
98
+ return;
99
+ if (t === "ready") {
100
+ try {
101
+ ws.send(JSON.stringify({
102
+ type: "welcome",
103
+ bridgeVersion: "cloud-1.0.0",
104
+ port: 0,
105
+ clientId: `cloud_${Date.now()}`,
106
+ multiClient: false,
107
+ }));
108
+ }
109
+ catch {
110
+ /* ignore */
111
+ }
112
+ return;
113
+ }
114
+ const mid = msg.id;
115
+ if (mid && this.pending.has(mid)) {
116
+ const p = this.pending.get(mid);
117
+ this.pending.delete(mid);
118
+ clearTimeout(p.timeout);
119
+ if (msg.error) {
120
+ p.reject(new Error(String(msg.error)));
121
+ }
122
+ else {
123
+ p.resolve(msg.result);
124
+ }
125
+ }
126
+ }
127
+ async webSocketClose(_ws, _code, _reason, _wasClean) {
128
+ for (const [rid, p] of this.pending) {
129
+ clearTimeout(p.timeout);
130
+ p.reject(new Error("Plugin disconnected"));
131
+ this.pending.delete(rid);
132
+ }
133
+ }
134
+ async webSocketError(_ws, _error) {
135
+ for (const [rid, p] of this.pending) {
136
+ clearTimeout(p.timeout);
137
+ p.reject(new Error("Plugin WebSocket error"));
138
+ this.pending.delete(rid);
139
+ }
140
+ }
141
+ }