UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

196 lines • 7.9 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { RedisAccessor } from "../../accessor/redis.js"; import { capacity as redisCapacity, count as redisCount, sizeOf as redisSizeOf, } from "../../core/redis/capacity.js"; import { copy as redisCopy } from "../../core/redis/copy.js"; import { create as redisCreate } from "../../core/redis/create.js"; import { exists as redisExists } from "../../core/redis/exists.js"; import { serverLocation } from "../../core/redis/location.js"; import { mkdir as redisMkdir } from "../../core/redis/mkdir.js"; import { read as redisRead } from "../../core/redis/read.js"; import { readdir as redisReaddir, tree as redisTree } from "../../core/redis/readdir.js"; import { rename as redisRename } from "../../core/redis/rename.js"; import { rmR as redisRmR } from "../../core/redis/rm.js"; import { rmdir as redisRmdir } from "../../core/redis/rmdir.js"; import { setAttrs as redisSetAttrs } from "../../core/redis/set_attrs.js"; import { stat as redisStat } from "../../core/redis/stat.js"; import { readStream as redisStream } from "../../core/redis/stream.js"; import { truncate as redisTruncate } from "../../core/redis/truncate.js"; import { unlink as redisUnlink } from "../../core/redis/unlink.js"; import { write as redisWrite } from "../../core/redis/write.js"; import { VFSName } from "../../types.js"; import { compareCodePoints } from "../../utils/sort.js"; import { BaseVFS } from "../base.js"; import { charge, storageUsage } from "../capacity.js"; import { REDACTED_SECRET } from "../secrets.js"; import {} from "../types.js"; /** * A redis keyspace mounted as a filesystem, over whatever store it is given. * * The runtime packages subclass this as `RedisVFS`, only to build the store: node opens a * RESP client from a redis URL, the browser speaks Upstash's REST shape over * fetch. Everything a mount does, ops table and commands included, lives here * once, because none of it depends on the transport. */ export class RedisVFS extends BaseVFS { name = VFSName.REDIS; cachesReads = false; // byte store: stat() sizes every file from metadata sizesAlwaysKnown = true; indexTtl = 0; store; accessor; usage; /** * `capacity` is the most bytes the mount's files may hold, a size like * `100MB` or a count of bytes; null for no limit. Counted at the first * write or `df`; a writer in another process is counted at the next `df`. * * @throws VFSConfigError when `capacity` is not a size. */ constructor(store, capacity = null) { super(); this.store = store; this.accessor = new RedisAccessor(store); this.usage = storageUsage(this.name, capacity); } charge(path, write) { return charge(this.usage, path, (p) => redisSizeOf(this.accessor, p), () => redisCount(this.accessor), write); } get url() { return this.store.url; } get keyPrefix() { return this.store.keyPrefix; } // Keys are {prefix}file:{path}, so only an identical prefix shares them: // it is escaped whole so a "/" in it never meets the path. storageLocation() { return `${this.name}:${serverLocation(this.url)}:${encodeURIComponent(this.keyPrefix)}`; } async close() { await this.store.close(); await super.close(); } readsRanges = true; local = true; readTree(path) { return redisTree(this.accessor, path); } readdir(path) { return redisReaddir(this.accessor, path); } read(path, _handed, offset = 0, size = null) { if (offset === 0 && size === null) return redisStream(this.accessor, path); return redisRead(this.accessor, path, size === null ? { offset } : { offset, size }); } stat(path) { return redisStat(this.accessor, path); } exists(path) { return redisExists(this.accessor, path); } write(path, data, _signal, _handed, atomic = false) { return this.charge(path, (held) => redisWrite(this.accessor, path, held.stream(data), false, atomic)); } append(path, data) { return this.charge(path, (held) => redisWrite(this.accessor, path, held.stream(data, true), true)); } create(path) { return this.charge(path, () => redisCreate(this.accessor, path)); } mkdir(path, parents = false) { return redisMkdir(this.accessor, path, parents); } unlink(path) { return this.charge(path, () => redisUnlink(this.accessor, path)); } rmdir(path) { return redisRmdir(this.accessor, path); } rmR(path) { return this.charge(path, () => redisRmR(this.accessor, path)); } rename(src, dst) { return this.charge(src, () => this.charge(dst, () => redisRename(this.accessor, src, dst))); } copy(src, dst) { return this.charge(dst, async (held) => { if (this.usage === null) return redisCopy(this.accessor, src, dst); held.growTo(await redisSizeOf(this.accessor, src)); await redisCopy(this.accessor, src, dst); const copied = await redisSizeOf(this.accessor, dst); try { held.growTo(copied); } catch (err) { await redisTruncate(this.accessor, dst, held.reserved, true); throw err; } }); } truncate(path, length, noCreate = false) { return this.charge(path, async (held) => { if (!noCreate || (await this.exists(path))) held.growTo(length); await redisTruncate(this.accessor, path, length, noCreate); }); } /** The mount's own quota when capped, counted afresh so a writer in * another process shows; else the server's memory. Mirrors Python's. */ async capacity() { if (this.usage === null) return redisCapacity(this.accessor); this.usage.stale(); await this.usage.counted(() => redisCount(this.accessor)); return this.usage.report(); } setattr(path, fields) { return redisSetAttrs(this.accessor, path, fields); } async getState() { const files = {}; for (const key of await this.store.listFiles()) { const data = await this.store.getFile(key); if (data !== null) files[key] = data; } const dirs = [...(await this.store.listDirs())].sort(compareCodePoints); return { type: this.name, config: { url: REDACTED_SECRET, keyPrefix: this.keyPrefix, ...(this.usage !== null ? { capacity: this.usage.limit } : {}), }, keyPrefix: this.keyPrefix, files, dirs, attrs: await this.store.listAttrs(), modified: await this.store.listModified(), }; } async loadState(state) { await this.store.restore({ files: state.files ?? {}, dirs: state.dirs ?? ['/'], attrs: state.attrs ?? {}, modified: state.modified ?? {}, }); this.usage?.stale(); } } //# sourceMappingURL=redis.js.map