@xuda.io/runtime-bundle
Version:
The Xuda Runtime Bundle refers to a collection of scripts and libraries packaged together to provide the necessary runtime environment for executing plugins or components in the Xuda platform.
1,262 lines (1,127 loc) • 45.7 kB
JavaScript
const _this = {};
const normalize_runtime_flag = function (value) {
return value === true || value === 'true' || value === 1 || value === '1' ? true : undefined;
};
// boaz
export const init_module = (e) => {
_this.func = e.func;
_this.glb = e.glb;
_this.SESSION_OBJ = e.SESSION_OBJ;
_this.APP_OBJ = e.APP_OBJ;
_this.IS_DOCKER = normalize_runtime_flag(e.IS_DOCKER);
_this.IS_API_SERVER = normalize_runtime_flag(e.IS_API_SERVER);
_this.IS_PROCESS_SERVER = normalize_runtime_flag(e.IS_PROCESS_SERVER);
};
const clone_json = function (data) {
if (typeof data === 'undefined') {
return data;
}
return JSON.parse(JSON.stringify(data));
};
const get_doc_ts = function (doc) {
return Math.max(Number(doc?.ts) || 0, Number(doc?.order_ts) || 0, Number(doc?.docDate) || 0, Number(doc?.studio_meta?.created) || 0, Number(doc?.studio_meta?.migratedTs) || 0);
};
const resolve_studio_app_id = function (SESSION_ID, app_id) {
const _session = _this.SESSION_OBJ[SESSION_ID];
const resolved_app_id =
(app_id && app_id !== 'unknown' && app_id) ||
(_session?.app_id && _session.app_id !== 'unknown' && _session.app_id) ||
_session?.url_params?.app_id ||
_session?.url_params?.id ||
_session?.build_info?.src_app_id ||
_session?.build_info?.app_id ||
(typeof XUDA_BUILD_SRC !== 'undefined' && XUDA_BUILD_SRC) ||
(typeof XUDA_BUILD_INFO !== 'undefined' && (XUDA_BUILD_INFO?.src_app_id || XUDA_BUILD_INFO?.app_id)) ||
_this.func?.runtime?.ui?.get_attr?.(_session?.root_element, 'app_id') ||
null;
if (resolved_app_id && resolved_app_id !== 'unknown' && _session && (!_session.app_id || _session.app_id === 'unknown')) {
_session.app_id = resolved_app_id;
}
return resolved_app_id && resolved_app_id !== 'unknown' ? resolved_app_id : null;
};
const resolve_draft_runtime_flag = function (SESSION_ID, app_id, ...candidates) {
const _session = _this.SESSION_OBJ?.[SESSION_ID];
const candidate_app_ids = [
resolve_studio_app_id(SESSION_ID, app_id),
...candidates.map((candidate) => candidate?._id || candidate?.app_id).filter(Boolean),
_session?.app_id,
].filter(Boolean);
for (const candidate_app_id of candidate_app_ids) {
const app_obj = _this.APP_OBJ?.[candidate_app_id];
if (typeof app_obj?.draft === 'boolean') {
return app_obj.draft;
}
if (typeof app_obj?.is_draft_runtime === 'boolean') {
return app_obj.is_draft_runtime;
}
}
for (const candidate of candidates) {
if (typeof candidate?.draft === 'boolean') {
return candidate.draft;
}
if (typeof candidate?.is_draft_runtime === 'boolean') {
return candidate.is_draft_runtime;
}
}
if (typeof _session?.build_info?.draft === 'boolean') {
return _session.build_info.draft;
}
return false;
};
const should_try_local_studio_snapshot = function (SESSION_ID, app_id) {
const _session = _this.SESSION_OBJ[SESSION_ID];
const is_server_runtime = !!(_this.IS_DOCKER || _this.IS_PROCESS_SERVER || _this.IS_API_SERVER);
const resolved_app_id = resolve_studio_app_id(SESSION_ID, app_id);
const is_supported_mode = !_session?.engine_mode || ['miniapp', 'live_preview'].includes(_session.engine_mode);
return !is_server_runtime && is_supported_mode && !!resolved_app_id;
};
const connect_studio_pouchdb = function (app_id, rt = false, custom) {
if (_this.func?.utils?.connect_studio_pouchdb) {
return _this.func.utils.connect_studio_pouchdb(app_id, rt, custom);
}
if (custom) {
return new PouchDB(custom, { auto_compaction: true });
}
var db_name = 'xuda_studio_db';
if (app_id) {
db_name += '_' + app_id;
}
if (rt) {
db_name = `xuda_rt_${app_id}`;
}
return new PouchDB(db_name, { auto_compaction: true });
};
const connect_studio_resources_pouchdb = function () {
return connect_studio_pouchdb(null, false, 'xuda_studio_resources');
};
const upsert_cache_doc = async function (db, doc) {
try {
const existing_doc = await db.get(doc._id);
doc._rev = existing_doc._rev;
} catch (error) {}
return await db.put(doc);
};
const write_cached_build_info = async function (SESSION_ID, build_info_ret) {
const db = await func.utils.connect_pouchdb(SESSION_ID);
await upsert_cache_doc(db, {
_id: `cache_build_info`,
build_info: build_info_ret,
docType: 'cache_build_info',
});
};
const write_cached_rt_info = async function (SESSION_ID, rt_info_ret) {
const db = await func.utils.connect_pouchdb(SESSION_ID);
await upsert_cache_doc(db, {
_id: `cache_rt_info`,
data: rt_info_ret,
docType: 'cache_app',
});
};
const build_draft_error_message = function (app_id, remote_error) {
let message = `Unable to load remote app ${app_id}. No local Studio draft was found in browser storage.`;
if (remote_error) {
message += ` Remote error: ${remote_error}`;
}
return message;
};
const load_active_studio_docs = async function (studio_db) {
try {
const ret = await studio_db.find({
selector: {
docType: 'studio',
stat: 3,
},
limit: 999999999,
});
return ret.docs || [];
} catch (error) {
const ret = await studio_db.allDocs({
include_docs: true,
});
return (ret.rows || []).map((row) => row.doc).filter((doc) => doc?.docType === 'studio' && doc?.stat === 3);
}
};
const infer_draft_plugin_manifest_entry = function (plugin_name, plugin_doc, dist_path, source_path, extra = {}) {
const has_file = function (file_path) {
return !!plugin_doc?.files?.[`${plugin_name}/${file_path}`];
};
if (has_file(dist_path)) {
return {
exist: true,
dist: true,
is_empty: false,
...extra,
};
}
if (has_file(source_path)) {
return {
exist: true,
dist: false,
is_empty: false,
...extra,
};
}
return null;
};
const normalize_draft_plugins = async function (app_plugins_purchased = {}) {
const ret = {};
let resources_db = null;
try {
resources_db = connect_studio_resources_pouchdb();
} catch (error) {}
for (const [plugin_name, plugin_meta] of Object.entries(app_plugins_purchased || {})) {
const plugin = clone_json(plugin_meta || {});
if (!plugin.installed) {
ret[plugin_name] = plugin;
continue;
}
if (!plugin.manifest) {
plugin.manifest = {};
}
let plugin_doc = null;
if (resources_db) {
try {
plugin_doc = await resources_db.get(plugin_name);
} catch (error) {}
}
if (!plugin.manifest['runtime.mjs']) {
plugin.manifest['runtime.mjs'] =
infer_draft_plugin_manifest_entry(plugin_name, plugin_doc, 'dist/runtime.mjs', 'runtime.mjs', {
css: !!plugin_doc?.files?.[`${plugin_name}/dist/runtime.css`],
}) || {
exist: true,
dist: true,
is_empty: false,
css: false,
};
}
if (!plugin.manifest['index.mjs']) {
plugin.manifest['index.mjs'] =
infer_draft_plugin_manifest_entry(plugin_name, plugin_doc, 'dist/index.mjs', 'index.mjs') || {
exist: true,
dist: true,
is_empty: false,
};
}
ret[plugin_name] = plugin;
}
return ret;
};
const build_draft_docs_map = function (app_id, studio_docs = []) {
const docs_obj = {};
let globals_doc = null;
for (const doc of studio_docs) {
const cloned_doc = clone_json(doc);
docs_obj[cloned_doc._id] = cloned_doc;
if (cloned_doc?.properties?.menuType === 'globals') {
globals_doc = cloned_doc;
}
}
if (globals_doc && !docs_obj[`global_${app_id}`]) {
docs_obj[`global_${app_id}`] = {
...clone_json(globals_doc),
_id: `global_${app_id}`,
};
}
return docs_obj;
};
const get_local_draft_snapshot = async function (SESSION_ID, app_id) {
app_id = resolve_studio_app_id(SESSION_ID, app_id);
if (!should_try_local_studio_snapshot(SESSION_ID, app_id)) {
return null;
}
const studio_db = connect_studio_pouchdb(app_id);
let app_doc;
try {
app_doc = await studio_db.get(app_id);
} catch (error) {
return null;
}
const studio_docs = await load_active_studio_docs(studio_db);
const docs_obj = build_draft_docs_map(app_id, studio_docs);
const draft_docs = Object.values(docs_obj);
const tables = [];
const programs = [];
for (const doc of draft_docs) {
const menu_type = doc?.properties?.menuType;
if (menu_type === 'table') {
tables.push(doc._id);
continue;
}
if (menu_type === 'globals') {
continue;
}
programs.push(doc._id);
}
let last_changed_ts = get_doc_ts(app_doc);
for (const doc of draft_docs) {
last_changed_ts = Math.max(last_changed_ts, get_doc_ts(doc));
}
const build = last_changed_ts || 0;
const app_doc_data = clone_json(app_doc) || {};
const is_draft_runtime = _this.SESSION_OBJ?.[SESSION_ID]?.engine_mode === 'live_preview' ? !!app_doc_data.draft : true;
delete app_doc_data._rev;
const rt_info_obj = {
...app_doc_data,
_id: app_id,
app_id,
draft: is_draft_runtime,
is_deployment: false,
is_draft_runtime,
app_type: app_doc_data.app_type || 'master',
app_build_id: build.toString(),
app_id_reference: app_doc_data.app_id_reference || app_id,
app_replicate: app_doc_data.app_replicate || app_id,
app_db_name: app_doc_data.app_db_name || app_id,
app_plugins_purchased: await normalize_draft_plugins(app_doc_data.app_plugins_purchased || {}),
account_info: app_doc_data.account_info || {},
login_info: app_doc_data.login_info || {},
deploy_data: clone_json(app_doc_data.deploy_data || {}),
client_ip: '',
rpi_http_methods: ['get_doc_obj_from_build', 'dbs_read', 'dbs_create', 'dbs_update', 'dbs_delete'],
accessible_deployed_progs_arr: programs,
accessible_deployed_tables_arr: tables,
};
const build_info = {
build,
src_app_id: app_id,
master_build: build,
server_ts: last_changed_ts,
last_changed_ts,
progs_changed: programs,
tables_changed: tables,
draft: is_draft_runtime,
};
return {
build_info,
build_info_ret: {
code: 1,
data: build_info,
},
rt_info: rt_info_obj,
rt_info_ret: {
code: 1,
data: rt_info_obj,
},
docs_obj,
};
};
const persist_draft_snapshot = async function (SESSION_ID, draft_snapshot) {
await write_cached_build_info(SESSION_ID, draft_snapshot.build_info_ret);
await write_cached_rt_info(SESSION_ID, draft_snapshot.rt_info_ret);
};
export const project_loader = async function (SESSION_ID, app_id, prog_id) {
try {
var _session = _this.SESSION_OBJ[SESSION_ID];
app_id = resolve_studio_app_id(SESSION_ID, app_id) || app_id;
if (app_id && app_id !== 'unknown') {
_session.app_id = app_id;
}
if (_this.func.UI.utils.get_url_attribute(SESSION_ID, 'clear_cache')) {
await func.index.delete_pouch(SESSION_ID);
}
let last_changed_ts = 0;
var ret_build_info = {};
let draft_snapshot = null;
if (_session.engine_mode === 'live_preview') {
draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (draft_snapshot) {
ret_build_info = draft_snapshot.build_info_ret;
_session.build_info = draft_snapshot.build_info;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info);
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
last_changed_ts = draft_snapshot.build_info.last_changed_ts || 0;
} else {
if (!IS_ONLINE) {
return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id), null, true);
}
ret_build_info = await get_app_build_info(SESSION_ID, app_id);
if (ret_build_info.code < 0) {
return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id, ret_build_info.data), null, true);
}
_session.build_info = ret_build_info.data;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, ret_build_info?.data);
await write_cached_build_info(SESSION_ID, ret_build_info);
last_changed_ts = ret_build_info.data?.last_changed_ts || ret_build_info.data?.server_ts || 0;
}
} else if (typeof XUDA_BUILD_INFO !== 'undefined') {
// call from xuda real-preview app
_session.build_info = XUDA_BUILD_INFO;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, _session.build_info);
if (_session.is_draft_runtime) {
draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (!draft_snapshot) {
return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id), null, true);
}
ret_build_info = draft_snapshot.build_info_ret;
_session.build_info = draft_snapshot.build_info;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info);
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
}
last_changed_ts = _session.build_info?.last_changed_ts || 0;
} else {
// test
const db = await func.utils.connect_pouchdb(SESSION_ID);
if (['live_preview', 'miniapp'].includes(_session.engine_mode)) {
// if on-line
if (IS_ONLINE) {
ret_build_info = await get_app_build_info(SESSION_ID, app_id);
if (ret_build_info.code < 0) {
draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (!draft_snapshot) {
return _this.func.UI.utils.progressScreen.show(SESSION_ID, build_draft_error_message(app_id, ret_build_info.data), null, true);
}
ret_build_info = draft_snapshot.build_info_ret;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info);
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
} else {
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, ret_build_info?.data);
}
_session.build_info = ret_build_info.data;
await write_cached_build_info(SESSION_ID, ret_build_info);
} else {
// off-line get from pouch
try {
ret_build_info = (await db.get(`cache_build_info`)).build_info;
_session.build_info = ret_build_info.data;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, ret_build_info?.data);
} catch (error) {
draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (!draft_snapshot) {
return console.error('cache_build_info error');
}
ret_build_info = draft_snapshot.build_info_ret;
_session.build_info = draft_snapshot.build_info;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, draft_snapshot.rt_info, draft_snapshot.build_info);
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
}
}
last_changed_ts = ret_build_info.data.last_changed_ts;
} else {
// deployments
try {
// const db = await func.utils.connect_pouchdb(SESSION_ID);
// only get indication for fresh installation
await db.get(`cache_rt_info`);
} catch (err) {
// fresh load
const startup_module = await func.common.get_module(SESSION_ID, 'xuda-deploy-startup-loader.mjs');
await startup_module.loader(SESSION_ID);
}
last_changed_ts = _session.opt.last_changed_ts;
}
}
// if (_session.engine_mode !== 'live_preview') {
// try {
// const db = await func.utils.connect_pouchdb(SESSION_ID);
// // only get indication for fresh installation
// await db.get(`cache_rt_info`);
// } catch (err) {
// const startup_module = await func.common.get_module(SESSION_ID, 'xuda-deploy-startup-loader.mjs');
// await startup_module.loader(SESSION_ID);
// }
// last_changed_ts = _session.opt.last_changed_ts;
// }
const rt_info_ret = await get_rt_info(SESSION_ID, app_id, last_changed_ts);
const active_draft_snapshot = rt_info_ret?.draft_snapshot || draft_snapshot;
await insert_custom_prop(SESSION_ID);
if (_session?.app_admin_prop?.app_admin_direction) {
_session.root_element.setAttribute('dir', _session.app_admin_prop.app_admin_direction);
}
// load live preview module
const progs_loader_module = await _this.func.common.get_module(SESSION_ID, `xuda-progs-loader-module.mjs`);
if (_session.is_draft_runtime && active_draft_snapshot?.docs_obj) {
await progs_loader_module.prime_objects_cache(SESSION_ID, 'DOCS_OBJ', active_draft_snapshot.docs_obj);
} else if (_session.app_id !== 'unknown' && _session.engine_mode !== 'live_preview') {
await progs_loader_module.load_objects_cache(SESSION_ID);
}
if (_session.engine_mode === 'live_preview') {
const live_preview_module = await _this.func.common.get_module(SESSION_ID, 'xuda-live-preview-module.esm.js');
live_preview_module.live_preview_loader(SESSION_ID);
return;
}
if (_this.APP_OBJ[_session.app_id]?.is_deployment) {
try {
await init_runtime_websocket(SESSION_ID, _session.app_id);
} catch (error) {
throw error;
}
}
await _this.func.UI.main.embed_loader(SESSION_ID);
} catch (error) {
throw error;
}
};
const get_app_build_info = async function (SESSION_ID, app_id) {
return new Promise(function (resolve, reject) {
var _session = _this.SESSION_OBJ[SESSION_ID];
let app_id_reference = _this.APP_OBJ[app_id].app_id_reference;
fetch(_this.func.common.get_url(SESSION_ID, 'rpi', 'get_app_build'), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'xu-gtp-token': _session.gtp_token,
'xu-app-token': _session.app_token,
},
body: JSON.stringify({
app_id: app_id,
app_id_reference,
engine_mode: _session.engine_mode,
}),
})
.then((response) => {
if (!response.ok) {
return response.text().then((text) => {
throw new Error(text);
});
}
return response.json();
})
.then(async (json) => {
_this.SESSION_OBJ[SESSION_ID].build_info = json.data;
resolve(json);
})
.catch((err) => {
try {
resolve(JSON.parse(err.message));
} catch (error) {
resolve({ code: -1, data: err.message });
}
});
});
};
const get_user_group_account_info = async function (SESSION_ID, uid) {
var _session = SESSION_OBJ[SESSION_ID];
const response = await fetch(`https://${_session.domain}/cpi/get_account_info`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
uid,
uid_query:
document.cookie
.split('; ')
.find((c) => c.startsWith('uid='))
?.split('=')[1] || '',
}),
});
const json = await response.json();
return json.data;
};
const get_rt_info = async function (SESSION_ID, app_id, last_changed_ts) {
var _session = _this.SESSION_OBJ[SESSION_ID];
app_id = resolve_studio_app_id(SESSION_ID, app_id) || app_id;
return new Promise(async function (resolve, reject) {
const response = {
success: async function (ret, ajaxP) {
if (ret.code < 0) {
return response.error(ret.data);
}
const rt_info_obj = ret.data;
var app_id = rt_info_obj._id;
_this.APP_OBJ[app_id] = rt_info_obj;
_session.app_id = app_id;
if (rt_info_obj?.deploy_data?.global_variables) {
_session.url_params = {
..._session.url_params,
...rt_info_obj.deploy_data.global_variables,
};
}
let account_info = { ...(rt_info_obj?.account_info || {}) };
if (_session.engine_mode === 'user_group') {
const user_group_data = await get_user_group_account_info(SESSION_ID, rt_info_obj?.account_info?.uid);
account_info = user_group_data.account_info;
account_info.uid = user_group_data._id;
}
_session.USR_OBJ = {
_id: account_info?.uid,
usr_name: account_info?.username,
usr_first_name: account_info?.first_name || account_info?.email,
usr_last_name: account_info?.last_name || '',
usr_email: account_info?.email,
usr_profile_picture: account_info?.profile_picture,
};
_session.login_info = rt_info_obj?.login_info;
_session.client_ip = rt_info_obj.client_ip;
_session.rpi_http_methods = rt_info_obj.rpi_http_methods;
_session.app_admin_prop = rt_info_obj.app_admin_prop;
_session.is_deployment = rt_info_obj.is_deployment;
_session.is_draft_runtime = resolve_draft_runtime_flag(SESSION_ID, app_id, rt_info_obj);
// const set_prog_cache = async function () {
// if (_session.prog_id) {
// if (!rt_info_obj.prog_docs[_session.prog_id]) {
// return console.error(
// `error - program ${_session.prog_id} not found.`
// );
// }
// DOCS_OBJ[app_id][_session.prog_id] = rt_info_obj.prog_docs[
// _session.prog_id
// ] || {
// _id: _session.prog_id,
// };
// const module = await _this.func.common.get_module(
// SESSION_ID,
// `xuda-progs-loader-module.mjs`
// );
// module.save_objects_cache(
// SESSION_ID,
// _session.prog_id,
// "DOCS_OBJ",
// rt_info_obj.prog_docs[_session.prog_id]
// );
// }
// // for (let prog_id of [
// // ...rt_info_obj?.accessible_deployed_progs_arr,
// // ...rt_info_obj?.accessible_deployed_tables_arr,
// // ] || []) {
// // module.DOCS_OBJ_get(SESSION_ID, prog_id);
// // }
// };
// if (_session.engine_mode !== "live_preview") {
// set_prog_cache();
// }
resolve({
rt_info_obj,
draft_snapshot: ret?.draft_snapshot || null,
});
const loaderLogo = document.querySelector('.loader_logo');
if (loaderLogo) {
const app_pic = typeof rt_info_obj.app_pic === 'string' ? rt_info_obj.app_pic.trim() : '';
if (app_pic && app_pic !== 'undefined' && app_pic !== 'null') {
loaderLogo.style.backgroundImage = `url( ${app_pic})`;
} else {
loaderLogo.style.removeProperty('background-image');
}
loaderLogo.style.display = 'none';
loaderLogo.style.display = '';
}
},
error: async function (err) {
if (err) {
return _this.func.UI.utils.progressScreen.show(SESSION_ID, err, null, true);
}
// location.reload();
console.warn('** reload request');
},
};
const db = await func.utils.connect_pouchdb(SESSION_ID);
if (_session.engine_mode === 'live_preview') {
const draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (draft_snapshot) {
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
response.success({
...draft_snapshot.rt_info_ret,
draft_snapshot,
});
return;
}
if (resolve_draft_runtime_flag(SESSION_ID, app_id, _session?.build_info)) {
return response.error(build_draft_error_message(app_id));
}
}
if (resolve_draft_runtime_flag(SESSION_ID, app_id, _session?.build_info)) {
const draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (!draft_snapshot) {
return response.error(build_draft_error_message(app_id));
}
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
response.success({
...draft_snapshot.rt_info_ret,
draft_snapshot,
});
return;
}
try {
let ret = await db.get(`cache_rt_info`);
const rt_info_obj = ret.data;
if (rt_info_obj.data.last_changed_ts !== last_changed_ts || (!['live_preview', 'miniapp'].includes(_session.engine_mode) && rt_info_obj.data.app_build_id !== _session.opt.app_build_id)) {
_this.func.UI.utils.progressScreen.show(SESSION_ID, 'New application setup detected, refreshing data and reloading in 5 sec');
setTimeout(async () => {
await func.index.delete_pouch(SESSION_ID);
location.reload();
}, 5000);
return;
}
response.success(rt_info_obj);
} catch (err) {
fetch(_this.func.common.get_url(SESSION_ID, 'rpi', 'get_rt_info'), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'xu-gtp-token': _session.gtp_token,
'xu-app-token': _session.app_token,
},
body: JSON.stringify({
app_id,
prog_id: _session.engine_mode === 'live_preview' ? '' : _session.prog_id,
engine_mode: _session.engine_mode,
build: _session.build_info?.build,
session_id: SESSION_ID,
is_cordova: _this.glb.is_cordova,
client_info: _session.SYS_GLOBAL_OBJ_CLIENT_INFO,
client_id: _session.SYS_GLOBAL_OBJ_CLIENT_INFO.fingerprint,
}),
})
.then((response) => {
if (!response.ok) {
return response.text().then((text) => {
throw new Error(text);
});
}
return response.json();
})
.then((json) => {
if (json.code < 0) {
throw new Error(json.data || 'unknown error');
}
response.success(json);
})
.then(async (json) => {
if (json?.code > 0) {
await write_cached_rt_info(SESSION_ID, json);
}
})
.catch(async (err) => {
if (should_try_local_studio_snapshot(SESSION_ID, app_id)) {
const draft_snapshot = await get_local_draft_snapshot(SESSION_ID, app_id);
if (draft_snapshot) {
await persist_draft_snapshot(SESSION_ID, draft_snapshot);
response.success({
...draft_snapshot.rt_info_ret,
draft_snapshot,
});
return;
}
response.error(build_draft_error_message(app_id, err.message));
return;
}
response.error(err.message);
});
}
});
};
// Parse an HTML string and return a fragment whose <script> tags are RECREATED as fresh
// elements so the browser EXECUTES them (innerHTML/insertAdjacentHTML-parsed scripts are
// flagged non-executable forever, per the HTML spec). src scripts get async=false to
// preserve document order (vue.global.js must run before the inline modules that use it,
// the tailwind bundle before its inline config, etc.). Returns { frag, loads } where
// loads are the src-script load promises (each already error-tolerant) so the caller can
// wait for the libs before depending on their globals.
const build_executable_fragment = function (html) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
const loads = [];
const recreate_script = function (node) {
const s = document.createElement('script');
for (const a of Array.from(node.attributes)) s.setAttribute(a.name, a.value);
s.setAttribute('data-xuda-executable-fragment', 'true');
const script_type = `${s.getAttribute('type') || ''}`.trim().toLowerCase();
const transform_inline_module = function (text) {
const imports = [];
let body = `${text || ''}`;
body = body.replace(/^\s*import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["'];?\s*/gm, function (_match, local_name, url) {
imports.push(`try { ${local_name} = (await import(${JSON.stringify(url)})).default; } catch (error) { console.error(error); }`);
return '';
});
body = body.replace(/^\s*import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']([^"']+)["'];?\s*/gm, function (_match, local_names, url) {
imports.push(`try { ({ ${local_names.trim()} } = await import(${JSON.stringify(url)})); } catch (error) { console.error(error); }`);
return '';
});
if (!imports.length || /\bexport\s+/.test(body)) {
return '';
}
const declarations = imports
.map((line) => line.match(/^\s*try\s+\{\s*(?:\(\{\s*)?([A-Za-z_$][\w$]*)/)?.[1])
.filter(Boolean)
.map((name) => `let ${name};`)
.join('\n');
return `;(async function () {\n${declarations}\n${imports.join('\n')}\n${body}\nconst __xuda_schedule_tailwind_refresh = function (reason, delay) {
if (!window.__xudaScheduleTailwindRefresh) {
const state = window.__xudaTailwindRefreshState ||= { timer: 0, running: false, pending: false };
window.__xudaScheduleTailwindRefresh = function (_reason, requestedDelay) {
if (state.running) {
state.pending = true;
return;
}
if (state.timer) {
state.pending = true;
return;
}
state.timer = setTimeout(async function () {
state.timer = 0;
state.running = true;
state.pending = false;
try {
window.__xudaTailwindNormalizeTheme?.();
window.__xudaTailwindNormalizePlugins?.();
await window.tailwind?.refresh?.();
} catch (error) {
console.error(error);
} finally {
state.running = false;
if (state.pending) {
state.pending = false;
window.__xudaScheduleTailwindRefresh('pending', 180);
}
}
}, requestedDelay || 80);
};
}
window.__xudaScheduleTailwindRefresh(reason, delay || 80);
};
__xuda_schedule_tailwind_refresh('inline-module', 80);
setTimeout(() => __xuda_schedule_tailwind_refresh('inline-module-settle', 180), 600);\n})();`;
};
const inline_module_text = !node.src && script_type === 'module' ? transform_inline_module(node.textContent || '') : '';
if (inline_module_text) {
s.removeAttribute('type');
s.setAttribute('data-xuda-inline-module', 'classic-dynamic-import');
s.text = inline_module_text;
}
const should_wait_for_script = !!node.src || (script_type === 'module' && !inline_module_text);
let inline_module_blob_url = '';
if (
!inline_module_text &&
!node.src &&
script_type === 'module' &&
typeof URL !== 'undefined' &&
typeof URL.createObjectURL === 'function' &&
typeof Blob !== 'undefined'
) {
inline_module_blob_url = URL.createObjectURL(new Blob([node.textContent || ''], { type: 'text/javascript' }));
s.src = inline_module_blob_url;
s.setAttribute('data-xuda-inline-module', 'blob');
}
if (should_wait_for_script) {
loads.push(
new Promise((res) => {
const done = function () {
if (inline_module_blob_url) {
setTimeout(() => URL.revokeObjectURL(inline_module_blob_url), 0);
}
res();
};
s.onload = s.onerror = done;
}),
);
}
if (node.src) {
if (!s.hasAttribute('async')) {
s.async = false;
}
} else if (!inline_module_blob_url && !inline_module_text) {
s.text = node.textContent;
}
return s;
};
const clone_executable_node = function (node) {
if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.COMMENT_NODE) {
return node.cloneNode(true);
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return node.cloneNode(true);
}
if (`${node.tagName || ''}`.toLowerCase() === 'script') {
return recreate_script(node);
}
const clone = node.cloneNode(false);
for (const child_node of Array.from(node.childNodes)) {
clone.appendChild(clone_executable_node(child_node));
}
return clone;
};
const frag = document.createDocumentFragment();
for (const child_node of Array.from(tpl.content.childNodes)) {
frag.appendChild(clone_executable_node(child_node));
}
return { frag, loads };
};
const insert_custom_prop = async function (SESSION_ID) {
try {
var app_id = _this.SESSION_OBJ[SESSION_ID].app_id;
// user_group (published group apps, e.g. drive.modulartalmud.com) was built against the
// OLD runtime, which executed the app_custom_header/body scripts (Vue global, tailwind
// bundle + inline config, uploaded up_*.js, body sortable/draggable). The dead-script
// injection below breaks all of those at once (unstyled tailwind buttons, missing Vue).
// So for user_group we inject through an EXECUTING fragment; every other engine mode
// keeps the legacy inert injection untouched (preview is verified working with it).
const exec_mode = _this.SESSION_OBJ[SESSION_ID]?.engine_mode === 'user_group';
let header_loads = [];
if (_this.APP_OBJ[app_id]?.app_custom_prop?.app_custom_header) {
var head = document.getElementsByTagName('head')[0];
const app_custom_header = _this.APP_OBJ[app_id].app_custom_prop.app_custom_header;
const header_html = func.utils.replace_studio_drive_url(SESSION_ID, app_custom_header);
if (exec_mode) {
const { frag, loads } = build_executable_fragment(header_html);
head.appendChild(frag);
header_loads = loads;
} else {
head.insertAdjacentHTML('beforeend', header_html);
}
}
if (_this.APP_OBJ[app_id]?.app_custom_prop?.app_custom_body) {
const root_el = _this.SESSION_OBJ[SESSION_ID].root_element;
const body_html = _this.APP_OBJ[app_id].app_custom_prop.app_custom_body;
if (exec_mode) {
const { frag, loads } = build_executable_fragment(body_html);
root_el.insertBefore(frag, root_el.firstChild);
header_loads = header_loads.concat(loads);
} else {
root_el.insertAdjacentHTML('afterbegin', body_html);
}
}
// Don't let a stalled CDN block render: cap the wait, same rule as the gallery libs.
if (header_loads.length) {
await Promise.race([Promise.all(header_loads), new Promise((res) => setTimeout(res, 4000))]);
}
// Gallery libs (Swiper + FsLightbox) are declared in app_custom_header, but <script> tags
// inserted via insertAdjacentHTML are fetched-NOT-executed, so they expose no globals.
// Force-load them through the executable loader BEFORE screen_ready (the FsLightbox
// controller reads window.fsLightbox / refreshFsLightbox / fsLightboxInstances as bare
// globals on screen_ready). TWO safety rules learned the hard way:
// 1) append "?xu_lib=1" so the URL does NOT match the dead header <script> — otherwise
// load_script attaches a load-listener to that already-settled, never-executed node
// whose load event already fired, so the callback never runs and the await hangs.
// 2) cap each load with a hard timeout so render is NEVER blocked, even if a lib stalls.
const rep = _this.APP_OBJ[app_id]?.app_replicate;
if (rep && typeof document !== 'undefined') {
const drv = (p) => func.utils.replace_studio_drive_url(SESSION_ID, `https://xuda.ai/studio-drive/${rep}/${p}`) + '?xu_lib=1';
const capped = (pr) => Promise.race([pr, new Promise((res) => setTimeout(res, 4000))]);
await Promise.all([
capped(func.utils.load_js_on_demand(drv('node_modules/swiper/swiper-bundle.min.js'), '')),
capped(func.utils.load_js_on_demand(drv('fslightbox_new.js'), '')),
]);
// Wrap Swiper so every carousel (a) scopes its nav selectors to its OWN container — the app
// reuses the same global '.swiper-button-next/prev' for every carousel, so the bare global
// selector binds them all to the FIRST button in the document and the visible gallery's
// arrows control nothing; and (b) observes DOM changes so slides/nav added after init get
// picked up. Idempotent via __xuWrapped; falls back to the original selector if unscopable.
const Sw = typeof window !== 'undefined' ? window.Swiper : undefined;
if (Sw && !Sw.__xuWrapped) {
const Wrapped = function (el, opts) {
opts = Object.assign({ observer: true, observeParents: true, observeSlideChildren: true }, opts || {});
const root = typeof el === 'string' ? document.querySelector(el) : el;
const nav = opts.navigation;
if (nav && root) {
// The buttons live OUTSIDE the .swiper element (siblings in the gallery panel, not
// children), so a container-only query misses them and falls back to the shared global
// selector → wrong button. Resolve each selector to THIS carousel's button by walking UP
// from the container to the nearest ancestor that contains it.
const scope = (sel) => {
if (typeof sel !== 'string') return sel;
let n = root;
for (let i = 0; i < 5 && n; i++) {
const hit = n.querySelector && n.querySelector(sel);
if (hit) return hit;
n = n.parentElement;
}
return sel;
};
if (nav.nextEl) nav.nextEl = scope(nav.nextEl);
if (nav.prevEl) nav.prevEl = scope(nav.prevEl);
}
return new Sw(el, opts);
};
for (const k in Sw) {
try {
Wrapped[k] = Sw[k];
} catch (e) {}
}
Wrapped.prototype = Sw.prototype;
Wrapped.__xuWrapped = true;
window.Swiper = Wrapped;
if (typeof globalThis !== 'undefined') globalThis.Swiper = Wrapped;
}
}
} catch (err) {
console.error(err);
}
};
const init_runtime_websocket = function (SESSION_ID, app_id) {
return new Promise(function (resolve, reject) {
const set_connected = async function (stat) {
var datasource_changes = {
[0]: {
['data_system']: { SYS_GLOBAL_BOL_CONNECTED: stat },
},
};
await func.datasource.update(SESSION_ID, datasource_changes);
};
//////////////// IO //////////////////
var _session = _this.SESSION_OBJ[SESSION_ID];
var _data_system = _session?.DS_GLB?.[0]?.data_system;
const url = `https://${_this.func.runtime.platform.get_hostname()}`;
var error;
RUNTIME_SERVER_WEBSOCKET = io(url, {
secure: true,
reconnection: true, // _this.glb.debug_js ? false : true,
// reconnection: true,
// reconnectionDelayMax: 10000,
rejectUnauthorized: false,
path: '/ws/socket.io',
// transports: ['websocket']
// query: { session_obj: SESSION_OBJ[SESSION_ID] },
});
RUNTIME_SERVER_WEBSOCKET.on('connect', () => {
console.info('RUNTIME_SERVER_WEBSOCKET connected');
if (_data_system) {
set_connected(1);
}
if (_session.opt.enable_offline) {
if (_session.root_element.classList.contains('runtime_offline')) {
_session.root_element.classList.remove('runtime_offline');
func.utils.alerts.toast(SESSION_ID, 'Switched to on-line mode', 'You are now online. All data stored while you were offline will be synchronized to the server.', 'success');
}
} else {
// back from temporarily disconnected from the server
_this.func.UI.utils.progressScreen.hide(SESSION_ID);
}
if (error) {
if (!RUNTIME_SERVER_WEBSOCKET_CONNECTED) {
// location.reload();
console.warn('** reload request');
}
}
});
RUNTIME_SERVER_WEBSOCKET.on('message', (e) => {
if (_this.APP_OBJ[app_id].is_deployment) {
_this.func.UI.utils.indicator.server.busy();
setTimeout(
function () {
_this.func.UI.utils.indicator.server.normal();
},
e?.data?.length * 100 || 100,
);
}
var data = e.data;
if (e.source === 'http_call') {
if (e.service === 'get_doc_obj_from_build') {
return func.runtime.platform.emit('get_doc_obj_from_build_response_' + data._id, {
data,
});
}
if (e.service === 'heartbeat') {
return func.runtime.platform.emit('heartbeat_response', {
data,
});
}
return func.runtime.platform.emit('get_ws_data_response_' + e.websocket_queue_num, {
data,
e,
});
}
if (e.source === 'deployment_server') {
console.log('document_changed', e);
return func.runtime.ui.refresh_document_changes_for_realtime_update(SESSION_ID, e.data);
}
if (data !== 'connected') return;
RUNTIME_SERVER_WEBSOCKET_CONNECTED = true;
resolve();
});
var callback_done = false;
RUNTIME_SERVER_WEBSOCKET.on('connect_error', (error) => {
if (!callback_done) {
resolve();
callback_done = true;
error = true;
}
});
RUNTIME_SERVER_WEBSOCKET.on('disconnect', async () => {
RUNTIME_SERVER_WEBSOCKET_CONNECTED = false;
if (_data_system) {
set_connected(0);
}
if (_session.opt.enable_offline) {
_session.root_element.classList.add('runtime_offline');
func.utils.alerts.toast(SESSION_ID, 'Switched to off-line mode', 'You have lost connection to the server and are now working offline. Once the connection is restored, all data will be synchronized.', 'warning');
} else {
// await func.index.delete_pouch();
// window.location.href = `https://${_session.domain}/error?error_code=408`;
_this.func.UI.utils.progressScreen.show(SESSION_ID, 'Your browser has temporarily disconnected from the server. Please wait while we attempt to reconnect.');
}
});
_this.func.runtime.platform.add_window_listener('beforeunload', function (event) {
var obj = {
service: 'close_websocket',
data: { session_id: SESSION_ID },
};
RUNTIME_SERVER_WEBSOCKET.emit('message', obj);
});
});
};
export const run_plugins_runtime_init = async function (SESSION_ID, app_id, method) {
var _session = _this.SESSION_OBJ[SESSION_ID];
const get_plugin_method_resource = function (manifest, method) {
const direct = manifest?.[`${method}.mjs`];
if (direct?.exist && !direct.is_empty) {
return `${direct.dist ? 'dist/' : ''}${method}.mjs`;
}
const legacy = manifest?.[method]?.mjs;
if (legacy?.exist && !legacy.is_empty) {
return `${legacy.dist ? 'dist/' : ''}${method}.mjs`;
}
return null;
};
const append_runtime_import_cache_tag = function (url) {
const cache_tag =
(typeof globalThis !== 'undefined' ? globalThis.__XU_RUNTIME_MODULE_CACHE_TAG__ || globalThis.__XU_SERVER_BOOTSTRAP__?.version : '') ||
_session?.build_info?.runtime_ts ||
_session?.build_info?.last_changed_ts ||
_session?.build_info?.server_ts ||
_session?.opt?.app_build_id ||
0;
if (!cache_tag) {
return url;
}
return `${url}${url.includes('?') ? '&' : '?'}xu_runtime_ts=${encodeURIComponent(cache_tag)}`;
};
const get_path = function (plugin_name, resource) {
if (_session.worker_type === 'Dev') {
return append_runtime_import_cache_tag(`../../plugins/${_session.domain}/${plugin_name}/${resource}`);
}
return append_runtime_import_cache_tag(`https://${_session.domain}/plugins/${plugin_name}/runtime/${resource}?gtp_token=${_session.gtp_token}&app_id=${_session.app_id}`);
};
const load_plugin_runtime_module = async function (plugin_name, resource) {
if (typeof _this.func?.utils?.get_plugin_resource === 'function') {
return await _this.func.utils.get_plugin_resource(SESSION_ID, plugin_name, resource);
}
const local_plugin_resource_url = await _this.func?.utils?.get_local_studio_plugin_resource_url?.(SESSION_ID, plugin_name, resource);
if (local_plugin_resource_url) {
return await import(/* @vite-ignore */ local_plugin_resource_url);
}
return await import(get_path(plugin_name, resource));
};
if (!_this.APP_OBJ[app_id].app_plugins_purchased) return;
for await (const [key, val] of Object.entries(_this.APP_OBJ[app_id].app_plugins_purchased)) {
if (!val.installed) continue;
const plugin_resource = get_plugin_method_resource(val.manifest, method);
if (!plugin_resource) continue;
let module;
try {
module = await load_plugin_runtime_module(key, plugin_resource);
} catch (error) {
console.error(error);
await _this.func?.utils?.report_issue?.(SESSION_ID, {
code: 'RUN_MSG_GUI_020',
source: 'run_plugins_runtime_init',
message: `plugin runtime import failed for ${key}`,
err: error,
details: {
plugin_name: key,
plugin_resource,
method,
},
});
continue;
}
try {
await module.default({ SESSION_ID: SESSION_ID });
} catch (error) {
console.error(error);
}
}
};