@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
86 lines • 4.08 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 { FileChangeKind } from "../../types.js";
import { normalize } from "../../utils/key_prefix.js";
import { rstripSlash } from "../../utils/slash.js";
import { eventAt, field, textField } from "../../events/payload.js";
// The eventName families a record carries: a write (Put, Post, Copy,
// CompleteMultipartUpload) or a removal (Delete, DeleteMarkerCreated,
// lifecycle expiry).
const WRITTEN = 'ObjectCreated:';
const REMOVED = ['ObjectRemoved:', 'LifecycleExpiration:'];
/**
* Map one S3 event notification record onto mount paths.
*
* The consumer owns the transport (S3 Event Notifications delivered to an SQS
* queue, an SNS topic or EventBridge) and passes each record of a
* notification's `Records` with its `eventName`. Nothing here runs a loop. A
* record names the bucket and the object key, URL-encoded with `+` for a
* space, and says whether the object was written or removed.
*
* S3 does not say whether a write made the object or replaced it, so a key the
* hook has mapped before is an UPDATE and any other is a CREATE, as the Box
* hook splits an upload. A key ending in `/` is a directory marker: writing
* one makes the directory, and removing one leaves whatever is below it, so
* that directory is UNKNOWN. In a versioned bucket a `Delete` names the one
* version it removed, and an older one may now be the object, so the key is
* UNKNOWN; a delete marker hides the object, which is a DELETE. A record for
* another bucket, or for a key outside the mount's `keyPrefix`, maps to
* nothing.
*
* Mirrors Python `S3EventHook` (`core/s3/events.py`).
*/
export class S3EventHook {
accessor;
seen = new Set();
constructor(accessor) {
this.accessor = accessor;
}
toEvents(root, eventName, payload) {
return Promise.resolve(this.map(root, eventName, payload));
}
map(root, eventName, payload) {
const config = this.accessor.config;
const s3 = field(payload, 's3');
const raw = textField(field(s3, 'object'), 'key');
if (textField(field(s3, 'bucket'), 'name') !== config.bucket || !raw)
return [];
const key = decodeURIComponent(raw.replace(/\+/g, ' '));
const prefix = normalize(config.keyPrefix);
if (prefix !== '' && !key.startsWith(prefix))
return [];
const relative = key.slice(prefix.length);
if (relative.endsWith('/')) {
const place = rstripSlash(relative);
if (place === '')
return [];
const kind = eventName.startsWith(WRITTEN) ? FileChangeKind.CREATE : FileChangeKind.UNKNOWN;
return [eventAt(root, place, kind)];
}
if (REMOVED.some((family) => eventName.startsWith(family))) {
const version = textField(field(s3, 'object'), 'versionId');
if (eventName.endsWith(':Delete') && version !== null) {
return [eventAt(root, relative, FileChangeKind.UNKNOWN)];
}
this.seen.delete(relative);
return [eventAt(root, relative, FileChangeKind.DELETE)];
}
if (!eventName.startsWith(WRITTEN))
return [];
const kind = this.seen.has(relative) ? FileChangeKind.UPDATE : FileChangeKind.CREATE;
this.seen.add(relative);
return [eventAt(root, relative, kind)];
}
}
//# sourceMappingURL=events.js.map