[adapters] 增加资源管线的多线程支持

This commit is contained in:
SmallMain
2024-10-21 20:23:37 +08:00
parent 6aec2d7cfa
commit 29427f6bf6
18 changed files with 1772 additions and 60 deletions

View File

@@ -0,0 +1,347 @@
const { getUserDataPath, readJsonSync, deleteFileSync, makeDirSync, writeFileSync, copyFile, downloadFile, writeFile, deleteFile, rmdirSync, unzip, isOutOfStorage } = require("./fs-utils.js");
const { extname } = require("./path.js");
const { main } = require("./ipc-worker.js");
var checkNextPeriod = false;
var writeCacheFileTimer = null;
var suffix = 0;
const REGEX = /^https?:\/\/.*/;
function isEmptyObject(obj) {
for (var key in obj) {
return false;
}
return true;
}
var cacheManager_worker = {
cacheDir: 'gamecaches',
cachedFileName: 'cacheList.json',
// whether or not cache asset into user's storage space
cacheEnabled: true,
// whether or not auto clear cache when storage ran out
autoClear: true,
// cache one per cycle
cacheInterval: 500,
deleteInterval: 500,
writeFileInterval: 2000,
// whether or not storage space has run out
outOfStorage: false,
tempFiles: null,
cachedFiles: null,
cacheQueue: {},
version: '1.0',
init(callback) {
this.cacheDir = getUserDataPath() + '/' + this.cacheDir;
var cacheFilePath = this.cacheDir + '/' + this.cachedFileName;
var result = readJsonSync(cacheFilePath);
if (result instanceof Error || !result.version) {
if (!(result instanceof Error)) rmdirSync(this.cacheDir, true);
this.cachedFiles = {};
makeDirSync(this.cacheDir, true);
writeFileSync(cacheFilePath, JSON.stringify({ files: this.cachedFiles, version: this.version }), 'utf8');
}
else {
this.cachedFiles = result.files;
}
this.tempFiles = {};
callback(this.cachedFiles);
},
transformUrl(url, reload) {
var inLocal = false;
var inCache = false;
var isInUserDataPath = url.startsWith(getUserDataPath());
if (isInUserDataPath) {
inLocal = true;
} else if (REGEX.test(url)) {
if (!reload) {
var cache = this.cachedFiles[url];
if (cache) {
inCache = true;
url = cache.url;
} else {
var tempUrl = this.tempFiles[url];
if (tempUrl) {
inLocal = true;
url = tempUrl;
}
}
}
} else {
inLocal = true;
}
return { url, inLocal, inCache };
},
download(
callback,
url,
options_reload,
options_header,
options_cacheEnabled,
options___cacheBundleRoot__,
) {
var result = this.transformUrl(url, options_reload);
if (result.inLocal) {
callback(null, result.url);
} else if (result.inCache) {
this.updateLastTime(url);
callback(null, result.url);
}
else {
downloadFile(url, null, options_header, null, (err, path) => {
if (err) {
callback(err.message, null);
return;
}
this.tempFiles[url] = path;
this.cacheFile(null, url, path, options_cacheEnabled, options___cacheBundleRoot__, true);
callback(null, path);
});
}
},
handleZip(
callback,
url,
options_header,
options___cacheBundleRoot__,
) {
let cachedUnzip = this.cachedFiles[url];
if (cachedUnzip) {
this.updateLastTime(url);
callback(null, cachedUnzip.url);
} else if (REGEX.test(url)) {
downloadFile(url, null, options_header, null, (err, downloadedZipPath) => {
if (err) {
callback(err.message, null);
return;
}
this.unzipAndCacheBundle(url, downloadedZipPath, options___cacheBundleRoot__, callback);
});
} else {
this.unzipAndCacheBundle(url, url, options___cacheBundleRoot__, callback);
}
},
getTemp(callback, url) {
callback(this.tempFiles.has(url) ? this.tempFiles.get(url) : '');
},
updateLastTime(url) {
if (this.cachedFiles[url]) {
var cache = this.cachedFiles[url];
cache.lastTime = Date.now();
}
},
writeCacheFile() {
if (!writeCacheFileTimer) {
writeCacheFileTimer = setTimeout(() => {
writeCacheFileTimer = null;
writeFile(this.cacheDir + '/' + this.cachedFileName, JSON.stringify({ files: this.cachedFiles, version: this.version }), "utf8", () => { });
}, this.writeFileInterval);
}
},
writeCacheFileSync() {
writeFileSync(this.cacheDir + '/' + this.cachedFileName, JSON.stringify({ files: this.cachedFiles, version: this.version }), "utf8");
},
_cache() {
var self = this;
for (var id in this.cacheQueue) {
var { srcUrl, isCopy, cacheBundleRoot, callback: _callback } = this.cacheQueue[id];
var time = Date.now().toString();
var localPath = '';
if (cacheBundleRoot) {
localPath = `${this.cacheDir}/${cacheBundleRoot}/${time}${suffix++}${extname(id)}`;
}
else {
localPath = `${this.cacheDir}/${time}${suffix++}${extname(id)}`;
}
function callback(err) {
checkNextPeriod = false;
if (err) {
if (isOutOfStorage(err.message)) {
self.outOfStorage = true;
self.autoClear && self.clearLRU();
return;
}
} else {
self.cachedFiles[id] = { bundle: cacheBundleRoot, url: localPath, lastTime: time };
delete self.cacheQueue[id];
self.writeCacheFile();
// TODO main.assetManager.addCachedFiles([[id, cacheBundleRoot, localPath, time]]);
if (_callback) _callback(id, cacheBundleRoot, localPath, time);
}
if (!isEmptyObject(self.cacheQueue)) {
checkNextPeriod = true;
setTimeout(self._cache.bind(self), self.cacheInterval);
}
}
if (!isCopy) {
downloadFile(srcUrl, localPath, null, callback);
}
else {
copyFile(srcUrl, localPath, callback);
}
return;
}
checkNextPeriod = false;
},
cacheFile(callback, id, srcUrl, cacheEnabled, cacheBundleRoot, isCopy) {
cacheEnabled = cacheEnabled !== undefined ? cacheEnabled : this.cacheEnabled;
if (!cacheEnabled || this.cacheQueue[id] || this.cachedFiles[id]) {
if (callback) callback(null);
return;
}
this.cacheQueue[id] = { srcUrl, cacheBundleRoot, isCopy, callback };
if (!checkNextPeriod) {
checkNextPeriod = true;
if (!this.outOfStorage) {
setTimeout(this._cache.bind(this), this.cacheInterval);
}
else {
checkNextPeriod = false;
}
}
},
clearCache(callback) {
main.assetManager.getAllBundles(bundles => {
this.cachedFiles = {};
this.writeCacheFileSync();
rmdirSync(this.cacheDir, true);
makeDirSync(this.cacheDir, true);
this.outOfStorage = false;
bundles.forEach(bundle => {
const [name, base] = bundle;
if (REGEX.test(base)) this.makeBundleFolder(name);
});
callback();
});
},
clearLRU(callback) {
main.assetManager.getAllBundles(bundles => {
var caches = [];
var self = this;
for (const key in this.cachedFiles) {
const val = this.cachedFiles[key];
if (val.bundle === 'internal') continue;
if (self._isZipFile(key) && bundles.find(bundle => bundle[1].indexOf(val.url) !== -1)) continue;
caches.push({ originUrl: key, url: val.url, lastTime: val.lastTime });
}
caches.sort(function (a, b) {
return a.lastTime - b.lastTime;
});
caches.length = Math.floor(caches.length / 3);
if (caches.length === 0) {
if (callback) {
callback([]);
}
return;
}
for (var i = 0, l = caches.length; i < l; i++) {
const item = caches[i];
delete this.cachedFiles[item.originUrl];
if (self._isZipFile(item.originUrl)) {
rmdirSync(item.url, true);
} else {
deleteFileSync(item.url);
}
}
this.outOfStorage = false;
self.writeCacheFileSync();
if (callback) {
callback(caches.map(v => v.originUrl));
} else {
main.assetManager.removeCachedFiles(caches.map(v => v.originUrl));
}
});
},
removeCache(callback, url) {
if (this.cachedFiles[url]) {
var self = this;
var path = this.cachedFiles[url].url;
delete this.cachedFiles[url];
if (self._isZipFile(url)) {
rmdirSync(path, true);
} else {
deleteFileSync(path);
}
this.outOfStorage = false;
self.writeCacheFileSync();
}
if (callback) callback();
},
makeBundleFolder(bundleName) {
makeDirSync(this.cacheDir + '/' + bundleName, true);
},
unzipAndCacheBundle(id, zipFilePath, cacheBundleRoot, onComplete) {
let time = Date.now().toString();
let targetPath = `${this.cacheDir}/${cacheBundleRoot}/${time}${suffix++}`;
let self = this;
makeDirSync(targetPath, true);
unzip(zipFilePath, targetPath, function (err) {
if (err) {
rmdirSync(targetPath, true);
if (isOutOfStorage(err.message)) {
self.outOfStorage = true;
self.autoClear && self.clearLRU();
}
onComplete && onComplete(err);
return;
}
self.cachedFiles[id] = { bundle: cacheBundleRoot, url: targetPath, lastTime: time };
self.writeCacheFile();
main.assetManager.addCachedFiles([[id, cacheBundleRoot, targetPath, time]]);
onComplete && onComplete(null, targetPath);
});
},
_isZipFile(url) {
return url.slice(-4) === '.zip';
},
};
module.exports = cacheManager_worker;

