@decohere/core 0.0.1-dev.125 → 0.0.1-dev.5

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 (252) hide show
  1. package/package.json +40 -15
  2. package/src/caching/cache-manager.ts +102 -0
  3. package/src/caching/cache.decorator.ts +56 -0
  4. package/src/caching/cache.ts +11 -0
  5. package/{es6/src/caching/index.js → src/caching/index.ts} +1 -1
  6. package/src/caching/invalidate-cache.decorator.ts +81 -0
  7. package/src/caching/memoize.decorator.ts +34 -0
  8. package/src/caching/node-cache-memoize.adapter.ts +47 -0
  9. package/src/cli/action.ts +77 -0
  10. package/src/cli/command.ts +173 -0
  11. package/src/cli/index.ts +30 -0
  12. package/src/cli/module.type.ts +48 -0
  13. package/src/cli/options.ts +81 -0
  14. package/src/collections/find-duplicates.ts +16 -0
  15. package/src/collections/merge-arrays.ts +9 -0
  16. package/src/collections/sum.ts +71 -0
  17. package/src/collections/unique-objects.ts +23 -0
  18. package/src/composition/index.ts +48 -0
  19. package/src/configuration/configuration.types.ts +13 -0
  20. package/src/diagnostics/index.ts +1 -0
  21. package/src/diagnostics/trace.ts +97 -0
  22. package/{es6/src/extensibility/decorator-factory-base.js → src/extensibility/decorator-factory-base.ts} +17 -9
  23. package/src/extensibility/decorator-factory.ts +44 -0
  24. package/src/extensibility/extension.ts +34 -0
  25. package/src/extensibility/mixwith.ts +42 -0
  26. package/src/extensibility/pipeline.ts +60 -0
  27. package/{types/src/extensibility/step.d.ts → src/extensibility/step.ts} +6 -4
  28. package/src/interop/docker/docker.ts +57 -0
  29. package/src/interop/errors/interop.error.ts +1 -0
  30. package/src/interop/git/git.ts +98 -0
  31. package/src/interop/git/git.types.ts +17 -0
  32. package/src/interop/index.ts +22 -0
  33. package/src/interop/node/index.ts +1 -0
  34. package/src/interop/node/node.ts +38 -0
  35. package/src/interop/npm/npm.ts +168 -0
  36. package/src/interop/npm/npm.types.ts +28 -0
  37. package/src/interop/pnpm/pnpm.ts +22 -0
  38. package/src/interop/pnpm/pnpm.types.ts +3 -0
  39. package/src/interop/shell/shell.argv.ts +31 -0
  40. package/src/interop/shell/shell.ts +100 -0
  41. package/src/interop/shell/shell.types.ts +9 -0
  42. package/src/interop/types/detect-package-manager.ts +45 -0
  43. package/src/interop/types/package-manager.ts +7 -0
  44. package/src/interop/yarn/yarn.ts +28 -0
  45. package/src/interop/yarn/yarn.types.ts +3 -0
  46. package/src/io/copy-file.ts +13 -0
  47. package/src/io/delete-file.ts +4 -0
  48. package/src/io/directory-exists.ts +9 -0
  49. package/src/io/file-exists.ts +10 -0
  50. package/src/io/find-directories.ts +13 -0
  51. package/src/io/find-file.ts +37 -0
  52. package/{es6/src/io/io.js → src/io/io.ts} +3 -0
  53. package/src/io/read-file.ts +25 -0
  54. package/src/io/remove-dir.ts +11 -0
  55. package/src/io/types/directory.type.ts +4 -0
  56. package/src/io/types/file.ts +34 -0
  57. package/src/io/url-to-path.ts +17 -0
  58. package/src/io/watch.ts +290 -0
  59. package/src/io/write-file.ts +21 -0
  60. package/src/objects/deep-merge.ts +131 -0
  61. package/{es6/src/reflection/index.js → src/reflection/index.ts} +1 -0
  62. package/src/reflection/types/reflection.types.ts +28 -0
  63. package/src/reflection/utilities.ts +92 -0
  64. package/src/semver/index.ts +1 -0
  65. package/src/semver/semver.ts +96 -0
  66. package/src/system/camel-case.ts +3 -0
  67. package/src/system/debounce.ts +13 -0
  68. package/src/system/decorator.ts +41 -0
  69. package/src/system/environment.ts +6 -0
  70. package/{types/src/system/function.types.d.ts → src/system/function.types.ts} +10 -2
  71. package/src/system/retry.decorator.ts +43 -0
  72. package/src/system/sleep.ts +6 -0
  73. package/src/system/type.utils.ts +5 -0
  74. package/src/testing/flush-promises.ts +7 -0
  75. package/src/testing/glob-with-memfs.ts +25 -0
  76. package/src/testing/index.ts +5 -0
  77. package/src/testing/memfs.ts +28 -0
  78. package/src/testing/mock-logger.ts +42 -0
  79. package/src/testing/spy-on-constructor.ts +33 -0
  80. package/tests/caching/cache-manager.spec.ts +147 -0
  81. package/tests/caching/cache-test-class.ts +91 -0
  82. package/tests/caching/cache.decorator.spec.ts +259 -0
  83. package/tests/caching/invalidate-cache-test-class.ts +52 -0
  84. package/tests/caching/invalidate-cache.decorator.spec.ts +120 -0
  85. package/tests/caching/memoize-test-class.ts +99 -0
  86. package/tests/caching/memoize.decorator.spec.ts +203 -0
  87. package/tests/cli/command.model.spec.ts +91 -0
  88. package/tests/collections/merge-arrays.spec.ts +50 -0
  89. package/tests/collections/sum.spec.ts +86 -0
  90. package/tests/collections/unique-objects.spec.ts +233 -0
  91. package/tests/extensibility/extension.spec.ts +29 -0
  92. package/tests/extensibility/pipeline.spec.ts +379 -0
  93. package/tests/interop/detect-package-manager.spec.ts +70 -0
  94. package/tests/interop/npm.spec.ts +129 -0
  95. package/tests/interop/pnpm.spec.ts +30 -0
  96. package/tests/interop/shell.spec.ts +47 -0
  97. package/tests/interop/yarn.spec.ts +18 -0
  98. package/tests/io/find-file.spec.ts +188 -0
  99. package/tests/io/watch.real-fs.spec.ts +76 -0
  100. package/tests/io/watch.spec.ts +291 -0
  101. package/tests/mock-prototype.ts +12 -0
  102. package/tests/semver/semver.spec.ts +51 -0
  103. package/tests/system/decorator.spec.ts +250 -0
  104. package/tests/system/retry.decorator.spec.ts +57 -0
  105. package/LICENSE +0 -15
  106. package/commonjs/src/caching/cache-manager.js +0 -98
  107. package/commonjs/src/caching/cache.decorator.js +0 -69
  108. package/commonjs/src/caching/cache.js +0 -6
  109. package/commonjs/src/caching/index.js +0 -23
  110. package/commonjs/src/caching/invalidate-cache.decorator.js +0 -92
  111. package/commonjs/src/caching/map-cache.js +0 -39
  112. package/commonjs/src/caching/memoize.decorator.js +0 -36
  113. package/commonjs/src/collections/collections.js +0 -21
  114. package/commonjs/src/collections/find-duplicates.js +0 -35
  115. package/commonjs/src/collections/index.js +0 -52
  116. package/commonjs/src/collections/merge-arrays.js +0 -34
  117. package/commonjs/src/collections/sum.js +0 -63
  118. package/commonjs/src/collections/unique-objects.js +0 -39
  119. package/commonjs/src/composition/index.js +0 -4
  120. package/commonjs/src/configuration/configuration.types.js +0 -11
  121. package/commonjs/src/configuration/index.js +0 -18
  122. package/commonjs/src/extensibility/decorator-factory-base.js +0 -22
  123. package/commonjs/src/extensibility/decorator-factory.js +0 -31
  124. package/commonjs/src/extensibility/extension.js +0 -37
  125. package/commonjs/src/extensibility/index.js +0 -23
  126. package/commonjs/src/extensibility/mixwith.js +0 -36
  127. package/commonjs/src/extensibility/pipeline.js +0 -56
  128. package/commonjs/src/extensibility/step.js +0 -18
  129. package/commonjs/src/io/copy-file.js +0 -25
  130. package/commonjs/src/io/delete-file.js +0 -21
  131. package/commonjs/src/io/directory-exists.js +0 -24
  132. package/commonjs/src/io/file-exists.js +0 -19
  133. package/commonjs/src/io/index.js +0 -66
  134. package/commonjs/src/io/io.js +0 -25
  135. package/commonjs/src/io/read-file.js +0 -39
  136. package/commonjs/src/io/remove-dir.js +0 -22
  137. package/commonjs/src/io/types/directory.type.js +0 -4
  138. package/commonjs/src/io/types/file.js +0 -28
  139. package/commonjs/src/io/types/index.js +0 -19
  140. package/commonjs/src/io/url-to-path.js +0 -33
  141. package/commonjs/src/io/write-file.js +0 -30
  142. package/commonjs/src/objects/deep-merge.js +0 -67
  143. package/commonjs/src/objects/index.js +0 -11
  144. package/commonjs/src/reflection/index.js +0 -20
  145. package/commonjs/src/reflection/types/index.js +0 -18
  146. package/commonjs/src/reflection/types/reflection.types.js +0 -26
  147. package/commonjs/src/reflection/utilities.js +0 -118
  148. package/commonjs/src/system/camel-case.js +0 -13
  149. package/commonjs/src/system/debounce.js +0 -19
  150. package/commonjs/src/system/decorator.js +0 -46
  151. package/commonjs/src/system/environment.js +0 -23
  152. package/commonjs/src/system/function.types.js +0 -18
  153. package/commonjs/src/system/index.js +0 -24
  154. package/commonjs/src/system/retry.decorator.js +0 -40
  155. package/commonjs/src/system/sleep.js +0 -13
  156. package/commonjs/src/system/type.utils.js +0 -4
  157. package/es6/src/caching/cache-manager.js +0 -82
  158. package/es6/src/caching/cache.decorator.js +0 -51
  159. package/es6/src/caching/cache.js +0 -3
  160. package/es6/src/caching/invalidate-cache.decorator.js +0 -71
  161. package/es6/src/caching/map-cache.js +0 -35
  162. package/es6/src/caching/memoize.decorator.js +0 -27
  163. package/es6/src/collections/find-duplicates.js +0 -17
  164. package/es6/src/collections/merge-arrays.js +0 -16
  165. package/es6/src/collections/sum.js +0 -45
  166. package/es6/src/collections/unique-objects.js +0 -21
  167. package/es6/src/composition/index.js +0 -1
  168. package/es6/src/configuration/configuration.types.js +0 -1
  169. package/es6/src/extensibility/decorator-factory.js +0 -21
  170. package/es6/src/extensibility/extension.js +0 -27
  171. package/es6/src/extensibility/mixwith.js +0 -26
  172. package/es6/src/extensibility/pipeline.js +0 -46
  173. package/es6/src/extensibility/step.js +0 -16
  174. package/es6/src/io/copy-file.js +0 -10
  175. package/es6/src/io/delete-file.js +0 -6
  176. package/es6/src/io/directory-exists.js +0 -9
  177. package/es6/src/io/file-exists.js +0 -9
  178. package/es6/src/io/read-file.js +0 -13
  179. package/es6/src/io/remove-dir.js +0 -7
  180. package/es6/src/io/types/directory.type.js +0 -1
  181. package/es6/src/io/types/file.js +0 -18
  182. package/es6/src/io/url-to-path.js +0 -15
  183. package/es6/src/io/write-file.js +0 -15
  184. package/es6/src/objects/deep-merge.js +0 -57
  185. package/es6/src/reflection/types/reflection.types.js +0 -8
  186. package/es6/src/reflection/utilities.js +0 -74
  187. package/es6/src/system/camel-case.js +0 -3
  188. package/es6/src/system/debounce.js +0 -9
  189. package/es6/src/system/decorator.js +0 -30
  190. package/es6/src/system/environment.js +0 -5
  191. package/es6/src/system/function.types.js +0 -26
  192. package/es6/src/system/retry.decorator.js +0 -30
  193. package/es6/src/system/sleep.js +0 -3
  194. package/es6/src/system/type.utils.js +0 -1
  195. package/types/src/caching/cache-manager.d.ts +0 -59
  196. package/types/src/caching/cache.d.ts +0 -11
  197. package/types/src/caching/cache.decorator.d.ts +0 -18
  198. package/types/src/caching/index.d.ts +0 -6
  199. package/types/src/caching/invalidate-cache.decorator.d.ts +0 -17
  200. package/types/src/caching/map-cache.d.ts +0 -23
  201. package/types/src/caching/memoize.decorator.d.ts +0 -16
  202. package/types/src/collections/collections.d.ts +0 -4
  203. package/types/src/collections/find-duplicates.d.ts +0 -2
  204. package/types/src/collections/index.d.ts +0 -1
  205. package/types/src/collections/merge-arrays.d.ts +0 -2
  206. package/types/src/collections/sum.d.ts +0 -4
  207. package/types/src/collections/unique-objects.d.ts +0 -2
  208. package/types/src/composition/index.d.ts +0 -37
  209. package/types/src/configuration/configuration.types.d.ts +0 -10
  210. package/types/src/configuration/index.d.ts +0 -1
  211. package/types/src/extensibility/decorator-factory-base.d.ts +0 -39
  212. package/types/src/extensibility/decorator-factory.d.ts +0 -19
  213. package/types/src/extensibility/extension.d.ts +0 -9
  214. package/types/src/extensibility/index.d.ts +0 -6
  215. package/types/src/extensibility/mixwith.d.ts +0 -6
  216. package/types/src/extensibility/pipeline.d.ts +0 -12
  217. package/types/src/io/copy-file.d.ts +0 -4
  218. package/types/src/io/delete-file.d.ts +0 -1
  219. package/types/src/io/directory-exists.d.ts +0 -1
  220. package/types/src/io/file-exists.d.ts +0 -1
  221. package/types/src/io/index.d.ts +0 -2
  222. package/types/src/io/io.d.ts +0 -8
  223. package/types/src/io/read-file.d.ts +0 -4
  224. package/types/src/io/remove-dir.d.ts +0 -5
  225. package/types/src/io/types/directory.type.d.ts +0 -4
  226. package/types/src/io/types/file.d.ts +0 -20
  227. package/types/src/io/types/index.d.ts +0 -2
  228. package/types/src/io/url-to-path.d.ts +0 -2
  229. package/types/src/io/write-file.d.ts +0 -5
  230. package/types/src/objects/deep-merge.d.ts +0 -41
  231. package/types/src/objects/index.d.ts +0 -1
  232. package/types/src/reflection/index.d.ts +0 -3
  233. package/types/src/reflection/types/index.d.ts +0 -1
  234. package/types/src/reflection/types/reflection.types.d.ts +0 -21
  235. package/types/src/reflection/utilities.d.ts +0 -27
  236. package/types/src/system/camel-case.d.ts +0 -1
  237. package/types/src/system/debounce.d.ts +0 -1
  238. package/types/src/system/decorator.d.ts +0 -15
  239. package/types/src/system/environment.d.ts +0 -6
  240. package/types/src/system/index.d.ts +0 -7
  241. package/types/src/system/retry.decorator.d.ts +0 -6
  242. package/types/src/system/sleep.d.ts +0 -1
  243. package/types/src/system/type.utils.d.ts +0 -1
  244. /package/{es6/src/collections/collections.js → src/collections/collections.ts} +0 -0
  245. /package/{es6/src/collections/index.js → src/collections/index.ts} +0 -0
  246. /package/{es6/src/configuration/index.js → src/configuration/index.ts} +0 -0
  247. /package/{es6/src/extensibility/index.js → src/extensibility/index.ts} +0 -0
  248. /package/{es6/src/io/index.js → src/io/index.ts} +0 -0
  249. /package/{es6/src/io/types/index.js → src/io/types/index.ts} +0 -0
  250. /package/{es6/src/objects/index.js → src/objects/index.ts} +0 -0
  251. /package/{es6/src/reflection/types/index.js → src/reflection/types/index.ts} +0 -0
  252. /package/{es6/src/system/index.js → src/system/index.ts} +0 -0
