mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
Add new chart-transform plugin capability (#1000)
* Chart-transform plugin capability * PR comments * Cleanup * Fix markdown * CodeRabbit feedback Signed-off-by: Joe <jphinspace@gmail.com> --------- Signed-off-by: Joe <jphinspace@gmail.com> Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Byron Gamatos
parent
f7942f3689
commit
05be9ebdbe
@@ -0,0 +1,336 @@
|
||||
// Chart-transform provider registration, selection, and diagnostics.
|
||||
// Transformation stays on the synchronous highway data plane and runs after
|
||||
// difficulty filtering; the selected provider is shared by highway instances.
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.feedBack = window.feedBack || {};
|
||||
const capabilities = window.feedBack.capabilities;
|
||||
if (!capabilities || capabilities.version !== 1) return;
|
||||
if (window.feedBack.chartTransformDomain && window.feedBack.chartTransformDomain.version === 1) return;
|
||||
|
||||
const STORAGE_KEY = 'feedBack.chartTransform.selectedProviderId';
|
||||
const PUBLIC_FAILURE_REASON = 'Chart transform provider failed';
|
||||
|
||||
// providerId → { id, label, pluginId, transform }
|
||||
const providers = new Map();
|
||||
let activeProviderId = null;
|
||||
let activeSource = 'startup';
|
||||
let lastFailure = null;
|
||||
// Count of highway instances the active provider is installed on
|
||||
// (the primary window.highway plus any announced via highway:created —
|
||||
// e.g. splitscreen panels). 0 = nothing capable exists yet.
|
||||
let installedCount = 0;
|
||||
// Known highway surfaces beyond window.highway, held weakly so closed
|
||||
// splitscreen panels can be collected. WeakRef is guarded for minimal
|
||||
// test environments; the strong-ref fallback only over-retains there.
|
||||
const _HasWeakRef = typeof WeakRef === 'function';
|
||||
let _surfaces = [];
|
||||
|
||||
function _handled(payload = {}) { return { outcome: 'handled', payload }; }
|
||||
function _degraded(reason, payload = {}) { return { outcome: 'degraded', reason, payload }; }
|
||||
|
||||
function _snapshot(extra = {}) {
|
||||
return {
|
||||
available: true,
|
||||
active: activeProviderId,
|
||||
activeSource,
|
||||
installed: installedCount > 0,
|
||||
surfaces: installedCount,
|
||||
providers: [...providers.values()].map(p => ({
|
||||
id: p.id,
|
||||
label: p.label,
|
||||
pluginId: p.pluginId,
|
||||
})),
|
||||
lastFailure: lastFailure ? { ...lastFailure } : null,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function _emit(name, detail) {
|
||||
try { capabilities.emitEvent('chart-transform', name, detail || {}); }
|
||||
catch (_) { /* eventing must not break rendering */ }
|
||||
}
|
||||
|
||||
function _contributeDiagnostics() {
|
||||
const diagnostics = window.feedBack && window.feedBack.diagnostics;
|
||||
if (diagnostics && typeof diagnostics.contribute === 'function') {
|
||||
try {
|
||||
diagnostics.contribute('chart-transform-capability', {
|
||||
schema: 'feedBack.chart_transform.diagnostics.v1',
|
||||
..._snapshot(),
|
||||
});
|
||||
} catch (_) { /* diagnostics must not break rendering */ }
|
||||
}
|
||||
}
|
||||
|
||||
function _persistSelection(providerId) {
|
||||
try {
|
||||
if (providerId) window.localStorage.setItem(STORAGE_KEY, providerId);
|
||||
else window.localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (_) { /* storage unavailable → in-memory selection only */ }
|
||||
}
|
||||
|
||||
function _persistedSelection() {
|
||||
try { return window.localStorage.getItem(STORAGE_KEY) || null; }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
|
||||
function _capable(hw) {
|
||||
return !!(hw && typeof hw.setChartTransform === 'function');
|
||||
}
|
||||
|
||||
// Every capable highway surface: window.highway plus live announced
|
||||
// instances (splitscreen panels), deduped, dead refs pruned in place.
|
||||
function _eachSurface(fn) {
|
||||
const seen = new Set();
|
||||
const primary = window.highway;
|
||||
if (_capable(primary)) { seen.add(primary); fn(primary); }
|
||||
const live = [];
|
||||
for (const ref of _surfaces) {
|
||||
const hw = _HasWeakRef ? ref.deref() : ref;
|
||||
if (!hw) continue;
|
||||
live.push(ref);
|
||||
if (seen.has(hw) || !_capable(hw)) continue;
|
||||
seen.add(hw);
|
||||
fn(hw);
|
||||
}
|
||||
_surfaces = live;
|
||||
return seen.size;
|
||||
}
|
||||
|
||||
function _rememberSurface(hw) {
|
||||
if (!_capable(hw) || hw === window.highway) return;
|
||||
let known = false;
|
||||
_eachSurface(() => {});
|
||||
for (const ref of _surfaces) {
|
||||
if ((_HasWeakRef ? ref.deref() : ref) === hw) { known = true; break; }
|
||||
}
|
||||
if (!known) _surfaces.push(_HasWeakRef ? new WeakRef(hw) : hw);
|
||||
}
|
||||
|
||||
// Hand the current selection to every highway surface (or clear it).
|
||||
// Selection survives with zero surfaces — it re-applies as instances
|
||||
// appear (song:ready for the primary, highway:created for panels).
|
||||
function _install() {
|
||||
const provider = activeProviderId ? providers.get(activeProviderId) : null;
|
||||
const payload = provider ? { id: provider.id, transform: provider.transform } : null;
|
||||
installedCount = 0;
|
||||
_eachSurface((hw) => {
|
||||
try {
|
||||
hw.setChartTransform(payload);
|
||||
if (payload) installedCount += 1;
|
||||
} catch (_) { /* one broken surface must not block the rest */ }
|
||||
});
|
||||
return installedCount > 0 || payload === null;
|
||||
}
|
||||
|
||||
function _setActive(providerId, source) {
|
||||
const from = activeProviderId;
|
||||
activeProviderId = providerId;
|
||||
activeSource = String(source || 'unknown');
|
||||
_persistSelection(providerId);
|
||||
_install();
|
||||
if (from !== providerId) {
|
||||
_emit('transform-changed', { from, to: providerId, source: activeSource });
|
||||
}
|
||||
_contributeDiagnostics();
|
||||
}
|
||||
|
||||
function _payload(ctx = {}) {
|
||||
return ctx.payload && typeof ctx.payload === 'object' ? ctx.payload : {};
|
||||
}
|
||||
|
||||
function _providersForParticipant(participantId) {
|
||||
return [...providers.values()].filter(provider => provider.pluginId === participantId);
|
||||
}
|
||||
|
||||
function _registerProviderParticipant(participantId) {
|
||||
const owned = _providersForParticipant(participantId);
|
||||
if (!owned.length) return;
|
||||
capabilities.registerParticipant(participantId, {
|
||||
'chart-transform': {
|
||||
roles: ['provider'],
|
||||
operations: ['chart.transform'],
|
||||
events: [],
|
||||
mode: 'active',
|
||||
compatibility: 'none',
|
||||
safety: 'safe',
|
||||
runtime: true,
|
||||
description: `${owned.length} registered chart transform provider${owned.length === 1 ? '' : 's'}.`,
|
||||
provider_policy: {
|
||||
providerIds: owned.map(provider => provider.id),
|
||||
providers: owned.map(provider => ({ id: provider.id, label: provider.label })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function _registerProvider(ctx = {}) {
|
||||
const payload = _payload(ctx);
|
||||
const providerId = String(payload.providerId || payload.id || '').trim();
|
||||
if (!providerId) return _degraded('Provider registration requires a providerId', _snapshot());
|
||||
if (typeof payload.transform !== 'function') {
|
||||
return _degraded('Provider registration requires a transform(input) function', _snapshot());
|
||||
}
|
||||
const participantId = String(ctx.source || ctx.requester || providerId);
|
||||
const existing = providers.get(providerId);
|
||||
if (existing && existing.pluginId !== participantId) {
|
||||
return _degraded(
|
||||
`Provider ${providerId} is already registered by a different participant`,
|
||||
_snapshot(),
|
||||
);
|
||||
}
|
||||
providers.set(providerId, {
|
||||
id: providerId,
|
||||
label: String(payload.label || providerId),
|
||||
pluginId: participantId,
|
||||
transform: payload.transform,
|
||||
});
|
||||
_registerProviderParticipant(participantId);
|
||||
_emit('provider-registered', { providerId });
|
||||
// Restore a persisted selection the moment its provider appears.
|
||||
if (!activeProviderId && _persistedSelection() === providerId) {
|
||||
_setActive(providerId, 'restore-selection');
|
||||
} else if (activeProviderId === providerId) {
|
||||
// Re-registration after script rehydration: reinstall the fresh
|
||||
// transform closure so the highway isn't holding a stale one.
|
||||
_install();
|
||||
}
|
||||
_contributeDiagnostics();
|
||||
return _handled(_snapshot({ registered: providerId }));
|
||||
}
|
||||
|
||||
function _unregisterProvider(ctx = {}) {
|
||||
const payload = _payload(ctx);
|
||||
const providerId = String(payload.providerId || payload.id || '').trim();
|
||||
const provider = providers.get(providerId);
|
||||
if (!provider) return _degraded(`Unknown chart-transform provider: ${providerId || '(none)'}`, _snapshot());
|
||||
const callerId = String(ctx.source || ctx.requester || providerId);
|
||||
if (provider.pluginId !== callerId) {
|
||||
return _degraded(
|
||||
`Provider ${providerId} can only be unregistered by its original registrant`,
|
||||
_snapshot(),
|
||||
);
|
||||
}
|
||||
providers.delete(providerId);
|
||||
if (activeProviderId === providerId) {
|
||||
// Keep the persisted selection so the provider re-activates on
|
||||
// its next registration; just detach it from the highway.
|
||||
activeProviderId = null;
|
||||
_install();
|
||||
_emit('transform-changed', { from: providerId, to: null, source: 'provider-unregistered' });
|
||||
}
|
||||
const remainingProviders = _providersForParticipant(provider.pluginId);
|
||||
if (remainingProviders.length) {
|
||||
_registerProviderParticipant(provider.pluginId);
|
||||
} else if (typeof capabilities.unregisterParticipant === 'function') {
|
||||
const live = typeof capabilities.inspect === 'function' ? capabilities.inspect('chart-transform') : null;
|
||||
const participant = ((live && live.participants) || []).find(p => p.pluginId === provider.pluginId);
|
||||
const roles = participant && Array.isArray(participant.roles) ? participant.roles : [];
|
||||
const providerOnly = roles.length === 1 && roles[0] === 'provider';
|
||||
if (!participant || providerOnly) {
|
||||
try { capabilities.unregisterParticipant(provider.pluginId, 'chart-transform'); }
|
||||
catch (_) { /* participant cleanup is best-effort */ }
|
||||
}
|
||||
}
|
||||
_emit('provider-unregistered', { providerId });
|
||||
_contributeDiagnostics();
|
||||
return _handled(_snapshot({ unregistered: providerId }));
|
||||
}
|
||||
|
||||
function _targetProviderId(ctx = {}) {
|
||||
const payload = _payload(ctx);
|
||||
const target = ctx.target && typeof ctx.target === 'object' ? ctx.target : {};
|
||||
return String(
|
||||
target.providerId || target.provider_id || target.id
|
||||
|| payload.providerId || payload.provider_id || payload.id
|
||||
|| (typeof ctx.target === 'string' ? ctx.target : '') || ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function _selectProvider(ctx = {}) {
|
||||
const providerId = _targetProviderId(ctx);
|
||||
if (!providerId) return _degraded('Transform selection requires a provider id', _snapshot());
|
||||
if (!providers.has(providerId)) {
|
||||
return _degraded(`Unknown chart-transform provider: ${providerId}`, _snapshot());
|
||||
}
|
||||
_setActive(providerId, ctx.requester ? `command:${ctx.requester}` : 'command');
|
||||
return _handled(_snapshot({ selected: providerId }));
|
||||
}
|
||||
|
||||
function _clearProvider(ctx = {}) {
|
||||
_setActive(null, ctx.requester ? `command:${ctx.requester}` : 'command');
|
||||
return _handled(_snapshot({ cleared: true }));
|
||||
}
|
||||
|
||||
function _refresh() {
|
||||
if (!activeProviderId || installedCount === 0) return _handled(_snapshot({ refreshed: false }));
|
||||
let refreshed = 0;
|
||||
_eachSurface((hw) => {
|
||||
if (typeof hw.refreshChartTransform !== 'function') return;
|
||||
try { hw.refreshChartTransform(); refreshed += 1; }
|
||||
catch (_) { /* one broken surface must not block the rest */ }
|
||||
});
|
||||
return _handled(_snapshot({ refreshed: refreshed > 0 }));
|
||||
}
|
||||
|
||||
capabilities.registerOwner('chart-transform', {
|
||||
pluginId: 'core.chart-transform',
|
||||
kind: 'provider-coordinator',
|
||||
safety: 'safe',
|
||||
commands: ['inspect', 'list-providers', 'register-provider', 'unregister-provider', 'select-provider', 'clear-provider', 'refresh'],
|
||||
operations: ['chart.transform'],
|
||||
events: ['provider-registered', 'provider-unregistered', 'transform-changed', 'transform-failed'],
|
||||
description: 'Owns chart-transform providers: pre-render/pre-scoring chart substitution applied after difficulty filtering, with selection, refresh, and failure attribution.',
|
||||
handlers: {
|
||||
inspect: () => _handled(_snapshot()),
|
||||
'list-providers': () => _handled(_snapshot()),
|
||||
'register-provider': (ctx) => _registerProvider(ctx),
|
||||
'unregister-provider': (ctx) => _unregisterProvider(ctx),
|
||||
'select-provider': (ctx) => _selectProvider(ctx),
|
||||
'clear-provider': (ctx) => _clearProvider(ctx),
|
||||
refresh: () => _refresh(),
|
||||
},
|
||||
});
|
||||
|
||||
// Bus mirroring (guarded: the bus may not exist in minimal/test envs).
|
||||
const sm = window.feedBack;
|
||||
if (typeof sm.on === 'function') {
|
||||
try {
|
||||
sm.on('highway:chart-transform-failed', (e) => {
|
||||
const detail = (e && e.detail) || e || {};
|
||||
lastFailure = {
|
||||
providerId: String(detail.id || activeProviderId || 'unknown'),
|
||||
reason: PUBLIC_FAILURE_REASON,
|
||||
};
|
||||
_emit('transform-failed', { ...lastFailure });
|
||||
_contributeDiagnostics();
|
||||
});
|
||||
// The primary highway is created after this module evaluates —
|
||||
// install a pending selection once a song is loading/ready.
|
||||
sm.on('song:ready', () => {
|
||||
if (activeProviderId && installedCount === 0 && _install()) {
|
||||
// setChartTransform restages immediately, so the chart
|
||||
// that just became ready picks the transform up now.
|
||||
_contributeDiagnostics();
|
||||
}
|
||||
});
|
||||
// Additional instances restage the active provider against their
|
||||
// own chart state.
|
||||
sm.on('highway:created', (e) => {
|
||||
const detail = (e && e.detail) || e || {};
|
||||
if (!_capable(detail.highway)) return;
|
||||
_rememberSurface(detail.highway);
|
||||
if (activeProviderId) _install();
|
||||
_contributeDiagnostics();
|
||||
});
|
||||
} catch (_) { /* bus mirroring is best-effort */ }
|
||||
}
|
||||
|
||||
window.feedBack.chartTransformDomain = {
|
||||
version: 1,
|
||||
snapshot: _snapshot,
|
||||
};
|
||||
_contributeDiagnostics();
|
||||
})();
|
||||
Reference in New Issue
Block a user