From 290b51a26564617d93229e498b09a2588f931de2 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 23 Mar 2026 11:42:18 +0900 Subject: [PATCH 1/2] feat(logs): add in-memory message buffer for offline queueing Queue add/remove/clear operations when the client is not yet authorized or connected, then flush them in FIFO order once the RPC connection becomes trusted. The add method returns a proxy handle immediately with placeholder data, swapping in the real server entry after flush. Includes optimization to discard pending operations when clear is called. Co-Authored-By: Claude Haiku 4.5 --- .../client/webcomponents/state/logs-client.ts | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/core/src/client/webcomponents/state/logs-client.ts b/packages/core/src/client/webcomponents/state/logs-client.ts index a0bd3362..82ed22c6 100644 --- a/packages/core/src/client/webcomponents/state/logs-client.ts +++ b/packages/core/src/client/webcomponents/state/logs-client.ts @@ -19,16 +19,76 @@ function createRpcHandle(rpc: DevToolsRpcClient, initialEntry: DevToolsLogEntry) } export function createClientLogsClient(rpc: DevToolsRpcClient): DevToolsLogsClient { + const buffer: (() => Promise)[] = [] + let flushing = false + + async function flush() { + if (flushing) + return + flushing = true + while (buffer.length > 0) { + const op = buffer.shift()! + await op() + } + flushing = false + } + + function enqueue(op: () => Promise): Promise { + if (rpc.isTrusted === true && buffer.length === 0) + return op() + return new Promise((resolve) => { + buffer.push(async () => { + await op() + resolve() + }) + }) + } + + rpc.events.on('rpc:is-trusted:updated', (isTrusted) => { + if (isTrusted && buffer.length > 0) + flush() + }) + return { async add(input: DevToolsLogEntryInput): Promise { - const entry = await rpc.call('devtoolskit:internal:logs:add', input) - return createRpcHandle(rpc, entry) + if (rpc.isTrusted === true && buffer.length === 0) { + const entry = await rpc.call('devtoolskit:internal:logs:add', input) + return createRpcHandle(rpc, entry) + } + + // Deferred handle: resolves once the buffered add flushes + let resolved: DevToolsLogHandle | undefined + const ready = new Promise(resolve => buffer.push(async () => { + const entry = await rpc.call('devtoolskit:internal:logs:add', input) + resolved = createRpcHandle(rpc, entry) + resolve(resolved) + })) + + const placeholder: DevToolsLogEntry = { + ...input, + id: input.id ?? `pending-${Date.now()}-${Math.random().toString(36).slice(2)}`, + from: 'browser', + timestamp: input.timestamp ?? Date.now(), + } + + return { + get entry() { return resolved?.entry ?? placeholder }, + get id() { return resolved?.id ?? placeholder.id }, + update: patch => resolved ? resolved.update(patch) : ready.then(h => h.update(patch)), + dismiss: () => resolved ? resolved.dismiss() : ready.then(h => h.dismiss()), + } }, - async remove(id: string): Promise { - await rpc.call('devtoolskit:internal:logs:remove', id) + + remove(id: string): Promise { + return enqueue(() => rpc.call('devtoolskit:internal:logs:remove', id)) }, - async clear(): Promise { - await rpc.call('devtoolskit:internal:logs:clear') + + clear(): Promise { + if (rpc.isTrusted === true && buffer.length === 0) + return rpc.call('devtoolskit:internal:logs:clear') + // Discard preceding buffered operations — they'd be cleared anyway + buffer.length = 0 + return enqueue(() => rpc.call('devtoolskit:internal:logs:clear')) }, } } From 84908519c799b800c8d2c02ba9820333752251a4 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 23 Mar 2026 11:49:21 +0900 Subject: [PATCH 2/2] fix: update --- .../client/webcomponents/state/logs-client.ts | 102 +++++++----------- 1 file changed, 39 insertions(+), 63 deletions(-) diff --git a/packages/core/src/client/webcomponents/state/logs-client.ts b/packages/core/src/client/webcomponents/state/logs-client.ts index 82ed22c6..2ddb89a1 100644 --- a/packages/core/src/client/webcomponents/state/logs-client.ts +++ b/packages/core/src/client/webcomponents/state/logs-client.ts @@ -1,45 +1,37 @@ -import type { DevToolsLogEntry, DevToolsLogEntryInput, DevToolsLogHandle, DevToolsLogsClient } from '@vitejs/devtools-kit' +import type { DevToolsLogEntryInput, DevToolsLogHandle, DevToolsLogsClient } from '@vitejs/devtools-kit' import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' -function createRpcHandle(rpc: DevToolsRpcClient, initialEntry: DevToolsLogEntry): DevToolsLogHandle { - let entry = initialEntry - return { - get entry() { return entry }, - get id() { return entry.id }, - async update(patch: Partial) { - const updated = await rpc.call('devtoolskit:internal:logs:update', entry.id, patch) - if (updated) - entry = updated - return updated ?? undefined - }, - async dismiss() { - await rpc.call('devtoolskit:internal:logs:remove', entry.id) - }, - } -} - export function createClientLogsClient(rpc: DevToolsRpcClient): DevToolsLogsClient { const buffer: (() => Promise)[] = [] - let flushing = false + let flushing: Promise | undefined async function flush() { - if (flushing) + if (rpc.isTrusted !== true) return - flushing = true - while (buffer.length > 0) { - const op = buffer.shift()! - await op() + if (flushing === undefined) { + // eslint-disable-next-line no-async-promise-executor + flushing = new Promise(async (resolve) => { + while (buffer.length > 0) { + const op = buffer.shift()! + await op() + } + resolve() + }) } - flushing = false + return flushing } - function enqueue(op: () => Promise): Promise { + async function enqueue(op: () => Promise): Promise { + if (rpc.isTrusted === true && buffer.length !== 0) + await flush() + if (rpc.isTrusted === true && buffer.length === 0) - return op() - return new Promise((resolve) => { + return await op() + + return new Promise((resolve) => { buffer.push(async () => { - await op() - resolve() + const result = await op() + resolve(result) }) }) } @@ -50,44 +42,28 @@ export function createClientLogsClient(rpc: DevToolsRpcClient): DevToolsLogsClie }) return { - async add(input: DevToolsLogEntryInput): Promise { - if (rpc.isTrusted === true && buffer.length === 0) { - const entry = await rpc.call('devtoolskit:internal:logs:add', input) - return createRpcHandle(rpc, entry) - } - - // Deferred handle: resolves once the buffered add flushes - let resolved: DevToolsLogHandle | undefined - const ready = new Promise(resolve => buffer.push(async () => { - const entry = await rpc.call('devtoolskit:internal:logs:add', input) - resolved = createRpcHandle(rpc, entry) - resolve(resolved) - })) - - const placeholder: DevToolsLogEntry = { - ...input, - id: input.id ?? `pending-${Date.now()}-${Math.random().toString(36).slice(2)}`, - from: 'browser', - timestamp: input.timestamp ?? Date.now(), - } - - return { - get entry() { return resolved?.entry ?? placeholder }, - get id() { return resolved?.id ?? placeholder.id }, - update: patch => resolved ? resolved.update(patch) : ready.then(h => h.update(patch)), - dismiss: () => resolved ? resolved.dismiss() : ready.then(h => h.dismiss()), - } + add(input: DevToolsLogEntryInput): Promise { + return enqueue(async () => { + let entry = await rpc.call('devtoolskit:internal:logs:add', input) + return { + get entry() { return entry }, + get id() { return entry.id }, + async update(patch: Partial) { + const updated = await rpc.call('devtoolskit:internal:logs:update', entry.id, patch) + if (updated) + entry = updated + return updated ?? undefined + }, + async dismiss() { + await rpc.call('devtoolskit:internal:logs:remove', entry.id) + }, + } + }) }, - remove(id: string): Promise { return enqueue(() => rpc.call('devtoolskit:internal:logs:remove', id)) }, - clear(): Promise { - if (rpc.isTrusted === true && buffer.length === 0) - return rpc.call('devtoolskit:internal:logs:clear') - // Discard preceding buffered operations — they'd be cleared anyway - buffer.length = 0 return enqueue(() => rpc.call('devtoolskit:internal:logs:clear')) }, }