const CURRENT_STORAGE_VERSION = 1; const VERCEL_KV_PREFIX = `v${CURRENT_STORAGE_VERSION}`;
const NETLIFY_STORE_NAME = `subconverter-data-v${CURRENT_STORAGE_VERSION}`;
let localStorageMap = new Map(); let kv; let isNetlifyBlobs = false;
let envCache = new Map();
function isNetlifyEnvironment() {
return typeof process !== 'undefined' &&
process.env.NETLIFY === 'true' ||
process.env.NETLIFY_BLOBS_CONTEXT != undefined ||
(process.cwd && process.cwd() === '/var/task');
}
function getenv(name, defaultValue = "") {
if (envCache.has(name)) {
return envCache.get(name);
}
let value = defaultValue;
try {
if (typeof process !== 'undefined' && process.env) {
if (name in process.env) {
value = process.env[name];
}
}
else if (typeof window !== 'undefined') {
if (window.__ENV__ && name in window.__ENV__) {
value = window.__ENV__[name];
}
}
} catch (error) {
console.warn(`Error reading environment variable ${name}:`, error);
}
envCache.set(name, value);
return value;
}
async function getKv() {
if (!kv) {
try {
if (typeof process !== 'undefined' &&
process.env.KV_REST_API_URL &&
process.env.KV_REST_API_TOKEN) {
const vercelKv = require('@vercel/kv');
const baseKv = vercelKv.kv; console.log("Using Vercel KV for storage (version prefix: ", VERCEL_KV_PREFIX, ")");
kv = {
_baseKv: baseKv,
get: (key) => baseKv.get(`${VERCEL_KV_PREFIX}/${key}`),
set: (key, value) => baseKv.set(`${VERCEL_KV_PREFIX}/${key}`, value),
exists: (key) => baseKv.exists(`${VERCEL_KV_PREFIX}/${key}`),
del: (key) => baseKv.del(`${VERCEL_KV_PREFIX}/${key}`),
scan: async (cursor, options = {}) => {
const { match = "*", count = 10 } = options;
const prefixedMatch = `${VERCEL_KV_PREFIX}/${match}`;
const [nextCursor, keys] = await baseKv.scan(cursor, { match: prefixedMatch, count });
const unprefixedKeys = keys.map(k => k.startsWith(VERCEL_KV_PREFIX + '/') ? k.substring(VERCEL_KV_PREFIX.length + 1) : k);
return [nextCursor, unprefixedKeys];
}
};
}
else if (isNetlifyEnvironment()) {
try {
const { getStore } = require('@netlify/blobs');
const store = getStore(NETLIFY_STORE_NAME); isNetlifyBlobs = true;
console.log("Using Netlify Blobs for storage (store: ", NETLIFY_STORE_NAME, ")");
kv = {
get: async (key) => {
try {
const value = await store.get(key, { type: 'arrayBuffer' });
return value ? new Uint8Array(value) : null;
} catch (error) {
if (error.message.includes('not found')) {
return null;
}
throw error;
}
},
set: async (key, value) => {
await store.set(key, value);
return "OK";
},
exists: async (key) => {
try {
const metadata = await store.getMetadata(key);
return metadata ? 1 : 0;
} catch (error) {
return 0;
}
},
scan: async (cursor, options = {}) => {
const { match = "*", count = 10 } = options;
const prefix = match.endsWith('*') ? match.slice(0, -1) : '';
const list = await store.list({ prefix });
let keys = list.blobs.map(blob => blob.key);
if (match !== "*" && !match.endsWith('*')) {
const pattern = match.replace(/\*/g, ".*");
const regex = new RegExp(`^${pattern}$`);
keys = keys.filter(key => regex.test(key));
}
return ['0', keys.slice(0, count)];
},
_store: store,
del: async (key) => {
try {
await store.delete(key);
return 1;
} catch (error) {
console.error(`Error deleting key ${key}:`, error);
return 0;
}
}
};
} catch (error) {
console.warn("Error initializing Netlify Blobs:", error);
throw error; }
} else {
console.log("No KV storage environment detected, using in-memory fallback (unversioned)");
kv = {
get: async (key) => localStorageMap.get(key) || null,
set: async (key, value) => { localStorageMap.set(key, value); return "OK"; },
exists: async (key) => localStorageMap.has(key) ? 1 : 0,
scan: async (cursor, options = {}) => {
const { match = "*", count = 10 } = options;
const pattern = match.replace(/\*/g, ".*");
const regex = new RegExp(`^${pattern}$`);
const allKeys = [...localStorageMap.keys()];
const matchingKeys = allKeys.filter(key => regex.test(key));
const startIndex = parseInt(cursor) || 0;
const endIndex = Math.min(startIndex + count, matchingKeys.length);
const keys = matchingKeys.slice(startIndex, endIndex);
const nextCursor = endIndex < matchingKeys.length ? String(endIndex) : '0';
return [nextCursor, keys];
},
del: async (key) => localStorageMap.delete(key) ? 1 : 0
};
}
} catch (error) {
console.warn("Error initializing storage, using in-memory fallback (unversioned):", error);
kv = {
get: async (key) => localStorageMap.get(key) || null,
set: async (key, value) => { localStorageMap.set(key, value); return "OK"; },
exists: async (key) => localStorageMap.has(key) ? 1 : 0,
scan: async (cursor, options = {}) => {
const { match = "*", count = 10 } = options;
const pattern = match.replace(/\*/g, ".*");
const regex = new RegExp(`^${pattern}$`);
const allKeys = [...localStorageMap.keys()];
const matchingKeys = allKeys.filter(key => regex.test(key));
const startIndex = parseInt(cursor) || 0;
const endIndex = Math.min(startIndex + count, matchingKeys.length);
const keys = matchingKeys.slice(startIndex, endIndex);
const nextCursor = endIndex < matchingKeys.length ? String(endIndex) : '0';
return [nextCursor, keys];
},
del: async (key) => localStorageMap.delete(key) ? 1 : 0
};
}
}
return kv;
}
async function kv_get(key) {
try {
const kvClient = await getKv();
const value = await kvClient.get(key);
if (value instanceof ArrayBuffer) {
return new Uint8Array(value);
} else if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
return value;
} else if (typeof value === 'string') {
return value;
}
return value === null ? undefined : value;
} catch (error) {
console.error(`KV get error for ${key}:`, error);
throw new Error(`Failed to get key ${key}: ${error.message}`);
}
}
async function kv_get_text(key) {
try {
const kvClient = await getKv();
if (kvClient._baseKv && typeof kvClient._baseKv.get === 'function') {
const value = await kvClient._baseKv.get(`${VERCEL_KV_PREFIX}/${key}`);
return (typeof value === 'string') ? value : undefined;
} else if (isNetlifyBlobs && kvClient._store && typeof kvClient._store.get === 'function') {
const value = await kvClient._store.get(key, { type: "text" });
return value === null ? undefined : value; } else {
const rawValue = await kv_get(key);
if (rawValue === undefined || rawValue === null) {
return undefined;
}
let textValue;
if (rawValue instanceof Uint8Array) {
textValue = new TextDecoder().decode(rawValue);
} else if (typeof rawValue === 'string') {
textValue = rawValue;
} else { console.warn(`kv_get_text: KV fallback returned unexpected type for key ${key}:`, typeof rawValue);
return undefined;
}
return textValue;
}
} catch (error) {
if (error.message && error.message.includes('not found')) {
console.debug(`KV get_text: Key ${key} not found.`);
return undefined; }
console.error(`KV get_text error for ${key}:`, error);
throw new Error(`Failed to get text for key ${key}: ${error.message}`);
}
}
async function kv_set(key, value ) {
try {
const kvClient = await getKv();
await kvClient.set(key, value);
} catch (error) {
console.error(`KV set error for ${key}:`, error);
throw new Error(`Failed to set key ${key}: ${error.message}`);
}
}
async function kv_set_text(key, value ) {
try {
const kvClient = await getKv();
if (kvClient._baseKv && typeof kvClient._baseKv.set === 'function') {
await kvClient._baseKv.set(`${VERCEL_KV_PREFIX}/${key}`, value);
} else if (isNetlifyBlobs && kvClient._store && typeof kvClient._store.set === 'function') {
await kvClient._store.set(key, value);
}
else {
if (kvClient.set === localStorageMap.set) { await kvClient.set(key, value); } else {
await kvClient.set(key, new TextEncoder().encode(value));
}
}
} catch (error) {
console.error(`KV set_text error for ${key}:`, error);
throw new Error(`Failed to set text for key ${key}: ${error.message}`);
}
}
async function kv_exists(key) {
try {
const kvClient = await getKv();
const exists = await kvClient.exists(key);
return exists > 0;
} catch (error) {
console.error(`KV exists error for ${key}:`, error);
return false;
}
}
async function kv_list(prefix) {
try {
const kvClient = await getKv();
if (isNetlifyBlobs && kvClient._store) {
const result = await kvClient._store.list({ prefix });
return result.blobs.map(blob => blob.key);
}
let cursor = 0;
const keys = [];
let scanResult;
do {
scanResult = await kvClient.scan(cursor, {
match: `${prefix}*`,
count: 100 });
cursor = scanResult[0]; const resultKeys = scanResult[1];
if (resultKeys && resultKeys.length > 0) {
keys.push(...resultKeys);
}
} while (cursor !== '0');
return keys;
} catch (error) {
console.error(`KV list error for prefix ${prefix}:`, error);
return [];
}
}
async function kv_del(key) {
try {
const kvClient = await getKv();
await kvClient.del(key);
} catch (error) {
console.error(`KV del error for ${key}:`, error);
}
}
async function fetch_url(url) {
try {
const response = await fetch(url);
return response;
} catch (error) {
console.error(`Fetch error for ${url}:`, error);
throw error;
}
}
async function response_status(response ) {
if (!(response instanceof Response)) {
throw new Error("Input is not a Response object");
}
return response.status;
}
async function response_bytes(response ) {
if (!(response instanceof Response)) {
throw new Error("Input is not a Response object");
}
try {
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
} catch (error) {
console.error(`Error reading response body:`, error);
throw error;
}
}
async function wasm_fetch_with_request(url, options) {
try {
let headers = {};
if (options && options.headers) {
const headerEntries = Object.entries(options.headers);
for (const [key, value] of headerEntries) {
headers[key] = value;
}
}
const method = options && options.method ? options.method : 'GET';
const body = options && options.body ? options.body : undefined;
let fetchFunc = fetch;
if (typeof fetch === 'undefined') {
try {
const nodeFetch = require('node-fetch');
fetchFunc = nodeFetch;
} catch (e) {
console.error('Neither global fetch nor node-fetch is available:', e);
throw new Error('No fetch implementation available');
}
}
const response = await fetchFunc(url, {
method,
headers,
body,
});
return response;
} catch (error) {
console.error(`WASM fetch error for ${url}:`, error);
throw error;
}
}
async function response_headers(response) {
if (!(response instanceof Response)) {
throw new Error("Input is not a Response object");
}
const headers = {};
for (const [key, value] of response.headers.entries()) {
headers[key] = value;
}
return headers;
}
async function response_text(response) {
if (!(response instanceof Response)) {
throw new Error("Input is not a Response object");
}
return await response.text();
}
function dummy() {
return "dummy";
}
async function migrateStorage(oldVersion, newVersion) {
console.warn(`Storage migration needed from v${oldVersion} to v${newVersion}. Migration logic not implemented yet.`);
await Promise.resolve(); }
module.exports = {
localStorageMap,
getKv, kv_get,
kv_set,
kv_exists,
kv_list,
kv_del,
fetch_url,
response_status,
response_bytes,
wasm_fetch_with_request,
response_headers,
response_text,
getenv,
dummy,
migrateStorage, kv_get_text,
kv_set_text,
};