Skip to content

Overview

This document provides an overview and detailed explanation of the data prefetching implementation. The DataPrefetcher class is responsible for setting up properties, creating request nodes, and handling the prefetching of data for classrooms and students. The worker script listens for messages to start the prefetching process and responds with the results.

DataPrefetcher Class

The DataPrefetcher class handles the setup of properties and the prefetching process.

export class DataPrefetcher {
    schoolId: string;
    accessToken: string;
    userId: string;
    userRole: string;
    classroomIds: string[];
    studentId: string;
    mode: 'student' | 'classroom' = 'classroom';
    queue = new Queue<RequestNode>();

    setupProps(opts: {
        schoolId: string;
        accessToken: string;
        userId: string;
        classroomIds: string[];
    }) {
        this.schoolId = opts.schoolId;
        this.accessToken = opts.accessToken;
        this.userId = opts.userId;
        this.classroomIds = opts.classroomIds;
        console.debug(`
        Props setup 
        schoolId: ${this.schoolId};
        accessToken: ${this.accessToken};
        userId: ${this.userId};
        userRole: ${this.userRole};
        classroomIds: ${this.classroomIds};
        `);
    }

    setupStudentProps(opts: {
        schoolId: string;
        accessToken: string;
        userId: string;
        classroomId: string;
        studentId: string;
    }) {
        this.mode = 'student';
        this.schoolId = opts.schoolId;
        this.accessToken = opts.accessToken;
        this.userId = opts.userId;
        this.classroomIds = [opts.classroomId];
        this.studentId = opts.studentId;
        console.debug(`
        Props setup 
        schoolId: ${this.schoolId};
        accessToken: ${this.accessToken};
        userId: ${this.userId};
        userRole: ${this.userRole};
        classroomIds: ${this.classroomIds};
                studentId: ${this.studentId};
        `);
    }

Request Node Creation

The DataPrefetcher class includes methods to create request nodes for different levels of data.

    getChildRequestNode(requestNode: RequestNode, url: string) {
        const newLevel = requestNode.level + 1;
        return new RequestNode(newLevel, url);
    }

    getStudentIepGoalRequestNode(
        domainNode: RequestNode,
        studentId: string,
        iepId: string,
        goalId: string
    ) {
        const requestNode = this.getChildRequestNode(
            domainNode,
            `/school/students/${studentId}/ieps/${iepId}/daily_summaries/subgoals?goal_id=${goalId}`
        );

        const subgoalHandler = async (goalNode: RequestNode, response: Response) => {
            const requestNodes: RequestNode[] = [];
            const jsonData = await response.json();
            console.debug('Goal node jsondata', jsonData);
            if (jsonData.data) {
                const subgoalIds = (jsonData.data.iep_subgoals?.library || [])
                    .concat(jsonData.data.iep_subgoals?.school || [])
                    .map((it) => it.subgoal_id);
                requestNodes.push(
                    this.getChildRequestNode(
                        goalNode,
                        `/school/students/${studentId}/student_overrides?record_id=${subgoalIds
                            .sort()
                            .join(',')}`
                    )
                );
            } else {
                console.debug(`data absent for studentId ${studentId} iepId ${iepId} goal ${goalId}`);
            }
            return requestNodes;
        };
        requestNode.handler = subgoalHandler;
        return requestNode;
    }

    getStudentIepDomainRequestNode(
        iepNode: RequestNode,
        studentId: string,
        iepId: string,
        domainId: string
    ) {
        const requestNode = this.getChildRequestNode(
            iepNode,
            `/school/students/${studentId}/ieps/${iepId}/daily_summaries/goals?domain_id=${domainId}`
        );
        const goalHandler = async (domainNode: RequestNode, response: Response) => {
            const requestNodes: RequestNode[] = [];
            const jsonData = await response.json();
            console.debug('iep domain jsondata', jsonData);
            if (jsonData.data) {
                const goalIds = jsonData.data?.goals?.map((it) => it.id);
                goalIds.forEach((it) => {
                    requestNodes.push(this.getStudentIepGoalRequestNode(domainNode, studentId, iepId, it));
                    requestNodes.push(this.getChildRequestNode(domainNode, `/library/goals/${it}`));
                    requestNodes.push(
                        this.getChildRequestNode(
                            domainNode,
                            `/library/lesson_plan_subgoals?goal_id=${it}&q[sorts]=created_at+asc`
                        )
                    );
                    requestNodes.push(
                        this.getChildRequestNode(
                            domainNode,
                            `/school/students/${studentId}/ieps/${iepId}/iep_materials?goal_id=${it}`
                        )
                    );
                });
            } else {
                console.debug(`data absent for studentId ${studentId} iepId ${iepId} domainId ${domainId}`);
            }
            return requestNodes;
        };
        requestNode.handler = goalHandler;
        return requestNode;
    }

