mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
Add restart command, fix post-update restarts as well.
This commit is contained in:
@@ -266,6 +266,9 @@ archives:
|
|||||||
- src: plugin/hooks/hooks.json
|
- src: plugin/hooks/hooks.json
|
||||||
dst: hooks
|
dst: hooks
|
||||||
strip_parent: true
|
strip_parent: true
|
||||||
|
- src: plugin/commands/*
|
||||||
|
dst: commands
|
||||||
|
strip_parent: true
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats:
|
formats:
|
||||||
|
|||||||
@@ -138,10 +138,13 @@ install: build stop-worker
|
|||||||
@# Install to marketplaces directory (for direct installs)
|
@# Install to marketplaces directory (for direct installs)
|
||||||
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks
|
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks
|
||||||
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin
|
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin
|
||||||
|
@mkdir -p $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/commands
|
||||||
cp $(BUILD_DIR)/worker $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/
|
cp $(BUILD_DIR)/worker $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/
|
||||||
cp $(BUILD_DIR)/mcp-server $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/
|
cp $(BUILD_DIR)/mcp-server $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/
|
||||||
cp $(BUILD_DIR)/hooks/* $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
cp $(BUILD_DIR)/hooks/* $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
||||||
cp $(PLUGIN_DIR)/hooks/hooks.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
cp $(PLUGIN_DIR)/hooks/hooks.json $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/hooks/
|
||||||
|
@# Copy slash commands if they exist
|
||||||
|
@if [ -d "$(PLUGIN_DIR)/commands" ]; then cp -r $(PLUGIN_DIR)/commands/* $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/commands/ 2>/dev/null || true; fi
|
||||||
@# Update plugin.json and marketplace.json with current version to prevent stale version directories
|
@# Update plugin.json and marketplace.json with current version to prevent stale version directories
|
||||||
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/plugin.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/plugin.json
|
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/plugin.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/plugin.json
|
||||||
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/marketplace.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/marketplace.json
|
@sed 's/"version": "[^"]*"/"version": "$(VERSION)"/g' $(PLUGIN_DIR)/.claude-plugin/marketplace.json > $(HOME)/.claude/plugins/marketplaces/claude-mnemonic/.claude-plugin/marketplace.json
|
||||||
|
|||||||
+80
-37
@@ -469,57 +469,100 @@ func (u *Updater) extractTarGz(archivePath, destDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) replaceBinaries(extractDir string) error {
|
func (u *Updater) replaceBinaries(extractDir string) error {
|
||||||
// Files to replace
|
// Binary files to replace
|
||||||
binaries := []struct {
|
binaryFiles := []string{
|
||||||
src string
|
"worker",
|
||||||
dest string
|
"mcp-server",
|
||||||
}{
|
"hooks/session-start",
|
||||||
{"worker", filepath.Join(u.installDir, "worker")},
|
"hooks/user-prompt",
|
||||||
{"mcp-server", filepath.Join(u.installDir, "mcp-server")},
|
"hooks/post-tool-use",
|
||||||
{"hooks/session-start", filepath.Join(u.installDir, "hooks", "session-start")},
|
"hooks/stop",
|
||||||
{"hooks/user-prompt", filepath.Join(u.installDir, "hooks", "user-prompt")},
|
"hooks/subagent-stop",
|
||||||
{"hooks/post-tool-use", filepath.Join(u.installDir, "hooks", "post-tool-use")},
|
"hooks/statusline",
|
||||||
{"hooks/stop", filepath.Join(u.installDir, "hooks", "stop")},
|
|
||||||
{"hooks/subagent-stop", filepath.Join(u.installDir, "hooks", "subagent-stop")},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range binaries {
|
// Get all install directories (marketplaces and cache directories)
|
||||||
src := filepath.Join(extractDir, b.src)
|
installDirs := u.getInstallDirectories()
|
||||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
|
||||||
continue // Skip if not in archive
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup existing binary
|
for _, installDir := range installDirs {
|
||||||
if _, err := os.Stat(b.dest); err == nil {
|
log.Debug().Str("dir", installDir).Msg("Installing binaries to directory")
|
||||||
backup := b.dest + ".bak"
|
|
||||||
if err := os.Rename(b.dest, backup); err != nil {
|
for _, binaryFile := range binaryFiles {
|
||||||
return fmt.Errorf("failed to backup %s: %w", b.dest, err)
|
src := filepath.Join(extractDir, binaryFile)
|
||||||
|
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||||
|
continue // Skip if not in archive
|
||||||
}
|
}
|
||||||
defer func(backup, dest string) {
|
|
||||||
// Clean up backup on success, restore on failure
|
dest := filepath.Join(installDir, binaryFile)
|
||||||
if _, err := os.Stat(dest); err == nil {
|
|
||||||
_ = os.Remove(backup)
|
// Backup existing binary
|
||||||
|
if _, err := os.Stat(dest); err == nil {
|
||||||
|
backup := dest + ".bak"
|
||||||
|
if err := os.Rename(dest, backup); err != nil {
|
||||||
|
log.Warn().Err(err).Str("file", dest).Msg("Failed to backup, continuing anyway")
|
||||||
} else {
|
} else {
|
||||||
_ = os.Rename(backup, dest)
|
defer func(backup, dest string) {
|
||||||
|
// Clean up backup on success, restore on failure
|
||||||
|
if _, err := os.Stat(dest); err == nil {
|
||||||
|
_ = os.Remove(backup)
|
||||||
|
} else {
|
||||||
|
_ = os.Rename(backup, dest)
|
||||||
|
}
|
||||||
|
}(backup, dest)
|
||||||
}
|
}
|
||||||
}(backup, b.dest)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Copy new binary
|
// Copy new binary
|
||||||
if err := copyFile(src, b.dest); err != nil {
|
if err := copyFile(src, dest); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", b.dest, err)
|
return fmt.Errorf("failed to install %s: %w", dest, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make executable
|
// Make executable
|
||||||
// #nosec G302 -- executables require 0755 permissions
|
// #nosec G302 -- executables require 0755 permissions
|
||||||
if err := os.Chmod(b.dest, 0755); err != nil {
|
if err := os.Chmod(dest, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to chmod %s: %w", b.dest, err)
|
return fmt.Errorf("failed to chmod %s: %w", dest, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getInstallDirectories returns all directories where binaries should be installed.
|
||||||
|
// This includes the marketplaces directory and any cache directories.
|
||||||
|
func (u *Updater) getInstallDirectories() []string {
|
||||||
|
dirs := []string{u.installDir}
|
||||||
|
|
||||||
|
// Also check cache directories where Claude Code looks for plugins
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for cache directories under ~/.claude/plugins/cache/claude-mnemonic/claude-mnemonic/
|
||||||
|
cacheBase := filepath.Join(home, ".claude/plugins/cache/claude-mnemonic/claude-mnemonic")
|
||||||
|
entries, err := os.ReadDir(cacheBase)
|
||||||
|
if err != nil {
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
cacheDir := filepath.Join(cacheBase, entry.Name())
|
||||||
|
// Only add if it's different from installDir and contains a worker binary
|
||||||
|
if cacheDir != u.installDir {
|
||||||
|
workerPath := filepath.Join(cacheDir, "worker")
|
||||||
|
if _, err := os.Stat(workerPath); err == nil {
|
||||||
|
dirs = append(dirs, cacheDir)
|
||||||
|
log.Debug().Str("dir", cacheDir).Msg("Found cache directory to update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
// Ensure parent directory exists
|
// Ensure parent directory exists
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
|
||||||
|
|||||||
@@ -1024,7 +1024,7 @@ func (s *Service) handleSelfCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUpdateRestart restarts the worker with the new binary.
|
// handleUpdateRestart restarts the worker with the new binary (after update).
|
||||||
func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
||||||
status := s.updater.GetStatus()
|
status := s.updater.GetStatus()
|
||||||
if status.State != "done" {
|
if status.State != "done" {
|
||||||
@@ -1050,3 +1050,29 @@ func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRestart restarts the worker process (general restart, not tied to update).
|
||||||
|
func (s *Service) handleRestart(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Info().Msg("Manual restart requested via API")
|
||||||
|
|
||||||
|
// Send response before restarting
|
||||||
|
writeJSON(w, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "Restarting worker...",
|
||||||
|
"version": s.version,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Flush the response
|
||||||
|
if f, ok := w.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart in background after response is sent
|
||||||
|
go func() {
|
||||||
|
// Small delay to ensure response is sent
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
if err := s.updater.Restart(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to restart worker")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -635,6 +635,9 @@ func (s *Service) setupRoutes() {
|
|||||||
s.router.Get("/api/update/status", s.handleUpdateStatus)
|
s.router.Get("/api/update/status", s.handleUpdateStatus)
|
||||||
s.router.Post("/api/update/restart", s.handleUpdateRestart)
|
s.router.Post("/api/update/restart", s.handleUpdateRestart)
|
||||||
|
|
||||||
|
// General restart endpoint (works before DB is ready)
|
||||||
|
s.router.Post("/api/restart", s.handleRestart)
|
||||||
|
|
||||||
// Selfcheck endpoint (works before DB is ready - checks all components)
|
// Selfcheck endpoint (works before DB is ready - checks all components)
|
||||||
s.router.Get("/api/selfcheck", s.handleSelfCheck)
|
s.router.Get("/api/selfcheck", s.handleSelfCheck)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Restart Claude Mnemonic Worker
|
||||||
|
|
||||||
|
Restart the claude-mnemonic worker process. Use this command when experiencing issues with the memory system.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Call the restart API endpoint using curl:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:37777/api/restart
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Wait a moment for the worker to restart (typically 1-2 seconds)
|
||||||
|
|
||||||
|
3. Verify the worker is running by checking the version:
|
||||||
|
```bash
|
||||||
|
curl -s http://127.0.0.1:37777/api/version
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Report the result to the user, including the version number from the response.
|
||||||
|
|
||||||
|
If the restart fails, suggest the user check `/tmp/claude-mnemonic-worker.log` for errors.
|
||||||
@@ -176,6 +176,7 @@ download_release() {
|
|||||||
info "Installing to ${INSTALL_DIR}..."
|
info "Installing to ${INSTALL_DIR}..."
|
||||||
mkdir -p "$INSTALL_DIR/hooks"
|
mkdir -p "$INSTALL_DIR/hooks"
|
||||||
mkdir -p "$INSTALL_DIR/.claude-plugin"
|
mkdir -p "$INSTALL_DIR/.claude-plugin"
|
||||||
|
mkdir -p "$INSTALL_DIR/commands"
|
||||||
|
|
||||||
# Copy binaries
|
# Copy binaries
|
||||||
cp "$tmp_dir/worker" "$INSTALL_DIR/"
|
cp "$tmp_dir/worker" "$INSTALL_DIR/"
|
||||||
@@ -185,6 +186,11 @@ download_release() {
|
|||||||
# Copy plugin configuration
|
# Copy plugin configuration
|
||||||
cp "$tmp_dir/.claude-plugin/"* "$INSTALL_DIR/.claude-plugin/"
|
cp "$tmp_dir/.claude-plugin/"* "$INSTALL_DIR/.claude-plugin/"
|
||||||
|
|
||||||
|
# Copy slash commands if they exist in the release
|
||||||
|
if [[ -d "$tmp_dir/commands" ]]; then
|
||||||
|
cp -r "$tmp_dir/commands/"* "$INSTALL_DIR/commands/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Make binaries executable
|
# Make binaries executable
|
||||||
chmod +x "$INSTALL_DIR/worker"
|
chmod +x "$INSTALL_DIR/worker"
|
||||||
chmod +x "$INSTALL_DIR/mcp-server"
|
chmod +x "$INSTALL_DIR/mcp-server"
|
||||||
@@ -230,6 +236,7 @@ register_plugin() {
|
|||||||
# Copy files to cache directory
|
# Copy files to cache directory
|
||||||
mkdir -p "$cache_path/.claude-plugin"
|
mkdir -p "$cache_path/.claude-plugin"
|
||||||
mkdir -p "$cache_path/hooks"
|
mkdir -p "$cache_path/hooks"
|
||||||
|
mkdir -p "$cache_path/commands"
|
||||||
cp -r "$INSTALL_DIR/"* "$cache_path/" 2>/dev/null || true
|
cp -r "$INSTALL_DIR/"* "$cache_path/" 2>/dev/null || true
|
||||||
|
|
||||||
# Register in installed_plugins.json
|
# Register in installed_plugins.json
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ if command -v jq &> /dev/null; then
|
|||||||
# Ensure cache directory exists and copy plugin files
|
# Ensure cache directory exists and copy plugin files
|
||||||
mkdir -p "$CACHE_PATH/.claude-plugin"
|
mkdir -p "$CACHE_PATH/.claude-plugin"
|
||||||
mkdir -p "$CACHE_PATH/hooks"
|
mkdir -p "$CACHE_PATH/hooks"
|
||||||
|
mkdir -p "$CACHE_PATH/commands"
|
||||||
|
|
||||||
# Copy files from marketplace to cache
|
# Copy files from marketplace to cache
|
||||||
cp -r "$MARKETPLACE_PATH/"* "$CACHE_PATH/" 2>/dev/null || true
|
cp -r "$MARKETPLACE_PATH/"* "$CACHE_PATH/" 2>/dev/null || true
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.11-2-g8ff0873-dirty",
|
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.11-2-g8ff0873-dirty",
|
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.11-2-g8ff0873-dirty",
|
"version": "v0.6.15-2-gd8a4939-dirty",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+25
-1
@@ -7,7 +7,7 @@ import Timeline from '@/components/Timeline.vue'
|
|||||||
import Sidebar from '@/components/Sidebar.vue'
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
const { isConnected, isProcessing, queueDepth } = useSSE()
|
const { isConnected, isReconnecting, reconnectCountdown, isProcessing, queueDepth } = useSSE()
|
||||||
const { stats } = useStats()
|
const { stats } = useStats()
|
||||||
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
||||||
const { health } = useHealth()
|
const { health } = useHealth()
|
||||||
@@ -33,6 +33,17 @@ const {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
|
<!-- Reconnection Banner -->
|
||||||
|
<Transition name="slide">
|
||||||
|
<div
|
||||||
|
v-if="isReconnecting"
|
||||||
|
class="fixed top-0 left-0 right-0 z-50 bg-amber-500/90 backdrop-blur-sm text-black px-4 py-2 text-center text-sm font-medium shadow-lg"
|
||||||
|
>
|
||||||
|
<i class="fas fa-sync-alt fa-spin mr-2" />
|
||||||
|
Connection lost. Reconnecting<span v-if="reconnectCountdown > 0"> in {{ reconnectCountdown }}s</span>...
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<Header
|
<Header
|
||||||
:is-connected="isConnected"
|
:is-connected="isConnected"
|
||||||
@@ -94,3 +105,16 @@ const {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.slide-enter-active,
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-enter-from,
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import type { SSEEvent } from '@/types'
|
|||||||
|
|
||||||
// Singleton state - shared across all useSSE() calls
|
// Singleton state - shared across all useSSE() calls
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
|
const isReconnecting = ref(false)
|
||||||
|
const reconnectCountdown = ref(0)
|
||||||
const isProcessing = ref(false)
|
const isProcessing = ref(false)
|
||||||
const queueDepth = ref(0)
|
const queueDepth = ref(0)
|
||||||
const lastEvent = ref<SSEEvent | null>(null)
|
const lastEvent = ref<SSEEvent | null>(null)
|
||||||
|
|
||||||
let eventSource: EventSource | null = null
|
let eventSource: EventSource | null = null
|
||||||
let reconnectTimeout: number | null = null
|
let reconnectTimeout: number | null = null
|
||||||
|
let countdownInterval: number | null = null
|
||||||
let connectionCount = 0
|
let connectionCount = 0
|
||||||
let reconnectAttempt = 0
|
let reconnectAttempt = 0
|
||||||
|
|
||||||
@@ -24,6 +27,28 @@ function getBackoffDelay(): number {
|
|||||||
return Math.floor(baseDelay + jitter)
|
return Math.floor(baseDelay + jitter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startCountdown(delayMs: number) {
|
||||||
|
reconnectCountdown.value = Math.ceil(delayMs / 1000)
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval)
|
||||||
|
}
|
||||||
|
countdownInterval = window.setInterval(() => {
|
||||||
|
reconnectCountdown.value = Math.max(0, reconnectCountdown.value - 1)
|
||||||
|
if (reconnectCountdown.value <= 0 && countdownInterval) {
|
||||||
|
clearInterval(countdownInterval)
|
||||||
|
countdownInterval = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCountdown() {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval)
|
||||||
|
countdownInterval = null
|
||||||
|
}
|
||||||
|
reconnectCountdown.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
export function useSSE() {
|
export function useSSE() {
|
||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
@@ -36,6 +61,8 @@ export function useSSE() {
|
|||||||
|
|
||||||
eventSource.onopen = () => {
|
eventSource.onopen = () => {
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
|
isReconnecting.value = false
|
||||||
|
stopCountdown()
|
||||||
reconnectAttempt = 0 // Reset backoff on successful connection
|
reconnectAttempt = 0 // Reset backoff on successful connection
|
||||||
console.log('[SSE] Connected')
|
console.log('[SSE] Connected')
|
||||||
}
|
}
|
||||||
@@ -62,6 +89,7 @@ export function useSSE() {
|
|||||||
|
|
||||||
eventSource.onerror = () => {
|
eventSource.onerror = () => {
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
|
isReconnecting.value = true
|
||||||
eventSource?.close()
|
eventSource?.close()
|
||||||
eventSource = null
|
eventSource = null
|
||||||
|
|
||||||
@@ -70,6 +98,7 @@ export function useSSE() {
|
|||||||
reconnectAttempt++
|
reconnectAttempt++
|
||||||
console.log(`[SSE] Reconnecting in ${Math.round(delay/1000)}s (attempt ${reconnectAttempt})`)
|
console.log(`[SSE] Reconnecting in ${Math.round(delay/1000)}s (attempt ${reconnectAttempt})`)
|
||||||
|
|
||||||
|
startCountdown(delay)
|
||||||
reconnectTimeout = window.setTimeout(() => {
|
reconnectTimeout = window.setTimeout(() => {
|
||||||
connect()
|
connect()
|
||||||
}, delay)
|
}, delay)
|
||||||
@@ -81,11 +110,13 @@ export function useSSE() {
|
|||||||
clearTimeout(reconnectTimeout)
|
clearTimeout(reconnectTimeout)
|
||||||
reconnectTimeout = null
|
reconnectTimeout = null
|
||||||
}
|
}
|
||||||
|
stopCountdown()
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close()
|
eventSource.close()
|
||||||
eventSource = null
|
eventSource = null
|
||||||
}
|
}
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
|
isReconnecting.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle page unload/refresh to ensure SSE connection is closed immediately
|
// Handle page unload/refresh to ensure SSE connection is closed immediately
|
||||||
@@ -132,6 +163,8 @@ export function useSSE() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isConnected,
|
isConnected,
|
||||||
|
isReconnecting,
|
||||||
|
reconnectCountdown,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
queueDepth,
|
queueDepth,
|
||||||
lastEvent,
|
lastEvent,
|
||||||
|
|||||||
Reference in New Issue
Block a user