Skip to content

Commit 7563ee4

Browse files
fix(Sky): Add defensive error handling to SkyBridge IPC handlers
Wrap all Tauri listener handlers, tree-item mapping, TreeView.refresh(), and webview registration calls in try/catch to prevent one bad handler from crashing the listener loop. This mirrors VS Code's Emitter pattern where each subscriber's call is isolated. Also add SSR safety checks for `window` being undefined during Astro's pre-render pass in both GetWorkbench() and GetServices(). The existing early-return contract is preserved - callers continue to use `if (!Services) return;`. Specifically handles: Tauri listen() registration failures (window closing mid-boot), tree-item serialization errors (missing label/handle), TreeView.refresh() synchronous throws (older xterm/tree shims), WebviewViewService.register duplicate viewId (hot-reload), and CustomEvent dispatch failures. Each failure is now isolated and logged rather than propagating and breaking unrelated listeners. This improves SkyBridge stability during startup, view transitions, and extension host reloads.
1 parent 876ea91 commit 7563ee4

1 file changed

Lines changed: 194 additions & 39 deletions

File tree

Source/Function/SkyBridge.ts

Lines changed: 194 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,23 @@ const _CelDispatchLog = (DomEvent: string, HasConsumer: boolean): void => {
120120
/**
121121
* Retrieves the VS Code `IWorkbench` stored globally by Mountain.astro.
122122
* Returns null if the workbench has not loaded yet.
123+
*
124+
* Defensive: `window` itself can be undefined under SSR / Astro
125+
* pre-render evaluation; the global access path is wrapped to keep the
126+
* function safe to call from any module-eval context.
123127
*/
124128
function GetWorkbench(): {
125129
commands: {
126130
executeCommand(id: string, ...args: unknown[]): Promise<unknown>;
127131
};
128132
env: { openUri(target: unknown): Promise<boolean> };
129133
} | null {
130-
return (window as any).__CEL_WORKBENCH__ ?? null;
134+
try {
135+
if (typeof window === "undefined") return null;
136+
return (window as any).__CEL_WORKBENCH__ ?? null;
137+
} catch {
138+
return null;
139+
}
131140
}
132141

133142
// Concrete workbench service handles written by the Output transform
@@ -233,6 +242,14 @@ interface CelServices {
233242
}
234243

235244
function GetServices(): CelServices | null {
245+
// SSR safety: `window` is undefined during Astro's pre-render
246+
// pass. Returning `null` lets every caller keep its existing
247+
// `if (!Services) return;` early-return contract.
248+
try {
249+
if (typeof window === "undefined") return null;
250+
} catch {
251+
return null;
252+
}
236253
return (window as any).__CEL_SERVICES__ ?? null;
237254
}
238255

@@ -664,10 +681,45 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
664681
Channel: string,
665682
Handler: (Payload: any) => void,
666683
) => {
667-
const Unlisten = await listen<any>(Channel, (Event) =>
668-
Handler(Event.payload),
669-
);
670-
Cleanups.push(Unlisten);
684+
// Centralized try/catch wrapper: a single bad handler must not
685+
// crash the Tauri listener loop, which would silently
686+
// disconnect future events on the same channel. Stock VS
687+
// Code's Emitter wraps each subscriber's call in a try/catch
688+
// for the same reason - one buggy listener should never
689+
// silence its peers.
690+
const SafeHandler = (Payload: any): void => {
691+
try {
692+
Handler(Payload);
693+
} catch (HandlerError) {
694+
try {
695+
console.warn(
696+
`[SkyBridge] handler for ${Channel} threw:`,
697+
HandlerError,
698+
);
699+
} catch {
700+
/* console may be replaced */
701+
}
702+
}
703+
};
704+
try {
705+
const Unlisten = await listen<any>(Channel, (Event) =>
706+
SafeHandler(Event.payload),
707+
);
708+
Cleanups.push(Unlisten);
709+
} catch (RegisterError) {
710+
// Tauri's `listen()` can reject if the IPC bridge is torn
711+
// down mid-install (e.g. window closing during boot). Log
712+
// and continue - the rest of the bridge install must
713+
// still complete so other channels work.
714+
try {
715+
console.warn(
716+
`[SkyBridge] failed to register listener for ${Channel}:`,
717+
RegisterError,
718+
);
719+
} catch {
720+
/* console may be replaced */
721+
}
722+
}
671723
};
672724

