@kapeta/local-cluster-service 0.12.0 → 0.13.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/index.js +2 -0
  3. package/dist/cjs/src/assetManager.d.ts +3 -1
  4. package/dist/cjs/src/assetManager.js +20 -4
  5. package/dist/cjs/src/assets/routes.js +22 -1
  6. package/dist/cjs/src/containerManager.d.ts +1 -1
  7. package/dist/cjs/src/containerManager.js +132 -122
  8. package/dist/cjs/src/instanceManager.d.ts +4 -3
  9. package/dist/cjs/src/instanceManager.js +87 -60
  10. package/dist/cjs/src/instances/routes.js +21 -11
  11. package/dist/cjs/src/operatorManager.d.ts +5 -3
  12. package/dist/cjs/src/operatorManager.js +34 -22
  13. package/dist/cjs/src/providerManager.js +1 -1
  14. package/dist/cjs/src/repositoryManager.d.ts +2 -4
  15. package/dist/cjs/src/repositoryManager.js +51 -66
  16. package/dist/cjs/src/socketManager.js +1 -1
  17. package/dist/cjs/src/taskManager.d.ts +64 -0
  18. package/dist/cjs/src/taskManager.js +163 -0
  19. package/dist/cjs/src/tasks/routes.d.ts +3 -0
  20. package/dist/cjs/src/tasks/routes.js +35 -0
  21. package/dist/cjs/src/utils/BlockInstanceRunner.js +0 -1
  22. package/dist/esm/index.js +2 -0
  23. package/dist/esm/src/assetManager.d.ts +3 -1
  24. package/dist/esm/src/assetManager.js +20 -4
  25. package/dist/esm/src/assets/routes.js +22 -1
  26. package/dist/esm/src/containerManager.d.ts +1 -1
  27. package/dist/esm/src/containerManager.js +132 -122
  28. package/dist/esm/src/instanceManager.d.ts +4 -3
  29. package/dist/esm/src/instanceManager.js +87 -60
  30. package/dist/esm/src/instances/routes.js +21 -11
  31. package/dist/esm/src/operatorManager.d.ts +5 -3
  32. package/dist/esm/src/operatorManager.js +34 -22
  33. package/dist/esm/src/providerManager.js +1 -1
  34. package/dist/esm/src/repositoryManager.d.ts +2 -4
  35. package/dist/esm/src/repositoryManager.js +51 -66
  36. package/dist/esm/src/socketManager.js +1 -1
  37. package/dist/esm/src/taskManager.d.ts +64 -0
  38. package/dist/esm/src/taskManager.js +159 -0
  39. package/dist/esm/src/tasks/routes.d.ts +3 -0
  40. package/dist/esm/src/tasks/routes.js +30 -0
  41. package/dist/esm/src/utils/BlockInstanceRunner.js +0 -1
  42. package/index.ts +2 -0
  43. package/package.json +1 -1
  44. package/src/assetManager.ts +28 -4
  45. package/src/assets/routes.ts +23 -1
  46. package/src/containerManager.ts +153 -142
  47. package/src/instanceManager.ts +116 -70
  48. package/src/instances/routes.ts +20 -12
  49. package/src/operatorManager.ts +46 -26
  50. package/src/providerManager.ts +1 -1
  51. package/src/repositoryManager.ts +65 -63
  52. package/src/socketManager.ts +1 -1
  53. package/src/taskManager.ts +225 -0
  54. package/src/tasks/routes.ts +38 -0
  55. package/src/utils/BlockInstanceRunner.ts +0 -4
@@ -9,11 +9,12 @@ import { assetManager } from './assetManager';
9
9
  import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
10
10
  import { configManager } from './configManager';
11
11
  import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceStatus, InstanceType, LogEntry } from './types';
12
- import { BlockDefinitionSpec, BlockInstance } from '@kapeta/schemas';
12
+ import { BlockDefinitionSpec, BlockInstance, Plan } from '@kapeta/schemas';
13
13
  import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
14
14
  import { KIND_OPERATOR, operatorManager } from './operatorManager';
15
15
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
16
16
  import { definitionsManager } from './definitionsManager';
17
+ import { Task, taskManager } from './taskManager';
17
18
 
18
19
  const CHECK_INTERVAL = 5000;
19
20
  const DEFAULT_HEALTH_PORT_TYPE = 'rest';