    getStudentIepRequestNode(studentNode: RequestNode, studentId: string, iepId: string) {
        const requestNode = this.getChildRequestNode(
            studentNode,
            `/school/students/${studentId}/ieps/${iepId}`
        );
        const domainHandler = async (iepNode: RequestNode, response: Response) => {
            const requestNodes: RequestNode[] = [];
            const jsonData = await response.json();
            console.debug('Student iep jsondata', jsonData);
            if (jsonData.data) {
                const domainIds = jsonData.data?.iep_domains
                    ?.filter((it) => it.selected)
                    ?.map((it) => it.domain_id);
                domainIds.forEach((it) => {
                    requestNodes.push(this.getStudentIepDomainRequestNode(iepNode, studentId, iepId, it));
                    requestNodes.push(this.getChildRequestNode(iepNode, `/library/domains/${it}`));
                });
            } else {
                console.debug(`data absent for studentId ${studentId} iepId ${iepId}`);
            }
            return requestNodes;
        };
        requestNode.handler = domainHandler;
        return requestNode;
    }

    getStudentRequestNode(classroomNode: RequestNode, studentId: string) {
        const requestNode = this.getChildRequestNode(classroomNode, `/school/students/${studentId}`);
        const iepHandler = async (studentNode: RequestNode, response: Response) => {
            let requestNodes: RequestNode[] = [];
            const jsonData = await response.json();
            console.debug('Student jsondata', jsonData);
            if (jsonData.data) {
                requestNodes = [
                    this.getStudentIepRequestNode(studentNode, studentId, jsonData.data.daily_iep?.id)
                ];
            } else {
                console.debug('data absent for studentId', studentId);
            }
            return requestNodes;
        };
        requestNode.handler = iepHandler;
        return requestNode;
    }

    getClassroomRequestNode(classroomId: string) {
        const classroomResponseHandler = async (parentNode: RequestNode, response: Response) => {
            let requestNodes: RequestNode[] = [];
            const jsonData = await response.json();
            console.debug('Classroom jsondata', jsonData);
            if (jsonData.data) {
                if (this.mode === 'classroom') {
                    requestNodes = jsonData.data
                        .filter((it) => it.iep_status === 'in_daily_assessments' && !it.archived)
                        .map((it) => this.getStudentRequestNode(parentNode, it.id));
                } else {
                    requestNodes = jsonData.data
                        .filter((it) => it.id === this.studentId)
                        .map((it) => this.getStudentRequestNode(parentNode, it.id));
                }
            } else {
                console.debug('data absent for classroomId', classroomId);
            }
            return requestNodes;
        };
        const requestNode = new RequestNode(
            0,
            `/school/students?classroom_id=${classroomId}&per_page=50`
        );
        requestNode.handler = classroomResponseHandler;
        return requestNode;
    }

Prefetch Method

The prefetch method initiates the prefetching process and handles the queue of request nodes.

    async prefetch() {
        const startTime = new Date();
        console.debug('[DataPrefetcher]', 'Start Prefetch');
        if (this.mode !== 'student') {
            await this.queue.enqueue(new RequestNode(0, `/users/profiles`));
            await this.queue.enqueue(new RequestNode(0, '/schools/me'));
            await this.queue.enqueue(new RequestNode(0, '/school/curriculums'));
            await this.queue.enqueue(new RequestNode(0, `/users/${this.userId}`));
        }
        for (let index = 0; index < this.classroomIds.length; index++) {
            const classroomId = this.classroomIds[index];
            await this.queue.enqueue(this.getClassroomRequestNode(classroomId));
        }

        while (!(await this.queue.isEmpty())) {
            const requestNode = await this.queue.dequeue();
            console.debug(`Process requestNode`, requestNode.url, requestNode.level);
            const handlerResponse = await requestNode.fetchNow(this.accessToken, this.schoolId);
            if (handlerResponse) {
                await this.queue.bulkEnqueue(handlerResponse);
            }
        }
        const endTime = new Date();
        const timeTakenSeconds = (endTime.getTime() - startTime.getTime()) / 1000;
        console.debug('[DataPrefetcher]', 'End Prefetch');
        console.debug('[DataPrefetcher]', `Total time taken seconds`, timeTakenSeconds);
        return {
            startTime: startTime.getTime(),
            endTime: endTime.getTime()
        };
    }
}

Worker Script

