Skip to content

Commit 92002a2

Browse files
committed
refactor(commands): build one context per invocation
canExecute opens the invocation with the context it builds; execute and postCommandAction reuse it, so every stage, the class instance and COMMAND_CONTEXT hold the same object.
1 parent 1aed823 commit 92002a2

2 files changed

Lines changed: 44 additions & 17 deletions

File tree

lib/common/services/command-definition-adapter.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,9 @@ export function createCommandFromDefinition<
254254
return values;
255255
};
256256

257-
// Read per call rather than snapshotted here: the options service only holds
258-
// this command's parsed values once validateOptions has run for it.
257+
// Read when an invocation opens rather than at definition time: the options
258+
// service only holds this command's parsed values once validateOptions has
259+
// run for it.
259260
const buildContext = (args: string[]): CommandContext<TSchema> => {
260261
const options: any = {};
261262
for (const optionName of optionNames) {
@@ -339,7 +340,7 @@ export function createCommandFromDefinition<
339340
// The state of one invocation. The command object itself is cached for the
340341
// process, so nothing invocation-scoped may live outside one of these.
341342
interface Invocation {
342-
/** The context of the stage that is running; COMMAND_CONTEXT reads it. */
343+
/** Built once when the invocation opens; every stage and COMMAND_CONTEXT share it. */
343344
context: CommandContext<TSchema>;
344345
injector: Injector;
345346
setup: Promise<Awaited<TSetup>>;
@@ -375,15 +376,8 @@ export function createCommandFromDefinition<
375376
const beginInvocation = (context: CommandContext<TSchema>): Invocation => {
376377
const invocation: Invocation = {
377378
context,
378-
// Each entry point builds its own context object, so the token reads
379-
// the live one rather than a snapshot: a handler that injects it gets
380-
// the very context it was handed.
381379
injector: targetInjector.createChild([
382-
{
383-
provide: COMMAND_CONTEXT,
384-
useFactory: () => invocation.context,
385-
shared: false,
386-
},
380+
{ provide: COMMAND_CONTEXT, useValue: context },
387381
]),
388382
setup: undefined,
389383
hasRun: false,
@@ -452,9 +446,9 @@ export function createCommandFromDefinition<
452446
? {}
453447
: {
454448
postCommandAction: async (args: string[]): Promise<void> => {
455-
const context = buildContext(args);
456-
const invocation = currentInvocation || beginInvocation(context);
457-
invocation.context = context;
449+
const invocation =
450+
currentInvocation || beginInvocation(buildContext(args));
451+
const context = invocation.context;
458452
const setupResult = await invocation.setup;
459453
await runInInjectionContext(invocation.injector, () =>
460454
definition.postRun.call(
@@ -490,12 +484,11 @@ export function createCommandFromDefinition<
490484
);
491485
},
492486
execute: async (args: string[]): Promise<void> => {
493-
const context = buildContext(args);
494487
const invocation =
495488
currentInvocation && !currentInvocation.hasRun
496489
? currentInvocation
497-
: beginInvocation(context);
498-
invocation.context = context;
490+
: beginInvocation(buildContext(args));
491+
const context = invocation.context;
499492
invocation.hasRun = true;
500493

501494
const setupResult = await invocation.setup;

test/define-command.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2550,6 +2550,40 @@ describe("defineCommand", () => {
25502550
assert.strictEqual(injectedInRun, runContext);
25512551
});
25522552

2553+
it("hands one context object to every stage of an invocation", async () => {
2554+
const testInjector = createTestInjector();
2555+
const seen: any[] = [];
2556+
2557+
const command = createCommandFromDefinition(
2558+
defineCommand({
2559+
name: "dctest-command-context-shared",
2560+
setup: (ctx) => {
2561+
seen.push(ctx, inject(COMMAND_CONTEXT));
2562+
},
2563+
canExecute: (ctx) => {
2564+
seen.push(ctx, inject(COMMAND_CONTEXT));
2565+
return true;
2566+
},
2567+
run: (ctx) => {
2568+
seen.push(ctx, inject(COMMAND_CONTEXT));
2569+
},
2570+
postRun: (ctx) => {
2571+
seen.push(ctx, inject(COMMAND_CONTEXT));
2572+
},
2573+
}),
2574+
testInjector,
2575+
);
2576+
2577+
await command.canExecute([]);
2578+
await command.execute([]);
2579+
await command.postCommandAction([]);
2580+
2581+
assert.lengthOf(seen, 8);
2582+
for (const context of seen) {
2583+
assert.strictEqual(context, seen[0]);
2584+
}
2585+
});
2586+
25532587
it("is scoped to the invocation, so the root injector never sees it", async () => {
25542588
const testInjector = createTestInjector();
25552589

0 commit comments

Comments
 (0)