@@ -64,7 +65,19 @@ export class InstanceManager {
64
65
 
65
66
  systemId = normalizeKapetaUri(systemId);
66
67
 
67
- return this._instances.filter((instance) => instance.systemId === systemId);
68
+ const planInfo = definitionsManager.getDefinition(systemId);
69
+
70
+ if (!planInfo) {
71
+ return [];
72
+ }
73
+
74
+ const plan = planInfo.definition as Plan;
75
+
76
+ const instanceIds = plan.spec.blocks.map((block) => block.id);
77
+
78
+ return this._instances.filter(
79
+ (instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId)
80
+ );
68
81
  }
69
82
 
70
83
  public getInstance(systemId: string, instanceId: string) {
@@ -261,35 +274,51 @@ export class InstanceManager {
261
274
  });
262
275
  }
263
276
 
264
- public async startAllForPlan(systemId: string): Promise<InstanceInfo[]> {
277
+ public async startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>> {
265
278
  systemId = normalizeKapetaUri(systemId);
266
279
  const plan = await assetManager.getPlan(systemId, true);
267
280
  if (!plan) {
268
- throw new Error('Plan not found: ' + systemId);
281
+ throw new Error(`Plan not found: ${systemId}`);
269
282
  }
270
283
 
271
284
  if (!plan.spec.blocks) {
272
- console.warn('No blocks found in plan', systemId);
273
- return [];
285
+ throw new Error(`No blocks found in plan: ${systemId}`);
274
286
  }
275
287
 
276
- let promises: Promise<InstanceInfo>[] = [];
277
- let errors = [];
278
- for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
279
- try {
280
- promises.push(this.start(systemId, blockInstance.id));
281
- } catch (e) {
282
- errors.push(e);
283
- }
284
- }
288
+ return taskManager.add(
289
+ `plan:start:${systemId}`,
290
+ async () => {
291
+ let promises: Promise<InstanceInfo>[] = [];
292
+ let errors = [];
293
+ for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
294
+ try {
295
+ promises.push(
296
+ this.start(systemId, blockInstance.id).then((taskOrInstance) => {
297
+ if (taskOrInstance instanceof Task) {
298
+ return taskOrInstance.wait();
299
+ }
300
+ return taskOrInstance;
301
+ })
302
+ );
303
+ } catch (e) {
304
+ errors.push(e);
305
+ }
306
+ }
285
307
 
286
- const settled = await Promise.allSettled(promises);
308
+ const settled = await Promise.allSettled(promises);
287
309
 
288
- if (errors.length > 0) {
289
- throw errors[0];
290
- }
310
+ if (errors.length > 0) {
311
+ throw errors[0];
312
+ }
291
313
 
292
- return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as InstanceInfo[];
314
+ return settled
315
+ .map((p) => (p.status === 'fulfilled' ? p.value : null))
316
+ .filter((p) => !!p) as InstanceInfo[];
317
+ },
318
+ {
319
+ name: `Starting plan ${systemId}`,
320
+ }
321
+ );
293
322
  }
294
323
 
295
324
  public async stop(systemId: string, instanceId: string) {
@@ -353,14 +382,21 @@ export class InstanceManager {
353
382
  });
354
383
  }
355
384
 
