@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
JavaScript
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