@adaas/a-concept 0.3.8 → 0.3.9

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.
@@ -45,18 +45,6 @@ export class A_Scope<
45
45
  _EntityType extends A_TYPES__Entity_Constructor[] = A_TYPES__Entity_Constructor[],
46
46
  _FragmentType extends A_Fragment[] = A_Fragment[],
47
47
  > {
48
-
49
- /**
50
- * Auto-incrementing counter for generating unique scope IDs.
51
- */
52
- private static _nextUid: number = 0;
53
-
54
- /**
55
- * Unique numeric ID for this scope instance. Used as a cache key discriminator
56
- * to prevent collisions between scopes with the same name or version.
57
- */
58
- readonly uid: number = A_Scope._nextUid++;
59
-
60
48
  /**
61
49
  * Scope Name uses for identification and logging purposes
62
50
  */
@@ -91,6 +79,12 @@ export class A_Scope<
91
79
  */
92
80
  protected _resolveConstructorCache: Map<string | Function, Function | null> = new Map();
93
81
 
82
+ /**
83
+ * Cached fingerprint string. Invalidated on every bumpVersion() call.
84
+ */
85
+ private _cachedFingerprint: string | undefined;
86
+ private _cachedFingerprintVersion: number = -1;
87
+
94
88
  // ===========================================================================
95
89
  // --------------------ALLowed Constructors--------------------------------
96
90
  // ===========================================================================
@@ -172,6 +166,20 @@ export class A_Scope<
172
166
  * allowing external caches to detect staleness via numeric comparison.
173
167
  */
174
168
  get version(): number { return this._version }
169
+ /**
170
+ * Returns a content-addressable fingerprint of the scope.
171
+ * Two scopes with identical content (components, entities, fragments, errors, imports, parent)
172
+ * will produce the same fingerprint. Dynamically recomputed when scope content changes.
173
+ */
174
+ get fingerprint(): string {
175
+ const aggregateVersion = this.aggregateVersion(new Set());
176
+ if (this._cachedFingerprint !== undefined && this._cachedFingerprintVersion === aggregateVersion) {
177
+ return this._cachedFingerprint;
178
+ }
179
+ this._cachedFingerprint = this.computeFingerprint(new Set());
180
+ this._cachedFingerprintVersion = aggregateVersion;
181
+ return this._cachedFingerprint;
182
+ }
175
183
  // ===========================================================================
176
184
  // --------------------Readonly Registered Properties--------------------------
177
185
  // ===========================================================================
@@ -222,6 +230,60 @@ export class A_Scope<
222
230
  protected bumpVersion(): void {
223
231
  this._version++;
224
232
  this._resolveConstructorCache.clear();
233
+ this._cachedFingerprint = undefined;
234
+ }
235
+
236
+ /**
237
+ * Computes the aggregate version of this scope and all reachable scopes (parent + imports).
238
+ * Used to detect when any transitive dependency has changed, so the fingerprint cache can be invalidated.
239
+ */
240
+ private aggregateVersion(visited: Set<A_Scope>): number {
241
+ if (visited.has(this)) return 0;
242
+ visited.add(this);
243
+ let v = this._version;
244
+ if (this._parent) v += this._parent.aggregateVersion(visited);
245
+ for (const imp of this._imports) v += imp.aggregateVersion(visited);
246
+ return v;
247
+ }
248
+
249
+ /**
250
+ * Computes a deterministic content-addressable fingerprint string.
251
+ * Includes components, entities, fragments, errors, parent, and imports.
252
+ */
253
+ private computeFingerprint(visited: Set<A_Scope>): string {
254
+ if (visited.has(this)) return '~circular~';
255
+ visited.add(this);
256
+
257
+ const parts: string[] = [];
258
+
259
+ // Parent
260
+ parts.push('P:' + (this._parent ? this._parent.computeFingerprint(visited) : '-'));
261
+
262
+ // Allowed constructors (sorted by name for determinism)
263
+ const allowedComponentNames = Array.from(this._allowedComponents).map(c => A_CommonHelper.getComponentName(c.name)).sort();
264
+ parts.push('AC:' + allowedComponentNames.join(','));
265
+
266
+ const allowedEntityNames = Array.from(this._allowedEntities).map(e => A_CommonHelper.getComponentName(e.name)).sort();
267
+ parts.push('AE:' + allowedEntityNames.join(','));
268
+
269
+ const allowedFragmentNames = Array.from(this._allowedFragments).map(f => A_CommonHelper.getComponentName(f.name)).sort();
270
+ parts.push('AF:' + allowedFragmentNames.join(','));
271
+
272
+ const allowedErrorNames = Array.from(this._allowedErrors).map(e => A_CommonHelper.getComponentName(e.name)).sort();
273
+ parts.push('AR:' + allowedErrorNames.join(','));
274
+
275
+ // Imports (sorted by fingerprint for determinism)
276
+ const importFingerprints = Array.from(this._imports).map(s => s.computeFingerprint(visited)).sort();
277
+ parts.push('I:' + importFingerprints.join(','));
278
+
279
+ const raw = parts.join('|');
280
+
281
+ // djb2 hash
282
+ let hash = 5381;
283
+ for (let i = 0; i < raw.length; i++) {
284
+ hash = ((hash << 5) + hash + raw.charCodeAt(i)) | 0;
285
+ }
286
+ return (hash >>> 0).toString(16);
225
287
  }
226
288
 
227
289
  /**
@@ -420,9 +482,15 @@ export class A_Scope<
420
482
  this._entities.clear();
421
483
  this._imports.clear();
422
484
 
423
- if (this.issuer()) {
424
- A_Context.deallocate(this);
425
- }
485
+ /**
486
+ * Since the scope is being destroyed, we can assume that all components, fragments and entities registered in the scope are no longer needed and can be garbage collected.
487
+ * And scope lives in WeakMap in the A_Context, so it will be garbage collected as well, so we don't need to manually deallocate it from the A_Context.
488
+ *
489
+ Verdict: The comment is accurate. Keeping deallocate() commented out is safe. The WeakMap design handles cleanup correctly in connection to A_Feature's lifecycle (as long as features properly destroy their scopes).
490
+ */
491
+ // if (this.issuer()) {
492
+ // A_Context.deallocate(this);
493
+ // }
426
494
 
427
495
  this.bumpVersion();
428
496
  }