package/package.json CHANGED
@@ -1,24 +1,17 @@
1
1
  {
2
2
  "name": "@decohere/core",
3
- "version": "0.0.1-dev.125",
4
- "description": "The runtime base every Decohere package builds on: reflection, composition, configuration types, extensibility, collections, objects, caching on a Map, and basic file IO. No side effects on import.",
3
+ "version": "0.0.1-dev.5",
4
+ "description": "Core utility library — collections, caching, IO, CLI primitives, reflection, and composition",
5
5
  "keywords": [
6
6
  "typescript",
7
7
  "utilities",
8
- "reflection",
9
- "composition",
8
+ "collections",
10
9
  "caching",
10
+ "io",
11
+ "cli",
12
+ "reflection",
11
13
  "decohere"
12
14
  ],
13
- "homepage": "https://github.com/typesmiths/decohere/tree/main/projects/core#readme",
14
- "bugs": {
15
- "url": "https://github.com/typesmiths/decohere/issues"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/typesmiths/decohere.git",
20
- "directory": "projects/core"
21
- },
22
15
  "license": "ISC",
23
16
  "author": "Typesmiths",
24
17
  "type": "module",
@@ -44,6 +37,11 @@
44
37
  "import": "./es6/src/extensibility/index.js",
45
38
  "require": "./commonjs/src/extensibility/index.js"
46
39
  },