673725
// Atom Q1: resolve UI requests via Mountain's `ResolveUIRequest` Tauri
@@ -2074,16 +2126,40 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
20742126
] as const;
20752127
for (const Channel of FanOut) {
20762128
await Register(Channel, (Payload: any) => {
2077-
const DomEvent = ChannelToDomEvent(Channel);
2078-
document.dispatchEvent(
2079-
new CustomEvent(DomEvent, { detail: Payload }),
2080-
);
2081-
// `cel-dispatch` tag: surfaces whether this CustomEvent has
2082-
// any consumer registered. Orphans (consumer-present=false)
2083-
// are F1.1 indicators - Mountain's emit reaches the DOM
2084-
// but nothing in the workbench listens, so the event
2085-
// effectively vanishes.
2086-
_CelDispatchLog(DomEvent, _CelConsumers.has(DomEvent));
2129+
// Defensive: a single handler that throws (bad payload from
2130+
// upstream, dispatchEvent rejected by the DOM, etc.) must
2131+
// not stop the rest of the fan-out from running. Same
2132+
// philosophy as VS Code's `safeStringify` / event-emitter
2133+
// per-listener try/catch - one bad consumer never silences
2134+
// the others.
2135+
let DomEvent = "";
2136+
try {
2137+
DomEvent = ChannelToDomEvent(Channel);
2138+
document.dispatchEvent(
2139+
new CustomEvent(DomEvent, { detail: Payload }),
2140+
);
2141+
} catch (DispatchError) {
2142+
try {
2143+
console.warn(
2144+
`[SkyBridge] FanOut dispatch failed for ${Channel}:`,
2145+
DispatchError,
2146+
);
2147+
} catch {
2148+
/* swallow - console may be replaced */
2149+
}
2150+
return;
2151+
}
2152+
try {
2153+
// `cel-dispatch` tag: surfaces whether this CustomEvent
2154+
// has any consumer registered. Orphans
2155+
// (consumer-present=false) are F1.1 indicators -
2156+
// Mountain's emit reaches the DOM but nothing in the
2157+
// workbench listens, so the event effectively vanishes.
2158+
_CelDispatchLog(DomEvent, _CelConsumers.has(DomEvent));
2159+
} catch {
2160+
/* dispatch-log failure must not propagate; the event
2161+
* itself already fired above */
2162+
}
20872163
});
20882164
}
20892165

@@ -2248,13 +2324,26 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
22482324
? Response.items
22492325
: [];
22502326
const ParentHandle = Element?.handle ?? "";
2251-
const Items = RawItems.map((Raw, Index) =>
2252-
ToTreeItem(Raw, {
2253-
ViewId,
2254-
ParentHandle,
2255-
Index,
2256-
}),
2257-
);
2327+
// Per-item try/catch so a single malformed tree node
2328+
// (extension-side serialisation glitch, missing
2329+
// `label`/`handle`) doesn't drop the entire panel
2330+
// children list. Stock VS Code's renderer skips bad
2331+
// items rather than failing the parent.
2332+
const Items: unknown[] = [];
2333+
for (let Index = 0; Index < RawItems.length; Index += 1) {
2334+
try {
2335+
Items.push(
2336+
ToTreeItem(RawItems[Index], {
2337+
ViewId,
2338+
ParentHandle,
2339+
Index,
2340+
}),
2341+
);
2342+
} catch {
2343+
/* skip the bad item; the rest of the children
2344+
* are still valid */
2345+
}
2346+
}
22582347
// Dual-emit: DOM CustomEvent for Sky-side observers
22592348
// (same shape as the workbench tree renderer sees so
22602349
// mirror panels don't need a second conversion).
@@ -2391,13 +2480,29 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
23912480
| undefined;
23922481
const ViewId = Detail?.viewId ?? "";
23932482
if (!ViewId) return;
2394-
const Services = GetServices();
2395-
const TreeView = Services?.TreeViewByViewId?.(ViewId);
2396-
if (TreeView?.refresh) {
2397-
TreeView.refresh().catch(() => {});
2483+
// Defensive: `Services?.TreeViewByViewId?.()` itself could
2484+
// throw (Registry lookup with a freshly disposed view), and
2485+
// `TreeView.refresh()` may synchronously throw before
2486+
// returning a Promise (older xterm/tree shims). Wrap so a
2487+
// single failure doesn't crash the listener loop.
2488+
try {
2489+
const Services = GetServices();
2490+
const TreeView = Services?.TreeViewByViewId?.(ViewId);
2491+
if (TreeView?.refresh) {
2492+
const RefreshResult = TreeView.refresh();
2493+
if (RefreshResult && typeof RefreshResult.catch === "function") {
2494+
RefreshResult.catch(() => {});
2495+
}
2496+
}
2497+
} catch {
2498+
/* swallow - already-disposed view / DI lookup race */
23982499
}
23992500
// Also re-prime the Sky observers.
2400-
void ProvideChildren(ViewId, undefined);
2501+
try {
2502+
void ProvideChildren(ViewId, undefined);
2503+
} catch {
2504+
/* swallow */
2505+
}
24012506
});
24022507

