@wopr-network/platform-core 1.30.0 → 1.31.0
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/dist/fleet/fleet-event-emitter.d.ts +2 -0
- package/dist/fleet/fleet-notification-listener.d.ts +1 -1
- package/dist/fleet/fleet-notification-listener.js +8 -6
- package/dist/fleet/init-fleet-updater.d.ts +3 -1
- package/dist/fleet/init-fleet-updater.js +22 -7
- package/package.json +1 -1
- package/src/fleet/fleet-event-emitter.ts +2 -0
- package/src/fleet/fleet-notification-listener.ts +15 -7
- package/src/fleet/init-fleet-updater.ts +25 -8
|
@@ -10,4 +10,4 @@ export interface FleetNotificationListenerDeps {
|
|
|
10
10
|
/** Debounce window in ms before sending summary email. Default 60_000. */
|
|
11
11
|
debounceMs?: number;
|
|
12
12
|
}
|
|
13
|
-
export declare function initFleetNotificationListener(deps: FleetNotificationListenerDeps): () => void
|
|
13
|
+
export declare function initFleetNotificationListener(deps: FleetNotificationListenerDeps): () => Promise<void>;
|
|
@@ -17,9 +17,7 @@ export function initFleetNotificationListener(deps) {
|
|
|
17
17
|
logger.warn("Fleet notification skipped: no email for tenant", { tenantId });
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
// BotFleetEvent doesn't carry version info; "latest" is a placeholder.
|
|
22
|
-
notificationService.notifyFleetUpdateComplete(tenantId, email, "latest", rollout.succeeded, rollout.failed);
|
|
20
|
+
notificationService.notifyFleetUpdateComplete(tenantId, email, rollout.version, rollout.succeeded, rollout.failed);
|
|
23
21
|
}
|
|
24
22
|
catch (err) {
|
|
25
23
|
logger.error("Fleet notification flush error", { err, tenantId });
|
|
@@ -35,6 +33,7 @@ export function initFleetNotificationListener(deps) {
|
|
|
35
33
|
if (!rollout) {
|
|
36
34
|
rollout = {
|
|
37
35
|
tenantId: botEvent.tenantId,
|
|
36
|
+
version: botEvent.version ?? "latest",
|
|
38
37
|
succeeded: 0,
|
|
39
38
|
failed: 0,
|
|
40
39
|
timer: setTimeout(() => flush(botEvent.tenantId), debounceMs),
|
|
@@ -45,6 +44,8 @@ export function initFleetNotificationListener(deps) {
|
|
|
45
44
|
// Reset timer on each new event (sliding window)
|
|
46
45
|
clearTimeout(rollout.timer);
|
|
47
46
|
rollout.timer = setTimeout(() => flush(botEvent.tenantId), debounceMs);
|
|
47
|
+
if (botEvent.version)
|
|
48
|
+
rollout.version = botEvent.version;
|
|
48
49
|
}
|
|
49
50
|
if (botEvent.type === "bot.updated") {
|
|
50
51
|
rollout.succeeded++;
|
|
@@ -53,13 +54,14 @@ export function initFleetNotificationListener(deps) {
|
|
|
53
54
|
rollout.failed++;
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
|
-
return () => {
|
|
57
|
+
return async () => {
|
|
57
58
|
unsubscribe();
|
|
58
|
-
|
|
59
|
+
const flushes = [];
|
|
59
60
|
for (const [tenantId, rollout] of pending) {
|
|
60
61
|
clearTimeout(rollout.timer);
|
|
61
|
-
|
|
62
|
+
flushes.push(flush(tenantId));
|
|
62
63
|
}
|
|
63
64
|
pending.clear();
|
|
65
|
+
await Promise.allSettled(flushes);
|
|
64
66
|
};
|
|
65
67
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type Docker from "dockerode";
|
|
14
14
|
import type { IBotProfileRepository } from "./bot-profile-repository.js";
|
|
15
|
-
import
|
|
15
|
+
import { FleetEventEmitter } from "./fleet-event-emitter.js";
|
|
16
16
|
import type { FleetManager } from "./fleet-manager.js";
|
|
17
17
|
import { ImagePoller } from "./image-poller.js";
|
|
18
18
|
import type { IProfileStore } from "./profile-store.js";
|
|
@@ -46,6 +46,8 @@ export interface FleetUpdaterHandle {
|
|
|
46
46
|
updater: ContainerUpdater;
|
|
47
47
|
orchestrator: RolloutOrchestrator;
|
|
48
48
|
snapshotManager: VolumeSnapshotManager;
|
|
49
|
+
/** Fleet event emitter for subscribing to bot/node lifecycle events */
|
|
50
|
+
eventEmitter: FleetEventEmitter;
|
|
49
51
|
/** Stop the poller and wait for any active rollout to finish */
|
|
50
52
|
stop: () => Promise<void>;
|
|
51
53
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* all bots need updating.
|
|
12
12
|
*/
|
|
13
13
|
import { logger } from "../config/logger.js";
|
|
14
|
+
import { FleetEventEmitter } from "./fleet-event-emitter.js";
|
|
14
15
|
import { ImagePoller } from "./image-poller.js";
|
|
15
16
|
import { RolloutOrchestrator } from "./rollout-orchestrator.js";
|
|
16
17
|
import { createRolloutStrategy } from "./rollout-strategy.js";
|
|
@@ -29,7 +30,8 @@ import { VolumeSnapshotManager } from "./volume-snapshot-manager.js";
|
|
|
29
30
|
* @param config - Optional pipeline configuration
|
|
30
31
|
*/
|
|
31
32
|
export function initFleetUpdater(docker, fleet, profileStore, profileRepo, config = {}) {
|
|
32
|
-
const { strategy: strategyType = "rolling-wave", strategyOptions, snapshotDir = "/data/fleet/snapshots", onBotUpdated, onRolloutComplete, configRepo, eventEmitter, } = config;
|
|
33
|
+
const { strategy: strategyType = "rolling-wave", strategyOptions, snapshotDir = "/data/fleet/snapshots", onBotUpdated, onRolloutComplete, configRepo, eventEmitter: configEventEmitter, } = config;
|
|
34
|
+
const emitter = configEventEmitter ?? new FleetEventEmitter();
|
|
33
35
|
const poller = new ImagePoller(docker, profileStore);
|
|
34
36
|
const updater = new ContainerUpdater(docker, profileStore, fleet, poller);
|
|
35
37
|
const snapshotManager = new VolumeSnapshotManager(docker, snapshotDir);
|
|
@@ -54,16 +56,28 @@ export function initFleetUpdater(docker, fleet, profileStore, profileRepo, confi
|
|
|
54
56
|
return results.filter((p) => p !== null);
|
|
55
57
|
},
|
|
56
58
|
onBotUpdated: (result) => {
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// Fire-and-forget: resolve tenantId + emit event asynchronously
|
|
60
|
+
// The orchestrator callback is sync (void return) — async work must not block rollout progress
|
|
61
|
+
void (async () => {
|
|
62
|
+
let tenantId = "";
|
|
63
|
+
try {
|
|
64
|
+
const profile = await profileRepo.get(result.botId);
|
|
65
|
+
if (profile)
|
|
66
|
+
tenantId = profile.tenantId;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Best-effort — event still fires with empty tenantId
|
|
70
|
+
}
|
|
71
|
+
// Extract version tag from image name (e.g. "ghcr.io/org/image:v1.2.3" → "v1.2.3")
|
|
72
|
+
const version = result.newImage.includes(":") ? (result.newImage.split(":").pop() ?? "latest") : "latest";
|
|
73
|
+
emitter.emit({
|
|
60
74
|
type: result.success ? "bot.updated" : "bot.update_failed",
|
|
61
75
|
botId: result.botId,
|
|
62
|
-
tenantId
|
|
76
|
+
tenantId,
|
|
63
77
|
timestamp: new Date().toISOString(),
|
|
78
|
+
version,
|
|
64
79
|
});
|
|
65
|
-
}
|
|
66
|
-
// Chain user-provided callback
|
|
80
|
+
})();
|
|
67
81
|
onBotUpdated?.(result);
|
|
68
82
|
},
|
|
69
83
|
onRolloutComplete: (result) => {
|
|
@@ -96,6 +110,7 @@ export function initFleetUpdater(docker, fleet, profileStore, profileRepo, confi
|
|
|
96
110
|
updater,
|
|
97
111
|
orchestrator,
|
|
98
112
|
snapshotManager,
|
|
113
|
+
eventEmitter: emitter,
|
|
99
114
|
stop: async () => {
|
|
100
115
|
poller.stop();
|
|
101
116
|
// Wait for any in-flight rollout to complete before returning
|
package/package.json
CHANGED
|
@@ -15,12 +15,13 @@ export interface FleetNotificationListenerDeps {
|
|
|
15
15
|
|
|
16
16
|
interface PendingRollout {
|
|
17
17
|
tenantId: string;
|
|
18
|
+
version: string;
|
|
18
19
|
succeeded: number;
|
|
19
20
|
failed: number;
|
|
20
21
|
timer: ReturnType<typeof setTimeout>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export function initFleetNotificationListener(deps: FleetNotificationListenerDeps): () => void {
|
|
24
|
+
export function initFleetNotificationListener(deps: FleetNotificationListenerDeps): () => Promise<void> {
|
|
24
25
|
const { eventEmitter, notificationService, preferences, resolveEmail } = deps;
|
|
25
26
|
const debounceMs = deps.debounceMs ?? 60_000;
|
|
26
27
|
const pending = new Map<string, PendingRollout>();
|
|
@@ -40,9 +41,13 @@ export function initFleetNotificationListener(deps: FleetNotificationListenerDep
|
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
notificationService.notifyFleetUpdateComplete(
|
|
45
|
+
tenantId,
|
|
46
|
+
email,
|
|
47
|
+
rollout.version,
|
|
48
|
+
rollout.succeeded,
|
|
49
|
+
rollout.failed,
|
|
50
|
+
);
|
|
46
51
|
} catch (err) {
|
|
47
52
|
logger.error("Fleet notification flush error", { err, tenantId });
|
|
48
53
|
}
|
|
@@ -57,6 +62,7 @@ export function initFleetNotificationListener(deps: FleetNotificationListenerDep
|
|
|
57
62
|
if (!rollout) {
|
|
58
63
|
rollout = {
|
|
59
64
|
tenantId: botEvent.tenantId,
|
|
65
|
+
version: botEvent.version ?? "latest",
|
|
60
66
|
succeeded: 0,
|
|
61
67
|
failed: 0,
|
|
62
68
|
timer: setTimeout(() => flush(botEvent.tenantId), debounceMs),
|
|
@@ -66,6 +72,7 @@ export function initFleetNotificationListener(deps: FleetNotificationListenerDep
|
|
|
66
72
|
// Reset timer on each new event (sliding window)
|
|
67
73
|
clearTimeout(rollout.timer);
|
|
68
74
|
rollout.timer = setTimeout(() => flush(botEvent.tenantId), debounceMs);
|
|
75
|
+
if (botEvent.version) rollout.version = botEvent.version;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
if (botEvent.type === "bot.updated") {
|
|
@@ -75,13 +82,14 @@ export function initFleetNotificationListener(deps: FleetNotificationListenerDep
|
|
|
75
82
|
}
|
|
76
83
|
});
|
|
77
84
|
|
|
78
|
-
return () => {
|
|
85
|
+
return async () => {
|
|
79
86
|
unsubscribe();
|
|
80
|
-
|
|
87
|
+
const flushes: Promise<void>[] = [];
|
|
81
88
|
for (const [tenantId, rollout] of pending) {
|
|
82
89
|
clearTimeout(rollout.timer);
|
|
83
|
-
|
|
90
|
+
flushes.push(flush(tenantId));
|
|
84
91
|
}
|
|
85
92
|
pending.clear();
|
|
93
|
+
await Promise.allSettled(flushes);
|
|
86
94
|
};
|
|
87
95
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type Docker from "dockerode";
|
|
15
15
|
import { logger } from "../config/logger.js";
|
|
16
16
|
import type { IBotProfileRepository } from "./bot-profile-repository.js";
|
|
17
|
-
import
|
|
17
|
+
import { FleetEventEmitter } from "./fleet-event-emitter.js";
|
|
18
18
|
import type { FleetManager } from "./fleet-manager.js";
|
|
19
19
|
import { ImagePoller } from "./image-poller.js";
|
|
20
20
|
import type { IProfileStore } from "./profile-store.js";
|
|
@@ -46,6 +46,8 @@ export interface FleetUpdaterHandle {
|
|
|
46
46
|
updater: ContainerUpdater;
|
|
47
47
|
orchestrator: RolloutOrchestrator;
|
|
48
48
|
snapshotManager: VolumeSnapshotManager;
|
|
49
|
+
/** Fleet event emitter for subscribing to bot/node lifecycle events */
|
|
50
|
+
eventEmitter: FleetEventEmitter;
|
|
49
51
|
/** Stop the poller and wait for any active rollout to finish */
|
|
50
52
|
stop: () => Promise<void>;
|
|
51
53
|
}
|
|
@@ -76,9 +78,11 @@ export function initFleetUpdater(
|
|
|
76
78
|
onBotUpdated,
|
|
77
79
|
onRolloutComplete,
|
|
78
80
|
configRepo,
|
|
79
|
-
eventEmitter,
|
|
81
|
+
eventEmitter: configEventEmitter,
|
|
80
82
|
} = config;
|
|
81
83
|
|
|
84
|
+
const emitter = configEventEmitter ?? new FleetEventEmitter();
|
|
85
|
+
|
|
82
86
|
const poller = new ImagePoller(docker, profileStore);
|
|
83
87
|
const updater = new ContainerUpdater(docker, profileStore, fleet, poller);
|
|
84
88
|
const snapshotManager = new VolumeSnapshotManager(docker, snapshotDir);
|
|
@@ -106,16 +110,28 @@ export function initFleetUpdater(
|
|
|
106
110
|
return results.filter((p) => p !== null);
|
|
107
111
|
},
|
|
108
112
|
onBotUpdated: (result) => {
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
// Fire-and-forget: resolve tenantId + emit event asynchronously
|
|
114
|
+
// The orchestrator callback is sync (void return) — async work must not block rollout progress
|
|
115
|
+
void (async () => {
|
|
116
|
+
let tenantId = "";
|
|
117
|
+
try {
|
|
118
|
+
const profile = await profileRepo.get(result.botId);
|
|
119
|
+
if (profile) tenantId = profile.tenantId;
|
|
120
|
+
} catch {
|
|
121
|
+
// Best-effort — event still fires with empty tenantId
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Extract version tag from image name (e.g. "ghcr.io/org/image:v1.2.3" → "v1.2.3")
|
|
125
|
+
const version = result.newImage.includes(":") ? (result.newImage.split(":").pop() ?? "latest") : "latest";
|
|
126
|
+
|
|
127
|
+
emitter.emit({
|
|
112
128
|
type: result.success ? "bot.updated" : "bot.update_failed",
|
|
113
129
|
botId: result.botId,
|
|
114
|
-
tenantId
|
|
130
|
+
tenantId,
|
|
115
131
|
timestamp: new Date().toISOString(),
|
|
132
|
+
version,
|
|
116
133
|
});
|
|
117
|
-
}
|
|
118
|
-
// Chain user-provided callback
|
|
134
|
+
})();
|
|
119
135
|
onBotUpdated?.(result);
|
|
120
136
|
},
|
|
121
137
|
onRolloutComplete: (result) => {
|
|
@@ -152,6 +168,7 @@ export function initFleetUpdater(
|
|
|
152
168
|
updater,
|
|
153
169
|
orchestrator,
|
|
154
170
|
snapshotManager,
|
|
171
|
+
eventEmitter: emitter,
|
|
155
172
|
stop: async () => {
|
|
156
173
|
poller.stop();
|
|
157
174
|
// Wait for any in-flight rollout to complete before returning
|