<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value="server/todos.json
">
<input type="hidden" name="project[files][CHANGELOG.md]" value="# offline-first-electron

## 1.0.1

### Patch Changes

- Updated dependencies [[`85f5435`](https://github.com/TanStack/db/commit/85f54355a426baefc88ccc55179e0cfcb4dac168), [`e21ef32`](https://github.com/TanStack/db/commit/e21ef3246e80decbf57134defbc0e41230363360), [`d351c67`](https://github.com/TanStack/db/commit/d351c677d687e667450138f66ab3bd0e11e7e347), [`3d65bb1`](https://github.com/TanStack/db/commit/3d65bb149bfc544346b1bf3113f7fdfde6ea3a68), [`1654c41`](https://github.com/TanStack/db/commit/1654c41759e1caabcd5ddea8a433f0634be60f86)]:
  - @tanstack/offline-transactions@1.0.25
  - @tanstack/query-db-collection@1.0.31
  - @tanstack/node-db-sqlite-persistence@0.1.1
  - @tanstack/electron-db-sqlite-persistence@0.1.1
  - @tanstack/react-db@0.1.78
">
<input type="hidden" name="project[files][index.html]" value="&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;TanStack DB – Electron Offline-First&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/src/main.tsx&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;offline-first-electron&quot;,&quot;version&quot;:&quot;1.0.1&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;main&quot;:&quot;electron/main.ts&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;pnpm dev:renderer\&quot; \&quot;pnpm dev:server\&quot; \&quot;pnpm dev:electron\&quot;&quot;,&quot;dev:renderer&quot;:&quot;vite&quot;,&quot;dev:server&quot;:&quot;tsx server/index.ts&quot;,&quot;dev:electron&quot;:&quot;wait-on http://localhost:5173 &amp;&amp; electron .&quot;,&quot;server&quot;:&quot;tsx server/index.ts&quot;,&quot;postinstall&quot;:&quot;prebuild-install --runtime electron --target 40.2.1 --arch arm64 || echo &#39;prebuild-install failed, try: npx @electron/rebuild&#39;&quot;},&quot;dependencies&quot;:{&quot;@tanstack/electron-db-sqlite-persistence&quot;:&quot;https://pkg.pr.new/TanStack/db/@tanstack/electron-db-sqlite-persistence@f2821a4&quot;,&quot;@tanstack/node-db-sqlite-persistence&quot;:&quot;https://pkg.pr.new/TanStack/db/@tanstack/node-db-sqlite-persistence@f2821a4&quot;,&quot;@tanstack/offline-transactions&quot;:&quot;https://pkg.pr.new/TanStack/db/@tanstack/offline-transactions@f2821a4&quot;,&quot;@tanstack/query-db-collection&quot;:&quot;https://pkg.pr.new/TanStack/db/@tanstack/query-db-collection@f2821a4&quot;,&quot;@tanstack/react-db&quot;:&quot;https://pkg.pr.new/TanStack/db/@tanstack/react-db@f2821a4&quot;,&quot;@tanstack/react-query&quot;:&quot;^5.90.20&quot;,&quot;better-sqlite3&quot;:&quot;^12.6.2&quot;,&quot;react&quot;:&quot;^19.2.4&quot;,&quot;react-dom&quot;:&quot;^19.2.4&quot;,&quot;zod&quot;:&quot;^3.25.76&quot;},&quot;devDependencies&quot;:{&quot;@electron/rebuild&quot;:&quot;^3.7.1&quot;,&quot;@types/better-sqlite3&quot;:&quot;^7.6.13&quot;,&quot;@types/cors&quot;:&quot;^2.8.19&quot;,&quot;@types/express&quot;:&quot;^5.0.6&quot;,&quot;@types/react&quot;:&quot;^19.2.13&quot;,&quot;@types/react-dom&quot;:&quot;^19.2.3&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^5.1.3&quot;,&quot;concurrently&quot;:&quot;^9.2.1&quot;,&quot;cors&quot;:&quot;^2.8.6&quot;,&quot;electron&quot;:&quot;^40.2.1&quot;,&quot;express&quot;:&quot;^5.2.1&quot;,&quot;tsx&quot;:&quot;^4.21.0&quot;,&quot;typescript&quot;:&quot;^5.9.2&quot;,&quot;vite&quot;:&quot;^7.3.0&quot;,&quot;wait-on&quot;:&quot;^8.0.3&quot;}}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2020&quot;,
    &quot;useDefineForClassFields&quot;: true,
    &quot;lib&quot;: [&quot;ES2020&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;skipLibCheck&quot;: true,
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;allowImportingTsExtensions&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;moduleDetection&quot;: &quot;force&quot;,
    &quot;noEmit&quot;: true,
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;strict&quot;: true,
    &quot;noUnusedLocals&quot;: false,
    &quot;noUnusedParameters&quot;: false
  },
  &quot;include&quot;: [&quot;src&quot;, &quot;electron&quot;, &quot;server&quot;, &quot;vite.config.ts&quot;]
}
">
<input type="hidden" name="project[files][tsconfig.node.json]" value="{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;skipLibCheck&quot;: true,
    &quot;strict&quot;: true,
    &quot;noEmit&quot;: true
  },
  &quot;include&quot;: [&quot;electron&quot;, &quot;server&quot;, &quot;vite.config.ts&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import { defineConfig } from &#39;vite&#39;
import react from &#39;@vitejs/plugin-react&#39;

export default defineConfig({
  plugins: [react()],
  base: &#39;./&#39;,
})
">
<input type="hidden" name="project[files][electron/main.ts]" value="import path from &#39;node:path&#39;
import { fileURLToPath } from &#39;node:url&#39;
import { BrowserWindow, Menu, app, ipcMain } from &#39;electron&#39;
import Database from &#39;better-sqlite3&#39;
import { createNodeSQLitePersistence } from &#39;@tanstack/node-db-sqlite-persistence&#39;
import { exposeElectronSQLitePersistence } from &#39;@tanstack/electron-db-sqlite-persistence&#39;

const __dirname = path.dirname(fileURLToPath(import.meta.url))

// Open SQLite database in Electron&#39;s user data directory
const dbPath = path.join(app.getPath(&#39;userData&#39;), &#39;todos.sqlite&#39;)
console.log(`[Main] SQLite database path: ${dbPath}`)

const database = new Database(dbPath)

// Create persistence adapter from better-sqlite3 database
const persistence = createNodeSQLitePersistence({ database })

// Expose persistence over IPC so the renderer can use it
exposeElectronSQLitePersistence({ ipcMain, persistence })

// ── Key-value store for offline transaction outbox ──
// Uses a simple SQLite table so pending mutations survive app restarts.
database.exec(`
  CREATE TABLE IF NOT EXISTS kv_store (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL
  )
`)

ipcMain.handle(&#39;kv:get&#39;, (_e, key: string) =&gt; {
  const row = database
    .prepare(&#39;SELECT value FROM kv_store WHERE key = ?&#39;)
    .get(key) as { value: string } | undefined
  console.log(
    `[KV] get &quot;${key}&quot; → ${row ? `found (${row.value.length} chars)` : &#39;null&#39;}`,
  )
  return row?.value ?? null
})

ipcMain.handle(&#39;kv:set&#39;, (_e, key: string, value: string) =&gt; {
  console.log(`[KV] set &quot;${key}&quot; (${value.length} chars)`)
  database
    .prepare(
      &#39;INSERT INTO kv_store (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value&#39;,
    )
    .run(key, value)
})

ipcMain.handle(&#39;kv:delete&#39;, (_e, key: string) =&gt; {
  console.log(`[KV] delete &quot;${key}&quot;`)
  database.prepare(&#39;DELETE FROM kv_store WHERE key = ?&#39;).run(key)
})

ipcMain.handle(&#39;kv:keys&#39;, () =&gt; {
  const rows = database.prepare(&#39;SELECT key FROM kv_store&#39;).all() as Array&lt;{
    key: string
  }&gt;
  console.log(`[KV] keys → [${rows.map((r) =&gt; `&quot;${r.key}&quot;`).join(&#39;, &#39;)}]`)
  return rows.map((r) =&gt; r.key)
})

ipcMain.handle(&#39;kv:clear&#39;, () =&gt; {
  database.exec(&#39;DELETE FROM kv_store&#39;)
})

// Reset: drop all tables from the SQLite database
ipcMain.handle(&#39;tanstack-db:reset-database&#39;, () =&gt; {
  const tables = database
    .prepare(&quot;SELECT name FROM sqlite_master WHERE type=&#39;table&#39;&quot;)
    .all() as Array&lt;{ name: string }&gt;
  for (const { name } of tables) {
    database.prepare(`DROP TABLE IF EXISTS &quot;${name}&quot;`).run()
  }
  console.log(&#39;[Main] Database reset — all tables dropped&#39;)
})

function createWindow() {
  const preloadPath = path.join(__dirname, &#39;preload.cjs&#39;)

  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      contextIsolation: true,
      nodeIntegration: false,
      preload: preloadPath,
    },
  })

  // Dev: load Vite dev server. Prod: load built files.
  if (process.env.NODE_ENV !== &#39;production&#39;) {
    win.loadURL(&#39;http://localhost:5173&#39;)
  } else {
    win.loadFile(path.join(__dirname, &#39;..&#39;, &#39;dist&#39;, &#39;index.html&#39;))
  }
}

app.whenReady().then(() =&gt; {
  // Add a menu with &quot;New Window&quot; so cross-window sync can be tested.
  // BroadcastChannel only works between windows in the same Electron process,
  // so opening a second `electron .` process won&#39;t sync — use this menu instead.
  const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [{ role: &#39;quit&#39; }],
    },
    {
      label: &#39;File&#39;,
      submenu: [
        {
          label: &#39;New Window&#39;,
          accelerator: &#39;CmdOrCtrl+N&#39;,
          click: () =&gt; createWindow(),
        },
        { role: &#39;close&#39; },
      ],
    },
    {
      label: &#39;View&#39;,
      submenu: [
        {
          label: &#39;Toggle DevTools&#39;,
          accelerator: &#39;CmdOrCtrl+Shift+I&#39;,
          click: (_item, win) =&gt; win?.webContents.toggleDevTools(),
        },
        { role: &#39;reload&#39; },
        { role: &#39;forceReload&#39; },
      ],
    },
    {
      label: &#39;Edit&#39;,
      submenu: [
        { role: &#39;undo&#39; },
        { role: &#39;redo&#39; },
        { type: &#39;separator&#39; },
        { role: &#39;cut&#39; },
        { role: &#39;copy&#39; },
        { role: &#39;paste&#39; },
        { role: &#39;selectAll&#39; },
      ],
    },
  ])
  Menu.setApplicationMenu(menu)

  createWindow()
})

app.on(&#39;window-all-closed&#39;, () =&gt; {
  if (process.platform !== &#39;darwin&#39;) {
    app.quit()
  }
})

app.on(&#39;activate&#39;, () =&gt; {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

app.on(&#39;before-quit&#39;, () =&gt; {
  database.close()
})
">
<input type="hidden" name="project[files][electron/preload.cjs]" value="const { contextBridge, ipcRenderer } = require(&#39;electron&#39;)

contextBridge.exposeInMainWorld(&#39;electronAPI&#39;, {
  invoke: (channel, ...args) =&gt; ipcRenderer.invoke(channel, ...args),
  resetDatabase: () =&gt; ipcRenderer.invoke(&#39;tanstack-db:reset-database&#39;),
  kv: {
    get: (key) =&gt; ipcRenderer.invoke(&#39;kv:get&#39;, key),
    set: (key, value) =&gt; ipcRenderer.invoke(&#39;kv:set&#39;, key, value),
    delete: (key) =&gt; ipcRenderer.invoke(&#39;kv:delete&#39;, key),
    keys: () =&gt; ipcRenderer.invoke(&#39;kv:keys&#39;),
    clear: () =&gt; ipcRenderer.invoke(&#39;kv:clear&#39;),
  },
})
">
<input type="hidden" name="project[files][server/index.ts]" value="import fs from &#39;node:fs&#39;
import path from &#39;node:path&#39;
import { fileURLToPath } from &#39;node:url&#39;
import cors from &#39;cors&#39;
import express from &#39;express&#39;

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const app = express()
const PORT = 3001

app.use(cors())
app.use(express.json())

// Types
interface Todo {
  id: string
  text: string
  completed: boolean
  createdAt: string
  updatedAt: string
}

// Persist server state to a JSON file so data survives restarts
const TODOS_FILE = path.join(__dirname, &#39;todos.json&#39;)

function loadTodos(): Map&lt;string, Todo&gt; {
  try {
    const data = JSON.parse(fs.readFileSync(TODOS_FILE, &#39;utf-8&#39;)) as Array&lt;Todo&gt;
    return new Map(data.map((t) =&gt; [t.id, t]))
  } catch {
    return new Map()
  }
}

function saveTodos() {
  fs.writeFileSync(
    TODOS_FILE,
    JSON.stringify(Array.from(todosStore.values()), null, 2),
  )
}

const todosStore = loadTodos()

// Helper function to generate IDs
function generateId(): string {
  return Math.random().toString(36).substring(2) + Date.now().toString(36)
}

// Simulate network delay
const delay = (ms: number) =&gt; new Promise((resolve) =&gt; setTimeout(resolve, ms))

// GET all todos
app.get(&#39;/api/todos&#39;, async (_req, res) =&gt; {
  console.log(&#39;GET /api/todos&#39;)
  await delay(200)
  const todos = Array.from(todosStore.values()).sort(
    (a, b) =&gt; new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
  )
  res.json(todos)
})

// POST create todo — accepts client-generated ID
app.post(&#39;/api/todos&#39;, async (req, res) =&gt; {
  console.log(&#39;POST /api/todos&#39;, req.body)
  await delay(200)

  const { id, text, completed } = req.body
  if (!text || text.trim() === &#39;&#39;) {
    return res.status(400).json({ error: &#39;Todo text is required&#39; })
  }

  const now = new Date().toISOString()
  const todo: Todo = {
    id: id || generateId(),
    text,
    completed: completed ?? false,
    createdAt: now,
    updatedAt: now,
  }
  todosStore.set(todo.id, todo)
  saveTodos()
  res.status(201).json(todo)
})

// PUT update todo
app.put(&#39;/api/todos/:id&#39;, async (req, res) =&gt; {
  console.log(&#39;PUT /api/todos/&#39; + req.params.id, req.body)
  await delay(200)

  const existing = todosStore.get(req.params.id)
  if (!existing) {
    return res.status(404).json({ error: &#39;Todo not found&#39; })
  }

  const updated: Todo = {
    ...existing,
    ...req.body,
    updatedAt: new Date().toISOString(),
  }
  todosStore.set(req.params.id, updated)
  saveTodos()
  res.json(updated)
})

// DELETE todo
app.delete(&#39;/api/todos/:id&#39;, async (req, res) =&gt; {
  console.log(&#39;DELETE /api/todos/&#39; + req.params.id)
  await delay(200)

  if (!todosStore.delete(req.params.id)) {
    return res.status(404).json({ error: &#39;Todo not found&#39; })
  }
  saveTodos()
  res.json({ success: true })
})

// DELETE all todos
app.delete(&#39;/api/todos&#39;, async (_req, res) =&gt; {
  console.log(&#39;DELETE /api/todos (clear all)&#39;)
  await delay(200)
  todosStore.clear()
  saveTodos()
  res.json({ success: true })
})

app.listen(PORT, &#39;0.0.0.0&#39;, () =&gt; {
  console.log(`Server running at http://0.0.0.0:${PORT}`)
})
">
<input type="hidden" name="project[files][src/App.css]" value="* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family:
    -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;
  background: #f5f5f5;
  color: #1a1a1a;
}

.container {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem 1rem;
}

h1 {
  font-size: 1.5rem;
  margin-bottom: 1rem;
  color: #111;
}

.status-bar {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.625rem;
  border-radius: 9999px;
  font-size: 0.75rem;
  font-weight: 600;
}

.badge-online {
  background: #dcfce7;
  color: #166534;
}

.badge-offline {
  background: #fee2e2;
  color: #991b1b;
}

.badge-pending {
  background: #fef3c7;
  color: #92400e;
}

.badge-count {
  background: #e0e7ff;
  color: #3730a3;
}

.badge-offline-enabled {
  background: #dbeafe;
  color: #1e40af;
}

.badge-offline-disabled {
  background: #f3f4f6;
  color: #6b7280;
}

.error-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.75rem;
  margin-bottom: 1rem;
  background: #fee2e2;
  border: 1px solid #fecaca;
  border-radius: 0.375rem;
  font-size: 0.8rem;
  color: #991b1b;
}

.error-bar button {
  background: none;
  border: none;
  color: #991b1b;
  font-size: 1rem;
  cursor: pointer;
  padding: 0 0.25rem;
}

.reset-btn {
  margin-left: auto;
  padding: 0.25rem 0.75rem;
  background: white;
  color: #dc2626;
  border: 1px solid #dc2626;
  border-radius: 0.375rem;
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
}

.reset-btn:hover {
  background: #dc2626;
  color: white;
}

.input-row {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.todo-input {
  flex: 1;
  padding: 0.625rem 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  outline: none;
}

.todo-input:focus {
  border-color: #6366f1;
  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}

.add-btn {
  padding: 0.625rem 1rem;
  background: #6366f1;
  color: white;
  border: none;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.add-btn:hover:not(:disabled) {
  background: #4f46e5;
}

.add-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.todo-list {
  list-style: none;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 0.375rem;
  margin-bottom: 0.5rem;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #9ca3af;
}

.toggle-btn {
  width: 1.5rem;
  height: 1.5rem;
  border: 2px solid #d1d5db;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  color: #6366f1;
  flex-shrink: 0;
}

.completed .toggle-btn {
  background: #6366f1;
  border-color: #6366f1;
  color: white;
}

.todo-text {
  flex: 1;
  font-size: 0.875rem;
}

.delete-btn {
  background: none;
  border: none;
  color: #9ca3af;
  font-size: 1.25rem;
  cursor: pointer;
  padding: 0 0.25rem;
  line-height: 1;
}

.delete-btn:hover {
  color: #ef4444;
}

.empty-message {
  text-align: center;
  color: #9ca3af;
  padding: 2rem 0;
}

.instructions {
  margin-top: 2rem;
  padding: 1rem;
  background: #f0f0ff;
  border-radius: 0.5rem;
  font-size: 0.8rem;
  color: #444;
}

.instructions h3 {
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
}

.instructions ul {
  padding-left: 1.25rem;
}

.instructions li {
  margin-bottom: 0.25rem;
}
">
<input type="hidden" name="project[files][src/App.tsx]" value="import { useEffect, useRef, useState } from &#39;react&#39;
import { createTodos } from &#39;./db/todos&#39;
import { TodoList } from &#39;./components/TodoList&#39;
import &#39;./App.css&#39;

type TodosState = ReturnType&lt;typeof createTodos&gt;

export function App() {
  const [todosState, setTodosState] = useState&lt;TodosState | null&gt;(null)
  const [error, setError] = useState&lt;string | null&gt;(null)
  const initRef = useRef(false)

  useEffect(() =&gt; {
    // Prevent double-initialization from React StrictMode.
    // The offline executor acquires a Web Lock for leadership — disposing
    // and re-creating it in the StrictMode mount→cleanup→mount cycle
    // leaves a ghost lock that blocks the second executor from becoming leader.
    if (initRef.current) return
    initRef.current = true

    try {
      const state = createTodos()
      setTodosState(state)
    } catch (err) {
      console.error(&#39;Failed to initialize:&#39;, err)
      setError(err instanceof Error ? err.message : String(err))
    }
  }, [])

  if (error) {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;h1&gt;Initialization Error&lt;/h1&gt;
        &lt;p style={{ color: &#39;#ef4444&#39; }}&gt;{error}&lt;/p&gt;
      &lt;/div&gt;
    )
  }

  if (!todosState) {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;p&gt;Loading...&lt;/p&gt;
      &lt;/div&gt;
    )
  }

  return (
    &lt;TodoList
      collection={todosState.collection}
      executor={todosState.executor}
    /&gt;
  )
}
">
<input type="hidden" name="project[files][src/main.tsx]" value="import { StrictMode } from &#39;react&#39;
import { createRoot } from &#39;react-dom/client&#39;
import { QueryClientProvider } from &#39;@tanstack/react-query&#39;
import { queryClient } from &#39;./utils/queryClient&#39;
import { App } from &#39;./App&#39;

createRoot(document.getElementById(&#39;root&#39;)!).render(
  &lt;StrictMode&gt;
    &lt;QueryClientProvider client={queryClient}&gt;
      &lt;App /&gt;
    &lt;/QueryClientProvider&gt;
  &lt;/StrictMode&gt;,
)
">
<input type="hidden" name="project[files][src/db/todos.ts]" value="import { createCollection } from &#39;@tanstack/react-db&#39;
import { queryCollectionOptions } from &#39;@tanstack/query-db-collection&#39;
import { startOfflineExecutor } from &#39;@tanstack/offline-transactions&#39;
import {
  ElectronCollectionCoordinator,
  createElectronSQLitePersistence,
  persistedCollectionOptions,
} from &#39;@tanstack/electron-db-sqlite-persistence&#39;
import { z } from &#39;zod&#39;
import { queryClient } from &#39;../utils/queryClient&#39;
import type { StorageAdapter } from &#39;@tanstack/offline-transactions&#39;
import type { Todo } from &#39;../utils/api&#39;
import type { PendingMutation } from &#39;@tanstack/db&#39;

// Declare the electronAPI exposed via preload
declare global {
  interface Window {
    electronAPI: {
      invoke: (channel: string, ...args: Array&lt;unknown&gt;) =&gt; Promise&lt;unknown&gt;
      resetDatabase: () =&gt; Promise&lt;void&gt;
      kv: {
        get: (key: string) =&gt; Promise&lt;string | null&gt;
        set: (key: string, value: string) =&gt; Promise&lt;void&gt;
        delete: (key: string) =&gt; Promise&lt;void&gt;
        keys: () =&gt; Promise&lt;Array&lt;string&gt;&gt;
        clear: () =&gt; Promise&lt;void&gt;
      }
    }
  }
}

/**
 * SQLite-backed storage adapter for the offline transactions outbox.
 * Stores pending mutations in the main process SQLite database via IPC,
 * so they survive app restarts (unlike IndexedDB in Electron).
 */
class ElectronSQLiteStorageAdapter implements StorageAdapter {
  private prefix: string

  constructor(prefix = &#39;offline-tx:&#39;) {
    this.prefix = prefix
  }

  private prefixedKey(key: string): string {
    return `${this.prefix}${key}`
  }

  async get(key: string): Promise&lt;string | null&gt; {
    return window.electronAPI.kv.get(this.prefixedKey(key))
  }

  async set(key: string, value: string): Promise&lt;void&gt; {
    await window.electronAPI.kv.set(this.prefixedKey(key), value)
  }

  async delete(key: string): Promise&lt;void&gt; {
    await window.electronAPI.kv.delete(this.prefixedKey(key))
  }

  async keys(): Promise&lt;Array&lt;string&gt;&gt; {
    const allKeys = await window.electronAPI.kv.keys()
    return allKeys
      .filter((k) =&gt; k.startsWith(this.prefix))
      .map((k) =&gt; k.slice(this.prefix.length))
  }

  async clear(): Promise&lt;void&gt; {
    const keys = await this.keys()
    for (const key of keys) {
      await window.electronAPI.kv.delete(this.prefixedKey(key))
    }
  }
}

/**
 * Fetch with retry and exponential backoff.
 * Keeps retrying on network errors and non-OK responses so the app
 * degrades gracefully when the server is temporarily unreachable.
 */
async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  retryConfig: { retries?: number; delay?: number; backoff?: number } = {},
): Promise&lt;Response&gt; {
  const { retries = 6, delay = 1000, backoff = 2 } = retryConfig

  for (let i = 0; i &lt;= retries; i++) {
    try {
      const response = await fetch(url, options)
      if (response.ok) return response

      console.warn(
        `Fetch attempt ${i + 1} failed with status: ${response.status}. Retrying...`,
      )
    } catch (error) {
      console.error(
        `Fetch attempt ${i + 1} failed due to a network error:`,
        error,
      )
      if (i &gt;= retries) throw error
    }

    if (i &lt; retries) {
      const currentDelay = delay * Math.pow(backoff, i)
      await new Promise((resolve) =&gt; setTimeout(resolve, currentDelay))
    }
  }

  throw new Error(`Failed to fetch ${url} after ${retries} retries.`)
}

// Schema — use ISO strings for dates (SQLite-friendly)
const todoSchema = z.object({
  id: z.string(),
  text: z.string(),
  completed: z.boolean(),
  createdAt: z.string(),
  updatedAt: z.string(),
})

// Multi-window coordinator: uses BroadcastChannel + Web Locks for
// leader election and cross-window notification (both available in Electron)
const coordinator = new ElectronCollectionCoordinator({
  dbName: &#39;electron-todos&#39;,
})

// Create persistence via IPC bridge to the main process SQLite database
const persistence = createElectronSQLitePersistence({
  invoke: window.electronAPI.invoke,
  coordinator,
})

const BASE_URL = &#39;http://localhost:3001&#39;

// Compose: query sync wrapped in persisted collection
const queryOpts = queryCollectionOptions({
  id: &#39;todos-collection&#39;,
  queryClient,
  queryKey: [&#39;todos&#39;],
  queryFn: async (): Promise&lt;Array&lt;Todo&gt;&gt; =&gt; {
    const response = await fetchWithRetry(`${BASE_URL}/api/todos`)
    if (!response.ok) {
      throw new Error(`Failed to fetch todos: ${response.status}`)
    }
    return response.json()
  },
  getKey: (item) =&gt; item.id,
  schema: todoSchema,
  refetchInterval: 3000,
})

// Sync function to push mutations to the backend
async function syncTodos({
  transaction,
  idempotencyKey,
}: {
  transaction: { mutations: Array&lt;PendingMutation&gt; }
  idempotencyKey: string
}) {
  const mutations = transaction.mutations

  console.log(`[Sync] Processing ${mutations.length} mutations`, idempotencyKey)

  for (const mutation of mutations) {
    try {
      switch (mutation.type) {
        case &#39;insert&#39;: {
          const todoData = mutation.modified as Todo
          const response = await fetchWithRetry(`${BASE_URL}/api/todos`, {
            method: &#39;POST&#39;,
            headers: {
              &#39;Content-Type&#39;: &#39;application/json&#39;,
              &#39;Idempotency-Key&#39;: idempotencyKey,
            },
            body: JSON.stringify({
              id: todoData.id,
              text: todoData.text,
              completed: todoData.completed,
            }),
          })
          if (!response.ok) {
            throw new Error(`Failed to sync insert: ${response.statusText}`)
          }
          break
        }

        case &#39;update&#39;: {
          const todoData = mutation.modified as Partial&lt;Todo&gt;
          const id = (mutation.modified as Todo).id
          const response = await fetchWithRetry(`${BASE_URL}/api/todos/${id}`, {
            method: &#39;PUT&#39;,
            headers: {
              &#39;Content-Type&#39;: &#39;application/json&#39;,
              &#39;Idempotency-Key&#39;: idempotencyKey,
            },
            body: JSON.stringify({
              text: todoData.text,
              completed: todoData.completed,
            }),
          })
          if (!response.ok) {
            throw new Error(`Failed to sync update: ${response.statusText}`)
          }
          break
        }

        case &#39;delete&#39;: {
          const id = (mutation.original as Todo).id
          const response = await fetchWithRetry(`${BASE_URL}/api/todos/${id}`, {
            method: &#39;DELETE&#39;,
            headers: {
              &#39;Idempotency-Key&#39;: idempotencyKey,
            },
          })
          if (!response.ok) {
            throw new Error(`Failed to sync delete: ${response.statusText}`)
          }
          break
        }
      }
    } catch (error) {
      console.error(&#39;[Sync] Error syncing mutation:&#39;, mutation, error)
      throw error
    }
  }

  // Refresh the collection after sync
  await collection.utils.refetch()
}

// Create the persisted collection
const collection = createCollection(
  persistedCollectionOptions({
    ...queryOpts,
    persistence,
    schemaVersion: 1,
  }),
)

// Create todos setup: collection + offline executor
export function createTodos() {
  const executor = startOfflineExecutor({
    collections: { todos: collection },
    storage: new ElectronSQLiteStorageAdapter(&#39;offline-tx:&#39;),
    mutationFns: {
      syncTodos,
    },
    onLeadershipChange: (isLeader) =&gt; {
      console.log(&#39;[Offline] Leadership changed:&#39;, isLeader)
    },
    onStorageFailure: (diagnostic) =&gt; {
      console.warn(&#39;[Offline] Storage failure:&#39;, diagnostic)
    },
  })

  console.log(&#39;[Offline] Executor mode:&#39;, executor.mode)

  // Log when initialization completes and pending transactions are loaded
  executor
    .waitForInit()
    .then(() =&gt; {
      console.log(
        &#39;[Offline] Init complete. isOfflineEnabled:&#39;,
        executor.isOfflineEnabled,
      )
      console.log(&#39;[Offline] Pending count:&#39;, executor.getPendingCount())
    })
    .catch((err) =&gt; {
      console.error(&#39;[Offline] Init failed:&#39;, err)
    })

  return {
    collection,
    executor,
    close: () =&gt; {
      executor.dispose()
    },
  }
}

// Helper to create offline actions
export function createTodoActions(
  offline: ReturnType&lt;typeof createTodos&gt;[&#39;executor&#39;],
) {
  const addTodo = offline.createOfflineAction({
    mutationFnName: &#39;syncTodos&#39;,
    onMutate: (text: string) =&gt; {
      const now = new Date().toISOString()
      const newTodo: Todo = {
        id: crypto.randomUUID(),
        text: text.trim(),
        completed: false,
        createdAt: now,
        updatedAt: now,
      }
      collection.insert(newTodo)
      return newTodo
    },
  })

  const toggleTodo = offline.createOfflineAction({
    mutationFnName: &#39;syncTodos&#39;,
    onMutate: (id: string) =&gt; {
      const todo = collection.get(id)
      if (!todo) return
      collection.update(id, (draft) =&gt; {
        draft.completed = !draft.completed
        draft.updatedAt = new Date().toISOString()
      })
      return todo
    },
  })

  const deleteTodo = offline.createOfflineAction({
    mutationFnName: &#39;syncTodos&#39;,
    onMutate: (id: string) =&gt; {
      const todo = collection.get(id)
      if (todo) {
        collection.delete(id)
      }
      return todo
    },
  })

  return { addTodo, toggleTodo, deleteTodo }
}
">
<input type="hidden" name="project[files][src/utils/api.ts]" value="const BASE_URL = &#39;http://localhost:3001&#39;

export interface Todo {
  id: string
  text: string
  completed: boolean
  createdAt: string
  updatedAt: string
}

export const todoApi = {
  async getAll(): Promise&lt;Array&lt;Todo&gt;&gt; {
    const response = await fetch(`${BASE_URL}/api/todos`)
    if (!response.ok) {
      throw new Error(`Failed to fetch todos: ${response.status}`)
    }
    return response.json()
  },

  async create(data: {
    id: string
    text: string
    completed?: boolean
  }): Promise&lt;Todo&gt; {
    const response = await fetch(`${BASE_URL}/api/todos`, {
      method: &#39;POST&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify(data),
    })
    if (!response.ok) {
      throw new Error(`Failed to create todo: ${response.status}`)
    }
    return response.json()
  },

  async update(
    id: string,
    data: { text?: string; completed?: boolean },
  ): Promise&lt;Todo | null&gt; {
    const response = await fetch(`${BASE_URL}/api/todos/${id}`, {
      method: &#39;PUT&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify(data),
    })
    if (response.status === 404) return null
    if (!response.ok) {
      throw new Error(`Failed to update todo: ${response.status}`)
    }
    return response.json()
  },

  async delete(id: string): Promise&lt;boolean&gt; {
    const response = await fetch(`${BASE_URL}/api/todos/${id}`, {
      method: &#39;DELETE&#39;,
    })
    if (response.status === 404) return false
    if (!response.ok) {
      throw new Error(`Failed to delete todo: ${response.status}`)
    }
    return true
  },

  async deleteAll(): Promise&lt;void&gt; {
    const response = await fetch(`${BASE_URL}/api/todos`, {
      method: &#39;DELETE&#39;,
    })
    if (!response.ok) {
      throw new Error(`Failed to delete all todos: ${response.status}`)
    }
  },
}
">
<input type="hidden" name="project[files][src/utils/queryClient.ts]" value="import { QueryClient } from &#39;@tanstack/react-query&#39;

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      retry: 3,
    },
  },
})
">
<input type="hidden" name="project[files][src/components/TodoList.tsx]" value="import { useCallback, useEffect, useState } from &#39;react&#39;
import { useLiveQuery } from &#39;@tanstack/react-db&#39;
import { createTodoActions } from &#39;../db/todos&#39;
import { todoApi } from &#39;../utils/api&#39;
import type { createTodos } from &#39;../db/todos&#39;

interface TodoListProps {
  collection: ReturnType&lt;typeof createTodos&gt;[&#39;collection&#39;]
  executor: ReturnType&lt;typeof createTodos&gt;[&#39;executor&#39;]
}

export function TodoList({ collection, executor }: TodoListProps) {
  const [inputText, setInputText] = useState(&#39;&#39;)
  const [isOnline, setIsOnline] = useState(navigator.onLine)
  const [pendingCount, setPendingCount] = useState(0)
  const [error, setError] = useState&lt;string | null&gt;(null)
  const [actions] = useState(() =&gt; createTodoActions(executor))

  // Monitor network status
  useEffect(() =&gt; {
    const handleOnline = () =&gt; {
      setIsOnline(true)
      executor.notifyOnline()
    }
    const handleOffline = () =&gt; setIsOnline(false)

    window.addEventListener(&#39;online&#39;, handleOnline)
    window.addEventListener(&#39;offline&#39;, handleOffline)
    return () =&gt; {
      window.removeEventListener(&#39;online&#39;, handleOnline)
      window.removeEventListener(&#39;offline&#39;, handleOffline)
    }
  }, [executor])

  // Poll pending mutation count
  useEffect(() =&gt; {
    const interval = setInterval(() =&gt; {
      setPendingCount(executor.getPendingCount())
    }, 100)
    return () =&gt; clearInterval(interval)
  }, [executor])

  // Query all todos sorted by creation date
  const { data: todos = [], isLoading } = useLiveQuery((query) =&gt;
    query
      .from({ todo: collection })
      .orderBy(({ todo }) =&gt; todo.createdAt, &#39;desc&#39;),
  )

  const handleAddTodo = useCallback(() =&gt; {
    const text = inputText.trim()
    if (!text) return
    try {
      setError(null)
      actions.addTodo(text)
      setInputText(&#39;&#39;)
    } catch (err) {
      setError(err instanceof Error ? err.message : &#39;Failed to add todo&#39;)
    }
  }, [inputText, actions])

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent) =&gt; {
      if (e.key === &#39;Enter&#39;) handleAddTodo()
    },
    [handleAddTodo],
  )

  if (isLoading) {
    return (
      &lt;div className=&quot;container&quot;&gt;
        &lt;p&gt;Loading...&lt;/p&gt;
      &lt;/div&gt;
    )
  }

  return (
    &lt;div className=&quot;container&quot;&gt;
      &lt;h1&gt;TanStack DB – Electron Offline-First&lt;/h1&gt;

      &lt;div className=&quot;status-bar&quot;&gt;
        &lt;span
          className={`badge ${isOnline ? &#39;badge-online&#39; : &#39;badge-offline&#39;}`}
        &gt;
          {isOnline ? &#39;Online&#39; : &#39;Offline&#39;}
        &lt;/span&gt;
        &lt;span
          className={`badge ${executor.isOfflineEnabled ? &#39;badge-offline-enabled&#39; : &#39;badge-offline-disabled&#39;}`}
        &gt;
          {executor.isOfflineEnabled ? &#39;Offline Mode&#39; : &#39;Online Only&#39;}
        &lt;/span&gt;
        {pendingCount &gt; 0 &amp;&amp; (
          &lt;span className=&quot;badge badge-pending&quot;&gt;{pendingCount} pending&lt;/span&gt;
        )}
        &lt;span className=&quot;badge badge-count&quot;&gt;
          {todos.length} todo{todos.length !== 1 ? &#39;s&#39; : &#39;&#39;}
        &lt;/span&gt;
        &lt;button
          className=&quot;reset-btn&quot;
          onClick={async () =&gt; {
            if (
              !confirm(
                &#39;Reset everything? This clears all todos and local data.&#39;,
              )
            )
              return
            try {
              await todoApi.deleteAll()
            } catch {
              // server may be offline — continue with local reset
            }
            await window.electronAPI.resetDatabase()
            window.location.reload()
          }}
        &gt;
          Reset
        &lt;/button&gt;
      &lt;/div&gt;

      {error &amp;&amp; (
        &lt;div className=&quot;error-bar&quot;&gt;
          &lt;span&gt;{error}&lt;/span&gt;
          &lt;button onClick={() =&gt; setError(null)}&gt;×&lt;/button&gt;
        &lt;/div&gt;
      )}

      &lt;div className=&quot;input-row&quot;&gt;
        &lt;input
          type=&quot;text&quot;
          className=&quot;todo-input&quot;
          value={inputText}
          onChange={(e) =&gt; setInputText(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder=&quot;What needs to be done?&quot;
        /&gt;
        &lt;button
          className=&quot;add-btn&quot;
          onClick={handleAddTodo}
          disabled={!inputText.trim()}
        &gt;
          Add
        &lt;/button&gt;
      &lt;/div&gt;

      &lt;ul className=&quot;todo-list&quot;&gt;
        {todos.map((todo) =&gt; (
          &lt;li
            key={todo.id}
            className={`todo-item ${todo.completed ? &#39;completed&#39; : &#39;&#39;}`}
          &gt;
            &lt;button
              className=&quot;toggle-btn&quot;
              onClick={() =&gt; actions.toggleTodo(todo.id)}
            &gt;
              {todo.completed ? &#39;✓&#39; : &#39;○&#39;}
            &lt;/button&gt;
            &lt;span className=&quot;todo-text&quot;&gt;{todo.text}&lt;/span&gt;
            &lt;button
              className=&quot;delete-btn&quot;
              onClick={() =&gt; actions.deleteTodo(todo.id)}
            &gt;
              ×
            &lt;/button&gt;
          &lt;/li&gt;
        ))}
      &lt;/ul&gt;

      {todos.length === 0 &amp;&amp; (
        &lt;p className=&quot;empty-message&quot;&gt;No todos yet. Add one above!&lt;/p&gt;
      )}

      &lt;div className=&quot;instructions&quot;&gt;
        &lt;h3&gt;Try it out&lt;/h3&gt;
        &lt;ul&gt;
          &lt;li&gt;
            &lt;strong&gt;Persistence:&lt;/strong&gt; Add todos, quit the app, reopen — data
            is still there
          &lt;/li&gt;
          &lt;li&gt;
            &lt;strong&gt;Multiple windows:&lt;/strong&gt; Press Cmd+N (or File → New
            Window) to open a second window — each window syncs independently
            with the server
          &lt;/li&gt;
          &lt;li&gt;
            &lt;strong&gt;Offline mode:&lt;/strong&gt; Stop the server (
            &lt;code&gt;pnpm dev:server&lt;/code&gt;), add todos, restart — they sync
            automatically
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="offline-first-electron">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>