@beignet/core
Version:
Core framework primitives for Beignet
243 lines • 7.55 kB
JavaScript
function hasControlCharacter(value) {
for (const char of value) {
const code = char.charCodeAt(0);
if (code <= 31 || code === 127)
return true;
}
return false;
}
/**
* Assert that a storage key follows Beignet's provider-neutral key rules.
*
* Valid keys are non-empty relative object paths. They do not contain control
* characters, backslashes, empty path segments, or `.` / `..` segments.
* Providers may enforce additional adapter-specific restrictions after this
* shared assertion.
*/
export function assertValidStorageKey(key) {
if (key.length === 0) {
throw new Error("Storage key must not be empty.");
}
if (hasControlCharacter(key)) {
throw new Error("Storage key must not include control characters.");
}
if (key.startsWith("/")) {
throw new Error("Storage key must not start with '/'.");
}
if (key.endsWith("/")) {
throw new Error("Storage key must not end with '/'.");
}
if (key.includes("\\")) {
throw new Error("Storage key must use '/' separators, not '\\'.");
}
const segments = key.split("/");
if (segments.some((segment) => segment === "")) {
throw new Error("Storage key must not include empty path segments.");
}
if (segments.some((segment) => segment === "." || segment === "..")) {
throw new Error("Storage key must not include '.' or '..' segments.");
}
}
/**
* Normalize and validate an optional storage key prefix.
*
* Empty and slash-only prefixes normalize to an empty string.
*/
export function normalizeStorageKeyPrefix(prefix) {
if (!prefix)
return "";
const normalized = prefix.replace(/^\/+|\/+$/g, "");
if (!normalized)
return "";
assertValidStorageKey(normalized);
return normalized;
}
/**
* Prefix a storage key with an optional app or environment namespace.
*/
export function prefixStorageKey({ keyPrefix, key, }) {
const normalizedPrefix = normalizeStorageKeyPrefix(keyPrefix);
assertValidStorageKey(key);
return normalizedPrefix ? `${normalizedPrefix}/${key}` : key;
}
/**
* Format an encoded public URL for a validated storage key.
*/
export function createStoragePublicUrl({ publicBaseUrl, key, }) {
assertValidStorageKey(key);
const base = publicBaseUrl.replace(/\/+$/, "");
const encodedKey = key
.split("/")
.map((part) => encodeURIComponent(part))
.join("/");
return `${base}/${encodedKey}`;
}
function copyBytes(bytes) {
return new Uint8Array(bytes);
}
function bytesToArrayBuffer(bytes) {
const buffer = new ArrayBuffer(bytes.byteLength);
new Uint8Array(buffer).set(bytes);
return buffer;
}
function bytesToStream(bytes) {
const copy = copyBytes(bytes);
return new ReadableStream({
start(controller) {
controller.enqueue(copy);
controller.close();
},
});
}
async function streamToBytes(stream) {
const reader = stream.getReader();
const chunks = [];
let size = 0;
try {
while (true) {
const result = await reader.read();
if (result.done)
break;
chunks.push(result.value);
size += result.value.byteLength;
}
}
finally {
reader.releaseLock();
}
const bytes = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes;
}
async function storageBodyToBytes(body) {
if (typeof body === "string") {
return new TextEncoder().encode(body);
}
if (body instanceof Uint8Array) {
return copyBytes(body);
}
if (body instanceof ArrayBuffer) {
return new Uint8Array(body.slice(0));
}
if (body instanceof Blob) {
return new Uint8Array(await body.arrayBuffer());
}
return streamToBytes(body);
}
function cloneObject(entry) {
return {
key: entry.key,
size: entry.size,
...(entry.contentType !== undefined
? { contentType: entry.contentType }
: {}),
...(entry.cacheControl !== undefined
? { cacheControl: entry.cacheControl }
: {}),
metadata: { ...entry.metadata },
visibility: entry.visibility,
lastModified: new Date(entry.lastModified),
};
}
function createObjectBody(entry) {
const object = cloneObject(entry);
let bodyUsed = false;
function consumeBytes() {
if (bodyUsed) {
throw new Error("Storage object body has already been consumed.");
}
bodyUsed = true;
return copyBytes(entry.bytes);
}
return {
...object,
get bodyUsed() {
return bodyUsed;
},
stream() {
return bytesToStream(consumeBytes());
},
async bytes() {
return consumeBytes();
},
async arrayBuffer() {
return bytesToArrayBuffer(consumeBytes());
},
async text() {
return new TextDecoder().decode(consumeBytes());
},
};
}
/**
* Create an in-memory object storage adapter for tests, examples, and
* single-process development.
*
* This adapter validates object keys using Beignet's storage key rules. It is
* not durable and does not share objects across processes.
*
* @param options - Optional public URL base for public objects.
* @returns A storage port backed by a local `Map`.
*/
export function createMemoryStorage(options = {}) {
const objects = new Map();
return {
async put(key, body, putOptions) {
assertValidStorageKey(key);
const bytes = await storageBodyToBytes(body);
const entry = {
key,
size: bytes.byteLength,
...(putOptions?.contentType !== undefined
? { contentType: putOptions.contentType }
: {}),
...(putOptions?.cacheControl !== undefined
? { cacheControl: putOptions.cacheControl }
: {}),
metadata: { ...(putOptions?.metadata ?? {}) },
visibility: putOptions?.visibility ?? "private",
lastModified: new Date(),
bytes,
};
objects.set(key, entry);
return cloneObject(entry);
},
async get(key) {
assertValidStorageKey(key);
const entry = objects.get(key);
if (!entry)
return null;
return createObjectBody(entry);
},
async stat(key) {
assertValidStorageKey(key);
const entry = objects.get(key);
if (!entry)
return null;
return cloneObject(entry);
},
async delete(key) {
assertValidStorageKey(key);
return objects.delete(key);
},
async exists(key) {
assertValidStorageKey(key);
return objects.has(key);
},
async publicUrl(key) {
assertValidStorageKey(key);
const entry = objects.get(key);
if (entry?.visibility !== "public" || !options.publicBaseUrl) {
return null;
}
return createStoragePublicUrl({
publicBaseUrl: options.publicBaseUrl,
key,
});
},
};
}
//# sourceMappingURL=storage.js.map