UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

179 lines • 6.92 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 { 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