UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

138 lines • 5.6 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 { createS3Client, isConditionLost, isNotFound, loadS3Module, streamToBuffer, } from "../../core/s3/client.js"; import { generationOf } from "./types.js"; const decoder = new TextDecoder(); /** * One JSON-record-per-object client with ETag-anchored CAS. * * The generic building block both S3 stores share (mirroring how the * Redis stores share one cas.lua): a record is one object at * `{prefix}{name}.json`, and a conditional write anchors on the exact * ETag the compare-read returned, so the generation check and the * write hit the same stored version. Immutable-blob planes never need * this; it exists only for the few mutable control-plane cells. * Mirrors the Python S3RecordClient. */ export class S3RecordClient { config; prefix; clientPromise = null; constructor(config, prefix) { this.config = config; this.prefix = prefix; } client() { this.clientPromise ??= createS3Client(this.config); return this.clientPromise; } key(name) { return `${this.prefix}${name}.json`; } /** Read one record; `[fields, etag]`, `[null, '']` when absent. */ async get(name) { const [client, mod] = await Promise.all([this.client(), loadS3Module(this.config)]); let response; try { response = await client.send(new mod.GetObjectCommand({ Bucket: this.config.bucket, Key: this.key(name) })); } catch (err) { if (isNotFound(err)) return [null, '']; throw err; } const body = await streamToBuffer(response.Body); const etag = typeof response.ETag === 'string' ? response.ETag : ''; return [JSON.parse(decoder.decode(body)), etag]; } async put(name, fields) { const [client, mod] = await Promise.all([this.client(), loadS3Module(this.config)]); await client.send(new mod.PutObjectCommand({ Bucket: this.config.bucket, Key: this.key(name), Body: JSON.stringify(fields), })); } /** * Write one record iff its stored generation matches. * * Compare-read the record, check the generation client-side, then * make the write conditional on the exact version read: If-None-Match * for create (expected 0, nothing stored), If-Match on the read ETag * otherwise. A 412/409 means another writer moved the record between * our read and write; the caller adopts and retries. */ async casPut(name, fields, expectedGeneration) { const [stored, etag] = await this.get(name); if (generationOf(stored) !== expectedGeneration) return false; const [client, mod] = await Promise.all([this.client(), loadS3Module(this.config)]); const condition = stored === null ? { IfNoneMatch: '*' } : { IfMatch: etag }; try { await client.send(new mod.PutObjectCommand({ Bucket: this.config.bucket, Key: this.key(name), Body: JSON.stringify(fields), ...condition, })); } catch (err) { if (isConditionLost(err)) return false; throw err; } return true; } async listNames() { const [client, mod] = await Promise.all([this.client(), loadS3Module(this.config)]); const names = []; let continuationToken; do { const response = await client.send(new mod.ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: this.prefix, ...(continuationToken !== undefined ? { ContinuationToken: continuationToken } : {}), })); const contents = (response.Contents ?? []); for (const entry of contents) { const key = entry.Key.slice(this.prefix.length); if (key.endsWith('.json')) names.push(key.slice(0, -'.json'.length)); } continuationToken = response.IsTruncated === true ? response.NextContinuationToken : undefined; } while (continuationToken !== undefined); return names; } async delete(names) { if (names.length === 0) return; const [client, mod] = await Promise.all([this.client(), loadS3Module(this.config)]); await client.send(new mod.DeleteObjectsCommand({ Bucket: this.config.bucket, Delete: { Objects: names.map((name) => ({ Key: this.key(name) })) }, })); } async clear() { await this.delete(await this.listNames()); } async close() { if (this.clientPromise !== null) { const client = await this.clientPromise; client.destroy?.(); this.clientPromise = null; } } } //# sourceMappingURL=s3.js.map