@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
196 lines • 7.9 kB
JavaScript
// ========= 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