UNPKG

@casual-simulation/aux-vm-browser

Version:

A set of utilities required to securely run an AUX in a web browser.

1,099 lines 44.9 kB
import { calculateStringTagValue, getBotTag, hasValue, SYSTEM_PORTAL, SYSTEM_PORTAL_BOT, SYSTEM_TAG, getTagMask, calculateBooleanTagValue, EDITING_BOT, EDITING_TAG, getShortId, isScript, EDITING_TAG_SPACE, getTagValueForSpace, parseNewTag, DNA_TAG_PREFIX, isFormula, SYSTEM_PORTAL_TAG, SYSTEM_PORTAL_TAG_SPACE, isBotLink, calculateBotIdTagValue, SYSTEM_PORTAL_SEARCH, parseScript, parseFormula, formatValue, BOT_LINK_TAG_PREFIX, getScriptPrefix, SYSTEM_TAG_NAME, calculateFormattedBotValue, SYSTEM_PORTAL_DIFF, SYSTEM_PORTAL_DIFF_BOT, SYSTEM_PORTAL_DIFF_TAG, SYSTEM_PORTAL_DIFF_TAG_SPACE, getOpenSystemPortalPane, isModule, parseModule, } from '@casual-simulation/aux-common'; import { isEqual, sortBy, union, unionBy } from 'es-toolkit/compat'; import { BehaviorSubject, combineLatest, merge, Observable, Subscription, } from 'rxjs'; import { bufferTime, distinctUntilChanged, filter, mergeMap, map, scan, startWith, switchMap, takeUntil, } from 'rxjs/operators'; /** * The number of tags that should be processed per time that the search buffer is updated. */ const TAGS_PER_SEARCH_UPDATE = 10; /** * The number of miliseconds that should be waited between search updates. */ const SEARCH_UPDATE_WAIT_INTERVAL = 100; /** * Defines a class that is able to manage the state of the system portal. */ export class SystemPortalCoordinator { // private _portals: PortalManager; get tagSortMode() { return this._tagSortMode; } set tagSortMode(mode) { this._tagSortMode = mode; const result = this._findSelection(this._itemsUpdated.value); if (!isEqual(result, this._selectionUpdated.value)) { this._selectionUpdated.next(result); } } get items() { return this._itemsUpdated.value; } unsubscribe() { return this._sub.unsubscribe(); } get closed() { return this._sub.closed; } get onItemsUpdated() { return this._itemsUpdated; } get onSelectionUpdated() { return this._selectionUpdated; } get onRecentsUpdated() { return this._recentsUpdated; } get onSearchResultsUpdated() { return this._searchUpdated; } get onDiffUpdated() { return this._diffUpdated; } get onDiffSelectionUpdated() { return this._diffSelectionUpdated; } get onSystemPortalPaneUpdated() { return this._systemPortalPaneUpdated; } /** * Creates a new system portal coorindator. * @param simulationManager The simulation manager that should be used. * @param bufferEvents Whether to buffer the update events. */ constructor(simulationManager, bufferEvents = true) { this._sub = new Subscription(); this._recentTags = []; this._recentTagsListSize = 10; this._tagSortMode = 'scripts-first'; this._extraTags = []; this._simulationManager = simulationManager; this._buffer = bufferEvents; // this._watcher = watcher; // this._helper = helper; // this._portals = portals; this._itemsUpdated = new BehaviorSubject({ hasPortal: false, }); this._selectionUpdated = new BehaviorSubject({ hasSelection: false, }); this._recentsUpdated = new BehaviorSubject({ hasRecents: false, }); this._searchUpdated = new BehaviorSubject({ numMatches: 0, numBots: 0, items: [], }); this._diffUpdated = new BehaviorSubject({ hasPortal: false, }); this._diffSelectionUpdated = new BehaviorSubject({ hasSelection: false, }); this._systemPortalPaneUpdated = new BehaviorSubject(null); const itemsUpdated = this._calculateItemsUpdated(); const itemsUpdatedDistinct = itemsUpdated.pipe(distinctUntilChanged((x, y) => isEqual(x, y))); const selectionUpdated = this._calculateSelectionUpdated(itemsUpdatedDistinct); const diffUpdated = this._calculateDiffUpdated(itemsUpdated); const diffSelectionUpdated = this._calculateDiffSelectionUpdated(diffUpdated); const paneUpdated = this._calculateSystemPortalPaneUpdated(); this._sub.add(itemsUpdatedDistinct.subscribe(this._itemsUpdated)); this._sub.add(selectionUpdated.subscribe(this._selectionUpdated)); this._sub.add(this._calculateRecentsUpdated().subscribe(this._recentsUpdated)); this._sub.add(this._calculateSearchResults().subscribe(this._searchUpdated)); this._sub.add(diffUpdated.subscribe(this._diffUpdated)); this._sub.add(diffSelectionUpdated.subscribe(this._diffSelectionUpdated)); this._sub.add(paneUpdated.subscribe(this._systemPortalPaneUpdated)); } async addTag(tag) { const parsed = parseNewTag(tag); const primarySim = this._simulationManager.primary; const helper = primarySim.helper; const selectedBotId = calculateBotIdTagValue(helper.userBot, SYSTEM_PORTAL_BOT, null); const selectedBot = selectedBotId ? helper.botsState[selectedBotId] : null; if ((parsed.isScript || parsed.isFormula) && selectedBot) { if (!hasValue(selectedBot.tags[parsed.name])) { await helper.updateBot(selectedBot, { tags: { [parsed.name]: parsed.isScript ? '@' : parsed.isFormula ? DNA_TAG_PREFIX : '', }, }); } } this._updateSelection(undefined, [parsed.name]); } /** * Adds the given tag as a pinned tag. * Pinned tags are a separate list of tags that are persisted across multiple selections. * @param tag The name of the tag to pin. */ async addPinnedTag(tag) { const parsed = parseNewTag(tag); if (this._extraTags.includes(parsed.name)) { return; } const primarySim = this._simulationManager.primary; const helper = primarySim.helper; const selectedBotId = calculateBotIdTagValue(helper.userBot, SYSTEM_PORTAL_BOT, null); const selectedBot = selectedBotId ? helper.botsState[selectedBotId] : null; if ((parsed.isScript || parsed.isFormula) && selectedBot) { if (!hasValue(selectedBot.tags[parsed.name])) { await helper.updateBot(selectedBot, { tags: { [parsed.name]: parsed.isScript ? '@' : parsed.isFormula ? DNA_TAG_PREFIX : '', }, }); } } this._updateSelection([parsed.name]); } removePinnedTag(tag) { const index = this._extraTags.findIndex((t) => t === tag.name); if (index >= 0) { this._extraTags.splice(index, 1); } this._updateSelection(); } _updateSelection(extraTags, addedTags) { const update = this._findSelection(this._itemsUpdated.value, extraTags, addedTags); if (!isEqual(update, this._selectionUpdated.value)) { this._selectionUpdated.next(update); } } _calculateItemsUpdated() { const allBotsUpdatedAddedAndRemoved = this._simulationManager.simulationAdded.pipe(mergeMap((sim) => { return merge(sim.watcher.botsDiscovered, sim.watcher.botsUpdated, sim.watcher.botsRemoved).pipe(takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))); })); const bufferedEvents = this._buffer ? allBotsUpdatedAddedAndRemoved.pipe(bufferTime(10)) : allBotsUpdatedAddedAndRemoved; return bufferedEvents.pipe(map(() => this._findMatchingItems())); } _findMatchingItems() { let items = []; let hasPortal = false; let selectedBot = null; let selectedBotSimulationId = null; for (let [id, sim] of this._simulationManager.simulations) { const helper = sim.helper; if (!helper.userBot) { continue; } const systemTag = calculateStringTagValue(null, helper.userBot, SYSTEM_TAG_NAME, SYSTEM_TAG); const systemPortal = calculateStringTagValue(null, helper.userBot, SYSTEM_PORTAL, null); const showAllSystemBots = calculateBooleanTagValue(null, helper.userBot, SYSTEM_PORTAL, false); if (showAllSystemBots || hasValue(systemPortal)) { if (!hasValue(selectedBot)) { selectedBot = calculateBotIdTagValue(helper.userBot, SYSTEM_PORTAL_BOT, null); if (hasValue(selectedBot)) { selectedBotSimulationId = sim.id; } } let areaItems = []; let areas = new Map(); for (let bot of helper.objects) { if (bot.id === helper.userId) { continue; } const system = calculateFormattedBotValue(null, bot, systemTag); if (bot.id === selectedBot || (hasValue(system) && (showAllSystemBots || system.includes(systemPortal)))) { const area = getSystemArea(system); const title = getBotTitle(system, area); let item = areas.get(area); if (!item) { item = { area, bots: [], }; areaItems.push(item); areas.set(area, item); } item.bots.push({ bot, title, system, }); } } for (let item of areaItems) { item.bots = sortBy(item.bots, (b) => b.title); } areaItems = sortBy(areaItems, (i) => i.area); hasPortal = true; if (areaItems.length > 0) { items.push({ simulationId: sim.id, areas: areaItems, }); } } } if (hasPortal) { return { hasPortal: true, items, selectedBot, selectedBotSimulationId, }; } else { return { hasPortal: false, }; } } _calculateSelectionUpdated(itemsUpdated) { return combineLatest([ itemsUpdated, this._simulationManager.simulationAdded.pipe(mergeMap((sim) => sim.watcher.botTagsChanged(sim.helper.userId).pipe(filter((change) => change.tags.has(SYSTEM_PORTAL_TAG) || change.tags.has(SYSTEM_PORTAL_TAG_SPACE)), startWith(1), takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))))), ]).pipe(map(([update, _]) => update), map((update) => this._findSelection(update)), distinctUntilChanged((first, second) => isEqual(first, second))); } _findSelection(update, tagsToPin, tagsToAdd) { if (!update.hasPortal || !update.selectedBot || !update.selectedBotSimulationId) { return { hasSelection: false, }; } const sim = this._simulationManager.simulations.get(update.selectedBotSimulationId); if (!sim) { return { hasSelection: false, }; } const helper = sim.helper; const bot = helper.botsState[update.selectedBot]; if (!bot) { return { hasSelection: false, }; } const selectedTag = calculateStringTagValue(null, helper.userBot, SYSTEM_PORTAL_TAG, null); const selectedSpace = calculateStringTagValue(null, helper.userBot, SYSTEM_PORTAL_TAG_SPACE, null); let normalTags = Object.keys(bot.tags).map((t) => createSelectionTag(bot, t)); let maskTags = []; for (let space in bot.masks) { const tags = Object.keys(bot.masks[space]); maskTags.push(...tags.map((t) => createSelectionTag(bot, t, space))); } if (hasValue(selectedTag)) { if (hasValue(selectedSpace)) { maskTags.push(createSelectionTag(bot, selectedTag, selectedSpace)); } else { maskTags.push(createSelectionTag(bot, selectedTag)); } } let addedTags = []; if (hasValue(tagsToAdd)) { addedTags.push(...tagsToAdd.map((t, i) => ({ ...createSelectionTag(bot, t), focusValue: i === 0, }))); } const sortMode = this.tagSortMode; const inputTags = unionBy([...addedTags, ...normalTags, ...maskTags], (t) => `${t.name}.${t.space}`); const tags = sortTags(inputTags); let ret = { hasSelection: true, sortMode, bot, tags, simulationId: sim.id, }; let pinnedTags = []; if (hasValue(tagsToPin)) { pinnedTags.push(...tagsToPin.map((t, i) => ({ ...createSelectionTag(bot, t), focusValue: i === 0, }))); } pinnedTags.push(...this._extraTags.map((t) => createSelectionTag(bot, t))); if (hasValue(tagsToPin)) { this._extraTags.push(...tagsToPin); } pinnedTags = sortTags(pinnedTags); if (pinnedTags.length > 0) { ret.pinnedTags = pinnedTags; } if (hasValue(selectedTag)) { ret.tag = selectedTag; ret.space = selectedSpace; } return ret; function createSelectionTag(bot, tag, space) { let selectionTag = { name: tag, }; const tagValue = !hasValue(space) ? getBotTag(bot, tag) : getTagMask(bot, space, tag); if (isScript(tagValue)) { selectionTag.isScript = true; } if (isFormula(tagValue)) { selectionTag.isFormula = true; } if (isBotLink(tagValue)) { selectionTag.isLink = true; } if (hasValue(space)) { selectionTag.space = space; } const prefix = sim.portals.getScriptPrefix(tagValue); if (hasValue(prefix)) { selectionTag.prefix = prefix; } return selectionTag; } function sortTags(input) { return sortMode === 'scripts-first' ? sortBy(input, (t) => !t.isScript, (t) => t.name) : sortBy(input, (t) => t.name); } } _calculateRecentsUpdated() { const changes = this._simulationManager.simulationAdded.pipe(mergeMap((sim) => sim.watcher.botTagsChanged(sim.helper.userId).pipe(filter((c) => c.tags.has(EDITING_BOT) || c.tags.has(EDITING_TAG)), takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))))); return changes.pipe(map(() => this._updateRecentsList())); } _updateRecentsList() { let hasNewBot = false; let hasNewTag = false; let newRecentTags = []; for (let [id, sim] of this._simulationManager.simulations) { const helper = sim.helper; const newBotId = calculateBotIdTagValue(helper.userBot, EDITING_BOT, null); const newTag = calculateStringTagValue(null, helper.userBot, EDITING_TAG, null); const newSpace = calculateStringTagValue(null, helper.userBot, EDITING_TAG_SPACE, null); const systemTag = calculateStringTagValue(null, helper.userBot, SYSTEM_TAG_NAME, SYSTEM_TAG); if (!newBotId || !newTag) { continue; } const newBot = helper.botsState[newBotId]; if (!newBot || !newTag) { continue; } if (newBot) { hasNewBot = true; } if (newTag) { hasNewTag = true; } const recentTagsCounts = new Map(); recentTagsCounts.set(`${newTag}.${newSpace}`, 1); for (let tag of this._recentTags) { if (tag.simulationId !== sim.id) { continue; } if (tag.botId === newBot.id && tag.tag === newTag && tag.space === newSpace) { continue; } const key = `${tag.tag}.${tag.space}`; recentTagsCounts.set(key, (recentTagsCounts.get(key) ?? 0) + 1); } let newTags = []; newTags.push({ ...getTagPrefix(sim, systemTag, recentTagsCounts, newTag, newBot, newSpace), botId: newBot.id, tag: newTag, space: newSpace, simulationId: sim.id, }); for (let recent of this._recentTags) { if (recent.simulationId !== sim.id) { continue; } if (recent.tag === newTag && recent.botId === newBot.id && recent.space === newSpace) { continue; } else if (newTags.length < this._recentTagsListSize) { const recentBot = helper.botsState[recent.botId]; if (!recentBot) { continue; } newTags.push({ ...getTagPrefix(sim, systemTag, recentTagsCounts, recent.tag, recentBot, recent.space), tag: recent.tag, botId: recent.botId, space: recent.space, simulationId: sim.id, }); } else { break; } } newRecentTags.push(newTags); } if (!hasNewBot || !hasNewTag) { return this._recentsUpdated.value; } this._recentTags = newRecentTags.flatMap((t) => t); if (this._recentTags.length > 0) { return { hasRecents: true, recentTags: this._recentTags, }; } return { hasRecents: false, }; function getTagPrefix(sim, systemTag, recentTagsCounts, tag, bot, space) { const tagValue = getTagValueForSpace(bot, tag, space); const isTagScript = isScript(tagValue); const isTagFormula = isFormula(tagValue); const isTagLink = isBotLink(tagValue); const tagPrefix = sim.portals.getScriptPrefix(tagValue); const system = calculateFormattedBotValue(null, bot, systemTag); let ret = { system, isScript: isTagScript, isFormula: isTagFormula, isLink: isTagLink, }; if (hasValue(tagPrefix)) { ret.prefix = tagPrefix; } const area = getSystemArea(system); const prefix = hasValue(system) && hasValue(area) ? system.substring(area.length + 1) : null; return { hint: prefix ?? getShortId(bot), ...ret, }; } } _calculateDiffUpdated(itemsUpdated) { return combineLatest([ itemsUpdated, this._simulationManager.simulationAdded.pipe(mergeMap((sim) => sim.watcher.botTagsChanged(sim.helper.userId).pipe(filter((change) => change.tags.has(SYSTEM_PORTAL_DIFF) || change.tags.has(SYSTEM_PORTAL_DIFF_BOT)), startWith(1), takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))))), ]).pipe(map(([update, _]) => update), map((update) => this._findDiff(update)), distinctUntilChanged((first, second) => isEqual(first, second))); } _findDiff(update) { if (!update.hasPortal) { return { hasPortal: false, }; } let areas = []; let areasMap = new Map(); let systems = new Map(); let selectedKey = null; let showAllSystemBots = null; let systemPortal = null; for (let [id, sim] of this._simulationManager.simulations) { const helper = sim.helper; if (!helper.userBot) { return { hasPortal: false, }; } const diffTag = calculateStringTagValue(null, helper.userBot, SYSTEM_PORTAL_DIFF, null); if (!hasValue(systemPortal)) { systemPortal = calculateStringTagValue(null, helper.userBot, SYSTEM_PORTAL, null); } if (!hasValue(showAllSystemBots)) { showAllSystemBots = calculateBooleanTagValue(null, helper.userBot, SYSTEM_PORTAL, null); } if (!hasValue(diffTag)) { return { hasPortal: false, }; } if (!hasValue(selectedKey)) { selectedKey = calculateBotIdTagValue(helper.userBot, SYSTEM_PORTAL_DIFF_BOT, null); } for (let bot of helper.objects) { const system = calculateFormattedBotValue(null, bot, diffTag); if (hasValue(system)) { let list = systems.get(system); if (!list) { list = [ { bot, simulationId: sim.id, diffTag, }, ]; systems.set(system, list); } else { list.push({ bot, simulationId: sim.id, diffTag, }); } } } } for (let i of update.items) { const sim = this._simulationManager.simulations.get(i.simulationId); const helper = sim.helper; const systemTag = calculateStringTagValue(null, helper.userBot, SYSTEM_TAG_NAME, SYSTEM_TAG); for (let area of i.areas) { let items = []; for (let item of area.bots) { if (systems.has(item.system)) { const bots = systems.get(item.system); const [bot] = bots.splice(0, 1); if (bots.length <= 0) { systems.delete(item.system); } const originalBot = item.bot; const newBot = bot.bot; const diffTag = bot.diffTag; const changedTags = sortBy(findChangedTags(originalBot, newBot).filter((t) => t.name !== diffTag && t.name !== systemTag), (t) => t.name); items.push({ key: item.bot.id, title: item.title, originalBot: item.bot, originalBotSimulationId: item.bot ? i.simulationId : null, newBot, newBotSimulationId: bot.simulationId, changedTags: changedTags .filter((t) => t.status !== 'none') .map((t) => ({ tag: t.name, space: t.space, })), }); } else { // bot was removed items.push({ key: item.bot.id, title: item.title, removedBot: item.bot, removedBotSimulationId: i.simulationId, }); } } if (items.length > 0) { let diffArea = areasMap.get(area.area); if (!diffArea) { diffArea = { area: area.area, bots: items, }; areas.push(diffArea); areasMap.set(area.area, diffArea); } else { diffArea.bots.push(...items); } } } } for (let [system, bots] of systems) { if (!showAllSystemBots && !system.includes(systemPortal)) { continue; } // added bots const area = getSystemArea(system); const title = getBotTitle(system, area); let diffArea = areas.find((a) => a.area === area); if (!diffArea) { diffArea = { area, bots: [], }; areasMap.set(area, diffArea); areas.push(diffArea); } for (let bot of bots) { diffArea.bots.push({ key: bot.bot.id, title: title, addedBot: bot.bot, addedBotSimulationId: bot.simulationId, }); } } for (let item of areas) { item.bots = sortBy(item.bots, (b) => b.title); } return { hasPortal: true, selectedKey: selectedKey, items: sortBy(areas, (a) => a.area), }; } _calculateDiffSelectionUpdated(diffUpdated) { return combineLatest([ diffUpdated, this._simulationManager.simulationAdded.pipe(mergeMap((sim) => sim.watcher.botTagsChanged(sim.helper.userId).pipe(filter((change) => change.tags.has(SYSTEM_PORTAL_DIFF_BOT) || change.tags.has(SYSTEM_PORTAL_DIFF_TAG) || change.tags.has(SYSTEM_PORTAL_DIFF_TAG_SPACE) // change.tags.has(SYSTEM_PORTAL_TAG_SPACE) ), startWith(1), takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))))), ]).pipe(map(([update, _]) => update), map((update) => this._findDiffSelection(update)), distinctUntilChanged((first, second) => isEqual(first, second))); } _findDiffSelection(update) { if (!update.hasPortal || !update.selectedKey) { return { hasSelection: false, }; } let bot; for (let i of update.items) { if (bot) { break; } for (let b of i.bots) { if (b.key === update.selectedKey) { bot = b; break; } } } if (!bot) { return { hasSelection: false, }; } const newBot = 'addedBot' in bot ? bot.addedBot : 'removedBot' in bot ? null : bot.newBot; const newBotSimulationId = 'addedBot' in bot ? bot.addedBotSimulationId : 'removedBot' in bot ? bot.removedBotSimulationId : bot.newBotSimulationId; const oldBot = 'addedBot' in bot ? null : 'removedBot' in bot ? bot.removedBot : bot.originalBot; const oldBotSimulationId = 'addedBot' in bot ? bot.addedBotSimulationId : 'removedBot' in bot ? bot.removedBotSimulationId : bot.originalBotSimulationId; const systemTagSimId = oldBotSimulationId; const diffTagSimId = newBotSimulationId; const systemTagSim = this._simulationManager.simulations.get(systemTagSimId); const diffTagSim = this._simulationManager.simulations.get(diffTagSimId); const diffTag = calculateStringTagValue(null, diffTagSim.helper.userBot, SYSTEM_PORTAL_DIFF, null) ?? calculateStringTagValue(null, systemTagSim.helper.userBot, SYSTEM_PORTAL_DIFF, null); const systemTag = calculateStringTagValue(null, systemTagSim.helper.userBot, SYSTEM_TAG_NAME, null) ?? calculateStringTagValue(null, diffTagSim.helper.userBot, SYSTEM_TAG_NAME, SYSTEM_TAG); const selectedTag = calculateStringTagValue(null, systemTagSim.helper.userBot, SYSTEM_PORTAL_DIFF_TAG, null) ?? calculateStringTagValue(null, diffTagSim.helper.userBot, SYSTEM_PORTAL_DIFF_TAG, null); const selectedSpace = calculateStringTagValue(null, systemTagSim.helper.userBot, SYSTEM_PORTAL_DIFF_TAG_SPACE, null) ?? calculateStringTagValue(null, diffTagSim.helper.userBot, SYSTEM_PORTAL_DIFF_TAG_SPACE, null); const changedTags = findChangedTags(oldBot, newBot).filter((t) => t.name !== diffTag && t.name !== systemTag); return { hasSelection: true, newBot, newBotSimulationId: newBot ? newBotSimulationId : null, originalBot: oldBot, originalBotSimulationId: oldBot ? oldBotSimulationId : null, tag: selectedTag, space: selectedSpace, tags: sortBy(changedTags, (t) => `${t.name}.${t.space}`), }; } _calculateSearchResults() { const changes = this._simulationManager.simulationAdded.pipe(mergeMap((sim) => sim.watcher.botTagsChanged(sim.helper.userId).pipe(filter((c) => c.tags.has(SYSTEM_PORTAL_SEARCH) || c.tags.has(SYSTEM_TAG_NAME)), takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))))); return changes.pipe(switchMap(() => this._searchResultsUpdate())); } _searchResultsUpdate() { let runSearch = async (observer, cancelFlag) => { let hadUpdate = false; let matchCount = 0; let botCount = 0; let completeItems = []; let tagCounter = 0; let hasUpdate = false; let buffer = this._buffer; for (let [id, sim] of this._simulationManager.simulations) { if (cancelFlag.closed) { break; } const helper = sim.helper; const systemTag = calculateStringTagValue(null, helper.userBot, SYSTEM_TAG_NAME, SYSTEM_TAG); let bots = sortBy(helper.objects, (b) => calculateFormattedBotValue(null, b, systemTag)); const query = calculateStringTagValue(null, helper.userBot, SYSTEM_PORTAL_SEARCH, null); if (!query) { continue; } let areas = new Map(); function createUpdate() { let itemAreas = []; for (let [area, value] of areas) { itemAreas.push({ area, bots: value, }); } const items = [...completeItems]; const item = { simulationId: sim.id, areas: sortBy(itemAreas, (i) => i.area), }; if (itemAreas.length > 0) { items.push(item); } hadUpdate = true; return { update: { numMatches: matchCount, numBots: botCount, items, }, item, }; } function checkTagCounter() { if (buffer && hasUpdate && tagCounter > TAGS_PER_SEARCH_UPDATE) { hasUpdate = false; tagCounter = 0; let { update } = createUpdate(); observer.next(update); return true; } return false; } const prefixes = sim.portals.prefixes; for (let bot of bots) { if (cancelFlag.closed) { break; } if (bot.id === helper.userId) { continue; } const system = calculateFormattedBotValue(null, bot, systemTag); const area = getSystemArea(system); const title = getBotTitle(system, area); let tags = []; if (bot.id === query) { const result = searchTag('id', null, bot.id, query, prefixes); if (result) { tags.push(result); matchCount += result.matches.length; tagCounter += 1; } } if (bot.space === query) { const result = searchTag('space', null, bot.space, query, prefixes); if (result) { tags.push(result); matchCount += result.matches.length; tagCounter += 1; } } for (let tag in bot.tags) { let value = bot.tags[tag]; const result = searchTag(tag, null, value, query, prefixes); if (result) { tags.push(result); matchCount += result.matches.length; tagCounter += 1; } } for (let space in bot.masks) { let spaceTags = bot.masks[space]; for (let tag in spaceTags) { let value = spaceTags[tag]; const result = searchTag(tag, space, value, query, prefixes); if (result) { tags.push(result); matchCount += result.matches.length; tagCounter += 1; } } } if (tags.length > 0) { hasUpdate = true; let arr = areas.get(area); if (!arr) { arr = []; areas.set(area, arr); } botCount += 1; arr.push({ bot, title, tags, }); } if (checkTagCounter()) { // Wait for the sleep interval so that other processes // can run before resuming the search. await sleep(SEARCH_UPDATE_WAIT_INTERVAL); } } const { update, item } = createUpdate(); observer.next(update); completeItems.push(item); } if (!hadUpdate) { observer.next({ numMatches: 0, numBots: 0, items: [], }); } }; return new Observable((observer) => { let sub = new Subscription(); runSearch(observer, sub); return sub; }); } _calculateSystemPortalPaneUpdated() { const simulationChanges = merge(this._simulationManager.simulationAdded.pipe(map((sim) => ['added', sim])), this._simulationManager.simulationRemoved.pipe(map((sim) => ['removed', sim]))); const allSimulations = simulationChanges.pipe(scan((array, change, index) => { if (change[0] === 'added') { array.push(change[1]); } else { const index = array.indexOf(change[1]); if (index >= 0) { array.splice(index, 1); } } return array; }, [])); const panes = allSimulations.pipe(switchMap((simulations) => { return combineLatest(simulations.map((sim) => { return sim.watcher .botTagsChanged(sim.helper.userId) .pipe(map((change) => getOpenSystemPortalPane(null, change.bot)), takeUntil(this._simulationManager.simulationRemoved.pipe(filter((s) => s.id === sim.id)))); })); }), map((panes) => panes.find((pane) => hasValue(pane)) ?? null)); return panes.pipe(distinctUntilChanged()); } } /** * Finds the "area" for the given system identifier. * System identifiers are dot-separated (.) strings. (like "core.ui.menu") * The area includes the first two sections sections of a system ID except for the last section. * (the area of "core.ui.menu" is "core.menu" and the area of "core.ui" is "core") * @param system * @returns */ export function getSystemArea(system) { if (!hasValue(system)) { return ''; } const firstDotIndex = system.indexOf('.'); if (firstDotIndex < 0) { return system; } const secondDotIndex = system.indexOf('.', firstDotIndex + 1); if (secondDotIndex < 0) { return system.substring(0, firstDotIndex); } return system.substring(0, secondDotIndex); } /** * Finds the title for the bot given the system identifier and area. * @param system The system identifier. * @param area The area for the system. */ export function getBotTitle(system, area) { return (system ?? '').substring(area.length).replace(/^[.]/, ''); } /** * Searches the given tag for matches to the given query. * @param tag The name of the tag that is being searched. * @param space The space that the tag is in. * @param value The value of the tag. * @param query The value to search for. */ export function searchTag(tag, space, value, query, prefixes) { let str = formatValue(value); const tagNameMatches = searchValue(tag, 0, query, true); let matches = []; let prefix; let isValueScript = false; let isValueModule = false; let isValueFormula = false; let isValueLink = false; if (hasValue(str)) { isValueScript = isScript(str); isValueModule = isModule(str); isValueFormula = isFormula(str); isValueLink = isBotLink(str); prefix = getScriptPrefix(prefixes, str); let parsedValue; let offset = 0; if (isValueScript) { parsedValue = parseScript(str); offset = 1; } else if (isValueModule) { parsedValue = parseModule(str); offset = '📄'.length; } else if (isValueFormula) { parsedValue = parseFormula(str); offset = DNA_TAG_PREFIX.length; } else if (isValueLink) { parsedValue = str.substring(BOT_LINK_TAG_PREFIX.length); offset = BOT_LINK_TAG_PREFIX.length; } else { parsedValue = str; } matches = searchValue(parsedValue, offset, query); } if (matches.length > 0 || tagNameMatches.length > 0) { let result = { tag, matches: [...tagNameMatches, ...matches], }; if (hasValue(space)) { result.space = space; } if (hasValue(prefix)) { result.prefix = prefix; } if (isValueScript) { result.isScript = true; } else if (isValueFormula) { result.isFormula = true; } else if (isValueLink) { result.isLink = true; } return result; } return null; } /** * Searches the given value for matches of the given query. * @param value The value to search. * @param indexOffset The offset that should be added to absolute indexes in the matches. * @param query The value to search for. * @param isTagName Whether the match is for a tag name. * @returns */ export function searchValue(value, indexOffset, query, isTagName) { let results = []; let i = 0; while (i < value.length) { const match = value.indexOf(query, i); if (match >= 0) { i = match + query.length; let lineStart = match; let distance = 0; const maxSearchDistance = 40; for (; lineStart > 0 && distance <= maxSearchDistance; lineStart -= 1) { const char = value[lineStart]; if (char === '\n') { lineStart += 1; break; } else if (char !== ' ' && char !== '\t') { distance += 1; } } let lineEnd = match + query.length; for (; lineEnd < value.length && distance <= maxSearchDistance; lineEnd += 1) { const char = value[lineEnd]; if (char === '\n') { break; } else if (char !== ' ' && char !== '\t') { distance += 1; } } const line = value.substring(lineStart, lineEnd); let highlightStart = match - lineStart; let highlightEnd = highlightStart + query.length; results.push({ index: match + indexOffset, endIndex: match + query.length + indexOffset, text: line, highlightStartIndex: highlightStart, highlightEndIndex: highlightEnd, isTagName, }); } else { break; } } return results; } function findChangedTags(originalBot, newBot) { let changes = diffTags(originalBot?.tags ?? {}, newBot?.tags ?? {}, undefined); const allSpaces = union(Object.keys(originalBot?.masks ?? {}), Object.keys(newBot?.masks ?? {})); for (let space of allSpaces) { changes.push(...diffTags(originalBot?.masks?.[space] ?? {}, newBot?.masks?.[space] ?? {}, space)); } return changes; } function diffTags(firstTags, secondTags, space) { let changes = []; let hasTagsDiff = false; const allTags = union(Object.keys(firstTags), Object.keys(secondTags)); for (let tag of allTags) { const firstValue = firstTags[tag]; const secondValue = secondTags[tag]; if (!isEqual(firstValue, secondValue)) { const status = hasValue(secondValue) && !hasValue(firstValue) ? 'added' : hasValue(firstValue) && !hasValue(secondValue) ? 'removed' : 'changed'; // updated, deleted, or added hasTagsDiff = true; changes.push({ name: tag, space, status: status, }); } else { changes.push({ name: tag, space, status: 'none', }); } } return changes; } function sleep(ms) { return new Promise((resolve, reject) => setTimeout(resolve, ms)); } //# sourceMappingURL=SystemPortalCoordinator.js.map