Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
471 changes: 471 additions & 0 deletions docs/roadmap/reactive-state-management.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions jest.config.renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/test/setup-renderer.ts'],
moduleNameMapper: {
'\\.css$': '<rootDir>/test/mocks/styleMock.ts',
'^@zubridge/electron$': '<rootDir>/test/__mocks__/@zubridge/electron.ts',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', {
Expand Down
324 changes: 244 additions & 80 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.1",
"@zubridge/electron": "^2.1.1",
"is-camera-on": "^4.0.0",
"js-yaml": "^4.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tar": "^7.5.2",
"xstate": "^5.25.0"
"xstate": "^5.25.0",
"zustand": "^5.0.9"
},
"overrides": {
"tmp": "^0.2.5"
Expand Down
18 changes: 18 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ import {
selectEffectivePauseState,
} from './runner-state-service';

// Zustand store
import { initStore, connectWindow, cleanupStore } from './store/init';

// ============================================================================
// App Initialization
// ============================================================================
Expand Down Expand Up @@ -165,6 +168,9 @@ app.whenReady().then(async () => {
// Initialize state machine (must be early - before anything uses state)
initRunnerStateMachine();

// Initialize Zustand store (after state machine, so XState sync works)
initStore();

// Subscribe to state changes for UI updates
onStateChange((snapshot) => {
const mainWindow = getMainWindow();
Expand Down Expand Up @@ -429,6 +435,12 @@ app.whenReady().then(async () => {
setDockIcon();
setupIpcHandlers();

// Connect window to Zustand store via zubridge
const newMainWindow = getMainWindow();
if (newMainWindow) {
connectWindow(newMainWindow);
}

// Initialize auto-updater
const mainWindow = getMainWindow();
if (mainWindow) {
Expand Down Expand Up @@ -642,6 +654,9 @@ app.on('before-quit', async (event) => {
sendRunnerEvent({ type: 'SHUTDOWN_COMPLETE' });
stopRunnerStateMachine();

// Clean up Zustand store (flushes persistence)
cleanupStore();

logger?.info('Exiting');
app.quit();
}
Expand Down Expand Up @@ -695,6 +710,9 @@ process.on('SIGINT', async () => {
sendRunnerEvent({ type: 'SHUTDOWN_COMPLETE' });
stopRunnerStateMachine();

// Clean up Zustand store (flushes persistence)
cleanupStore();

getLogger()?.info('Exiting');
app.quit();
});
38 changes: 38 additions & 0 deletions src/main/ipc-handlers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
getLogger,
} from '../app-state';
import { IPC_CHANNELS, SleepProtection, LogLevel } from '../../shared/types';
import { store } from '../store';
import { ThemeSetting } from '../store/types';

const log = () => getLogger();

Expand Down Expand Up @@ -58,6 +60,42 @@ export const registerSettingsHandlers = (): void => {

saveConfig({ ...current, ...sanitizedSettings });

// Update Zustand store to sync with renderer via zubridge
const storeState = store.getState();
if (settings.theme !== undefined) {
storeState.setTheme(settings.theme as ThemeSetting);
}
if (settings.logLevel !== undefined) {
storeState.setLogLevel(settings.logLevel as LogLevel);
}
if (settings.runnerLogLevel !== undefined) {
storeState.setRunnerLogLevel(settings.runnerLogLevel as LogLevel);
}
if (settings.sleepProtection !== undefined) {
storeState.setSleepProtection(settings.sleepProtection as SleepProtection);
}
if (settings.userFilter !== undefined) {
storeState.setUserFilter(settings.userFilter);
}
if (settings.power !== undefined) {
storeState.setPower(settings.power);
}
if (settings.notifications !== undefined) {
storeState.setNotifications(settings.notifications);
}
if (settings.launchAtLogin !== undefined) {
storeState.setLaunchAtLogin(settings.launchAtLogin);
}
if (settings.targets !== undefined) {
storeState.setTargets(settings.targets);
}
if (settings.maxConcurrentJobs !== undefined) {
storeState.setMaxConcurrentJobs(settings.maxConcurrentJobs);
}
if (settings.runnerConfig !== undefined) {
storeState.updateRunnerConfig(settings.runnerConfig);
}

// Update sleep protection if setting changed
if (settings.sleepProtection !== undefined) {
setSleepProtectionSetting(settings.sleepProtection as SleepProtection);
Expand Down
7 changes: 7 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron';
import { preloadBridge } from '@zubridge/electron/preload';
import {
IPC_CHANNELS,
RunnerState,
Expand All @@ -21,6 +22,12 @@ import {
ResourcePauseState,
} from '../shared/types';

// Initialize zubridge preload handlers
const { handlers: zubridgeHandlers } = preloadBridge();

// Expose zubridge to renderer
contextBridge.exposeInMainWorld('zubridge', zubridgeHandlers);

// Expose protected methods to the renderer process
contextBridge.exposeInMainWorld('localmost', {
// App / Setup
Expand Down
58 changes: 58 additions & 0 deletions src/main/store/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Zubridge integration - syncs Zustand store to renderer processes.
*/

import { BrowserWindow } from 'electron';
import { createZustandBridge } from '@zubridge/electron/main';
import { store } from './index';

// Bridge instance
let bridge: ReturnType<typeof createZustandBridge> | null = null;
let unsubscribe: (() => void) | null = null;

/**
* Initialize the zubridge for a window.
* Call this after creating the main window.
*/
export function initBridge(mainWindow: BrowserWindow): void {
if (bridge) {
// Already initialized, just subscribe the new window
const sub = bridge.subscribe([mainWindow]);
// Store the unsubscribe function
if (unsubscribe) {
const oldUnsub = unsubscribe;
unsubscribe = () => {
oldUnsub();
sub.unsubscribe();
};
} else {
unsubscribe = sub.unsubscribe;
}
return;
}

// Create the bridge
bridge = createZustandBridge(store);

// Subscribe the window
const sub = bridge.subscribe([mainWindow]);
unsubscribe = sub.unsubscribe;
}

/**
* Clean up the bridge when the app is quitting.
*/
export function destroyBridge(): void {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
bridge = null;
}

/**
* Get the bridge instance (for advanced use cases).
*/
export function getBridge() {
return bridge;
}
Loading
Loading