neo.mjs 6.35.0 → 6.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/apps/ServiceWorker.mjs +2 -2
- package/apps/colors/view/ViewportController.mjs +27 -17
- package/apps/portal/view/home/FeatureSection.mjs +209 -0
- package/apps/portal/view/home/parts/Colors.mjs +34 -84
- package/apps/portal/view/home/parts/Features.mjs +1 -1
- package/apps/portal/view/home/parts/Helix.mjs +36 -95
- package/apps/portal/view/home/parts/How.mjs +31 -48
- package/apps/portal/view/learn/ContentView.mjs +53 -20
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/videoMove/MainContainer.mjs +3 -4
- package/examples/videoMove/neo-config.json +2 -1
- package/package.json +2 -2
- package/resources/data/deck/learnneo/pages/benefits/Multi-Threading.md +221 -0
- package/resources/data/deck/learnneo/tree.json +12 -13
- package/resources/scss/src/apps/portal/Viewport.scss +19 -0
- package/resources/scss/src/apps/portal/home/ContentBox.scss +9 -2
- package/resources/scss/src/apps/portal/home/FeatureSection.scss +68 -0
- package/resources/scss/src/apps/portal/home/parts/Colors.scss +1 -5
- package/resources/scss/src/apps/portal/home/parts/Helix.scss +1 -7
- package/resources/scss/src/apps/portal/home/parts/How.scss +0 -22
- package/resources/scss/src/apps/portal/learn/ContentView.scss +22 -10
- package/resources/scss/src/code/LivePreview.scss +1 -0
- package/resources/scss/src/examples/videoMove/MainContainer.scss +31 -0
- package/resources/scss/theme-neo-light/design-tokens/Core.scss +1 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +5 -1
- package/src/code/LivePreview.mjs +16 -19
- package/src/component/Toast.mjs +8 -8
- package/src/component/Video.mjs +22 -28
- package/src/component/wrapper/AmChart.mjs +15 -8
- package/src/main/addon/AmCharts.mjs +1 -1
- package/src/manager/DomEvent.mjs +1 -1
- package/src/worker/App.mjs +1 -1
- package/src/worker/mixin/RemoteMethodAccess.mjs +1 -3
- package/resources/data/deck/learnneo/pages/WhyNeo-Multi-Threaded.md +0 -15
@@ -0,0 +1,68 @@
|
|
1
|
+
.portal-home-feature-section {
|
2
|
+
align-items : stretch;
|
3
|
+
display : flex;
|
4
|
+
flex-wrap : nowrap;
|
5
|
+
justify-content : center;
|
6
|
+
min-height : 100%;
|
7
|
+
scroll-snap-align: center;
|
8
|
+
|
9
|
+
@media (max-width: 840px) {
|
10
|
+
flex-direction: column;
|
11
|
+
}
|
12
|
+
|
13
|
+
&.portal-position-end {
|
14
|
+
flex-direction: row-reverse;
|
15
|
+
}
|
16
|
+
|
17
|
+
.neo-content {
|
18
|
+
font-size: min(max(2.3vw, 16px), 30px);
|
19
|
+
}
|
20
|
+
|
21
|
+
.neo-h1 {
|
22
|
+
font-size : min(max(5.5vw, 30px), 64px);
|
23
|
+
line-height: 1em;
|
24
|
+
margin : 0;
|
25
|
+
text-align : center;
|
26
|
+
}
|
27
|
+
|
28
|
+
.neo-h2 {
|
29
|
+
font-size : min(max(3.5vw, 24px), 44px);
|
30
|
+
font-weight: 600;
|
31
|
+
line-height: 1em;
|
32
|
+
margin : 0;
|
33
|
+
text-align : center;
|
34
|
+
}
|
35
|
+
|
36
|
+
.page-live-preview {
|
37
|
+
height: 100%;
|
38
|
+
margin: 0;
|
39
|
+
}
|
40
|
+
|
41
|
+
.portal-content-text {
|
42
|
+
align-items : center;
|
43
|
+
display : flex;
|
44
|
+
flex : 1;
|
45
|
+
flex-direction : column;
|
46
|
+
flex-wrap : nowrap;
|
47
|
+
justify-content: center;
|
48
|
+
padding : 2rem;
|
49
|
+
|
50
|
+
@media (max-width: 600px) {
|
51
|
+
flex : .5 !important;
|
52
|
+
justify-content: start;
|
53
|
+
padding : 1rem;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
.portal-content-wrapper {
|
58
|
+
background-color: lightgray;
|
59
|
+
flex : 2;
|
60
|
+
padding : 20px;
|
61
|
+
|
62
|
+
@media (max-width: 600px) {
|
63
|
+
max-height: 35em;
|
64
|
+
min-height: 35em;
|
65
|
+
padding : 5px;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
@@ -1,25 +1,7 @@
|
|
1
1
|
.portal-home-parts-how {
|
2
|
-
.neo-worker-setup {
|
3
|
-
align-items : center;
|
4
|
-
display : flex;
|
5
|
-
height : 100%;
|
6
|
-
justify-content: center;
|
7
|
-
width : 100%;
|
8
|
-
|
9
|
-
--fill-opacity : 0.05;
|
10
|
-
--stroke-opacity: 0.05;
|
11
|
-
|
12
|
-
&:hover {
|
13
|
-
--fill-opacity : 1;
|
14
|
-
--stroke-opacity: 1;
|
15
|
-
}
|
16
|
-
}
|
17
|
-
|
18
2
|
.portal-content-container {
|
19
|
-
background-color: #17141c;
|
20
3
|
border : 1px solid #e6e6e6;
|
21
4
|
border-radius : 8px;
|
22
|
-
padding : 20px;
|
23
5
|
}
|
24
6
|
|
25
7
|
@media (max-width: 500px) {
|
@@ -33,10 +15,6 @@
|
|
33
15
|
@media (max-width: 840px) {
|
34
16
|
min-height: 100%;
|
35
17
|
|
36
|
-
&.neo-flex-container {
|
37
|
-
flex-direction: column;
|
38
|
-
}
|
39
|
-
|
40
18
|
.portal-content-text {
|
41
19
|
flex: 0.8 !important;
|
42
20
|
}
|
@@ -12,15 +12,16 @@
|
|
12
12
|
padding: 1rem 1rem 0;
|
13
13
|
}
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
border-radius: 4px;
|
18
|
-
overflow-x : scroll;
|
19
|
-
padding : 12px;
|
15
|
+
a {
|
16
|
+
color: #3E63DD;;
|
20
17
|
}
|
21
18
|
|
22
|
-
|
23
|
-
|
19
|
+
blockquote {
|
20
|
+
background-color: var(--gray-100);
|
21
|
+
border-left : 4px solid var(--sem-color-surface-primary-background);
|
22
|
+
font-style : italic;
|
23
|
+
margin-left : 0;
|
24
|
+
padding : 5px 5px 5px 15px;
|
24
25
|
}
|
25
26
|
|
26
27
|
details {
|
@@ -72,11 +73,22 @@
|
|
72
73
|
padding : 2px 16px;
|
73
74
|
font-size : 1em;
|
74
75
|
margin-bottom: 1em;
|
76
|
+
|
77
|
+
&:hover {
|
78
|
+
/* On mouse-over, add a deeper shadow */
|
79
|
+
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
80
|
+
}
|
75
81
|
}
|
76
82
|
|
77
|
-
|
78
|
-
|
79
|
-
|
83
|
+
p {
|
84
|
+
margin: 0.5em 0 0.7em 0;
|
85
|
+
}
|
86
|
+
|
87
|
+
pre[data-javascript] {
|
88
|
+
border : thin solid lightgray;
|
89
|
+
border-radius: 4px;
|
90
|
+
overflow-x : scroll;
|
91
|
+
padding : 12px;
|
80
92
|
}
|
81
93
|
}
|
82
94
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
.examples-videomove-maincontainer.neo-viewport {
|
2
|
+
padding: 50px;
|
3
|
+
|
4
|
+
.video-wrapper {
|
5
|
+
.neo-container {
|
6
|
+
background-color: rgb(139,166,255);
|
7
|
+
padding : 50px;
|
8
|
+
|
9
|
+
&:last-child {
|
10
|
+
margin-left: 50px;
|
11
|
+
}
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
@media (max-width: 840px) {
|
16
|
+
padding: 1em;
|
17
|
+
|
18
|
+
.video-wrapper {
|
19
|
+
flex-direction: column;
|
20
|
+
|
21
|
+
.neo-container {
|
22
|
+
padding: 1em;
|
23
|
+
|
24
|
+
&:last-child {
|
25
|
+
margin-left: 0;
|
26
|
+
margin-top : 1em;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
package/src/DefaultConfig.mjs
CHANGED
@@ -260,12 +260,12 @@ const DefaultConfig = {
|
|
260
260
|
useVdomWorker: true,
|
261
261
|
/**
|
262
262
|
* buildScripts/injectPackageVersion.mjs will update this value
|
263
|
-
* @default '6.
|
263
|
+
* @default '6.37.0'
|
264
264
|
* @memberOf! module:Neo
|
265
265
|
* @name config.version
|
266
266
|
* @type String
|
267
267
|
*/
|
268
|
-
version: '6.
|
268
|
+
version: '6.37.0'
|
269
269
|
};
|
270
270
|
|
271
271
|
Object.assign(DefaultConfig, {
|
package/src/Neo.mjs
CHANGED
@@ -633,7 +633,7 @@ function autoGenerateGetSet(proto, key) {
|
|
633
633
|
}
|
634
634
|
|
635
635
|
if (!Neo[getSetCache]) {
|
636
|
-
Neo[getSetCache] = {}
|
636
|
+
Neo[getSetCache] = {}
|
637
637
|
}
|
638
638
|
|
639
639
|
if (!Neo[getSetCache][key]) {
|
@@ -667,6 +667,10 @@ function autoGenerateGetSet(proto, key) {
|
|
667
667
|
},
|
668
668
|
|
669
669
|
set(value) {
|
670
|
+
if (value === undefined) {
|
671
|
+
return
|
672
|
+
}
|
673
|
+
|
670
674
|
let me = this,
|
671
675
|
_key = '_' + key,
|
672
676
|
uKey = key[0].toUpperCase() + key.slice(1),
|
package/src/code/LivePreview.mjs
CHANGED
@@ -48,10 +48,6 @@ class LivePreview extends Container {
|
|
48
48
|
* @member {Boolean} enableFullscreen=true
|
49
49
|
*/
|
50
50
|
enableFullscreen: true,
|
51
|
-
/**
|
52
|
-
* @member {Number} height=400
|
53
|
-
*/
|
54
|
-
height: 400,
|
55
51
|
/**
|
56
52
|
* @member {Object|String} layout='fit'
|
57
53
|
*/
|
@@ -226,11 +222,14 @@ class LivePreview extends Container {
|
|
226
222
|
}
|
227
223
|
|
228
224
|
let me = this,
|
225
|
+
container = me.getPreviewContainer(),
|
229
226
|
source = me.editorValue || me.value,
|
230
227
|
cleanLines = [],
|
231
|
-
importModuleNames = [],
|
232
228
|
moduleNameAndPath = [],
|
233
|
-
className = me.findLastClassName(source)
|
229
|
+
className = me.findLastClassName(source),
|
230
|
+
params = [],
|
231
|
+
vars = [],
|
232
|
+
codeString, promises;
|
234
233
|
|
235
234
|
source.split('\n').forEach(line => {
|
236
235
|
let importMatch = line.match(importRegex);
|
@@ -239,18 +238,14 @@ class LivePreview extends Container {
|
|
239
238
|
let moduleName = importMatch[1],
|
240
239
|
path = importMatch[2];
|
241
240
|
|
242
|
-
moduleNameAndPath.push({moduleName, path})
|
243
|
-
|
244
|
-
importModuleNames.push(moduleName);
|
241
|
+
moduleNameAndPath.push({moduleName, path})
|
245
242
|
} else if (line.match(exportRegex)) {
|
246
243
|
// Skip export statements
|
247
244
|
} else {
|
248
|
-
cleanLines.push(` ${line}`)
|
245
|
+
cleanLines.push(` ${line}`)
|
249
246
|
}
|
250
247
|
});
|
251
248
|
|
252
|
-
var params = [];
|
253
|
-
var vars = [];
|
254
249
|
// Figure out the parts of the source we'll be running.
|
255
250
|
// o The promises/import() corresponding to the user's import statements
|
256
251
|
// o The vars holding the name of the imported module based on the module name for each import
|
@@ -266,14 +261,13 @@ class LivePreview extends Container {
|
|
266
261
|
// });
|
267
262
|
// Making the promise part of the eval seems weird, but it made it easier to
|
268
263
|
// set up the import vars.
|
269
|
-
|
270
|
-
let promises = moduleNameAndPath.map(item => {
|
264
|
+
promises = moduleNameAndPath.map(item => {
|
271
265
|
params.push(`${item.moduleName}Module`);
|
272
266
|
vars.push(` const ${item.moduleName} = ${item.moduleName}Module.default;`);
|
273
267
|
return `import('${item.path}')`
|
274
268
|
});
|
275
269
|
|
276
|
-
|
270
|
+
codeString = [
|
277
271
|
'Promise.all([',
|
278
272
|
` ${promises.join(',\n')}`,
|
279
273
|
`]).then(([${params.join(', ')}]) => {`,
|
@@ -287,7 +281,6 @@ class LivePreview extends Container {
|
|
287
281
|
'.catch(error => container.add({ntype:\'component\', html:error.message}));'
|
288
282
|
].join('\n')
|
289
283
|
|
290
|
-
const container = me.getPreviewContainer();
|
291
284
|
container.removeAll();
|
292
285
|
|
293
286
|
try {
|
@@ -338,14 +331,18 @@ class LivePreview extends Container {
|
|
338
331
|
* @param {Number} data.value
|
339
332
|
*/
|
340
333
|
onActiveIndexChange(data) {
|
341
|
-
let me
|
342
|
-
|
334
|
+
let me = this,
|
335
|
+
isPreview = data.value === 1;
|
343
336
|
|
344
337
|
if (data.item.reference === 'preview') {
|
345
338
|
me.doRunSource()
|
346
339
|
}
|
340
|
+
// Navigating to the source view should destroy the app, in case the preview view is not popped out
|
341
|
+
else if (!isPreview && !me.previewContainer) {
|
342
|
+
me.getReference('preview').removeAll()
|
343
|
+
}
|
347
344
|
|
348
|
-
me.getReference('popout-window-button').hidden =
|
345
|
+
me.getReference('popout-window-button').hidden = !isPreview
|
349
346
|
me.disableRunSource = false;
|
350
347
|
}
|
351
348
|
|
package/src/component/Toast.mjs
CHANGED
@@ -114,17 +114,17 @@ class Toast extends Base {
|
|
114
114
|
}]}
|
115
115
|
}
|
116
116
|
|
117
|
+
/**
|
118
|
+
* Timeout in ms after which the toast is removed
|
119
|
+
* @member {Number} removeDelay=3000
|
120
|
+
*/
|
121
|
+
removeDelay = 3000
|
117
122
|
/**
|
118
123
|
* Used by the ToastManager
|
119
124
|
* @member {Boolean} running=false
|
120
125
|
* @private
|
121
126
|
*/
|
122
127
|
running = false
|
123
|
-
/**
|
124
|
-
* Timeout in ms after which the toast is removed
|
125
|
-
* @member {Number} timeout=3000
|
126
|
-
*/
|
127
|
-
timeout = 3000 // todo: conflicting class field name => timeout()
|
128
128
|
|
129
129
|
/**
|
130
130
|
* @param {Object} config
|
@@ -206,9 +206,9 @@ class Toast extends Base {
|
|
206
206
|
let me = this;
|
207
207
|
|
208
208
|
if (!me.closable && value) {
|
209
|
-
|
210
|
-
|
211
|
-
}
|
209
|
+
me.timeout(me.removeDelay).then(() => {
|
210
|
+
me.destroy(true)
|
211
|
+
})
|
212
212
|
}
|
213
213
|
}
|
214
214
|
|
package/src/component/Video.mjs
CHANGED
@@ -38,8 +38,7 @@ class Video extends BaseComponent {
|
|
38
38
|
*/
|
39
39
|
autoplay: false,
|
40
40
|
/**
|
41
|
-
* In case the browser does not support the video source
|
42
|
-
* the component should show an error.
|
41
|
+
* In case the browser does not support the video source the component should show an error.
|
43
42
|
* @member {Boolean} errorMsg='The browser does not support the video'
|
44
43
|
*/
|
45
44
|
errorMsg: 'Your browser does not support the video tag.',
|
@@ -90,25 +89,7 @@ class Video extends BaseComponent {
|
|
90
89
|
let me = this;
|
91
90
|
|
92
91
|
me.handleAutoplay();
|
93
|
-
me.addDomListeners(
|
94
|
-
{click: me.play, delegate: '.neo-video-ghost'},
|
95
|
-
{click: me.pause, delegate: '.neo-video-media'}
|
96
|
-
)
|
97
|
-
}
|
98
|
-
|
99
|
-
/**
|
100
|
-
* beforeSetPlaying autgen by playing_
|
101
|
-
*
|
102
|
-
* @param {Boolean} value
|
103
|
-
* @param {Boolean} oldValue
|
104
|
-
* @returns {Boolean}
|
105
|
-
*/
|
106
|
-
beforeSetPlaying(value, oldValue) {
|
107
|
-
if (!Neo.isBoolean(value)) {
|
108
|
-
return oldValue
|
109
|
-
}
|
110
|
-
|
111
|
-
return value
|
92
|
+
me.addDomListeners({click: me.play, delegate: '.neo-video-ghost'});
|
112
93
|
}
|
113
94
|
|
114
95
|
/**
|
@@ -137,23 +118,37 @@ class Video extends BaseComponent {
|
|
137
118
|
afterSetUrl(value, oldValue) {
|
138
119
|
if (!value) return;
|
139
120
|
|
140
|
-
let {vdom}
|
141
|
-
media
|
121
|
+
let {vdom} = this,
|
122
|
+
media = VDomUtil.getFlags(vdom, 'media')[0],
|
123
|
+
ua = navigator.userAgent || navigator.vendor || window.opera,
|
124
|
+
isOperaMini = /Opera Mini/i.test(ua);
|
142
125
|
|
143
126
|
media.cn = [{
|
144
127
|
tag: 'source',
|
145
128
|
src: value,
|
146
129
|
type: this.type
|
147
|
-
}, {
|
148
|
-
tag: 'span',
|
149
|
-
html: this.errorMsg,
|
150
130
|
}];
|
151
131
|
|
132
|
+
// Opera Mini might not support the video-source => check the user agent string
|
133
|
+
if (isOperaMini) {
|
134
|
+
media.cn.push({
|
135
|
+
tag: 'span',
|
136
|
+
html: this.errorMsg,
|
137
|
+
});
|
138
|
+
}
|
139
|
+
|
152
140
|
this.update()
|
153
141
|
}
|
154
142
|
|
155
143
|
/**
|
156
144
|
* autoplay - run the event listeners
|
145
|
+
* Only called in constructor and sets playing => calls update already
|
146
|
+
*
|
147
|
+
* Rationale: update() sends the vdom & vnode on a workers roundtrip to get the deltas.
|
148
|
+
* While this is happening, the component locks itself for future updates until the new vnode got back (async).
|
149
|
+
* After the delay the framework would trigger a 2nd roundtrip to get the deltas for the visible node.
|
150
|
+
*
|
151
|
+
* @protected
|
157
152
|
*/
|
158
153
|
handleAutoplay() {
|
159
154
|
if (!this.autoplay) return;
|
@@ -166,12 +161,11 @@ class Video extends BaseComponent {
|
|
166
161
|
// Allows inline playback on iOS devices
|
167
162
|
media.playsInline = true;
|
168
163
|
|
169
|
-
this.update();
|
170
164
|
this.playing = true;
|
171
165
|
}
|
172
166
|
|
173
167
|
/**
|
174
|
-
* Clicked media
|
168
|
+
* Simulates `Clicked media` programmatically
|
175
169
|
*/
|
176
170
|
pause() {
|
177
171
|
this.playing = false
|
@@ -89,24 +89,23 @@ class AmChart extends Component {
|
|
89
89
|
* @protected
|
90
90
|
*/
|
91
91
|
afterSetMounted(value, oldValue) {
|
92
|
-
let me
|
93
|
-
{appName, id, windowId} = me
|
92
|
+
let me = this,
|
93
|
+
{appName, id, windowId} = me,
|
94
|
+
opts = {appName, id, windowId};
|
94
95
|
|
95
96
|
if (value === false && oldValue !== undefined) {
|
96
|
-
Neo.main.addon.AmCharts.destroy(
|
97
|
+
Neo.main.addon.AmCharts.destroy(opts)
|
97
98
|
}
|
98
99
|
|
99
100
|
super.afterSetMounted(value, oldValue);
|
100
101
|
|
101
102
|
if (value) {
|
102
|
-
|
103
|
-
|
103
|
+
opts = {
|
104
|
+
...opts,
|
104
105
|
combineSeriesTooltip: me.combineSeriesTooltip,
|
105
106
|
config : me.chartConfig,
|
106
|
-
id,
|
107
107
|
package : me.package,
|
108
|
-
type : me.chartType
|
109
|
-
windowId
|
108
|
+
type : me.chartType
|
110
109
|
};
|
111
110
|
|
112
111
|
if (me.chartData) {
|
@@ -136,6 +135,14 @@ class AmChart extends Component {
|
|
136
135
|
return value
|
137
136
|
}
|
138
137
|
|
138
|
+
destroy(...args) {
|
139
|
+
let {appName, id, windowId} = this;
|
140
|
+
|
141
|
+
Neo.main.addon.AmCharts.destroy({appName, id, windowId})
|
142
|
+
|
143
|
+
super.destroy(...args)
|
144
|
+
}
|
145
|
+
|
139
146
|
/**
|
140
147
|
*
|
141
148
|
*/
|
package/src/manager/DomEvent.mjs
CHANGED
package/src/worker/App.mjs
CHANGED
@@ -384,7 +384,7 @@ class App extends Base {
|
|
384
384
|
* @param {Object} data
|
385
385
|
* @param {Number} data.angle
|
386
386
|
* @param {String} data.layout landscape|portrait
|
387
|
-
* @param {
|
387
|
+
* @param {String} data.type landscape-primary|landscape-secondary|portrait-primary|portrait-secondary
|
388
388
|
*/
|
389
389
|
onOrientationChange(data) {
|
390
390
|
Object.values(Neo.apps).forEach(app => {
|
@@ -67,9 +67,7 @@ class RemoteMethodAccess extends Base {
|
|
67
67
|
throw new Error('Duplicate remote method definition ' + className + '.' + method)
|
68
68
|
}
|
69
69
|
|
70
|
-
|
71
|
-
pkg[method] = me.generateRemote(remote, method)
|
72
|
-
}
|
70
|
+
pkg[method] ??= me.generateRemote(remote, method)
|
73
71
|
})
|
74
72
|
}
|
75
73
|
}
|
@@ -1,15 +0,0 @@
|
|
1
|
-
When a Neo.mjs application starts, the framework spawns three web-workers, in addition
|
2
|
-
to the main browser thread, resulting in:
|
3
|
-
|
4
|
-
1. The <b>main</b> browser thread, where DOM updates are applied
|
5
|
-
2. An <b>application</b> web-worker where normal application logic is run
|
6
|
-
3. A <b>data</b> web-worker were HTTP and socket calls are run
|
7
|
-
4. A <b>view</b> web-worker that manages delta updates
|
8
|
-
|
9
|
-
<img src="https://s3.amazonaws.com/mjs.neo.learning.images/why/IndexHtmlFlow.png" width="120%"></img>
|
10
|
-
|
11
|
-
The benefits of using web workers is that each runs in parallel its own thread. In a typical framework
|
12
|
-
all code is run in the main thread, so processes compete for CPU cycles.
|
13
|
-
|
14
|
-
Neo.mjs also allows you to easily spawn additional threads in order to have processor-intensive
|
15
|
-
tasks to be run separately.
|