Skip to content

Commit 515b3e8

Browse files
committed
refactor: convert export worker to plain js
1 parent 510cef5 commit 515b3e8

3 files changed

Lines changed: 75 additions & 44 deletions

File tree

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
import { parentPort } from 'worker_threads';
22

3-
import type { FormTypes, InstrumentMeasureValue } from '@opendatacapture/runtime-core';
4-
import { DEFAULT_GROUP_NAME } from '@opendatacapture/schemas/core';
5-
import type { InstrumentRecordsExport } from '@opendatacapture/schemas/instrument-records';
63
import { removeSubjectIdScope } from '@opendatacapture/subject-utils';
74

8-
import type { BeginChunkProcessingData, InitData, ParentMessage, RecordType, WorkerMessage } from './thread-types';
9-
10-
type ExpandDataType =
11-
| {
12-
measure: string;
13-
measureValue: FormTypes.RecordArrayFieldValue | InstrumentMeasureValue;
14-
success: true;
15-
}
16-
| {
17-
message: string;
18-
success: false;
19-
};
20-
21-
function expandData(listEntry: any[]): ExpandDataType[] {
22-
const validRecordArrayList: ExpandDataType[] = [];
5+
/** @type {typeof import('@opendatacapture/schemas/core').DEFAULT_GROUP_NAME} */
6+
const DEFAULT_GROUP_NAME = 'root';
7+
8+
/**
9+
* @typedef {Object} SuccessExpand
10+
* @property {string} measure
11+
* @property {any} measureValue
12+
* @property {true} success
13+
*/
14+
15+
/**
16+
* @typedef {Object} FailureExpand
17+
* @property {string} message
18+
* @property {false} success
19+
*/
20+
21+
/** @typedef {SuccessExpand | FailureExpand} ExpandDataType */
22+
23+
/**
24+
* Flattens nested record array data into a list of expandable data objects.
25+
* @param {any[]} listEntry - The array of records to expand.
26+
* @returns {ExpandDataType[]} An array of expanded measure objects.
27+
* @throws {Error} If the provided listEntry is empty.
28+
*/
29+
function expandData(listEntry) {
30+
/** @type {SuccessExpand[]} */
31+
const validRecordArrayList = [];
2332
if (listEntry.length < 1) {
2433
throw new Error('Record Array is Empty');
2534
}
2635
for (const objectEntry of Object.values(listEntry)) {
27-
for (const [dataKey, dataValue] of Object.entries(
28-
objectEntry as { [key: string]: FormTypes.RecordArrayFieldValue }
29-
)) {
36+
for (const [dataKey, dataValue] of Object.entries(objectEntry)) {
3037
validRecordArrayList.push({
3138
measure: dataKey,
3239
measureValue: dataValue,
@@ -37,34 +44,48 @@ function expandData(listEntry: any[]): ExpandDataType[] {
3744
return validRecordArrayList;
3845
}
3946

40-
let initData: Map<
41-
string | undefined,
42-
{
43-
edition: number;
44-
id: string;
45-
name: string;
46-
}
47-
>;
48-
49-
function handleInit(data: InitData) {
47+
/**
48+
* Internal cache for instrument metadata.
49+
* @type {Map<string | undefined, { edition: number, id: string, name: string }>}
50+
*/
51+
let initData;
52+
53+
/**
54+
* Initializes the worker with instrument metadata.
55+
* * @param {Array<{id: string, edition: number, name: string}>} data - The initialization payload.
56+
*/
57+
function handleInit(data) {
5058
initData = new Map(data.map((instrument) => [instrument.id, instrument]));
51-
5259
parentPort?.postMessage({ success: true });
5360
}
5461

55-
function handleChunkComplete(_data: BeginChunkProcessingData) {
62+
/**
63+
* Processes a chunk of records and posts results back to the parent thread.
64+
* @param {import('./thread-types').BeginChunkProcessingData} _data - The collection of records to process.
65+
* @throws {Error} If initData is not defined.
66+
*/
67+
function handleChunkComplete(_data) {
5668
if (!initData) {
5769
throw new Error('Expected init data to be defined');
5870
}
5971
const instrumentsMap = initData;
6072

61-
const processRecord = (record: RecordType): InstrumentRecordsExport => {
62-
const instrument = instrumentsMap.get(record.instrumentId)!;
73+
/**
74+
* Transforms a single record into an array of exportable rows.
75+
* @param {import('./thread-types').RecordType} record - The raw instrument record.
76+
* @returns {import('@opendatacapture/schemas/instrument-records').InstrumentRecordsExport} The processed rows.
77+
*/
78+
const processRecord = (record) => {
79+
const instrument = instrumentsMap.get(record.instrumentId);
80+
if (!instrument) {
81+
throw new Error(`Instrument not found for ID: ${record.instrumentId}`);
82+
}
6383

64-
if (!record.computedMeasures) return [];
84+
if (!record.computedMeasures) {
85+
return [];
86+
}
6587

66-
//const instrument = instrumentsMap.get(record.instrumentId)!;
67-
const rows: InstrumentRecordsExport = [];
88+
const rows = [];
6889

6990
for (const [measureKey, measureValue] of Object.entries(record.computedMeasures)) {
7091
if (measureValue == null) continue;
@@ -83,7 +104,7 @@ function handleChunkComplete(_data: BeginChunkProcessingData) {
83104
subjectSex: record.subject.sex,
84105
timestamp: record.date,
85106
username: record.session.user?.username ?? 'N/A',
86-
value: measureValue as InstrumentMeasureValue
107+
value: measureValue
87108
});
88109
continue;
89110
}
@@ -117,19 +138,27 @@ function handleChunkComplete(_data: BeginChunkProcessingData) {
117138

118139
try {
119140
const results = _data.map(processRecord);
120-
parentPort?.postMessage({ data: results.flat(), success: true } satisfies WorkerMessage);
141+
parentPort?.postMessage({ data: results.flat(), success: true });
121142
} catch (error) {
122-
parentPort?.postMessage({ error: (error as Error).message, success: false } satisfies WorkerMessage);
143+
if (error instanceof Error) {
144+
parentPort?.postMessage({ error: error.message, success: false });
145+
} else {
146+
parentPort?.postMessage({ error: 'Unknown Error', success: false });
147+
}
123148
}
124149
}
125150

126-
parentPort!.on('message', (message: ParentMessage) => {
151+
/**
152+
* Worker Message Router
153+
* @param {import('./thread-types').ParentMessage} message
154+
*/
155+
parentPort?.on('message', (message) => {
127156
switch (message.type) {
128157
case 'BEGIN_CHUNK_PROCESSING':
129158
return handleChunkComplete(message.data);
130159
case 'INIT':
131160
return handleInit(message.data);
132161
default:
133-
throw new Error(`Unexpected message type: ${(message satisfies never as { [key: string]: any }).type}`);
162+
throw new Error(`Unexpected message type: ${message.type}`);
134163
}
135164
});

apps/api/src/instrument-records/instrument-records.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export class InstrumentRecordsService {
164164

165165
const workerPromises = chunks.map((chunk) => {
166166
return new Promise<InstrumentRecordsExport>((resolve, reject) => {
167-
const worker = new Worker(join(import.meta.dirname, 'export-worker.ts'));
167+
const worker = new Worker(join(import.meta.dirname, 'export-worker.js'));
168168
worker.postMessage({ data: availableInstrumentArray, type: 'INIT' } satisfies InitMessage);
169169

170170
worker.on('message', (message: InitialMessage) => {

apps/api/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
4+
"allowJs": true,
45
"baseUrl": ".",
6+
"checkJs": true,
57
"emitDecoratorMetadata": true,
68
"experimentalDecorators": true,
79
"paths": {

0 commit comments

Comments
 (0)