View File

@@ -0,0 +1,261 @@
/****************************************************************************
Copyright (c) 2017-2019 Xiamen Yaji Software Co., Ltd.
https://www.cocos.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of fsUtils software and associated engine source code (the "Software"), a limited,
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
to use Cocos Creator solely to develop games on your target platforms. You shall
not use Cocos Creator software for developing other software or tools that's
used for developing games. You are not granted to publish, distribute,
sublicense, and/or sell copies of Cocos Creator.
The software or tools in fsUtils License Agreement are licensed, not sold.
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
var fs = worker.getFileSystemManager();
var outOfStorageRegExp = /the maximum size of the file storage/;
var fsUtils = {
fs,
isOutOfStorage(errMsg) {
return outOfStorageRegExp.test(errMsg);
},
getUserDataPath() {
return worker.env.USER_DATA_PATH;
},
checkFsValid() {
if (!fs) {
console.warn('can not get the file system!');
return false;
}
return true;
},
deleteFile(filePath, onComplete) {
fs.unlink({
filePath: filePath,
success: function () {
onComplete && onComplete(null);
},
fail: function (res) {
console.warn(`Delete file failed: path: ${filePath} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg));
}
});
},
deleteFileSync(filePath, onComplete) {
try {
fs.unlinkSync(filePath);
onComplete && onComplete(null);
} catch (error) {
console.warn(`Delete file failed: path: ${filePath} message: ${error.message}`);
onComplete && onComplete(error);
}
},
downloadFile(remoteUrl, filePath, header, onProgress, onComplete) {
var options = {
url: remoteUrl,
success: function (res) {
if (res.statusCode === 200) {
onComplete && onComplete(null, res.tempFilePath || res.filePath);
}
else {
if (res.filePath) {
fsUtils.deleteFile(res.filePath);
}
console.warn(`Download file failed: path: ${remoteUrl} message: ${res.statusCode}`);
onComplete && onComplete(new Error(res.statusCode), null);
}
},
fail: function (res) {
console.warn(`Download file failed: path: ${remoteUrl} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg), null);
}
}
if (filePath) options.filePath = filePath;
if (header) options.header = header;
var task = worker.downloadFile(options);
onProgress && task.onProgressUpdate(onProgress);
},
saveFile(srcPath, destPath, onComplete) {
fs.saveFile({
tempFilePath: srcPath,
filePath: destPath,
success: function (res) {
onComplete && onComplete(null);
},
fail: function (res) {
console.warn(`Save file failed: path: ${srcPath} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg));
}
});
},
copyFile(srcPath, destPath, onComplete) {
fs.copyFile({
srcPath: srcPath,
destPath: destPath,
success: function () {
onComplete && onComplete(null);
},
fail: function (res) {
console.warn(`Copy file failed: path: ${srcPath} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg));
}
});
},
writeFile(path, data, encoding, onComplete) {
fs.writeFile({
filePath: path,
encoding: encoding,
data: data,
success: function () {
onComplete && onComplete(null);
},
fail: function (res) {
console.warn(`Write file failed: path: ${path} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg));
}
});
},
writeFileSync(path, data, encoding) {
try {
fs.writeFileSync(path, data, encoding);
return null;
}
catch (e) {
console.warn(`Write file failed: path: ${path} message: ${e.message}`);
return new Error(e.message);
}
},
readFile(filePath, encoding, onComplete) {
fs.readFile({
filePath: filePath,
encoding: encoding,
success: function (res) {
onComplete && onComplete(null, res.data);
},
fail: function (res) {
console.warn(`Read file failed: path: ${filePath} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg), null);
}
});
},
readDir(filePath, onComplete) {
fs.readdir({
dirPath: filePath,
success: function (res) {
onComplete && onComplete(null, res.files);
},
fail: function (res) {
console.warn(`Read directory failed: path: ${filePath} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg), null);
}
});
},
readText(filePath, onComplete) {
fsUtils.readFile(filePath, 'utf8', onComplete);
},
readArrayBuffer(filePath, onComplete) {
fsUtils.readFile(filePath, '', onComplete);
},
readJson(filePath, onComplete) {
fsUtils.readFile(filePath, 'utf8', function (err, text) {
var out = null;
if (!err) {
try {
out = JSON.parse(text);
}
catch (e) {
console.warn(`Read json failed: path: ${filePath} message: ${e.message}`);
err = new Error(e.message);
}
}
onComplete && onComplete(err, out);
});
},
readJsonSync(path) {
try {
var str = fs.readFileSync(path, 'utf8');
return JSON.parse(str);
}
catch (e) {
console.warn(`Read json failed: path: ${path} message: ${e.message}`);
return new Error(e.message);
}
},
makeDirSync(path, recursive) {
try {
fs.mkdirSync(path, recursive);
return null;
}
catch (e) {
console.warn(`Make directory failed: path: ${path} message: ${e.message}`);
return new Error(e.message);
}
},
rmdirSync(dirPath, recursive) {
try {
fs.rmdirSync(dirPath, recursive);
}
catch (e) {
console.warn(`rm directory failed: path: ${dirPath} message: ${e.message}`);
return new Error(e.message);
}
},
exists(filePath, onComplete) {
fs.access({
path: filePath,
success: function () {
onComplete && onComplete(true);
},
fail: function () {
onComplete && onComplete(false);
}
});
},
unzip(zipFilePath, targetPath, onComplete) {
fs.unzip({
zipFilePath,
targetPath,
success() {
onComplete && onComplete(null);
},
fail(res) {
console.warn(`unzip failed: path: ${zipFilePath} message: ${res.errMsg}`);
onComplete && onComplete(new Error('unzip failed: ' + res.errMsg));
},
})
},
};
module.exports = fsUtils;

View File

@@ -0,0 +1,235 @@
/****************************************************************************
Copyright (c) 2017-2019 Xiamen Yaji Software Co., Ltd.
https://www.cocos.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of fsUtils software and associated engine source code (the "Software"), a limited,
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
to use Cocos Creator solely to develop games on your target platforms. You shall
not use Cocos Creator software for developing other software or tools that's
used for developing games. You are not granted to publish, distribute,
sublicense, and/or sell copies of Cocos Creator.
The software or tools in fsUtils License Agreement are licensed, not sold.
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
var fs = worker.getFileSystemManager();
var outOfStorageRegExp = /the maximum size of the file storage/;
var fsUtils = {
fs,
isOutOfStorage(errMsg) {
return outOfStorageRegExp.test(errMsg);
},
getUserDataPath() {
return worker.env.USER_DATA_PATH;
},
checkFsValid() {
if (!fs) {
console.warn('can not get the file system!');
return false;
}
return true;
},
deleteFile(filePath, onComplete) {
try {
fs.unlinkSync(filePath);
onComplete && onComplete(null);
} catch (error) {
console.warn(`Delete file failed: path: ${filePath} message: ${error.message}`);
onComplete && onComplete(error);
}
},
deleteFileSync(filePath, onComplete) {
try {
fs.unlinkSync(filePath);
onComplete && onComplete(null);
} catch (error) {
console.warn(`Delete file failed: path: ${filePath} message: ${error.message}`);
onComplete && onComplete(error);
}
},
downloadFile(remoteUrl, filePath, header, onProgress, onComplete) {
var options = {
url: remoteUrl,
success: function (res) {
if (res.statusCode === 200) {
onComplete && onComplete(null, res.tempFilePath || res.filePath);
}
else {
if (res.filePath) {
fsUtils.deleteFile(res.filePath);
}
console.warn(`Download file failed: path: ${remoteUrl} message: ${res.statusCode}`);
onComplete && onComplete(new Error(res.statusCode), null);
}
},
fail: function (res) {
console.warn(`Download file failed: path: ${remoteUrl} message: ${res.errMsg}`);
onComplete && onComplete(new Error(res.errMsg), null);
}
}
if (filePath) options.filePath = filePath;
if (header) options.header = header;
var task = worker.downloadFile(options);
onProgress && task.onProgressUpdate(onProgress);
},
saveFile(srcPath, destPath, onComplete) {
try {
fs.saveFileSync(srcPath, destPath);
onComplete && onComplete(null);
} catch (error) {
console.warn(`Save file failed: path: ${srcPath} message: ${error.message}`);
onComplete && onComplete(error);
}
},
copyFile(srcPath, destPath, onComplete) {
try {
fs.copyFileSync(srcPath, destPath);
onComplete && onComplete(null);
} catch (error) {
console.warn(`Copy file failed: path: ${srcPath} message: ${error.message}`);
onComplete && onComplete(error);
}
},
writeFile(path, data, encoding, onComplete) {
try {
fs.writeFileSync(path, data, encoding);
onComplete && onComplete(null);
} catch (error) {
console.warn(`Write file failed: path: ${path} message: ${error.message}`);
onComplete && onComplete(error);
}
},
writeFileSync(path, data, encoding) {
try {
fs.writeFileSync(path, data, encoding);
return null;
}
catch (e) {
console.warn(`Write file failed: path: ${path} message: ${e.message}`);
return new Error(e.message);
}
},
readFile(filePath, encoding, onComplete) {
try {
var data = fs.readFileSync(filePath, encoding);
onComplete && onComplete(null, data);
} catch (error) {
console.warn(`Read file failed: path: ${filePath} message: ${error.message}`);
onComplete && onComplete(error, null);
}
},
readDir(filePath, onComplete) {
try {
var files = fs.readdirSync(filePath);
onComplete && onComplete(null, files);
} catch (error) {
console.warn(`Read directory failed: path: ${filePath} message: ${error.message}`);
onComplete && onComplete(error, null);
}
},
readText(filePath, onComplete) {
fsUtils.readFile(filePath, 'utf8', onComplete);
},
readArrayBuffer(filePath, onComplete) {
fsUtils.readFile(filePath, '', onComplete);
},
readJson(filePath, onComplete) {
fsUtils.readFile(filePath, 'utf8', function (err, text) {
var out = null;
if (!err) {
try {
out = JSON.parse(text);
}
catch (e) {
console.warn(`Read json failed: path: ${filePath} message: ${e.message}`);
err = new Error(e.message);
}
}
onComplete && onComplete(err, out);
});
},
readJsonSync(path) {
try {
var str = fs.readFileSync(path, 'utf8');
return JSON.parse(str);
}
catch (e) {
console.warn(`Read json failed: path: ${path} message: ${e.message}`);
return new Error(e.message);
}
},
makeDirSync(path, recursive) {
try {
fs.mkdirSync(path, recursive);
return null;
}
catch (e) {
console.warn(`Make directory failed: path: ${path} message: ${e.message}`);
return new Error(e.message);
}
},
rmdirSync(dirPath, recursive) {
try {
fs.rmdirSync(dirPath, recursive);
}
catch (e) {
console.warn(`rm directory failed: path: ${dirPath} message: ${e.message}`);
return new Error(e.message);
}
},
exists(filePath, onComplete) {
try {
fs.accessSync(filePath);
onComplete && onComplete(true);
} catch (error) {
onComplete && onComplete(false);
}
},
unzip(zipFilePath, targetPath, onComplete) {
fs.unzip({
zipFilePath,
targetPath,
success() {
onComplete && onComplete(null);
},
fail(res) {
console.warn(`unzip failed: path: ${zipFilePath} message: ${res.errMsg}`);
onComplete && onComplete(new Error('unzip failed: ' + res.errMsg));
},
})
},
};
module.exports = fsUtils;

View File

@@ -0,0 +1 @@
module.exports = globalThis.CC_WORKER_FS_SYNC ? require("./fs-utils-sync.js") : require("./fs-utils-async.js");

View File

@@ -0,0 +1,6 @@
const { registerHandler } = require("./ipc-worker.js");
if (globalThis.CC_WORKER_ASSET_PIPELINE) {
const cacheManager = require("./cache-manager-worker.js");
registerHandler("cacheManager", cacheManager);
}

View File

@@ -0,0 +1,6 @@
require("./macro.js");
const { init } = require("./ipc-worker.js");
init(() => {
require("./handlers.js");
});

View File

@@ -0,0 +1,232 @@
//
// IPC 说明
//
// - 从主线程调用 Worker
//
// 注册:
// 1.在 worker 端调用 registerHandler(name, obj) 注册处理对象。
// 2.所有非函数属性会生成 `get_xxx()`、`set_xxx(v)`、`write_xxx(v)` 三个函数,
// 其中,`write_` 函数会在设置完毕后回调到主线程。
// 3.所有函数属性请确保第一个参数是 callback用于回调到主线程
// 用法是 callback(...args)。
//
// 注册好函数后,在主线程通过 worker.name.key(args | null, (args)=>{}) 调用。
// 注意在主线程调用时传入和返回的都是参数数组。
//
// - 从 Worker 调用 主线程:
//
// 注册:
// 1.在 main 端调用 registerHandler(name, obj) 注册处理对象。
// 2.所有非函数属性会生成 `get_xxx()`、`set_xxx(v)`、`write_xxx(v)` 三个函数,
// 其中,`write_` 函数会在设置完毕后回调到 Worker。
// 3.所有函数属性请确保参数是 [args, cmdId, callback],用于回调到 Worker
// 用法是 callback(cmdId, args)。
// 注意在主线程回调时传入的是参数数组。
//
// 注册好函数后,在 Worker 通过 main.name.key(...args, (...args)=>{}) 调用。
// 最后一个参数如果是函数的话则会当作 callback 处理。
//
const { CC_WORKER_SCHEDULER, CC_WORKER_DEBUG } = globalThis;
var _inited = false;
var _initCallback = null;
var _cmdId = 0;
var callbacks = {};
var _cmd = 0;
var handlers = {};
function callToMain(cmd, args) {
const id = ++_cmdId;
args.unshift(id, cmd);
if (typeof args[args.length - 1] === "function") {
const callback = args.pop();
callbacks[id] = callback;
}
if (CC_WORKER_DEBUG) {
console.log("worker send call:", args);
}
if (CC_WORKER_SCHEDULER) {
sendScheduler.send(args);
} else {
worker.postMessage(args);
}
}
function callbackToMain(id, cmd, args) {
const msg = [id, cmd, args];
if (CC_WORKER_DEBUG) {
console.log("worker send callback:", msg);
}
if (CC_WORKER_SCHEDULER) {
sendScheduler.send(msg);
} else {
worker.postMessage(msg);
}
}
function registerHandler(name, obj) {
const descs = Object.getOwnPropertyDescriptors(obj);
for (const key in descs) {
const desc = descs[key];
if (typeof desc.value === "function") {
const cmd = ++_cmd;
handlers[cmd] = {
name,
key,
func: (id, cmd, args) => {
obj[key](
(...args) => {
callbackToMain(id, cmd, args);
},
...(args ? args : []),
);
},
};
} else {
// getter/setter
let cmd = ++_cmd;
handlers[cmd] = {
name,
key: "get_" + key,
func: (id, cmd, args) => {
callbackToMain(id, cmd, [obj[key]]);
}
};
cmd = ++_cmd;
handlers[cmd] = {
name,
key: "set_" + key,
func: (id, cmd, args) => {
obj[key] = args ? args[0] : undefined;
}
};
cmd = ++_cmd;
handlers[cmd] = {
name,
key: "write_" + key,
func: (id, cmd, args) => {
obj[key] = args ? args[0] : undefined;
callbackToMain(id, cmd, null);
}
};
}
}
}
function init(callback) {
_initCallback = callback;
if (CC_WORKER_SCHEDULER) {
sendScheduler.init();
}
worker.onMessage(CC_WORKER_SCHEDULER
? msgs => {
for (let index = 0; index < msgs.length; index++) {
const msg = msgs[index];
handleMainMessage(msg);
}
}
: handleMainMessage
);
}
function _initFromWorker(id, meta) {
const [
wrappers,
CC_WORKER_FS_SYNC,
CC_WORKER_ASSET_PIPELINE,
] = meta;
for (const wrapper of wrappers) {
const { name, key, cmd } = wrapper;
if (!main[name]) {
main[name] = {};
}
main[name][key] = (...args) => {
callToMain(cmd, args);
};
}
globalThis.CC_WORKER_FS_SYNC = CC_WORKER_FS_SYNC;
globalThis.CC_WORKER_ASSET_PIPELINE = CC_WORKER_ASSET_PIPELINE;
_inited = true;
if (_initCallback) _initCallback();
const _handlers = [];
for (const cmd in handlers) {
const { name, key } = handlers[cmd];
_handlers.push({ name, cmd: Number(cmd), key });
}
callbackToMain(id, 0, _handlers);
}
function handleMainMessage(msg) {
// 格式:[id, cmd, [args]]
// 如果 cmd 是正数,则是主线程的调用
// 反之,是返回到 Worker 的 Callback
// [0, 0, meta] 为初始化调用
const id = msg[0];
const cmd = msg[1];
const args = msg[2];
if (cmd > 0) {
const handler = handlers[cmd];
if (handler) {
const { func, name, key } = handler;
if (CC_WORKER_DEBUG) {
console.log(`worker recv call (${name}.${key}):`, msg);
}
func(id, cmd, args);
} else {
console.error("worker recv unknown call:", msg);
}
} else if (cmd < 0) {
if (CC_WORKER_DEBUG) {
console.log("worker recv callback:", msg);
}
if (callbacks[id]) {
callbacks[id](msg.slice(2));
delete callbacks[id];
}
} else {
if (CC_WORKER_DEBUG) {
console.log("worker recv init:", msg);
}
_initFromWorker(id, args);
}
}
const sendScheduler = {
queue: [],
init() {
setInterval(() => {
if (this.queue.length > 0) {
worker.postMessage(this.queue);
this.queue = [];
}
}, 0);
},
send(msg) {
this.queue.push(msg);
},
};
const main = {};
module.exports = { init, registerHandler, main };

View File

@@ -0,0 +1,13 @@
// 是否启用 Worker 调度模式,这会减少通信次数(必须一致)
globalThis.CC_WORKER_SCHEDULER = true;
// 是否启用 Worker 调试模式
globalThis.CC_WORKER_DEBUG = true;
// --- 以下从主线程同步值 ---
// 是否启用 Worker 使用同步版本的文件系统 API
globalThis.CC_WORKER_FS_SYNC = null;
// 是否启用 Worker 驱动资源管线(下载、缓存)
globalThis.CC_WORKER_ASSET_PIPELINE = null;

View File

@@ -0,0 +1,156 @@
var EXTNAME_RE = /(\.[^\.\/\?\\]*)(\?.*)?$/;
var DIRNAME_RE = /((.*)(\/|\\|\\\\))?(.*?\..*$)?/;
var NORMALIZE_RE = /[^\.\/]+\/\.\.\//;
/**
* !#en The module provides utilities for working with file and directory paths
* !#zh 用于处理文件与目录的路径的模块
* @class path
* @static
*/
var path = /** @lends cc.path# */{
/**
* !#en Join strings to be a path.
* !#zh 拼接字符串为 Path
* @method join
* @example {@link cocos2d/core/utils/CCPath/join.js}
* @returns {String}
*/
join: function () {
var l = arguments.length;
var result = "";
for (var i = 0; i < l; i++) {
result = (result + (result === "" ? "" : "/") + arguments[i]).replace(/(\/|\\\\)$/, "");
}
return result;
},
/**
* !#en Get the ext name of a path including '.', like '.png'.
* !#zh 返回 Path 的扩展名,包括 '.',例如 '.png'。
* @method extname
* @example {@link cocos2d/core/utils/CCPath/extname.js}
* @param {String} pathStr
* @returns {*}
*/
extname: function (pathStr) {
var temp = EXTNAME_RE.exec(pathStr);
return temp ? temp[1] : '';
},
/**
* !#en Get the main name of a file name
* !#zh 获取文件名的主名称
* @method mainFileName
* @param {String} fileName
* @returns {String}
* @deprecated
*/
mainFileName: function (fileName) {
if (fileName) {
var idx = fileName.lastIndexOf(".");
if (idx !== -1)
return fileName.substring(0, idx);
}
return fileName;
},
/**
* !#en Get the file name of a file path.
* !#zh 获取文件路径的文件名。
* @method basename
* @example {@link cocos2d/core/utils/CCPath/basename.js}
* @param {String} pathStr
* @param {String} [extname]
* @returns {*}
*/
basename: function (pathStr, extname) {
var index = pathStr.indexOf("?");
if (index > 0) pathStr = pathStr.substring(0, index);
var reg = /(\/|\\)([^\/\\]+)$/g;
var result = reg.exec(pathStr.replace(/(\/|\\)$/, ""));
if (!result) return pathStr;
var baseName = result[2];
if (extname && pathStr.substring(pathStr.length - extname.length).toLowerCase() === extname.toLowerCase())
return baseName.substring(0, baseName.length - extname.length);
return baseName;
},
/**
* !#en Get dirname of a file path.
* !#zh 获取文件路径的目录名。
* @method dirname
* @example {@link cocos2d/core/utils/CCPath/dirname.js}
* @param {String} pathStr
* @returns {*}
*/
dirname: function (pathStr) {
var temp = DIRNAME_RE.exec(pathStr);
return temp ? temp[2] : '';
},
/**
* !#en Change extname of a file path.
* !#zh 更改文件路径的扩展名。
* @method changeExtname
* @example {@link cocos2d/core/utils/CCPath/changeExtname.js}
* @param {String} pathStr
* @param {String} [extname]
* @returns {String}
*/
changeExtname: function (pathStr, extname) {
extname = extname || "";
var index = pathStr.indexOf("?");
var tempStr = "";
if (index > 0) {
tempStr = pathStr.substring(index);
pathStr = pathStr.substring(0, index);
}
index = pathStr.lastIndexOf(".");
if (index < 0) return pathStr + extname + tempStr;
return pathStr.substring(0, index) + extname + tempStr;
},
/**
* !#en Change file name of a file path.
* !#zh 更改文件路径的文件名。
* @example {@link cocos2d/core/utils/CCPath/changeBasename.js}
* @param {String} pathStr
* @param {String} basename
* @param {Boolean} [isSameExt]
* @returns {String}
*/
changeBasename: function (pathStr, basename, isSameExt) {
if (basename.indexOf(".") === 0) return this.changeExtname(pathStr, basename);
var index = pathStr.indexOf("?");
var tempStr = "";
var ext = isSameExt ? this.extname(pathStr) : "";
if (index > 0) {
tempStr = pathStr.substring(index);
pathStr = pathStr.substring(0, index);
}
index = pathStr.lastIndexOf("/");
index = index <= 0 ? 0 : index + 1;
return pathStr.substring(0, index) + basename + ext + tempStr;
},
//todo make public after verification
_normalize: function (url) {
var oldUrl = url = String(url);
//removing all ../
do {
oldUrl = url;
url = url.replace(NORMALIZE_RE, "");
} while (oldUrl.length !== url.length);
return url;
},
// The platform-specific file separator. '\\' or '/'.
sep: '/',
// @param {string} path
stripSep(path) {
return path.replace(/[\/\\]$/, '');
}
};
module.exports = path;