Skip to content

Commit aff9a70

Browse files
Griffin EvansRhysSullivan
authored andcommitted
Fix loadSharedHandle being able to poison future getExecutor() calls
1 parent b3d0f83 commit aff9a70

1 file changed

Lines changed: 35 additions & 4 deletions

File tree

apps/local/src/executor.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,47 @@ export const createExecutorHandle = async (options: LocalExecutorOptions = {}) =
253253
};
254254
};
255255

256+
class SharedHandleCreateError extends Data.TaggedError("SharedHandleCreateError")<{
257+
readonly cause: unknown;
258+
}> {}
259+
256260
export type ExecutorHandle = Awaited<ReturnType<typeof createExecutorHandle>>;
257261

258262
let sharedHandlePromise: ReturnType<typeof createExecutorHandle> | null = null;
259263
let sharedHandleLifecycle: Promise<void> = Promise.resolve();
260264

261-
const loadSharedHandle = () => {
262-
if (!sharedHandlePromise) {
263-
sharedHandlePromise = sharedHandleLifecycle.then(() => createExecutorHandle());
265+
const loadSharedHandle = (): Promise<ExecutorHandle> => {
266+
if (sharedHandlePromise) {
267+
return sharedHandlePromise;
264268
}
265-
return sharedHandlePromise;
269+
270+
// Capture the lifecycle tail at call time so creation stays ordered behind
271+
// in-flight dispose
272+
const lifecycle = sharedHandleLifecycle;
273+
274+
// Identity token the heal closure compares against. Using a `let` declared
275+
// up front avoids any reference-before-init ambiguity in the closure.
276+
let slot: Promise<ExecutorHandle>;
277+
278+
const acquire = Effect.tryPromise({
279+
try: () => lifecycle.then(() => createExecutorHandle()),
280+
catch: (cause) => new SharedHandleCreateError({ cause }),
281+
}).pipe(
282+
// Self-heal: a failed creation must not poison the memo. Clear the slot on
283+
// any non-success outcome so the next getExecutor() retries, but only if a
284+
// dispose/reload hasn't already swapped in a newer promise (identity guard).
285+
Effect.onError(() =>
286+
Effect.sync(() => {
287+
if (sharedHandlePromise === slot) {
288+
sharedHandlePromise = null;
289+
}
290+
}),
291+
),
292+
);
293+
294+
slot = Effect.runPromise(acquire);
295+
sharedHandlePromise = slot;
296+
return slot;
266297
};
267298

268299
export const getExecutor = () => loadSharedHandle().then((handle) => handle.executor);

0 commit comments

Comments
 (0)