356
- public async stopAllForPlan(systemId: string) {
385
+ public stopAllForPlan(systemId: string) {
357
386
  systemId = normalizeKapetaUri(systemId);
358
387
  const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
359
-
360
- return this.stopInstances(instancesForPlan);
388
+ return taskManager.add(
389
+ `plan:stop:${systemId}`,
390
+ async () => {
391
+ return this.stopInstances(instancesForPlan);
392
+ },
393
+ {
394
+ name: `Stopping plan ${systemId}`,
395
+ }
396
+ );
361
397
  }
362
398
 
363
- public async start(systemId: string, instanceId: string): Promise<InstanceInfo> {
399
+ public async start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>> {
364
400
  return this.exclusive(systemId, instanceId, async () => {
365
401
  systemId = normalizeKapetaUri(systemId);
366
402
  const plan = await assetManager.getPlan(systemId, true);
@@ -453,53 +489,63 @@ export class InstanceManager {
453
489
  }
454
490
 
455
491
  const instanceConfig = await configManager.getConfigForSection(systemId, instanceId);
456
- const runner = new BlockInstanceRunner(systemId);
457
-
458
- const startTime = Date.now();
459
- try {
460
- const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
461
-
462
- instance.status = InstanceStatus.READY;
463
-
464
- return this.saveInternalInstance({
465
- ...instance,
466
- type: processInfo.type,
467
- pid: processInfo.pid ?? -1,
468
- health: null,
469
- portType: processInfo.portType,
470
- status: InstanceStatus.READY,
471
- });
472
- } catch (e: any) {
473
- console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
474
- const logs: LogEntry[] = [
475
- {
476
- source: 'stdout',
477
- level: 'ERROR',
478
- message: e.message,
479
- time: Date.now(),
480
- },
481
- ];
482
-
483
- const out = await this.saveInternalInstance({
484
- ...instance,
485
- type: InstanceType.LOCAL,
486
- pid: null,
487
- health: null,
488
- portType: DEFAULT_HEALTH_PORT_TYPE,
489
- status: InstanceStatus.FAILED,
490
- errorMessage: e.message ?? 'Failed to start - Check logs for details.',
491
- });
492
-
493
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
494
-
495
- this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
496
- error: `Failed to start instance: ${e.message}`,
497
- status: EVENT_INSTANCE_EXITED,
498
- instanceId: blockInstance.id,
499
- });
492
+ const task = taskManager.add(
493
+ `instance:start:${systemId}:${instanceId}`,
494
+ async () => {
495
+ const runner = new BlockInstanceRunner(systemId);
496
+ const startTime = Date.now();
497
+ try {
498
+ const processInfo = await runner.start(blockRef, instanceId, instanceConfig);
499
+
500
+ instance.status = InstanceStatus.READY;
501
+
502
+ return this.saveInternalInstance({
503
+ ...instance,
504
+ type: processInfo.type,
505
+ pid: processInfo.pid ?? -1,
506
+ health: null,
507
+ portType: processInfo.portType,
508
+ status: InstanceStatus.READY,
509
+ });
510
+ } catch (e: any) {
511
+ console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
512
+ const logs: LogEntry[] = [
513
+ {
514
+ source: 'stdout',
515
+ level: 'ERROR',
516
+ message: e.message,
517
+ time: Date.now(),
518
+ },
519
+ ];
520
+
521
+ const out = await this.saveInternalInstance({
522
+ ...instance,
523
+ type: InstanceType.UNKNOWN,
524
+ pid: null,
525
+ health: null,
526
+ portType: DEFAULT_HEALTH_PORT_TYPE,
527
+ status: InstanceStatus.FAILED,
528
+ errorMessage: e.message ?? 'Failed to start - Check logs for details.',
529
+ });
530
+
531
+ this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
532
+
533
+ this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
534
+ error: `Failed to start instance: ${e.message}`,
535
+ status: EVENT_INSTANCE_EXITED,
536
+ instanceId: blockInstance.id,
537
+ });
538
+
539
+ return out;
540
+ }
541
+ },
542
+ {
543
+ name: `Starting instance: ${instance.name}`,
544
+ systemId,
545
+ }
546
+ );
500
547
 
501
- return out;
502
- }
548
+ return task;
503
549
  });
504
550
  }
505
551
 
@@ -6,6 +6,7 @@ import { NextFunction, Request, Response } from 'express';
6
6
  import { kapetaHeaders, KapetaRequest } from '../middleware/kapeta';
7
7
  import { stringBody } from '../middleware/stringBody';
8
8
  import { DesiredInstanceStatus, InstanceInfo, InstanceOwner, InstanceType, KapetaBodyRequest } from '../types';
9
+ import { Task } from '../taskManager';
9
10
 
10
11
  const router = Router();
11
12
  router.use('/', corsHandler);