24032508
// `cel:tree-view:dispose` - extension disposed its tree data
@@ -2410,10 +2515,16 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
24102515
| undefined;
24112516
const ViewId = Detail?.viewId ?? "";
24122517
if (!ViewId) return;
2413-
const Services = GetServices();
2414-
const TreeView = Services?.TreeViewByViewId?.(ViewId);
2415-
if (TreeView && TreeView.dataProvider !== undefined) {
2416-
TreeView.dataProvider = undefined;
2518+
// Defensive: setter may throw if the workbench already
2519+
// torn down the view in a parallel disposal race.
2520+
try {
2521+
const Services = GetServices();
2522+
const TreeView = Services?.TreeViewByViewId?.(ViewId);
2523+
if (TreeView && TreeView.dataProvider !== undefined) {
2524+
TreeView.dataProvider = undefined;
2525+
}
2526+
} catch {
2527+
/* view already disposed - nothing to clear */
24172528
}
24182529
});
24192530
}
@@ -2701,12 +2812,35 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
27012812
const Handle = Args[0] ?? Payload?.handle;
27022813
const ViewId: string = String(Args[1] ?? Payload?.viewId ?? "");
27032814
if (!ViewId) return;
2704-
WebviewViewResolvers.set(ViewId, Number(Handle));
2705-
document.dispatchEvent(
2706-
new CustomEvent("cel:webview:registerView", {
2707-
detail: { handle: Handle, viewId: ViewId, payload: Payload },
2708-
}),
2709-
);
2815+
// Defensive: a malformed payload (Mountain emit shape drift,
2816+
// missing handle, etc.) shouldn't kill the rest of the
2817+
// listener pipeline. Track + DOM-dispatch are best-effort;
2818+
// the WebviewViewService.register call below is what actually
2819+
// makes the panel work, so isolate failures so one doesn't
2820+
// cascade into the other.
2821+
try {
2822+
WebviewViewResolvers.set(ViewId, Number(Handle));
2823+
} catch {
2824+
/* Map.set on a non-string viewId is unreachable since we
2825+
* String()-coerced above, but keep the guard so a future
2826+
* payload-shape change can't poison the registry */
2827+
}
2828+
try {
2829+
document.dispatchEvent(
2830+
new CustomEvent("cel:webview:registerView", {
2831+
detail: { handle: Handle, viewId: ViewId, payload: Payload },
2832+
}),
2833+
);
2834+
} catch (DispatchError) {
2835+
try {
2836+
console.warn(
2837+
`[SkyBridge] webview/registerView CustomEvent dispatch failed for ${ViewId}:`,
2838+
DispatchError,
2839+
);
2840+
} catch {
2841+
/* console may be replaced */
2842+
}
2843+
}
27102844
// Per-fire trace so the SkyEmit -> Sky-listener bridge is
27112845
// observable in Mountain.dev.log. The listener used to silently
27122846
// `return` when `Services?.WebviewViews?.register` was missing,
@@ -2829,8 +2963,29 @@ async function _InstallSkyBridgeOnce(): Promise<void> {
28292963
}
28302964
},
28312965
});
2832-
} catch (_e) {
2833-
/* swallow - workbench DI not yet resolved */
2966+
} catch (RegisterError) {
2967+
// `IWebviewViewService.register` throws on duplicate viewId -
2968+
// stock VS Code's `webviewViewService.ts:108` does
2969+
// `throw new Error("View resolver already registered for ...")`
2970+
// when a viewId is registered twice. That happens when the
2971+
// extension host re-registers after a hot-reload or when our
2972+
// SkyBridge reentrancy guard didn't engage in time. Swallow
2973+
// the dup-error specifically (the existing resolver is
2974+
// already serving the view); log anything else so we can
2975+
// triage real failures.
2976+
try {
2977+
const Message = (RegisterError as any)?.message ?? String(
2978+
RegisterError,
2979+
);
2980+
if (!String(Message).includes("already registered")) {
2981+
console.warn(
2982+
`[SkyBridge] WebviewViews.register failed for ${ViewId}:`,
2983+
RegisterError,
2984+
);
2985+
}
2986+
} catch {
2987+
/* console may be replaced */
2988+
}
28342989
}
28352990
});
28362991

0 commit comments

Comments
 (0)