The worker script listens for messages to start the prefetching process and responds with the results. It is expected that the entire pretfetcing happens in a webworker.

import { DataPrefetcher } from './DataPrefetcher';

export type WorkerStartCommandRequest = {
    command: 'start';
    data: {
        schoolId: string;
        accessToken: string;
        userId: string;
        userRole: string;
        classroomIds: string[];
    };
};
export type WorkeStudentStartCommandRequest = {
    command: 'start_student';
    data: {
        userId: string;
        schoolId: string;
        accessToken: string;
        studentId: string;
        classroomId: string;
    };
};
export type WorkerCommandRequest = WorkerStartCommandRequest | WorkeStudentStartCommandRequest;
export type PrefetchCompletedCommandResponse = {
    command: 'prefetch-completed';
    data: {
        startTime: number;
        endTime: number;
    };
};
export type PrefetchCompletedStudentCommandResponse = {
    command: 'prefetch-student-completed';
    data: {
        studentId: string;
        startTime: number;
        endTime: number;
    };
};
export type WorkerCommandResponse = PrefetchCompletedCommandResponse;

self.onmessage = (event) => {
    console.info('[Worker]', 'Received message with data', event.data);
    const dataPrefetcher = new DataPrefetcher();
    const commandRequest = event.data as WorkerCommandRequest;
    console.log('Received command request', commandRequest);
    switch (commandRequest.command) {
        case 'start':
            console.log('Start for entire classroom');
            dataPrefetcher.setupProps(commandRequest.data);
            dataPrefetcher.prefetch().then((it) => {
                const prefetchCompletedCommandResponse: PrefetchCompletedCommandResponse = {
                    command: 'prefetch-completed',
                    data: it
                };
                self.postMessage(prefetchCompletedCommandResponse);
            });
            break;
        case 'start_student':
            console.log('Start for particular student');
            dataPrefetcher.setupStudentProps(commandRequest.data);
            dataPrefetcher.prefetch().then((it) => {
                const prefetchCompletedCommandResponse: PrefetchCompletedStudentCommandResponse = {
                    command: 'prefetch-student-completed',
                    data: {
                        ...it,
                        studentId: commandRequest.data.studentId
                    }
                };
                self.postMessage(prefetchCompletedCommandResponse);
            });
            break;
        default:
            console.log('Unknow command request');
            break;
    }
};

Worker Command Types

The worker script defines several types for handling different command requests and responses.

export type WorkerStartCommandRequest = {
    command: 'start';
    data: {
        schoolId: string;
        accessToken: string;
        userId: string;
        userRole: string;
        classroomIds: string[];
    };
};

export type WorkeStudentStartCommandRequest = {
    command: 'start_student';
    data: {
        userId: string;
        schoolId: string;
        accessToken: string;
        studentId: string;
        classroomId: string;
    };
};

export type WorkerCommandRequest = WorkerStartCommandRequest | WorkeStudentStartCommandRequest;

export type PrefetchCompletedCommandResponse = {
    command: 'prefetch-completed';
    data: {
        startTime: number;
        endTime: number;
    };
};

export type PrefetchCompletedStudentCommandResponse = {
    command: 'prefetch-student-completed';
    data: {
        studentId: string;
        startTime: number;
        endTime: number;
    };
};

export type WorkerCommandResponse = PrefetchCompletedCommandResponse;

Handling Messages

The worker script listens for messages and handles different commands to start the prefetching process.

self.onmessage = (event) => {
    console.info('[Worker]', 'Received message with data', event.data);
    const dataPrefetcher = new DataPrefetcher();
    const commandRequest = event.data as WorkerCommandRequest;
    console.log('Received command request', commandRequest);
    switch (commandRequest.command) {
        case 'start':
            console.log('Start for entire classroom');
            dataPrefetcher.setupProps(commandRequest.data);
            dataPrefetcher.prefetch().then((it) => {
                const prefetchCompletedCommandResponse: PrefetchCompletedCommandResponse = {
                    command: 'prefetch-completed',
                    data: it
                };
                self.postMessage(prefetchCompletedCommandResponse);
            });
            break;
        case 'start_student':
            console.log('Start for particular student');
            dataPrefetcher.setupStudentProps(commandRequest.data);
            dataPrefetcher.prefetch().then((it) => {
                const prefetchCompletedCommandResponse: PrefetchCompletedStudentCommandResponse = {
                    command: 'prefetch-student-completed',
                    data: {
                        ...it,
                        studentId: commandRequest.data.studentId
                    }
                };
                self.postMessage(prefetchCompletedCommandResponse);
            });
            break;
        default:
            console.log('Unknow command request');
            break;
    }
};