@valentinkolb/cloud 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentinkolb/cloud",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Modular Hono+SolidJS framework for building per-app docker services behind a dynamic gateway. Powers cloud.stuve-ulm.de.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -367,7 +367,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
367
367
  stopping = true;
368
368
  log.info(`Stopping: ${meta.id}`);
369
369
  try { if (startOpts.lifecycle?.stop) await startOpts.lifecycle.stop(cloudCtx); } catch {}
370
- stopRuntimeWatcher();
370
+ await stopRuntimeWatcher();
371
371
  await heartbeat.stop();
372
372
  };
373
373
 
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * `defineApp().start()` and the `middleware.runtime()` factory both call
5
5
  * `ensureRuntimeWatcher()` — the first call subscribes to the Redis
6
- * registry and starts refreshing on every event; subsequent calls are
7
- * no-ops. Reads happen via `getCurrentRuntime()`.
6
+ * registry and starts refreshing on every event; subsequent calls
7
+ * await the same in-flight init promise. Reads happen via `getCurrentRuntime()`.
8
8
  *
9
9
  * One process = one app = one watcher; lives until `stopRuntimeWatcher()`
10
10
  * (called from defineApp's shutdown handler) or process exit.
@@ -17,37 +17,53 @@ import { buildRuntimeFromRegistry } from "./runtime-context";
17
17
  const log = logger("runtime-watcher");
18
18
 
19
19
  let current: CloudRuntime | undefined;
20
- let started = false;
20
+ let initPromise: Promise<void> | undefined;
21
+ let watcherTask: Promise<void> | undefined;
21
22
  let abort: AbortController | undefined;
22
23
 
23
24
  const refresh = async () => {
24
25
  current = buildRuntimeFromRegistry(await listApps());
25
26
  };
26
27
 
27
- export const ensureRuntimeWatcher = async (): Promise<void> => {
28
- if (started) return;
29
- started = true;
30
- await refresh();
31
- abort = new AbortController();
32
- void (async () => {
33
- try {
34
- const snap = await appRegistry.snapshot({ prefix: "apps/" });
35
- for await (const _ev of appRegistry
36
- .reader({ prefix: "apps/", after: snap.cursor })
37
- .stream({ signal: abort.signal })) {
38
- await refresh();
28
+ export const ensureRuntimeWatcher = (): Promise<void> => {
29
+ // Concurrent callers (start() + first request) wait on the same init —
30
+ // returning before `current` is populated would race getCurrentRuntime().
31
+ if (initPromise) return initPromise;
32
+ initPromise = (async () => {
33
+ await refresh();
34
+ abort = new AbortController();
35
+ watcherTask = (async () => {
36
+ try {
37
+ const snap = await appRegistry.snapshot({ prefix: "apps/" });
38
+ for await (const _ev of appRegistry
39
+ .reader({ prefix: "apps/", after: snap.cursor })
40
+ .stream({ signal: abort!.signal })) {
41
+ await refresh();
42
+ }
43
+ } catch (err) {
44
+ if (err instanceof Error && err.name === "AbortError") return;
45
+ log.error("Registry watcher failed", { error: err instanceof Error ? err.message : String(err) });
39
46
  }
40
- } catch (err) {
41
- if (err instanceof Error && err.name === "AbortError") return;
42
- log.error("Registry watcher failed", { error: err instanceof Error ? err.message : String(err) });
43
- }
47
+ })();
44
48
  })();
49
+ return initPromise;
45
50
  };
46
51
 
47
- export const stopRuntimeWatcher = (): void => {
52
+ export const stopRuntimeWatcher = async (): Promise<void> => {
53
+ // Await the watcher loop's exit before clearing state — otherwise an
54
+ // in-flight refresh() can write `current` after we cleared it, or a
55
+ // restart can overlap two readers.
48
56
  abort?.abort();
57
+ if (watcherTask) {
58
+ try {
59
+ await watcherTask;
60
+ } catch {
61
+ // already logged inside the loop
62
+ }
63
+ }
49
64
  abort = undefined;
50
- started = false;
65
+ watcherTask = undefined;
66
+ initPromise = undefined;
51
67
  current = undefined;
52
68
  };
53
69