@kapeta/local-cluster-service 0.15.3 → 0.16.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/src/RepositoryWatcher.d.ts +23 -0
  3. package/dist/cjs/src/RepositoryWatcher.js +269 -0
  4. package/dist/cjs/src/assetManager.d.ts +4 -6
  5. package/dist/cjs/src/assetManager.js +34 -44
  6. package/dist/cjs/src/assets/routes.js +2 -2
  7. package/dist/cjs/src/cacheManager.d.ts +16 -0
  8. package/dist/cjs/src/cacheManager.js +38 -0
  9. package/dist/cjs/src/definitionsManager.d.ts +2 -4
  10. package/dist/cjs/src/definitionsManager.js +7 -21
  11. package/dist/cjs/src/instanceManager.js +4 -1
  12. package/dist/cjs/src/repositoryManager.d.ts +9 -4
  13. package/dist/cjs/src/repositoryManager.js +18 -91
  14. package/dist/cjs/src/types.d.ts +2 -0
  15. package/dist/cjs/src/utils/utils.js +1 -1
  16. package/dist/esm/src/RepositoryWatcher.d.ts +23 -0
  17. package/dist/esm/src/RepositoryWatcher.js +262 -0
  18. package/dist/esm/src/assetManager.d.ts +4 -6
  19. package/dist/esm/src/assetManager.js +35 -45
  20. package/dist/esm/src/assets/routes.js +2 -2
  21. package/dist/esm/src/cacheManager.d.ts +16 -0
  22. package/dist/esm/src/cacheManager.js +31 -0
  23. package/dist/esm/src/definitionsManager.d.ts +2 -4
  24. package/dist/esm/src/definitionsManager.js +7 -21
  25. package/dist/esm/src/instanceManager.js +4 -1
  26. package/dist/esm/src/repositoryManager.d.ts +9 -4
  27. package/dist/esm/src/repositoryManager.js +18 -91
  28. package/dist/esm/src/types.d.ts +2 -0
  29. package/dist/esm/src/utils/utils.js +1 -1
  30. package/package.json +2 -2
  31. package/src/RepositoryWatcher.ts +302 -0
  32. package/src/assetManager.ts +51 -55
  33. package/src/assets/routes.ts +2 -2
  34. package/src/cacheManager.ts +45 -0
  35. package/src/definitionsManager.ts +9 -33
  36. package/src/identities/routes.ts +0 -1
  37. package/src/instanceManager.ts +4 -1
  38. package/src/repositoryManager.ts +20 -95
  39. package/src/types.ts +2 -2
  40. package/src/utils/utils.ts +1 -1
@@ -1,8 +1,6 @@
1
1
  import Path from 'node:path';
2
- import FS from 'node:fs';
3
- import FSExtra from 'fs-extra';
2
+ import FS from 'fs-extra';
4
3
  import YAML from 'yaml';
5
- import NodeCache from 'node-cache';
6
4
  import { Definition, DefinitionInfo } from '@kapeta/local-cluster-config';
7
5
  import { codeGeneratorManager } from './codeGeneratorManager';
8
6
  import { ProgressListener } from './progressListener';
@@ -13,6 +11,10 @@ import { Actions } from '@kapeta/nodejs-registry-utils';
13
11
  import { definitionsManager } from './definitionsManager';
14
12
  import { normalizeKapetaUri } from './utils/utils';
15
13
  import { taskManager } from './taskManager';
14
+ import { SourceOfChange } from './types';
15
+ import { cacheManager } from './cacheManager';
16
+
17
+ const CACHE_TTL = 60 * 60 * 1000; // 1 hour
16
18
 