40
+ "./interop": {
41
+ "types": "./types/src/interop/index.d.ts",
42
+ "import": "./es6/src/interop/index.js",
43
+ "require": "./commonjs/src/interop/index.js"
44
+ },
47
45
  "./io": {
48
46
  "types": "./types/src/io/index.d.ts",
49
47
  "import": "./es6/src/io/index.js",
@@ -64,18 +62,45 @@
64
62
  "import": "./es6/src/system/index.js",
65
63
  "require": "./commonjs/src/system/index.js"
66
64
  },
65
+ "./semver": {
66
+ "types": "./types/src/semver/index.d.ts",
67
+ "import": "./es6/src/semver/index.js",
68
+ "require": "./commonjs/src/semver/index.js"
69
+ },
70
+ "./testing": {
71
+ "types": "./types/src/testing/index.d.ts",
72
+ "import": "./es6/src/testing/index.js",
73
+ "require": "./commonjs/src/testing/index.js"
74
+ },
67
75
  "./composition": {
68
76
  "types": "./types/src/composition/index.d.ts",
69
77
  "import": "./es6/src/composition/index.js",
70
78
  "require": "./commonjs/src/composition/index.js"
79
+ },
80
+ "./cli": {
81
+ "types": "./types/src/cli/index.d.ts",
82
+ "import": "./es6/src/cli/index.js",
83
+ "require": "./commonjs/src/cli/index.js"
84
+ },
85
+ "./diagnostics": {
86
+ "types": "./types/src/diagnostics/index.d.ts",
87
+ "import": "./es6/src/diagnostics/index.js",
88
+ "require": "./commonjs/src/diagnostics/index.js"
71
89
  }