@@ -28,13 +29,11 @@ router.get('/:systemId/instances', (req: Request, res: Response) => {
28
29
  * Start all instances in a plan
29
30
  */
30
31
  router.post('/:systemId/start', async (req: Request, res: Response) => {
31
- const instances = await instanceManager.startAllForPlan(req.params.systemId);
32
+ const task = await instanceManager.startAllForPlan(req.params.systemId);
32
33
 
33
34
  res.status(202).send({
34
35
  ok: true,
35
- processes: instances.map((p) => {
36
- return { pid: p.pid, type: p.type };
37
- }),
36
+ taskId: task.id,
38
37
  });
39
38
  });
40
39
 
@@ -42,10 +41,11 @@ router.post('/:systemId/start', async (req: Request, res: Response) => {
42
41
  * Stop all instances in plan
43
42
  */
44
43
  router.post('/:systemId/stop', async (req: Request, res: Response) => {
45
- await instanceManager.stopAllForPlan(req.params.systemId);
44
+ const task = instanceManager.stopAllForPlan(req.params.systemId);
46
45
 
47
46
  res.status(202).send({
48
47
  ok: true,
48
+ taskId: task.id,
49
49
  });
50
50
  });
51
51
 
@@ -53,13 +53,19 @@ router.post('/:systemId/stop', async (req: Request, res: Response) => {
53
53
  * Start single instance in a plan
54
54
  */
55
55
  router.post('/:systemId/:instanceId/start', async (req: Request, res: Response) => {
56
- const process = await instanceManager.start(req.params.systemId, req.params.instanceId);
57
-
58
- res.status(202).send({
59
- ok: true,
60
- pid: process.pid,
61
- type: process.type,
62
- });
56
+ const result = await instanceManager.start(req.params.systemId, req.params.instanceId);
57
+ if (result instanceof Task) {
58
+ res.status(202).send({
59
+ ok: true,
60
+ taskId: result.id,
61
+ });
62
+ } else {
63
+ res.status(202).send({
64
+ ok: true,
65
+ pid: result.pid,
66
+ type: result.type,
67
+ });
68
+ }
63
69
  });
64
70
 
65
71
  /**
@@ -146,8 +152,10 @@ router.put('/', async (req: KapetaBodyRequest, res: Response) => {
146
152
  const oldInstance = instanceManager.getInstance(req.kapeta!.systemId, req.kapeta!.instanceId);
147
153
  if (oldInstance) {
148
154
  instance.pid = oldInstance.pid;
155
+ instance.desiredStatus = oldInstance.desiredStatus;
149
156
  }
150
157
  instance.type = InstanceType.DOCKER;
158
+ instance.owner = InstanceOwner.INTERNAL;
151
159
  } else {
152
160
  // Coming from user starting the instance outside of kapeta
153
161
  instance.type = InstanceType.LOCAL;
@@ -12,21 +12,26 @@ import { definitionsManager } from './definitionsManager';
12
12
  import { getBindHost, normalizeKapetaUri } from './utils/utils';
13
13
  import _ from 'lodash';
14
14
  import AsyncLock from 'async-lock';
15
+ import { taskManager } from './taskManager';
15
16
 
16
17
  export const KIND_OPERATOR = 'core/resource-type-operator';
17
18
 
18
19
  class Operator {
19
- private _data: any;
20
- constructor(data: any) {
20
+ private readonly _data: DefinitionInfo;
21
+ constructor(data: DefinitionInfo) {
21
22
  this._data = data;
22
23
  }
23
24
 
24
- getData() {
25
+ getLocalData() {
26
+ return this._data.definition.spec.local;
27
+ }
28
+
29
+ getDefinitionInfo() {
25
30
  return this._data;
26
31
  }
27
32
 
28
33
  getCredentials() {
29
- return this._data.credentials;
34
+ return this._data.definition.spec.local.credentials;
30
35
  }
31
36
  }
32
37
 
@@ -72,7 +77,7 @@ class OperatorManager {
72
77
  throw new Error(`Operator missing local definition: ${resourceType}:${version}`);
73
78
  }
74
79
 
75
- return new Operator(operator.definition.spec.local);
80
+ return new Operator(operator);
76
81
  }
77
82
 
78
83
  /**
@@ -161,7 +166,7 @@ class OperatorManager {
161
166
  return await this.operatorLock.acquire(key, async () => {
162
167
  const operator = this.getOperator(resourceType, version);
163
168
 
164
- const operatorData = operator.getData();
169
+ const operatorData = operator.getLocalData();
165
170
 
166
171
  const portTypes = Object.keys(operatorData.ports);
167
172
 
@@ -203,6 +208,8 @@ class OperatorManager {
203
208
  kapeta: 'true',
204
209
  };
205
210
 
211
+ const operatorMetadata = operator.getDefinitionInfo().definition.metadata;
212
+
206
213
  const bindHost = getBindHost();
207
214
 
208
215
  const ExposedPorts: { [key: string]: any } = {};
@@ -225,28 +232,41 @@ class OperatorManager {
225
232
  Env.push(name + '=' + value);
226
233
  });
227
234
 
228
- let HealthCheck = undefined;
229
-
230
- if (operatorData.health) {
231
- HealthCheck = containerManager.toDockerHealth(operatorData.health);
232
- }
233
-
234
- const container = await containerManager.ensureContainer({
235
- name: containerName,
236
- Image: operatorData.image,
237
- Hostname: containerName + '.kapeta',
238
- Labels,
239
- Cmd: operatorData.cmd,
240
- ExposedPorts,
241
- Env,
242
- HealthCheck,
243
- HostConfig: {
244
- PortBindings,
245
- Mounts,
235
+ const task = taskManager.add(
236
+ `operator:ensure:${key}`,
237
+ async () => {
238
+ let HealthCheck = undefined;
239
+
240
+ if (operatorData.health) {
241
+ HealthCheck = containerManager.toDockerHealth(operatorData.health);
242
+ }
243
+
244
+ const container = await containerManager.ensureContainer({
245
+ name: containerName,
246
+ Image: operatorData.image,
247
+ Hostname: containerName + '.kapeta',
248
+ Labels,
249
+ Cmd: operatorData.cmd,
250
+ ExposedPorts,
251
+ Env,
252
+ HealthCheck,
253
+ HostConfig: {
254
+ PortBindings,
255
+ Mounts,
256
+ },
257
+ });
258
+
259
+ await containerManager.waitForReady(container);
260
+
261
+ return new ContainerInfo(container);
246
262
  },
247
- });
263
+ {
264
+ name: `Ensuring ${operatorMetadata.title ?? operatorMetadata.name}`,
265
+ systemId,
266
+ }
267
+ );
248
268
 
249
- return new ContainerInfo(container);
269
+ return task.wait();
250
270
  });
251
271
  }
252
272
  }
@@ -23,7 +23,7 @@ class ProviderManager {
23
23
  return FSExtra.readFile(this._webAssetCache[id], 'utf8');
24
24
  }
25
25
 
26
- await repositoryManager.ensureAsset(handle, name, version);
26
+ await repositoryManager.ensureAsset(handle, name, version, true);
27
27
 
28
28
  const installedProvider = this.getWebProviders().find((providerDefinition) => {
29
29
  return providerDefinition.definition.metadata.name === fullName && providerDefinition.version === version;
@@ -10,6 +10,9 @@ import { progressListener } from './progressListener';
10
10
  import { Dependency } from '@kapeta/schemas';
11
11
  import { Actions, Config, RegistryService } from '@kapeta/nodejs-registry-utils';
12
12
  import { definitionsManager } from './definitionsManager';
13
+ import { Task, taskManager } from './taskManager';
14
+ import { normalizeKapetaUri } from './utils/utils';
15
+ import { assetManager } from './assetManager';
13
16
 
14
17
  const INSTALL_ATTEMPTED: { [p: string]: boolean } = {};
15
18
 
@@ -18,14 +21,12 @@ class RepositoryManager {
18
21
  private _registryService: RegistryService;
19
22
  private _cache: { [key: string]: boolean };
20
23
  private watcher?: () => void;
21
- private _installQueue: (() => Promise<void>)[];
22
- private _processing: boolean = false;
24
+
23
25
  constructor() {
24
26
  this.changeEventsEnabled = true;
25
27
  this.listenForChanges();
26
28
  this._registryService = new RegistryService(Config.data.registry.url);
27
29
  this._cache = {};
28
- this._installQueue = [];
29
30
  }
30
31
 
31
32
  setChangeEventsEnabled(enabled: boolean) {
@@ -113,75 +114,69 @@ class RepositoryManager {
113
114
  this.watcher = undefined;
114
115
  }
115
116
 
116
- private async _install(refs: string[]): Promise<void> {
117
+ private async _install(refs: string[]): Promise<Task[]> {
117
118
  //We make sure to only install one asset at a time - otherwise unexpected things might happen
118
- const out = new Promise<void>((resolve, reject) => {
119
- this._installQueue.push(async () => {
119
+ const createInstaller = (ref: string) => {
120
+ return async () => {
121
+ if (INSTALL_ATTEMPTED[ref]) {
122
+ return;
123
+ }
124
+
125
+ if (definitionsManager.exists(ref)) {
126
+ return;
127
+ }
128
+ console.log(`Installing asset: ${ref}`);
129
+ INSTALL_ATTEMPTED[ref] = true;
130
+ //Auto-install missing asset
120
131
  try {
121
- const normalizedRefs = refs.map((ref) => parseKapetaUri(ref).id);
122
- const filteredRefs = normalizedRefs
123
- .filter((ref) => !INSTALL_ATTEMPTED[ref])
124
- .filter((ref) => !definitionsManager.exists(ref));
125
- if (filteredRefs.length > 0) {
126
- console.log(`Auto-installing dependencies: ${filteredRefs.join(', ')}`);
127
- filteredRefs.forEach((ref) => (INSTALL_ATTEMPTED[ref] = true));
128
- //Auto-install missing asset
129
- try {
130
- //We change to a temp dir to avoid issues with the current working directory
131
- process.chdir(os.tmpdir());
132
- //Disable change events while installing
133
- this.setChangeEventsEnabled(false);
134
- socketManager.emit(`install`, 'install:action', {
135
- type: 'start',
136
- refs,
137
- });
138
- await Actions.install(progressListener, normalizedRefs, {});
139
- socketManager.emit(`install`, 'install:action', {
140
- type: 'done',
141
- refs,
142
- });
143
- } catch (e: any) {
144
- socketManager.emit(`install`, 'install:action', {
145
- type: 'failed',
146
- refs,
147
- error: e.message,
148
- });
149
- } finally {
150
- this.setChangeEventsEnabled(true);
151
- }
152
- }
153
- resolve();
154
- } catch (e) {
155
- reject(e);
132
+ //We change to a temp dir to avoid issues with the current working directory
133
+ process.chdir(os.tmpdir());
134
+ //Disable change events while installing
135
+ this.setChangeEventsEnabled(false);
136
+ await Actions.install(progressListener, [ref], {});
156
137
  } finally {
157
- this._processNext().catch((e) => console.error(e));
138
+ this.setChangeEventsEnabled(true);
158
139
  }
159
- });
160
- });
140
+ definitionsManager.clearCache();
141
+ assetManager.clearCache();
142
+ console.log(`Asset installed: ${ref}`);
143
+ };
144
+ };
161
145
 
162
- this._processNext().catch((e) => console.error(e));
146
+ const tasks: Task[] = [];
163
147
 
164
- return out;
165
- }
148
+ while (refs.length > 0) {
149
+ let ref = refs.shift();
150
+ if (!ref) {
151
+ continue;
152
+ }
153
+ ref = normalizeKapetaUri(ref);
166
154
 
167
- async _processNext() {
168
- if (this._processing) {
169
- return;
170
- }
171
- this._processing = true;
172
- try {
173
- while (this._installQueue.length > 0) {
174
- const item = this._installQueue.shift();
175
- if (item) {
176
- await item();
177
- }
155
+ if (INSTALL_ATTEMPTED[ref]) {
156
+ continue;
178
157
  }
179
- } finally {
180
- this._processing = false;
158
+
159
+ if (definitionsManager.exists(ref)) {
160
+ continue;
161
+ }
162
+
163
+ const task = taskManager.add(`asset:install:${ref}`, createInstaller(ref), {
164
+ name: `Installing ${ref}`,
165
+ group: 'asset:install:',
166
+ });
167
+
168
+ tasks.push(task);
181
169
  }
170
+
171
+ return tasks;
182
172
  }
183
173
 
184
- async ensureAsset(handle: string, name: string, version: string) {
174
+ async ensureAsset(
175
+ handle: string,
176
+ name: string,
177
+ version: string,
178
+ wait: boolean = true
179
+ ): Promise<undefined | Task[]> {
185
180
  const fullName = `${handle}/${name}`;
186
181
  const ref = `${fullName}:${version}`;
187
182
 
@@ -219,15 +214,22 @@ class RepositoryManager {
219
214
  }
220
215
 
221
216
  this._cache[ref] = true;
217
+ let tasks: Task[] | undefined = undefined;
222
218
  if (!installedAsset) {
223
- await this._install([ref]);
219
+ tasks = await this._install([ref]);
224
220
  } else {
225
221
  //Ensure dependencies are installed
226
222
  const refs = assetVersion.dependencies.map((dep: Dependency) => dep.name);
227
223
  if (refs.length > 0) {
228
- await this._install(refs);
224
+ tasks = await this._install(refs);
229
225
  }
230
226
  }
227
+
228
+ if (tasks && wait) {
229
+ await Promise.all(tasks.map((t) => t.future.promise));
230
+ }
231
+
232
+ return tasks;
231
233
  }
232
234
  }
233
235
 
@@ -32,7 +32,7 @@ export class SocketManager {
32
32
  }
33
33
 
34
34
  emitGlobal(type: string, payload: any) {
35
- this.io.emit(type, { payload });
35
+ this.io.emit(type, payload);
36
36
  }
37
37
 
38
38
  _bindIO() {