Skip to content

Commit

Permalink
[Threads] implement a toolset to work with threads #45
Browse files Browse the repository at this point in the history
  • Loading branch information
TitanNano committed Dec 13, 2021
1 parent dbebec0 commit c2cac46
Show file tree
Hide file tree
Showing 15 changed files with 781 additions and 0 deletions.
1 change: 1 addition & 0 deletions tests/Gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const task_default = function() {
'web/**/*.js',
'ServiceWorker/**/*.js',
'traits/**/*.js',
'threading/**/*.js',
], { base: './', })
.pipe(sourcemaps.init())
.pipe(babel(babelConfig))
Expand Down
2 changes: 2 additions & 0 deletions tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ require('./util/array');

require('./rendering');

require('./threading');

require('./features');

describe('IndexedDB Driver', () => {
Expand Down
47 changes: 47 additions & 0 deletions tests/threading/CurrentThread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-env mocha */

const expect = require('chai').expect;
const mochaVM = require('../../node/mochaVM');

module.exports = function() {
const vm = mochaVM({});

mochaVM.applyNodeEnv(vm);

vm.updateContext({
self: vm.getContext(),

addEventListener() {},
postMessage() {},

BroadcastChannel: function(name) { // eslint-disable-line object-shorthand
this.name = name;
},

Worker: function(sourcePath) { // eslint-disable-line object-shorthand
this.source = sourcePath;

this.postMessage = function() {};
},

MessagePort: function() { // eslint-disable-line object-shorthand
this.postMessage = function() {};

this.onmessage = {};
}
});

vm.runModule('../../testable/threading/lib/CurrentThread.js');

it('should be able to bootstrap the current thread', () => {
const { testResult } = vm.apply(() => {
/* globals CurrentThread */

CurrentThread.bootstrap();

global.testResult = CurrentThread;
});

expect(testResult).to.have.property('mainThread');
});
};
236 changes: 236 additions & 0 deletions tests/threading/Thread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/* eslint-env mocha */

const expect = require('chai').expect;
const mochaVM = require('../../node/mochaVM');

module.exports = function() {
const vm = mochaVM({});

mochaVM.applyNodeEnv(vm);

vm.updateContext({
self: vm.getContext(),

addEventListener() {},
postMessage() {},

BroadcastChannel: function(name) { // eslint-disable-line object-shorthand
this.name = name;
},

Worker: function(sourcePath) { // eslint-disable-line object-shorthand
this.source = sourcePath;

this.postMessage = function() {};
},

MessagePort: function() { // eslint-disable-line object-shorthand
this.postMessage = function() {};

this.onmessage = {};
},

setTimeout(...args) {
return setTimeout(...args);
}
});

vm.runModule('../../testable/threading/lib/Thread.js');

it('should create a new thread', () => {
const { testResult, testContext } = vm.apply(() => {
/* globals Thread */

const thread = Object.create(Thread).constructor('./test-thread.js');

global.testResult = thread;
global.testContext = { Thread };
});

expect(testResult).to.have.property('__proto__', testContext.Thread);
});

it('should be able to create a thread from strings', () => {
const { testResult, testContext } = vm.apply(() => {
const thread = Thread.from('channels/shared');

global.testResult = thread;
global.testContext = { Thread };
});

expect(testResult).to.have.property('__proto__', testContext.Thread);
});

it('should throw if no MessageChannel is provided', () => {
const { testResult } = vm.apply(() => {
global.testResult = () => Thread.from({ invalid: true });
});

expect(testResult).to.throw();
});

it('should create a thread from a message port', () => {
const { testResult, testContext } = vm.apply(() => {
const thread = Thread.from(new MessagePort());

global.testResult = thread;
global.testContext = { Thread };
});

expect(testResult).to.have.property('__proto__', testContext.Thread);
});

it('should not return a then method, we are not a promise', () => {
const { testResult } = vm.apply(() => {
const thread = Object.create(Thread).constructor('./test-thread.js');

global.testResult = thread.then;
});

expect(testResult).to.be.null;
});

it('should return the original property value, if it exists', () => {
const { testResult, testContext } = vm.apply(() => {
const thread = Object.create(Thread).constructor('./test-thread.js');

global.testResult = thread.call;
global.testContext = { Thread };
});

expect(testResult).to.be.equal(testContext.Thread.call);
});

it('should try to invoke the remote method', () => {
const { testResult } = vm.apply(() => {
const thread = Object.create(Thread).constructor('./test-thread.js');

global.testResult = thread.doSomething();
});

expect(testResult).to.have.property('then');
expect(testResult).to.have.property('catch');
});

it('should not copy transfered arguments', () => {
const { testResult, testContext } = vm.apply(() => {
const arg1 = { content: 'test1' };
const arg2 = { content: 'test2' };

global.testContext = { arg1, arg2 };

const worker = new Worker('./test-thread.js');

worker.postMessage = function(...args) {
global.testResult = args;
};

const thread = Object.create(Thread).constructor(worker);

thread.call('test', [arg1, arg2], [arg2]);
});

expect(testResult).to.have.property('0').which.does.deep.include({ args: [testContext.arg1, testContext.arg2] });
expect(testResult).to.have.property('1').which.does.deep.equal([testContext.arg2]);
});

it('should emit thread events', () => {
const { testResult } = vm.apply(() => {
const worker = new Worker('./test-thread.js');
const thread = Object.create(Thread).constructor(worker);

global.testResult = new Promise((resolve, reject) => {
thread.on('test-event', data => resolve(data));

worker.onmessage({ data: { type: 'THREAD_MESSAGE_EVENT', name: 'test-event', data: { a: 1, b: 2} } });

setTimeout(() => reject(), 500);
});
});

expect(testResult).to.have.property('then');
expect(testResult).to.have.property('catch');

return testResult
.then(data => expect(data).to.be.deep.equal({ a: 1, b: 2 }))
.catch(() => expect.fail('eventhandler timedout'));
});

it('should resolve an async remote call', () => {
const { testResult } = vm.apply(() => {
const worker = new Worker('./test-thread.js');
const thread = Object.create(Thread).constructor(worker);

worker.postMessage = function(event) {
worker.onmessage({ data: { type: 'THREAD_MESSAGE_RETURN_VALUE', data: { transaction: event.transaction, return: true }, } });
};

global.testResult = thread.call('method', [1, 2, 3]);
});

expect(testResult).to.have.property('then');
expect(testResult).to.have.property('catch');

const timeout = setTimeout(() => expect.fail('async resolve timedout'), 500);

return testResult.then(
data => (clearTimeout(timeout), expect(data).to.be.true),
() => (clearTimeout(timeout), expect.fail('remote call failed'))
);
});

it('should reject an async remote call that threw', () => {
const { testResult } = vm.apply(() => {
const worker = new Worker('./test-thread.js');
const thread = Object.create(Thread).constructor(worker);

worker.postMessage = function(event) {
worker.onmessage({ data: { type: 'THREAD_MESSAGE_RETURN_VALUE', data: { transaction: event.transaction, error: true }, } });
};

global.testResult = thread.call('method', [1, 2, 3]);
});

expect(testResult).to.have.property('then');
expect(testResult).to.have.property('catch');

const timeout = setTimeout(() => expect.fail('async resolve timedout'), 500);

return testResult
.then(() => {
clearTimeout(timeout);

return expect.fail('remote call should throw');
}, (error) => {
clearTimeout(timeout);

return expect(error).to.be.true;
});
});

it('should do nothing for unkown events', (done) => {
const { testResult } = vm.apply(() => {
const worker = new Worker('./test-thread.js');
const thread = Object.create(Thread).constructor(worker);

worker.postMessage = function(event) {
worker.onmessage({ data: { type: 'THREAD_MESSAGE_UNKOWN', data: { transaction: event.transaction, return: true }, } });
};

global.testResult = thread.call('method', [1, 2, 3]);
});

expect(testResult).to.have.property('then');
expect(testResult).to.have.property('catch');

const timeout = setTimeout(() => {
expect(true).to.be.true;
done();
}, 300);

testResult.then(
() => (clearTimeout(timeout), expect.fail('async call should not resolve')),
() => (clearTimeout(timeout), expect.fail('event handler should not fail'))
).catch(done);
});
};
6 changes: 6 additions & 0 deletions tests/threading/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-env mocha */

describe('Threading', () => {
require('./Thread')();
require('./CurrentThread')();
});
3 changes: 3 additions & 0 deletions threading/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as CurrentThread } from './lib/CurrentThread';
export { default as Thread } from './lib/Thread';
export { MultiThreadEvent } from './lib/MultiThreadEvent';
2 changes: 2 additions & 0 deletions threading/io.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './index';
export { default as IOThread } from './lib/IOThread';
Loading

0 comments on commit c2cac46

Please sign in to comment.