Electron 44 + TypeScript, drei Prozesse (haupt / kern als utilityProcess / fenster mit React), Nachrichten-Schema mit Pruef-Funktionen, node:sqlite als einzige Datenbank-Stelle (kern/speicher/db.ts, § 6.7), Prozess-Leine im Haupt (Job Object aus beweise/leine.js — Begruendung fuer den Ort im Dateikopf), MessagePort direkt Fenster<->Kern, Einzelinstanz-Sperre, Kern-Neustart-Wache, strikte CSP im gebauten Fenster. Wächter-Tests nach § 4.3: koffi nur an zwei benannten Orten (R1), kein leerer catch und kein catch-mit-Leerwert (R2/R4), kein HTTP-Server (§ 4.1), node:sqlite nur in db.ts (§ 6.7). Dazu Datenbank- und Schema-Tests: 18/18 gruen, Typpruefung in drei Kontexten. Der W-0-Beweis (npm run smoke, echtes Programm): SMOKE: OK — fenster=geladen kern=pid:13676,node:24.18.1 datenbank=ok leine=gesetzt pong=ok version=5.0.0-w0 Nachgemessen ausserdem: taskkill /F auf den Haupt-Prozess (ohne /T, der Absturz-Fall aus rc10) — der Kern stirbt mit. Bau-Fallen dokumentiert (BAUEN.md): npm 11 blockt Electrons Install-Skript (§ 3.5), plugin-react 6 verlangt Vite 8 (deshalb 5.2), TypeScript bewusst auf 5.9.3 gepinnt statt tsgo 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
// Hält den EINEN MessagePort zum Kern — bewusst außerhalb von React:
|
|
// Die Port-Übergabe vom Haupt passiert genau einmal je Fenster-Ladung;
|
|
// React-Re-Mounts (StrictMode im Dev, HMR) dürfen sie nicht verlieren.
|
|
//
|
|
// R2 gilt auch hier: Der letzte bekannte Kern-Stand bleibt stehen und wird
|
|
// jedem neuen Abonnenten sofort wiedergegeben — er wird nie auf »leer«
|
|
// zurückgesetzt, nur weil gerade keine Nachricht kam.
|
|
import { istKernNachricht, type KernNachricht } from '../gemeinsam/nachrichten'
|
|
|
|
export interface Verbindung {
|
|
verbunden: boolean
|
|
kern: Extract<KernNachricht, { art: 'kern-bereit' }> | null
|
|
pongLatenzMs: number | null
|
|
}
|
|
|
|
type Horcher = (stand: Verbindung) => void
|
|
|
|
const stand: Verbindung = { verbunden: false, kern: null, pongLatenzMs: null }
|
|
const horcher = new Set<Horcher>()
|
|
let gestartet = false
|
|
|
|
function melden(): void {
|
|
for (const h of horcher) h({ ...stand })
|
|
}
|
|
|
|
function portAnnehmen(port: MessagePort): void {
|
|
stand.verbunden = true
|
|
port.onmessage = (ereignis: MessageEvent) => {
|
|
const nachricht: unknown = ereignis.data
|
|
if (!istKernNachricht(nachricht)) return
|
|
if (nachricht.art === 'kern-bereit') {
|
|
stand.kern = nachricht
|
|
} else if (nachricht.art === 'pong') {
|
|
stand.pongLatenzMs = Math.max(0, Date.now() - nachricht.zeit)
|
|
// Der Beweis für den Smoke-Lauf: Ping ging hin, Pong kam zurück.
|
|
window.rippy.melden('kern-pong')
|
|
}
|
|
melden()
|
|
}
|
|
port.start()
|
|
port.postMessage({ art: 'ping', zeit: Date.now() })
|
|
melden()
|
|
}
|
|
|
|
/** Idempotent — der erste Aufruf registriert den Horcher und meldet der
|
|
* Brücke, dass gepufferte Ports jetzt zugestellt werden können. */
|
|
export function verbindungStarten(): void {
|
|
if (gestartet) return
|
|
gestartet = true
|
|
window.addEventListener('message', (ereignis: MessageEvent) => {
|
|
const daten: unknown = ereignis.data
|
|
if (
|
|
typeof daten === 'object' &&
|
|
daten !== null &&
|
|
(daten as Record<string, unknown>).art === 'kern-port' &&
|
|
ereignis.ports.length > 0
|
|
) {
|
|
portAnnehmen(ereignis.ports[0])
|
|
}
|
|
})
|
|
window.rippy.bereit()
|
|
}
|
|
|
|
export function aufVerbindung(h: Horcher): () => void {
|
|
horcher.add(h)
|
|
h({ ...stand })
|
|
return () => horcher.delete(h)
|
|
}
|