Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/playground/src/clients/indexedDB/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,32 @@ export class IndexedDBClient {
return res;
}

public async deleteRange(startId: number, endId: number): Promise<void> {
await this._lock.acquireAsync();

return new Promise((res, rej) => {
const store = this._createStore();
const range = IDBKeyRange.bound(startId, endId);
const query = store.openCursor(range);

query.onsuccess = () => {
const cursor = query.result;
if (cursor) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to see if it's actually deleting the logs but it reached here and the cursor was null, so nothing was deleted.

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after the fix:
image

cursor.delete();
cursor.continue();
} else {
this._lock.release();
res();
}
};

query.onerror = e => {
this._lock.release();
rej(e);
};
});
}

public disconnect() {
const db = this._assertConnection();
db.close();
Expand Down
138 changes: 99 additions & 39 deletions packages/playground/src/components/logger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,23 @@ export default {
}

const logs = ref<Indexed<LoggerInstance>[]>([]);

/**
* Keep a reference to the original console.error so that internal
* logger failures don't recursively go through the interceptor and
* generate more log entries.
*/
const originalConsoleError = console.error.bind(console);

const interceptor = new LoggerInterceptor(console);

const logsDBClient = new IndexedDBClient("TF_LOGGER_DB", VERSION, KEY);
const connectDB = useAsync(() => logsDBClient.connect(), {
init: true,
async onAfterTask({ error }) {
if (error) {
// Stop intercepting entirely on persistent DB failure.
interceptor.dispose();
return;
}

Expand All @@ -155,6 +165,7 @@ export default {

_interceptorQueue.forEach(interceptMessage);
_interceptorQueue = [];
if (logQueue.length > 0) flushLogQueue();
},
});

Expand Down Expand Up @@ -207,59 +218,104 @@ export default {

interceptor.on(interceptMessage);

// This should be used if db failed to connect to be synced later
let _interceptorQueue: LI[] = [];
const logQueue: LI[] = [];
let flushTimeout: ReturnType<typeof setTimeout> | null = null;
const BATCH_SIZE = 50;
const FLUSH_DELAY = 500;

function scheduleFlush() {
if (flushTimeout) return;
flushTimeout = setTimeout(flushLogQueue, FLUSH_DELAY);
}

async function interceptMessage(instance: LI) {
if (connectDB?.value?.error) {
_interceptorQueue.push(instance);
return;
const MAX_VISIBLE_LOGS = 2000;
const MAX_STORED_LOGS = 10000;
const ROTATION_BUFFER = 1000;

async function flushLogQueue() {
if (logQueue.length === 0 || !connectDB?.value?.data) return;

const batch = logQueue.splice(0, BATCH_SIZE);
const items: Indexed<LoggerInstance>[] = [];

for (const instance of batch) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { logger: _, date: __, ...log } = instance;

try {
items.push(
await logsDBClient.write({
type: log.type,
timestamp: log.timestamp,
message: log.messages.map(IndexedDBClient.serializer.serialize).join(" ").replace(/\n\s/g, "\n"),
}),
);
} catch (error) {
// Use the original console.error to avoid re-interception.
originalConsoleError("Failed to write log to IndexedDB:", error);
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { logger: _, date: __, ...log } = instance;

if (import.meta.env.DEV) {
if (
log.messages
.map(v => {
try {
return String(v);
} catch {
return "{ [[null proto]] }";
}
})
.join()
.includes("vite") &&
log.type === "debug"
) {
return;
if (items.length > 0 && logs.value) {
logs.value.push(...items);
if (logs.value.length > MAX_VISIBLE_LOGS) {
logs.value.splice(0, logs.value.length - MAX_VISIBLE_LOGS);
}
scrollToBottom();
}
if (connectDB && connectDB.value.data) {
const item = await logsDBClient.write({
type: log.type,
timestamp: log.timestamp,
message: log.messages.map(IndexedDBClient.serializer.serialize).join(" ").replace(/\n\s/g, "\n"),
});
if (logs.value) {
logs.value.push(item);
scrollToBottom();

// Rotate old logs if count exceeds limit
const currentCount = await logsDBClient.count();
if (currentCount > MAX_STORED_LOGS) {
const toDelete = currentCount - MAX_STORED_LOGS + ROTATION_BUFFER;
try {
await logsDBClient.deleteRange(1, toDelete);
count.value = await logsDBClient.count();
} catch (error) {
originalConsoleError("Failed to rotate logs:", error);
}
}

flushTimeout = logQueue.length > 0 ? setTimeout(flushLogQueue, FLUSH_DELAY) : null;
}

let _init_scroll = false;
function scrollToBottom() {
const el = scroller.value?.$el;
if (!el || el.scrollHeight === 0 || el.offsetHeight === 0) {
function interceptMessage(instance: LI) {
// Drop very noisy categories early to avoid unnecessary work.
const payload = instance.messages.map(String).join().toLowerCase();
if (
instance.type === "warn" &&
(payload.includes("vue") || payload.includes("vite") || payload.includes("hmr"))
) {
return;
}

if (!_init_scroll || el.scrollTop === el.scrollHeight - el.offsetHeight) {
_init_scroll = true;
scroller.value?.scrollToBottom();
if (connectDB?.value?.error) {
_interceptorQueue.push(instance);
return;
}

if (!connectDB?.value?.data) return;

logQueue.push(instance);

if (logQueue.length >= BATCH_SIZE) {
flushTimeout && clearTimeout(flushTimeout);
flushTimeout = null;
return flushLogQueue();
}

scheduleFlush();
}

let _init_scroll = false;
function scrollToBottom() {
const el = scroller.value?.$el;
if (!el || el.scrollHeight === 0 || el.offsetHeight === 0) return;
if (_init_scroll && el.scrollTop !== el.scrollHeight - el.offsetHeight) return;

_init_scroll = true;
scroller.value?.scrollToBottom();
}

async function downloadLogs() {
Expand All @@ -281,6 +337,10 @@ export default {

onBeforeUnmount(() => {
document.removeEventListener("click", handleClickOutside);
if (flushTimeout) {
clearTimeout(flushTimeout);
flushLogQueue();
}
});

const handleClickOutside = (event: MouseEvent) => {
Expand Down