@@ -701,24 +769,21 @@ export class A_Scope<
701
769
  // 3) Check if it's a Component
702
770
  case A_TypeGuards.isComponentConstructor(ctor): {
703
771
  found = this.isAllowedComponent(ctor)
704
- || !![...this.allowedComponents]
705
- .find(c => A_CommonHelper.isInheritedFrom(c, ctor));
772
+ || !!A_Context.findDescendantIn(ctor, this.allowedComponents);
706
773
 
707
774
  break;
708
775
  }
709
776
  // 4) Check if it's an Entity
710
777
  case A_TypeGuards.isEntityConstructor(ctor): {
711
778
  found = this.isAllowedEntity(ctor)
712
- || !![...this.allowedEntities]
713
- .find(e => A_CommonHelper.isInheritedFrom(e, ctor));
779
+ || !!A_Context.findDescendantIn(ctor, this.allowedEntities);
714
780
 
715
781
  break;
716
782
  }
717
783
  // 5) Check if it's a Fragment
718
784
  case A_TypeGuards.isFragmentConstructor(ctor): {
719
785
  found = this.isAllowedFragment(ctor)
720
- || !![...this.allowedFragments]
721
- .find(f => A_CommonHelper.isInheritedFrom(f, ctor));
786
+ || !!A_Context.findDescendantIn(ctor, this.allowedFragments);
722
787
 
723
788
  break;
724
789
  }
@@ -726,8 +791,7 @@ export class A_Scope<
726
791
  // 6) Check if it's an Error
727
792
  case A_TypeGuards.isErrorConstructor(ctor): {
728
793
  found = this.isAllowedError(ctor)
729
- || !![...this.allowedErrors]
730
- .find(e => A_CommonHelper.isInheritedFrom(e, ctor));
794
+ || !!A_Context.findDescendantIn(ctor, this.allowedErrors);
731
795
 
732
796
  break;
733
797
  }
@@ -735,8 +799,7 @@ export class A_Scope<
735
799
  // 7) Check scope issuer
736
800
  case this.issuer()
