Skip to content

Commit 8982f29

Browse files
authored
refactor(sequencing): Extract god class into focused, single-responsibility modules (#1317)
1 parent 1935563 commit 8982f29

19 files changed

+4598
-2952
lines changed

package-lock.json

Lines changed: 55 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { Activity } from "../activity";
2+
import { ActivityTree } from "../activity_tree";
3+
import { ActivityTreeQueries } from "../utils/activity_tree_queries";
4+
import { ChoiceConstraintValidator } from "../validators/choice_constraint_validator";
5+
import { FlowTraversalService } from "../traversal/flow_traversal_service";
6+
import {
7+
SequencingResult,
8+
DeliveryRequestType,
9+
ChoiceTraversalResult
10+
} from "../rules/sequencing_request_types";
11+
12+
/**
13+
* ChoiceRequestHandler - Handles choice-based sequencing requests
14+
*
15+
* This handler manages:
16+
* - CHOICE: Navigate to a specific activity selected by the learner
17+
* - JUMP: Navigate directly to a specific activity (4th Edition)
18+
*
19+
* Choice navigation is more complex than flow navigation because it must
20+
* validate many constraints at each ancestor level.
21+
*/
22+
export class ChoiceRequestHandler {
23+
constructor(
24+
private activityTree: ActivityTree,
25+
private constraintValidator: ChoiceConstraintValidator,
26+
private traversalService: FlowTraversalService,
27+
private treeQueries: ActivityTreeQueries
28+
) {}
29+
30+
/**
31+
* Choice Sequencing Request Process (SB.2.9)
32+
* Processes a choice navigation request to a specific activity
33+
* @param {string} targetActivityId - The target activity ID
34+
* @param {Activity | null} currentActivity - Current activity (may be null)
35+
* @return {SequencingResult}
36+
*/
37+
public handleChoice(
38+
targetActivityId: string,
39+
currentActivity: Activity | null
40+
): SequencingResult {
41+
const result = new SequencingResult();
42+
43+
// Find the target activity
44+
const targetActivity = this.activityTree.getActivity(targetActivityId);
45+
if (!targetActivity) {
46+
result.exception = "SB.2.9-1";
47+
return result;
48+
}
49+
50+
// SB.2.9-6: Check if current activity is terminated
51+
if (currentActivity && currentActivity.isActive) {
52+
result.exception = "SB.2.9-6";
53+
return result;
54+
}
55+
56+
// Validate choice constraints
57+
const validation = this.constraintValidator.validateChoice(
58+
currentActivity,
59+
targetActivity,
60+
{ checkAvailability: true }
61+
);
62+
63+
if (!validation.valid) {
64+
result.exception = validation.exception;
65+
return result;
66+
}
67+
68+
// Find common ancestor
69+
const commonAncestor = this.treeQueries.findCommonAncestor(
70+
currentActivity,
71+
targetActivity
72+
);
73+
74+
// Terminate descendent attempts from common ancestor
75+
if (currentActivity) {
76+
this.terminateDescendentAttemptsProcess(
77+
commonAncestor || this.activityTree.root!
78+
);
79+
}
80+
81+
// Form the activity path from target to common ancestor
82+
const activityPath = this.buildActivityPath(targetActivity, commonAncestor);
83+
84+
// Evaluate each activity in the path
85+
for (const pathActivity of activityPath) {
86+
if (!this.traversalService.checkActivityProcess(pathActivity)) {
87+
// Sequencing ends with no delivery
88+
return result;
89+
}
90+
}
91+
92+
// If target is not a leaf, use choice flow to find a deliverable leaf
93+
let deliveryTarget = targetActivity;
94+
if (targetActivity.children.length > 0) {
95+
const flowResult = this.choiceFlowSubprocess(targetActivity);
96+
97+
if (!flowResult) {
98+
result.exception = "SB.2.9-7";
99+
return result;
100+
}
101+
102+
deliveryTarget = flowResult;
103+
}
104+
105+
// Deliver the identified activity
106+
result.deliveryRequest = DeliveryRequestType.DELIVER;
107+
result.targetActivity = deliveryTarget;
108+
return result;
109+
}
110+
111+
/**
112+
* Jump Sequencing Request Process (SB.2.13)
113+
* Processes a jump navigation request (SCORM 2004 4th Edition)
114+
* Jump bypasses most sequencing rules
115+
* @param {string} targetActivityId - The target activity ID
116+
* @return {SequencingResult}
117+
*/
118+
public handleJump(targetActivityId: string): SequencingResult {
119+
const result = new SequencingResult();
120+
121+
// Find the target activity
122+
const targetActivity = this.activityTree.getActivity(targetActivityId);
123+
if (!targetActivity) {
124+
result.exception = "SB.2.13-1";
125+
return result;
126+
}
127+
128+
// Check if target is in the activity tree
129+
if (!this.treeQueries.isInTree(targetActivity)) {
130+
result.exception = "SB.2.13-2";
131+
return result;
132+
}
133+
134+
// Check if target is available
135+
if (!targetActivity.isAvailable) {
136+
result.exception = "SB.2.13-3";
137+
return result;
138+
}
139+
140+
// Deliver the target activity directly
141+
result.deliveryRequest = DeliveryRequestType.DELIVER;
142+
result.targetActivity = targetActivity;
143+
return result;
144+
}
145+
146+
/**
147+
* Get all activities available for choice navigation
148+
* @return {Activity[]} - Array of available activities
149+
*/
150+
public getAvailableChoices(): Activity[] {
151+
const allActivities = this.activityTree.getAllActivities();
152+
const currentActivity = this.activityTree.currentActivity;
153+
const availableActivities: Activity[] = [];
154+
155+
for (const activity of allActivities) {
156+
// Skip root activity
157+
if (activity === this.activityTree.root) {
158+
continue;
159+
}
160+
161+
// Skip if hidden, unavailable, or invisible
162+
if (activity.isHiddenFromChoice || !activity.isAvailable || !activity.isVisible) {
163+
continue;
164+
}
165+
166+
// Check if choice is allowed by parent
167+
if (activity.parent && !activity.parent.sequencingControls.choice) {
168+
continue;
169+
}
170+
171+
// Validate the full choice path
172+
const validation = this.constraintValidator.validateChoice(currentActivity, activity);
173+
if (validation.valid) {
174+
availableActivities.push(activity);
175+
}
176+
}
177+
178+
return availableActivities;
179+
}
180+
181+
/**
182+
* Build the activity path from target to common ancestor
183+
* @param {Activity} targetActivity - Target activity
184+
* @param {Activity | null} commonAncestor - Common ancestor
185+
* @return {Activity[]} - Path of activities
186+
*/
187+
private buildActivityPath(
188+
targetActivity: Activity,
189+
commonAncestor: Activity | null
190+
): Activity[] {
191+
const activityPath: Activity[] = [];
192+
let activity: Activity | null = targetActivity;
193+
194+
while (activity && activity !== commonAncestor) {
195+
activityPath.unshift(activity);
196+
activity = activity.parent;
197+
}
198+
199+
return activityPath;
200+
}
201+
202+
/**
203+
* Choice Flow Subprocess (SB.2.9.1)
204+
* Handles the flow logic specific to choice navigation requests
205+
* @param {Activity} targetActivity - The target activity for the choice
206+
* @return {Activity | null} - The activity to deliver, or null if flow fails
207+
*/
208+
private choiceFlowSubprocess(targetActivity: Activity): Activity | null {
209+
// If target is a leaf, it's the delivery candidate
210+
if (targetActivity.children.length === 0) {
211+
return targetActivity;
212+
}
213+
214+
// If target is a cluster, traverse to find a deliverable leaf
215+
return this.choiceFlowTreeTraversal(targetActivity);
216+
}
217+
218+
/**
219+
* Choice Flow Tree Traversal (SB.2.9.2)
220+
* Traverses into a cluster to find a deliverable leaf
221+
* @param {Activity} fromActivity - The cluster to traverse from
222+
* @return {Activity | null} - A leaf activity for delivery, or null
223+
*/
224+
private choiceFlowTreeTraversal(fromActivity: Activity): Activity | null {
225+
this.traversalService.ensureSelectionAndRandomization(fromActivity);
226+
const children = fromActivity.getAvailableChildren();
227+
228+
// Validate children against constraints
229+
const validChildren = this.constraintValidator.validateFlowConstraints(
230+
fromActivity,
231+
children
232+
);
233+
234+
if (!validChildren.valid) {
235+
return null;
236+
}
237+
238+
// Find the first deliverable child
239+
for (const child of validChildren.validChildren) {
240+
const traversalResult = this.enhancedChoiceTraversal(child);
241+
if (traversalResult.activity) {
242+
return traversalResult.activity;
243+
}
244+
}
245+
246+
return null;
247+
}
248+
249+
/**
250+
* Enhanced Choice Activity Traversal (SB.2.4)
251+
* Traverses with stopForwardTraversal and forwardOnly checks
252+
* @param {Activity} activity - The activity to traverse
253+
* @param {boolean} isBackwardTraversal - Whether this is backward traversal
254+
* @return {ChoiceTraversalResult} - Result with activity or exception
255+
*/
256+
private enhancedChoiceTraversal(
257+
activity: Activity,
258+
isBackwardTraversal: boolean = false
259+
): ChoiceTraversalResult {
260+
// Cannot walk backward from root
261+
if (isBackwardTraversal && activity === this.activityTree.root) {
262+
return new ChoiceTraversalResult(null, "SB.2.4-3");
263+
}
264+
265+
// Check availability
266+
if (!activity.isAvailable) {
267+
return new ChoiceTraversalResult(null, null);
268+
}
269+
270+
// Check hidden from choice
271+
if (activity.isHiddenFromChoice) {
272+
return new ChoiceTraversalResult(null, null);
273+
}
274+
275+
// Check stopForwardTraversal
276+
if (activity.sequencingControls && activity.sequencingControls.stopForwardTraversal) {
277+
return new ChoiceTraversalResult(null, "SB.2.4-1");
278+
}
279+
280+
// Validate traversal constraints
281+
const traversalValidation = this.constraintValidator.validateTraversalConstraints(activity);
282+
if (!traversalValidation.canTraverse) {
283+
return new ChoiceTraversalResult(null, null);
284+
}
285+
286+
// If it's a leaf, check if it can be delivered
287+
if (activity.children.length === 0) {
288+
if (this.traversalService.checkActivityProcess(activity)) {
289+
return new ChoiceTraversalResult(activity, null);
290+
}
291+
return new ChoiceTraversalResult(null, null);
292+
}
293+
294+
// Check constrainChoice for clusters
295+
if (
296+
activity.parent?.sequencingControls.constrainChoice &&
297+
!traversalValidation.canTraverseInto
298+
) {
299+
return new ChoiceTraversalResult(null, "SB.2.4-2");
300+
}
301+
302+
// Traverse into cluster
303+
if (traversalValidation.canTraverseInto) {
304+
const flowResult = this.choiceFlowTreeTraversal(activity);
305+
return new ChoiceTraversalResult(flowResult, null);
306+
}
307+
308+
return new ChoiceTraversalResult(null, null);
309+
}
310+
311+
/**
312+
* Terminate descendent attempts (simplified)
313+
* @param {Activity} activity - The activity
314+
*/
315+
private terminateDescendentAttemptsProcess(activity: Activity): void {
316+
activity.isActive = false;
317+
for (const child of activity.children) {
318+
this.terminateDescendentAttemptsProcess(child);
319+
}
320+
}
321+
}

0 commit comments

Comments
 (0)