@maximai/maxim-js
Version:
Maxim AI JS SDK. Visit https://getmaxim.ai for more info.
362 lines • 15.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogWriter = void 0;
const fs_1 = __importDefault(require("fs"));
const mime_types_1 = __importDefault(require("mime-types"));
const os_1 = __importDefault(require("os"));
const attachment_1 = require("../apis/attachment");
const logs_1 = require("../apis/logs");
const mutex_1 = require("../utils/mutex");
const queue_1 = require("../utils/queue");
const utils_1 = require("../utils/utils");
const attachment_2 = require("./components/attachment");
const types_1 = require("./components/types");
class LogWriter {
constructor(config) {
this.id = (0, utils_1.generateUniqueId)();
this.queue = new queue_1.Queue();
this.attachmentQueue = new queue_1.Queue();
this.mutex = mutex_1.Mutex.get(`maxim-logs-${this.id}`);
this.flushInterval = null;
this.logsDir = `${os_1.default.tmpdir()}/maxim-sdk/${this.id}/maxim-logs`;
this.config = config;
this.isDebug = config.isDebug || false;
this._raiseExceptions = config.raiseExceptions;
this.maxInMemoryLogs = config.maxInMemoryLogs || 100;
this.cache = config.cache;
this.logsAPIService = new logs_1.MaximLogsAPI(config.baseUrl, config.apiKey);
this.attachmentAPIService = new attachment_1.MaximAttachmentAPI(config.baseUrl, config.apiKey);
if (config.autoFlush) {
this.flushInterval = setInterval(() => {
this.flush();
this.flushAttachments();
}, config.flushInterval ? config.flushInterval * 1000 : 10000);
this.flushInterval.unref();
}
}
get writerConfig() {
return this.config;
}
get raiseExceptions() {
return this._raiseExceptions;
}
get writerLogsAPIService() {
return this.logsAPIService;
}
get writerCache() {
return this.cache;
}
isOnAWSLambda() {
return process.env["AWS_LAMBDA_FUNCTION_NAME"] !== undefined;
}
hasAccessToFilesystem() {
try {
fs_1.default.accessSync(os_1.default.tmpdir(), fs_1.default.constants.W_OK);
return true;
}
catch (err) {
return false;
}
}
writeToFile(logs) {
try {
return new Promise((resolve, reject) => {
if (!fs_1.default.existsSync(this.logsDir)) {
fs_1.default.mkdirSync(this.logsDir, { recursive: true });
}
const content = logs.map((l) => l.serialize()).join("\n");
const filename = `logs-${new Date().toISOString()}.log`;
fs_1.default.writeFile(`${this.logsDir}/${filename}`, content, (err) => {
if (err) {
reject(err);
return;
}
resolve(`${this.logsDir}/${filename}`);
});
});
}
catch (err) {
if (this._raiseExceptions) {
throw err;
}
else {
console.error(`[Maxim-SDK] Error while writing to file: ${err instanceof Error ? err.message : err}`);
return undefined;
}
}
}
async flushLogFiles() {
if (!this.hasAccessToFilesystem() || !fs_1.default.existsSync(this.logsDir)) {
return;
}
const files = fs_1.default.readdirSync(this.logsDir);
await Promise.all(files.map(async (file) => {
const logs = fs_1.default.readFileSync(`${this.logsDir}/${file}`, "utf-8");
try {
await this.logsAPIService.pushLogs(this.config.repositoryId, logs);
try {
fs_1.default.rmSync(`${this.logsDir}/${file}`);
}
catch (ignored) { }
}
catch (err) {
if (err && typeof err === "object" && "message" in err && typeof err.message === "string")
console.error(`Error while pushing logs: ${err.message}`);
}
}));
}
async uploadFile(attachmentData, entity, entityId) {
var _a;
try {
if (!attachmentData.path) {
console.error("[MaximSDK] Path is not set for file attachment. Skipping upload");
return;
}
const mimeType = attachmentData.mimeType || mime_types_1.default.lookup(attachmentData.path) || "application/octet-stream";
const size = fs_1.default.statSync(attachmentData.path).size;
const key = attachmentData.key;
const data = fs_1.default.readFileSync(attachmentData.path);
const resp = await this.attachmentAPIService.getUploadUrl(key, mimeType, size);
const addAttachmentData = { ...attachmentData };
delete addAttachmentData.path;
const addAttachmentLog = new types_1.CommitLog(entity, entityId, "add-attachment", addAttachmentData);
this.queue.enqueue(addAttachmentLog);
await this.attachmentAPIService.uploadToSignedUrl(resp.url, data, mimeType);
if (this.isDebug) {
console.log(`[MaximSDK] File uploaded to the Maxim API. URL: ${resp.url}, Mime type: ${mimeType}, Size: ${size}`);
}
}
catch (err) {
const currentRetry = "retry" in attachmentData && typeof attachmentData.retry === "number" ? ((_a = attachmentData.retry) !== null && _a !== void 0 ? _a : 0) : 0;
if (currentRetry < 3) {
attachmentData.retry = currentRetry + 1;
const retryLog = new types_1.CommitLog(entity, entityId, "upload-attachment", attachmentData);
this.attachmentQueue.enqueue(retryLog);
}
else {
console.error(`[MaximSDK] Failed to upload file: ${err instanceof Error ? err.message : err}`);
}
}
}
async uploadFileData(attachmentData, entity, entityId) {
var _a;
try {
if (!attachmentData.data) {
console.error("[MaximSDK] Data is not set for file data attachment. Skipping upload");
return;
}
const mimeType = attachmentData.mimeType || "application/octet-stream";
const key = attachmentData.key;
const size = attachmentData.data.length;
const resp = await this.attachmentAPIService.getUploadUrl(key, mimeType, size);
const addAttachmentData = { ...attachmentData };
delete addAttachmentData.data;
const addAttachmentLog = new types_1.CommitLog(entity, entityId, "add-attachment", addAttachmentData);
this.queue.enqueue(addAttachmentLog);
await this.attachmentAPIService.uploadToSignedUrl(resp.url, attachmentData.data, mimeType);
if (this.isDebug) {
console.log(`[MaximSDK] File data uploaded to the Maxim API. URL: ${resp.url}, Mime type: ${mimeType}, Size: ${size}`);
}
}
catch (err) {
const currentRetry = "retry" in attachmentData && typeof attachmentData.retry === "number" ? ((_a = attachmentData.retry) !== null && _a !== void 0 ? _a : 0) : 0;
if (currentRetry < 3) {
attachmentData.retry = currentRetry + 1;
const retryLog = new types_1.CommitLog(entity, entityId, "upload-attachment", attachmentData);
this.attachmentQueue.enqueue(retryLog);
}
else {
console.error(`[MaximSDK] Failed to upload file data: ${err instanceof Error ? err.message : err}`);
}
}
}
async uploadAttachment(attachment) {
const entity = attachment.type;
const entityId = attachment.id;
const attachmentData = attachment.data;
const populatedAttachment = (0, attachment_2.populateAttachmentFields)(attachmentData);
const attachmentType = populatedAttachment.type;
switch (attachmentType) {
case "file":
await this.uploadFile(populatedAttachment, entity, entityId);
break;
case "fileData":
await this.uploadFileData(populatedAttachment, entity, entityId);
break;
case "url":
const addAttachmentLog = new types_1.CommitLog(entity, entityId, "add-attachment", populatedAttachment);
this.queue.enqueue(addAttachmentLog);
break;
default:
const exhaustiveCheck = attachmentType;
console.error(`[MaximSDK] Unknown attachment type: ${attachmentType}. Skipping upload.`);
}
}
async flushAttachments() {
const attachments = this.attachmentQueue.dequeueAll();
if (attachments.length === 0) {
return;
}
await Promise.all(attachments.map(async (attachment) => {
return this.uploadAttachment(attachment);
}));
}
async flushLogs(logs) {
try {
await this.flushLogFiles();
if (this.isDebug) {
console.log("[MaximSDK] Flushing new logs");
logs.map((l) => console.log(l.serialize()));
}
const MAX_SIZE = 5242880;
const chunks = [];
let currentChunk = "";
for (const log of logs) {
const serialized = log.serialize() + "\n";
if (currentChunk.length + serialized.length > MAX_SIZE) {
chunks.push(currentChunk);
currentChunk = serialized;
}
else {
currentChunk += serialized;
}
}
if (currentChunk.length > 0) {
chunks.push(currentChunk);
}
for (const chunk of chunks) {
await this.logsAPIService.pushLogs(this.config.repositoryId, chunk);
if (this.isDebug)
console.log(`[MaximSDK] Flushed chunk of size ${chunk.length} bytes`);
}
return;
}
catch (err) {
console.error("Error while pushing logs", err);
if (this.isOnAWSLambda() || !this.hasAccessToFilesystem()) {
this.queue.enqueueAll(logs);
return;
}
await this.writeToFile(logs);
}
}
getAttachmentKey(log) {
if (log.action === "upload-attachment") {
const repoId = this.config.repositoryId;
const entity = log.type;
const entityId = log.id;
const attachmentData = log.data;
const fileId = attachmentData.id;
return `${repoId}/${entity}/${entityId}/files/original/${fileId}`;
}
return null;
}
commit(log) {
try {
if (this.isDebug)
console.log("[MaximSDK] Committing log: ", log.serialize());
if (!/^[a-zA-Z0-9_-]+$/.test(log.id)) {
if (this._raiseExceptions) {
throw new Error(`Invalid ID: ${log.id}. ID must only contain alphanumeric characters, hyphens, and underscores. Event will not be logged.`);
}
return;
}
if (log.action === "upload-attachment") {
if (!log.data) {
console.error("[MaximSDK] Attachment data is not set for log. Skipping upload.");
return;
}
const key = this.getAttachmentKey(log);
if (key) {
log.data.key = key;
this.attachmentQueue.enqueue(log);
}
else {
console.error(`[MaximSDK] Failed to generate attachment key due to invalid action: ${log.action}. Skipping upload.`);
}
}
else {
this.queue.enqueue(log);
}
if (this.queue.size + this.attachmentQueue.size > this.maxInMemoryLogs) {
this.flush();
}
}
catch (err) {
if (this._raiseExceptions) {
throw err;
}
else {
console.error(`[Maxim-SDK] Error while committing log: ${err instanceof Error ? err.message : err}`);
}
}
}
async flush() {
try {
let items = [];
const MUTEX_TIMEOUT_MS = 30000;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Mutex acquisition timed out after ${MUTEX_TIMEOUT_MS}ms`));
}, MUTEX_TIMEOUT_MS);
});
await Promise.race([
new Promise(async (resolve) => {
await this.mutex.withLock(async () => {
try {
await this.flushAttachments();
items = this.queue.dequeueAll();
if (items.length === 0) {
await this.flushLogFiles();
if (this.isDebug)
console.log("[MaximSDK] No logs to flush");
resolve();
}
if (this.isDebug)
console.log("[MaximSDK] Flushing logs");
await this.flushLogs(items);
}
catch (err) {
console.error("[MaximSDK] Couldn't flush logs", err);
resolve();
}
});
resolve();
}),
timeoutPromise,
]);
if (this.isDebug)
console.log("[MaximSDK] Flush complete");
}
catch (err) {
if (this._raiseExceptions) {
throw err;
}
else {
console.error(`[Maxim-SDK] Error while flushing logs: ${err instanceof Error ? err.message : err}`);
}
}
}
async cleanup() {
try {
if (this.flushInterval)
clearInterval(this.flushInterval);
await this.flush();
this.logsAPIService.destroyAgents();
this.attachmentAPIService.destroyAgents();
}
catch (err) {
if (this._raiseExceptions) {
throw err;
}
else {
console.error(`[Maxim-SDK] Error while cleaning up: ${err instanceof Error ? err.message : err}`);
}
}
}
}
exports.LogWriter = LogWriter;
//# sourceMappingURL=writer.js.map