737
801
  && (this.issuer()!.constructor === ctor
738
- || A_CommonHelper.isInheritedFrom(this.issuer()!.constructor, ctor
739
- )
802
+ || A_Context.isIndexedInheritedFrom(this.issuer()!.constructor, ctor as Function)
740
803
  ): {
741
804
  found = true;
742
805
  break;
@@ -922,20 +985,16 @@ export class A_Scope<
922
985
  // If it's a constructor, find any extension from the allowed constructors and return it
923
986
  switch (true) {
924
987
  case A_TypeGuards.isComponentConstructor(name):
925
- return Array.from(this.allowedComponents)
926
- .find((c) => A_CommonHelper.isInheritedFrom(c, name)) as A_TYPES__Component_Constructor<T> | undefined;
988
+ return A_Context.findDescendantIn(name, this.allowedComponents) as A_TYPES__Component_Constructor<T> | undefined;
927
989
 
928
990
  case A_TypeGuards.isEntityConstructor(name):
929
- return Array.from(this.allowedEntities)
930
- .find((e) => A_CommonHelper.isInheritedFrom(e, name)) as A_TYPES__Entity_Constructor<T> | undefined;
991
+ return A_Context.findDescendantIn(name, this.allowedEntities) as A_TYPES__Entity_Constructor<T> | undefined;
931
992
 
932
993
  case A_TypeGuards.isFragmentConstructor(name):
933
- return Array.from(this.allowedFragments)
934
- .find((f) => A_CommonHelper.isInheritedFrom(f, name)) as A_TYPES__Fragment_Constructor<T> | undefined;
994
+ return A_Context.findDescendantIn(name, this.allowedFragments) as A_TYPES__Fragment_Constructor<T> | undefined;
935
995
 
936
996
  case A_TypeGuards.isErrorConstructor(name):
937
- return Array.from(this.allowedErrors)
938
- .find((e) => A_CommonHelper.isInheritedFrom(e, name)) as A_TYPES__Error_Constructor<T> | undefined;
997
+ return A_Context.findDescendantIn(name, this.allowedErrors) as A_TYPES__Error_Constructor<T> | undefined;
939
998
  }
940
999
 
941
1000
  if (!A_TypeGuards.isString(name))
@@ -975,27 +1034,17 @@ export class A_Scope<
975
1034
 
976
1035
  if (component) return component as A_TYPES__Component_Constructor<T>;
977
1036
  else
978
- // 1.2) Check components prototypes
1037
+ // 1.2) Check component ancestor names using inheritance index
979
1038
  {
980
- const protoComponent = Array.from(this.allowedComponents).find(
981
-
982
- // it should go rthough prototyopes and check their names to be equal to the provided name
983
- c => {
984
- let current = c;
985
-
986
- while (current) {
987
- if (current.name === name
988
- || current.name === A_FormatterHelper.toPascalCase(name)
989
- ) {
990
- return true;
991
- }
992
- current = Object.getPrototypeOf(current);
993
- }
994
-
995
- return false;
996
-
1039
+ const pascalName = A_FormatterHelper.toPascalCase(name);
1040
+ const protoComponent = Array.from(this.allowedComponents).find(c => {
1041
+ const ancestors = A_Context.getAncestors(c);
1042
+ if (!ancestors) return false;
1043
+ for (const ancestor of ancestors) {
1044
+ if (ancestor.name === name || ancestor.name === pascalName) return true;
997
1045
  }
998
- );
1046
+ return false;
1047
+ });
999
1048
  if (protoComponent) return protoComponent as A_TYPES__Component_Constructor<T>;
1000
1049
  }
1001
1050
 
@@ -1008,11 +1057,17 @@ export class A_Scope<
1008
1057
  );
1009
1058
  if (entity) return entity as A_TYPES__Entity_Constructor<T>;
1010
1059
  else
1011
- // 2.2) Check entities prototypes
1060
+ // 2.2) Check entity ancestor names using inheritance index
1012
1061
  {
1013
- const protoEntity = Array.from(this.allowedEntities).find(
1014
- e => A_CommonHelper.isInheritedFrom(e, name as any)
1015
- );
1062
+ const pascalName = A_FormatterHelper.toPascalCase(name);
1063
+ const protoEntity = Array.from(this.allowedEntities).find(e => {
1064
+ const ancestors = A_Context.getAncestors(e);
1065
+ if (!ancestors) return false;
1066
+ for (const ancestor of ancestors) {
1067
+ if (ancestor.name === name || ancestor.name === pascalName) return true;
1068
+ }
1069
+ return false;
1070
+ });
1016
1071
  if (protoEntity) return protoEntity as A_TYPES__Entity_Constructor<T>;
1017
1072
  }
1018
1073
 
@@ -1022,11 +1077,17 @@ export class A_Scope<
1022
1077
  );