72
90
  },
73
91
  "scripts": {
74
92
  "test": "echo \"Error: no test specified\" && exit 1"
75
93
  },
76
94
  "dependencies": {
77
- "@decohere/scribe": "0.0.1-dev.125",
95
+ "@cacheable/node-cache": "^2.0.1",
96
+ "@decohere/scribe": "0.0.1-dev.5",
97
+ "commander": "^13.1.0",
98
+ "glob": "^13.0.1",
99
+ "picomatch": "^4.0.4",
78
100
  "reflect-metadata": "^0.2.2"
79
101
  },
80
- "devDependencies": {}
102
+ "devDependencies": {
103
+ "memfs": "^4.17.0",
104
+ "vitest": "^3.2.6"
105
+ }
81
106
  }
@@ -0,0 +1,102 @@
1
+ import type { ICache } from "./cache.js";
2
+ import { NodeCacheAdapter } from "./node-cache-memoize.adapter.js";
3
+
4
+ export const Caches = {
5
+ default: "DEFAULT",
6
+ memoize: "MEMOIZE_CACHE",
7
+ } as const;
8
+
9
+ /**
10
+ * Central cache manager for managing named cache instances
11
+ */
12
+ class CacheManager {
13
+ private caches = new Map<string, ICache>();
14
+ private readonly defaultCacheName = Caches.default;
15
+
16
+ /**
17
+ * Set a cache instance with the given name
18
+ * @param name - The name of the cache
19
+ * @param cache - The cache instance
20
+ */
21
+ public setCache(name: string, cache: ICache): void {
22
+ this.caches.set(name, cache);
23
+ }
24
+
25
+ /**
26
+ * Get a cache instance by name
27
+ * @param name - The name of the cache, defaults to "DEFAULT"
28
+ * @returns The cache instance
29
+ */
30
+ public getCache(name?: string, memoize: boolean = false): ICache {
31
+ const cacheName = name ?? this.defaultCacheName;
32
+
33
+ if (!this.caches.has(cacheName)) {
34
+ const cache = NodeCacheAdapter.create({ enableMemoization: memoize });
35
+ this.caches.set(cacheName, cache);
36
+ }
37
+
38
+ return this.caches.get(cacheName);
39
+ }
40
+
41
+ /**
42
+ * Clear a cache by name. If no name is provided, clears the default cache.
43
+ * Note: This only works if the cache implements a clear() method.
44
+ * @param name - The name of the cache to clear
45
+ */
46
+ public flush(name?: string): void {
47
+ const cacheName = name ?? this.defaultCacheName;
48
+ const cache = this.caches.get(cacheName);
49
+ cache.flushAll();
50
+ }
51
+
52
+ public flushAllCaches(): void {
53
+ for (const [, cache] of this.caches) {
54
+ cache.flushAll();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Remove a cache instance by name
60
+ * @param name - The name of the cache to remove
61
+ */
62
+ public removeCache(name: string): void {
63
+ this.caches.delete(name);
64
+ }
65
+
66
+ /**
67
+ * Check if a cache with the given name exists
68
+ * @param name - The name of the cache
69
+ * @returns True if the cache exists, false otherwise
70
+ */
71
+ public hasCache(name: string): boolean {
72
+ return this.caches.has(name);
73
+ }
74
+
75
+ private readonly instanceIds = new WeakMap<object, number>();
76
+ private instanceIdCounter = 0;
77
+
78
+ /**
79
+ * Generate a cache key for a given instance, function name, and arguments.
80
+ * Each instance receives a stable numeric ID so cache entries are never
81
+ * shared across instances of the same class.
82
+ * @param instance - The class instance (`this` at call time)
83
+ * @param functionName - The name of the decorated method
84
+ * @param args - The method arguments
85
+ * @returns A unique cache key string
86
+ */
87
+ public generateCacheKey(instance: object, functionName: string, args: unknown[]): string {
88
+ if (!this.instanceIds.has(instance)) {
89
+ this.instanceIds.set(instance, ++this.instanceIdCounter);
90
+ }
91
+ const instanceId = this.instanceIds.get(instance);
92
+ const argsStr = JSON.stringify({ args }, (_key, val) =>
93
+ typeof val === "function" ? `[Function:${val.name}]` : val,
94
+ );
95
+ return `${instance.constructor.name}#${instanceId}|${functionName}|${argsStr}`;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Singleton instance of the cache manager
101
+ */
102
+ export const Caching = new CacheManager();
@@ -0,0 +1,56 @@
1
+ import { isPromise } from "node:util/types";
2
+ import { FunctionDecorator, ignoreIf } from "../system/decorator.js";
3
+ import type { ICache } from "./cache.js";
4
+ import { Caching } from "./cache-manager.js";
5
+
6
+ /**
7
+ * Options for configuring the @cache() decorator
8
+ */
9
+ export interface CacheOptions {
10
+ cache?: ICache;
11
+ name?: string;
12
+ /**
13
+ * Custom key function. When provided, replaces the default instance-scoped key
14
+ * with a caller-defined key, enabling cross-instance cache sharing.
15
+ */
16
+ keyFn?: (...args: unknown[]) => string;
17
+ }
18
+
19
+ export class CacheDecorator extends FunctionDecorator<CacheOptions> {
20
+ @ignoreIf("FORGE_DISABLE_CACHE")
21
+ public decorator(_target: object, propertyKey: string, descriptor: PropertyDescriptor) {
22
+ const cacheInstance = this.options?.cache ?? Caching.getCache(this.options?.name);
23
+ const keyFn = this.options?.keyFn;
24
+ const originalMethod = descriptor.value;
25
+
26
+ const wrapped = function (...args: unknown[]) {
27
+ const cacheKey = keyFn
28
+ ? keyFn(...args)
29
+ : Caching.generateCacheKey(this, propertyKey, args);
30
+ if (cacheInstance.has(cacheKey)) {
31
+ const val = cacheInstance.get(cacheKey);
32
+ return val;
33
+ }
34
+
35
+ const resultOrPromise = originalMethod.apply(this, args);
36
+ if (isPromise(resultOrPromise)) {
37
+ resultOrPromise.then(
38
+ (result) => {
39
+ cacheInstance.set(cacheKey, result);
40
+ },
41
+ () => {
42
+ /* rejection propagates via the returned promise */
43
+ },
44
+ );
45
+ } else {
46
+ cacheInstance.set(cacheKey, resultOrPromise);
47
+ }
48
+
49
+ return resultOrPromise;
50
+ };
51
+ Object.defineProperty(wrapped, "name", { value: propertyKey });
52
+ descriptor.value = wrapped;
53
+ }
54
+ }
55
+
56
+ export const cache = new CacheDecorator().decoratorFactory;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Interface for cache implementations used by the @cache() decorator
3
+ */
4
+ export interface ICache {
5
+ get(key: string): unknown;
6
+ set(key: string, value: unknown): void;
7
+ has(key: string): boolean;
8
+ del(key: string): void;
9
+ flushAll();
10
+ get isMemoize(): boolean;
11
+ }
@@ -3,4 +3,4 @@ export * from "./cache.js";
3
3
  export * from "./cache-manager.js";
4
4
  export * from "./invalidate-cache.decorator.js";
5
5
  export * from "./memoize.decorator.js";
6
- export * from "./map-cache.js";
6
+ export * from "./node-cache-memoize.adapter.js";
@@ -0,0 +1,81 @@
1
+ import { isPromise } from "node:util/types";
2
+ import { FunctionDecorator, ignoreIf } from "../system/decorator.js";
3
+ import type { CacheOptions } from "./cache.decorator.js";
4
+ import { Caches, Caching } from "./cache-manager.js";
5
+
6
+ type InvalidateCacheOptions = CacheOptions & {
7
+ getKey: (instance: object, args: unknown[]) => string;
8
+ };
9
+
10
+ export class InvalidateCacheDecorator extends FunctionDecorator<InvalidateCacheOptions> {
11
+ @ignoreIf("FORGE_DISABLE_CACHE")
12
+ public decorator(_target: object, _propertyKey: string, descriptor: PropertyDescriptor) {
13
+ const { getKey, cache, name } = this.options;
14
+ const cacheInstance = cache ?? Caching.getCache(name);
15
+ const originalMethod = descriptor.value;
16
+
17
+ descriptor.value = function (...args: unknown[]) {
18
+ const resultOrPromise = originalMethod.apply(this, args);
19
+ const evict = () => {
20
+ const key = getKey(this, args);
21
+ cacheInstance.del(key);
22
+ };
23
+ if (isPromise(resultOrPromise)) {
24
+ resultOrPromise.then(evict);
25
+ } else {
26
+ evict();
27
+ }
28
+ return resultOrPromise;
29
+ };
30
+ }
31
+ }
32
+
33
+ const buildGetKey = (
34
+ method: string | ((...args: never[]) => unknown),
35
+ keyFn?: (...args: unknown[]) => string,
36
+ ) => {
37
+ if (typeof method === "string") return () => method;
38
+ if (keyFn) return (_instance: object, args: unknown[]) => keyFn(...args);
39
+ return (instance: object, args: unknown[]) =>
40
+ Caching.generateCacheKey(instance, method.name, args);
41
+ };
42
+
43
+ export const invalidateCache = (
44
+ method: string | ((...args: never[]) => unknown),
45
+ options?: CacheOptions,
46
+ ) => {
47
+ const cache = options?.cache ?? Caching.getCache(options?.name);
48
+ return new InvalidateCacheDecorator().decoratorFactory({
49
+ getKey: buildGetKey(method, options?.keyFn),
50
+ ...options,
51
+ cache,
52
+ });
53
+ };
54
+ invalidateCache.evict = (
55
+ instance: object,
56
+ method: (...args: never[]) => unknown,
57
+ args: unknown[] = [],
58
+ ) => {
59
+ const key = Caching.generateCacheKey(instance, method.name, args);
60
+ Caching.getCache().del(key);
61
+ };
62
+
63
+ export const invalidateMemoize = (
64
+ method: string | ((...args: never[]) => unknown),
65
+ options?: CacheOptions,
66
+ ) => {
67
+ const cache = options?.cache ?? Caching.getCache(options?.name ?? Caches.memoize, true);
68
+ return new InvalidateCacheDecorator().decoratorFactory({
69
+ getKey: buildGetKey(method),
70
+ ...options,
71
+ cache,
72
+ });
73
+ };
74
+ invalidateMemoize.evict = (
75
+ instance: object,
76
+ method: (...args: never[]) => unknown,
77
+ args: unknown[] = [],
78
+ ) => {
79
+ const key = Caching.generateCacheKey(instance, method.name, args);
80
+ Caching.getCache(Caches.memoize, true).del(key);
81
+ };
@@ -0,0 +1,34 @@
1
+ import { CacheDecorator } from "./cache.decorator.js";
2
+ import { Caches, Caching } from "./cache-manager.js";
3
+
4
+ /**
5
+ * Memoize decorator class that extends CacheDecorator with memoization support
6
+ * Automatically uses a memoization-capable cache (NodeCache with useClones: false)
7
+ */
8
+ export class MemoizeDecorator extends CacheDecorator {
9
+ public decorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
10
+ const cache =
11
+ this.options?.cache ?? Caching.getCache(this.options?.name ?? Caches.memoize, true);
12
+ if (!cache.isMemoize) {
13
+ throw new Error(
14
+ "Memoize decorator requires a cache that supports memoization (stores references, not clones)",
15
+ );
16
+ }
17
+
18
+ // Pass the resolved cache to the base class by updating options
19
+ // This ensures the base class uses CacheNames.memoize when no cache was provided
20
+ this.options = { ...this.options, cache };
21
+
22
+ return super.decorator(target, propertyKey, descriptor);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Memoize decorator factory
28
+ * Provides true memoization by storing object references instead of clones
29
+ *
30
+ * @param options - Optional configuration for the memoize decorator
31
+ * @returns Decorator function
32
+ */
33
+
34
+ export const memoize = new MemoizeDecorator().decoratorFactory;
@@ -0,0 +1,47 @@
1
+ import NodeCache from "@cacheable/node-cache";
2
+ import type { ICache } from "./cache.js";
3
+
4
+ type NodeCacheMemoizeOptions = {
5
+ enableMemoization: boolean;
6
+ };
7
+
8
+ /**
9
+ * NodeCache adapter configured for memoization (useClones: false)
10
+ * This allows storing object references instead of clones for true memoization
11
+ */
12
+ export class NodeCacheAdapter implements ICache {
13
+ public static create(options: NodeCacheMemoizeOptions = { enableMemoization: false }) {
14
+ const nodeCache = new NodeCache({ useClones: !options.enableMemoization });
15
+ return new NodeCacheAdapter(nodeCache);
16
+ }
17
+ public readonly supportsMemoization = true as const;
18
+ private cache: NodeCache<unknown>;
19
+
20
+ public constructor(inst: NodeCache<unknown> = new NodeCache()) {
21
+ this.cache = inst;
22
+ }
23
+
24
+ public get isMemoize() {
25
+ return !this.cache.options.useClones;
26
+ }
27
+
28
+ public get(key: string): unknown {
29
+ return this.cache.get(key);
30
+ }
31
+
32
+ public set(key: string, value: unknown): void {
33
+ this.cache.set(key, value);
34
+ }
35
+
36
+ public has(key: string): boolean {
37
+ return this.cache.has(key);
38
+ }
39
+
40
+ public del(key: string): void {
41
+ this.cache.del(key);
42
+ }
43
+
44
+ public flushAll(): void {
45
+ this.cache.flushAll();
46
+ }
47
+ }
@@ -0,0 +1,77 @@
1
+ import type { Constructable } from "../reflection/index.js";
2
+ import type { UnionToIntersection } from "../system/type.utils.js";
3
+
4
+ export const ActionMap = Symbol("ActionMap");
5
+
6
+ export type ActionClassMap = Record<string, Constructable<Action>>;
7
+
8
+ export interface ResultStatus {
9
+ succeeded: boolean;
10
+ error?: string;
11
+ }
12
+
13
+ export interface Result {
14
+ readonly name: string;
15
+ readonly succeeded: boolean;
16
+ readonly skipped: boolean;
17
+ readonly message: string;
18
+ }
19
+
20
+ export class ActionResponse<TData = unknown> implements Result {
21
+ public static Success<TData>(name: string, data?: TData[]) {
22
+ return new ActionResponse<TData>({ name, data, succeeded: true });
23
+ }
24
+
25
+ public static Skipped(name: string) {
26
+ return new ActionResponse({ name, skipped: true });
27
+ }
28
+
29
+ public static Blocking(name: string) {
30
+ return new ActionResponse({ name, succeeded: true, blocking: true });
31
+ }
32
+
33
+ public static Error<TData = unknown>(name: string, message: string): ActionResponse<TData>;
34
+ public static Error<TData extends ResultStatus>(
35
+ name: string,
36
+ data: TData[],
37
+ ): ActionResponse<TData>;
38
+ public static Error<TData extends ResultStatus>(
39
+ name: string,
40
+ messageOrData: string | TData[],
41
+ data?: TData[],
42
+ ) {
43
+ if (Array.isArray(messageOrData)) {
44
+ const message = messageOrData
45
+ .filter((r) => !r.succeeded)
46
+ .map((r) => r.error ?? "Unknown error")
47
+ .join("\n");
48
+ return new ActionResponse<TData>({ name, message, data: messageOrData });
49
+ }
50
+ return new ActionResponse<TData>({ name, message: messageOrData, data });
51
+ }
52
+
53
+ public constructor(data: Partial<ActionResponse<TData>>) {
54
+ Object.assign(this, data);
55
+ }
56
+
57
+ public readonly name: string;
58
+ public readonly succeeded: boolean = false;
59
+ public readonly skipped: boolean = false;
60
+ public readonly blocking: boolean = false;
61
+ public readonly message: string;
62
+ public readonly data: TData[];
63
+ }
64
+
65
+ export interface Action<T extends object = object> {
66
+ run(options: T): Promise<ActionResponse>;
67
+ }
68
+
69
+ export type Actions = Record<string, Action>;
70
+
71
+ export type ActionOptions<A extends Action> = A extends Action<infer T> ? T : never;
72
+
73
+ export type MergeOptions<TActions extends Record<string, Action>> = UnionToIntersection<
74
+ {
75
+ [K in keyof TActions]: ActionOptions<TActions[K]>;
76
+ }[keyof TActions]
77
+ >;
@@ -0,0 +1,173 @@
1
+ import { Command as CommanderType, type OptionValues } from "commander";
2
+ import type { Constructable } from "../reflection/index.js";
3
+ import type { ActionResponse, Actions } from "./action.js";
4
+
5
+ export type CommandType = Constructable<DecohereCommand>;
6
+
7
+ export type CommandHandler = (opts: OptionValues, cmd: DecohereCommand) => Promise<void>;
8
+
9
+ export type CommandResponse<TActions extends Actions = Actions> = {
10
+ [ActionName in keyof TActions]?: ActionResponse;
11
+ };
12
+
13
+ export const CommandResponse = {
14
+ success<T extends Actions>(
15
+ _ctx: unknown,
16
+ responses: CommandResponse<T>,
17
+ ): CommandResponse<T> {
18
+ return responses;
19
+ },
20
+
21
+ help<T extends Actions = Actions>(): CommandResponse<T> {
22
+ return {} as CommandResponse<T>;
23
+ },
24
+
25
+ fromAction(name: string, response: ActionResponse): CommandResponse {
26
+ return { [name]: response };
27
+ },
28
+ };
29
+
30
+ /** Thrown when a command execution fails due to action errors. */
31
+ export class CommandError extends Error {}
32
+
33
+ export class DecohereCommand<TActions extends Actions = Actions> extends CommanderType {
34
+ public get actions(): readonly Constructable<import("./action.js").Action>[] {
35
+ return [];
36
+ }
37
+
38
+ public constructor(
39
+ name?: string | Constructable | Record<string, CommandType>,
40
+ handler?: CommandHandler,
41
+ ) {
42
+ super();
43
+ if (typeof name === "string" || typeof name === "function") {
44
+ this.name(this.parseCommandName(name));
45
+ } else {
46
+ this.name(this.parseCommandName(this.constructor as unknown as Constructable));
47
+ if (name) {
48
+ this.registerSubcommand(name, handler);
49
+ }
50
+ }
51
+ if (handler && !this.commands.length) {
52
+ this.action(handler);
53
+ }
54
+ this.exitOverride();
55
+ }
56
+
57
+ /**
58
+ * Whether the command, with these options, runs until it is stopped (`flux serve`,
59
+ * `forge build --watch`). The CLI then listens for SIGINT and SIGTERM from the moment it
60
+ * starts, so a stop during start-up still shuts down cleanly; other commands keep Node's
61
+ * default, where Ctrl+C ends them at once.
62
+ */
63
+ public runsUntilStopped(_options: OptionValues): boolean {
64
+ return false;
65
+ }
66
+
67
+ public async run(
68
+ actions: TActions,
69
+ options: OptionValues,
70
+ ): Promise<CommandResponse<TActions>> {
71
+ const responses = await this.runActions(actions, options);
72
+ this.throwOnErrors(responses);
73
+ return CommandResponse.success(options, responses);
74
+ }
75
+
76
+ public registerSubcommand(command: CommandType, onAction?: CommandHandler): void;
77
+ public registerSubcommand(
78
+ commands: Record<string, DecohereCommand | CommandType>,
79
+ onAction?: CommandHandler,
80
+ ): void;
81
+ public registerSubcommand(
82
+ commands: CommandType | Record<string, DecohereCommand | CommandType>,
83
+ onAction?: CommandHandler,
84
+ ): void {
85
+ if (typeof commands === "function") {
86
+ const instance = new commands(commands, onAction);
87
+ this.addCommand(instance);
88
+ if (onAction && !instance.commands.length) {
89
+ instance.action(onAction);
90
+ }
91
+ return;
92
+ }
93
+
94
+ for (const [key, command] of Object.entries(commands)) {
95
+ const instance =
96
+ command instanceof DecohereCommand ? command : new command(key, onAction);
97
+ this.addCommand(instance);
98
+ if (onAction && !instance.commands.length) {
99
+ instance.action(onAction);
100
+ }
101
+ }
102
+ }
103
+
104
+ protected async runActions(
105
+ actions: TActions,
106
+ options: OptionValues,
107
+ ): Promise<CommandResponse<TActions>> {
108
+ const selectedActions = this.getSelectedActions(actions);
109
+ const responses = {} as CommandResponse<TActions>;
110
+ for (const [actionKey, action] of selectedActions) {
111
+ responses[actionKey] = (await action.run(
112
+ options,
113
+ )) as CommandResponse<TActions>[keyof TActions];
114
+ }
115
+ return responses as CommandResponse<TActions>;
116
+ }
117
+
118
+ protected getSelectedActions(
119
+ actions: TActions,
120
+ ): Array<[keyof TActions, TActions[keyof TActions]]> {
121
+ if (!this.actions.length) {
122
+ return [];
123
+ }
124
+
125
+ const actionEntries = Object.entries(actions) as Array<
126
+ [keyof TActions, TActions[keyof TActions]]
127
+ >;
128
+ return this.actions.map((ActionClass) => {
129
+ const selectedAction = actionEntries.find(([, action]) => action instanceof ActionClass);
130
+ if (!selectedAction) {
131
+ throw new CommandError(
132
+ `${this.name()} command missing action instance for ${ActionClass.name}`,
133
+ );
134
+ }
135
+
136
+ return selectedAction;
137
+ });
138
+ }
139
+
140
+ protected throwOnErrors(responses: CommandResponse<TActions>): void {
141
+ const failures = Object.entries(responses).filter(([, response]) => {
142
+ if (!response) {
143
+ return false;
144
+ }
145
+
146
+ return !response.succeeded && !response.skipped;
147
+ });
148
+
149
+ if (!failures.length) {
150
+ return;
151
+ }
152
+
153
+ throw new CommandError(
154
+ `${this.name()} command failed. ${failures
155
+ .map(
156
+ ([actionName, response]) => `${actionName}: ${response?.message ?? "Unknown error"}`,
157
+ )
158
+ .join("\n")}`,
159
+ );
160
+ }
161
+
162
+ private parseCommandName(stringLike: string | Constructable) {
163
+ const command =
164
+ typeof stringLike === "string"
165
+ ? stringLike
166
+ : (stringLike.name?.toLowerCase().replace("command", "") ?? "");
167
+
168
+ if (command === "") {
169
+ throw new CommandError(`Could not parse command name from ${stringLike.toString()}`);
170
+ }
171
+ return command;
172
+ }
173
+ }