From 461a3de983e7c70ea5fc72d1338361bfdbddbb29 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Mon, 13 Apr 2026 16:31:46 -0400 Subject: [PATCH 1/8] fix #1750 --- src/commands/compile.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/compile.ts b/src/commands/compile.ts index e7af69e1..56c4e98a 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -283,11 +283,12 @@ function updateStorage(content: string[], storage: string[]): string[] { let contentString = content.join("\n"); contentString = contentString // update existing Storages - .replaceAll(/\n(\s*storage\s+(\w+)\s*{\s*)([^}]*?)(\s*})/gim, (_match, beforeXML, name, _oldXML, afterXML) => { - const newXML = storageMap.get(name); + .replaceAll(/\n(\s*storage\s+(\w+)\s*{\s*)(.*?)(>\s*})/gis, (_match, beforeXML, name, _oldXML, afterXML) => { + let newXML = storageMap.get(name); if (newXML === undefined) { return ""; } + newXML = newXML.slice(0, newXML.length - 1); storageMap.delete(name); return "\n" + beforeXML + newXML + afterXML; }); From 7db3d01c1203476302524af2f7fe69e4c6de2de3 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Tue, 8 Sep 2026 13:49:04 -0400 Subject: [PATCH 2/8] draft --- src/api/index.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 53a80938..b04178a9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -328,13 +328,26 @@ export class AtelierAPI { private async request( minVersion: number, - method: string, + method: "GET" | "HEAD" | "PUT" | "POST" | "DELETE", path?: string, body?: any, params?: any, headers?: any, - options?: any + options?: { + /** Abort the request if it hasn't completed within this many milliseconds. */ + timeout?: number; + /** Suppress writing this request/response to the ObjectScript output channel, even when `objectscript.outputRESTTraffic` is on. */ + noOutput?: boolean; + /** + * On a 401 or network error, suppress the automatic retry/self-heal handling (a single retry + * with fresh credentials, or scheduling a connection check), leaving the error to the caller. + */ + checkingConnection?: boolean; + /** On a 401 response, suppress the automatic single retry with fresh credentials. */ + _retriedAfter401?: boolean; + } ): Promise { + const effectiveCheckingConnection = options?.checkingConnection ?? checkingConnection; const { active, apiVersion, host, port, https } = this.config; if (!active || !port || !host) { return Promise.reject(); @@ -365,7 +378,6 @@ export class AtelierAPI { }); return result.length ? "?" + result.join("&") : ""; }; - method = method.toUpperCase(); if (body && !headers["Content-Type"]) { headers["Content-Type"] = "application/json"; } @@ -465,7 +477,7 @@ export class AtelierAPI { if (response.status === 401) { authRequestMap.delete(mapKey); cookiesMap.delete(mapKey); - if (this.wsOrFile && !checkingConnection) { + if (this.wsOrFile && !effectiveCheckingConnection) { if (!options?._retriedAfter401) { return this.request(minVersion, method, originalPath, body, params, headers, { ...options, @@ -596,7 +608,7 @@ export class AtelierAPI { panel.tooltip = "Disconnected"; workspaceState.update(this.configName.toLowerCase() + ":host", undefined); workspaceState.update(this.configName.toLowerCase() + ":port", undefined); - if (!checkingConnection) { + if (!effectiveCheckingConnection) { setTimeout(() => checkConnection(false, undefined, true), 30000); } } @@ -605,7 +617,10 @@ export class AtelierAPI { } public serverInfo(checkNs = true, timeout?: number): Promise>> { - return this.request(0, "GET", undefined, undefined, undefined, undefined, { timeout }).then((info) => { + return this.request(0, "GET", undefined, undefined, undefined, undefined, { + timeout, + checkingConnection: false, + }).then((info) => { if (info && info.result && info.result.content && info.result.content.api > 0) { const data = info.result.content; const apiVersion = data.api; From 0b3408e6fb85c13ba2f5a4506fcacd08f04e44b0 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Tue, 8 Sep 2026 15:00:36 -0400 Subject: [PATCH 3/8] Rename checkConnection to ensureConnection, tighten request() types, drop global checkingConnection - checkConnection -> ensureConnection: the function verifies AND interactively repairs a connection (may prompt for credentials, mutates persisted state), which "check" undersells. Added a doc comment. Renamed the misleading inActivate param to withTimeout (same boolean/behavior, clearer name). - AtelierAPI.request(): method is now a literal union instead of string, options is a documented inline type instead of any. Added a per-request checkingConnection option so request()'s 401/network-error self-heal guard is decided by the specific call, not ambient global state. - Removed the module-level checkingConnection boolean entirely. It served two unrelated purposes: a reentrancy guard for ensureConnection (now a connectionsBeingChecked Set keyed by configName, which also fixes a bug where checking one workspace folder's connection silently no-opped checks for other folders) and the signal read by request() (now purely the per-request option, no ambient fallback). --- src/api/index.ts | 12 +++++----- src/commands/serverActions.ts | 4 ++-- src/extension.ts | 41 +++++++++++++++++++++-------------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index b04178a9..be17d4fb 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,9 +8,8 @@ import BasicAuthorization, { extensionContext, workspaceState, panel, - checkConnection, + ensureConnection, schemas, - checkingConnection, inactiveServerIds, } from "../extension"; import { currentWorkspaceFolder, outputChannel, outputConsole } from "../utils"; @@ -347,7 +346,6 @@ export class AtelierAPI { _retriedAfter401?: boolean; } ): Promise { - const effectiveCheckingConnection = options?.checkingConnection ?? checkingConnection; const { active, apiVersion, host, port, https } = this.config; if (!active || !port || !host) { return Promise.reject(); @@ -477,7 +475,7 @@ export class AtelierAPI { if (response.status === 401) { authRequestMap.delete(mapKey); cookiesMap.delete(mapKey); - if (this.wsOrFile && !effectiveCheckingConnection) { + if (this.wsOrFile && !options?.checkingConnection) { if (!options?._retriedAfter401) { return this.request(minVersion, method, originalPath, body, params, headers, { ...options, @@ -485,7 +483,7 @@ export class AtelierAPI { }); } setTimeout(() => { - checkConnection( + ensureConnection( this.config.auth.resolved(), typeof this.wsOrFile === "object" ? this.wsOrFile : undefined, true @@ -608,8 +606,8 @@ export class AtelierAPI { panel.tooltip = "Disconnected"; workspaceState.update(this.configName.toLowerCase() + ":host", undefined); workspaceState.update(this.configName.toLowerCase() + ":port", undefined); - if (!effectiveCheckingConnection) { - setTimeout(() => checkConnection(false, undefined, true), 30000); + if (!options?.checkingConnection) { + setTimeout(() => ensureConnection(false, undefined, true), 30000); } } throw error; diff --git a/src/commands/serverActions.ts b/src/commands/serverActions.ts index 23176c50..aeec30b1 100644 --- a/src/commands/serverActions.ts +++ b/src/commands/serverActions.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import { config, workspaceState, - checkConnection, + ensureConnection, explorerProvider, filesystemSchemas, FILESYSTEM_SCHEMA, @@ -72,7 +72,7 @@ export async function serverActions(): Promise { return connConfig.update("conn", { ...targetConfig, active: !active }, target); } case "refreshConnection": { - await checkConnection(true, undefined, true); + await ensureConnection(true, undefined, true); break; } case "switchNamespace": { diff --git a/src/extension.ts b/src/extension.ts index f3496d8b..9d303846 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -215,8 +215,6 @@ export function config(setting?: string, workspaceFolderName?: string): any { let reporter: TelemetryReporter | undefined; -export let checkingConnection = false; - export let serverManagerApi: serverManager.ServerManagerAPI; type ConnSpec = serverManager.IServerSpec & { @@ -375,18 +373,29 @@ export function getResolvedConnectionSpec( /** The `api.serverId`s of all servers that are known to be inactive */ export const inactiveServerIds: Set = new Set(); -export async function checkConnection( +/** `configName`s for which an `ensureConnection` call is currently in progress */ +const connectionsBeingChecked: Set = new Set(); + +/** + * Verify `uri`'s connection works, repairing it if not (may show a modal credential prompt). + * Reflects the outcome in the status bar, `objectscript.conn.active`, and `workspaceState`. + * No-ops if a call is already in progress. + * @param clearState Discard cached connection details first, forcing fresh resolution. + * @param triggerRefreshes Refresh the explorer/projects views once settled. + * @param withTimeout Time out the check so it can't hang extension activation. + */ +export async function ensureConnection( clearState = false, uri?: vscode.Uri, triggerRefreshes?: boolean, - inActivate = false + withTimeout = false ): Promise { - // Do nothing if already checking the connection - if (checkingConnection) { + const { apiTarget, configName } = connectionTarget(uri); + // Do nothing if already checking this connection + if (connectionsBeingChecked.has(configName)) { return; } - const { apiTarget, configName } = connectionTarget(uri); const wsKey = configName.toLowerCase(); if (clearState) { // clean-up cached values @@ -474,7 +483,7 @@ export async function checkConnection( if (!api.externalServer) await setConnectionState(configName, false); return; } - checkingConnection = true; + connectionsBeingChecked.add(configName); const username = auth.username || "UnknownUser"; const identity = username.startsWith("*") ? `using ${username.slice(1, -1)}` : `as user \`${username}\``; @@ -496,7 +505,7 @@ export async function checkConnection( // Do the check // Only time out requests when called from activate() // Timeout is needed in that case to prevent extension activation from hanging - const serverInfoTimeout = inActivate ? 5000 : undefined; + const serverInfoTimeout = withTimeout ? 5000 : undefined; return api .serverInfo(true, serverInfoTimeout) .then(gotServerInfo) @@ -539,7 +548,7 @@ export async function checkConnection( success = false; }) .finally(() => { - checkingConnection = false; + connectionsBeingChecked.delete(configName); }); } } else { @@ -573,7 +582,7 @@ export async function checkConnection( return false; }) .finally(() => { - checkingConnection = false; + connectionsBeingChecked.delete(configName); }) ); } else { @@ -600,7 +609,7 @@ export async function checkConnection( if (!api.externalServer) await setConnectionState(configName, false); }) .finally(() => { - checkingConnection = false; + connectionsBeingChecked.delete(configName); if (triggerRefreshes) { setTimeout(() => { explorerProvider.refresh(); @@ -948,7 +957,7 @@ export async function activate(context: vscode.ExtensionContext): Promise Date: Tue, 8 Sep 2026 15:05:49 -0400 Subject: [PATCH 4/8] Fix ensureConnection's own probe being denied the 401 self-heal retry AtelierAPI's retryAfter401 constructor flag (used only by ensureConnection, which always passed false) left wsOrFile unset, which made request()'s 401/network-error retry-and-self-heal guard always false for ensureConnection's own serverInfo() call - regardless of any other condition. So a merely-stale session (credentials fine, cookie expired) skipped straight to the manual credential prompt, and dismissing it disabled a working connection. Removed retryAfter401 entirely; wsOrFile is now always recorded. The scheduled follow-up ensureConnection calls this unlocks are safe: a redundant one just no-ops via the connectionsBeingChecked reentrancy guard if the original call is still in progress. This also let a per-request checkingConnection option (added earlier this session to work around the symptom) be removed again - the actual fix makes it unnecessary. --- src/api/index.ts | 22 +++++----------------- src/extension.ts | 6 +++--- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index be17d4fb..2687537e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -120,10 +120,8 @@ export class AtelierAPI { return filename; } - public constructor(wsOrFile?: string | vscode.Uri, retryAfter401 = true) { - if (retryAfter401) { - this.wsOrFile = wsOrFile; - } + public constructor(wsOrFile?: string | vscode.Uri) { + this.wsOrFile = wsOrFile; let workspaceFolderName = ""; let namespace = ""; if (wsOrFile) { @@ -337,11 +335,6 @@ export class AtelierAPI { timeout?: number; /** Suppress writing this request/response to the ObjectScript output channel, even when `objectscript.outputRESTTraffic` is on. */ noOutput?: boolean; - /** - * On a 401 or network error, suppress the automatic retry/self-heal handling (a single retry - * with fresh credentials, or scheduling a connection check), leaving the error to the caller. - */ - checkingConnection?: boolean; /** On a 401 response, suppress the automatic single retry with fresh credentials. */ _retriedAfter401?: boolean; } @@ -475,7 +468,7 @@ export class AtelierAPI { if (response.status === 401) { authRequestMap.delete(mapKey); cookiesMap.delete(mapKey); - if (this.wsOrFile && !options?.checkingConnection) { + if (this.wsOrFile) { if (!options?._retriedAfter401) { return this.request(minVersion, method, originalPath, body, params, headers, { ...options, @@ -606,19 +599,14 @@ export class AtelierAPI { panel.tooltip = "Disconnected"; workspaceState.update(this.configName.toLowerCase() + ":host", undefined); workspaceState.update(this.configName.toLowerCase() + ":port", undefined); - if (!options?.checkingConnection) { - setTimeout(() => ensureConnection(false, undefined, true), 30000); - } + setTimeout(() => ensureConnection(false, undefined, true), 30000); } throw error; } } public serverInfo(checkNs = true, timeout?: number): Promise>> { - return this.request(0, "GET", undefined, undefined, undefined, undefined, { - timeout, - checkingConnection: false, - }).then((info) => { + return this.request(0, "GET", undefined, undefined, undefined, undefined, { timeout }).then((info) => { if (info && info.result && info.result.content && info.result.content.api > 0) { const data = info.result.content; const apiVersion = data.api; diff --git a/src/extension.ts b/src/extension.ts index 9d303846..1e651596 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -408,7 +408,7 @@ export async function ensureConnection( await workspaceState.update(wsKey + ":docker", undefined); _onDidChangeConnection.fire(); } - let api = new AtelierAPI(apiTarget, false); + let api = new AtelierAPI(apiTarget); const { active, host = "", port = 0, superserverPort = 0, ns = "", auth } = api.config; vscode.commands.executeCommand("setContext", "vscode-objectscript.connectActive", active); if (!panel.text) { @@ -472,7 +472,7 @@ export async function ensureConnection( if (api.externalServer) { inactiveServerIds.delete(api.serverId); } - api = new AtelierAPI(apiTarget, false); + api = new AtelierAPI(apiTarget); if (!api.config.host || !api.config.port || !api.config.ns) { const message = "'host', 'port' and 'ns' must be specified."; @@ -533,7 +533,7 @@ export async function ensureConnection( if (newSpec) { // We were able to resolve credentials, so try again await workspaceState.update(wsKey + ":password", newSpec.auth?.accessToken); - api = new AtelierAPI(apiTarget, false); + api = new AtelierAPI(apiTarget); await api .serverInfo(true, serverInfoTimeout) .then(async (info) => { From bf558531732c8c6dc11b2c72f76f09d85674ad45 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Tue, 8 Sep 2026 16:05:25 -0400 Subject: [PATCH 5/8] Remove redundant connectionsBeingChecked.delete calls, keep one cleanup site --- src/extension.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 1e651596..fbe28505 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -546,9 +546,6 @@ export async function ensureConnection( if (error?.statusCode != 401) errorMessage = undefined; await workspaceState.update(wsKey + ":password", undefined); success = false; - }) - .finally(() => { - connectionsBeingChecked.delete(configName); }); } } else { @@ -581,9 +578,6 @@ export async function ensureConnection( await workspaceState.update(wsKey + ":password", undefined); return false; }) - .finally(() => { - connectionsBeingChecked.delete(configName); - }) ); } else { inactiveServerIds.add(api.serverId); From 62c10c3c124350cf7cff07e9bdfa215e11086839 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 9 Sep 2026 10:38:31 -0400 Subject: [PATCH 6/8] rn --- src/extension.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index fbe28505..9937da01 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -374,7 +374,7 @@ export function getResolvedConnectionSpec( export const inactiveServerIds: Set = new Set(); /** `configName`s for which an `ensureConnection` call is currently in progress */ -const connectionsBeingChecked: Set = new Set(); +const checkingConnection: Set = new Set(); /** * Verify `uri`'s connection works, repairing it if not (may show a modal credential prompt). @@ -392,7 +392,7 @@ export async function ensureConnection( ): Promise { const { apiTarget, configName } = connectionTarget(uri); // Do nothing if already checking this connection - if (connectionsBeingChecked.has(configName)) { + if (checkingConnection.has(configName)) { return; } @@ -483,7 +483,7 @@ export async function ensureConnection( if (!api.externalServer) await setConnectionState(configName, false); return; } - connectionsBeingChecked.add(configName); + checkingConnection.add(configName); const username = auth.username || "UnknownUser"; const identity = username.startsWith("*") ? `using ${username.slice(1, -1)}` : `as user \`${username}\``; @@ -603,7 +603,7 @@ export async function ensureConnection( if (!api.externalServer) await setConnectionState(configName, false); }) .finally(() => { - connectionsBeingChecked.delete(configName); + checkingConnection.delete(configName); if (triggerRefreshes) { setTimeout(() => { explorerProvider.refresh(); From 4d3f5a94cdb07786f2734a3dc5d3d4a8a68295dd Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 9 Sep 2026 10:44:10 -0400 Subject: [PATCH 7/8] update --- src/extension.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 9937da01..b61a9809 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -374,7 +374,7 @@ export function getResolvedConnectionSpec( export const inactiveServerIds: Set = new Set(); /** `configName`s for which an `ensureConnection` call is currently in progress */ -const checkingConnection: Set = new Set(); +const ensuringConnection: Set = new Set(); /** * Verify `uri`'s connection works, repairing it if not (may show a modal credential prompt). @@ -392,7 +392,7 @@ export async function ensureConnection( ): Promise { const { apiTarget, configName } = connectionTarget(uri); // Do nothing if already checking this connection - if (checkingConnection.has(configName)) { + if (ensuringConnection.has(configName)) { return; } @@ -483,7 +483,7 @@ export async function ensureConnection( if (!api.externalServer) await setConnectionState(configName, false); return; } - checkingConnection.add(configName); + ensuringConnection.add(configName); const username = auth.username || "UnknownUser"; const identity = username.startsWith("*") ? `using ${username.slice(1, -1)}` : `as user \`${username}\``; @@ -603,7 +603,7 @@ export async function ensureConnection( if (!api.externalServer) await setConnectionState(configName, false); }) .finally(() => { - checkingConnection.delete(configName); + ensuringConnection.delete(configName); if (triggerRefreshes) { setTimeout(() => { explorerProvider.refresh(); From 1592922e145bf7698280410ac5a836b38f84b404 Mon Sep 17 00:00:00 2001 From: "Kuang-Chen (KC) Lu" Date: Wed, 9 Sep 2026 11:31:08 -0400 Subject: [PATCH 8/8] Fix stale checkConnection reference in comment --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index b61a9809..fc4ad561 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1601,7 +1601,7 @@ export async function activate(context: vscode.ExtensionContext): Promise