1023
1078
  if (fragment) return fragment as A_TYPES__Fragment_Constructor<T>;
1024
1079
  else
1025
- // 3.2) Check fragments prototypes
1080
+ // 3.2) Check fragment ancestor names using inheritance index
1026
1081
  {
1027
- const protoFragment = Array.from(this.allowedFragments).find(
1028
- f => A_CommonHelper.isInheritedFrom(f, name as any)
1029
- );
1082
+ const pascalName = A_FormatterHelper.toPascalCase(name);
1083
+ const protoFragment = Array.from(this.allowedFragments).find(f => {
1084
+ const ancestors = A_Context.getAncestors(f);
1085
+ if (!ancestors) return false;
1086
+ for (const ancestor of ancestors) {
1087
+ if (ancestor.name === name || ancestor.name === pascalName) return true;
1088
+ }
1089
+ return false;
1090
+ });
1030
1091
  if (protoFragment) return protoFragment as A_TYPES__Fragment_Constructor<T>;
1031
1092
  }
1032
1093
 
@@ -1183,7 +1244,7 @@ export class A_Scope<
1183
1244
  case A_TypeGuards.isComponentConstructor(param1): {
1184
1245
  // 1) Check components
1185
1246
  this.allowedComponents.forEach(ctor => {
1186
- if (A_CommonHelper.isInheritedFrom(ctor, param1)) {
1247
+ if (A_Context.isIndexedInheritedFrom(ctor, param1)) {
1187
1248
  const instance = this.resolveOnce<T>(ctor);
1188
1249
  if (instance) results.push(instance as T);
1189
1250
  }
@@ -1194,7 +1255,7 @@ export class A_Scope<
1194
1255
  case A_TypeGuards.isFragmentConstructor(param1): {
1195
1256
  // 2) Check fragments
1196
1257
  this.allowedFragments.forEach(ctor => {
1197
- if (A_CommonHelper.isInheritedFrom(ctor, param1)) {
1258
+ if (A_Context.isIndexedInheritedFrom(ctor, param1)) {
1198
1259
  const instance = this.resolveOnce<T>(ctor);
1199
1260
  if (instance) results.push(instance as T);
1200
1261
  }
@@ -1206,7 +1267,7 @@ export class A_Scope<
1206
1267
  // 3) Check entities
1207
1268
  this.entities.forEach(entity => {
1208
1269
 
1209
- if (A_CommonHelper.isInheritedFrom(entity.constructor, param1)) {
1270
+ if (A_Context.isIndexedInheritedFrom(entity.constructor, param1)) {
1210
1271
  results.push(entity as T);
1211
1272
  }
1212
1273
  });
@@ -1578,7 +1639,7 @@ export class A_Scope<
1578
1639
  if (issuer
1579
1640
  && (
1580
1641
  issuer.constructor === ctor
1581
- || A_CommonHelper.isInheritedFrom(issuer?.constructor, ctor)
1642
+ || A_Context.isIndexedInheritedFrom(issuer?.constructor, ctor)
1582
1643
  )) {
1583
1644
  return issuer!;
1584
1645
  }
@@ -1639,9 +1700,10 @@ export class A_Scope<
1639
1700
  case fragmentInstancePresented && this._fragments.has(fragment):
1640
1701
  return fragmentInstancePresented;
1641
1702
 
1642
- // 3) In case when there's a component that is inherited from the required component
1643
- case !fragmentInstancePresented && Array.from(this._allowedFragments).some(el => A_CommonHelper.isInheritedFrom(el, fragment)): {
1644
- const found = Array.from(this._allowedFragments).find(el => A_CommonHelper.isInheritedFrom(el, fragment))!;
1703
+ // 3) In case when there's a fragment that is inherited from the required fragment
1704
+ case !fragmentInstancePresented: {
1705
+ const found = A_Context.findDescendantIn(fragment, this._allowedFragments);
1706
+ if (!found) return undefined;
1645
1707
 
1646
1708
  return this.resolveFragment(found);
1647
1709
  }
@@ -1695,8 +1757,9 @@ export class A_Scope<
1695
1757
  }
1696
1758
 
1697
1759
  // 3) In case when there's a component that is inherited from the required component
1698
- case !this.allowedComponents.has(component) && Array.from(this.allowedComponents).some(el => A_CommonHelper.isInheritedFrom(el, component)): {
1699
- const found = Array.from(this.allowedComponents).find(el => A_CommonHelper.isInheritedFrom(el, component))!;
1760
+ case !this.allowedComponents.has(component): {
1761
+ const found = A_Context.findDescendantIn(component, this.allowedComponents);
1762
+ if (!found) return undefined;
1700
1763
 
1701
1764
  return this.resolveComponent(found);
1702
1765
  }
@@ -1787,6 +1850,7 @@ export class A_Scope<
1787
1850
  param1 as InstanceType<_ComponentType[number]>
1788
1851
  );
1789
1852
 
1853
+ A_Context.indexConstructor(param1.constructor);
1790
1854
  A_Context.register(this, param1);
1791
1855
  this.bumpVersion();
1792
1856
 
@@ -1799,6 +1863,7 @@ export class A_Scope<
1799
1863
  this.allowedEntities.add(param1.constructor as _EntityType[number]);
1800
1864
 
1801
1865
  this._entities.set(param1.aseid.toString(), param1 as InstanceType<_EntityType[number]>);
1866
+ A_Context.indexConstructor(param1.constructor);
1802
1867
  A_Context.register(this, param1);
1803
1868
  this.bumpVersion();
1804
1869
  break;
@@ -1814,6 +1879,7 @@ export class A_Scope<
1814
1879
  param1 as _FragmentType[number]
1815
1880
  );
1816
1881
 
1882
+ A_Context.indexConstructor(param1.constructor);
1817
1883
  A_Context.register(this, param1);
1818
1884
  this.bumpVersion();
1819
1885
 
@@ -1829,6 +1895,7 @@ export class A_Scope<
1829
1895
  param1 as InstanceType<_ErrorType[number]>
1830
1896
  );
1831
1897
 
1898
+ A_Context.indexConstructor(param1.constructor);
1832
1899
  A_Context.register(this, (param1 as any));
1833
1900
  this.bumpVersion();
1834
1901
  break;
@@ -1841,6 +1908,7 @@ export class A_Scope<
1841
1908
  case A_TypeGuards.isComponentConstructor(param1): {
1842
1909
  if (!this.allowedComponents.has(param1)) {
1843
1910
  this.allowedComponents.add(param1 as _ComponentType[number]);
1911
+ A_Context.indexConstructor(param1);
1844
1912
  this.bumpVersion();
1845
1913
  }
1846
1914
  break;
@@ -1849,6 +1917,7 @@ export class A_Scope<
1849
1917
  case A_TypeGuards.isFragmentConstructor(param1): {
1850
1918
  if (!this.allowedFragments.has(param1)) {
1851
1919
  this.allowedFragments.add(param1 as A_TYPES__Fragment_Constructor<_FragmentType[number]>);
1920
+ A_Context.indexConstructor(param1);
1852
1921
  this.bumpVersion();
1853
1922
  }
1854
1923
  break;
@@ -1857,6 +1926,7 @@ export class A_Scope<
1857
1926
  case A_TypeGuards.isEntityConstructor(param1): {
1858
1927
  if (!this.allowedEntities.has(param1)) {
1859
1928
  this.allowedEntities.add(param1 as _EntityType[number]);
1929
+ A_Context.indexConstructor(param1);
1860
1930
  this.bumpVersion();
1861
1931
  }
1862
1932
  break;
@@ -1865,6 +1935,7 @@ export class A_Scope<
1865
1935
  case A_TypeGuards.isErrorConstructor(param1): {
1866
1936
  if (!this.allowedErrors.has(param1)) {
1867
1937
  this.allowedErrors.add(param1 as _ErrorType[number]);
1938
+ A_Context.indexConstructor(param1);
1868
1939
  this.bumpVersion();
1869
1940
  }
1870
1941
  break;
@@ -2041,7 +2112,7 @@ export class A_Scope<
2041
2112
  this.allowedFragments.delete(param1 as A_TYPES__Fragment_Constructor<_FragmentType[number]>);
2042
2113
  // and then deregister all instances of this fragment
2043
2114
  Array.from(this._fragments.entries()).forEach(([ctor, instance]) => {
2044
- if (A_CommonHelper.isInheritedFrom(ctor, param1)) {
2115
+ if (A_Context.isIndexedInheritedFrom(ctor, param1)) {
2045
2116
  this._fragments.delete(ctor);
2046
2117
  A_Context.deregister(instance);
2047
2118
  }
@@ -2055,7 +2126,7 @@ export class A_Scope<
2055
2126
  this.allowedEntities.delete(param1 as _EntityType[number]);
2056
2127
  // and then deregister all instances of this entity
2057
2128
  Array.from(this._entities.entries()).forEach(([aseid, instance]) => {
2058
- if (A_CommonHelper.isInheritedFrom(instance.constructor, param1)) {
2129
+ if (A_Context.isIndexedInheritedFrom(instance.constructor, param1)) {
2059
2130
  this._entities.delete(aseid);
2060
2131
  A_Context.deregister(instance);
2061
2132
  }
@@ -2069,7 +2140,7 @@ export class A_Scope<
2069
2140
  this.allowedErrors.delete(param1 as _ErrorType[number]);
2070
2141
  // and then deregister all instances of this error
2071
2142
  Array.from(this._errors.entries()).forEach(([code, instance]) => {
2072
- if (A_CommonHelper.isInheritedFrom(instance.constructor, param1)) {
2143
+ if (A_Context.isIndexedInheritedFrom(instance.constructor, param1)) {
2073
2144
  this._errors.delete(code);
2074
2145
  A_Context.deregister(instance);
2075
2146
  }
@@ -9,6 +9,8 @@ import { A_Context } from "@adaas/a-concept/a-context";
9
9
  import { A_Feature } from "@adaas/a-concept/a-feature";
10
10
  import { A_Meta } from "@adaas/a-concept/a-meta";
11
11
  import { A_Inject } from "@adaas/a-concept/a-inject";
12
+ import { A_Entity } from "@adaas/a-concept/a-entity";
13
+ import { A_Scope } from "@adaas/a-concept/a-scope";
12
14
 
13
15
  jest.retryTimes(0);
14
16
 
@@ -173,7 +175,7 @@ describe('A-Meta tests', () => {
173
175
  expect(meta.get(A_TYPES__ComponentMetaKey.EXTENSIONS)?.size()).toBe(1);
174
176
  expect(meta.get(A_TYPES__ComponentMetaKey.EXTENSIONS)?.has('.*\\.testFeature$')).toBe(true);
175
177
  })
176
- it('Should propagate a custom me', async () => {
178
+ it('Should propagate a custom meta', async () => {
177
179
 
178
180
  class InjectableComponent extends A_Component { }
179
181
 
@@ -205,7 +207,7 @@ describe('A-Meta tests', () => {
205
207
  }
206
208
  }
207
209
 
208
- class SubCustomComponent extends CustomComponent {}
210
+ class SubCustomComponent extends CustomComponent { }
209
211
 
210
212
 
211
213
  const meta = A_Context.meta<CustomComponentMeta>(SubCustomComponent);
@@ -216,4 +218,46 @@ describe('A-Meta tests', () => {
216
218
  expect(meta.get(A_TYPES__ComponentMetaKey.EXTENSIONS)?.size()).toBe(1);
217
219
  expect(meta.get(A_TYPES__ComponentMetaKey.EXTENSIONS)?.has('.*\\.testFeature$')).toBe(true);
218
220
  })
221
+ it('Should inherit properly extends methods when they are bind to entity', async () => {
222
+
223
+
224
+ const results: string[] = [];
225
+
226
+ class CustomComponentMeta extends A_ComponentMeta<{ customField: string } & A_TYPES__ComponentMeta> {
227
+
228
+ get customField(): string | undefined {
229
+ return this.get('customField');
230
+ }
231
+ }
232
+
233
+
234
+ class MyEntity extends A_Entity {
235
+ feature() {
236
+ this.call('testFeature');
237
+ }
238
+ }
239
+
240
+
241
+ @A_Meta.Define(CustomComponentMeta)
242
+ class CustomComponent extends A_Component {
243
+
244
+ @A_Feature.Extend({
245
+ name: 'testFeature',
246
+ scope: [MyEntity]
247
+ })
248
+ async feature() {
249
+ results.push('feature called');
250
+ }
251
+ }
252
+
253
+ const scope = new A_Scope({ name: 'testScope', components: [CustomComponent] });
254
+
255
+ const entity = new MyEntity();
256
+
257
+ scope.register(entity);
258
+
259
+ entity.feature();
260
+
261
+ expect(results).toContain('feature called');
262
+ })
219
263
  })