mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
* perf(startup): stop queueing window creation behind the proxy apply and i18n
Three independent, measured startup wins, all free:
1. Park the initial Chromium proxy apply on `mainProcessState` instead of
awaiting it mid-`initializeReadyFoundation`. `setProxy` still starts at the
identical moment; the default-session request guard (which holds, not
cancels) is what actually fences fetchers on it, so only window creation
stops waiting. Runtime launch still awaits it before the desktop relay and
before every headless-serve fetcher.
2. Run `initializeMainProcessI18nAndMenu` concurrently with
`initializeMainProcessRuntimeLaunch`. Nothing in window creation reads a
translated string or the native menu.
3. Load `emojibase-data` in main through `createRequire` on first use instead
of a static import, keeping 166 KB of JSON off `out/main/index.js` and its
~2 ms parse off every launch. The renderer keeps its eager copy unchanged.
out/main/index.js 7,210,071 -> 7,040,147 bytes. No renderer behaviour changes.
* fix(packaging): ship the emoji shortcode dataset main lazily requires
app.asar carries no node_modules, so main's bare requires resolve only out of
Resources/node_modules. emojibase-data is a devDependency and is not in the
packaged runtime allowlist, so the new createRequire in
deferred-emoji-shortcode-dataset.ts threw MODULE_NOT_FOUND in every packaged
build — breaking sanitizeWorktreeName, and with it workspace creation.
Copy the single 166 KB dataset (not the 49 MB package root) into
Resources/node_modules, and gate every createRequire'd bare specifier in
src/main against the packaged resource plan. verifyPackagedMainRuntimeDeps
cannot catch these: the bundler renames the require binding.
* test(proxy): fail CI when a main-process fetcher escapes the default-session guard
The hoist relies on installElectronProxyRequestGuard(session.defaultSession) holding every app-owned request until the persisted proxy lands. Nothing enforced that every fetcher actually lands on defaultSession. Two source-anchored rules do now: no net.fetch/net.request may name a session/partition, and every non-net .fetch( call site is counted against an allowlist.
* test(proxy): close the shorthand and chained-receiver holes in the fetch call-site audit
The audit caught `net.request({ session: x })` and `ident.fetch(`, but not the two
shapes a real regression is just as likely to take: the `{ url, session }` shorthand
that both `net.request` overloads accept, and a receiver with no bare identifier
(`session.fromPartition(...).fetch(`, `ctx.session.fetch(`). Rule 1 now also matches
the shorthand key; rule 2 scans every `.fetch(` and excludes only a literal
`net`/`globalThis`/`global` receiver. Audited counts are unchanged (2/2/1).
* fix(startup): scope the deferred emoji loader to the projects that own it
TS6307: the composite web project lists src/main/ipc/worktree-logic.ts, which
now imports the deferred dataset loader, and the shared lazy test reached into
src/main from a project that has no src/main files. Add the loader to
tsconfig.tc.web.json and move the cross-project case into a src/main test.
Also close the last two review gaps: gate the runtime-RPC startup failure
dialog (the only launch-phase translateMain reader) on a published i18n
barrier so a concurrent i18n phase cannot leave a non-English user with the
English fallback, and let the fetch call-site audit match `net.fetch (url)`.
154 lines
5.4 KiB
TypeScript
154 lines
5.4 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const phaseEvents: string[] = []
|
|
let releaseI18n: (() => void) | null = null
|
|
|
|
vi.mock('./main-process-ready-foundation', () => ({
|
|
initializeReadyFoundation: vi.fn(async () => {
|
|
phaseEvents.push('foundation')
|
|
})
|
|
}))
|
|
vi.mock('./main-process-ready-runtime', () => ({
|
|
initializeReadyRuntimeServices: vi.fn(async () => {
|
|
phaseEvents.push('runtime-services')
|
|
})
|
|
}))
|
|
vi.mock('./main-process-i18n-menu', () => ({
|
|
initializeMainProcessI18nAndMenu: vi.fn(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
phaseEvents.push('i18n-start')
|
|
releaseI18n = () => {
|
|
phaseEvents.push('i18n-done')
|
|
resolve()
|
|
}
|
|
})
|
|
)
|
|
}))
|
|
vi.mock('./main-process-runtime-launch', () => ({
|
|
initializeMainProcessRuntimeLaunch: vi.fn(async () => {
|
|
phaseEvents.push('launch-start')
|
|
await Promise.resolve()
|
|
phaseEvents.push('window-created')
|
|
})
|
|
}))
|
|
|
|
const { initializeMainProcessReady } = await import('./main-process-ready')
|
|
|
|
describe('ready-phase concurrency', () => {
|
|
beforeEach(() => {
|
|
phaseEvents.length = 0
|
|
releaseI18n = null
|
|
})
|
|
|
|
it('creates the window without waiting for i18n and the native menu', async () => {
|
|
const options = {
|
|
openMainWindow: vi.fn(),
|
|
handleMacAppActivation: vi.fn()
|
|
} as unknown as Parameters<typeof initializeMainProcessReady>[0]
|
|
|
|
const ready = initializeMainProcessReady(options)
|
|
// Drain the launch phase's microtasks while i18n is still pending.
|
|
for (let tick = 0; tick < 8; tick += 1) {
|
|
await Promise.resolve()
|
|
}
|
|
|
|
expect(phaseEvents).toEqual([
|
|
'foundation',
|
|
'runtime-services',
|
|
'i18n-start',
|
|
'launch-start',
|
|
'window-created'
|
|
])
|
|
|
|
releaseI18n?.()
|
|
await ready
|
|
expect(phaseEvents.at(-1)).toBe('i18n-done')
|
|
})
|
|
|
|
it('still resolves only once i18n and the menu have settled', async () => {
|
|
const options = {
|
|
openMainWindow: vi.fn(),
|
|
handleMacAppActivation: vi.fn()
|
|
} as unknown as Parameters<typeof initializeMainProcessReady>[0]
|
|
|
|
const ready = initializeMainProcessReady(options)
|
|
let settled = false
|
|
void ready.then(() => {
|
|
settled = true
|
|
})
|
|
for (let tick = 0; tick < 8; tick += 1) {
|
|
await Promise.resolve()
|
|
}
|
|
|
|
expect(settled).toBe(false)
|
|
releaseI18n?.()
|
|
await ready
|
|
expect(settled).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('initial proxy application ordering', () => {
|
|
const readStartupSource = (file: string): string =>
|
|
readFileSync(join(process.cwd(), 'src/main/startup', file), 'utf8')
|
|
|
|
it('parks the default-session proxy apply instead of blocking window creation on it', () => {
|
|
const foundation = readStartupSource('main-process-ready-foundation.ts')
|
|
|
|
expect(foundation).toContain('state.initialProxyApplicationReady = applyElectronProxySettings(')
|
|
// The request guard, not this phase, is what fences fetchers on the proxy; awaiting it here
|
|
// only queued openMainWindow behind a ~24 ms setProxy round trip.
|
|
expect(foundation).not.toMatch(/await\s+(?:state\.)?initialProxyApplication/)
|
|
})
|
|
|
|
it('awaits the proxy after the window opens and before the desktop relay starts', () => {
|
|
const launch = readStartupSource('main-process-runtime-launch.ts')
|
|
const desktopStart = launch.indexOf('async function launchDesktopMode(')
|
|
const desktopEnd = launch.indexOf('\nexport async function initializeMainProcessRuntimeLaunch')
|
|
expect(desktopStart).toBeGreaterThanOrEqual(0)
|
|
expect(desktopEnd).toBeGreaterThan(desktopStart)
|
|
const desktop = launch.slice(desktopStart, desktopEnd)
|
|
|
|
const windowIndex = desktop.indexOf('openMainWindow()')
|
|
const proxyIndex = desktop.indexOf('await state.initialProxyApplicationReady')
|
|
const relayIndex = desktop.indexOf('new DesktopRelayService(')
|
|
|
|
expect(windowIndex).toBeGreaterThanOrEqual(0)
|
|
expect(proxyIndex).toBeGreaterThan(windowIndex)
|
|
expect(relayIndex).toBeGreaterThan(proxyIndex)
|
|
})
|
|
|
|
it('waits for i18n before the only launch-phase dialog that reads a translated string', () => {
|
|
const ready = readStartupSource('main-process-ready.ts')
|
|
const launch = readStartupSource('main-process-runtime-launch.ts')
|
|
|
|
// Published before the launch phase starts, or the barrier the dialog awaits is still the
|
|
// default resolved promise.
|
|
const publishIndex = ready.indexOf('state.mainProcessI18nReady = ')
|
|
expect(publishIndex).toBeGreaterThanOrEqual(0)
|
|
expect(ready.indexOf('initializeMainProcessRuntimeLaunch(options)')).toBeGreaterThan(
|
|
publishIndex
|
|
)
|
|
expect(launch).toMatch(
|
|
/state\.mainProcessI18nReady\.then\(\(\) =>\s*\n?\s*showRuntimeRpcStartupFailureDialog\(/
|
|
)
|
|
})
|
|
|
|
it('keeps headless serve strictly ordered behind the proxy apply', () => {
|
|
const launch = readStartupSource('main-process-runtime-launch.ts')
|
|
const serveStart = launch.indexOf('async function launchServeMode(')
|
|
const serveEnd = launch.indexOf('\nasync function launchDesktopMode(', serveStart)
|
|
expect(serveStart).toBeGreaterThanOrEqual(0)
|
|
expect(serveEnd).toBeGreaterThan(serveStart)
|
|
const serve = launch.slice(serveStart, serveEnd)
|
|
|
|
const proxyIndex = serve.indexOf('await state.initialProxyApplicationReady')
|
|
const rpcIndex = serve.indexOf('runtimeRpc.start()')
|
|
|
|
expect(proxyIndex).toBeGreaterThanOrEqual(0)
|
|
expect(rpcIndex).toBeGreaterThan(proxyIndex)
|
|
})
|
|
})
|