@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
|
@@ -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
|
|
7
|
-
*
|
|
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
|
|
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 =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
65
|
+
watcherTask = undefined;
|
|
66
|
+
initPromise = undefined;
|
|
51
67
|
current = undefined;
|
|
52
68
|
};
|
|
53
69
|
|