@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
105 lines • 4.46 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 { underPath } from "../../utils/key_prefix.js";
/**
* The invalidations a cache writer checks before it installs.
*
* A writer holds bytes it read earlier; an invalidation that lands between
* the read and the install makes them stale even though the writer was
* granted its turn after it. The writer takes a stamp before it waits and
* compares it before it installs.
*
* The RAM store holds a per-key lock only for its in-memory install,
* never across hashing or backend I/O. A free lock admits its writer
* without suspending on both hosts; a queued writer checks its stamp
* after acquiring the lock. Redis holds no client-side key lock: it
* sends atomic writes to the server and checks after acquiring its
* client (an await on TypeScript, synchronous on Python). The guard
* tracks local invalidations, not writes by other Redis clients.
*
* Two counters, because invalidations have different reaches. The
* store-wide epoch answers `clear`. The per-key counter answers a removal
* of one key and a prefix eviction, so a large fill is not thrown away
* because an unrelated key or folder was dropped. A prefix eviction can
* name its victims although a fill in flight has no entry yet: every
* writer registers here in `enter` before it waits. Per-key counters
* exist only while a writer for that key is in flight, which bounds the
* map by concurrent writers, not by every key ever removed.
*/
export class Invalidation {
epoch = 0;
keys = new Map();
writers = new Map();
/** Register a writer for `key` and take its stamp. */
enter(key) {
this.writers.set(key, (this.writers.get(key) ?? 0) + 1);
return [this.epoch, this.keys.get(key) ?? 0];
}
/** Unregister a writer for `key`; the last one out drops the key's counter. */
leave(key) {
const left = (this.writers.get(key) ?? 1) - 1;
if (left > 0) {
this.writers.set(key, left);
}
else {
this.writers.delete(key);
this.keys.delete(key);
}
}
/** Whether an invalidation reached `key` since `stamp`. */
stale(key, stamp) {
return stamp[0] !== this.epoch || stamp[1] !== (this.keys.get(key) ?? 0);
}
/** Record a removal of `key` for the writers in flight on it. */
invalidate(key) {
if (this.writers.has(key))
this.keys.set(key, (this.keys.get(key) ?? 0) + 1);
}
/**
* Record a prefix eviction for the writers in flight under it, matched
* as the stores match it (`startsWith`), sparing the excluded roots.
*/
invalidatePrefix(prefix, excluded = []) {
for (const key of this.writers.keys()) {
if (key.startsWith(prefix) && !excluded.some((root) => underPath(key, root)))
this.keys.set(key, (this.keys.get(key) ?? 0) + 1);
}
}
/** Record an invalidation whose victims cannot be enumerated. */
invalidateAll() {
this.epoch++;
}
}
/**
* Track a fill's keys until it completes, fails or is cancelled.
*
* The callback starts without a yield, before any lock/client wait. Call
* its predicate immediately before each install or retry. Unknown keys
* are not live, and duplicate keys count as one writer.
*/
export async function withFillGuard(invalidation, keys, fill) {
const stamps = new Map([...new Set(keys)].map((key) => [key, invalidation.enter(key)]));
const isLive = (key) => {
const stamp = stamps.get(key);
return stamp !== undefined && !invalidation.stale(key, stamp);
};
try {
await fill(isLive);
}
finally {
for (const key of stamps.keys())
invalidation.leave(key);
}
}
//# sourceMappingURL=invalidation.js.map