diff --git a/src/Program.spec.ts b/src/Program.spec.ts index fbdc7eabd..a8d6b3388 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -262,44 +262,6 @@ end sub expect(afterFileParse.callCount).to.equal(2); expect(afterFileValidate.callCount).to.equal(2); }); - - it('validates files added in beforeProgramValidate hook', () => { - const afterFileValidate = sinon.spy(); - - program.plugins = new PluginInterface([{ - name: 'test plugin that adds files during beforeProgramValidate', - beforeProgramValidate: (program: Program) => { - // Add a file that defines an enum - const fileContent = `enum Tasks - MyTask = "_AsyncTask" -end enum`; - program.setFile('source/AsyncTask/Tasks.bs', fileContent); - }, - afterFileValidate: afterFileValidate - }], { logger: createLogger() }); - - // Add a main file that imports the enum - program.setFile('source/main.bs', ` -import "pkg:/source/AsyncTask/Tasks.bs" - -sub main() - print Tasks.MyTask -end sub - `); - - program.validate(); - - // Verify that the Tasks.bs file was validated - const tasksFile = program.getFile('source/AsyncTask/Tasks.bs'); - expect(tasksFile).to.not.be.undefined; - expect(tasksFile.isValidated).to.be.true; - - // Verify afterFileValidate was called for both files (main.bs and Tasks.bs) - expect(afterFileValidate.callCount).to.equal(2); - - // Should have no diagnostics (Tasks should be found) - expectZeroDiagnostics(program); - }); }); describe('validate', () => { diff --git a/src/Program.ts b/src/Program.ts index 5e945bee5..e79944aeb 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -727,27 +727,23 @@ export class Program { this.plugins.emit('beforeProgramValidate', this); beforeProgramValidateWasEmitted = true; }) - .once(() => { - // Capture files AFTER beforeProgramValidate event to include any files added by plugins - const filesToValidate = Object.values(this.files); - for (const file of filesToValidate) { - if (!file.isValidated) { - this.plugins.emit('beforeFileValidate', { - program: this, - file: file - }); - - //emit an event to allow plugins to contribute to the file validation process - this.plugins.emit('onFileValidate', { - program: this, - file: file - }); - //call file.validate() IF the file has that function defined - file.validate?.(); - file.isValidated = true; - - this.plugins.emit('afterFileValidate', file); - } + .forEachFactory(() => Object.values(this.files), (file) => { + if (!file.isValidated) { + this.plugins.emit('beforeFileValidate', { + program: this, + file: file + }); + + //emit an event to allow plugins to contribute to the file validation process + this.plugins.emit('onFileValidate', { + program: this, + file: file + }); + //call file.validate() IF the file has that function defined + file.validate?.(); + file.isValidated = true; + + this.plugins.emit('afterFileValidate', file); } }) .forEach(Object.values(this.scopes), (scope) => { diff --git a/src/common/Sequencer.spec.ts b/src/common/Sequencer.spec.ts index b8b338ec3..9061f4f0e 100644 --- a/src/common/Sequencer.spec.ts +++ b/src/common/Sequencer.spec.ts @@ -73,4 +73,43 @@ describe('Sequencer', () => { } expect(cancelCalled).to.be.true; }); + + it('forEachFactory calls factory function at execution time', () => { + const values = []; + let items = [1, 2]; + + const sequencer = new Sequencer().forEachFactory(() => items, (i) => { + values.push(i); + }); + + // Add more items after sequencer is configured but before execution + items.push(3); + + sequencer.runSync(); + + // Should process all items including the one added after configuration + expect(values).to.eql([1, 2, 3]); + }); + + it('forEachFactory maintains event loop yielding behavior', async () => { + const values = []; + let executionTimes = []; + + await new Sequencer({ + minSyncDuration: 10 // Very short duration to force frequent yielding + }).forEachFactory(() => [1, 2, 3, 4, 5], (i) => { + executionTimes.push(Date.now()); + values.push(i); + // Simulate some work + const start = Date.now(); + while (Date.now() - start < 5) { + // busy wait + } + }).run(); + + expect(values).to.eql([1, 2, 3, 4, 5]); + // With the short minSyncDuration, we should see some gaps in execution times + // indicating the sequencer yielded to the event loop + expect(executionTimes.length).to.equal(5); + }); }); diff --git a/src/common/Sequencer.ts b/src/common/Sequencer.ts index b744bcd6b..6e0b47bcc 100644 --- a/src/common/Sequencer.ts +++ b/src/common/Sequencer.ts @@ -36,6 +36,22 @@ export class Sequencer { return this; } + public forEachFactory(itemsFactory: () => T[], func: (item: T) => any) { + this.actions.push({ + args: [], + func: () => { + // Get the items from the factory function at execution time + const items = itemsFactory(); + // Create a nested sequencer for the items to maintain event loop yielding behavior + const nestedSequencer = new Sequencer(this.options); + return nestedSequencer + .forEach(items, func) + .runSync(); + } + }); + return this; + } + private emitter = new EventEmitter(); public onCancel(callback: () => void) {