@camstack/server 0.1.7 → 0.2.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/package.json +11 -9
- package/src/__tests__/addon-install-e2e.test.ts +0 -1
- package/src/__tests__/addon-pages-e2e.test.ts +40 -18
- package/src/__tests__/addon-settings-router.spec.ts +6 -1
- package/src/__tests__/addon-upload.spec.ts +91 -29
- package/src/__tests__/agent-registry.spec.ts +26 -9
- package/src/__tests__/agent-status-page.spec.ts +1 -3
- package/src/__tests__/auth-session-cookie.test.ts +28 -1
- package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
- package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +206 -0
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +292 -0
- package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
- package/src/__tests__/cap-route-adapter.spec.ts +28 -15
- package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +177 -0
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +137 -0
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
- package/src/__tests__/cap-routers/harness.ts +11 -7
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
- package/src/__tests__/capability-e2e.test.ts +9 -11
- package/src/__tests__/cli-e2e.test.ts +80 -59
- package/src/__tests__/core-cap-bridge.spec.ts +3 -1
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
- package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
- package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
- package/src/__tests__/framework-allowlist.spec.ts +5 -4
- package/src/__tests__/https-e2e.test.ts +12 -6
- package/src/__tests__/lifecycle-e2e.test.ts +60 -11
- package/src/__tests__/live-events-subscription.spec.ts +17 -18
- package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
- package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +265 -5
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
- package/src/__tests__/native-cap-route.spec.ts +42 -19
- package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
- package/src/__tests__/singleton-contention.test.ts +23 -11
- package/src/__tests__/streaming-diagnostic.test.ts +156 -53
- package/src/__tests__/streaming-scale.test.ts +69 -35
- package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
- package/src/agent-status-page.ts +4 -3
- package/src/api/__tests__/addons-custom.spec.ts +22 -8
- package/src/api/__tests__/capabilities.router.test.ts +18 -9
- package/src/api/addon-upload.ts +46 -15
- package/src/api/addons-custom.router.ts +7 -6
- package/src/api/auth-whoami.ts +3 -1
- package/src/api/bridge-addons.router.ts +3 -1
- package/src/api/capabilities.router.ts +117 -78
- package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
- package/src/api/core/__tests__/integration-markers.spec.ts +10 -0
- package/src/api/core/addon-settings.router.ts +4 -1
- package/src/api/core/agents.router.ts +52 -53
- package/src/api/core/auth.router.ts +55 -36
- package/src/api/core/bulk-update-coordinator.ts +25 -22
- package/src/api/core/cap-providers.ts +459 -166
- package/src/api/core/capabilities.router.ts +30 -23
- package/src/api/core/hwaccel.router.ts +37 -10
- package/src/api/core/live-events.router.ts +16 -9
- package/src/api/core/logs.router.ts +58 -25
- package/src/api/core/notifications.router.ts +2 -1
- package/src/api/core/repl.router.ts +1 -3
- package/src/api/core/settings-backend.router.ts +68 -70
- package/src/api/core/system-events.router.ts +41 -32
- package/src/api/health/health.routes.ts +7 -13
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
- package/src/api/oauth2/consent-page.ts +4 -3
- package/src/api/oauth2/oauth2-routes.ts +41 -12
- package/src/api/trpc/__tests__/client-ip.spec.ts +27 -1
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
- package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +136 -0
- package/src/api/trpc/cap-mount-helpers.ts +64 -44
- package/src/api/trpc/cap-route-error-formatter.ts +17 -9
- package/src/api/trpc/client-ip.ts +17 -0
- package/src/api/trpc/core-cap-bridge.ts +3 -1
- package/src/api/trpc/generated-cap-mounts.ts +801 -286
- package/src/api/trpc/generated-cap-routers.ts +5723 -719
- package/src/api/trpc/scope-access.ts +7 -7
- package/src/api/trpc/trpc.context.ts +7 -4
- package/src/api/trpc/trpc.middleware.ts +4 -2
- package/src/api/trpc/trpc.router.ts +117 -48
- package/src/auth/session-cookie.ts +10 -0
- package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
- package/src/boot/boot-config.ts +103 -122
- package/src/boot/integration-id-backfill.ts +109 -0
- package/src/boot/post-boot.service.ts +5 -3
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +62 -0
- package/src/core/addon/addon-call-gateway.ts +20 -6
- package/src/core/addon/addon-package.service.ts +183 -89
- package/src/core/addon/addon-registry.service.ts +1212 -1267
- package/src/core/addon/addon-row-manifest.ts +29 -0
- package/src/core/addon/addon-search.service.ts +2 -1
- package/src/core/addon/addon-settings-provider.ts +27 -7
- package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
- package/src/core/addon-pages/addon-pages.service.ts +3 -1
- package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
- package/src/core/agent/agent-registry.service.ts +60 -38
- package/src/core/auth/auth.service.spec.ts +6 -8
- package/src/core/config/config.service.spec.ts +1 -1
- package/src/core/events/event-bus.service.spec.ts +44 -21
- package/src/core/events/event-bus.service.ts +5 -1
- package/src/core/feature/feature.service.spec.ts +4 -1
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
- package/src/core/logging/logging.service.spec.ts +61 -21
- package/src/core/logging/logging.service.ts +19 -5
- package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
- package/src/core/moleculer/cap-call-fn.ts +5 -1
- package/src/core/moleculer/cap-route-authority.ts +18 -6
- package/src/core/moleculer/moleculer.service.ts +145 -29
- package/src/core/network/network-quality.service.spec.ts +7 -1
- package/src/core/notification/notification-wrapper.service.ts +1 -3
- package/src/core/notification/toast-wrapper.service.ts +1 -5
- package/src/core/repl/repl-engine.service.spec.ts +66 -39
- package/src/core/repl/repl-engine.service.ts +11 -12
- package/src/core/storage/storage-location-manager.spec.ts +12 -3
- package/src/core/streaming/stream-probe.service.ts +22 -13
- package/src/core/topology/topology-emitter.service.ts +5 -1
- package/src/launcher.ts +14 -9
- package/src/main.ts +658 -495
- package/src/manual-boot.ts +133 -154
- package/tsconfig.json +20 -8
- package/src/core/storage/settings-store.spec.ts +0 -213
- package/src/core/storage/settings-store.ts +0 -2
- package/src/core/storage/sql-schema.spec.ts +0 -140
- package/src/core/storage/sql-schema.ts +0 -3
|
@@ -43,23 +43,22 @@ const AuthProviderSummarySchema = z.object({
|
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
/** Wire shape of the authenticated user returned by `auth.me`. */
|
|
46
|
-
const MeSchema = z
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
permissions: z.object({
|
|
46
|
+
const MeSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
id: z.string(),
|
|
49
|
+
username: z.string(),
|
|
51
50
|
isAdmin: z.boolean(),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
permissions: z.object({
|
|
52
|
+
isAdmin: z.boolean(),
|
|
53
|
+
allowedProviders: z.union([z.literal('*'), z.array(z.string())]),
|
|
54
|
+
allowedDevices: z.record(z.string(), z.unknown()),
|
|
55
|
+
}),
|
|
56
|
+
isApiKey: z.boolean(),
|
|
57
|
+
agentId: z.string().optional(),
|
|
58
|
+
})
|
|
59
|
+
.nullable()
|
|
58
60
|
|
|
59
|
-
export function createAuthRouter(
|
|
60
|
-
auth: AuthService,
|
|
61
|
-
registry: CapabilityRegistry | null,
|
|
62
|
-
) {
|
|
61
|
+
export function createAuthRouter(auth: AuthService, registry: CapabilityRegistry | null) {
|
|
63
62
|
return trpcRouter({
|
|
64
63
|
login: publicProcedure
|
|
65
64
|
.input(z.object({ username: z.string(), password: z.string() }))
|
|
@@ -73,13 +72,16 @@ export function createAuthRouter(
|
|
|
73
72
|
// common cause is a settings-store regression that breaks
|
|
74
73
|
// the `users` collection query path.
|
|
75
74
|
throw new Error(
|
|
76
|
-
'Login unavailable — `user-management` capability not registered. '
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
'Login unavailable — `user-management` capability not registered. ' +
|
|
76
|
+
'The `local-auth` addon failed to initialize; check server logs ' +
|
|
77
|
+
'for an error tagged `[hub/local-auth]`.',
|
|
79
78
|
)
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
const user = await userMgmt.validateCredentials({
|
|
81
|
+
const user = await userMgmt.validateCredentials({
|
|
82
|
+
username: input.username,
|
|
83
|
+
password: input.password,
|
|
84
|
+
})
|
|
83
85
|
if (!user) throw new Error('Invalid credentials')
|
|
84
86
|
|
|
85
87
|
// ── TOTP gate ────────────────────────────────────────────────
|
|
@@ -91,9 +93,10 @@ export function createAuthRouter(
|
|
|
91
93
|
// `kind: 'totp-challenge'` so it can't be replayed against
|
|
92
94
|
// protected endpoints (the auth middleware rejects anything
|
|
93
95
|
// without the standard session shape).
|
|
94
|
-
const totpStatus =
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const totpStatus =
|
|
97
|
+
typeof userMgmt.getTotpStatus === 'function'
|
|
98
|
+
? await userMgmt.getTotpStatus({ userId: user.id })
|
|
99
|
+
: { enabled: false }
|
|
97
100
|
if (totpStatus.enabled) {
|
|
98
101
|
const challengeToken = auth.signTotpChallengeToken({
|
|
99
102
|
userId: user.id,
|
|
@@ -154,18 +157,20 @@ export function createAuthRouter(
|
|
|
154
157
|
if (!userMgmt) {
|
|
155
158
|
throw new Error('Login unavailable — `user-management` capability not registered')
|
|
156
159
|
}
|
|
157
|
-
const verify =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
const verify =
|
|
161
|
+
typeof userMgmt.verifyTotp === 'function'
|
|
162
|
+
? await userMgmt.verifyTotp({ userId: claims.userId, code: input.code.trim() })
|
|
163
|
+
: { valid: false }
|
|
160
164
|
if (!verify.valid) {
|
|
161
165
|
throw new Error('Invalid TOTP code')
|
|
162
166
|
}
|
|
163
167
|
// Re-fetch the user record so scopes / allowedDevices reflect
|
|
164
168
|
// any admin change made between the two legs. Without this we
|
|
165
169
|
// could mint a stale-scope session.
|
|
166
|
-
const fresh =
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
const fresh =
|
|
171
|
+
typeof userMgmt.listUsers === 'function'
|
|
172
|
+
? (await userMgmt.listUsers()).find((u) => u.id === claims.userId)
|
|
173
|
+
: null
|
|
169
174
|
if (!fresh) {
|
|
170
175
|
throw new Error('User no longer exists')
|
|
171
176
|
}
|
|
@@ -202,15 +207,20 @@ export function createAuthRouter(
|
|
|
202
207
|
// exists only to block third-party tampering — operators acting on
|
|
203
208
|
// themselves bypass it intentionally.
|
|
204
209
|
changeOwnPassword: protectedProcedure
|
|
205
|
-
.input(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
210
|
+
.input(
|
|
211
|
+
z.object({
|
|
212
|
+
currentPassword: z.string().min(1),
|
|
213
|
+
newPassword: z.string().min(8),
|
|
214
|
+
}),
|
|
215
|
+
)
|
|
209
216
|
.output(z.object({ success: z.literal(true) }))
|
|
210
217
|
.mutation(async ({ input, ctx }) => {
|
|
211
218
|
const userMgmt = registry?.getSingleton('user-management') as
|
|
212
219
|
| {
|
|
213
|
-
validateCredentials: (i: {
|
|
220
|
+
validateCredentials: (i: {
|
|
221
|
+
username: string
|
|
222
|
+
password: string
|
|
223
|
+
}) => Promise<{ id: string } | null>
|
|
214
224
|
resetPassword: (i: { id: string; newPassword: string }) => Promise<{ success: true }>
|
|
215
225
|
}
|
|
216
226
|
| null
|
|
@@ -220,7 +230,10 @@ export function createAuthRouter(
|
|
|
220
230
|
// Re-validate the current password against the live store to
|
|
221
231
|
// confirm session identity → password ownership match. Stops a
|
|
222
232
|
// stolen session-token from rotating credentials silently.
|
|
223
|
-
const ok = await userMgmt.validateCredentials({
|
|
233
|
+
const ok = await userMgmt.validateCredentials({
|
|
234
|
+
username: ctx.user.username,
|
|
235
|
+
password: input.currentPassword,
|
|
236
|
+
})
|
|
224
237
|
if (!ok) throw new Error('Current password is incorrect')
|
|
225
238
|
await userMgmt.resetPassword({ id: ctx.user.id, newPassword: input.newPassword })
|
|
226
239
|
return { success: true as const }
|
|
@@ -231,7 +244,9 @@ export function createAuthRouter(
|
|
|
231
244
|
.output(z.object({ secret: z.string(), otpauthUrl: z.string() }))
|
|
232
245
|
.mutation(async ({ ctx }) => {
|
|
233
246
|
const userMgmt = registry?.getSingleton('user-management') as
|
|
234
|
-
| {
|
|
247
|
+
| {
|
|
248
|
+
setupTotp: (i: { userId: string }) => Promise<{ secret: string; otpauthUrl: string }>
|
|
249
|
+
}
|
|
235
250
|
| null
|
|
236
251
|
| undefined
|
|
237
252
|
if (!userMgmt) throw new Error('user-management capability not available')
|
|
@@ -270,7 +285,11 @@ export function createAuthRouter(
|
|
|
270
285
|
.output(z.object({ enabled: z.boolean(), confirmedAt: z.number().nullable() }))
|
|
271
286
|
.query(async ({ ctx }) => {
|
|
272
287
|
const userMgmt = registry?.getSingleton('user-management') as
|
|
273
|
-
| {
|
|
288
|
+
| {
|
|
289
|
+
getTotpStatus: (i: {
|
|
290
|
+
userId: string
|
|
291
|
+
}) => Promise<{ enabled: boolean; confirmedAt: number | null }>
|
|
292
|
+
}
|
|
274
293
|
| null
|
|
275
294
|
| undefined
|
|
276
295
|
if (!userMgmt || !ctx.user) return { enabled: false, confirmedAt: null }
|
|
@@ -29,7 +29,11 @@ export interface IBulkUpdateLogger {
|
|
|
29
29
|
export interface BulkUpdateCoordinatorDeps {
|
|
30
30
|
readonly eventBus: IBulkUpdateEventBus
|
|
31
31
|
readonly updateAddon: (input: { name: string; version: string }) => Promise<void>
|
|
32
|
-
readonly updateFrameworkPackage: (input: {
|
|
32
|
+
readonly updateFrameworkPackage: (input: {
|
|
33
|
+
packageName: string
|
|
34
|
+
version: string
|
|
35
|
+
deferRestart: boolean
|
|
36
|
+
}) => Promise<void>
|
|
33
37
|
readonly restartServer: (input: { confirm: true }) => Promise<void>
|
|
34
38
|
readonly logger: IBulkUpdateLogger
|
|
35
39
|
/** Injectable clock for tests. Default: `() => Date.now()`. */
|
|
@@ -77,7 +81,7 @@ export class BulkUpdateCoordinator {
|
|
|
77
81
|
|
|
78
82
|
const id = randomUUID()
|
|
79
83
|
|
|
80
|
-
const items: BulkUpdateItem[] = input.items.map(i => ({
|
|
84
|
+
const items: BulkUpdateItem[] = input.items.map((i) => ({
|
|
81
85
|
name: i.name,
|
|
82
86
|
isSystem: i.isSystem,
|
|
83
87
|
// fromVersion: the cap interface receives name+version+isSystem only;
|
|
@@ -110,7 +114,7 @@ export class BulkUpdateCoordinator {
|
|
|
110
114
|
// Emit initial state so clients see the bulk as started immediately
|
|
111
115
|
this.emit(state)
|
|
112
116
|
|
|
113
|
-
void this.runLoop(id, cancelFlag).catch(err => {
|
|
117
|
+
void this.runLoop(id, cancelFlag).catch((err) => {
|
|
114
118
|
this.deps.logger.error('BulkUpdateCoordinator: loop crashed unexpectedly', err)
|
|
115
119
|
})
|
|
116
120
|
|
|
@@ -133,9 +137,9 @@ export class BulkUpdateCoordinator {
|
|
|
133
137
|
|
|
134
138
|
list(nodeId?: string): readonly BulkUpdateState[] {
|
|
135
139
|
const all = [...this.states.keys()]
|
|
136
|
-
.map(id => this.get(id))
|
|
140
|
+
.map((id) => this.get(id)) // get() applies lazy-cleanup
|
|
137
141
|
.filter((s): s is BulkUpdateState => s !== null)
|
|
138
|
-
return nodeId === undefined ? all : all.filter(s => s.nodeId === nodeId)
|
|
142
|
+
return nodeId === undefined ? all : all.filter((s) => s.nodeId === nodeId)
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
cancel(id: string): { cancelled: boolean } {
|
|
@@ -148,7 +152,7 @@ export class BulkUpdateCoordinator {
|
|
|
148
152
|
if (state.completedAtMs !== undefined) return { cancelled: false }
|
|
149
153
|
|
|
150
154
|
flag.cancelled = true
|
|
151
|
-
this.mutate(id, s => ({ ...s, cancelled: true }))
|
|
155
|
+
this.mutate(id, (s) => ({ ...s, cancelled: true }))
|
|
152
156
|
return { cancelled: true }
|
|
153
157
|
}
|
|
154
158
|
|
|
@@ -160,24 +164,24 @@ export class BulkUpdateCoordinator {
|
|
|
160
164
|
// ── Phase 1: regular addons ──────────────────────────────────────
|
|
161
165
|
this.transitionPhase(id, 'regular')
|
|
162
166
|
|
|
163
|
-
for (const item of initial.items.filter(i => !i.isSystem)) {
|
|
167
|
+
for (const item of initial.items.filter((i) => !i.isSystem)) {
|
|
164
168
|
if (cancelFlag.cancelled) break
|
|
165
169
|
await this.processItem(id, item, false)
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// ── Phase 2: system packages (deferRestart: true) ────────────────
|
|
169
|
-
if (!cancelFlag.cancelled && initial.items.some(i => i.isSystem)) {
|
|
173
|
+
if (!cancelFlag.cancelled && initial.items.some((i) => i.isSystem)) {
|
|
170
174
|
this.transitionPhase(id, 'system')
|
|
171
175
|
|
|
172
|
-
for (const item of initial.items.filter(i => i.isSystem)) {
|
|
176
|
+
for (const item of initial.items.filter((i) => i.isSystem)) {
|
|
173
177
|
if (cancelFlag.cancelled) break
|
|
174
178
|
await this.processItem(id, item, true)
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
// ── Phase 3: single restart ──────────────────────────────────
|
|
178
|
-
const anySystemPendingRestart = this.states
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
const anySystemPendingRestart = this.states
|
|
183
|
+
.get(id)!
|
|
184
|
+
.items.some((i) => i.isSystem && i.status === 'done-pending-restart')
|
|
181
185
|
|
|
182
186
|
if (anySystemPendingRestart && !cancelFlag.cancelled) {
|
|
183
187
|
this.transitionPhase(id, 'restarting')
|
|
@@ -214,7 +218,7 @@ export class BulkUpdateCoordinator {
|
|
|
214
218
|
|
|
215
219
|
private async processItem(id: string, item: BulkUpdateItem, isSystem: boolean): Promise<void> {
|
|
216
220
|
this.setItemStatus(id, item.name, 'updating', { startedAtMs: this.now() })
|
|
217
|
-
this.mutate(id, s => ({ ...s, current: item.name }))
|
|
221
|
+
this.mutate(id, (s) => ({ ...s, current: item.name }))
|
|
218
222
|
this.emit(this.states.get(id)!)
|
|
219
223
|
|
|
220
224
|
try {
|
|
@@ -234,7 +238,7 @@ export class BulkUpdateCoordinator {
|
|
|
234
238
|
this.setItemStatus(id, item.name, 'failed', { error: msg, completedAtMs: this.now() })
|
|
235
239
|
}
|
|
236
240
|
|
|
237
|
-
this.mutate(id, s => ({ ...s, current: null }))
|
|
241
|
+
this.mutate(id, (s) => ({ ...s, current: null }))
|
|
238
242
|
this.emit(this.states.get(id)!)
|
|
239
243
|
}
|
|
240
244
|
|
|
@@ -246,26 +250,25 @@ export class BulkUpdateCoordinator {
|
|
|
246
250
|
status: BulkUpdateItemStatus,
|
|
247
251
|
fields: { error?: string; startedAtMs?: number; completedAtMs?: number } = {},
|
|
248
252
|
): void {
|
|
249
|
-
this.mutate(id, s => {
|
|
250
|
-
const items = s.items.map(it =>
|
|
251
|
-
it.name === name ? { ...it, status, ...fields } : it,
|
|
252
|
-
)
|
|
253
|
+
this.mutate(id, (s) => {
|
|
254
|
+
const items = s.items.map((it) => (it.name === name ? { ...it, status, ...fields } : it))
|
|
253
255
|
// completed = all terminal states: done | done-pending-restart | failed
|
|
254
256
|
const completed = items.filter(
|
|
255
|
-
it =>
|
|
257
|
+
(it) =>
|
|
258
|
+
it.status === 'done' || it.status === 'done-pending-restart' || it.status === 'failed',
|
|
256
259
|
).length
|
|
257
|
-
const failed = items.filter(it => it.status === 'failed').length
|
|
260
|
+
const failed = items.filter((it) => it.status === 'failed').length
|
|
258
261
|
return { ...s, items, completed, failed }
|
|
259
262
|
})
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
private transitionPhase(id: string, phase: BulkUpdatePhase): void {
|
|
263
|
-
this.mutate(id, s => ({ ...s, phase }))
|
|
266
|
+
this.mutate(id, (s) => ({ ...s, phase }))
|
|
264
267
|
this.emit(this.states.get(id)!)
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
private completeBulk(id: string): void {
|
|
268
|
-
this.mutate(id, s => ({ ...s, completedAtMs: this.now(), current: null }))
|
|
271
|
+
this.mutate(id, (s) => ({ ...s, completedAtMs: this.now(), current: null }))
|
|
269
272
|
this.emit(this.states.get(id)!)
|
|
270
273
|
|
|
271
274
|
// Free the nodeId slot so a new bulk for the same node can be started
|