17
19
  export interface EnrichedAsset {
18
20
  ref: string;
@@ -55,18 +57,6 @@ function parseRef(ref: string) {
55
57
  }
56
58
 
57
59
  class AssetManager {
58
- private cache: NodeCache;
59
-
60
- constructor() {
61
- this.cache = new NodeCache({
62
- stdTTL: 60 * 60, // 1 hour
63
- });
64
- }
65
-
66
- public clearCache() {
67
- this.cache.flushAll();
68
- }
69
-
70
60
  /**
71
61
  *
72
62
  * @param {string[]} [assetKinds]
@@ -110,8 +100,8 @@ class AssetManager {
110
100
  ): Promise<EnrichedAsset | undefined> {
111
101
  ref = normalizeKapetaUri(ref);
112
102
  const cacheKey = `getAsset:${ref}`;
113
- if (!noCache && this.cache.has(cacheKey)) {
114
- return this.cache.get(cacheKey);
103
+ if (!noCache && cacheManager.has(cacheKey)) {
104
+ return cacheManager.get(cacheKey);
115
105
  }
116
106
  const uri = parseKapetaUri(ref);
117
107
  if (autoFetch) {
@@ -126,30 +116,30 @@ class AssetManager {
126
116
  throw new Error('Asset not found: ' + ref);
127
117
  }
128
118
  if (asset) {
129
- this.cache.set(cacheKey, asset);
119
+ cacheManager.set(cacheKey, asset, CACHE_TTL);
130
120
  }
131
121
 
132
122
  return asset;
133
123
  }
134
124
 
135
- async createAsset(path: string, yaml: BlockDefinition): Promise<EnrichedAsset[]> {
136
- if (FS.existsSync(path)) {
125
+ async createAsset(
126
+ path: string,
127
+ yaml: BlockDefinition,
128
+ sourceOfChange: SourceOfChange = 'filesystem'
129
+ ): Promise<EnrichedAsset[]> {
130
+ if (await FS.pathExists(path)) {
137
131
  throw new Error('File already exists: ' + path);
138
132
  }
139
133
 
140
134
  const dirName = Path.dirname(path);
141
- if (!FS.existsSync(dirName)) {
142
- FSExtra.mkdirpSync(dirName);
135
+ if (!(await FS.pathExists(dirName))) {
136
+ await FS.mkdirp(dirName);
143
137
  }
144
-
145
- console.log('Wrote to ' + path);
146
- FS.writeFileSync(path, YAML.stringify(yaml));
147
-
138
+ await repositoryManager.setSourceOfChangeFor(path, sourceOfChange);
139
+ await FS.writeFile(path, YAML.stringify(yaml));
148
140
  const asset = await this.importFile(path);
149
- console.log('Imported');
150
141
 
151
- this.cache.flushAll();
152
- definitionsManager.clearCache();
142
+ cacheManager.flush();
153
143
 
154
144
  const ref = `kapeta://${yaml.metadata.name}:local`;
155
145
 
@@ -158,7 +148,7 @@ class AssetManager {
158
148
  return asset;
159
149
  }
160
150
 
161
- async updateAsset(ref: string, yaml: BlockDefinition) {
151
+ async updateAsset(ref: string, yaml: BlockDefinition, sourceOfChange: SourceOfChange = 'filesystem') {
162
152
  const asset = await this.getAsset(ref, true, false);
163
153
  if (!asset) {
164
154
  throw new Error('Attempted to update unknown asset: ' + ref);
@@ -172,30 +162,13 @@ class AssetManager {
172
162
  throw new Error('Attempted to update corrupted asset: ' + ref);
173
163
  }
174
164
 
165
+ await repositoryManager.setSourceOfChangeFor(asset.ymlPath, sourceOfChange);
166
+ await FS.writeFile(asset.ymlPath, YAML.stringify(yaml));
175
167
  console.log('Wrote to ' + asset.ymlPath);
176
- FS.writeFileSync(asset.ymlPath, YAML.stringify(yaml));
177
- this.cache.flushAll();
178
- definitionsManager.clearCache();
179
168
 
180
- this.maybeGenerateCode(asset.ref, asset.ymlPath, yaml);
181
- }
169
+ cacheManager.flush();
182
170
 
183
- private maybeGenerateCode(ref: string, ymlPath: string, block: BlockDefinition) {
184
- ref = normalizeKapetaUri(ref);
185
- if (codeGeneratorManager.canGenerateCode(block)) {
186
- const assetTitle = block.metadata.title ? block.metadata.title : parseKapetaUri(block.metadata.name).name;
187
- taskManager.add(
188
- `codegen:${ref}`,
189
- async () => {
190
- await codeGeneratorManager.generate(ymlPath, block);
191
- },
192
- {
193
- name: `Generating code for ${assetTitle}`,
194
- }
195
- );
196
- return true;
197
- }
198
- return false;
171
+ this.maybeGenerateCode(asset.ref, asset.ymlPath, yaml);
199
172
  }
200
173
 
201
174
  async importFile(filePath: string) {
@@ -203,17 +176,20 @@ class AssetManager {
203
176
  filePath = filePath.substring('file://'.length);
204
177
  }
205
178
 
206
- if (!FS.existsSync(filePath)) {
179
+ if (!(await FS.pathExists(filePath))) {
207
180
  throw new Error('File not found: ' + filePath);
208
181
  }
182
+ const content = await FS.readFile(filePath);
209
183
 
210
- const assetInfos = YAML.parseAllDocuments(FS.readFileSync(filePath).toString()).map((doc) => doc.toJSON());
184
+ const assetInfos = YAML.parseAllDocuments(content.toString()).map((doc) => doc.toJSON());
211
185
 
212
186
  await Actions.link(new ProgressListener(), Path.dirname(filePath));
213
187
 
214
188
  const version = 'local';
215
189
  const refs = assetInfos.map((assetInfo) => `kapeta://${assetInfo.metadata.name}:${version}`);
216
- this.cache.flushAll();
190
+
191
+ cacheManager.flush();
192
+
217
193
  return this.getAssets().filter((a) => refs.some((ref) => compareRefs(ref, a.ref)));
218
194
  }
219
195
 
@@ -222,7 +198,9 @@ class AssetManager {
222
198
  if (!asset) {
223
199
  throw new Error('Asset does not exists: ' + ref);
224
200
  }
225
- this.cache.flushAll();
201
+
202
+ cacheManager.flush();
203
+
226
204
  await Actions.uninstall(new ProgressListener(), [asset.ref]);
227
205
  }
228
206
 
@@ -236,6 +214,24 @@ class AssetManager {
236
214
 
237
215
  return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
238
216
  }
217
+
218
+ private maybeGenerateCode(ref: string, ymlPath: string, block: BlockDefinition) {
219
+ ref = normalizeKapetaUri(ref);
220
+ if (codeGeneratorManager.canGenerateCode(block)) {
221
+ const assetTitle = block.metadata.title ? block.metadata.title : parseKapetaUri(block.metadata.name).name;
222
+ taskManager.add(
223
+ `codegen:${ref}`,
224
+ async () => {
225
+ await codeGeneratorManager.generate(ymlPath, block);
226
+ },
227
+ {
228
+ name: `Generating code for ${assetTitle}`,
229
+ }
230
+ );
231
+ return true;
232
+ }
233
+ return false;
234
+ }
239
235
  }
240
236
 
241
237
  export const assetManager = new AssetManager();
@@ -70,7 +70,7 @@ router.post('/create', async (req: Request, res: Response) => {
70
70
  const content = parseBody(req);
71
71
 
72
72
  try {
73
- const assets = await assetManager.createAsset(req.query.path as string, content);
73
+ const assets = await assetManager.createAsset(req.query.path as string, content, 'user');
74
74
 
75
75
  res.status(200).send(assets);
76
76
  } catch (err: any) {
@@ -91,7 +91,7 @@ router.put('/update', async (req: Request, res: Response) => {
91
91
  const content = parseBody(req);
92
92
 
93
93
  try {
94
- await assetManager.updateAsset(req.query.ref as string, content);
94
+ await assetManager.updateAsset(req.query.ref as string, content, 'user');
95
95
 
96
96
  res.sendStatus(204);
97
97
  } catch (err: any) {
@@ -0,0 +1,45 @@
1
+ import NodeCache from 'node-cache';
2
+
3
+ const DEFAULT_CACHE_TTL = 60 * 1000; // 1 min
4
+ export interface CacheEntry<T = any> {
5
+ expires: number;
6
+ data: T;
7
+ }
8
+ class CacheManager {
9
+ private cache: NodeCache = new NodeCache();
10
+
11
+ public flush() {
12
+ this.cache.flushAll();
13
+ }
14
+
15
+ public doCached<T>(key: string, getter: () => T, ttl = DEFAULT_CACHE_TTL) {
16
+ const data = this.cache.get<T>(key);
17
+ if (data !== undefined) {
18
+ return data;
19
+ }
20
+ const result = getter();
21
+ this.cache.set(key, result, ttl);
22
+ return result;
23
+ }
24
+
25
+ public get<T>(key: string): T | undefined {
26
+ return this.cache.get<T>(key);
27
+ }
28
+
29
+ public set<T>(key: string, data: T, ttl = DEFAULT_CACHE_TTL) {
30
+ this.cache.set(key, data, ttl);
31
+ }
32
+
33
+ public has(key: string): boolean {
34
+ return this.cache.has(key);
35
+ }
36
+
37
+ public remove(key: string) {
38
+ return this.cache.del(key);
39
+ }
40
+ }
41
+
42
+ export const cacheManager = new CacheManager();
43
+
44
+ export const doCached = <T>(key: string, getter: () => T, ttl = DEFAULT_CACHE_TTL) =>
45
+ cacheManager.doCached(key, getter, ttl);
@@ -1,17 +1,9 @@
1
1
  import ClusterConfiguration, { DefinitionInfo } from '@kapeta/local-cluster-config';
2
2
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
3
-
4
- const CACHE_TTL = 60 * 1000; // 1 min
5
-
6
- interface DefinitionCacheEntry {
7
- expires: number;
8
- definitions: DefinitionInfo[];
9
- }
3
+ import { doCached } from './cacheManager';
10
4
 
11
5
  class DefinitionsManager {
12
- private cache: { [key: string]: DefinitionCacheEntry } = {};
13
-
14
- private getKey(kindFilter?: string | string[]) {
6
+ private getHash(kindFilter?: string | string[]) {
15
7
  if (kindFilter) {
16
8
  if (Array.isArray(kindFilter)) {
17
9
  return kindFilter.join(',');
@@ -21,38 +13,22 @@ class DefinitionsManager {
21
13
  return 'none';
22
14
  }
23
15
 
24
- public clearCache() {
25
- this.cache = {};
26
- }
27
-
28
- private doCached(key: string, getter: () => DefinitionInfo[]) {
29
- if (this.cache[key]) {
30
- if (this.cache[key].expires > Date.now()) {
31
- return this.cache[key].definitions;
32
- }
33
- delete this.cache[key];
34
- }
35
-
36
- this.cache[key] = {
37
- expires: Date.now() + CACHE_TTL,
38
- definitions: getter(),
39
- };
40
-
41
- return this.cache[key].definitions;
16
+ private getFullKey(kindFilter?: string | string[]) {
17
+ return `DefinitionsManager:${this.getHash(kindFilter)}`;
42
18
  }
43
19
 
44
- public getDefinitions(kindFilter?: string | string[]) {
45
- const key = this.getKey(kindFilter);
20
+ public getDefinitions(kindFilter?: string | string[]): DefinitionInfo[] {
21
+ const key = this.getFullKey(kindFilter);
46
22
 
47
- return this.doCached(key, () => ClusterConfiguration.getDefinitions(kindFilter));
23
+ return doCached<DefinitionInfo[]>(key, () => ClusterConfiguration.getDefinitions(kindFilter));
48
24
  }
49
25
 
50
26
  public exists(ref: string) {
51
27
  return !!this.getDefinition(ref);
52
28
  }
53
29
 
54
- public getProviderDefinitions() {
55
- return this.doCached('providers', () => ClusterConfiguration.getProviderDefinitions());
30
+ public getProviderDefinitions(): DefinitionInfo[] {
31
+ return doCached<DefinitionInfo[]>('providers', () => ClusterConfiguration.getProviderDefinitions());
56
32
  }
57
33
 
58
34
  public getDefinition(ref: string) {
@@ -6,7 +6,6 @@ import { Request, Response } from 'express';
6
6
 
7
7
  const router = Router();
8
8
 
9
-
10
9
  router.use('/', corsHandler);
11
10
 
12
11
  router.get('/current', async (req: Request, res: Response) => {
@@ -74,8 +74,11 @@ export class InstanceManager {
74
74
  }
75
75
 
76
76
  const plan = planInfo.definition as Plan;
77
+ if (!plan?.spec?.blocks) {
78
+ return [];
79
+ }
77
80
 
78
- const instanceIds = plan.spec.blocks.map((block) => block.id);
81
+ const instanceIds = plan.spec?.blocks?.map((block) => block.id) || [];
79
82
 
80
83
  return this._instances.filter(
81
84
  (instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId)
@@ -1,18 +1,14 @@
1
- import FS from 'node:fs';
2
1
  import os from 'node:os';
3
- import Path from 'node:path';
4
- import watch from 'recursive-watch';
5
- import FSExtra from 'fs-extra';
6
- import ClusterConfiguration from '@kapeta/local-cluster-config';
7
- import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
2
  import { socketManager } from './socketManager';
9
3
  import { Dependency } from '@kapeta/schemas';
10
4
  import { Actions, Config, RegistryService } from '@kapeta/nodejs-registry-utils';
11
5
  import { definitionsManager } from './definitionsManager';
12
6
  import { Task, taskManager } from './taskManager';
13
7
  import { normalizeKapetaUri } from './utils/utils';
14
- import { assetManager } from './assetManager';
15
8
  import { ProgressListener } from './progressListener';
9
+ import { RepositoryWatcher } from './RepositoryWatcher';
10
+ import { SourceOfChange } from './types';
11
+ import { cacheManager } from './cacheManager';
16
12
 
17
13
  const EVENT_DEFAULT_PROVIDERS_START = 'default-providers-start';
18
14
  const EVENT_DEFAULT_PROVIDERS_END = 'default-providers-end';
@@ -35,101 +31,35 @@ const DEFAULT_PROVIDERS = [
35
31
  const INSTALL_ATTEMPTED: { [p: string]: boolean } = {};
36
32
 
37
33
  class RepositoryManager {
38
- private changeEventsEnabled: boolean;
39
34
  private _registryService: RegistryService;
40
35
  private _cache: { [key: string]: boolean };
41
- private watcher?: () => void;
36
+ private watcher: RepositoryWatcher;
42
37
 
43
38
  constructor() {
44
- this.changeEventsEnabled = true;
45
- this.listenForChanges();
46
39
  this._registryService = new RegistryService(Config.data.registry.url);
47
40
  this._cache = {};
48
- }
49
-
50
- setChangeEventsEnabled(enabled: boolean) {
51
- this.changeEventsEnabled = enabled;
41
+ this.watcher = new RepositoryWatcher();
42
+ this.listenForChanges();
52
43
  }
53
44
 
54
45
  listenForChanges() {
55
- const baseDir = ClusterConfiguration.getRepositoryBasedir();
56
- if (!FS.existsSync(baseDir)) {
57
- FSExtra.mkdirpSync(baseDir);
58
- }
59
-
60
- let allDefinitions = ClusterConfiguration.getDefinitions();
61
-
62
- console.log('Watching local repository for provider changes: %s', baseDir);
63
- try {
64
- this.watcher = watch(baseDir, (filename: string) => {
65
- if (!filename) {
66
- return;
67
- }
68
-
69
- const [handle, name, version] = filename.toString().split(/\//g);
70
- if (!name || !version) {
71
- return;
72
- }
73
-
74
- if (!this.changeEventsEnabled) {
75
- return;
76
- }
77
-
78
- const ymlPath = Path.join(baseDir, handle, name, version, 'kapeta.yml');
79
- const newDefinitions = ClusterConfiguration.getDefinitions();
80
-
81
- const newDefinition = newDefinitions.find((d) => d.ymlPath === ymlPath);
82
- let currentDefinition = allDefinitions.find((d) => d.ymlPath === ymlPath);
83
- const ymlExists = FS.existsSync(ymlPath);
84
- let type;
85
- if (ymlExists) {
86
- if (currentDefinition) {
87
- type = 'updated';
88
- } else if (newDefinition) {
89
- type = 'added';
90
- currentDefinition = newDefinition;
91
- } else {
92
- //Other definition was added / updated - ignore
93
- return;
94
- }
95
- } else {
96
- if (currentDefinition) {
97
- const ref = parseKapetaUri(
98
- `${currentDefinition.definition.metadata.name}:${currentDefinition.version}`
99
- ).id;
100
- delete INSTALL_ATTEMPTED[ref];
101
- //Something was removed
102
- type = 'removed';
103
- } else {
104
- //Other definition was removed - ignore
105
- return;
106
- }
107
- }
46
+ this.watcher.watch();
47
+ }
108
48
 
109
- const payload = {
110
- type,
111
- definition: currentDefinition?.definition,
112
- asset: { handle, name, version },
113
- };
49
+ async stopListening() {
50
+ return this.watcher.unwatch();
51
+ }
114
52
 
115
- allDefinitions = newDefinitions;
116
- socketManager.emit(`assets`, 'changed', payload);
117
- definitionsManager.clearCache();
118
- });
119
- } catch (e) {
120
- // Fallback to run without watch mode due to potential platform issues.
121
- // https://nodejs.org/docs/latest/api/fs.html#caveats
122
- console.log('Unable to watch for changes. Changes to assets will not update automatically.', e);
123
- return;
124
- }
53
+ /**
54
+ * Setting the source of change helps us know
55
+ * how to react to changes in the UI.
56
+ */
57
+ setSourceOfChangeFor(file: string, source: SourceOfChange) {
58
+ return this.watcher.setSourceOfChangeFor(file, source);
125
59
  }
126
60
 
127
- stopListening() {
128
- if (!this.watcher) {
129
- return;
130
- }
131
- this.watcher();
132
- this.watcher = undefined;
61
+ clearSourceOfChangeFor(file: string) {
62
+ return this.watcher.clearSourceOfChangeFor(file);
133
63
  }
134
64
 
135
65
  public ensureDefaultProviders(): void {
@@ -157,17 +87,12 @@ class RepositoryManager {
157
87
  try {
158
88
  //We change to a temp dir to avoid issues with the current working directory
159
89
  process.chdir(os.tmpdir());
160
- //Disable change events while installing
161
- this.setChangeEventsEnabled(false);
162
90
  await Actions.install(new ProgressListener(), [ref], {});
163
91
  } catch (e) {
164
92
  console.error(`Failed to install asset: ${ref}`, e);
165
93
  throw e;
166
- } finally {
167
- this.setChangeEventsEnabled(true);
168
94
  }
169
- definitionsManager.clearCache();
170
- assetManager.clearCache();
95
+ cacheManager.flush();
171
96
  //console.log(`Asset installed: ${ref}`);
172
97
  };
173
98
  };
package/src/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import EventEmitter from 'events';
2
1
  import express from 'express';
3
2
  import { Resource } from '@kapeta/schemas';
4
3
  import { StringBodyRequest } from './middleware/stringBody';
@@ -6,7 +5,8 @@ import { KapetaRequest } from './middleware/kapeta';
6
5
 
7
6
  export type StringMap = { [key: string]: string };
8
7
  export type AnyMap = { [key: string]: any };
9
-
8
+ export type SourceOfChange = 'user' | 'filesystem';
9
+ export type WatchEventName = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
10
10
  export type LogLevel = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE' | 'FATAL';
11
11
  export type LogSource = 'stdout' | 'stderr';
12
12
  export type EnvironmentType = 'docker' | 'process';
@@ -26,7 +26,7 @@ export function readYML(path: string) {
26
26
  try {
27
27
  return YAML.parse(rawYaml.toString());
28
28
  } catch (err) {
29
- throw new Error('Failed to parse plan YAML: ' + err);
29
+ throw new Error(`Failed to parse plan YAML: ${err}`);
30
30
  }
31
31
  }
32
32