@casual-simulation/aux-common
Version:
Common library for AUX projects
304 lines • 10 kB
JavaScript
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences.
*
* Copyright (c) 2019-2025 Casual Simulation, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { fromByteArray, toByteArray } from 'base64-js';
import { applyUpdate, mergeUpdates } from 'yjs';
import { botAdded, createBot, formatBotDate, formatBotRotation, formatBotVector, hasValue, isBot, isRuntimeBot, ORIGINAL_OBJECT, } from '../bots';
import { YjsPartitionImpl } from './YjsPartition';
import { DateTime } from 'luxon';
import { Rotation, Vector2, Vector3 } from '../math';
import { forOwn } from 'es-toolkit/compat';
import '../BlobPolyfill';
/**
* Creates a new initialization update using the given action.
*/
export function constructInitializationUpdate(action) {
const partition = new YjsPartitionImpl({
type: 'yjs',
});
let instUpdate;
partition.doc.on('update', (update) => {
instUpdate = {
id: 0,
timestamp: Date.now(),
update: fromByteArray(update),
};
});
partition.applyEvents(action.bots.map((b) => botAdded(createBot(b.id, b.tags))));
return instUpdate;
}
/**
* Gets the bots state that is encoded from the given action.
* @param action The action.
*/
export function getStateFromUpdates(action) {
let partition = new YjsPartitionImpl({
type: 'yjs',
});
for (let { update } of action.updates) {
const updateBytes = toByteArray(update);
applyUpdate(partition.doc, updateBytes);
}
return partition.state;
}
/**
* Merges the given inst updates into a single update.
* @param updates The list of updates to merge.
*/
export function mergeInstUpdates(updates, id = updates.length, timestamp = Date.now()) {
const update = mergeUpdates(updates.map((u) => toByteArray(u.update)));
return {
id,
timestamp,
update: fromByteArray(update),
};
}
/**
* Converts the given error to a copiable value.
* Returns a new value that can be sent over to web workers.
* @param err The error to convert.
*/
export function convertErrorToCopiableValue(err) {
if (err instanceof Error) {
let obj = {
message: err.message,
name: err.name,
stack: err.stack,
};
if (err.response) {
let response = err.response;
obj.response = {
data: response.data,
headers: response.headers,
status: response.status,
statusText: response.statusText,
};
}
return obj;
}
else {
return err;
}
}
/**
* Converts the given value to a copiable value.
* Copiable values are strings, numbers, booleans, arrays, and objects made of any of those types.
* Non-copiable values are functions and errors.
* @param value
*/
export function convertToCopiableValue(value) {
try {
return _convertToCopiableValue(value, 0, new Map());
}
catch (err) {
if (err instanceof DeepObjectError) {
return '[Nested object]';
}
throw err;
}
}
function _convertToCopiableValue(value, depth, map) {
if (depth > 1000) {
throw new DeepObjectError();
}
if (typeof value === 'function') {
return `[Function ${value.name}]`;
}
else if (value instanceof Error) {
return `${value.name}: ${value.message}`;
}
else if (typeof value === 'object') {
if (map.has(value)) {
return map.get(value);
}
if (isRuntimeBot(value)) {
const result = {
id: value.id,
tags: value.tags.toJSON(),
};
if (hasValue(value.space)) {
result.space = value.space;
}
map.set(value, result);
return result;
}
else if (isBot(value)) {
const result = {
id: value.id,
tags: value.tags,
};
map.set(value, result);
return result;
}
else if (Array.isArray(value)) {
const result = [];
map.set(value, result);
result.push(...value.map((val) => _convertToCopiableValue(val, depth + 1, map)));
return result;
}
else if (value === null || value === undefined) {
return value;
}
else if (value instanceof Date) {
return value;
}
else if (value instanceof Blob) {
return value;
}
else if (value instanceof ArrayBuffer) {
return value;
}
else if (ArrayBuffer.isView(value)) {
return value;
}
else if (value instanceof DateTime) {
return formatBotDate(value);
}
else if (value instanceof Vector2 || value instanceof Vector3) {
return formatBotVector(value);
}
else if (value instanceof Rotation) {
return formatBotRotation(value);
}
else if (ORIGINAL_OBJECT in value) {
const val = value[ORIGINAL_OBJECT];
map.set(value, val);
return val;
}
else {
let result = {};
map.set(value, result);
forOwn(value, (val, key, object) => {
result[key] = _convertToCopiableValue(val, depth + 1, map);
});
return result;
}
}
return value;
}
/**
* Deeply traverses over all the keys in the given value and calls the given action for each stored value.
* To prevent infinite loops, the traversal will stop if the depth exceeds 1000.
*
* @param value The value to traverse.
* @param action The action to perform for each value. If the action returns false, then the traversal will stop.
* @param depth The depth of the traversal.
*/
function deepTraverse(value, action, depth = 0) {
if (depth > 1000) {
throw new DeepObjectError();
}
if (hasValue(value) && typeof value === 'object') {
if (Array.isArray(value)) {
for (let val of value) {
if (action(val) === false) {
return;
}
deepTraverse(val, action, depth + 1);
}
}
else {
for (let key in value) {
const val = value[key];
if (action(val) === false) {
return;
}
deepTraverse(val, action, depth + 1);
}
}
}
}
export class DeepObjectError extends Error {
constructor() {
super('Object too deeply nested.');
}
}
/**
* Checks each of the tags in the given bot to ensure that the bot is copiable and returns a new bot with serializable values if the given bot is contains non-serializable values.
* @param bot The bot to check.
*/
export function ensureBotIsSerializable(bot) {
let newBot;
for (let tag in bot.tags) {
const value = bot.tags[tag];
if (value instanceof DateTime ||
value instanceof Vector2 ||
value instanceof Vector3 ||
value instanceof Rotation) {
updateTag(tag, convertToCopiableValue(value));
}
else if (hasValue(value) && typeof value === 'object') {
if (ORIGINAL_OBJECT in value) {
updateTag(tag, value[ORIGINAL_OBJECT]);
}
else {
deepTraverse(value, (val) => {
if (hasValue(val) &&
typeof val === 'object' &&
ORIGINAL_OBJECT in val) {
updateTag(tag, convertToCopiableValue(value));
return false;
}
});
}
}
}
return newBot !== null && newBot !== void 0 ? newBot : bot;
function updateTag(tag, value) {
if (!newBot) {
newBot = createBot(bot.id, { ...bot.tags }, bot.space);
}
newBot.tags[tag] = value;
}
}
/**
* Checks that the given tag is copiable and returns the copiable version of the value if it is not.
* @param value The value to check.
*/
export function ensureTagIsSerializable(value) {
if (value instanceof DateTime ||
value instanceof Vector2 ||
value instanceof Vector3 ||
value instanceof Rotation) {
return convertToCopiableValue(value);
}
else if (hasValue(value) &&
typeof value === 'object' &&
ORIGINAL_OBJECT in value) {
return value[ORIGINAL_OBJECT];
}
return value;
}
/**
* Determines if the given config supports the given remote action.
* @param config The config. If this is true, then it will be treated as if every remote event is supported.
* @param action The action to test.
*/
export function supportsRemoteEvent(config, action) {
var _a, _b;
if (config === true) {
return true;
}
else if (config === false) {
return false;
}
else if (action.type === 'remote' && action.event.type in config) {
return (_a = config[action.event.type]) !== null && _a !== void 0 ? _a : false;
}
return (_b = config.remoteActions) !== null && _b !== void 0 ? _b : false;
}
//# sourceMappingURL=PartitionUtils.js.map