meteor-mysql
meteor-mysql copied to clipboard
Meteor 1.7: client side error TypeError: Class constructor Dependency cannot be invoked without 'new'
Hi, first, thank you for making meteor-mysql.
I've just upgraded Meteor from 1.6.1.1 to 1.7.0.5, and as you may have noticed, Meteor uses a new aproche where it can use packages as es6 or es5 depending on the client browser capabilities.
The problem i'm having actually is that i'm using a modern browser (Chrome Version 63.0.3239.132 (Build officiel) (64 bits)), and when i try to access the app with my browser, i get an error in the console:
Uncaught TypeError: Class constructor Dependency cannot be invoked without 'new'
in numtel_mysql.js at line 67
And my app doesn't work.
To me it seems that Meteor isn't compilling the tracker.js to ES5 so that class Tracker.Dependency
can be used as a Js constructor function. Therefore at line 67 in numtel_mysql.js, Tracker.Dependency.call(..)
doesn't work anymore and should something like new Tracker.Dependency(...)
instead.
Actually i don't fully understand how to configure Meteor's new way of managing modules, but i thought it would be benefic to anyone using the package to rise this issue. Perhapse juste modifying the file at line 67 could make things work again, i'll keep searching. Until then If anyone has a solution to make numtel work again with Meteor 1.7 please share it.
Update, after some research i've seen that numtel:mysql depends on Meteor's tracker package version 1.0 which used es5 syntax, and after Meteor update 1.6.1.4, tracker.js (version 1.2.x) is now written in es6, meaning it now uses class
keyword to declare Tracker.Dependency rather than legacy javascript function.
So, i'm not sure, i haven't got any response from Meteor's github repo about my issue yet, perhaps numtel:mysql could be rewritten in es6, or maybe configure Meteor to always convert tracker.js to es5 just to make it work with numtel:mysql which doesn't seem like a good idea. Anyway, i decided to go back to Meteor 1.6.1.3 waiting to gain more insight. Thanks
edit:Meteor version that i downgraded to
Interested in resolve of this aswell.
I have a proposal for fix. Tracker.Dependency.call(self) , does nothing but run Tracker.Dependency with the context of the objected. Causing a integration of the object by all returns of the Tracker.Dependency call, such as subscriptionId, stop.
Theoretically the issue can be described as the following:
var f1 = function() {
this.abc = 'def1';
}
f1.call(this);
As expected: If this is ran in the window context, the window context will be assigned abc with the value of def1.
The problem with the new Tracker is that it requires the "new" keyword.
That means, the above behavior should be kept to keep it functional.
Which leads to the question, how to run a function with the new keyword, with the current context assigned?
The proof of concept would be the following:
var self = this;
var f = function() {
self.abc = 'def';
}
var x = f.bind(self);
new x();
console.log(self.abc);
Running this in the context of window will cause the same as the above f1 function, but with the difference of using the new keyword.
So the approach would seem as simple as replacing:
Tracker.Dependency.call(self);
with
var _tracker = Tracker.Dependency.bind(self);
new _tracker();
Note: This is based on the hypothesis that the Tracker.Dependency function has not changed much except that it requires a constructor pattern now. This has not been tested yet.
So I have tested this and it's confirmed to work. While the plugin shipped as it is does not work with Meteor 1.7, after a few modifications it does work.
Steps:
- Install plugins etc as usual.
- Install mysql:numtel doing: meteor npm install --save mysql:numtel
- When installed, goto node_modules/mysql_numtel/ In there is 2 files, MysqlSubscription.js, and MysqlLive.js Copy these 2 Files.
- Goto your Meteor Project into folder /imports
- Create folder MysqlNumtel
- cd to folder MysqlNumtel
- Paste files MysqlSubscription.js and MysqlLive.js
- Now goto meteor cli again and type: meteor npm install --save lodash
- When installed, open the newly created files in /imports/MysqlNumtel/
- Add this to the top of the (both) files:
import _ from "lodash";
- And in MysqlSubscription.js , replace the Tracker.Dependency.call line with
var _tracker = Tracker.Dependency.bind(self);
new _tracker();
To use at server side simply do:
import LiveMySQL from '../imports/MysqlNumtel/LiveMysql.js';
To use at client side simply do:
import MySQLSubscription from '../imports/MysqlNumtel/MysqlSubscription.js';
Will create a NPM package to work with 1.7.
@fabbok1x Can you make a pull request? I'm guessing this also applies to the postgres version.
@Fabbok1x Can you make a pull request? I'm guessing this also applies to the postgres version.
Yes. However there is one thing I noticed (in ReactJS, not Meteor Helpers), which is that .depend() seemed to be missing. That is interesting because
MysqlSubscription.prototype = [];
_.extend(MysqlSubscription.prototype, Tracker.Dependency.prototype);
has worked before. Doing this in Meteor 1.7 with the newest lodash version will not extend the MysqlSubscription.prototype with e.g. the depend function.
After all it seems the _.extend is not required, the prototype will be created automatically and there do not seem to be more dependencies in its current implementation than .depend().
Extending the prototype with the depend function will -virtually- make it available again and usable. (There has been no errors atleast.)
MysqlSubscription.prototype.depend = Tracker.Dependency.prototype.depend;
Additionally,
var _tracker = Tracker.Dependency.bind(self);
new _tracker();
The code works with and without that at the moment.
@numtel I'm not exactly sure about the expected behavior of the previous Tracker.Dependency.call(self)
Was it to mutate the current self with variables assigned during the call, or was there another behavior expected?
As if it was about mutating / extending self by the variables assigned to this in the Tracker.Dependency.call, the closest fix I have in mind would be this:
var _tracker = Tracker.Dependency.bind(self);
Object.assign(self, new _tracker());
Created a pull request: https://github.com/numtel/meteor-mysql/pull/85
.reactive and .depend are not confirmed to work. Further digging into the issue would be required from my side. If someone has suggestions meanwhile feel free to comment.
This is how I currently work on these files
LiveMysql.js
import Future from 'fibers/future';
import LiveMysql from 'mysql-live-select';
import _ from "lodash";
class LiveMySQL extends LiveMysql {
constructor(settings) {
super(settings);
const self = this;
const fut = new Future;
let initLength;
}
}
class _publishCursor extends LiveMySQL{
constructor(sub, fut) {
super(sub);
sub.onStop(() => {
self.stop();
});
// Send reset message (for code pushes)
sub._session.send({
msg: 'added',
collection: sub._name,
id: sub._subscriptionId,
fields: { reset: true }
});
// Send aggregation of differences
self.on('update', (diff, rows) => {
sub._session.send({
msg: 'added',
collection: sub._name,
id: sub._subscriptionId,
fields: { diff }
});
if(sub._ready === false && !fut.isResolved()){
fut['return']();
}
});
self.on('error', error => {
if (!fut.isResolved()) {
fut['throw'](error);
}
return fut.wait();
});
}
}
class LiveMysqlSelect extends LiveMySQL {
constructor() {
super();
self = this;
this._cursorDescription = { collectionName: 'data' };
}
get fetch() {
const dataWithIds = self.queryCache.data.map((row, index) => {
const clonedRow = _.clone(row);
if(!('_id' in clonedRow)) {
clonedRow._id = String('id' in clonedRow ? clonedRow.id : index + 1);
}
// Ensure row index is included since response will not be ordered
if(!('_index' in clonedRow)) {
clonedRow._index = index + 1;
}
return clonedRow;
});
return dataWithIds;
}
}
export {LiveMySQL as default};
MysqlSubscription.js
import _ from "lodash";
class MysqlSubscription {
constructor(connection, name) {
const self = this;
const MySqlDep = new Tracker.Dependency();
let selfConnection;
const buffer = [];
let subscribeArgs;
this.connection = connection;
this.name = name;
this._events = [];
if (!(self instanceof MysqlSubscription)) {
throw new Error('use "new" to construct a MysqlSubscription');
}
if (typeof connection === 'string') {
// Using default connection
subscribeArgs = Array.prototype.slice.call(arguments, 0);
name = connection;
if (Meteor.isClient) {
connection = Meteor.connection;
} else if (Meteor.isServer) {
if (!selfConnection) {
selfConnection = DDP.connect(Meteor.absoluteUrl());
}
connection = selfConnection;
}
} else {
// Subscription arguments does not use the first argument (the connection)
subscribeArgs = Array.prototype.slice.call(arguments, 1);
}
Tracker.autorun(function () {
MySqlDep.depend();
});
let _tracker = Tracker.Dependency.bind(self);
new _tracker();
// Y U No give me subscriptionId, Meteor?!
const subsBefore = _.keys(connection._subscriptions);
_.extend(self, connection.subscribe(...subscribeArgs));
const subsNew = _.difference(_.keys(connection._subscriptions), subsBefore);
if (subsNew.length !== 1) throw new Error('Subscription failed!');
self.subscriptionId = subsNew[0];
buffer.push({
connection,
name,
subscriptionId: self.subscriptionId,
instance: self,
resetOnDiff: false
});
// If first store for this subscription name, register it!
if (_.filter(buffer, sub => sub.name === name && sub.connection === connection).length === 1) {
connection.registerStore(name, {
update(msg) {
const subBuffers = _.filter(buffer, sub => sub.subscriptionId === msg.id);
// If no existing subscriptions match this message's subscriptionId,
// discard message as it is most likely due to a subscription that has
// been destroyed.
// See test/MysqlSubscription :: Quick Change test cases
if (subBuffers.length === 0) return;
const subBuffer = subBuffers[0];
const sub = subBuffer.instance;
if (msg.msg === 'added' &&
msg.fields && msg.fields.reset === true) {
// This message indicates a reset of a result set
if (subBuffer.resetOnDiff === false) {
sub.dispatchEvent('reset', msg);
sub.splice(0, sub.length);
}
} else if (msg.msg === 'added' &&
msg.fields && 'diff' in msg.fields) {
// Aggregation of changes has arrived
if (subBuffer.resetOnDiff === true) {
sub.splice(0, sub.length);
subBuffer.resetOnDiff = false;
}
const newData = applyDiff(sub, msg.fields.diff);
// Prepend first 2 splice arguments to array
newData.unshift(sub.length);
newData.unshift(0);
// Update the subscription's data
sub.splice(...newData);
// Emit event for application
sub.dispatchEvent('update', msg.fields.diff, sub);
}
sub.changed();
}
});
}
};
}
// Inherit from Array and Tracker.Dependency
MysqlSubscription.prototype = new Array;
_.extend(MysqlSubscription.prototype, Tracker.Dependency.prototype);
class change extends MysqlSubscription {
constructor() {
super();
const self = this;
let selfBuffer = _.filter(buffer, function (sub) {
return sub.subscriptionId === self.subscriptionId;
})[0];
self.stop();
let connection = selfBuffer.connection;
let subscribeArgs = Array.prototype.slice.call(arguments);
subscribeArgs.unshift(selfBuffer.name);
let subsBefore = _.keys(connection._subscriptions);
_.extend(self, connection.subscribe.apply(connection, subscribeArgs));
let subsNew = _.difference(_.keys(connection._subscriptions), subsBefore);
if (subsNew.length !== 1) throw new Error('Subscription failed!');
self.subscriptionId = selfBuffer.subscriptionId = subsNew[0];
selfBuffer.resetOnDiff = true;
}
}
class _eventRoot extends MysqlSubscription {
constructor(eventName) {
super(eventName);
return eventName.split('.')[0];
}
}
class _selectEvents extends MysqlSubscription {
constructor(eventName, invert) {
super(eventName, invert);
const self = this;
let eventRoot, testKey, testVal;
if (!(eventName instanceof RegExp)) {
eventRoot = _eventRoot(eventName);
if (eventName === eventRoot) {
testKey = 'root';
testVal = eventRoot;
} else {
testKey = 'name';
testVal = eventName;
}
}
return _.filter(self._events, function (event) {
let pass;
if (eventName instanceof RegExp) {
pass = event.name.match(eventName);
} else {
pass = event[testKey] === testVal;
}
return invert ? !pass : pass;
});
}
}
class addEventListener extends MysqlSubscription {
constructor(eventName, listener) {
super(eventName, listener);
const self = this;
if (typeof listener !== 'function')
throw new Error('invalid-listener');
self._events.push({
name: eventName,
root: _eventRoot(eventName),
listener: listener
});
}
}
class removeEventListener extends MysqlSubscription {
constructor(eventName) {
super(eventName);
const self = this;
this._events = _selectEvents(eventName, true);
}
}
class dispatchEvent extends MysqlSubscription {
constructor(eventName) {
super(eventName);
const self = this;
let listenerArgs = Array.prototype.slice.call(arguments, 1);
let listeners = _selectEvents(eventName);
// Newest to oldest
for (let i = listeners.length - 1; i >= 0; i--) {
// Return false to stop further handling
if (listeners[i].listener.apply(self, listenerArgs) === false) return false;
}
return true;
}
}
class reactive extends MysqlSubscription {
constructor() {
super();
const self = this;
self.depend();
return self;
}
}
// Copied from mysql-live-select for use on the client side
function applyDiff(data, diff) {
data = data.map(function (row, index) {
row = _.clone(row);
row._index = index + 1;
return row;
});
let newResults = data.slice();
diff.removed !== null && diff.removed.forEach(
function (removed) { newResults[removed._index - 1] = undefined; });
// Deallocate first to ensure no overwrites
diff.moved !== null && diff.moved.forEach(
function (moved) { newResults[moved.old_index - 1] = undefined; });
diff.copied !== null && diff.copied.forEach(function (copied) {
let copyRow = _.clone(data[copied.orig_index - 1]);
copyRow._index = copied.new_index;
newResults[copied.new_index - 1] = copyRow;
});
diff.moved !== null && diff.moved.forEach(function (moved) {
let movingRow = data[moved.old_index - 1];
movingRow._index = moved.new_index;
newResults[moved.new_index - 1] = movingRow;
});
diff.added !== null && diff.added.forEach(
function (added) { newResults[added._index - 1] = added; });
let result = newResults.filter(function (row) { return row !== undefined; });
return result.map(function (row) {
row = _.clone(row);
delete row._index;
return row;
});
}
export {MysqlSubscription as default};
Currently I have an issue with publications and I get the error: Exception from sub feedPub id doKcKvKzgC5uKKXiq Error: Publish function can only return a Cursor or an array of Cursors
@underzerogr Great post. Do you get this error when using findOne? During which call is this occuring?
@Fabbok1x Thank you,
I m using Meteor 1.8 and working on this for some time now to modernize it and maybe make it future proof.
I get the publication error server side.
Here is the error in full
I20190114-14:11:57.781(2)? Exception from sub feedPub id QXk8jEGKKwSMNTzGH Error: Publish function can only return a Cursor or an array of Cursors
I20190114-14:11:57.781(2)? at Subscription._publishHandlerResult (packages/ddp-server/livedata_server.js:1129:18)
I20190114-14:11:57.781(2)? at Subscription._runHandler (packages/ddp-server/livedata_server.js:1060:10)
I20190114-14:11:57.781(2)? at Session._startSubscription (packages/ddp-server/livedata_server.js:859:9)
I20190114-14:11:57.781(2)? at Session.sub (packages/ddp-server/livedata_server.js:625:12)
I20190114-14:11:57.781(2)? at packages/ddp-server/livedata_server.js:559:43
These are the next steps to setting the publication:
livedb.js
import LiveMySQL from '/imports/meteor-mysql/lib/LiveMysql.js';
class liveDb extends LiveMySQL{
constructor() {
const minServerId = 2;
const maxServerId = Math.pow(2, 32);
const serverId = Math.floor(Math.random() * (maxServerId - minServerId) + minServerId);
const config = {...Meteor.settings.mysql, serverId };
super(config);
}
}
export {liveDb as default};
then this is the server side publication method
import liveDb from '/imports/api/db/server/livedb.js'
Meteor.publish('feedPub', function (params) {
if (limit === null || limit === 0)
limit = '0,50';
let clientIdd = getClientId(clientId);
const q = makeQuery(params);
const tableName = 'reports_cache_' + clientIdd;
let liveDatabase = new liveDb();
return liveDatabase.select(
q,
[{table: tableName}]
);
});
I'm currently trying upgrade from Meteor 1.6.1.4 to Meteor 1.7 and I ran into this issue.
@Fabbok1x, in your workaround you list the step
Install mysql:numtel doing: meteor npm install --save mysql:numtel
However when I run that I get the following error
> meteor npm install --save mysql:numtel
npm ERR! code EUNSUPPORTEDPROTOCOL
npm ERR! Unsupported URL Type "mysql:": mysql:numtel
I've tried npm versions 6.4.1 and 6.13.4. I've tried adding quotes around "mysql:numtel", but no difference. Did you run into this? Any thoughts on overcoming?
It's
npm install --save numtel:mysql
@underzerosyncbnb thanks for the quick reply! However, I get a similar error:
> npm install --save numtel:mysql
npm ERR! code EUNSUPPORTEDPROTOCOL
npm ERR! Unsupported URL Type "numtel:": numtel:mysql
maybe package.json is missing or you need to reinstall node
Either way this is a problem with npm. You can google it
I do have a package.json. I also tried using yarn, but the same error:
> yarn add numtel:mysql
...
error An unexpected error occurred: "numtel:mysql: Invalid protocol: numtel:".
So it appears to me numtel:mysql
won't work because both npm and yarn expect anything before a colon ':' to be a protocol, like https, git, file, etc.
So instead I tried the following steps:
-
meteor npm install --save lodash
-
meteor npm install --save mysql-live-select
- Create folder /imports/MysqlNumtel
- Copy files MysqlSubscription.js and MysqlLive.js from this repo into that folder
- Add to the top of the (both) files:
import _ from "lodash";
- Add to MysqlLive.js:
import LiveMysql from 'mysql-live-select';
- And in MysqlSubscription.js , replace the Tracker.Dependency.call line with
var _tracker = Tracker.Dependency.bind(self);
new _tracker();
- To use at server side simply do:
import LiveMySQL from '/imports/MysqlNumtel/LiveMysql.js';
- To use at client side simply do:
import MySQLSubscription from '/imports/MysqlNumtel/MysqlSubscription.js';
But then I when I reference LiveMysql in my code:
const liveDb = new LiveMysql(Meteor.settings.mysql);
I get the error:
TypeError: LiveMysql is not a constructor
at mysql.js (server/mysql.js:8:23)
@underzerosyncbnb at this point I tried your class files from your comment. However I'm seeing the same error
Exception from sub active_viewers id FN95hSmCFaa8PpeLY Error: Publish function can only return a Cursor or an array of Cursors
Also it seems like certain code is unused. For example in your LiveMysql.js
rewrite, the function _publishCursor
doesn't seem to be referenced anywhere.
And in MysqlSubscription.js
, reactive
is now a class instead of a function?
Did you ever get these classes to work? Any pointers would be incredibly appreciated!
Yes you are right those classes were work in progress. I had to abandon that because of very heavy schedule I had back then.
I can have a look at these again if there is a chance I make it work. I have a project stuck in 1.6.x due to this. It would be nice if I could update it.