Skip to content

Commit 0a50d13

Browse files
committed
Prevent view constructor racing
1 parent 185e112 commit 0a50d13

File tree

3 files changed

+149
-62
lines changed

3 files changed

+149
-62
lines changed

js/view.js

Lines changed: 146 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,35 @@
22

33
const path = require('node:path');
44
const { inspect, inherits } = require('node:util');
5-
const Emitter = require('events');
5+
const Emitter = require('node:events');
66

77
const { View } = require('../core');
88

99

10-
let inited = false;
10+
function ReEmitter() {}
11+
ReEmitter.prototype = {
12+
emit(type, data) {
13+
if (this.__interceptor) {
14+
this.__interceptor(type, data);
15+
}
16+
return super.emit(type, data);
17+
}
18+
};
1119

12-
let nextIndex = 0;
13-
const takeIndex = () => (++nextIndex);
20+
inherits(ReEmitter, Emitter);
21+
inherits(View, ReEmitter);
1422

23+
24+
let inited = false;
25+
let nextIndex = 0;
1526
const globalLibs = [];
1627
const viewInstances = {};
17-
28+
let queueLoading = [];
29+
let emptyFunction = () => null;
1830
let qmlCwd = process.cwd().replace(/\\/g, '/');
1931

32+
const takeIndex = () => (++nextIndex);
33+
2034
const parseJsonSafe = (json) => {
2135
try {
2236
return JSON.parse(json)[0];
@@ -26,47 +40,104 @@ const parseJsonSafe = (json) => {
2640
}
2741
};
2842

29-
inherits(View, Emitter);
30-
31-
class JsView extends View {
43+
class JsView extends Emitter {
3244
constructor(opts = {}) {
3345
if (!inited) {
3446
throw new Error('Not inited. Call View.init(...) first.');
3547
}
3648

37-
const width = opts.width || 512;
38-
const height = opts.height || 512;
49+
super();
3950

40-
super(width, height);
51+
this._index = takeIndex();
52+
viewInstances[this._index] = this;
53+
this._timeout = null;
4154

42-
this._unload();
55+
this._opts = {
56+
...opts,
57+
width: opts.width || 512,
58+
height: opts.height || 512,
59+
silent: !!opts.silent,
60+
};
4361

62+
this._isLoaded = false;
63+
this._isFile = null;
64+
this._source = null;
65+
this._finalSource = null;
66+
this._width = this._opts.width;
67+
this._height = this._opts.height;
4468
this._textureId = null;
69+
this._isConstructed = false;
4570

46-
this._width = width;
47-
this._height = height;
71+
// This is fake temporary plug to receive event calls before `new View` is ready
72+
this._view = {
73+
_libs: emptyFunction,
74+
_resize: emptyFunction,
75+
_mouse: emptyFunction,
76+
_keyboard: emptyFunction,
77+
_destroy: emptyFunction,
78+
_invoke: emptyFunction,
79+
_set: emptyFunction,
80+
_get: emptyFunction,
81+
};
4882

49-
globalLibs.forEach((l) => this._libs(l));
83+
JsView._enqueueLoad(this);
84+
}
85+
86+
87+
static _enqueueLoad(view) {
88+
queueLoading = [...queueLoading, view];
89+
if (queueLoading.length === 1) {
90+
view._createView();
91+
}
92+
}
93+
94+
static _finishLoad(view) {
95+
if (view._timeout) {
96+
clearTimeout(view._timeout);
97+
view._timeout = null;
98+
}
99+
view._isLoading = false;
50100

51-
this._index = takeIndex();
52-
viewInstances[this._index] = this;
101+
queueLoading = queueLoading.filter((item) => (item !== view));
102+
const [next] = queueLoading;
103+
if (next) {
104+
next._createView();
105+
}
106+
}
107+
108+
_createView() {
109+
this._timeout = setTimeout(
110+
() => {
111+
this._timeout = null;
112+
JsView._finishLoad(this);
113+
},
114+
5000,
115+
);
116+
117+
this._view = new View(this._width, this._height);
118+
this._view.__interceptor = (type, data) => {
119+
if (!type.startsWith('_qml_')) {
120+
this.emit(type, data);
121+
}
122+
};
123+
124+
globalLibs.forEach((l) => this._view._libs(l));
125+
this._isConstructed = true;
53126

54-
this._silent = !! opts.silent;
55-
this.on('_qml_error', (data) => setImmediate(() => {
56-
if (!this._silent) {
127+
this._view.on('_qml_error', (data) => setImmediate(() => {
128+
if (!this._opts.silent) {
57129
console.error(`Qml Error: (${data.type})`, data.message);
58130
}
59131
this.emit('error', new Error(`${data.type}: ${data.message}`));
60132
}));
61133

62134
// Expect FBO texture
63-
this.on('_qml_fbo', (data) => setImmediate(() => {
135+
this._view.on('_qml_fbo', (data) => setImmediate(() => {
64136
this._textureId = data.texture;
65137
this.emit('reset', this._textureId);
66138
}));
67139

68-
69-
this.on('_qml_load', (e) => setImmediate(() => {
140+
this._view.on('_qml_load', (e) => setImmediate(() => {
70141
if (e.source !== this._finalSource) {
71142
return;
72143
}
@@ -76,19 +147,22 @@ class JsView extends View {
76147
}
77148

78149
if (e.status !== 'success') {
150+
JsView._finishLoad(this);
79151
return console.error('Qml Error. Could not load:', this._source);
80152
}
81153

82154
this._isLoaded = true;
83-
155+
JsView._finishLoad(this);
84156
this.emit('load');
85157
}));
86158

87-
this.on('_qml_mouse', (e) => setImmediate(() => this.emit(e.type, e)));
88-
this.on('_qml_key', (e) => setImmediate(() => this.emit(e.type, e)));
159+
this._view.on('_qml_mouse', (e) => setImmediate(() => this.emit(e.type, e)));
160+
this._view.on('_qml_key', (e) => setImmediate(() => this.emit(e.type, e)));
89161

90-
if (opts.file || opts.source) {
91-
this.load(opts);
162+
if (this._opts.file || this._opts.source) {
163+
this.load();
164+
} else {
165+
setImmediate(() => JsView._finishLoad(this));
92166
}
93167
}
94168

@@ -102,14 +176,14 @@ class JsView extends View {
102176
return;
103177
}
104178
this._width = v;
105-
this.resize(this._width, this._height);
179+
this._view._resize(this._width, this._height);
106180
}
107181
set height(v) {
108182
if (this._height === v) {
109183
return;
110184
}
111185
this._height = v;
112-
this.resize(this._width, this._height);
186+
this._view._resize(this._width, this._height);
113187
}
114188

115189
get w() { return this.width; }
@@ -129,7 +203,7 @@ class JsView extends View {
129203
}
130204
this._width = width;
131205
this._height = height;
132-
this._resize(this._width, this._height);
206+
this._view._resize(this._width, this._height);
133207
}
134208

135209
get textureId() {
@@ -147,47 +221,60 @@ class JsView extends View {
147221
}
148222

149223
mousedown(e) {
150-
this._mouse(1, e.button, e.buttons, e.x, e.y);
224+
this._view._mouse(1, e.button, e.buttons, e.x, e.y);
151225
}
152226

153227
mouseup(e) {
154-
this._mouse(2, e.button, e.buttons, e.x, e.y);
228+
this._view._mouse(2, e.button, e.buttons, e.x, e.y);
155229
}
156230

157231

158232
mousemove(e) {
159-
this._mouse(0, 0, e.buttons, e.x, e.y);
233+
this._view._mouse(0, 0, e.buttons, e.x, e.y);
160234
}
161235

162236
wheel(e) {
163-
this._mouse(3, e.wheelDelta, e.buttons, e.x, e.y);
237+
this._view._mouse(3, e.wheelDelta, e.buttons, e.x, e.y);
164238
}
165239

166240
keydown(e) {
167-
this._keyboard(1, e.which, e.charCode);
241+
this._view._keyboard(1, e.which, e.charCode);
168242
}
169243

170244
keyup(e) {
171-
this._keyboard(0, e.which, e.charCode);
245+
this._view._keyboard(0, e.which, e.charCode);
172246
}
173247

174-
load(opts) {
175-
this._unload();
248+
load(opts = {}) {
249+
this._opts = { ...this._opts, ...opts };
250+
251+
// If not constructed yet, just let it cook
252+
if (!this._isConstructed) {
253+
return;
254+
}
255+
256+
// If already loading something - ignore
257+
if (this._isLoading) {
258+
return;
259+
}
176260

177-
if (opts.file) {
261+
this._isLoading = true;
262+
this._isLoaded = false;
263+
this._isFile = null;
264+
this._source = null;
265+
this._finalSource = null;
266+
this._textureId = null;
267+
268+
if (this._opts.file) {
178269
this._isFile = true;
179-
this._source = opts.file;
180-
} else if (opts.source) {
270+
this._source = this._opts.file;
271+
} else if (this._opts.source) {
181272
this._isFile = false;
182-
this._source = opts.source;
273+
this._source = this._opts.source;
183274
} else {
184275
throw new Error('To load QML, specify opts.file or opts.source.');
185276
}
186277

187-
this._loadWhenReady();
188-
}
189-
190-
_loadWhenReady() {
191278
if (this._isLoaded || this._index === -1) {
192279
return;
193280
}
@@ -197,43 +284,39 @@ class JsView extends View {
197284
? this._source
198285
: `${qmlCwd}/${this._source}`;
199286

200-
this._load(true, this._finalSource);
287+
this._view._load(true, this._finalSource);
201288
} else {
202289
this._finalSource = this._source;
203-
this._load(false, this._source);
290+
this._view._load(false, this._source);
204291
}
205292
}
206293

207-
_unload() {
294+
destroy() {
295+
this._isLoading = false;
208296
this._isLoaded = false;
209297
this._isFile = null;
210298
this._source = null;
211299
this._finalSource = null;
212-
}
213-
214-
destroy() {
215-
this._unload();
216-
217300
this._textureId = null;
218301

219302
if (viewInstances[this._index]) {
220303
delete viewInstances[this._index];
221-
this._destroy();
304+
this._view._destroy();
222305
}
223306

224307
this._index = -1;
225308
}
226309

227310
invoke(name, key, args) {
228-
return parseJsonSafe(this._invoke(name, key, JSON.stringify(args)));
311+
return parseJsonSafe(this._view._invoke(name, key, JSON.stringify(args)));
229312
}
230313

231314
set(name, key, value) {
232-
this._set(name, key, `[${JSON.stringify(value)}]`);
315+
this._view._set(name, key, `[${JSON.stringify(value)}]`);
233316
}
234317

235318
get(name, key) {
236-
return parseJsonSafe(this._get(name, key));
319+
return parseJsonSafe(this._view._get(name, key));
237320
}
238321

239322
static init(cwd, wnd, ctx, device = 0) {
@@ -268,6 +351,10 @@ class JsView extends View {
268351

269352
View._style(name, def);
270353
}
354+
355+
static update() {
356+
View.update();
357+
}
271358
}
272359

273360
module.exports = JsView;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"author": "Luis Blanco <[email protected]>",
33
"name": "qml-raub",
4-
"version": "3.0.1",
4+
"version": "3.1.0",
55
"description": "QML interoperation for Node.js",
66
"license": "MIT",
77
"main": "index.js",

0 commit comments

Comments
 (0)