diff --git a/apps/cron/20260201-000000/bun.lock b/apps/cron/20260201-000000/bun.lock index 3b839ad..eae4803 100644 --- a/apps/cron/20260201-000000/bun.lock +++ b/apps/cron/20260201-000000/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", - "@because/toes": "^0.0.6", + "@because/toes": "^0.0.7", "croner": "^9.1.0", }, "devDependencies": { @@ -20,7 +20,9 @@ "@because/hype": ["@because/hype@0.0.2", "https://npm.nose.space/@because/hype/-/hype-0.0.2.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-fdKeII6USGC1loVVj+tPz086cKz+Bm+XozNee3NOnK4VP+q4yNPP2Fq1Yujw5xeDYE+ZvJn40gKwlngRvmX2hA=="], - "@because/toes": ["@because/toes@0.0.6", "https://npm.nose.space/@because/toes/-/toes-0.0.6.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", "commander": "^14.0.2", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.2" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-37x3E1qtG6yhF6Cm9DtevCb02t3KbPTBvG/ysonVgA8RGQqV/vagOF4/CNLiVXioEweM5RWtrqOtYtTMqOjufA=="], + "@because/sneaker": ["@because/sneaker@0.0.1", "https://npm.nose.space/@because/sneaker/-/sneaker-0.0.1.tgz", { "dependencies": { "hono": "^4.9.8", "unique-names-generator": "^4.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-rN9hc13ofap+7SvfShJkTJQYBcViCiElyfb8FBMzP1SKIe8B71csZeLh+Ujye/5538ojWfM/5hRRPJ+Aa/0A+g=="], + + "@because/toes": ["@because/toes@0.0.7", "https://npm.nose.space/@because/toes/-/toes-0.0.7.tgz", { "dependencies": { "@because/forge": "^0.0.1", "@because/hype": "^0.0.2", "@because/sneaker": "^0.0.1", "commander": "^14.0.3", "diff": "^8.0.3", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5.9.3" }, "bin": { "toes": "src/cli/index.ts" } }, "sha512-TZJvJIZ1qDvsixfej5GG6huwnTTKIFuG7GQ+YaOursw0f5wCgrAVQ/QhYzzszNdQsGJhErGcpyDXf8I/lCQxAw=="], "@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], @@ -41,5 +43,7 @@ "typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unique-names-generator": ["unique-names-generator@4.7.1", "https://npm.nose.space/unique-names-generator/-/unique-names-generator-4.7.1.tgz", {}, "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow=="], } } diff --git a/package.json b/package.json index 8043b76..20f29ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@because/toes", - "version": "0.0.7", + "version": "0.0.8", "description": "personal web appliance - turn it on and forget about the cloud", "module": "src/index.ts", "type": "module", diff --git a/src/tools/events.ts b/src/tools/events.ts index 30e6f0b..668a647 100644 --- a/src/tools/events.ts +++ b/src/tools/events.ts @@ -11,32 +11,59 @@ interface Listener { const _listeners = new Set() -let _es: EventSource | undefined +let _abort: AbortController | undefined +let _connected = false function ensureConnection() { - if (_es && _es.readyState !== EventSource.CLOSED) return - if (_es) _es.close() + if (_connected) return + _connected = true const url = `${process.env.TOES_URL}/api/events/stream` - _es = new EventSource(url) + _abort = new AbortController() - _es.onerror = () => { - if (_es?.readyState === EventSource.CLOSED) { - console.warn('[toes] Event stream closed, reconnecting...') - _es = undefined - if (_listeners.size > 0) ensureConnection() - } - } + fetch(url, { signal: _abort.signal }) + .then(async (res) => { + const reader = res.body!.getReader() + const decoder = new TextDecoder() + let buf = '' - _es.onmessage = (msg) => { - try { - const event: ToesEvent = JSON.parse(msg.data) - _listeners.forEach(l => { - if (l.types.includes(event.type)) l.callback(event) - }) - } catch (e) { - console.warn('[toes] Failed to parse event:', e) - } + while (true) { + const { done, value } = await reader.read() + if (done) break + buf += decoder.decode(value, { stream: true }) + + const lines = buf.split('\n') + buf = lines.pop()! + for (const line of lines) { + if (!line.startsWith('data: ')) continue + try { + const event: ToesEvent = JSON.parse(line.slice(6)) + _listeners.forEach(l => { + if (l.types.includes(event.type)) l.callback(event) + }) + } catch (e) { + console.warn('[toes] Failed to parse event:', e) + } + } + } + }) + .catch((e) => { + if (e.name === 'AbortError') return + console.warn('[toes] Event stream error, reconnecting...', e.message) + }) + .finally(() => { + _connected = false + if (_listeners.size > 0) { + setTimeout(ensureConnection, 1000) + } + }) +} + +function closeConnection() { + if (_abort) { + _abort.abort() + _abort = undefined } + _connected = false } export function on(type: ToesEventType | ToesEventType[], callback: EventCallback): () => void { @@ -49,9 +76,6 @@ export function on(type: ToesEventType | ToesEventType[], callback: EventCallbac return () => { _listeners.delete(listener) - if (_listeners.size === 0 && _es) { - _es.close() - _es = undefined - } + if (_listeners.size === 0) closeConnection() } }