@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
179 lines • 6.92 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 { z } from 'zod';
import { YieldBudget } from "../io/yield_budget.js";
import { MovableStream, openStreamSync } from "./stream.js";
const position = z.number().int().nonnegative().nullable();
const units = z.array(z.number().int().min(0).max(0x10ffff));
const spans = z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]));
const finalizer = new FinalizationRegistry((stream) => {
stream.close();
});
const decoder = new TextDecoder();
const CHUNK = 4096;
const CACHE_SIZE = 256;
const scratch = new Uint8Array(4 * CHUNK);
// The code points as little-endian u32 units. A `transient` result may be the
// shared scratch, good until the next transient call: for a caller that hands
// it to Wasm before it awaits.
function encoded(text, transient = false) {
const bytes = transient && text.length <= CHUNK ? scratch : new Uint8Array(4 * text.length);
const view = new DataView(bytes.buffer);
let size = 0;
for (let at = 0; at < text.length; size += 4) {
const unit = text.codePointAt(at) ?? 0;
view.setUint32(size, unit, true);
at += unit > 0xffff ? 2 : 1;
}
return bytes.subarray(0, size);
}
// The text encoded a piece of about CHUNK characters at a time, never splitting
// a surrogate pair, for a caller that yields between pieces.
function* pieces(text) {
for (let at = 0; at < text.length;) {
let end = Math.min(at + CHUNK, text.length);
if ((text.codePointAt(end - 1) ?? 0) > 0xffff)
end += 1;
yield encoded(text.slice(at, end));
at = end;
}
}
function config(pattern, extglob, period, dialect) {
return {
pattern: Array.from(pattern, (ch) => ch.codePointAt(0) ?? 0),
extglob,
period,
dialect,
operation: { kind: 'matches' },
};
}
function reply(stream) {
let out = decoder.decode(stream.finish());
while (stream.busy())
out += decoder.decode(stream.drain());
return JSON.parse(out);
}
function text(result) {
return result.map((unit) => String.fromCodePoint(unit)).join('');
}
/** One compiled pattern. Offsets count supplied characters, including lone surrogates. */
export class Glob {
stream;
subject;
budget;
constructor(pattern, extglob = false, period = false, dialect = 'shell', signal) {
this.budget = new YieldBudget(signal);
this.stream = openStreamSync('glob', config(pattern, extglob, period, dialect));
finalizer.register(this, this.stream, this);
}
close() {
finalizer.unregister(this);
this.stream.close();
this.subject = undefined;
}
prepare(name, operation) {
const reuse = this.subject === name;
this.stream.begin({ ...operation, reuse_subject: reuse });
this.subject = name;
return reuse;
}
run(name, operation) {
if (!this.prepare(name, operation))
this.stream.feed(encoded(name, true));
return reply(this.stream);
}
async runAsync(name, operation) {
const budget = this.budget;
if (!this.prepare(name, operation)) {
for (const part of pieces(name)) {
const pending = budget.run();
if (pending !== undefined)
await pending;
this.stream.feed(part);
}
}
let out = decoder.decode(this.stream.finish());
while (this.stream.busy()) {
const pending = budget.run();
if (pending !== undefined)
await pending;
out += decoder.decode(this.stream.drain());
}
return JSON.parse(out);
}
async matchesAsync(name) {
return z.boolean().parse(await this.runAsync(name, { kind: 'matches' }));
}
end(name, start, longest = true) {
return position.parse(this.run(name, { kind: 'end', start, longest }));
}
suffix(name, longest = true) {
return position.parse(this.run(name, { kind: 'suffix', longest }));
}
async extent(name, prefix, longest) {
return position.parse(await this.runAsync(name, prefix ? { kind: 'end', start: 0, longest } : { kind: 'suffix', longest }));
}
async spans(name, all, anchor) {
return spans.parse(await this.runAsync(name, { kind: 'replace', all, anchor }));
}
shape() {
return text(units.parse(this.run('', { kind: 'shape' })));
}
parts() {
return z
.array(units)
.parse(this.run('', { kind: 'parts' }))
.map(text);
}
}
/**
* One compiled pattern asked whether whole names match, one Wasm call per name,
* or none where the grammar reads the pattern as plain text and the name is
* compared with it here. Mirrors Python's GlobMatcher.
*/
export class GlobMatcher {
stream;
literal;
constructor(pattern, extglob = false, period = false, dialect = 'shell') {
this.stream = new MovableStream('glob', config(pattern, extglob, period, dialect));
const stream = this.stream.live;
stream.begin({ kind: 'literal' });
const literal = units.nullable().parse(reply(stream));
this.literal = literal === null ? null : text(literal);
}
matches(name) {
if (this.literal !== null)
return name === this.literal;
return this.stream.live.test(encoded(name, true));
}
}
const matchers = new Map();
/**
* A compiled GlobMatcher, shared by every caller asking the same; the pattern
* compiled first leaves first. A hit is one lookup, since a filter asks once
* per name. Mirrors Python's glob_matcher.
*/
export function globMatcher(pattern, extglob = false, period = false, dialect = 'shell') {
let kinds = matchers.get(pattern);
if (kinds === undefined) {
const oldest = matchers.keys().next().value;
if (matchers.size >= CACHE_SIZE && oldest !== undefined)
matchers.delete(oldest);
kinds = [];
matchers.set(pattern, kinds);
}
const kind = (dialect === 'python' ? 4 : 0) + (extglob ? 2 : 0) + (period ? 1 : 0);
return (kinds[kind] ??= new GlobMatcher(pattern, extglob, period, dialect));
}
//# sourceMappingURL=glob.js.map