156 lines
5.2 KiB
JavaScript
156 lines
5.2 KiB
JavaScript
async function loadWorkspace({
|
|
document,
|
|
window = null,
|
|
createLoader = createFeatureLoader,
|
|
schedule = callback => setTimeout(callback, 750),
|
|
}) {
|
|
let cameOnline = false;
|
|
let replayed = false;
|
|
|
|
const status = document.querySelector('#my-work-action-status');
|
|
const retryButton = document.querySelector('#retry-workspace');
|
|
const names = ['work-core', 'today-timer', 'planning'];
|
|
const urls = Object.fromEntries(names.map(name => [name,
|
|
document.querySelector(`meta[name="stackchain-feature-${name}"]`)?.content || ''
|
|
]));
|
|
const originalUrls = {...urls};
|
|
const loader = createLoader({document, urls});
|
|
const attempts = Object.fromEntries(names.map(name => [name, 0]));
|
|
const failed = new Set();
|
|
const recoveries = new Map();
|
|
let retryInFlight = null;
|
|
let resolveWorkspaceReady;
|
|
const workspaceReady = new Promise(resolve => { resolveWorkspaceReady = resolve; });
|
|
let workspaceMarkedReady = false;
|
|
const markWorkspaceReady = () => {
|
|
if (workspaceMarkedReady) return false;
|
|
workspaceMarkedReady = true;
|
|
resolveWorkspaceReady(true);
|
|
return true;
|
|
};
|
|
|
|
const loadFeature = name => {
|
|
attempts[name] += 1;
|
|
urls[name] = originalUrls[name] + (attempts[name] > 1 ? '?retry=' + attempts[name] : '');
|
|
return loader.load(name);
|
|
};
|
|
const retryOnce = async name => {
|
|
try { return await loadFeature(name); }
|
|
catch (_error) {
|
|
await new Promise(resolve => schedule(resolve));
|
|
return loadFeature(name);
|
|
}
|
|
};
|
|
const showRecovery = () => {
|
|
if (status) status.textContent = failed.has('work-core') ?
|
|
'Workspace unavailable. Reconnect or retry.' :
|
|
'Today or planning tools unavailable. My Work is ready; reconnect or retry.';
|
|
if (retryButton) retryButton.hidden = retryButton.disabled = false;
|
|
};
|
|
const hideRecovery = () => {
|
|
if (failed.size) return;
|
|
if (retryButton) retryButton.hidden = true;
|
|
if (status) status.textContent = '';
|
|
};
|
|
const retryFailed = () => {
|
|
if (retryInFlight) return retryInFlight;
|
|
if (retryButton) retryButton.disabled = true;
|
|
const pending = Array.from(failed);
|
|
retryInFlight = Promise.all(pending.map(async name => {
|
|
try {
|
|
await loadFeature(name);
|
|
failed.delete(name);
|
|
recoveries.get(name)?.resolve(true);
|
|
recoveries.delete(name);
|
|
} catch (_error) {}
|
|
})).then(() => {
|
|
if (failed.size) showRecovery();
|
|
else hideRecovery();
|
|
}).finally(() => { retryInFlight = null; });
|
|
return retryInFlight;
|
|
};
|
|
retryButton?.addEventListener?.('click', retryFailed);
|
|
const handleOnline = () => { cameOnline = true; void retryFailed(); };
|
|
window?.addEventListener('online', handleOnline);
|
|
const serviceWorkerReady = window?.navigator?.serviceWorker?.register ?
|
|
window.navigator.serviceWorker.register('service-worker.js') : Promise.resolve(null);
|
|
|
|
try {
|
|
await retryOnce('work-core');
|
|
} catch (_error) {
|
|
failed.add('work-core');
|
|
showRecovery();
|
|
await new Promise(resolve => recoveries.set('work-core', {resolve}));
|
|
}
|
|
hideRecovery();
|
|
|
|
let optionalReady = null;
|
|
const hydrateWorkspace = () => {
|
|
if (optionalReady) return optionalReady;
|
|
let resolveOptional;
|
|
optionalReady = new Promise(resolve => { resolveOptional = resolve; });
|
|
const optional = ['today-timer', 'planning'].map(async name => {
|
|
try {
|
|
await retryOnce(name);
|
|
return true;
|
|
} catch (_error) {
|
|
failed.add(name);
|
|
showRecovery();
|
|
return new Promise(resolve => recoveries.set(name, {resolve}));
|
|
}
|
|
});
|
|
Promise.all(optional).then(() => {
|
|
hideRecovery();
|
|
resolveOptional(true);
|
|
});
|
|
return optionalReady;
|
|
};
|
|
|
|
const hydrationSelector = [
|
|
'[data-mobile-task]:not([data-mobile-task="queues"])',
|
|
'[data-progressive-loading="true"]',
|
|
'#app-menu-toggle',
|
|
'#work-settings-toggle',
|
|
].join(',');
|
|
const hydrateForAction = async event => {
|
|
const target = event.target?.closest?.(hydrationSelector);
|
|
if (!target) return;
|
|
event.preventDefault();
|
|
event.stopImmediatePropagation();
|
|
await hydrateWorkspace();
|
|
await workspaceReady;
|
|
document.removeEventListener?.('click', hydrateForAction, true);
|
|
target.click?.();
|
|
};
|
|
document.addEventListener?.('click', hydrateForAction, true);
|
|
const hash = window?.location?.hash || '';
|
|
const deepLinkReady = hash.startsWith('#/') && hash !== '#/my-work' ?
|
|
hydrateWorkspace() : Promise.resolve(false);
|
|
|
|
return {
|
|
hydrateWorkspace,
|
|
deepLinkReady,
|
|
workspaceReady,
|
|
markWorkspaceReady,
|
|
serviceWorkerReady,
|
|
get optionalReady() { return hydrateWorkspace(); },
|
|
retryFeature(name) {
|
|
if (!failed.has(name)) return Promise.resolve(true);
|
|
return retryFailed().then(() => !failed.has(name));
|
|
},
|
|
replayOnline(callback) {
|
|
if (replayed) return;
|
|
replayed = true;
|
|
window?.removeEventListener('online', handleOnline);
|
|
if (cameOnline) callback();
|
|
},
|
|
};
|
|
}
|
|
|
|
if (typeof window !== 'undefined' && typeof document !== 'undefined' &&
|
|
typeof createFeatureLoader === 'function') {
|
|
window.stackchainWorkspaceLifecycle = loadWorkspace({document, window});
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = loadWorkspace;
|