import { ComponentMetaspaceScope } from "../../component-types.mjs"; import thisCss from "./keystone-scrubber.css"; import thisHtml from "./keystone-scrubber.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs"; import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase_Parent, } from "../../ibgib-dynamic-component-bases.mjs"; import { ElementsBase, ChildInfoBase, IbGibDynamicComponentInstance, IbGibDynamicComponentInstanceInitOpts, } from "../../component-types.mjs"; import { getComponentSvc } from "../../ibgib-component-service.mjs"; import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs"; import { getContextModeService } from "../../../../context/context-mode-service.mjs"; const logalot = false; export const KEYSTONE_SCRUBBER_COMPONENT_NAME = 'ibgib-keystone-scrubber'; export class KeystoneScrubberComponentMeta extends IbGibDynamicComponentMetaBase { protected lc: string = `[KeystoneScrubberComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${KEYSTONE_SCRUBBER_COMPONENT_NAME}$`); componentName = KEYSTONE_SCRUBBER_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (!customElements.get(this.componentName)) { customElements.define(this.componentName, KeystoneScrubberComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const lc = `${this.lc}[${this.createInstance.name}]`; const component = document.createElement(this.componentName) as KeystoneScrubberComponentInstance; await component.initialize({ ibGibAddr, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } interface ScrubberTabInfo extends ChildInfoBase> { } interface KeystoneScrubberElements extends ElementsBase { containerEl: HTMLDivElement; btnScrubFirstEl: HTMLButtonElement; btnScrubPrev10El: HTMLButtonElement; btnScrubPrevEl: HTMLButtonElement; scrubberStatusEl: HTMLSpanElement; btnScrubNextEl: HTMLButtonElement; btnScrubNext10El: HTMLButtonElement; btnScrubLatestEl: HTMLButtonElement; } export class KeystoneScrubberComponentInstance extends IbGibDynamicComponentInstanceBase_Parent implements IbGibDynamicComponentInstance { /** Identity is device-scoped and mode-agnostic: always use the persistent metaspace. */ protected override metaspaceScope: ComponentMetaspaceScope = ComponentMetaspaceScope.persistent; protected override lc: string = `[KeystoneScrubberComponentInstance]`; private history: string[] = []; private currentFrameIndex: number = -1; private lastLoadedHistoryAddr: string | null = null; constructor() { console.log('yes this is being updated! I am just wondering if I am seeing old code in the browser, i.e., if refresh is happening or just getting cache (I: 6599db11c1086062b8f5e86db89cba26)') const lc = `${KeystoneScrubberComponentInstance.name}[ctor]`; if (logalot) { console.log(`${lc} (I: 3adca837d5c898d8e839d23c59538126)`); } super(); } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { const lc = `${this.lc}[${this.initialize.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 75eecfc01af7f3fa9897932ec5457526)`); } await super.initialize(opts); await this.loadIbGib(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } override async created(): Promise { const lc = `${this.lc}[${this.created.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: bc16e6212928658ed8034c75b6279526)`); } this.elements = {} as KeystoneScrubberElements; const shadow = this.shadowRoot!; this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement; this.elements.btnScrubFirstEl = shadow.getElementById('btn-scrub-first') as HTMLButtonElement; this.elements.btnScrubPrev10El = shadow.getElementById('btn-scrub-prev10') as HTMLButtonElement; this.elements.btnScrubPrevEl = shadow.getElementById('btn-scrub-prev') as HTMLButtonElement; this.elements.scrubberStatusEl = shadow.getElementById('scrubber-status') as HTMLSpanElement; this.elements.btnScrubNextEl = shadow.getElementById('btn-scrub-next') as HTMLButtonElement; this.elements.btnScrubNext10El = shadow.getElementById('btn-scrub-next10') as HTMLButtonElement; this.elements.btnScrubLatestEl = shadow.getElementById('btn-scrub-latest') as HTMLButtonElement; // ElementBase contentEl holds the child component this.elements.contentEl = shadow.getElementById('keystone-scrubber-content') as HTMLDivElement; this.initHandlers(); this.loadHistoryData().catch(err => console.error(`${this.lc} loadHistoryData error: ${extractErrorMsg(err)}`)); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } override async disconnected(): Promise { // No-op } private initHandlers() { this.elements!.btnScrubFirstEl.addEventListener('click', () => this.scrubFirst()); this.elements!.btnScrubPrev10El.addEventListener('click', () => this.scrubPrev10()); this.elements!.btnScrubPrevEl.addEventListener('click', () => this.scrubPrev()); this.elements!.btnScrubNextEl.addEventListener('click', () => this.scrubNext()); this.elements!.btnScrubNext10El.addEventListener('click', () => this.scrubNext10()); this.elements!.btnScrubLatestEl.addEventListener('click', () => this.scrubLatest()); } override async loadIbGib(): Promise { const lc = `${this.lc}[${this.loadIbGib.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: a4a9181bbb58a22cc71c8745a91ec426)`); } if (!this.ibGibAddr) return; await super.loadIbGib(); await this.loadHistoryData(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } public override async activateIbGib({ ibGibAddr, ibGib, }: { ibGibAddr?: IbGibAddr, ibGib?: IbGib_V1, }): Promise { const lc = `${this.lc}[activateIbGib]`; try { if (logalot) { console.log(`${lc} starting... (I: 890a8cb88f72c918a8bcf82c8af63e26)`); } if (!ibGibAddr) return; // Get or create the details component for this specific frame const childInfo = await this.getLoadedChildInfo({ addr: ibGibAddr, ibGib }); // Manage active states in childInfos const currentlyActive = this.activeChildInfo; if (currentlyActive) { currentlyActive.active = false; } childInfo.active = true; // Re-use the framework's content injection code const componentSvc = await getComponentSvc(); await componentSvc.inject({ parentEl: this.elements!.contentEl, componentToInject: childInfo.component!, }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private async loadHistoryData(): Promise { if (!this.ibGibAddr || !this.elements) return; if (this.ibGibAddr !== this.lastLoadedHistoryAddr) { await this.loadHistory(this.ibGibAddr); this.lastLoadedHistoryAddr = this.ibGibAddr; } await this.scrubTo(this.history.length - 1); } private async loadHistory(tipAddr: string): Promise { const lc = `${this.lc}[loadHistory]`; this.history = []; this.currentFrameIndex = -1; const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) return; const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) return; let currentAddr: string | undefined = tipAddr; const tempHistory: string[] = []; while (currentAddr) { const res = await metaspace.get({ addr: currentAddr, space }); if (!res.success || !res.ibGibs || res.ibGibs.length === 0) { console.warn(`${lc} Could not find frame at ${currentAddr}`); break; } const keystone = res.ibGibs[0] as KeystoneIbGib_V1; tempHistory.push(currentAddr); if (keystone.data?.n === 0) { break; } const past = keystone.rel8ns?.past; currentAddr = (past && past.length > 0) ? past[past.length - 1] : undefined; } this.history = tempHistory.reverse(); this.currentFrameIndex = this.history.length - 1; } private async scrubTo(index: number): Promise { if (this.history.length === 0) return; this.currentFrameIndex = Math.max(0, Math.min(index, this.history.length - 1)); this.updateScrubberUI(); const addr = this.history[this.currentFrameIndex]; await this.activateIbGib({ ibGibAddr: addr }); } private scrubFirst() { this.scrubTo(0); } private scrubPrev10() { this.scrubTo(this.currentFrameIndex - 10); } private scrubPrev() { this.scrubTo(this.currentFrameIndex - 1); } private scrubNext() { this.scrubTo(this.currentFrameIndex + 1); } private scrubNext10() { this.scrubTo(this.currentFrameIndex + 10); } private scrubLatest() { this.scrubTo(this.history.length - 1); } private updateScrubberUI() { if (!this.elements) return; this.elements.scrubberStatusEl.textContent = `Frame ${this.currentFrameIndex + 1} / ${this.history.length}`; this.elements.btnScrubFirstEl.disabled = this.currentFrameIndex === 0; this.elements.btnScrubPrev10El.disabled = this.currentFrameIndex === 0; this.elements.btnScrubPrevEl.disabled = this.currentFrameIndex === 0; this.elements.btnScrubNextEl.disabled = this.currentFrameIndex === this.history.length - 1; this.elements.btnScrubNext10El.disabled = this.currentFrameIndex === this.history.length - 1; this.elements.btnScrubLatestEl.disabled = this.currentFrameIndex === this.history.length - 1; } protected async getLoadedChildInfo({ addr, ibGib, }: { addr: IbGibAddr, ibGib?: IbGib_V1, }): Promise { const lc = `${this.lc}[${this.getLoadedChildInfo.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 9eea188f6b55f04b483fbda8a110f826)`); } if (!addr) { throw new Error(`(UNEXPECTED) addr falsy?`); } const tjpGib = getGibInfo({ ibGibAddr: addr }).tjpGib ?? getIbAndGib({ ibGibAddr: addr }).gib; const fnCreateAndLoadChildComponent = async () => { const componentSvc = await getComponentSvc(); const detailsComponent = await componentSvc.getComponentInstance({ path: 'ibgib-keystone-details', ibGibAddr: addr, useRegExpPrefilter: true, }); if (!detailsComponent) { throw new Error(`(UNEXPECTED) detailsComponent falsy?`); } return detailsComponent; } let tabInfo: ScrubberTabInfo; const filtered = this.childInfos.filter(x => x.addr === addr); if (filtered.length > 0) { if (filtered.length !== 1) { throw new Error(`(UNEXPECTED) filtered.length !== 1?`); } tabInfo = filtered[0]; if (!tabInfo.component) { tabInfo.component = await fnCreateAndLoadChildComponent(); } } else { const detailsComponent = await fnCreateAndLoadChildComponent(); ibGib = detailsComponent.ibGib ?? ibGib; const tabBtnEl = await this.addChild({ addr, ibGib: ibGib! }); tabInfo = { addr, childBtnEl: tabBtnEl, component: detailsComponent, active: false, }; this.childInfos.push(tabInfo); } return tabInfo; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } protected override async addChild({ addr, ibGib, }: { ibGib: IbGib_V1, addr?: IbGibAddr, }): Promise { const lc = `${this.lc}[${this.addChild.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 824f98b6f9a2c922f8b3a9f67ad63e26)`); } // why are we making a dummy??? // Return a dummy hidden element since we don't display visual tab buttons in the scrubber const dummy = document.createElement('div'); dummy.style.display = 'none'; return dummy; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } }