Drop fragments and normalize parameters for consistent registry keys.
| 144 | |
| 145 | // Drop fragments and normalize parameters for consistent registry keys. |
| 146 | std::string CanonicalizeHttpUrlKey(const std::string& url) { |
| 147 | if (!(StartsWith(url, "http://") || StartsWith(url, "https://"))) { |
| 148 | return url; |
| 149 | } |
| 150 | // Remove fragment |
| 151 | size_t hashPos = url.find('#'); |
| 152 | std::string noHash = (hashPos == std::string::npos) ? url : url.substr(0, hashPos); |
| 153 | |
| 154 | // Split into origin+path and query |
| 155 | size_t qPos = noHash.find('?'); |
| 156 | std::string originAndPath = (qPos == std::string::npos) ? noHash : noHash.substr(0, qPos); |
| 157 | std::string query = (qPos == std::string::npos) ? std::string() : noHash.substr(qPos + 1); |
| 158 | |
| 159 | // Normalize bridge endpoints to keep a single realm across HMR updates: |
| 160 | // - /ns/rt/<ver> -> /ns/rt |
| 161 | // - /ns/core/<ver> -> /ns/core |
| 162 | size_t schemePos = originAndPath.find("://"); |
| 163 | if (schemePos != std::string::npos) { |
| 164 | size_t pathStart = originAndPath.find('/', schemePos + 3); |
| 165 | if (pathStart != std::string::npos) { |
| 166 | std::string pathOnly = originAndPath.substr(pathStart); |
| 167 | auto normalizeBridge = [&](const char* needle) { |
| 168 | size_t nlen = strlen(needle); |
| 169 | if (pathOnly.size() <= nlen) return false; |
| 170 | if (pathOnly.compare(0, nlen, needle) != 0) return false; |
| 171 | if (pathOnly.size() == nlen) return true; |
| 172 | if (pathOnly[nlen] != '/') return false; |
| 173 | size_t i = nlen + 1; |
| 174 | size_t j = i; |
| 175 | while (j < pathOnly.size() && isdigit((unsigned char)pathOnly[j])) j++; |
| 176 | // Only normalize exact version segment: /ns/*/<digits> (no further segments) |
| 177 | if (j == i) return false; |
| 178 | if (j != pathOnly.size()) return false; |
| 179 | originAndPath = originAndPath.substr(0, pathStart) + std::string(needle); |
| 180 | return true; |
| 181 | }; |
| 182 | if (!normalizeBridge("/ns/rt")) { |
| 183 | normalizeBridge("/ns/core"); |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | if (query.empty()) return originAndPath; |
| 189 | |
| 190 | // Strip ?import markers and sort remaining query params for stability |
| 191 | std::vector<std::string> kept; |
| 192 | size_t start = 0; |
| 193 | while (start <= query.size()) { |
| 194 | size_t amp = query.find('&', start); |
| 195 | std::string pair = (amp == std::string::npos) ? query.substr(start) : query.substr(start, amp - start); |
| 196 | if (!pair.empty()) { |
| 197 | size_t eq = pair.find('='); |
| 198 | std::string name = (eq == std::string::npos) ? pair : pair.substr(0, eq); |
| 199 | if (!(name == "import")) kept.push_back(pair); |
| 200 | } |
| 201 | if (amp == std::string::npos) break; |
| 202 | start = amp + 1; |
| 203 | } |